Compare commits
279 Commits
ed4022dfd1
...
1.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac0b4a0750 | ||
|
|
29ca2ea496 | ||
|
|
2a3905bdfb | ||
|
|
d3755ab57d | ||
|
|
9824e107fe | ||
|
|
e437722937 | ||
|
|
fd8c4ae33c | ||
|
|
967144bd55 | ||
|
|
cd8eb05020 | ||
|
|
590060e504 | ||
|
|
26c7d4d2ee | ||
|
|
caa987fa42 | ||
|
|
ccb090e4b2 | ||
|
|
8d070ed10d | ||
|
|
343dbb26a2 | ||
|
|
fef4f6de36 | ||
|
|
1a61972750 | ||
|
|
45ae5d7f98 | ||
|
|
fecb123d0e | ||
|
|
5c62ed081a | ||
|
|
8f0f31041d | ||
|
|
0e48ab3b4c | ||
|
|
d4c33bcf2d | ||
|
|
2f105f5145 | ||
|
|
b7a193b062 | ||
|
|
65364d3095 | ||
|
|
c356e2e30d | ||
|
|
e45810b54c | ||
|
|
55db30febc | ||
|
|
163faec221 | ||
|
|
5398fed928 | ||
|
|
f959171db5 | ||
|
|
01b6bcd02c | ||
|
|
0ff4715b35 | ||
|
|
745a73a5b0 | ||
|
|
a69114edce | ||
|
|
489a4766fa | ||
|
|
adcee62386 | ||
|
|
b867d819b7 | ||
|
|
31d8b20a76 | ||
|
|
42ae0e7204 | ||
|
|
43869420c0 | ||
|
|
6c9d296fbd | ||
|
|
008428f10a | ||
|
|
ae7d338474 | ||
|
|
12ff25cc4b | ||
|
|
f31767e7e7 | ||
|
|
7c4f2841e1 | ||
|
|
c451ddd042 | ||
|
|
da6311055f | ||
|
|
945efb2059 | ||
|
|
ab2881f8e9 | ||
|
|
f23811fff4 | ||
|
|
19ab77501b | ||
|
|
37eeed583a | ||
|
|
caf08efb90 | ||
|
|
7ff998e4df | ||
|
|
0c1e8fe899 | ||
|
|
4158e8c6e3 | ||
|
|
653319e408 | ||
|
|
0a4f14fba7 | ||
|
|
fb4571fc51 | ||
|
|
c11fd40681 | ||
|
|
cbfde8b761 | ||
|
|
c774821cc4 | ||
|
|
2ed15a32bc | ||
|
|
c252853603 | ||
|
|
cfdddec073 | ||
|
|
97a63ab791 | ||
|
|
a0e00341e1 | ||
|
|
26d42491ce | ||
|
|
d678ba2f12 | ||
|
|
55b400eb05 | ||
|
|
0a6313b1f3 | ||
|
|
b4312376bb | ||
|
|
5ffb143a1a | ||
|
|
ee24473077 | ||
|
|
2b98a63f92 | ||
|
|
2e594d87e8 | ||
|
|
b343c2869d | ||
|
|
094b93f4fa | ||
|
|
cdc8a623c0 | ||
|
|
2b43c00a3a | ||
|
|
14928798d4 | ||
|
|
e5fb44fc6b | ||
|
|
9f9e4cd1d8 | ||
|
|
45fe7336fe | ||
|
|
b909e7d917 | ||
|
|
ce62748ade | ||
|
|
d4ecc36f6b | ||
|
|
fee364610e | ||
|
|
492af8525b | ||
|
|
fb58b5ae06 | ||
|
|
577034b0bd | ||
|
|
6fc452edb4 | ||
|
|
086ca49599 | ||
|
|
be1de35da8 | ||
|
|
3c1f57667f | ||
|
|
5997cd20b4 | ||
|
|
6220c9400e | ||
|
|
48a16b927f | ||
|
|
87545d77b4 | ||
|
|
4e0fe29c20 | ||
|
|
f8e3bc843d | ||
|
|
b3f1edaabd | ||
|
|
ef784dd19c | ||
|
|
69bbcee885 | ||
|
|
60b4a99506 | ||
|
|
c1030400f5 | ||
|
|
3f508da501 | ||
|
|
0437cb61a2 | ||
|
|
1372582c54 | ||
|
|
070c3d507f | ||
|
|
3e9c545952 | ||
|
|
e3845e1849 | ||
|
|
91703d6a91 | ||
|
|
cff3101b03 | ||
|
|
c9a48ec64c | ||
|
|
e6419a9759 | ||
|
|
9d1d2fc7a8 | ||
|
|
cdca4a82ab | ||
|
|
c2e4e12512 | ||
|
|
96c1c6717d | ||
|
|
abf776175c | ||
|
|
d6de184bfd | ||
|
|
12249c4e8a | ||
|
|
f1fbc3f0c7 | ||
|
|
bb8cf54bf9 | ||
|
|
dccf8bd786 | ||
|
|
793bb79d76 | ||
|
|
f5cdf27ce6 | ||
|
|
627c56cd25 | ||
|
|
783871d131 | ||
|
|
138c05f995 | ||
|
|
5009a479af | ||
|
|
2a95a10113 | ||
|
|
9d6065c80a | ||
|
|
cd8b2bf007 | ||
|
|
1faf74fbc4 | ||
|
|
09163afbcd | ||
|
|
405d141055 | ||
|
|
599e09fa9f | ||
|
|
4954de5f1a | ||
|
|
ac854ae559 | ||
|
|
9944f05139 | ||
|
|
f6558b656a | ||
|
|
bfbf57ba38 | ||
|
|
d0af35377c | ||
|
|
59c263a246 | ||
|
|
42700bd7d8 | ||
|
|
5a9b364b4b | ||
|
|
3619550c74 | ||
|
|
31cc4012ac | ||
|
|
2e329a8584 | ||
|
|
d92dd59f86 | ||
|
|
5bb7ca377d | ||
|
|
9ee9a3574c | ||
|
|
dc50a44682 | ||
|
|
4be5c14999 | ||
|
|
b92ffebdb4 | ||
|
|
7b6e48b09e | ||
|
|
368851b4bb | ||
|
|
fa2da5712d | ||
|
|
7df97d70bb | ||
|
|
5c5f595829 | ||
|
|
5b659e3d8a | ||
|
|
1a526777cc | ||
|
|
b0d99d4c29 | ||
|
|
c7554475a9 | ||
|
|
cab177e8a7 | ||
|
|
f7244a69ac | ||
|
|
c6ed79ed1d | ||
|
|
00e5358bbb | ||
|
|
598c35eb54 | ||
|
|
00c74ea6c6 | ||
|
|
0c415c9106 | ||
|
|
b0495d033d | ||
|
|
d72cc6e0ca | ||
|
|
b59cb9f57f | ||
|
|
af743be5b0 | ||
|
|
024d039701 | ||
|
|
e9a91656bc | ||
|
|
3505606b44 | ||
|
|
815abae0a0 | ||
|
|
004271241e | ||
|
|
8295967ff2 | ||
|
|
053dca365c | ||
|
|
62110b1a5b | ||
|
|
aae9b2866c | ||
|
|
0d8bddbc20 | ||
|
|
2a7d61bab7 | ||
|
|
de71035fa0 | ||
|
|
5368fb9311 | ||
|
|
3d583a9b6f | ||
|
|
94de0a42f1 | ||
|
|
5ba2901273 | ||
|
|
f5888b8382 | ||
|
|
6ece182629 | ||
|
|
777257b71d | ||
|
|
df0156d603 | ||
|
|
68cdad06ab | ||
|
|
ee7c6520b9 | ||
|
|
4ed1609e41 | ||
|
|
b64e61d45d | ||
|
|
b4c2c865d8 | ||
|
|
e32ffd8fa0 | ||
|
|
8be6a932e5 | ||
|
|
6ed59fd272 | ||
|
|
4df1cb207f | ||
|
|
d03e3bc7e5 | ||
|
|
df02acb97f | ||
|
|
6ef04700ab | ||
|
|
766694f330 | ||
|
|
93380b2f36 | ||
|
|
8c969d694f | ||
|
|
f2f3d42e8c | ||
|
|
8d8973acfc | ||
|
|
281bff2379 | ||
|
|
e30f54050f | ||
|
|
0ae808c1a0 | ||
|
|
2598573844 | ||
|
|
27782376a3 | ||
|
|
876f7e1db0 | ||
|
|
db654c5f82 | ||
|
|
751f3d5c3e | ||
|
|
2a8d307bc8 | ||
|
|
ec6963cff3 | ||
|
|
196a07977a | ||
|
|
f20366f0c7 | ||
|
|
f54dc18e85 | ||
|
|
251049c7fe | ||
|
|
5d347967b1 | ||
|
|
d5c506c4e0 | ||
|
|
6718f4133e | ||
|
|
9ff6678730 | ||
|
|
3957450fdb | ||
|
|
25e50a6e7a | ||
|
|
22f935f678 | ||
|
|
58b843630b | ||
|
|
dec3f3272a | ||
|
|
f3252c2548 | ||
|
|
ac28afb17a | ||
|
|
b1a42f44fa | ||
|
|
78eea2fa0f | ||
|
|
4290e6db52 | ||
|
|
0ffc1af218 | ||
|
|
10cefdba19 | ||
|
|
c888946588 | ||
|
|
9026c63979 | ||
|
|
cfc2c5bbaa | ||
|
|
a41da5c4c2 | ||
|
|
b4242e4295 | ||
|
|
1f0650a71c | ||
|
|
87fe8af6f5 | ||
|
|
592d3a5ae4 | ||
|
|
dc847a5a8c | ||
|
|
250e6c3f23 | ||
|
|
1ffe9dcea0 | ||
|
|
b40a4fc033 | ||
|
|
0900e1b485 | ||
|
|
432633af52 | ||
|
|
152ee39535 | ||
|
|
9d824f5c33 | ||
|
|
accb55e538 | ||
|
|
bfb3e0cbdf | ||
|
|
b9460dbceb | ||
|
|
1216e5f10d | ||
|
|
8b4b3c84e0 | ||
|
|
f7abca90cb | ||
|
|
83b31cb645 | ||
|
|
bb967834f0 | ||
|
|
e9f4b80b7b | ||
|
|
abd5295395 | ||
|
|
d4d36c39ca | ||
|
|
95701bf56e | ||
|
|
7b93413050 | ||
|
|
c2c69935b1 | ||
|
|
07485f39ba | ||
|
|
0c99e48950 |
35
.github/ISSUE_TEMPLATE/-bug--issues-template-.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: "[BUG] Issues Template "
|
||||||
|
about: Report something that doesn't work fine
|
||||||
|
title: "[BUG] Name of the bug / issues"
|
||||||
|
labels: bug
|
||||||
|
assignees: MasterAcnolo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Check Before Reporting
|
||||||
|
Make sure this bug doesn't already exist in the issues.
|
||||||
|
|
||||||
|
## Describe the Bug
|
||||||
|
A clear and concise description of the problem.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to…
|
||||||
|
2. Click on…
|
||||||
|
3. See the error…
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
What you expected the program to do.
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
Add logs if possible. Available at `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`.
|
||||||
|
|
||||||
|
## App Version
|
||||||
|
State the version of the app you are using.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- Version: [e.g. 1.2.4]
|
||||||
|
- Browser / Other: [e.g. Chrome, Node.js 20]
|
||||||
|
- Any others information that can be usefull for me
|
||||||
|
|
||||||
|
## Screenshots (if applicable)
|
||||||
22
.github/ISSUE_TEMPLATE/-feat--feature-template.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: "[FEAT] Feature Template"
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[FEAT] Name of the features"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: MasterAcnolo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Check Before Requesting
|
||||||
|
Make sure this feature doesn't already exist in the issues.
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
Describe the feature you want to add or improve.
|
||||||
|
|
||||||
|
## Why is this Feature Useful?
|
||||||
|
Explain the benefit for users or the project.
|
||||||
|
|
||||||
|
## Examples / Context
|
||||||
|
Provide an example or use case if possible.
|
||||||
|
|
||||||
|
## Screenshots / Mockups (if applicable)
|
||||||
29
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
Describe the feature or improvement you are adding.
|
||||||
|
Include why it's useful and what problem it solves.
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
|
||||||
|
- [ ] New feature
|
||||||
|
- [ ] Improvement / Refactor
|
||||||
|
- [ ] Documentation update
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] Code tested locally
|
||||||
|
- [ ] No linting errors
|
||||||
|
- [ ] Added or updated tests if relevant
|
||||||
|
- [ ] Updated documentation if relevant
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
Explain how reviewers can test the new feature or improvement.
|
||||||
|
|
||||||
|
## App Version
|
||||||
|
|
||||||
|
State the version of Freedom Loader you are targeting.
|
||||||
|
|
||||||
|
## Notes / Additional Info
|
||||||
|
|
||||||
|
Any context, screenshots, or logs that can help reviewing the PR.
|
||||||
25
.github/PULL_REQUEST_TEMPLATE/version.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Freedom Loader - x.y.z
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- List all changes made in this release (features, bug fixes, improvements)
|
||||||
|
- Use bullet points, keep it concise
|
||||||
|
- Example:
|
||||||
|
- `YT-DLP` Will now update when the application is launched
|
||||||
|
- Improved UI / UX
|
||||||
|
|
||||||
|
## Bug Reports / Issues
|
||||||
|
|
||||||
|
- Please report any bugs in the GitHub Issues section
|
||||||
|
|
||||||
|
## Next Release Roadmap (non-exhaustive)
|
||||||
|
|
||||||
|
- List upcoming features or improvements
|
||||||
|
- Example:
|
||||||
|
- More format options
|
||||||
|
- Subtitle support
|
||||||
|
- Language selection
|
||||||
|
- Parallel downloads
|
||||||
|
- Skip sponsored parts automatically
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/[previous-version]...[new-version]
|
||||||
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/node_modules
|
||||||
|
/downloads
|
||||||
|
/dist
|
||||||
|
/out
|
||||||
|
|
||||||
|
/ressources/ffmpeg.exe
|
||||||
|
/ressources/ffprobe.exe
|
||||||
|
/ressources/deno.exe
|
||||||
|
|
||||||
|
logs/*.json
|
||||||
|
logs/*.log
|
||||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
masteracnolo25@gmail.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
66
CONTRIBUTING.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Contributing to Freedom Loader
|
||||||
|
|
||||||
|
Welcome! Thanks for being interested in Freedom Loader. Your contributions-bug reports, feature ideas, or code improvements-are always appreciated. Every contribution makes Freedom Loader better 💪
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Check Before Contributing
|
||||||
|
- Make sure your issue, bug, or feature request doesn't already exist.
|
||||||
|
- Search issues and pull requests before creating a new one.
|
||||||
|
- Follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Report an Issue
|
||||||
|
Found a bug, weird behavior, or have an idea? Open an [**issue**](https://github.com/MasterAcnolo/Freedom-Loader/issues) using the **Bug Report template**.
|
||||||
|
Make sure to include:
|
||||||
|
- Steps to reproduce the issue
|
||||||
|
- App version and environment (OS, Node, Browser, etc.)
|
||||||
|
- Logs if possible: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`
|
||||||
|
- Screenshots if relevant
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Submit a Pull Request
|
||||||
|
To fix a bug, add a feature, or improve docs:
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a branch: `git checkout -b feature/my-awesome-feature`
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests or verification steps
|
||||||
|
5. Test everything locally
|
||||||
|
6. Open a PR to the `main` branch
|
||||||
|
|
||||||
|
**Tips for PRs:**
|
||||||
|
- Keep titles and descriptions clear and concise
|
||||||
|
- Document what you change and why
|
||||||
|
- Follow existing code style (camelCase for variables/functions)
|
||||||
|
- Small commits focused on a single purpose are easier to review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Improve the Documentation
|
||||||
|
Docs are important: README, wiki, guides… If something is unclear, improve it!
|
||||||
|
Small contributions like fixing typos or adding examples are always welcome.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Testing & Verification
|
||||||
|
- Test new features thoroughly
|
||||||
|
- Ensure bug fixes actually fix the reported issue
|
||||||
|
- Include any relevant logs or screenshots
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Review & Merge
|
||||||
|
- After submitting a PR, it will be reviewed by maintainers
|
||||||
|
- Be open to feedback and changes
|
||||||
|
- We try to respond quickly, but sometimes have other priorities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Code of Conduct
|
||||||
|
Please contribute respectfully and constructively. Everyone is welcome as long as discussions remain polite and productive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thanks to Zakaria for the website icon. Check him out here: [IG Link](https://www.instagram.com/designmark_studio/) 🔥
|
||||||
20
LICENSE
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2025 MasterAcnolo
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
150
README.md
@@ -1,2 +1,150 @@
|
|||||||
# MP3 Downloader
|
<div align="center">
|
||||||
|
<a href="https://masteracnolo.github.io/FreedomLoader/"><img
|
||||||
|
src="./build/banner.png"
|
||||||
|
alt="Banner"
|
||||||
|
style="width: 50%;;"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/MasterAcnolo/Freedom-Loader/releases"><img src="https://img.shields.io/badge/Release-1.4.0-blue?style=for-the-badge"></a>
|
||||||
|
<a href="https://masteracnolo.github.io/FreedomLoader/index.html"><img src="https://img.shields.io/badge/Freedom%20Loader-Open%20Page-111111?style=for-the-badge&logo=terminal&logoColor=white"></a>
|
||||||
|
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Freedom Loader is a **Windows desktop** application built with **Electron**.
|
||||||
|
It provides a simple and reliable way to download video or audio content with metadata and advanced options, without ads or questionable third-party services.
|
||||||
|
The goal is to offer a clean, user-friendly, and transparent download experience. It's important for me to make media content downloading accessible to people who love music or videos and don’t always have an internet connection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Video download with metadata (MP4)
|
||||||
|
- Audio-only download with thumbnail and tags (MP3)
|
||||||
|
- Full playlist support and better display in UI
|
||||||
|
- Automatic metadata retrieval (title, duration, author, etc.)
|
||||||
|
- Detailed logs (console + rotating files for 7 days)
|
||||||
|
- Progress Bar for download and network speed indicator
|
||||||
|
- Better UX when fetching info
|
||||||
|
- Custom output path with persistence
|
||||||
|
- Automatic dependency updates (YT-DLP)
|
||||||
|
- Automatic Application Update
|
||||||
|
- Multiple UI themes
|
||||||
|
- Simple, responsive interface
|
||||||
|
- Notifications on download completion (click to open folder)
|
||||||
|
- Discord Rich Presence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Download the latest installer from the **Releases** page and run it (Defender could stop you but click on "Run Anyway" and it's good).
|
||||||
|
Actually, **Firefox Browser** is the only browser supported for get cookies. You should install it. Follow the tutorial in the wiki page if you need some help, Firefox Section. [Link](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Where are my downloaded files stored?
|
||||||
|
|
||||||
|
In your **Downloads** folder, inside **Freedom Loader**.
|
||||||
|
Example path:
|
||||||
|
`C:\Users\[YOUR NAME]\Downloads\Freedom Loader`
|
||||||
|
|
||||||
|
### I get an error after launching the app
|
||||||
|
|
||||||
|
Try restarting the application, checking the logs, or opening the developer console:
|
||||||
|
`CTRL + SHIFT + I` -> “Toggle Developer Tools”.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="build/example-developertools.png" width="20%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [x] ~~Website for Freedom Loader~~
|
||||||
|
- [ ] More format options
|
||||||
|
- [ ] Linux version
|
||||||
|
- [x] ~~Auto-updating YT-DLP~~
|
||||||
|
- [x] ~~Automatic project updates~~
|
||||||
|
- [x] ~~Better download status~~
|
||||||
|
- [ ] Better website support (currently only YouTube is fully supported, other site could work but i can't prove it)
|
||||||
|
- [ ] Subtitle support
|
||||||
|
- [x] ~~UI/UX improvements~~
|
||||||
|
- [ ] Language selection
|
||||||
|
- [ ] Download specific parts of a video
|
||||||
|
- [ ] Custom file naming
|
||||||
|
- [ ] Parallel downloads
|
||||||
|
- [ ] Automatic sponsor skipping
|
||||||
|
- [x] ~~Extract/split video using native chapters~~
|
||||||
|
- [x] ~~Custom output path selection~~
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="build/apercu1.2.4.png" width="40%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
This project uses:
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/yt-dlp/yt-dlp"><img src="https://img.shields.io/badge/yt--dlp-Visit%20Repo-cf0000?style=for-the-badge&logo=github&logoColor=white"></a>
|
||||||
|
<a href="https://github.com/FFmpeg/FFmpeg"><img src="https://img.shields.io/badge/FFmpeg-Visit%20Repo-666666?style=for-the-badge&logo=ffmpeg&logoColor=white"></a>
|
||||||
|
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-Visit%20Repo-339933?style=for-the-badge&logo=node.js&logoColor=white"></a>
|
||||||
|
<a href="https://github.com/electron/electron"><img src="https://img.shields.io/badge/Electron-Visit%20Repo-47848F?style=for-the-badge&logo=electron&logoColor=white"></a>
|
||||||
|
<a href="https://github.com/denoland/deno/"><img src="https://img.shields.io/badge/Deno-Visit%20Repo-14e0cc?style=for-the-badge&logo=deno&logoColor=white"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome.
|
||||||
|
|
||||||
|
### Bug Reports
|
||||||
|
|
||||||
|
Use the GitHub Issues system and include reproduction steps and logs when possible.
|
||||||
|
Logs can be found here:
|
||||||
|
`C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`
|
||||||
|
|
||||||
|
### Feature Requests
|
||||||
|
|
||||||
|
Open a feature request issue with a clear description and use cases.
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
|
||||||
|
Fork the repository, make your changes, and submit a PR once everything is tested and clean.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support the Project
|
||||||
|
|
||||||
|
If you'd like to support development:
|
||||||
|
|
||||||
|
<a href="https://paypal.me/axelnicolas25">
|
||||||
|
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Freedom Loader is released under the **GNU General Public License v3.0 (GPLv3)**.
|
||||||
|
You are free to use, modify, and redistribute the software under the terms of this license.
|
||||||
|
|
||||||
|
See the full license in the [LICENSE](./LICENSE) file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
AI assistance was used occasionally to speed up development, but all code and decisions were reviewed and validated manually.
|
||||||
|
|||||||
40
SECURITY.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We actively maintain security updates for the following versions (Since the 1.3.0 the application auto-update, patch will be apply easily):
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| -------- | ----------------- |
|
||||||
|
| 1.3.x | :white_check_mark: |
|
||||||
|
| 1.2.x | :white_check_mark: |
|
||||||
|
| 1.1.x | :white_check_mark:|
|
||||||
|
| <1.1 | :white_check_mark: |
|
||||||
|
|
||||||
|
> Only versions marked with ✅ receive security updates. Older versions may be vulnerable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability in Freedom Loader, **do not create a public issue**. Report it privately:
|
||||||
|
|
||||||
|
- Email: **masteracnolo25@gmail.com**
|
||||||
|
- Include:
|
||||||
|
- Description of the vulnerability
|
||||||
|
- Steps to reproduce it
|
||||||
|
- App version and environment (OS, Node, Browser if relevant)
|
||||||
|
- Logs or screenshots if possible (`C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`)
|
||||||
|
|
||||||
|
We will respond as quickly as possible, typically within **48 hours**, and keep you updated on the fix.
|
||||||
|
|
||||||
|
We may acknowledge responsible disclosures in release notes unless you request anonymity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Happens Next
|
||||||
|
|
||||||
|
- We will confirm receipt of your report
|
||||||
|
- Investigate and reproduce the issue
|
||||||
|
- Deploy a fix and release an update
|
||||||
|
- Notify you when the fix is live
|
||||||
BIN
build/apercu1.2.4.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
build/app-icon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
build/banner.bmp
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
build/banner.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
build/confirm-icon.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
build/error.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
build/example-developertools.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
build/uninstaller-icon.ico
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
26
config.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const packageJson = require("./package.json");
|
||||||
|
const { app } = require("electron");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const localMode = !app.isPackaged;
|
||||||
|
|
||||||
|
const featuresPath = localMode ? path.join(__dirname, "./config/config.json") : path.join(path.join(process.resourcesPath,"config/" ,"config.json"));
|
||||||
|
|
||||||
|
let features = {};
|
||||||
|
|
||||||
|
function loadFeatures() {
|
||||||
|
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||||
|
features = JSON.parse(raw);
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configFeatures = loadFeatures();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
version: packageJson.version,
|
||||||
|
applicationPort: "8787",
|
||||||
|
localMode,
|
||||||
|
DiscordRPCID: "1410934537051181146",
|
||||||
|
configFeatures
|
||||||
|
}
|
||||||
15
config/config.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"autoUpdate": true,
|
||||||
|
"discordRPC": true,
|
||||||
|
"customTopBar": true,
|
||||||
|
"autoCheckInfo": true,
|
||||||
|
"addThumbnail": true,
|
||||||
|
"addMetadata": true,
|
||||||
|
"verboseLogs": false,
|
||||||
|
"autoDownloadPlaylist": true,
|
||||||
|
"customCodec": "h264",
|
||||||
|
"logSystem": true,
|
||||||
|
"outputTitleCheck": true,
|
||||||
|
"downloadSystem": true,
|
||||||
|
"notifySystem": true
|
||||||
|
}
|
||||||
267
main.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
const config = require("./config.js");
|
||||||
|
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||||
|
const { AutoUpdater } = require("./server/update.js");
|
||||||
|
const { configFeatures } = require("./config.js");
|
||||||
|
const { startRPC } = require("./server/discordRPC");
|
||||||
|
|
||||||
|
let mainWindow;
|
||||||
|
const logsFolderPath = logDir;
|
||||||
|
|
||||||
|
const basePath = config.localMode
|
||||||
|
? path.join(__dirname )
|
||||||
|
: path.join(path.dirname(process.execPath), "resources");
|
||||||
|
|
||||||
|
const configFolderPath = path.join(basePath, "config" ,"config.json");
|
||||||
|
|
||||||
|
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||||
|
|
||||||
|
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
||||||
|
app.disableHardwareAcceleration();
|
||||||
|
|
||||||
|
ipcMain.handle("version", () => config.version);
|
||||||
|
|
||||||
|
// Gestion single instance
|
||||||
|
const gotLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
||||||
|
function checkNativeDependencies() {
|
||||||
|
const deps = [
|
||||||
|
{ name: "yt-dlp.exe", path: path.join(process.resourcesPath, "binaries","yt-dlp.exe") },
|
||||||
|
{ name: "ffmpeg.exe", path: path.join(process.resourcesPath, "binaries", "ffmpeg.exe") },
|
||||||
|
{ name: "ffprobe.exe", path: path.join(process.resourcesPath, "binaries", "ffprobe.exe") },
|
||||||
|
{ name: "deno.exe", path: path.join(process.resourcesPath, "binaries", "deno.exe") },
|
||||||
|
];
|
||||||
|
const missing = deps.filter(dep => !fs.existsSync(dep.path));
|
||||||
|
let errorMsg = "";
|
||||||
|
if (missing.length > 0) {
|
||||||
|
const missingList = missing.map(dep => dep.name).join(", ");
|
||||||
|
logger.error(`Missing dependencies: ${missingList}`);
|
||||||
|
errorMsg += `The following files are missing in the 'ressources' folder:\n${missingList}`;
|
||||||
|
}
|
||||||
|
if (errorMsg) {
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
dialog.showErrorBox(
|
||||||
|
"Missing dependencies",
|
||||||
|
`${errorMsg}\n\nThe application will now exit. Try to reinstall`
|
||||||
|
);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!config.localMode){
|
||||||
|
if (!gotLock) {
|
||||||
|
// Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
|
||||||
|
// Ici la nouvelle instance continue normalement
|
||||||
|
} else {
|
||||||
|
if (!checkNativeDependencies()) {
|
||||||
|
// Arrêt déjà géré dans la fonction
|
||||||
|
} else {
|
||||||
|
app.on("second-instance", () => {
|
||||||
|
// La vieille instance se ferme
|
||||||
|
if (mainWindow) {
|
||||||
|
logger.info("New Instance Detected, closing the older...");
|
||||||
|
mainWindow.destroy();
|
||||||
|
mainWindow = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Création fenêtre principale
|
||||||
|
async function createMainWindow() {
|
||||||
|
if (mainWindow) {
|
||||||
|
logger.warn("Window already exists, no new creation!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
title: `Freedom Loader ${config.version}`,
|
||||||
|
width: 750,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 750,
|
||||||
|
minHeight: 800,
|
||||||
|
frame: !configFeatures.customTopBar,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, "preload.js"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||||
|
logger.info("Window Loaded");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Window Loading Error:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.on("closed", () => {
|
||||||
|
logger.info("Main Window Closed");
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDownloadPath(userPath) {
|
||||||
|
const userHome = os.homedir(); // C:\Users\<User>
|
||||||
|
|
||||||
|
if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader");
|
||||||
|
|
||||||
|
// Résolution canonique et suivi des symlinks
|
||||||
|
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||||
|
const normalizedHome = path.resolve(userHome) + path.sep;
|
||||||
|
|
||||||
|
if (!resolved.startsWith(normalizedHome)) {
|
||||||
|
throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !");
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// IPC
|
||||||
|
ipcMain.handle("select-download-folder", async () => {
|
||||||
|
try {
|
||||||
|
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
const validatedPath = validateDownloadPath(result.filePaths[0]);
|
||||||
|
logger.info(`Folder Checked and Valid : ${validatedPath}`);
|
||||||
|
return validatedPath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`An Error Occured when validating folder : ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("validate-download-path", (event, userPath) => {
|
||||||
|
return validateDownloadPath(userPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
|
||||||
|
|
||||||
|
ipcMain.on("set-progress", (event, percent) => {
|
||||||
|
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// Topbar window controls
|
||||||
|
ipcMain.on("window-minimize", () => {
|
||||||
|
if (mainWindow) mainWindow.minimize();
|
||||||
|
});
|
||||||
|
ipcMain.on("window-maximize", () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMaximized()) {
|
||||||
|
mainWindow.unmaximize();
|
||||||
|
} else {
|
||||||
|
mainWindow.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.on("window-close", () => {
|
||||||
|
if (mainWindow) mainWindow.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Topbar custom actions
|
||||||
|
ipcMain.on("open-devtools", () => {
|
||||||
|
if (mainWindow) mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
});
|
||||||
|
ipcMain.on("open-logs", () => {
|
||||||
|
if (logsFolderPath) shell.openPath(logsFolderPath);
|
||||||
|
});
|
||||||
|
ipcMain.on("open-website", () => {
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html");
|
||||||
|
});
|
||||||
|
ipcMain.on("open-wiki", () => {
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html");
|
||||||
|
});
|
||||||
|
ipcMain.on("open-config", () => {
|
||||||
|
if (configFolderPath) shell.openPath(configFolderPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// App ready
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
logSessionStart();
|
||||||
|
logger.info("App Ready, Server Express starting...");
|
||||||
|
|
||||||
|
const serverPath = path.join(__dirname, "server", "server.js")
|
||||||
|
|
||||||
|
const expressServer = require(serverPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expressServer.startServer();
|
||||||
|
logger.info("Express Server Started");
|
||||||
|
|
||||||
|
ipcMain.handle("features", () => {
|
||||||
|
return configFeatures;
|
||||||
|
});
|
||||||
|
|
||||||
|
const featureWhitelist = new Set([
|
||||||
|
"autoUpdate",
|
||||||
|
"discordRPC",
|
||||||
|
"customTopBar",
|
||||||
|
"autoCheckInfo",
|
||||||
|
"addThumbnail",
|
||||||
|
"addMetadata",
|
||||||
|
"verboseLogs",
|
||||||
|
"autoDownloadPlaylist",
|
||||||
|
"customCodec"
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
ipcMain.handle("set-feature", (event, { key, value }) => {
|
||||||
|
try {
|
||||||
|
if (!featureWhitelist.has(key)) {
|
||||||
|
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionnel mais propre
|
||||||
|
if (configFeatures[key] === value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
configFeatures[key] = value;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
configFolderPath,
|
||||||
|
JSON.stringify(configFeatures, null, 2),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Feature updated: ${key} = ${value}`);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`set-feature failed (${key}): ${err.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
configFeatures.discordRPC ? startRPC() : "";
|
||||||
|
|
||||||
|
await createMainWindow();
|
||||||
|
configFeatures.autoUpdate ? AutoUpdater(mainWindow) : ""; // Auto Update
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Window or Server error :", err);
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
logger.info("All Window Closed, shuting down app");
|
||||||
|
if (process.platform !== "darwin") app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("before-quit", () => logSessionEnd());
|
||||||
5956
package-lock.json
generated
Normal file
101
package.json
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "freedom-loader",
|
||||||
|
"productName": "Freedom Loader",
|
||||||
|
"version": "1.4.0",
|
||||||
|
"author": "MasterAcnolo",
|
||||||
|
"description": "Freedom Loader",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron --trace-warnings .",
|
||||||
|
"build": "electron-builder",
|
||||||
|
"update": "npm update"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"debug": "^4.4.1",
|
||||||
|
"discord-rpc": "^4.0.1",
|
||||||
|
"electron-updater": "^6.6.2",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"webidl-conversions": "^8.0.0",
|
||||||
|
"winston": "^3.17.0",
|
||||||
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^37.1.0",
|
||||||
|
"electron-builder": "^26.0.12"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.masteracnolo.freedomloader",
|
||||||
|
"productName": "Freedom Loader",
|
||||||
|
"publish": [
|
||||||
|
{
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "MasterAcnolo",
|
||||||
|
"repo": "Freedom-Loader"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"**/*",
|
||||||
|
"node_modules/**/*",
|
||||||
|
"!logs/*.json"
|
||||||
|
],
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "build"
|
||||||
|
},
|
||||||
|
"asar": true,
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "build/confirm-icon.png",
|
||||||
|
"to": "confirm-icon.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "build/app-icon.ico",
|
||||||
|
"to": "app-icon.ico"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "build/banner.bmp",
|
||||||
|
"to": "banner.bmp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "build/error.png",
|
||||||
|
"to": "error.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/yt-dlp.exe",
|
||||||
|
"to": "binaries/yt-dlp.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/ffmpeg.exe",
|
||||||
|
"to": "binaries/ffmpeg.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/ffprobe.exe",
|
||||||
|
"to": "binaries/ffprobe.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/deno.exe",
|
||||||
|
"to": "binaries/deno.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "config/config.json",
|
||||||
|
"to": "config/config.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": "nsis",
|
||||||
|
"icon": "build/app-icon.ico"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"perMachine": true,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"installerIcon": "build/app-icon.ico",
|
||||||
|
"uninstallerIcon": "build/uninstaller-icon.ico",
|
||||||
|
"installerSidebar": "build/banner.bmp",
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true,
|
||||||
|
"shortcutName": "Freedom Loader"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
preload.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
||||||
|
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
||||||
|
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
|
||||||
|
getFeatures: () => ipcRenderer.invoke("features"),
|
||||||
|
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
|
||||||
|
getVersion: () => ipcRenderer.invoke("version"),
|
||||||
|
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Contrôles de fenêtre et outils custom pour la topbar
|
||||||
|
contextBridge.exposeInMainWorld("topbarAPI", {
|
||||||
|
minimize: () => ipcRenderer.send("window-minimize"),
|
||||||
|
maximize: () => ipcRenderer.send("window-maximize"),
|
||||||
|
close: () => ipcRenderer.send("window-close"),
|
||||||
|
openDevTools: () => ipcRenderer.send("open-devtools"),
|
||||||
|
openLogs: () => ipcRenderer.send("open-logs"),
|
||||||
|
openWebsite: () => ipcRenderer.send("open-website"),
|
||||||
|
openWiki: () => ipcRenderer.send("open-wiki"),
|
||||||
|
openConfig: () => ipcRenderer.send("open-config")
|
||||||
|
});
|
||||||
1
public/assets/icon/close.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 128C92.7 128 64 156.7 64 192L64 448C64 483.3 92.7 512 128 512L512 512C547.3 512 576 483.3 576 448L576 192C576 156.7 547.3 128 512 128L128 128zM231 231C240.4 221.6 255.6 221.6 264.9 231L319.9 286L374.9 231C384.3 221.6 399.5 221.6 408.8 231C418.1 240.4 418.2 255.6 408.8 264.9L353.8 319.9L408.8 374.9C418.2 384.3 418.2 399.5 408.8 408.8C399.4 418.1 384.2 418.2 374.9 408.8L319.9 353.8L264.9 408.8C255.5 418.2 240.3 418.2 231 408.8C221.7 399.4 221.6 384.2 231 374.9L286 319.9L231 264.9C221.6 255.5 221.6 240.3 231 231z"/></svg>
|
||||||
|
After Width: | Height: | Size: 751 B |
1
public/assets/icon/maximize.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 128C92.7 128 64 156.7 64 192L64 448C64 483.3 92.7 512 128 512L512 512C547.3 512 576 483.3 576 448L576 192C576 156.7 547.3 128 512 128L128 128zM152 192L488 192C501.3 192 512 202.7 512 216C512 229.3 501.3 240 488 240L152 240C138.7 240 128 229.3 128 216C128 202.7 138.7 192 152 192z"/></svg>
|
||||||
|
After Width: | Height: | Size: 515 B |
1
public/assets/icon/minimize.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 480C64 462.3 78.3 448 96 448L544 448C561.7 448 576 462.3 576 480C576 497.7 561.7 512 544 512L96 512C78.3 512 64 497.7 64 480z"/></svg>
|
||||||
|
After Width: | Height: | Size: 360 B |
BIN
public/assets/images/Chirac.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
public/assets/images/Drift.jpg
Normal file
|
After Width: | Height: | Size: 967 KiB |
BIN
public/assets/images/NF.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
public/assets/images/Songbird.webp
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
public/assets/images/Spicy.png
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
BIN
public/assets/logo/Freedom Loader White.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/assets/logo/Freedom Loader.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/assets/logo/FreedomLoader2.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
276
public/index.html
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="
|
||||||
|
default-src 'self' 'unsafe-inline' data:;
|
||||||
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||||
|
font-src https://fonts.gstatic.com;
|
||||||
|
img-src 'self' data: https://i.ytimg.com;
|
||||||
|
">
|
||||||
|
<title></title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="styles/styles.css">
|
||||||
|
<link rel="stylesheet" href="styles/layout/topbar.css">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="topbar" id="topbar">
|
||||||
|
<div class="custom-controls">
|
||||||
|
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
||||||
|
<button id="logs-btn" title="Logs">Logs</button>
|
||||||
|
<button id="website-btn" title="Website">Website</button>
|
||||||
|
<button id="wiki-btn" title="Wiki">Wiki</button>
|
||||||
|
</div>
|
||||||
|
<div class="window-controls">
|
||||||
|
<button id="minimize-btn" title="Réduire"><img src="assets/icon/minimize.svg"></button>
|
||||||
|
<button id="maximize-btn" title="Maximiser"><img src="assets/icon/maximize.svg"></button>
|
||||||
|
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="settings-button" id="settings-button">
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-q</title><path d="M262.29,192.31a64,64,0,1,0,57.4,57.4A64.13,64.13,0,0,0,262.29,192.31ZM416.39,256a154.34,154.34,0,0,1-1.53,20.79l45.21,35.46A10.81,10.81,0,0,1,462.52,326l-42.77,74a10.81,10.81,0,0,1-13.14,4.59l-44.9-18.08a16.11,16.11,0,0,0-15.17,1.75A164.48,164.48,0,0,1,325,400.8a15.94,15.94,0,0,0-8.82,12.14l-6.73,47.89A11.08,11.08,0,0,1,298.77,470H213.23a11.11,11.11,0,0,1-10.69-8.87l-6.72-47.82a16.07,16.07,0,0,0-9-12.22,155.3,155.3,0,0,1-21.46-12.57,16,16,0,0,0-15.11-1.71l-44.89,18.07a10.81,10.81,0,0,1-13.14-4.58l-42.77-74a10.8,10.8,0,0,1,2.45-13.75l38.21-30a16.05,16.05,0,0,0,6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16,16,0,0,0-6.07-13.94l-38.19-30A10.81,10.81,0,0,1,49.48,186l42.77-74a10.81,10.81,0,0,1,13.14-4.59l44.9,18.08a16.11,16.11,0,0,0,15.17-1.75A164.48,164.48,0,0,1,187,111.2a15.94,15.94,0,0,0,8.82-12.14l6.73-47.89A11.08,11.08,0,0,1,213.23,42h85.54a11.11,11.11,0,0,1,10.69,8.87l6.72,47.82a16.07,16.07,0,0,0,9,12.22,155.3,155.3,0,0,1,21.46,12.57,16,16,0,0,0,15.11,1.71l44.89-18.07a10.81,10.81,0,0,1,13.14,4.58l42.77,74a10.8,10.8,0,0,1-2.45,13.75l-38.21,30a16.05,16.05,0,0,0-6.05,14.08C416.17,247.67,416.39,251.83,416.39,256Z" style="fill:none;stroke:var(--settings-button-text-color);stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="settings-panel" id="settings-panel">
|
||||||
|
<div class="settings-modal">
|
||||||
|
|
||||||
|
<button class="close-btn" onclick="closePanel()">✕</button>
|
||||||
|
|
||||||
|
<div class="settings-title">
|
||||||
|
<h2>SETTINGS</h2>
|
||||||
|
<p style="font-size: 0.8em;">Changes are saved automatically. <br> Somes changes could need a restart</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Basic Settings visibles -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>Basic Settings</h3>
|
||||||
|
|
||||||
|
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
||||||
|
<div class="setting-info">
|
||||||
|
<div style="display: flex;">
|
||||||
|
<span class="setting-title">Thème</span>
|
||||||
|
|
||||||
|
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<small>Choose the theme of the application</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="discordRPC" id="discordRPC">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Discord RPC</span>
|
||||||
|
<small>Enable/disable Discord Rich Presence for your app.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="autoDownloadPlaylist" id="autoDownloadPlaylist">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Download Playlist automatically?</span>
|
||||||
|
<small>If enabled, playlists will download without confirmation.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="notifySystem" id="notifySystem">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Notification at the end</span>
|
||||||
|
<small>Notify when a download finishes.</small>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="customTopBar" id="customTopBar">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Custom Top bar</span>
|
||||||
|
<small>Use this if the topbar is missing or you prefer a custom one.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Auto Fetch Info</span>
|
||||||
|
<small>Automatically fetch information for new downloads.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Advanced Settings -->
|
||||||
|
<details class="settings-section">
|
||||||
|
<summary>Advanced Settings</summary>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="autoUpdate" id="autoUpdate">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Auto Update</span>
|
||||||
|
<small>Enable automatic app updates.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Output title (special chars)</span>
|
||||||
|
<small>Check for unusual characters in output titles.</small>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Add Metadata</span>
|
||||||
|
<small>Include metadata in downloaded files.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="addThumbnail" id="addThumbnail">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Add Thumbnail</span>
|
||||||
|
<small>Include thumbnails in downloaded files.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item" style="margin-left: 32px; margin-top: 20px;">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Custom Codec</span>
|
||||||
|
<select data-key="customCodec">
|
||||||
|
<option value="h264">H264</option>
|
||||||
|
<option value="av1">AV1</option>
|
||||||
|
<option value="av01">AV01</option>
|
||||||
|
<option value="vp9.2">VP9.2</option>
|
||||||
|
<option value="vp9">VP9</option>
|
||||||
|
<option value="h265">H265</option>
|
||||||
|
<option value="theora">Theora</option>
|
||||||
|
</select>
|
||||||
|
<small>Choose your preferred video codec.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Developer Settings -->
|
||||||
|
<details class="settings-section">
|
||||||
|
<summary>Developer Settings</summary>
|
||||||
|
|
||||||
|
<!-- <div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="downloadSystem" id="downloadSystem">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Download System</span>
|
||||||
|
<small>Enable/disable the internal download system.</small>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- <div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="logSystem" id="logSystem">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Log System</span>
|
||||||
|
<small>Enable logging system.</small>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Verbose Logs</span>
|
||||||
|
<small>Show verbose logs for debugging purposes.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="settings-footer">
|
||||||
|
<button id="open-json-btn">Open JSON Configuration</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" id="container">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1 id="title"> Freedom Loader </h1>
|
||||||
|
<p id="subtitle"> Because why not? </p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form action="/download" method="POST" id="downloadForm">
|
||||||
|
<input name="url" placeholder="URL" id="UrlInput">
|
||||||
|
|
||||||
|
<label class="checkbox-inline">
|
||||||
|
|
||||||
|
<label class="checkbox-mp3">
|
||||||
|
<input type="checkbox" name="audioOnly" value="1">
|
||||||
|
<div class="checkmark"></div>
|
||||||
|
</label>
|
||||||
|
Audio Only (MP3)
|
||||||
|
</label>
|
||||||
|
<select name="quality">
|
||||||
|
<option value="best">Best Quality</option>
|
||||||
|
<option value="medium">Medium Quality</option>
|
||||||
|
<option value="worst">Lowest Quality</option>
|
||||||
|
<option value="1080">1080p</option>
|
||||||
|
<option value="720">720p</option>
|
||||||
|
<option value="480">480p</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button type="submit" onclick="startProgress()">Download</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-path">
|
||||||
|
Will be save on <span id="savePath"></span>
|
||||||
|
<button id="changePath">Edit</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="downloadProgressWrapper">
|
||||||
|
<div id="downloadProgress"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 20px;">
|
||||||
|
<div id="downloadProgressText"></div>
|
||||||
|
<div id="downloadSpeedText"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="downloadStatus"></div>
|
||||||
|
|
||||||
|
<div class="infos-container">
|
||||||
|
|
||||||
|
<div id="loaderBox" class="loader-box" style="display:none;">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<span>Fetching info...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span id="version-badge" class="version-badge"></span>
|
||||||
|
|
||||||
|
<h2 id="reference"> Because Why Not ? </h2>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="script/custompath.js"></script>
|
||||||
|
<script src="script/settingsPanel.js"></script>
|
||||||
|
<script src="script/appVersion.js"></script>
|
||||||
|
<script src="script/downloadstatus.js"></script>
|
||||||
|
<script src="script/fetchinfo.js"></script>
|
||||||
|
<script src="script/progressBar.js"></script>
|
||||||
|
<script src="script/customthemes.js"></script>
|
||||||
|
<script type="module" src="script/topbar.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
public/script/appVersion.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
async function versionLabel(){
|
||||||
|
const appVersion = await window.electronAPI.getVersion();
|
||||||
|
document.getElementById("version-badge").textContent = `v${appVersion}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
versionLabel();
|
||||||
62
public/script/custompath.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
const savePathElem = document.getElementById("savePath");
|
||||||
|
const form = document.getElementById("downloadForm");
|
||||||
|
|
||||||
|
// input caché = DERNIER chemin validé par le back
|
||||||
|
let hidden = document.getElementById("savePathInput");
|
||||||
|
if (!hidden) {
|
||||||
|
hidden = document.createElement("input");
|
||||||
|
hidden.type = "hidden";
|
||||||
|
hidden.name = "savePath";
|
||||||
|
hidden.id = "savePathInput";
|
||||||
|
form.appendChild(hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyPathFromBack(path) {
|
||||||
|
savePathElem.textContent = path;
|
||||||
|
hidden.value = path;
|
||||||
|
localStorage.setItem("customDownloadPath", path); // UX only
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInitialPath() {
|
||||||
|
// On affiche ce que le user a vu la dernière fois (UX)
|
||||||
|
const cached = localStorage.getItem("customDownloadPath");
|
||||||
|
if (cached) {
|
||||||
|
savePathElem.textContent = cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAIS la source de vérité reste le back
|
||||||
|
try {
|
||||||
|
const validatedPath = await window.electronAPI.getValidatedDownloadPath(
|
||||||
|
cached
|
||||||
|
);
|
||||||
|
await applyPathFromBack(validatedPath);
|
||||||
|
} catch {
|
||||||
|
// fallback sûr
|
||||||
|
const defaultPath =
|
||||||
|
await window.electronAPI.getDefaultDownloadPath();
|
||||||
|
await applyPathFromBack(defaultPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadInitialPath();
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById("changePath")
|
||||||
|
.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
const selectedPath =
|
||||||
|
await window.electronAPI.selectDownloadFolder();
|
||||||
|
|
||||||
|
if (!selectedPath) return; // annulé
|
||||||
|
|
||||||
|
// Validation back obligatoire
|
||||||
|
const validatedPath =
|
||||||
|
await window.electronAPI.getValidatedDownloadPath(selectedPath);
|
||||||
|
|
||||||
|
await applyPathFromBack(validatedPath);
|
||||||
|
} catch (err) {
|
||||||
|
alert("Dossier non autorisé.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
62
public/script/customthemes.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const themes = {
|
||||||
|
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
|
||||||
|
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
|
||||||
|
neon: { label: "Purple", subtitle: "How was your day ?"},
|
||||||
|
nf: { label: "NF", subtitle: "You call it music, i call it my Therapist" },
|
||||||
|
songbird: { label: "Songbird", subtitle: "From Her to Eternity" },
|
||||||
|
drift: { label: "Drift", subtitle: "Si la route t'appelle, contre appel" },
|
||||||
|
fanatic: { label: "Fanatic", subtitle: "Always Fnatic !" },
|
||||||
|
chirac: { label: "Chirac", subtitle: "J'aime les pommes" },
|
||||||
|
spicy: { label: "Spicy", subtitle: "The Spiciest One" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
|
||||||
|
function populateThemeSelect() {
|
||||||
|
for (const [themeKey, themeInfo] of Object.entries(themes)) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = themeKey;
|
||||||
|
option.textContent = themeInfo.label;
|
||||||
|
themeSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(themeKey) {
|
||||||
|
document.body.classList.remove(...Object.keys(themes));
|
||||||
|
document.body.classList.add(themeKey);
|
||||||
|
|
||||||
|
const subtitleElement = document.getElementById("subtitle");
|
||||||
|
if (subtitleElement && themes[themeKey]) {
|
||||||
|
subtitleElement.textContent = themes[themeKey].subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTheme(themeKey) {
|
||||||
|
localStorage.setItem("selectedTheme", themeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTheme() {
|
||||||
|
const savedTheme = localStorage.getItem("selectedTheme");
|
||||||
|
|
||||||
|
if (savedTheme && themes[savedTheme]) {
|
||||||
|
applyTheme(savedTheme);
|
||||||
|
themeSelect.value = savedTheme;
|
||||||
|
} else {
|
||||||
|
applyTheme("dark"); // thème par défaut
|
||||||
|
themeSelect.value = "dark";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quand l'utilisateur change le thème depuis le select
|
||||||
|
themeSelect.addEventListener("change", (event) => {
|
||||||
|
const selectedTheme = event.target.value;
|
||||||
|
if (themes[selectedTheme]) {
|
||||||
|
applyTheme(selectedTheme);
|
||||||
|
saveTheme(selectedTheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialisation
|
||||||
|
populateThemeSelect();
|
||||||
|
loadTheme();
|
||||||
37
public/script/downloadstatus.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const form = document.getElementById("downloadForm");
|
||||||
|
const statusDiv = document.getElementById("downloadStatus");
|
||||||
|
const button = form.querySelector("button");
|
||||||
|
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
button.disabled = true; // Empêche les clics multiples
|
||||||
|
statusDiv.textContent = "Download in progress...";
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const params = new URLSearchParams(formData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/download", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: params,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
statusDiv.textContent = res;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
statusDiv.textContent = text;
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
statusDiv.textContent = "❌ An Error has Occured.";
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
statusDiv.textContent = "";
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
191
public/script/fetchinfo.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||||
|
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes) {
|
||||||
|
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchVideoInfo(url) {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/info", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: new URLSearchParams({ url }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return { error: `An Error occured when fetching info` };
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data) return { error: "Data is Missing" };
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
return { error: "Network or JSON Issue" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const urlInput = document.getElementById("UrlInput");
|
||||||
|
const infoDiv = document.getElementById("videoInfo");
|
||||||
|
const loaderBox = document.getElementById("loaderBox");
|
||||||
|
|
||||||
|
let lastFetchedUrl = "";
|
||||||
|
|
||||||
|
urlInput.addEventListener("input", async () => {
|
||||||
|
|
||||||
|
const configFeatures = await window.electronAPI.getFeatures();
|
||||||
|
if (!configFeatures.autoCheckInfo) return;
|
||||||
|
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
|
// Si champ vide -> reset total
|
||||||
|
if (!url || url.length < 2) {
|
||||||
|
infoDiv.innerHTML = "";
|
||||||
|
infoDiv.classList.remove("visible", "playlist-mode");
|
||||||
|
lastFetchedUrl = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === lastFetchedUrl) return;
|
||||||
|
lastFetchedUrl = url;
|
||||||
|
|
||||||
|
loaderBox.style.display = "flex";
|
||||||
|
const data = await fetchVideoInfo(url);
|
||||||
|
loaderBox.style.display = "none";
|
||||||
|
|
||||||
|
// Gestion des erreurs
|
||||||
|
if (data.error) {
|
||||||
|
infoDiv.innerHTML = `
|
||||||
|
<div style="
|
||||||
|
padding:7px;
|
||||||
|
background:var(----infos-box-color);
|
||||||
|
">
|
||||||
|
<strong>${data.error}</strong>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
infoDiv.classList.add("visible");
|
||||||
|
infoDiv.classList.remove("playlist-mode");
|
||||||
|
loaderBox.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- PLAYLIST ----------
|
||||||
|
if (data.type === "playlist") {
|
||||||
|
infoDiv.classList.add("playlist-mode");
|
||||||
|
infoDiv.innerHTML = `
|
||||||
|
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist Detected: ${data.title}</strong></h3>
|
||||||
|
<h3 style="color:var(--video-info-heading-color);"><strong>Video Count: ${data.count}</strong></h3>
|
||||||
|
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p>
|
||||||
|
<div id="playlistVideos"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const listDiv = document.getElementById("playlistVideos");
|
||||||
|
|
||||||
|
data.videos.forEach(v => {
|
||||||
|
const durationStr = v.duration
|
||||||
|
? `${Math.floor(v.duration / 60)}m ${(v.duration % 60).toString().padStart(2,"0")}s` : "Inconnue";
|
||||||
|
|
||||||
|
const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
|
||||||
|
|
||||||
|
listDiv.innerHTML += `
|
||||||
|
<div style="margin-bottom:12px;">
|
||||||
|
<img src="${v.thumbnail}" width="160" alt="Thumbnail">
|
||||||
|
<p><strong>${v.title}</strong></p>
|
||||||
|
<p>Duration : ${durationStr}</p>
|
||||||
|
<p><a href="${videoUrl}" target="_blank">URL</a>
|
||||||
|
<button class="copy-btn" data-url="${videoUrl}">📋</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestion du bouton copier
|
||||||
|
listDiv.addEventListener("click", (event) => {
|
||||||
|
if (event.target.classList.contains("copy-btn")) {
|
||||||
|
const btn = event.target;
|
||||||
|
if (btn.disabled) return;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
const url = btn.dataset.url;
|
||||||
|
navigator.clipboard.writeText(url)
|
||||||
|
.then(() => {
|
||||||
|
const original = btn.textContent;
|
||||||
|
|
||||||
|
btn.style.opacity = 0;
|
||||||
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = "✅";
|
||||||
|
btn.style.opacity = 1;
|
||||||
|
btn.style.transform = "scale(1)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.style.opacity = 0;
|
||||||
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = original;
|
||||||
|
btn.style.opacity = 1;
|
||||||
|
btn.style.transform = "scale(1)";
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
}, 300);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
const original = btn.textContent;
|
||||||
|
btn.textContent = "❌";
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = original;
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
infoDiv.classList.add("visible");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
infoDiv.classList.remove("playlist-mode");
|
||||||
|
|
||||||
|
// ---------- VIDEO NORMALE ----------
|
||||||
|
const durationStr = data.duration
|
||||||
|
? `${Math.floor(data.duration/60)}m ${(data.duration%60).toString().padStart(2,"0")}s`
|
||||||
|
: "Unknown";
|
||||||
|
|
||||||
|
const sizeStr = formatSize(data.filesize_approx);
|
||||||
|
const readableDate = formatDate(data.upload_date);
|
||||||
|
const categories = data.categories?.join(", ") || "Non spécifiées";
|
||||||
|
|
||||||
|
infoDiv.innerHTML = `
|
||||||
|
<h3>${data.title}</h3>
|
||||||
|
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||||
|
<ul>
|
||||||
|
<li><strong>Duration :</strong> ${durationStr}</li>
|
||||||
|
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
||||||
|
<li><strong>Upload Date :</strong> ${readableDate}</li>
|
||||||
|
<li><strong>Views :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||||
|
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
|
||||||
|
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank">${data.webpage_url}</a></li>
|
||||||
|
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank">${data.channel_url}</a></li>
|
||||||
|
<li><strong>Estimed Size :</strong> ${sizeStr}</li>
|
||||||
|
<li><strong>Category :</strong> ${categories}</li>
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
infoDiv.classList.add("visible");
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
63
public/script/progressBar.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
|
const progressBar = document.getElementById("downloadProgress");
|
||||||
|
const progressBarText = document.getElementById("downloadProgressText")
|
||||||
|
|
||||||
|
const speedElement = document.getElementById("downloadSpeedText");
|
||||||
|
const speedEvt = new EventSource("/download/speed");
|
||||||
|
|
||||||
|
function startProgress() {
|
||||||
|
progressWrapper.style.display = "block";
|
||||||
|
progressBar.style.width = "0%";
|
||||||
|
progressBarText.style.display = "block";
|
||||||
|
progressBarText.innerHTML = "0%";
|
||||||
|
|
||||||
|
speedElement.style.display = "block";
|
||||||
|
speedElement.innerHTML = "0 Mib/s";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(percent) {
|
||||||
|
progressBar.style.width = `${percent}%`;
|
||||||
|
progressBarText.innerHTML = `${percent}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetProgress() {
|
||||||
|
progressBar.style.width = "0%";
|
||||||
|
progressBarText.textContent = "";
|
||||||
|
progressBarText.style.display = "none";
|
||||||
|
progressWrapper.style.display = "none";
|
||||||
|
|
||||||
|
speedElement.textContent = "0 Mib/s";
|
||||||
|
speedElement.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connexion SSE
|
||||||
|
const evtSource = new EventSource("/download/progress");
|
||||||
|
evtSource.onmessage = e => {
|
||||||
|
if (e.data === "reset") {
|
||||||
|
startProgress();
|
||||||
|
window.electronAPI.setProgress(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.data === "done") {
|
||||||
|
resetProgress();
|
||||||
|
window.electronAPI.setProgress(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percent = parseFloat(e.data);
|
||||||
|
|
||||||
|
if (!isNaN(percent)) {
|
||||||
|
updateProgress(percent);
|
||||||
|
window.electronAPI.setProgress(percent); // update barre des tâches
|
||||||
|
if (percent >= 100) setTimeout(() => {
|
||||||
|
resetProgress();
|
||||||
|
window.electronAPI.setProgress(-1); // retire la barre
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
speedEvt.onmessage = e => {
|
||||||
|
speedElement.style.display = "block";
|
||||||
|
speedElement.textContent = e.data; // ex: "5.2MiB/s"
|
||||||
|
};
|
||||||
50
public/script/settingsPanel.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const settingsPanel = document.getElementById("settings-panel");
|
||||||
|
const settingsModal = document.querySelector(".settings-modal");
|
||||||
|
|
||||||
|
const settingsButton = document.getElementById("settings-button");
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
settingsButton.addEventListener("click", () => {
|
||||||
|
const isOpen = settingsPanel.style.display === "block";
|
||||||
|
settingsPanel.style.display = isOpen ? "none" : "block";
|
||||||
|
});
|
||||||
|
|
||||||
|
function closePanel() {
|
||||||
|
isOpen = false;
|
||||||
|
settingsPanel.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// charge les features
|
||||||
|
async function loadSettings() {
|
||||||
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
|
document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => {
|
||||||
|
const key = el.dataset.key;
|
||||||
|
if (features[key] !== undefined) {
|
||||||
|
if (el.type === "checkbox") el.checked = features[key];
|
||||||
|
else if (el.tagName === "SELECT") el.value = features[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
el.addEventListener("change", () => {
|
||||||
|
let value = el.type === "checkbox" ? el.checked : el.value;
|
||||||
|
window.electronAPI.setFeature(key, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ouvrir le JSON
|
||||||
|
document.getElementById("open-json-btn").addEventListener("click", () => {
|
||||||
|
window.topbarAPI.openConfig(); // ton IPC existant
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/* clic sur l'overlay = fermer */
|
||||||
|
settingsPanel.addEventListener("click", closePanel);
|
||||||
|
|
||||||
|
/* clic dans la modal = stop propagation */
|
||||||
|
settingsModal.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
34
public/script/topbar.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Ajout des listeners pour la topbar
|
||||||
|
function setupTopbarListeners() {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const { topbarAPI } = window;
|
||||||
|
if (!topbarAPI) return;
|
||||||
|
const minBtn = document.getElementById('minimize-btn');
|
||||||
|
const maxBtn = document.getElementById('maximize-btn');
|
||||||
|
const closeBtn = document.getElementById('close-btn');
|
||||||
|
const devtoolsBtn = document.getElementById('devtools-btn');
|
||||||
|
const logsBtn = document.getElementById('logs-btn');
|
||||||
|
const websiteBtn = document.getElementById('website-btn');
|
||||||
|
const wikiBtn = document.getElementById('wiki-btn');
|
||||||
|
const configBtn = document.getElementById('config-btn');
|
||||||
|
|
||||||
|
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||||
|
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
||||||
|
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
||||||
|
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
||||||
|
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
||||||
|
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
||||||
|
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
||||||
|
if (configBtn) configBtn.onclick = () => topbarAPI.openConfig();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTopbarListeners(); // IF it put it the if check. It don't work. Why ?
|
||||||
|
|
||||||
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
|
if(!features.customTopBar){
|
||||||
|
document.getElementById("topbar").style.display = "none";
|
||||||
|
document.getElementById("container").style.marginTop = "0";
|
||||||
|
document.getElementById("theme-switcher").style.top = "30px";
|
||||||
|
}
|
||||||
73
public/styles/components/checkbox.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
.checkbox-mp3 input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-mp3 {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
--clr: var(--checkbox-bg-checked);
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 1.3em;
|
||||||
|
width: 1.3em;
|
||||||
|
background-color: var(--checkbox-bg-default);
|
||||||
|
border-radius: .5rem;
|
||||||
|
transition: background-color 300ms, border-radius 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-mp3 input:checked ~ .checkmark {
|
||||||
|
background-color: var(--clr);
|
||||||
|
border-radius: .5rem;
|
||||||
|
animation: pulse 500ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-mp3 input:checked ~ .checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .checkmark:after {
|
||||||
|
left: 0.45em;
|
||||||
|
top: 0.25em;
|
||||||
|
width: 0.25em;
|
||||||
|
height: 0.5em;
|
||||||
|
border: solid var(--checkbox-checkmark-border-color);
|
||||||
|
border-width: 0 0.15em 0.15em 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 var(--checkbox-pulse-color);
|
||||||
|
rotate: 20deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
rotate: -20deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
box-shadow: 0 0 0 10px var(--checkbox-pulse-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 13px var(--checkbox-pulse-color);
|
||||||
|
rotate: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
public/styles/components/editpathbutton.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.download-path {
|
||||||
|
margin-top: 1em;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--default-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-path #savePath:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-path #savePath {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
color: var(--form-input-text-color);
|
||||||
|
padding: 0.3em 0.6em;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: monospace;
|
||||||
|
border: 1px dashed var(--form-input-border-color);
|
||||||
|
word-break: break-all;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.download-path button#changePath {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
background-color: var(--form-button-bg-color);
|
||||||
|
color: var(--form-button-text-color);
|
||||||
|
border: none;
|
||||||
|
padding: 0.3em 0.8em;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 550;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-path button#changePath:hover {
|
||||||
|
background-color: var(--form-button-bg-hover-color);
|
||||||
|
}
|
||||||
34
public/styles/components/loader.css
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
.loader-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: var(--form-bg-color);
|
||||||
|
border: 2px solid var(--mp3-text-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--mp3-text-color);
|
||||||
|
font-weight: bold;
|
||||||
|
animation: pulse 1.2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-box div {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: var(--form-button-bg-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
animation: bounce 1.2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-box div:nth-child(1) { animation-delay: 0s; }
|
||||||
|
.loader-box div:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.loader-box div:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: scale(0); }
|
||||||
|
40% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 5px var(--form-button-bg-color); }
|
||||||
|
50% { box-shadow: 0 0 20px var(--form-button-bg-color); }
|
||||||
|
}
|
||||||
41
public/styles/components/progressBar.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/* Wrapper de la barre de téléchargement */
|
||||||
|
#downloadProgressWrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 10px auto;
|
||||||
|
display: none;
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barre de progression */
|
||||||
|
#downloadProgress {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--video-info-link-color, #4caf50);
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadProgressText, #downloadSpeedText {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #fff;
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive pour mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#downloadProgressWrapper {
|
||||||
|
max-width: 90%;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadProgressText, #downloadSpeedText {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
public/styles/layout/container.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.container, .infos-container {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 1em;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadStatus {
|
||||||
|
margin-top: 1em;
|
||||||
|
min-height: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--download-status-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
76
public/styles/layout/form.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
form {
|
||||||
|
background-color: var(--form-bg-color);
|
||||||
|
padding: 2em;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input[type="text"],
|
||||||
|
form input[type="url"],
|
||||||
|
form select {
|
||||||
|
padding: 0.75em;
|
||||||
|
border: 1px solid var(--form-input-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--form-input-bg-color);
|
||||||
|
color: var(--form-input-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
form input[type="checkbox"] {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button {
|
||||||
|
background-color: var(--form-button-bg-color);
|
||||||
|
color: var(--form-button-text-color);
|
||||||
|
border: none;
|
||||||
|
padding: 0.75em;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button:hover {
|
||||||
|
background-color: var(--form-button-bg-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
form#downloadForm {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
color: var(--form-input-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#UrlInput {
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
border-color: var(--form-input-border-color);
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
background-color: var(--form-input-bg-color);
|
||||||
|
color: var(--form-input-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#UrlInput:focus {
|
||||||
|
border-color: var(--form-input-border-focus-color);
|
||||||
|
box-shadow: 0 0 5px rgba(0,123,255,0.4);
|
||||||
|
outline: none;
|
||||||
|
background-color: var(--form-input-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#UrlInput::placeholder {
|
||||||
|
color: var(--form-input-placeholder-color);
|
||||||
|
}
|
||||||
11
public/styles/layout/header.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
header h1{
|
||||||
|
margin-bottom: 0.2vw;
|
||||||
|
}
|
||||||
234
public/styles/layout/settingsPanel.css
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
/* SETTINGS BUTTON */
|
||||||
|
.settings-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 60px;
|
||||||
|
right: 20px;
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: var(--settings-button-bg-color);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button svg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
stroke: var(--settings-button-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* SETTINGS PANEL */
|
||||||
|
.settings-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 60px;
|
||||||
|
right: 40px;
|
||||||
|
width: 40vw;
|
||||||
|
max-width: 320px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background-color: var(--settings-bg-primary-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||||
|
display: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 9999;
|
||||||
|
animation: slideIn 0.2s ease-out;
|
||||||
|
color: var(--settings-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-modal {
|
||||||
|
width: 100%;
|
||||||
|
min-height: auto;
|
||||||
|
max-height: none;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TITLE */
|
||||||
|
.settings-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ccc;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SECTION */
|
||||||
|
.settings-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: var(--settings-bg-secondary-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SETTING ITEM */
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-info small {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--settings-subtitle-color);
|
||||||
|
margin-top: 2px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme .settings-title{
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item select {
|
||||||
|
width: 70%;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #242424;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item select:hover,
|
||||||
|
.setting-item select:focus {
|
||||||
|
background: #474747;
|
||||||
|
color: #fff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SELECT */
|
||||||
|
.settings-section select {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DETAILS / DROPDOWN */
|
||||||
|
details {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] {
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOOTER */
|
||||||
|
.settings-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#open-json-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: var(--form-button-bg-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#experimental-args {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #333;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CLOSE BTN */
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANIMATION */
|
||||||
|
@keyframes popIn {
|
||||||
|
from { transform: scale(0.95); opacity: 0; }
|
||||||
|
to { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(20px); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
116
public/styles/layout/topbar.css
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1100;
|
||||||
|
|
||||||
|
height: 48px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
background: #1f1f1f98;
|
||||||
|
color: #eaeaea;
|
||||||
|
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LEFT - custom buttons */
|
||||||
|
.custom-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RIGHT - window buttons */
|
||||||
|
.window-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GLOBAL BUTTON RESET */
|
||||||
|
.topbar button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
transition: background 0.15s ease, transform 0.1s ease;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom buttons (Logs / Wiki / etc.) */
|
||||||
|
.custom-controls button {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-controls button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Window buttons (Windows style) */
|
||||||
|
.window-controls button {
|
||||||
|
width: 46px;
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls img {
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#close-btn:hover {
|
||||||
|
background: #e81123;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active effect */
|
||||||
|
.topbar button:active {
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#close-btn:active {
|
||||||
|
background: #c50f1f;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge {
|
||||||
|
position: fixed;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 12px;
|
||||||
|
z-index: 2000;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #b0b0b0;
|
||||||
|
background: none;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
155
public/styles/layout/videoinfo.css
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#videoInfo {
|
||||||
|
margin-top: 20px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 1.5em 2em;
|
||||||
|
background-color: var(--infos-box-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||||
|
color: var(--video-info-text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: opacity 0.5s ease, max-height 0.5s ease;
|
||||||
|
user-select: text;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo.visible {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 1500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: 0.6em;
|
||||||
|
color: var(--video-info-heading-color);
|
||||||
|
font-weight: 700;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo ul li {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--video-info-list-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo ul li strong {
|
||||||
|
color: var(--video-info-list-strong-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo ul li a {
|
||||||
|
color: var(--video-info-link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo ul li a:hover,
|
||||||
|
#videoInfo ul li a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Playlist Mode --- */
|
||||||
|
#videoInfo.playlist-mode {
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style du bloc "Playlist détectée" */
|
||||||
|
#videoInfo.playlist-mode > p {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout liste des vidéos */
|
||||||
|
#playlistVideos {
|
||||||
|
margin-top: 1em;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Un item de playlist */
|
||||||
|
#playlistVideos > div {
|
||||||
|
background: var(--playlist-background-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos > div p {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo.playlist-mode {
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos > div:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
all: unset;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: transform 0.1s ease, color 0.1s ease, opacity 0.15s ease;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos > div p a{
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--video-info-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos > div p a:hover{
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#videoInfo {
|
||||||
|
padding: 1em;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo h3 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
118
public/styles/styles.css
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/* Layout */
|
||||||
|
@import url("layout/form.css");
|
||||||
|
@import url("layout/settingsPanel.css");
|
||||||
|
@import url("layout/container.css");
|
||||||
|
@import url("layout/header.css");
|
||||||
|
@import url("layout/videoinfo.css");
|
||||||
|
|
||||||
|
/* Components */
|
||||||
|
@import url("components/checkbox.css");
|
||||||
|
@import url("components/loader.css");
|
||||||
|
|
||||||
|
@import url("theme/themeimport.css");
|
||||||
|
@import url("components/editpathbutton.css");
|
||||||
|
@import url("components/progressBar.css");
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.6s ease;
|
||||||
|
}
|
||||||
|
#app.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,body{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
/*outline: 2px solid red; /*<-- Utile pour le debug ça */
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--default-text-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Because why not */
|
||||||
|
#reference{
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
bottom:0vh;
|
||||||
|
left: 10px;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #b0b0b02f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar globale pour Chromium/Electron */
|
||||||
|
body::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.35);
|
||||||
|
transform: scaleX(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thème clair */
|
||||||
|
body.light::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container{
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p{
|
||||||
|
color: var(--subtitle-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-inline{
|
||||||
|
color: var(--mp3-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
.container,
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
label {
|
||||||
|
transition: background-color 0.2s ease, color 0.5s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
103
public/styles/theme/chirac-theme.css
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
body.chirac {
|
||||||
|
|
||||||
|
background-image: url('../../assets/images/Chirac.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-attachment: fixed;
|
||||||
|
|
||||||
|
|
||||||
|
--background-color: rgba(255, 255, 255, 0.85);
|
||||||
|
--default-text-color: #002395;
|
||||||
|
--subtitle-color: #EF4135;
|
||||||
|
--mp3-text-color: #002395;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: rgba(255, 255, 255, 0.9);
|
||||||
|
--form-input-bg-color: #f5f5f5;
|
||||||
|
--form-input-border-color: #002395;
|
||||||
|
--form-input-border-focus-color: #EF4135;
|
||||||
|
--form-input-text-color: #002395;
|
||||||
|
--form-input-placeholder-color: #999999;
|
||||||
|
--form-button-bg-color: #002395;
|
||||||
|
--form-button-text-color: #ffffff;
|
||||||
|
--form-button-bg-hover-color: #EF4135;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #cccccc;
|
||||||
|
--checkbox-bg-checked: #002395;
|
||||||
|
--checkbox-checkmark-border-color: #ffffff;
|
||||||
|
--checkbox-pulse-color: rgba(0, 35, 149, 0.5);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #002395;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: rgba(255, 255, 255, 0.9);
|
||||||
|
--video-info-text-color: #000000;
|
||||||
|
--video-info-heading-color: #EF4135;
|
||||||
|
--video-info-list-color: #444444;
|
||||||
|
--video-info-list-strong-color: #000000;
|
||||||
|
--video-info-link-color: #002395;
|
||||||
|
--video-info-link-hover-color: #EF4135;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #d1d1d1ca;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: #00196a;
|
||||||
|
--settings-button-text-color: #dcdcdc;
|
||||||
|
|
||||||
|
--settings-bg-primary-color: #0b0528;
|
||||||
|
--settings-bg-secondary-color: #1c1055;
|
||||||
|
|
||||||
|
--settings-text-color: #cecece;
|
||||||
|
--settings-subtitle-color: #adadad;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Je serai le président de tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Mes chers compatriotes
|
||||||
|
Je serai le président de tous les Français, de tous les Français
|
||||||
|
Je mesure la difficulté de la tâche qui nous attend
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Je veux un État, je veux les Français
|
||||||
|
Je veux un État, je veux
|
||||||
|
Je veux un État-tat-tat-tat vigoureux
|
||||||
|
Un État-tat-tat-tat impartial
|
||||||
|
Un État-tat-tat-tat vigoureux
|
||||||
|
Un État-tat-tat-tat exigeant
|
||||||
|
Un État-tat-tat-tat
|
||||||
|
Je serai le président de tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Tous les Français
|
||||||
|
Un État qui n'isole pas, tous, pas, pas
|
||||||
|
Tous, pas, tous, pas, pas
|
||||||
|
Tous, pas, tous, pas, pas (les Français)
|
||||||
|
Tous, tous, pas, tous, tous, pas, pas
|
||||||
|
Tous, tous, pas, tous, tous, pas, pas
|
||||||
|
Tous, tous, tous, pas, tous, pas, pas (les Français)
|
||||||
|
Tous, pas, tous, tous, pas, pas
|
||||||
|
Tous, tous, tous, pas, tous, tous, pas, pas
|
||||||
|
Tous, tous, pas, tous, tous, pas, pas
|
||||||
|
Tous, tous, tous-tous, tous-tous, pas, pas
|
||||||
|
Tous, tous-tous, tous-tous, pas, pas
|
||||||
|
Un État-tat-tat-tat vigoureux
|
||||||
|
Un État-tat-tat-tat impartial
|
||||||
|
Un État-tat-tat-tat exigeant
|
||||||
|
Un État-tat-tat-tat
|
||||||
|
Je serai le président de tous les Français
|
||||||
|
|
||||||
|
*/
|
||||||
49
public/styles/theme/dark-theme.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
body.dark {
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #121212; /* noir/gris très foncé */
|
||||||
|
--default-text-color: #e0e0e0; /* texte clair */
|
||||||
|
--subtitle-color: #2196f3; /* bleu accent */
|
||||||
|
--mp3-text-color: #f5f5f5; /* texte checkbox */
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #1e1e1e; /* fond formulaire */
|
||||||
|
--form-input-bg-color: #2a2a2a; /* fond inputs */
|
||||||
|
--form-input-border-color: #444444; /* bordure */
|
||||||
|
--form-input-border-focus-color: #2196f3; /* focus bleu */
|
||||||
|
--form-input-text-color: #f5f5f5; /* texte clair */
|
||||||
|
--form-input-placeholder-color: #888888; /* placeholder gris */
|
||||||
|
--form-button-bg-color: #2196f3; /* bouton bleu */
|
||||||
|
--form-button-text-color: #ffffff; /* texte bouton */
|
||||||
|
--form-button-bg-hover-color: #1976d2; /* hover bleu plus foncé */
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #555555; /* gris moyen */
|
||||||
|
--checkbox-bg-checked: #2196f3; /* bleu accent */
|
||||||
|
--checkbox-checkmark-border-color: #ffffff; /* blanc */
|
||||||
|
--checkbox-pulse-color: rgba(33, 150, 243, 0.5);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #2196f3;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #1a1a1a;
|
||||||
|
--video-info-text-color: #e0e0e0;
|
||||||
|
--video-info-heading-color: #2196f3;
|
||||||
|
--video-info-list-color: #cccccc;
|
||||||
|
--video-info-list-strong-color: #ffffff;
|
||||||
|
--video-info-link-color: #2196f3;
|
||||||
|
--video-info-link-hover-color: #1976d2;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #303030;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: #1e1e1e;
|
||||||
|
--settings-button-text-color: var(--default-text-color);
|
||||||
|
|
||||||
|
--settings-bg-primary-color: #1e1e1e;
|
||||||
|
--settings-bg-secondary-color: #2a2a2a;
|
||||||
|
--settings-text-color: var(--default-text-color);
|
||||||
|
--settings-subtitle-color: #aaa;
|
||||||
|
|
||||||
|
}
|
||||||
61
public/styles/theme/drift-theme.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
body.drift {
|
||||||
|
|
||||||
|
background-image: url('../../assets/images/Drift.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
|
||||||
|
/* Palette principale */
|
||||||
|
--background-color: #0f1218;
|
||||||
|
--default-text-color: #3d4b61;
|
||||||
|
--subtitle-color: #1a1a1a;
|
||||||
|
--mp3-text-color: #c0d4ff;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #1e222a;
|
||||||
|
--form-input-bg-color: #13171d;
|
||||||
|
--form-input-border-color: #3b4b6d;
|
||||||
|
--form-input-border-focus-color: #6ea8ff;
|
||||||
|
--form-input-text-color: #f0f4ff;
|
||||||
|
--form-input-placeholder-color: #7a8fae;
|
||||||
|
--form-button-bg-color: #2a2979;
|
||||||
|
--form-button-text-color: #ffffff;
|
||||||
|
--form-button-bg-hover-color: #353393;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #2a2e38;
|
||||||
|
--checkbox-bg-checked: #2a2979;
|
||||||
|
--checkbox-checkmark-border-color: #ffffff;
|
||||||
|
--checkbox-pulse-color: rgba(76,108,255,0.3);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #ff8e8e;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #1e222ad0;
|
||||||
|
--video-info-text-color: #e8e8f0;
|
||||||
|
--video-info-heading-color: #9ab6ff;
|
||||||
|
--video-info-list-color: #c0d4ff;
|
||||||
|
--video-info-list-strong-color: #ffffff;
|
||||||
|
--video-info-link-color: #4c6cff;
|
||||||
|
--video-info-link-hover-color: #6c8dff;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #424b5cc2;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: #1e222ace;
|
||||||
|
--settings-button-text-color: #b3b3b4;
|
||||||
|
|
||||||
|
--settings-bg-primary-color: #1e222a;
|
||||||
|
--settings-bg-secondary-color: #272d38;
|
||||||
|
|
||||||
|
--settings-text-color: #bababa;
|
||||||
|
--settings-subtitle-color: #cacaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
body.drift .download-path{
|
||||||
|
color: #ededed;
|
||||||
|
}
|
||||||
49
public/styles/theme/fanatic-theme.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
body.fanatic{
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #121212;
|
||||||
|
--default-text-color: #eee;
|
||||||
|
--subtitle-color: #FF5900;
|
||||||
|
--mp3-text-color: #ffffff;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #1e1e1e;
|
||||||
|
--form-input-bg-color: #2b2b2b;
|
||||||
|
--form-input-border-color: #333333;
|
||||||
|
--form-input-border-focus-color: #FF5900;
|
||||||
|
--form-input-text-color: #eeeeee;
|
||||||
|
--form-input-placeholder-color: #666666;
|
||||||
|
--form-button-bg-color: #FF5900;
|
||||||
|
--form-button-text-color: #121212;
|
||||||
|
--form-button-bg-hover-color: #e65500;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #555555;
|
||||||
|
--checkbox-bg-checked: #ff6600;
|
||||||
|
--checkbox-checkmark-border-color: #eee;
|
||||||
|
--checkbox-pulse-color: rgba(255, 102, 0, 0.5);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #FF5900;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #222222;
|
||||||
|
--video-info-text-color: #eee;
|
||||||
|
--video-info-heading-color: #FF5900;
|
||||||
|
--video-info-list-color: #ccc;
|
||||||
|
--video-info-list-strong-color: #fff;
|
||||||
|
--video-info-link-color: #FF5900;
|
||||||
|
--video-info-link-hover-color: #e65500;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #2b2b2b;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: #1e1e1e;
|
||||||
|
--settings-button-text-color: #dcdcdc;
|
||||||
|
|
||||||
|
--settings-bg-primary-color: #1e1e1e;
|
||||||
|
--settings-bg-secondary-color: #2d2d2d;
|
||||||
|
|
||||||
|
--settings-text-color: #cecece;
|
||||||
|
--settings-subtitle-color: #adadad;
|
||||||
|
}
|
||||||
48
public/styles/theme/light-theme.css
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
body.light {
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #dadada;
|
||||||
|
--default-text-color: #222222;
|
||||||
|
--subtitle-color: #007bff;
|
||||||
|
--mp3-text-color: #000000;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #ffffff;
|
||||||
|
--form-input-bg-color: #fafafa;
|
||||||
|
--form-input-border-color: #cccccc;
|
||||||
|
--form-input-border-focus-color: #007bff;
|
||||||
|
--form-input-text-color: #333333;
|
||||||
|
--form-input-placeholder-color: #888888;
|
||||||
|
--form-button-bg-color: #007bff;
|
||||||
|
--form-button-text-color: #ffffff;
|
||||||
|
--form-button-bg-hover-color: #0056b3;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #cccccc;
|
||||||
|
--checkbox-bg-checked: #007bff;
|
||||||
|
--checkbox-checkmark-border-color: #ffffff;
|
||||||
|
--checkbox-pulse-color: rgba(0, 123, 255, 0.4);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #007bff;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #ffffff;
|
||||||
|
--video-info-text-color: #222222;
|
||||||
|
--video-info-heading-color: #007bff;
|
||||||
|
--video-info-list-color: #444444;
|
||||||
|
--video-info-list-strong-color: #000000;
|
||||||
|
--video-info-link-color: #007bff;
|
||||||
|
--video-info-link-hover-color: #0056b3;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #c5c5c5b2;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: #e4e4e4;
|
||||||
|
--settings-button-text-color: var(--default-text-color);
|
||||||
|
|
||||||
|
--settings-bg-primary-color: #cbcbcb;
|
||||||
|
--settings-bg-secondary-color: #cbcbcb;
|
||||||
|
--settings-text-color: var(--default-text-color);
|
||||||
|
--settings-subtitle-color: #656565;
|
||||||
|
}
|
||||||
59
public/styles/theme/neon-theme.css
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
body.neon {
|
||||||
|
|
||||||
|
background: (url("../../assets/images/drift.jpg"));
|
||||||
|
background-image: linear-gradient(135deg, #0f0c29, #302b63, #24243e); /* dégradé sombre violet/bleu */
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #1b1b2f;
|
||||||
|
--default-text-color: #e0e0ff;
|
||||||
|
--subtitle-color: #a3a3ff;
|
||||||
|
--mp3-text-color: #c0c0ff;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #2c2c4a;
|
||||||
|
--form-input-bg-color: #1f1f3a;
|
||||||
|
--form-input-border-color: #5858f0;
|
||||||
|
--form-input-border-focus-color: #a3a3ff;
|
||||||
|
--form-input-text-color: #ffffff;
|
||||||
|
--form-input-placeholder-color: #8888ff;
|
||||||
|
--form-button-bg-color: #5858f0;
|
||||||
|
--form-button-text-color: #ffffff;
|
||||||
|
--form-button-bg-hover-color: #7a7aff;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #302b63;
|
||||||
|
--checkbox-bg-checked: #5858f0;
|
||||||
|
--checkbox-checkmark-border-color: #ffffff;
|
||||||
|
--checkbox-pulse-color: rgba(88,88,240,0.3);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #a3a3ff;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #2c2c4a;
|
||||||
|
--video-info-text-color: #e0e0ff;
|
||||||
|
--video-info-heading-color: #a3a3ff;
|
||||||
|
--video-info-list-color: #c0c0ff;
|
||||||
|
--video-info-list-strong-color: #ffffff;
|
||||||
|
--video-info-link-color: #5858f0;
|
||||||
|
--video-info-link-hover-color: #7a7aff;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #3e3e64;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: #202038;
|
||||||
|
--settings-button-text-color: var(--default-text-color);
|
||||||
|
|
||||||
|
--settings-bg-primary-color: #242454;
|
||||||
|
--settings-bg-secondary-color: #2f2f6f;
|
||||||
|
--settings-text-color: var(--default-text-color);
|
||||||
|
--settings-subtitle-color: #d2d2d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.neon .download-path {
|
||||||
|
color: var(--default-text-color);
|
||||||
|
}
|
||||||
58
public/styles/theme/nf-theme.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
body.nf {
|
||||||
|
|
||||||
|
background-image: url('../../assets/images/NF.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: 70% center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #1a1a1a;
|
||||||
|
--default-text-color: #e6dfd5;
|
||||||
|
--subtitle-color: #f5f0e6;
|
||||||
|
--mp3-text-color: #d8cfc2;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #2a2a2a;
|
||||||
|
--form-input-bg-color: #222222;
|
||||||
|
--form-input-border-color: #4a4036;
|
||||||
|
--form-input-border-focus-color: #cbb89d;
|
||||||
|
--form-input-text-color: #f0e8dd;
|
||||||
|
--form-input-placeholder-color: #9a8f80;
|
||||||
|
--form-button-bg-color: #cbb89d;
|
||||||
|
--form-button-text-color: #1a1a1a;
|
||||||
|
--form-button-bg-hover-color: #b7a588;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #4a4036;
|
||||||
|
--checkbox-bg-checked: #cbb89d;
|
||||||
|
--checkbox-checkmark-border-color: #1a1a1a;
|
||||||
|
--checkbox-pulse-color: rgba(203,184,157,0.3);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #cbb89d;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #2a2a2a;
|
||||||
|
--video-info-text-color: #e6dfd5;
|
||||||
|
--video-info-heading-color: #f5f0e6;
|
||||||
|
--video-info-list-color: #d8cfc2;
|
||||||
|
--video-info-list-strong-color: #ffffff;
|
||||||
|
--video-info-link-color: #cbb89d;
|
||||||
|
--video-info-link-hover-color: #b7a588;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #4f4f4f;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: #2a2a2a;
|
||||||
|
--settings-button-text-color: var(--default-text-color);
|
||||||
|
|
||||||
|
--settings-bg-primary-color: #2a2a2a;
|
||||||
|
--settings-bg-secondary-color: #313131;
|
||||||
|
--settings-text-color: var(--default-text-color);
|
||||||
|
--settings-subtitle-color: #7a7979;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.nf .download-path {
|
||||||
|
color: var(--default-text-color);
|
||||||
|
}
|
||||||
58
public/styles/theme/songbird-theme.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
body.songbird {
|
||||||
|
|
||||||
|
background-image: url('../../assets/images/Songbird.webp');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #0a0a0a; /* noir profond */
|
||||||
|
--default-text-color: #ff2a2a; /* rouge néon */
|
||||||
|
--subtitle-color: #f2f2f2; /* rouge sombre */
|
||||||
|
--mp3-text-color: #ff4d4d; /* rouge un peu plus clair */
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: rgba(20,0,0,0.85); /* noir semi-transparent avec rouge */
|
||||||
|
--form-input-bg-color: #1a0a0a;
|
||||||
|
--form-input-border-color: #ff2a2a;
|
||||||
|
--form-input-border-focus-color: #b30000;
|
||||||
|
--form-input-text-color: #ffffff;
|
||||||
|
--form-input-placeholder-color: #950404;
|
||||||
|
--form-button-bg-color: #b30000;
|
||||||
|
--form-button-text-color: #ffffff;
|
||||||
|
--form-button-bg-hover-color: #ff2a2a;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #630000;
|
||||||
|
--checkbox-bg-checked: #ff2a2a;
|
||||||
|
--checkbox-checkmark-border-color: #ffffff;
|
||||||
|
--checkbox-pulse-color: rgba(255,42,42,0.3);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #ff2a2a;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: rgba(30,0,0,0.9);
|
||||||
|
--video-info-text-color: #ff4d4d;
|
||||||
|
--video-info-heading-color: #ff2a2a;
|
||||||
|
--video-info-list-color: #b30000;
|
||||||
|
--video-info-list-strong-color: #ffffff;
|
||||||
|
--video-info-link-color: #ff2a2a;
|
||||||
|
--video-info-link-hover-color: #b30000;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #3f0000;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: rgba(29, 0, 0, 0.784);
|
||||||
|
--settings-button-text-color: var(--default-text-color);
|
||||||
|
|
||||||
|
--settings-bg-primary-color: #121212;
|
||||||
|
--settings-bg-secondary-color: #1a1a1a;
|
||||||
|
--settings-text-color: var(--default-text-color);
|
||||||
|
--settings-subtitle-color: #7a7979;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.cyberpunk .download-path {
|
||||||
|
color: var(--default-text-color);
|
||||||
|
}
|
||||||
59
public/styles/theme/spicy-theme.css
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
body.spicy {
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #121212; /* fond noir profond */
|
||||||
|
--default-text-color: #ffffff; /* texte clair */
|
||||||
|
--subtitle-color: #f87918; /* accent rouge piment */
|
||||||
|
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #1a1a1a; /* fond formulaire gris très foncé */
|
||||||
|
--form-input-bg-color: #2b2b2b; /* fond inputs gris foncé */
|
||||||
|
--form-input-border-color: #444444; /* bordure gris */
|
||||||
|
--form-input-border-focus-color: #ff1a1a; /* focus rouge piment */
|
||||||
|
--form-input-text-color: #eeeeee; /* texte inputs */
|
||||||
|
--form-input-placeholder-color: #888888; /* placeholder gris moyen */
|
||||||
|
--form-button-bg-color: #ff1a1a; /* bouton rouge vif */
|
||||||
|
--form-button-text-color: #ffffff; /* texte bouton blanc */
|
||||||
|
--form-button-bg-hover-color: #cc0000; /* hover rouge foncé */
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #555555; /* gris moyen */
|
||||||
|
--checkbox-bg-checked: #ff1a1a; /* rouge piment */
|
||||||
|
--checkbox-checkmark-border-color: #f5f5f5; /* blanc cassé */
|
||||||
|
--checkbox-pulse-color: rgba(255, 26, 26, 0.5); /* pulse rouge translucide */
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #ffffff; /* rouge vif */
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #1a1a1a; /* fond infos gris foncé */
|
||||||
|
--video-info-text-color: #f5f5f5; /* texte clair */
|
||||||
|
--video-info-heading-color: #ff1a1a; /* titres rouges */
|
||||||
|
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
||||||
|
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
||||||
|
--video-info-link-color: #ff1a1a; /* liens rouges */
|
||||||
|
--video-info-link-hover-color: #cc0000; /* liens hover rouge foncé */
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #353232;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: #1a1a1a;
|
||||||
|
--settings-button-text-color: #dcdcdc;
|
||||||
|
|
||||||
|
--settings-bg-primary-color: #1a1a1a;
|
||||||
|
--settings-bg-secondary-color: #292929;
|
||||||
|
|
||||||
|
--settings-text-color: #cecece;
|
||||||
|
--settings-subtitle-color: #adadad;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optionnel: background piments */
|
||||||
|
body.spicy {
|
||||||
|
background-image: url('../../assets/images/Spicy.png');
|
||||||
|
background-size:cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
9
public/styles/theme/themeimport.css
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@import url("chirac-theme.css");
|
||||||
|
@import url("dark-theme.css");
|
||||||
|
@import url("fanatic-theme.css");
|
||||||
|
@import url("light-theme.css");
|
||||||
|
@import url("nf-theme.css");
|
||||||
|
@import url("drift-theme.css");
|
||||||
|
@import url("neon-theme.css");
|
||||||
|
@import url("songbird-theme.css");
|
||||||
|
@import url("spicy-theme.css")
|
||||||
46
public/styles/variables.css
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
:root {
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #001224;
|
||||||
|
--default-text-color: #eee;
|
||||||
|
--subtitle-color: #007bff;
|
||||||
|
--mp3-text-color: #000000;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #ffffff;
|
||||||
|
--form-input-bg-color: #f9f9f9;
|
||||||
|
--form-input-border-color: #cccccc;
|
||||||
|
--form-input-border-focus-color: #0056b3;
|
||||||
|
--form-input-text-color: #333333;
|
||||||
|
--form-input-placeholder-color: #aaaaaa;
|
||||||
|
--form-button-bg-color: #007bff;
|
||||||
|
--form-button-text-color: #ffffff;
|
||||||
|
--form-button-bg-hover-color: #0056b3;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #cccccc;
|
||||||
|
--checkbox-bg-checked: #3B82F6;
|
||||||
|
--checkbox-checkmark-border-color: #E0E0E2;
|
||||||
|
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #007bff;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #f0f0f0;
|
||||||
|
--video-info-text-color: #222222;
|
||||||
|
--video-info-heading-color: #007bff;
|
||||||
|
--video-info-list-color: #444444;
|
||||||
|
--video-info-list-strong-color: #000000;
|
||||||
|
--video-info-link-color: #007bff;
|
||||||
|
--video-info-link-hover-color: #0056b3;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: var(--form-bg-color);
|
||||||
|
--settings-button-text-color: var(--default-text-color);
|
||||||
|
|
||||||
|
--settings-bg-primary-color: var(--form-bg-color);
|
||||||
|
--settings-bg-secondary-color: var(--form-bg-color);
|
||||||
|
--settings-text-color: var(--default-text-color);
|
||||||
|
--settings-subtitle-color: #aaa;
|
||||||
|
|
||||||
|
}
|
||||||
BIN
ressources/yt-dlp.exe
Normal file
64
server/controller/download.controller.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const { fetchDownload } = require("../services/download.services");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
const { isValidUrl, isSafePath } = require("../helpers/validation");
|
||||||
|
const { notifyDownloadFinished } = require("../helpers/notify");
|
||||||
|
|
||||||
|
const listeners = [];
|
||||||
|
const speedListeners = [];
|
||||||
|
|
||||||
|
async function downloadController(req, res) {
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
url: req.body.url,
|
||||||
|
audioOnly: req.body.audioOnly === "1",
|
||||||
|
quality: req.body.quality || "best",
|
||||||
|
outputFolder: req.body.savePath || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !");
|
||||||
|
if (options.outputFolder && !isSafePath(options.outputFolder)) return res.status(400).send("❌ Save Path Not Allowed.");
|
||||||
|
|
||||||
|
const filePath = await fetchDownload(options, listeners, speedListeners);
|
||||||
|
notifyDownloadFinished(filePath);
|
||||||
|
res.send("✅ Download Done !");
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Server Error in /download : ${err.message}`);
|
||||||
|
res.status(500).send(`❌ Server Error`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function progressController(req, res) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendProgress = percent => res.write(`data: ${percent}\n\n`);
|
||||||
|
listeners.push(sendProgress);
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
listeners.splice(listeners.indexOf(sendProgress), 1);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function speedController(req, res) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendSpeed = speed => res.write(`data: ${speed}\n\n`);
|
||||||
|
speedListeners.push(sendSpeed);
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
speedListeners.splice(speedListeners.indexOf(sendSpeed), 1);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { downloadController, progressController, speedController};
|
||||||
46
server/controller/info.controller.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const { fetchInfo } = require("../services/info.services");
|
||||||
|
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
const { isValidUrl } = require("../helpers/validation");
|
||||||
|
|
||||||
|
async function infoController(req, res){
|
||||||
|
|
||||||
|
const url = req.body.url || req.query.url; // Gérer POST et GET
|
||||||
|
|
||||||
|
|
||||||
|
/* Si pas d'url ou url invalide*/
|
||||||
|
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("❌ Invalid URL Or Missing");
|
||||||
|
|
||||||
|
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
|
||||||
|
|
||||||
|
if (url.includes("?list")) {
|
||||||
|
logger.info("Estimated Data Type: Playlist")
|
||||||
|
|
||||||
|
} else{
|
||||||
|
logger.info("Estimated Data Type: Video")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchInfo(url);
|
||||||
|
|
||||||
|
if (data._type === "playlist") {
|
||||||
|
|
||||||
|
const playlist = parsePlaylist(data);
|
||||||
|
logger.info(`Playlist : ${playlist.title} (${playlist.count} vidéos)`);
|
||||||
|
return res.json(playlist);
|
||||||
|
|
||||||
|
} else{
|
||||||
|
const video = parseVideo(data);
|
||||||
|
logger.info(`Unique Video: ${video.title}`);
|
||||||
|
return res.json(video);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
|
||||||
|
return res.status(500).send(`❌ Unable to fetch info`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {infoController};
|
||||||
52
server/discordRPC.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const config = require('../config');
|
||||||
|
const RPC = require("discord-rpc");
|
||||||
|
const { logger } = require("./logger");
|
||||||
|
|
||||||
|
const clientId = `${config.DiscordRPCID}`;
|
||||||
|
const rpc = new RPC.Client({ transport: "ipc" });
|
||||||
|
|
||||||
|
let intervalId;
|
||||||
|
|
||||||
|
function startRPC() {
|
||||||
|
rpc.on("ready", () => {
|
||||||
|
const presence = {
|
||||||
|
largeImageKey: "icon",
|
||||||
|
smallImageKey: "acnolo_pfp",
|
||||||
|
smallImageText: "By MasterAcnolo",
|
||||||
|
startTimestamp: new Date(),
|
||||||
|
details: `Open Source Download Tools - ${config.version}`,
|
||||||
|
state: "masteracnolo.github.io/FreedomLoader",
|
||||||
|
};
|
||||||
|
|
||||||
|
rpc.setActivity(presence);
|
||||||
|
|
||||||
|
// Met à jour la présence toutes les 15s
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
rpc.setActivity(presence);
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function cleanExit() {
|
||||||
|
try {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
|
||||||
|
if (rpc && rpc.transport) {
|
||||||
|
await rpc.destroy();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Error while closing the RPC:", err);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("exit", cleanExit);
|
||||||
|
process.on("SIGINT", cleanExit);
|
||||||
|
process.on("SIGTERM", cleanExit);
|
||||||
|
|
||||||
|
rpc.login({ clientId }).catch(err => {
|
||||||
|
logger.error("Unable to connect to the RPC:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { startRPC };
|
||||||
85
server/helpers/buildArgs.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const getUserBrowser = require("./getBrowser");
|
||||||
|
const { ffmpegPath, denoPath} = require("./path");
|
||||||
|
const { configFeatures } = require("../../config.js");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
|
||||||
|
function validateCodec(codec){
|
||||||
|
|
||||||
|
const validCodec = [
|
||||||
|
"av01","vp9.2", "vp9" , "h265" , "h264" , "vp8" , "h263", "theora"
|
||||||
|
]
|
||||||
|
|
||||||
|
if(validCodec.includes(codec)){
|
||||||
|
logger.info(`Codec Valid: ${codec}`)
|
||||||
|
return codec
|
||||||
|
} else{
|
||||||
|
logger.error(`Codec not valid: ${codec}. Using default codec`)
|
||||||
|
return "h264"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||||
|
|
||||||
|
logger.info("--- CONFIGURATION ---");
|
||||||
|
|
||||||
|
logger.info(`CONFIG autoUpdate: ${configFeatures.autoUpdate}`);
|
||||||
|
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
||||||
|
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
||||||
|
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
||||||
|
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
||||||
|
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
||||||
|
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
||||||
|
logger.info(`CONFIG autoDownloadPlaylist: ${configFeatures.autoDownloadPlaylist}`);
|
||||||
|
logger.info(`CONFIG customCodec: ${configFeatures.customCodec}`);
|
||||||
|
|
||||||
|
// ACTIVATE THEM WHEN THE FEATURE IS IMPLEMENTED
|
||||||
|
// logger.info(`CONFIG logSystem: ${configFeatures.logSystem}`);
|
||||||
|
// logger.info(`CONFIG outputTitleCheck: ${configFeatures.outputTitleCheck}`);
|
||||||
|
// logger.info(`CONFIG downloadSystem: ${configFeatures.downloadSystem}`);
|
||||||
|
// logger.info(`CONFIG notifySystem: ${configFeatures.notifySystem}`);
|
||||||
|
|
||||||
|
logger.info("---------------------");
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
configFeatures.verboseLogs ? "--verbose" : null, // Verbose Logs
|
||||||
|
"--cookies-from-browser", `${getUserBrowser()}`,
|
||||||
|
"--no-continue",
|
||||||
|
"--no-overwrites",
|
||||||
|
"--newline", // YT-DLP Logs Format
|
||||||
|
configFeatures.addThumbnail ? "--embed-thumbnail" : null,
|
||||||
|
configFeatures.addMetadata ? "--add-metadata" : null,
|
||||||
|
"--concurrent-fragments", "8",
|
||||||
|
"--retries", "10",
|
||||||
|
"--fragment-retries", "10",
|
||||||
|
"--ffmpeg-location", ffmpegPath,
|
||||||
|
"--extractor-args","youtube:player_client=default",
|
||||||
|
"--js-runtimes", `deno:${denoPath}`,
|
||||||
|
"-S",
|
||||||
|
`vcodec:${validateCodec(configFeatures.customCodec) || "h264"}`+
|
||||||
|
`,acodec:aac`,
|
||||||
|
configFeatures.autoDownloadPlaylist ? "--yes-playlist" : "--no-playlist"
|
||||||
|
];
|
||||||
|
|
||||||
|
if (audioOnly) args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3");
|
||||||
|
else args.push("--merge-output", "mp4");
|
||||||
|
|
||||||
|
const qualityMap = {
|
||||||
|
best: "bestvideo+bestaudio/best/mp4",
|
||||||
|
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||||
|
worst: "worstvideo+worstaudio/worst/mp4",
|
||||||
|
1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]/mp4",
|
||||||
|
720: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||||
|
480: "bestvideo[height<=480]+bestaudio/best[height<=480]/mp4",
|
||||||
|
};
|
||||||
|
|
||||||
|
args.push("-f", qualityMap[quality] || "best");
|
||||||
|
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
|
||||||
|
args.push(url);
|
||||||
|
|
||||||
|
return args.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { buildYtDlpArgs };
|
||||||
|
|
||||||
32
server/helpers/getBrowser.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const notify = require("./notify")
|
||||||
|
|
||||||
|
function getUserBrowser() {
|
||||||
|
const userProfile = os.homedir();
|
||||||
|
|
||||||
|
// Liste des navigateurs supportés par yt-dlp (Actuellement je peux que garantir Firefox, Désolé)
|
||||||
|
const browsers = [
|
||||||
|
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
||||||
|
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
||||||
|
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
|
||||||
|
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
|
||||||
|
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
|
||||||
|
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
|
||||||
|
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
|
||||||
|
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const browser of browsers) {
|
||||||
|
if (fs.existsSync(browser.path)) {
|
||||||
|
return browser.name;
|
||||||
|
} else{
|
||||||
|
notify.notifyFirefoxBrowserMissing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notify.notifyCookiesBrowserError()
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getUserBrowser
|
||||||
44
server/helpers/notify.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const { Notification, shell } = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
function notifyDownloadFinished(folder) {
|
||||||
|
const iconPath = path.join(process.resourcesPath, "confirm-icon.png");
|
||||||
|
const notif = new Notification({
|
||||||
|
title: "Freedom Loader",
|
||||||
|
body: "Your download is complete, click here to open it.",
|
||||||
|
icon: iconPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("click", () => shell.openPath(folder));
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyCookiesBrowserError(){
|
||||||
|
const iconPath = path.join(process.resourcesPath, "error.png");
|
||||||
|
const notif = new Notification({
|
||||||
|
title: "Cookies Error",
|
||||||
|
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
||||||
|
icon: iconPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyFirefoxBrowserMissing(){
|
||||||
|
const iconPath = path.join(process.ressourcesPath, "error.png");
|
||||||
|
const notif = new Notification({
|
||||||
|
title: "Firefox Missing",
|
||||||
|
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
||||||
|
icon: iconPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
notifyDownloadFinished,
|
||||||
|
notifyCookiesBrowserError,
|
||||||
|
notifyFirefoxBrowserMissing
|
||||||
|
};
|
||||||
33
server/helpers/parseInfo.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
function parseVideo(data) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "video",
|
||||||
|
// id: data.id,
|
||||||
|
// title: data.title,
|
||||||
|
// url: data.webpage_url,
|
||||||
|
// duration: data.duration,
|
||||||
|
// thumbnail: data.thumbnail,
|
||||||
|
// uploader: data.uploader
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePlaylist(data) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "playlist",
|
||||||
|
title: data.title || data.id,
|
||||||
|
channel: data.uploader,
|
||||||
|
count: data.entries.length,
|
||||||
|
videos: (data.entries || []).map(v => ({
|
||||||
|
id: v.id,
|
||||||
|
title: v.title,
|
||||||
|
url: v.webpage_url,
|
||||||
|
duration: v.duration,
|
||||||
|
thumbnail: v.thumbnail,
|
||||||
|
uploader: v.uploader
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parseVideo, parsePlaylist };
|
||||||
33
server/helpers/path.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { app } = require("electron");
|
||||||
|
const config = require("../../config");
|
||||||
|
|
||||||
|
const { logger } = require("../logger.js");
|
||||||
|
|
||||||
|
let userYtDlp;
|
||||||
|
let ffmpegPath;
|
||||||
|
let denoPath;
|
||||||
|
|
||||||
|
const sourceYtDlp = path.join(process.resourcesPath, "binaries","yt-dlp.exe");
|
||||||
|
|
||||||
|
if (config.localMode) {
|
||||||
|
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
|
||||||
|
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- contient ffmpeg.exe et ffprobe.exe
|
||||||
|
denoPath = path.join(__dirname, "../../ressources/deno.exe");
|
||||||
|
} else {
|
||||||
|
userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe");
|
||||||
|
ffmpegPath = path.join(process.resourcesPath, "binaries","ffmpeg.exe");
|
||||||
|
denoPath = path.join(process.resourcesPath, "binaries","deno.exe");
|
||||||
|
|
||||||
|
if (!fs.existsSync(userYtDlp)) {
|
||||||
|
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userYtDlp){ logger.error("Missing YT-DLP")}
|
||||||
|
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
|
||||||
|
if (!denoPath){ logger.error("Missing DENO")}
|
||||||
|
|
||||||
|
module.exports = { userYtDlp, ffmpegPath, denoPath };
|
||||||
14
server/helpers/rateLimit.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const RateLimit = require("express-rate-limit");
|
||||||
|
|
||||||
|
|
||||||
|
/* Rate Limite */
|
||||||
|
const rateLimite = RateLimit({
|
||||||
|
windowMs: 15 * 60, // 15 minutes
|
||||||
|
max: 5, // limit each IP to 100 requests per windowMs on the root path
|
||||||
|
message: "Too many requests, please try again later.",
|
||||||
|
statusCode: 429, // HTTP status code for "Too Many Requests"
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
rateLimite
|
||||||
|
}
|
||||||
21
server/helpers/validation.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
function isValidUrl(url) {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSafePath(folder) {
|
||||||
|
if (!folder || folder.length < 3) return false;
|
||||||
|
const unsafe = ["System32", "/etc", "\\Windows"];
|
||||||
|
const resolved = path.resolve(folder);
|
||||||
|
return !unsafe.some(u => resolved.includes(u));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { isValidUrl, isSafePath };
|
||||||
61
server/logger.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const { createLogger, format, transports } = require("winston");
|
||||||
|
const DailyRotateFile = require("winston-daily-rotate-file");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
const config = require("../config")
|
||||||
|
|
||||||
|
// Dossier de logs Windows
|
||||||
|
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||||
|
|
||||||
|
// Création du dossier "logs" si nécessaire
|
||||||
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
|
|
||||||
|
const logFormat = format.combine(
|
||||||
|
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
|
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Configuration du logger
|
||||||
|
const logger = createLogger({
|
||||||
|
level: "info",
|
||||||
|
format: logFormat,
|
||||||
|
transports: [
|
||||||
|
new DailyRotateFile({
|
||||||
|
dirname: logDir,
|
||||||
|
filename: "LOGS-%DATE%.log",
|
||||||
|
datePattern: "YYYY-MM-DD",
|
||||||
|
zippedArchive: false,
|
||||||
|
maxFiles: "7d",
|
||||||
|
format: logFormat,
|
||||||
|
options: { flags: "a" },
|
||||||
|
}),
|
||||||
|
new transports.Console({
|
||||||
|
format: logFormat,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSessionStartLine() {
|
||||||
|
return `--- Starting session: ${new Date().toISOString()} ---`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessionEndLine() {
|
||||||
|
return `--- Ending session: ${new Date().toISOString()} ---`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logSessionStart() {
|
||||||
|
logger.info(getSessionStartLine());
|
||||||
|
logger.info(`Application Version: ${config.version}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function logSessionEnd() {
|
||||||
|
logger.info(getSessionEndLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
logger,
|
||||||
|
logSessionStart,
|
||||||
|
logSessionEnd,
|
||||||
|
logDir,
|
||||||
|
};
|
||||||
11
server/routes/download.route.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { downloadController, progressController, speedController } = require("../controller/download.controller");
|
||||||
|
|
||||||
|
router.post("/", downloadController);
|
||||||
|
|
||||||
|
// SSE pour la progression du téléchargement
|
||||||
|
router.get("/progress", progressController);
|
||||||
|
router.get("/speed", speedController);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
7
server/routes/info.route.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { infoController } = require("../controller/info.controller");
|
||||||
|
|
||||||
|
router.post("/", infoController);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
70
server/server.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { logger, logSessionEnd } = require("./logger");
|
||||||
|
const config = require("../config");
|
||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { userYtDlp } = require("./helpers/path");
|
||||||
|
const { rateLimite } = require("./helpers/rateLimit")
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Dossier de téléchargement
|
||||||
|
const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||||
|
|
||||||
|
// Création du dossier si nécessaire
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(outputFolder, { recursive: true });
|
||||||
|
logger.info(`Freedom Loader folder ready in ${outputFolder}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Unable to create folder:", err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour yt-dlp au
|
||||||
|
execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
|
||||||
|
if (err) logger.warn("yt-dlp update error:", err);
|
||||||
|
else logger.info(`Update yt-dlp : ${stdout}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middlewares
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(express.static(path.join(__dirname, "../public")));
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use("/download", require("./routes/download.route"));
|
||||||
|
app.use("/info", require("./routes/info.route"));
|
||||||
|
|
||||||
|
app.get("/", rateLimite ,(req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fonction pour démarrer le serveur
|
||||||
|
async function startServer() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = app.listen(config.applicationPort, () => {
|
||||||
|
logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
|
||||||
|
resolve(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("error", (err) => {
|
||||||
|
logger.error("Express Server Error :", err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const gracefulExit = () => {
|
||||||
|
logSessionEnd();
|
||||||
|
server.close(() => {
|
||||||
|
logger.info("Express Server close cleanly.");
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGINT", gracefulExit);
|
||||||
|
process.on("SIGTERM", gracefulExit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { startServer };
|
||||||
62
server/services/download.services.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { userYtDlp } = require("../helpers/path");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
||||||
|
const notify = require("../helpers/notify")
|
||||||
|
|
||||||
|
function fetchDownload(options, listeners, speedListeners) {
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||||
|
fs.mkdirSync(outputFolder, { recursive: true });
|
||||||
|
|
||||||
|
const args = buildYtDlpArgs({ ...options, outputFolder });
|
||||||
|
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||||
|
|
||||||
|
const child = execFile(userYtDlp, args);
|
||||||
|
|
||||||
|
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
|
||||||
|
|
||||||
|
child.on("close", code => {
|
||||||
|
listeners.forEach(fn => fn("done"));
|
||||||
|
if (code === 0) resolve(outputFolder);
|
||||||
|
else reject(new Error(`YT-DLP failed with code : ${code}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
child.stdout.on("data", data => {
|
||||||
|
data.toString().split("\n").forEach(line => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
logger.info(`[yt-dlp] ${line}`);
|
||||||
|
|
||||||
|
/* Barre de Chargement*/
|
||||||
|
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
|
||||||
|
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
||||||
|
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
|
||||||
|
|
||||||
|
if (line.includes("MiB/s") || line.includes("KiB/s")) {
|
||||||
|
const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/);
|
||||||
|
if (match) {
|
||||||
|
const speed = match[0];
|
||||||
|
speedListeners.forEach(fn => fn(speed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.match(/ERROR: Could not copy .* cookie database/i)) {
|
||||||
|
notify.notifyCookiesBrowserError();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", data => {
|
||||||
|
data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { fetchDownload };
|
||||||
51
server/services/info.services.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { userYtDlp, denoPath } = require("../helpers/path");
|
||||||
|
const getUserBrowser = require("../helpers/getBrowser");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
|
||||||
|
function fetchInfo(url) {
|
||||||
|
const args = [
|
||||||
|
"--js-runtimes", `deno:${denoPath}`,
|
||||||
|
"--dump-single-json",
|
||||||
|
"--ignore-errors",
|
||||||
|
"--yes-playlist",
|
||||||
|
"--cookies-from-browser",`${getUserBrowser()}`,
|
||||||
|
"--extractor-args","youtube:player_client=default",
|
||||||
|
"--ignore-no-formats-error",
|
||||||
|
];
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
execFile(userYtDlp,[...args, url],
|
||||||
|
{ timeout: 30_000, // 30s, si jamais plus, abandon de la requête
|
||||||
|
maxBuffer: 10 * 1024 * 1024 }, // 10MO de réponse max (Par défaut: 200ko)
|
||||||
|
|
||||||
|
(error, stdout, stderr) => {
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
const lower = stderr.toLowerCase();
|
||||||
|
if (lower.includes("sign in to confirm") || lower.includes("failed to decrypt") || lower.includes("could not copy")) {
|
||||||
|
return reject(new Error(`Unable to extract cookies from ${getUserBrowser()}. Please log in to this browser and try again.`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.error(`YT-DLP Error: ${error.message}`);
|
||||||
|
if (stderr) logger.debug(`stderr: ${stderr}`);
|
||||||
|
return reject(new Error("Unable to fetch Info."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stdout) return reject(new Error("No Data Received."));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(stdout);
|
||||||
|
resolve(data);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`JSON Parsing Error: ${e.message}`);
|
||||||
|
reject(new Error("JSON unreadable."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { fetchInfo };
|
||||||
47
server/update.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const { autoUpdater } = require("electron-updater");
|
||||||
|
const { app, Notification, shell} = require("electron");
|
||||||
|
const { logger } = require("./logger");
|
||||||
|
|
||||||
|
function AutoUpdater() {
|
||||||
|
|
||||||
|
autoUpdater.on("update-available", (info) => {
|
||||||
|
logger.info(`New Version Available : ${info.version}`);
|
||||||
|
new Notification({
|
||||||
|
title: "Freedom Loader",
|
||||||
|
body: `New Version Available : ${info.version}. Application will restart`
|
||||||
|
}).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-downloaded", (info) => {
|
||||||
|
logger.info(`Update Downloaded : ${info.version}`);
|
||||||
|
new Notification({
|
||||||
|
title: "Freedom Loader",
|
||||||
|
body: `Update ${info.version} downloaded.`
|
||||||
|
}).on("click", () =>
|
||||||
|
shell.openExternal("https://github.com/MasterAcnolo/Freedom-Loader/releases/latest")
|
||||||
|
).show();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("error", (err) => {
|
||||||
|
logger.error("Auto Update Error :", err.message);
|
||||||
|
new Notification({
|
||||||
|
title: "Freedom Loader - Update Error",
|
||||||
|
body: err.message
|
||||||
|
}).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
try {
|
||||||
|
await autoUpdater.checkForUpdates();
|
||||||
|
logger.info("Update check completed");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Error during update check :", err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { AutoUpdater };
|
||||||