Compare commits
343 Commits
1.1.3
...
4f03c5876f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f03c5876f | ||
|
|
a5f05f32af | ||
|
|
0d1b1e4919 | ||
|
|
0e77a94ec7 | ||
|
|
7f481d20b2 | ||
|
|
1ff32ca021 | ||
|
|
90d8a9f65d | ||
|
|
d5965c2ce0 | ||
|
|
15f59a6093 | ||
|
|
8b31436e25 | ||
|
|
7d5203001b | ||
|
|
60709c4fad | ||
|
|
2fb6509edc | ||
|
|
eebce83976 | ||
|
|
a56a070bc8 | ||
|
|
e1ef7032bb | ||
|
|
e7333b7b6b | ||
|
|
e2b51d32d1 | ||
|
|
81515c6ab8 | ||
|
|
c88189b44f | ||
|
|
b745ebd29f | ||
|
|
13ee4c170b | ||
|
|
0218664c5a | ||
|
|
6afc5e06d6 | ||
|
|
519dcad6e6 | ||
|
|
ac94d68224 | ||
|
|
0f876fcb70 | ||
|
|
a6c11ba7c4 | ||
|
|
44132dd7b5 | ||
|
|
15ab6892b7 | ||
|
|
725b6f0ada | ||
|
|
ad0e4f858b | ||
|
|
68108e3a66 | ||
|
|
eff2affd42 | ||
|
|
87cf05643f | ||
|
|
ce14b8abd7 | ||
|
|
44ed1bbf0e | ||
|
|
e0d017d9fd | ||
|
|
c3ea1ac0bd | ||
|
|
238d4ea717 | ||
|
|
d879a5eb1d | ||
|
|
40bea35286 | ||
|
|
83ac76381a | ||
|
|
6ff0c71be7 | ||
|
|
fd0b339bcc | ||
|
|
bc5faf38b6 | ||
|
|
e744e55475 | ||
|
|
a581c9309d | ||
|
|
61040769ee | ||
|
|
06f6a30797 | ||
|
|
a2653eef57 | ||
|
|
89573d11f7 | ||
|
|
f0c28a7f5e | ||
|
|
f3c37c2498 | ||
|
|
8fe4e7c38a | ||
|
|
912f4042e5 | ||
|
|
5dc61b4a15 | ||
|
|
5fb49b0c9d | ||
|
|
75fdf91fcd | ||
|
|
011e0a703a | ||
|
|
5aebc78d44 | ||
|
|
60137a15dc | ||
|
|
d05b9e2511 | ||
|
|
ebb48f4f8b | ||
|
|
8be47b9821 | ||
|
|
ca8b107f61 | ||
|
|
f25d07cb83 | ||
|
|
49e1e1b507 | ||
|
|
23d11d8933 | ||
|
|
9f88fe19c3 | ||
|
|
9b4b0306c7 | ||
|
|
96401a6e1f | ||
|
|
0c7103c5c9 | ||
|
|
798084e3bf | ||
|
|
5c4e6669fe | ||
|
|
00c542925c | ||
|
|
24460cf0b5 | ||
|
|
a3c1b1d753 | ||
|
|
5ade3fe144 | ||
|
|
577069b471 | ||
|
|
a4da47647a | ||
|
|
c5b29a0647 | ||
|
|
90e1a5fde7 | ||
|
|
dcfea2073c | ||
|
|
0be1ef1710 | ||
|
|
da393b19d9 | ||
|
|
1c27cc19cf | ||
|
|
ff7366c46c | ||
|
|
459d4c6df4 | ||
|
|
d2ca3e99d2 | ||
|
|
8ad3ea55cc | ||
|
|
71dbd104f8 | ||
|
|
57a5defb08 | ||
|
|
25c78ff8b7 | ||
|
|
792a59d8d7 | ||
|
|
b13c98dda2 | ||
|
|
1f103c20b9 | ||
|
|
70d62959ca | ||
|
|
83ca963502 | ||
|
|
9fb9295093 | ||
|
|
74fcdbb738 | ||
|
|
32c9a66a35 | ||
|
|
45e0840fea | ||
|
|
19b3f8b947 | ||
|
|
cc5bde5b48 | ||
|
|
9a48ac05e3 | ||
|
|
5750444655 | ||
|
|
fdb22f2342 | ||
|
|
760550e521 | ||
|
|
334e83ae1f | ||
|
|
5d776594fc | ||
|
|
adb36e3fa2 | ||
|
|
87945fba80 | ||
|
|
2234f9ed81 | ||
|
|
7cd3b4be06 | ||
|
|
a42b020b96 | ||
|
|
c1e70dae80 | ||
|
|
6877c51c0e | ||
|
|
5a41090175 | ||
|
|
5983f21759 | ||
|
|
ee9e02410f | ||
|
|
909480083e | ||
|
|
ca0492ed04 | ||
|
|
6c73dd75be | ||
|
|
556dccec08 | ||
|
|
f720d7c889 | ||
|
|
bb23f37fb5 | ||
|
|
220e81d8b2 | ||
|
|
44d8772827 | ||
|
|
335fb9652b | ||
|
|
243b98cbf9 | ||
|
|
23da75a17b | ||
|
|
756d2c048e | ||
|
|
659c1d4cf9 | ||
|
|
443fd31170 | ||
|
|
6d8095ceb2 | ||
|
|
c18cb6c8aa | ||
|
|
a3c5025434 | ||
|
|
3d7986cb87 | ||
|
|
3f8701567f | ||
|
|
ccb16c49d5 | ||
|
|
86d9ac3592 | ||
|
|
43d87c2b47 | ||
|
|
19aa8ed8fa | ||
|
|
3ddfaa999e | ||
|
|
3d7f3b7481 | ||
|
|
cfe2e17313 | ||
|
|
dedf10c6b3 | ||
|
|
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 | ||
|
|
a27b3555f6 | ||
|
|
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 |
35
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: "Bug Report"
|
||||||
|
about: Report something that doesn't work fine
|
||||||
|
title: "[BUG]"
|
||||||
|
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)
|
||||||
44
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or enhancement
|
||||||
|
title: "[FEATURE] "
|
||||||
|
labels: enhancement
|
||||||
|
assignees: 'MasterAcnolo'
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
|
||||||
|
A clear and concise description of the feature you'd like to see.
|
||||||
|
|
||||||
|
## Problem it Solves
|
||||||
|
|
||||||
|
Describe the problem or limitation this feature would address.
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
Describe how you envision this feature working.
|
||||||
|
|
||||||
|
## Alternative Solutions
|
||||||
|
|
||||||
|
Have you considered any alternative approaches? Describe them here.
|
||||||
|
|
||||||
|
## Use Case
|
||||||
|
|
||||||
|
Provide specific examples of how you would use this feature.
|
||||||
|
|
||||||
|
## Implementation Considerations
|
||||||
|
|
||||||
|
Are there any technical challenges or dependencies you're aware of?
|
||||||
|
|
||||||
|
## Priority
|
||||||
|
|
||||||
|
How important is this feature to you?
|
||||||
|
- [ ] Critical - Blocking my workflow
|
||||||
|
- [ ] High - Would significantly improve my experience
|
||||||
|
- [ ] Medium - Nice to have
|
||||||
|
- [ ] Low - Minor improvement
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
Add any other context, mockups, or screenshots about the feature request here.
|
||||||
56
.github/ISSUE_TEMPLATE/performance-issue.md
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
name: Performance Issue
|
||||||
|
about: Report slow downloads, high CPU/memory usage, or crashes
|
||||||
|
title: "[PERFORMANCE] "
|
||||||
|
labels: performance
|
||||||
|
assignees: MasterAcnolo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Issue Type
|
||||||
|
|
||||||
|
- [ ] Slow download speed
|
||||||
|
- [ ] High CPU usage
|
||||||
|
- [ ] High memory usage
|
||||||
|
- [ ] Application freeze/hang
|
||||||
|
- [ ] Application crash
|
||||||
|
- [ ] Other (specify below)
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Clear description of the performance issue.
|
||||||
|
|
||||||
|
## System Information
|
||||||
|
|
||||||
|
- **Freedom Loader Version**: [e.g. 1.4.1]
|
||||||
|
- **Windows Version**: [e.g. Windows 11 22H2]
|
||||||
|
- **CPU**: [e.g. Intel i7-9700K]
|
||||||
|
- **RAM**: [e.g. 16GB]
|
||||||
|
- **Storage**: [e.g. SSD/HDD]
|
||||||
|
- **Other Applications Opened**: [e.g. Discord/Opera GX]
|
||||||
|
|
||||||
|
## Reproduction Details
|
||||||
|
|
||||||
|
- **Download URL**: [if applicable]
|
||||||
|
- **File Size/Length**: [e.g. 2GB / 2 hours]
|
||||||
|
- **Quality Setting**: [e.g. 1080p, Best]
|
||||||
|
- **Audio Only**: Yes / No
|
||||||
|
|
||||||
|
## Expected Performance
|
||||||
|
|
||||||
|
What performance did you expect?
|
||||||
|
|
||||||
|
## Actual Performance
|
||||||
|
|
||||||
|
What performance are you experiencing? (Include metrics if possible: download speed, CPU %, memory usage)
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
Attach logs with verbose mode enabled if possible:
|
||||||
|
```
|
||||||
|
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
Any other relevant information (network speed, antivirus software, other running applications).
|
||||||
30
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: Question or Support
|
||||||
|
about: Ask a question about using Freedom Loader
|
||||||
|
title: "[QUESTION] "
|
||||||
|
labels: question
|
||||||
|
assignees: 'MasterAcnolo'
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-submission Checklist
|
||||||
|
|
||||||
|
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/pages/faq)
|
||||||
|
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/pages/wiki)
|
||||||
|
- [ ] I have searched existing issues
|
||||||
|
|
||||||
|
## Question
|
||||||
|
|
||||||
|
What would you like to know?
|
||||||
|
|
||||||
|
## What I've Tried
|
||||||
|
|
||||||
|
Describe what you've already attempted to solve this yourself.
|
||||||
|
|
||||||
|
## Environment (if relevant)
|
||||||
|
|
||||||
|
- **Freedom Loader Version**: [e.g. 1.4.1]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
Any other information that might be helpful.
|
||||||
60
.github/PULL_REQUEST_TEMPLATE/contribution.md
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
Brief description of what this PR does.
|
||||||
|
|
||||||
|
## Related Issue
|
||||||
|
|
||||||
|
Fixes #(issue number)
|
||||||
|
Closes #(issue number)
|
||||||
|
Related to #(issue number)
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] Code refactoring (no functional changes)
|
||||||
|
- [ ] Documentation update
|
||||||
|
- [ ] Performance improvement
|
||||||
|
- [ ] UI/UX enhancement
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
- Change 1
|
||||||
|
- Change 2
|
||||||
|
- Change 3
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- [ ] I have tested this code locally
|
||||||
|
- [ ] I have tested with different download scenarios (video/audio, playlists)
|
||||||
|
- [ ] I have checked the application logs for errors
|
||||||
|
- [ ] I have verified the UI changes (if applicable)
|
||||||
|
- [ ] I have tested on Windows 10 and/or Windows 11
|
||||||
|
|
||||||
|
## Screenshots (if applicable)
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
|
||||||
|
[Add screenshot]
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
|
||||||
|
[Add screenshot]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] My code follows the project's code style
|
||||||
|
- [ ] I have performed a self-review of my code
|
||||||
|
- [ ] I have commented my code where necessary
|
||||||
|
- [ ] I have updated the documentation (if applicable)
|
||||||
|
- [ ] My changes generate no new warnings or errors
|
||||||
|
- [ ] I have not modified the version number (this will be done during release)
|
||||||
|
|
||||||
|
## Additional Notes
|
||||||
|
|
||||||
|
Any additional information or context about the PR.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note for Maintainer:** This is a contribution PR and should NOT include version bumps.
|
||||||
10
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Freedom Loader - 1.X.Y
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Found a bug or issue?
|
||||||
|
- Please report it in the GitHub Issues section
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.W.X...1.Y.Z
|
||||||
25
.gitignore
vendored
@@ -1,6 +1,23 @@
|
|||||||
|
# Dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/downloads
|
|
||||||
|
# Build Output Folder
|
||||||
/dist
|
/dist
|
||||||
logs/*.json
|
/out
|
||||||
logs/*.log
|
|
||||||
/out
|
# Config JSON File. Create it and fill it if you want to have dev only settings
|
||||||
|
config/config.dev.json
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
|
||||||
|
# Linux executable
|
||||||
|
ffmpeg
|
||||||
|
ffprobe
|
||||||
|
deno
|
||||||
|
|
||||||
|
# Windows Executable
|
||||||
|
ffmpeg.exe
|
||||||
|
ffprobe.exe
|
||||||
|
deno.exe
|
||||||
96
BINARIES.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# External binaries
|
||||||
|
|
||||||
|
This project depends on three external tools that are **not bundled in the repository**.
|
||||||
|
You must download them manually and place each binary in the correct folder before building or running the app locally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required tools
|
||||||
|
|
||||||
|
| Tool | Role |
|
||||||
|
|---------|---------------------------------------------|
|
||||||
|
| yt-dlp | Video and audio download engine |
|
||||||
|
| ffmpeg | Media encoding, transcoding, muxing |
|
||||||
|
| ffprobe | Media file analysis and metadata extraction |
|
||||||
|
| deno | JavaScript / TypeScript runtime |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Folder structure
|
||||||
|
|
||||||
|
Place the binaries exactly as shown below.
|
||||||
|
Do not rename them — the application resolves them by these exact names.
|
||||||
|
|
||||||
|
```
|
||||||
|
resources/
|
||||||
|
└── binaries/
|
||||||
|
├── linux/
|
||||||
|
│ ├── yt-dlp
|
||||||
|
│ ├── ffmpeg
|
||||||
|
│ ├── ffprobe
|
||||||
|
│ └── deno
|
||||||
|
└── win-32/
|
||||||
|
├── yt-dlp.exe
|
||||||
|
├── ffmpeg.exe
|
||||||
|
├── ffprobe.exe
|
||||||
|
└── deno.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Download links
|
||||||
|
|
||||||
|
### yt-dlp
|
||||||
|
|
||||||
|
| Platform | URL |
|
||||||
|
|----------|----------------------------------------------------------------------|
|
||||||
|
| Windows | https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe |
|
||||||
|
| Linux | https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp |
|
||||||
|
|
||||||
|
Rename the downloaded file to `yt-dlp` (or `yt-dlp.exe` on Windows).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ffmpeg and ffprobe
|
||||||
|
|
||||||
|
**Windows and Linux** — both binaries are included in the same archive.
|
||||||
|
|
||||||
|
| Platform | URL |
|
||||||
|
|-------------|-----------------------------------------------------------------------------------------------------------|
|
||||||
|
| Windows x64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip |
|
||||||
|
| Linux x64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz |
|
||||||
|
| Linux arm64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz |
|
||||||
|
|
||||||
|
Extract the archive and copy `ffmpeg` and `ffprobe` from the `bin/` subfolder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### deno
|
||||||
|
|
||||||
|
| Platform | URL |
|
||||||
|
|-------------|----------------------------------------------------------------------------------------------|
|
||||||
|
| Windows x64 | https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip |
|
||||||
|
| Linux x64 | https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip |
|
||||||
|
| Linux arm64 | https://github.com/denoland/deno/releases/latest/download/deno-aarch64-unknown-linux-gnu.zip |
|
||||||
|
|
||||||
|
Each archive contains a single binary. Rename it to `deno` (or `deno.exe` on Windows).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permissions (Linux )
|
||||||
|
|
||||||
|
After placing the binaries, make them executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x resources/binaries/linux/yt-dlp
|
||||||
|
chmod +x resources/binaries/linux/ffmpeg
|
||||||
|
chmod +x resources/binaries/linux/ffprobe
|
||||||
|
chmod +x resources/binaries/linux/deno
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- These binaries are excluded from version control via `.gitignore`.
|
||||||
|
- Always use the latest stable release of each tool.
|
||||||
111
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in the Freedom Loader 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
|
||||||
|
* Supporting fellow contributors and helping newcomers feel welcome
|
||||||
|
|
||||||
|
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
|
||||||
|
* Sustained disruption of discussions, issues, or pull requests
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders and project maintainers 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, including:
|
||||||
|
|
||||||
|
- GitHub repository (issues, pull requests, discussions, wiki)
|
||||||
|
- Project documentation and website
|
||||||
|
- Official communication channels (Discord, email, etc.)
|
||||||
|
- Social media accounts representing the project
|
||||||
|
- Project events and meetings (online or offline)
|
||||||
|
|
||||||
|
This Code of Conduct also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email 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.
|
||||||
|
|
||||||
|
## Our Commitment to Contributors
|
||||||
|
|
||||||
|
At Freedom Loader, we value every contribution—whether it's a bug report, feature suggestion, code improvement, or documentation fix. We strive to:
|
||||||
|
|
||||||
|
- Respond to issues and pull requests in a timely manner
|
||||||
|
- Provide constructive feedback on contributions
|
||||||
|
- Help new contributors get started
|
||||||
|
- Maintain a welcoming environment for all skill levels
|
||||||
|
- Credit contributors appropriately for their work
|
||||||
|
|
||||||
|
## Questions and Clarifications
|
||||||
|
|
||||||
|
If you have questions about this Code of Conduct or need clarification on what constitutes acceptable behavior, please:
|
||||||
|
|
||||||
|
- Open a [discussion](https://github.com/MasterAcnolo/Freedom-Loader/discussions) on GitHub
|
||||||
|
- Contact the maintainers at masteracnolo25@gmail.com
|
||||||
|
- Review the [Contributing Guidelines](CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), 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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: Freedom Loader is built by people who care. Let's keep this community a place where everyone feels welcome to contribute.
|
||||||
164
CONTRIBUTING.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Contributing to Freedom Loader
|
||||||
|
|
||||||
|
Welcome! Thanks for your interest in Freedom Loader. Your contributions—bug reports, feature ideas, or code improvements—are always appreciated. Every contribution makes Freedom Loader better.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
|
- [Issue Templates](.github/ISSUE_TEMPLATE/)
|
||||||
|
- [Pull Request Templates](.github/PULL_REQUEST_TEMPLATE/)
|
||||||
|
- [Security Policy](SECURITY.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ways to Contribute
|
||||||
|
|
||||||
|
### 1. Report a Bug
|
||||||
|
|
||||||
|
Found a bug or unexpected behavior? Open an [**issue**](https://github.com/MasterAcnolo/Freedom-Loader/issues) using the **Bug Report** template.
|
||||||
|
|
||||||
|
**Please include:**
|
||||||
|
- Clear description of the bug
|
||||||
|
- Steps to reproduce
|
||||||
|
- App version and environment (Windows version, Firefox version if relevant)
|
||||||
|
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log`
|
||||||
|
- Screenshots if applicable
|
||||||
|
|
||||||
|
### 2. Request a Feature
|
||||||
|
|
||||||
|
Have an idea to improve Freedom Loader? Open a [**Feature Request**](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose).
|
||||||
|
|
||||||
|
**Tips:**
|
||||||
|
- Describe the problem it solves
|
||||||
|
- Explain your proposed solution
|
||||||
|
- Include use cases and examples
|
||||||
|
|
||||||
|
### 3. Submit Code Changes
|
||||||
|
|
||||||
|
**Before starting:**
|
||||||
|
- Check existing issues and PRs to avoid duplicates
|
||||||
|
- For major changes, open an issue first to discuss
|
||||||
|
- Follow the project's code style and conventions
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch: `git checkout -b feature/your-feature-name`
|
||||||
|
3. Make your changes
|
||||||
|
4. Test thoroughly on Windows 10/11
|
||||||
|
5. Commit with clear messages: `git commit -m "Add feature X"`
|
||||||
|
6. Push to your fork: `git push origin feature/your-feature-name`
|
||||||
|
7. Open a Pull Request using the appropriate template
|
||||||
|
|
||||||
|
**Code Guidelines:**
|
||||||
|
- Use camelCase for variables and functions
|
||||||
|
- Comment complex logic
|
||||||
|
- Keep commits focused and atomic
|
||||||
|
- Update documentation if needed
|
||||||
|
- Don't modify version numbers (done during release)
|
||||||
|
|
||||||
|
### 4. Improve Documentation
|
||||||
|
|
||||||
|
Documentation improvements are always welcome:
|
||||||
|
- Fix typos or unclear sections
|
||||||
|
- Add missing examples
|
||||||
|
- Update outdated information
|
||||||
|
- Improve README, wiki, or guides
|
||||||
|
|
||||||
|
Small contributions matter—don't hesitate to submit documentation PRs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js 16.x or higher
|
||||||
|
- npm or yarn
|
||||||
|
- Git
|
||||||
|
- Windows 10/11 (for testing)
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
```bash
|
||||||
|
# Clone your fork
|
||||||
|
git clone https://github.com/YOUR-USERNAME/Freedom-Loader.git
|
||||||
|
cd Freedom-Loader
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run in development mode
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
Freedom-Loader/
|
||||||
|
├── main.js # Electron main process
|
||||||
|
├── preload.js # Electron preload script
|
||||||
|
├── config.js # Global configuration
|
||||||
|
├── server/ # Express backend
|
||||||
|
│ ├── routes/ # API routes
|
||||||
|
│ ├── controller/ # Business logic
|
||||||
|
│ └── helpers/ # Utility functions
|
||||||
|
├── public/ # Frontend (HTML, CSS, JS)
|
||||||
|
└── resources/ # Binaries (yt-dlp, ffmpeg, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Before submitting a PR, verify:
|
||||||
|
- [ ] Download functionality works (video/audio)
|
||||||
|
- [ ] Playlist downloads work
|
||||||
|
- [ ] Settings panel functions correctly
|
||||||
|
- [ ] Custom path selection works
|
||||||
|
- [ ] No errors in application logs
|
||||||
|
- [ ] UI changes work on all themes (if applicable)
|
||||||
|
- [ ] Tested on Windows 10 and/or Windows 11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pull Request Review Process
|
||||||
|
|
||||||
|
1. **Submission**: Open PR with clear description using the template
|
||||||
|
2. **Review**: Maintainers review code and provide feedback
|
||||||
|
3. **Updates**: Address feedback and push updates
|
||||||
|
4. **Approval**: Once approved, PR will be merged
|
||||||
|
5. **Release**: Changes included in next release
|
||||||
|
|
||||||
|
**Response times:**
|
||||||
|
- Bug fixes: Usually reviewed within 2-3 days
|
||||||
|
- Features: May take longer depending on complexity
|
||||||
|
- Documentation: Often reviewed quickly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
Be respectful and constructive in all interactions. We welcome everyone as long as discussions remain polite and productive.
|
||||||
|
|
||||||
|
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
- Check the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/faq)
|
||||||
|
- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki)
|
||||||
|
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Thanks to **Zakaria** for the website icon design.
|
||||||
|
Check out his work: [IG @designmark_studio](https://www.instagram.com/designmark_studio/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Thank you for contributing to Freedom Loader!**
|
||||||
113
README-EN.md
@@ -1,113 +0,0 @@
|
|||||||
# Freedom Loader
|
|
||||||
|
|
||||||
<!-- Release -->
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<!-- Build (Next Release) -->
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> This project aims to enable free, open-source, and unrestricted downloading of Video/Audio content. Eventually, it will allow users to choose
|
|
||||||
|
|
||||||
## 🚀 Features
|
|
||||||
|
|
||||||
- ✅ Video download with metadata(MP4)
|
|
||||||
- ✅ Audio-only download with thumbail and metadata(MP3)
|
|
||||||
- ✅ Full playlist support
|
|
||||||
- ✅ Automatic metadata retrieval (title, duration, etc.)
|
|
||||||
- ✅ Detailed logs: console and rotating files (7 days)
|
|
||||||
- ✅ Custom output path selection with saving
|
|
||||||
- ✅ Multiple graphical themes available for interface customization
|
|
||||||
- ✅ Simple, intuitive, and responsive interface
|
|
||||||
- ✅ Notification system when download finished (Click = Open Output Folder)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Installation
|
|
||||||
|
|
||||||
To install the application, simply download the installer from the releases section. Take the latest version and run it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ❓ Frequently Asked Questions
|
|
||||||
|
|
||||||
### _Where are my downloaded files located?_
|
|
||||||
|
|
||||||
In your computer's Downloads folder (**C:\Users\YOUR NAME\Downloads**), inside a folder named **Freedom Loader Output**
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### _After installing and running the program, I get an error_
|
|
||||||
|
|
||||||
Try restarting the application, checking the logs, or opening the browser's developer console by pressing `CTRL + SHIFT + I` or by going to `View > Toggle Developer Tools`
|
|
||||||
|
|
||||||
<!--  -->
|
|
||||||
<p align="center">
|
|
||||||
<img src="build/example-developertools.png" alt="Example Developer Tools" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## 🏗️ Upcoming Features (Roadmap)
|
|
||||||
|
|
||||||
- [ ] More format options
|
|
||||||
- [ ] Subtitle support
|
|
||||||
- [ ] Improved UI / UX
|
|
||||||
- [ ] Language selection
|
|
||||||
- [ ] Download a specific part of the video
|
|
||||||
- [ ] Rename output file
|
|
||||||
- [ ] Extract / Split video using native chapters
|
|
||||||
- [ ] Parallel downloads
|
|
||||||
- [ ] Skip sponsored parts of videos
|
|
||||||
- [x] ~~Custom output path selection (default **C:\Users\Username\Downloads** with a **Freedom Loader Output** folder created)~~
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🖼️ Preview
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="build/apercu1.0.1.png" alt="Exemple d'application" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> Here is an example of the app after launch and entering a URL
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ℹ️ About
|
|
||||||
|
|
||||||
This project was built using [](https://github.com/yt-dlp/yt-dlp)
|
|
||||||
Thanks to them for providing a free and open-source API.
|
|
||||||
|
|
||||||
This project is open-source and welcomes your contributions.
|
|
||||||
Here is how to get involved:
|
|
||||||
|
|
||||||
- **🪲 Bug Reporting**
|
|
||||||
Please use the [GitHub issues system](https://github.com/MasterAcnolo/Freedom-Loader/issues) to report any bugs you encounter.
|
|
||||||
Try to provide clear reproduction steps and logs if possible. Logs are located in the installation folder of the program. Example path:
|
|
||||||
`C:\Users\[YOUR NAME]\Desktop\Freedom Loader\resources\app\logs`
|
|
||||||
The log file will be named something like `combined-20xx-xx-xx.log`
|
|
||||||
<br>
|
|
||||||
- **🛠️ Contribution**
|
|
||||||
Pull Requests are welcome! Please fork the repo and submit your PR after ensuring you meet the contribution guidelines (tests, style, etc.).
|
|
||||||
<br>
|
|
||||||
- **💡 Feature Suggestions**
|
|
||||||
You can propose new ideas via a [feature request issue](https://github.com/MasterAcnolo/Freedom-Loader/issues).
|
|
||||||
Any clear and detailed proposal will be reviewed.
|
|
||||||
<br>
|
|
||||||
- **💰 Support the Project**
|
|
||||||
<p>
|
|
||||||
If you'd like to support me, you can donate via
|
|
||||||
<a href="https://paypal.me/axelnicolas25">
|
|
||||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="PayPal">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This project is licensed under the **MIT License** — see the [LICENSE](./LICENSE) file for details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
|
||||||
|
|
||||||
I have obviously used AI assistance for some parts of this project. Use AI sparingly and always be aware of what you are doing.
|
|
||||||
647
README.md
@@ -1,113 +1,562 @@
|
|||||||
# Freedom Loader
|
<div align="center">
|
||||||
|
|
||||||
<!-- Release -->
|
<a href="https://masteracnolo.github.io/Freedom-Loader-Site/"><img
|
||||||
|
src="./build/banner.png"
|
||||||
|
alt="Banner"
|
||||||
|
style="width: 50%;"/></a>
|
||||||
|
|
||||||

|
### **A clean, open-source multimedia downloader for Windows and Linux**
|
||||||
|
|
||||||
<!-- Build (Next Release) -->
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
||||||
|
[](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
|
||||||
|
<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>
|
||||||
|
|
||||||
> Ce projet a pour but de permettre le téléchargement de contenu Vidéo/Audio de manière gratuite, open source et libre. Il permettra à terme de choisir
|
|
||||||
|
|
||||||
## 🚀 Fonctionnalités
|
|
||||||
|
|
||||||
- ✅ Téléchargement de vidéos avec les métadonnées (MP4)
|
|
||||||
- ✅ Téléchargement audio uniquement, avec la couverture ainsi que les métadonnées (MP3)
|
|
||||||
- ✅ Support des playlists complètes
|
|
||||||
- ✅ Récupération automatique des métadonnées (titre, durée, etc.)
|
|
||||||
- ✅ Logs détaillés : console et fichiers rotatifs (7 jours)
|
|
||||||
- ✅ Choix du chemin de sortie personnalisé avec sauvegarde
|
|
||||||
- ✅ Plusieurs thèmes graphiques au choix pour personnaliser l’interface
|
|
||||||
- ✅ Interface simple, intuitive
|
|
||||||
- ✅ Notification quand le téléchargement est fini (Click = Ouverture Dossier Output)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Installation
|
## Overview
|
||||||
|
|
||||||
Pour installer l'application, il vous suffit de télécharger l'installateur dans la section releases, de prendre la dernière version et de l'exécuter.
|
Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services.
|
||||||
|
|
||||||
|
The primary goal is to make media downloading accessible to users who want offline access to their favorite content, particularly in situations where internet connectivity is unreliable or unavailable.
|
||||||
|
|
||||||
|
### See related repo:
|
||||||
|
|
||||||
|
- **[Website](https://masteracnolo.github.io/Freedom-Loader-Site/)** - Official project website and documentation
|
||||||
|
- **[Workshop](https://github.com/MasterAcnolo/Freedom-Loader-Workshop)** - Theme creation tool and theme browser
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Features](#features)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Preview](#preview)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Theme Workshop](#Theme-Workshop)
|
||||||
|
- [Technology Stack](#technology-stack)
|
||||||
|
- [Development](#development)
|
||||||
|
- [Roadmap](#roadmap)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Submitting a community theme](#Submitting-a-community-theme)
|
||||||
|
- [Support](#support)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
|
||||||
|
- **Multi-format downloads** - MP4 video with full metadata or MP3 audio with embedded artwork
|
||||||
|
- **Video information preview** - Fetch and display video metadata before downloading
|
||||||
|
- **Quality selection** - Choose from best, medium, lowest quality or specific resolutions (1080p, 720p, 480p)
|
||||||
|
- **Playlist support** - Download entire playlists with optional auto-download mode
|
||||||
|
- **Metadata embedding** - Automatic retrieval and embedding of title, artist, duration, and thumbnail
|
||||||
|
- **Video codec options** - Support for H.264, H.265, AV1, VP9, VP9.2, and Theora codecs
|
||||||
|
- **Custom output directory** - User-configurable download location with path validation
|
||||||
|
- **Cookie integration** - Firefox cookie support for accessing age-restricted and member-only content
|
||||||
|
|
||||||
|
### User Interface
|
||||||
|
|
||||||
|
- **Custom window controls** - Frameless window with custom top bar (optional)
|
||||||
|
- **Custom themes** — ZIP-based theme system with live preview
|
||||||
|
- **Theme Workshop** — web-based theme creator and browser
|
||||||
|
- **Toast notifications** — in-app feedback system
|
||||||
|
- **Real-time progress tracking** - Live download progress bar with network speed indicator
|
||||||
|
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
||||||
|
- **Settings panel** - In-app configuration interface with live updates
|
||||||
|
- **Quick access toolbar** - Direct links to logs, website, wiki, and developer tools
|
||||||
|
|
||||||
|
### System Features
|
||||||
|
|
||||||
|
- **Automatic updates** - Electron-updater integration with GitHub releases
|
||||||
|
- **YT-DLP auto-update** - Keeps the download engine up-to-date on startup
|
||||||
|
- **Structured logging** - Winston-based logging with daily file rotation (7-day retention)
|
||||||
|
- **Discord Rich Presence** - Optional integration showing current download activity
|
||||||
|
- **Single instance enforcement** - Prevents multiple app instances from running simultaneously
|
||||||
|
- **Native dependency validation** - Startup checks for yt-dlp, FFmpeg, FFprobe, and Deno binaries
|
||||||
|
- **Rate limiting** - Express-based request throttling to prevent server overload
|
||||||
|
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
||||||
|
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
||||||
|
- **Splash screen** — animated loading screen on startup
|
||||||
|
- **Stop download** — ability to cancel in-progress downloads
|
||||||
|
- **Playlist folders** — dedicated folder creation per playlist
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
[](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
|
||||||
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
|
|
||||||
|
[//]: # ([](https://snapcraft.io/freedom-loader))
|
||||||
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- **Windows**: Windows 10 or later
|
||||||
|
- **Linux**: any modern x86_64 distribution
|
||||||
|
- **Browser**: Mozilla Firefox ([download](https://www.firefox.com)) required for cookie extraction, on both platforms
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
1. Download the latest `.exe` from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
|
2. Run the installer (click "More info" → "Run anyway" if Windows Defender warns you)
|
||||||
|
3. Follow the installation wizard
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
#### Fedora (via Copr — recommended)
|
||||||
|
|
||||||
|
Installing via Copr lets `dnf` handle future updates automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf copr enable masteracnolo/freedom-loader
|
||||||
|
sudo dnf install freedom-loader
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Debian / Ubuntu (.deb)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download the latest .deb from the Releases page, then:
|
||||||
|
sudo apt install ./freedom-loader_1.x.x_amd64.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
[//]: # (#### Snap (any distro with snapd))
|
||||||
|
|
||||||
|
[//]: # ()
|
||||||
|
[//]: # (```bash)
|
||||||
|
|
||||||
|
[//]: # (sudo snap install freedom-loader)
|
||||||
|
|
||||||
|
[//]: # (```)
|
||||||
|
|
||||||
|
#### RPM (Fedora/RHEL/openSUSE, without Copr)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download the latest .rpm from the Releases page, then:
|
||||||
|
sudo dnf install ./freedom-loader-1.x.y.x86_64.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### AppImage (any distro, no installation needed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download the latest .AppImage from the Releases page, then:
|
||||||
|
chmod +x Freedom-Loader-1.x.y.AppImage
|
||||||
|
./Freedom-Loader-1.x.y.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
Once installed via any method, launch Freedom Loader from your application menu, or from a terminal with:
|
||||||
|
```bash
|
||||||
|
freedom-loader
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Locations
|
||||||
|
|
||||||
|
| | Windows | Linux |
|
||||||
|
|---------------|---------------------------------------------------------|--------------------------------------|
|
||||||
|
| **Downloads** | `C:\Users\[USERNAME]\Downloads\Freedom Loader` | `~/Downloads/Freedom Loader` |
|
||||||
|
| **Logs** | `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\` | `~/.local/share/FreedomLoader/logs/` |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The download folder can be changed anytime via the "Edit" button in the UI, on both platforms.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> On Linux, you can start Freedom Loader using `freedom-loader` command
|
||||||
|
|
||||||
|
### Basic Download
|
||||||
|
|
||||||
|
1. Launch Freedom Loader
|
||||||
|
2. Paste a video or playlist URL into the input field
|
||||||
|
3. Select the desired format (Video MP4 or Audio MP3)
|
||||||
|
4. Click the download button
|
||||||
|
5. Monitor progress in real-time
|
||||||
|
6. Receive a notification when the download completes
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
The application supports various configuration options:
|
||||||
|
|
||||||
|
- Custom download directory selection
|
||||||
|
- Discord Rich Presence activation
|
||||||
|
- Theme customization
|
||||||
|
- Log level configuration
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
If you encounter issues:
|
||||||
|
|
||||||
|
1. Restart the application
|
||||||
|
2. Check the logs in `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs`
|
||||||
|
3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools"
|
||||||
|
4. Create an issue on GitHub with logs and reproduction steps
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" align="center">
|
||||||
|
<img src="https://github.com/user-attachments/assets/d4de9079-7c82-44d3-ba22-8246bb467db3" alt="Download Preview" width="75%" />
|
||||||
|
<p><em>Real-time download progress with network speed indicator</em></p>
|
||||||
|
</td>
|
||||||
|
<td width="50%" align="center">
|
||||||
|
<img src="https://github.com/user-attachments/assets/d59aff95-6754-486a-b880-d70730146dc3" alt="Settings Panel" width="40%" />
|
||||||
|
<p><em>Settings panel with theme and feature customization</em></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Freedom Loader can be configured either through the settings panel in the UI or by directly editing the `config.json` file located in the application data directory.
|
||||||
|
|
||||||
|
### Configuration File Location
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
```
|
||||||
|
%APPDATA%\Freedom Loader\config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux
|
||||||
|
```
|
||||||
|
~/.config/Freedom Loader/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Options
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"autoUpdate": true,
|
||||||
|
"discordRPC": true,
|
||||||
|
"customTopBar": true,
|
||||||
|
"autoCheckInfo": true,
|
||||||
|
"addThumbnail": true,
|
||||||
|
"addMetadata": true,
|
||||||
|
"verboseLogs": false,
|
||||||
|
"autoDownloadPlaylist": true,
|
||||||
|
"createPlaylistFolders": true,
|
||||||
|
"customCodec": "h264",
|
||||||
|
"logSystem": true,
|
||||||
|
"outputTitleCheck": true,
|
||||||
|
"downloadSystem": true,
|
||||||
|
"notifySystem": true,
|
||||||
|
"enableHardwareAcceleration": true,
|
||||||
|
"theme": "dark"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|------------------------------|---------|----------|------------------------------------------------------------|
|
||||||
|
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
||||||
|
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
||||||
|
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
||||||
|
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
||||||
|
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
|
||||||
|
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||||
|
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||||
|
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||||
|
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
||||||
|
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||||
|
| `logSystem` | boolean | `true` | Enable application logging |
|
||||||
|
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
||||||
|
| `downloadSystem` | boolean | `true` | Enable download system |
|
||||||
|
| `notifySystem` | boolean | `true` | Enable system notifications |
|
||||||
|
| `enableHardwareAcceleration` | boolean | `true` | Enable Hardware Acceleration |
|
||||||
|
| `theme` | string | `dark` | Current Theme Name |
|
||||||
|
|
||||||
|
### Supported Codecs
|
||||||
|
|
||||||
|
- `h264` - H.264/AVC (widely compatible, recommended default)
|
||||||
|
- `h265` - H.265/HEVC (better compression, less compatible)
|
||||||
|
- `av1` - AV1 (modern codec, excellent compression)
|
||||||
|
- `av01` - AV1 variant
|
||||||
|
- `vp9` - VP9 (open format, good quality)
|
||||||
|
- `vp9.2` - VP9 Profile 2 (HDR support)
|
||||||
|
- `theora` - Theora (legacy open codec)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Configuration changes may require an application restart to take effect.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Freedom-Loader/
|
||||||
|
├── app/ # Electron main process modules
|
||||||
|
│ ├── autoUpdater.js
|
||||||
|
│ ├── dependencyCheck.js
|
||||||
|
│ ├── discordRPC.js
|
||||||
|
│ ├── ipcHandlers.js
|
||||||
|
│ ├── pathValidator.js
|
||||||
|
│ ├── splashManager.js
|
||||||
|
│ ├── themeManager.js
|
||||||
|
│ ├── windowManager.js
|
||||||
|
│ └── ytDlpUpdater.js
|
||||||
|
├── build/ # Build resources and assets
|
||||||
|
├── config/ # Configuration files
|
||||||
|
│ ├── config.default.json
|
||||||
|
│ └── config.dev.json
|
||||||
|
├── public/ # Frontend assets and UI
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── splash.html
|
||||||
|
│ ├── assets/
|
||||||
|
│ │ ├── icon/ # Application icons
|
||||||
|
│ │ └── logo/ # Logo assets
|
||||||
|
│ ├── script/ # Frontend JavaScript modules
|
||||||
|
│ │ ├── appVersion.js
|
||||||
|
│ │ ├── clipboardPaste.js
|
||||||
|
│ │ ├── custompath.js
|
||||||
|
│ │ ├── customthemes.js
|
||||||
|
│ │ ├── downloadstatus.js
|
||||||
|
│ │ ├── fetchinfo.js
|
||||||
|
│ │ ├── progressBar.js
|
||||||
|
│ │ ├── settingsPanel.js
|
||||||
|
│ │ ├── toast.js
|
||||||
|
│ │ └── topbar.js
|
||||||
|
│ └── styles/ # CSS stylesheets
|
||||||
|
│ ├── styles.css
|
||||||
|
│ ├── variables.css
|
||||||
|
│ ├── components/ # Component-specific styles
|
||||||
|
│ │ ├── checkbox.css
|
||||||
|
│ │ ├── editpathbutton.css
|
||||||
|
│ │ ├── loader.css
|
||||||
|
│ │ ├── progressBar.css
|
||||||
|
│ │ └── toast.css
|
||||||
|
│ └── layout/ # Layout styles
|
||||||
|
│ ├── container.css
|
||||||
|
│ ├── form.css
|
||||||
|
│ ├── header.css
|
||||||
|
│ ├── settingsPanel.css
|
||||||
|
│ ├── topbar.css
|
||||||
|
│ └── videoinfo.css
|
||||||
|
├── resources/ # Internal resources (binaries)
|
||||||
|
├── server/ # Express backend server
|
||||||
|
│ ├── logger.js
|
||||||
|
│ ├── server.js
|
||||||
|
│ ├── controller/ # Request handlers
|
||||||
|
│ │ ├── download.controller.js
|
||||||
|
│ │ └── info.controller.js
|
||||||
|
│ ├── helpers/ # Utility functions
|
||||||
|
│ │ ├── buildArgs.helpers.js
|
||||||
|
│ │ ├── getBrowser.helpers.js
|
||||||
|
│ │ ├── notify.helpers.js
|
||||||
|
│ │ ├── parseInfo.helpers.js
|
||||||
|
│ │ ├── path.helpers.js
|
||||||
|
│ │ ├── rateLimit.helpers.js
|
||||||
|
│ │ └── validation.helpers.js
|
||||||
|
│ ├── routes/ # API route definitions
|
||||||
|
│ │ ├── download.route.js
|
||||||
|
│ │ └── info.route.js
|
||||||
|
│ └── services/ # Business logic layer
|
||||||
|
│ ├── download.services.js
|
||||||
|
│ └── info.services.js
|
||||||
|
├── theme/ # Theme system
|
||||||
|
│ ├── template.theme.json # Theme template
|
||||||
|
│ ├── Dark/ # Default dark theme
|
||||||
|
│ │ └── dark.theme.json
|
||||||
|
│ └── Light/ # Default light theme
|
||||||
|
│ └── light.theme.json
|
||||||
|
├── main.js # Electron main process entry point
|
||||||
|
├── preload.js # Electron preload script (IPC bridge)
|
||||||
|
├── config.js # Global configuration loader
|
||||||
|
├── package.json # Project metadata and dependencies
|
||||||
|
├── CODE_OF_CONDUCT.md # Community guidelines
|
||||||
|
├── CONTRIBUTING.md # Contribution guidelines
|
||||||
|
├── LICENSE # GPLv3 license
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architecture Overview
|
||||||
|
|
||||||
|
Freedom Loader uses a client-server architecture within a single Electron application:
|
||||||
|
|
||||||
|
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
|
||||||
|
- **Backend**: Express.js server running locally for download management
|
||||||
|
- **App modules**: Electron-specific logic (window, IPC, updates, RPC) isolated in `app/`
|
||||||
|
- **IPC Bridge**: Secure communication via Electron's preload script
|
||||||
|
- **Logging**: Winston-based structured logging with file rotation
|
||||||
|
- **Updates**: Automatic checking and installation via electron-updater
|
||||||
|
|
||||||
|
## Theme Workshop
|
||||||
|
|
||||||
|
Freedom Loader includes a web-based theme creator available at [Freedom Loader Workshop](https://masteracnolo.github.io/Freedom-Loader-Workshop/).
|
||||||
|
|
||||||
|
- Create custom themes with a live preview of the actual UI
|
||||||
|
- Browse and download community themes
|
||||||
|
- Export themes as `.zip` files ready to drop into the `theme/` folder
|
||||||
|
- Supports background images, custom colors for every UI element
|
||||||
|
|
||||||
|
### Installing a custom theme
|
||||||
|
1. Download a `.zip` theme file
|
||||||
|
2. Drop it in the `theme/` folder of your Freedom Loader installation
|
||||||
|
3. Restart the app — your theme appears in the settings panel
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
### Core Technologies
|
||||||
|
|
||||||
|
[](https://github.com/yt-dlp/yt-dlp)
|
||||||
|
[](https://ffmpeg.org/)
|
||||||
|
[](https://www.electronjs.org/)
|
||||||
|
[](https://nodejs.org/)
|
||||||
|
|
||||||
|
### Key Dependencies
|
||||||
|
|
||||||
|
- **Electron**: Cross-platform desktop application framework
|
||||||
|
- **Express**: Web framework for local HTTP server
|
||||||
|
- **yt-dlp**: Media download engine
|
||||||
|
- **FFmpeg**: Media processing and conversion
|
||||||
|
- **Winston**: Logging library
|
||||||
|
- **discord-rpc**: Discord Rich Presence integration
|
||||||
|
- **electron-updater**: Automatic application updates
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 16.x or higher
|
||||||
|
- npm or yarn
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/MasterAcnolo/Freedom-Loader.git
|
||||||
|
cd Freedom-Loader
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run in development mode
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Dependencies
|
||||||
|
|
||||||
|
This project relies on several external binaries.
|
||||||
|
Some of them are **not included** in the public repository and must be added manually.
|
||||||
|
|
||||||
|
You must download the required binaries and place them in the `resources` folder, using the exact filenames listed below.
|
||||||
|
|
||||||
|
#### Required binaries
|
||||||
|
|
||||||
|
- **Deno**
|
||||||
|
- Download from: https://sourceforge.net/projects/deno.mirror/files/latest/download
|
||||||
|
- Rename to: `deno.exe`
|
||||||
|
|
||||||
|
- **FFmpeg**
|
||||||
|
- Download from: https://www.ffmpeg.org/download.html
|
||||||
|
- Required files:
|
||||||
|
- `ffmpeg.exe`
|
||||||
|
- `ffprobe.exe`
|
||||||
|
|
||||||
|
- **yt-dlp**
|
||||||
|
- Already bundled with the project
|
||||||
|
- No manual installation required
|
||||||
|
|
||||||
|
Final folder structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
resources/
|
||||||
|
├── deno.exe
|
||||||
|
├── ffmpeg.exe
|
||||||
|
├── ffprobe.exe
|
||||||
|
└── yt-dlp.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
> These binaries are required for the application to start correctly.
|
||||||
|
> If any of them are missing, an error message will be displayed at application startup.
|
||||||
|
|
||||||
|
### Development Guidelines
|
||||||
|
|
||||||
|
- Follow existing code style and conventions
|
||||||
|
- Write clear commit messages
|
||||||
|
- Test thoroughly before submitting changes
|
||||||
|
- Update documentation when adding features
|
||||||
|
- Maintain compatibility with Windows 10+ and Linux
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome and appreciated. This project benefits from community involvement.
|
||||||
|
|
||||||
|
## Submitting a community theme
|
||||||
|
|
||||||
|
Want your theme featured in the Workshop? Please **do not open an issue on this repository** for theme submissions.
|
||||||
|
Head over to the [Freedom Loader Workshop repository](https://github.com/MasterAcnolo/Freedom-Loader-Workshop) and open an issue there with your theme files, preview screenshots, and description.
|
||||||
|
|
||||||
|
### Bug Reports
|
||||||
|
|
||||||
|
Use the GitHub Issues system and include:
|
||||||
|
|
||||||
|
- Clear description of the issue
|
||||||
|
- Steps to reproduce
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Relevant logs from `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs/`
|
||||||
|
- Screenshots if applicable
|
||||||
|
|
||||||
|
### Feature Requests
|
||||||
|
|
||||||
|
Open a feature request issue with:
|
||||||
|
|
||||||
|
- Clear description of the proposed feature
|
||||||
|
- Use cases and benefits
|
||||||
|
- Potential implementation approach (if applicable)
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/your-feature`)
|
||||||
|
3. Make your changes with clear, descriptive commits
|
||||||
|
4. Test thoroughly
|
||||||
|
5. Update documentation as needed
|
||||||
|
6. Submit a pull request with a detailed description
|
||||||
|
|
||||||
|
Please read [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you find this project useful and want to support its development:
|
||||||
|
|
||||||
|
[](https://paypal.me/axelnicolas25)
|
||||||
|
|
||||||
|
Your support helps maintain infrastructure, develop new features, and keep the project free and ad-free.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Freedom Loader is released under the **GNU General Public License v3.0 (GPLv3)**.
|
||||||
|
|
||||||
|
You are free to use, modify, and redistribute this software under the terms of the GPL-3.0 license. See the [LICENSE](LICENSE) file for complete details.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
||||||
|
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
||||||
|
- [Deno](https://deno.com/) for helping me bypassing challenges
|
||||||
|
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
||||||
|
- The open-source community for continuous support and contributions
|
||||||
|
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
||||||
|
- Zakaria for the website icon
|
||||||
|
- Jacques Chirac to love Apples
|
||||||
|
- All users who test, report issues, and help improve the application
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ❓ Questions fréquentes
|
<div align="center">
|
||||||
|
|
||||||
### _Où se situent mes fichiers une fois téléchargés ?_
|
**Freedom Loader** - Put freedom in your downloads
|
||||||
|
|
||||||
Dans le dossier Téléchargements (**C:\Users\VOTRE NOM\Downloads**) de votre ordinateur, dans un dossier nommé **Freedom Loader Output**
|
[Website](https://masteracnolo.github.io/Freedom-Loader-Site/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/Freedom-Loader-Site/wiki) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
|
||||||
<br>
|
|
||||||
|
|
||||||
### _Une fois le programme installé et lancé, j'ai une erreur_
|
</div>
|
||||||
|
|
||||||
Essayez de relancer l'application, de consulter les logs ou d'ouvrir la console de développement du navigateur en faisant le raccourci `CTRL + SHIFT + I` ou via `View > Toggle Developer Tools`.
|
|
||||||
|
|
||||||
<!--  -->
|
|
||||||
<p align="center">
|
|
||||||
<img src="build/example-developertools.png" alt="Exemple Developer Tools" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## 🏗️ Fonctionnalités à venir (Roadmap)
|
|
||||||
|
|
||||||
- [ ] Plus de choix de formats
|
|
||||||
- [ ] Support des sous-titres
|
|
||||||
- [ ] Meilleure UI / UX
|
|
||||||
- [ ] Choix des langues
|
|
||||||
- [ ] Téléchargement d’une partie de la vidéo
|
|
||||||
- [ ] Renommage du fichier de sortie
|
|
||||||
- [ ] Extraction / découpage de la vidéo via chapitrage natif
|
|
||||||
- [ ] Téléchargements parallèles
|
|
||||||
- [ ] Suppression automatique des parties sponsorisées (skip sponsor)
|
|
||||||
- [x] ~~Choix personnalisé du chemin de sortie (par défaut **C:\Users\Utilisateur\Downloads**, où un dossier **Freedom Loader Output** sera créé)~~
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🖼️ Aperçu
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="build/apercu1.0.1.png" alt="Exemple d'application" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> Voici un exemple de l’application lancée après avoir saisi une URL.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ℹ️ À propos
|
|
||||||
|
|
||||||
Ce projet a été réalisé en utilisant [](https://github.com/yt-dlp/yt-dlp)
|
|
||||||
Merci à eux de nous permettre d’utiliser leur API gratuitement et en open source.
|
|
||||||
|
|
||||||
Ce projet est open source et accueille volontiers vos contributions.
|
|
||||||
Voici comment participer :
|
|
||||||
|
|
||||||
- **🪲 Signalement de bugs**
|
|
||||||
Merci d’utiliser le [système d’issues GitHub](https://github.com/MasterAcnolo/Freedom-Loader/issues) pour remonter les bugs rencontrés.
|
|
||||||
Essayez de fournir une reproduction claire et les logs si possible. Les logs se trouvent dans le dossier d’installation du programme. Exemple de chemin :
|
|
||||||
`C:\Users\[VOTRE NOM]\Desktop\Freedom Loader\resources\app\logs`
|
|
||||||
Le fichier aura un nom du type `combined-20xx-xx-xx.log`.
|
|
||||||
<br>
|
|
||||||
- **🛠️ Contribution**
|
|
||||||
Les Pull Requests sont les bienvenues ! Merci de forker le repo et de soumettre votre PR après avoir vérifié le respect des guidelines (tests, style, etc.).
|
|
||||||
<br>
|
|
||||||
- **💡 Suggestions de fonctionnalités**
|
|
||||||
Vous pouvez proposer de nouvelles idées via une [issue de type feature request](https://github.com/MasterAcnolo/Freedom-Loader/issues).
|
|
||||||
Toute proposition claire et détaillée sera étudiée.
|
|
||||||
<br>
|
|
||||||
- **💰 Soutenir le projet**
|
|
||||||
<p>
|
|
||||||
Si vous souhaitez m’encourager, vous pouvez faire un don sur
|
|
||||||
<a href="https://paypal.me/axelnicolas25">
|
|
||||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="PayPal">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 Licence
|
|
||||||
|
|
||||||
Ce projet est sous licence **MIT** — voir le fichier [LICENSE](./LICENSE) pour plus d’informations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
|
||||||
|
|
||||||
J’ai bien évidemment eu recours à l’IA pour m’aider sur certaines parties du projet. Utilisez l’IA avec parcimonie et soyez toujours conscient de ce que vous faites.
|
|
||||||
|
|||||||
160
SECURITY.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Freedom Loader follows a **latest-only** support model. Security fixes are applied to the current release only — no backports to older versions.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------------- | ------------------ |
|
||||||
|
| Latest stable | :white_check_mark: |
|
||||||
|
| Older versions | :x: |
|
||||||
|
|
||||||
|
I recommend always running the latest version. The built-in auto-updater will notify you when a new release is available. Older versions will not receive security patches regardless of severity.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability in Freedom Loader, please help us keep the project and its users safe by reporting it responsibly.
|
||||||
|
|
||||||
|
### How to Report
|
||||||
|
|
||||||
|
**DO NOT** open a public issue for security vulnerabilities.
|
||||||
|
|
||||||
|
Instead, please report security issues privately by:
|
||||||
|
|
||||||
|
1. **Email**: Send details to **masteracnolo25@gmail.com** with the subject line: `[SECURITY] Vulnerability Report`
|
||||||
|
2. **Include**:
|
||||||
|
- Description of the vulnerability
|
||||||
|
- Steps to reproduce the issue
|
||||||
|
- Potential impact and severity
|
||||||
|
- Suggested fix (if available)
|
||||||
|
- Your contact information for follow-up
|
||||||
|
|
||||||
|
### What to Expect
|
||||||
|
|
||||||
|
- **Acknowledgment**: I will acknowledge receipt of your report within 48 hours
|
||||||
|
- **Assessment**: I will assess the vulnerability and determine its severity
|
||||||
|
- **Updates**: I will keep you informed of our progress
|
||||||
|
- **Resolution**: I aim to release a fix within 7-14 days for critical vulnerabilities
|
||||||
|
- **Credit**: I will credit you in the release notes (unless you prefer to remain anonymous)
|
||||||
|
|
||||||
|
## Security Best Practices for Users
|
||||||
|
|
||||||
|
When using Freedom Loader, I recommend:
|
||||||
|
|
||||||
|
### General Security
|
||||||
|
|
||||||
|
- **Keep Updated**: Always use the latest version of Freedom Loader — older versions receive no security patches
|
||||||
|
- **Official Sources**: Download only from official sources:
|
||||||
|
- [GitHub Releases](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
|
- [Fedora COPR](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
|
||||||
|
<!-- - [Snap Store](https://snapcraft.io/freedom-loader) -->
|
||||||
|
- **Verify Downloads**: Check that installers are from one of the above sources
|
||||||
|
- **Antivirus**: Keep your antivirus software up to date
|
||||||
|
|
||||||
|
### Configuration Security
|
||||||
|
|
||||||
|
- **Download Path**: Only set download paths within your user directory
|
||||||
|
- **Cookies**: Be aware that Firefox cookies are used for authentication — keep Firefox secure
|
||||||
|
- **Logs**: Logs may contain sensitive information such as URLs or local paths — avoid sharing them publicly without review
|
||||||
|
|
||||||
|
### Privacy Considerations
|
||||||
|
|
||||||
|
Freedom Loader respects your privacy:
|
||||||
|
|
||||||
|
- **No Data Collection**: I don't collect, store, or transmit your personal data
|
||||||
|
- **No Telemetry**: No usage tracking or analytics
|
||||||
|
- **Local Operation**: All downloads are processed locally on your machine
|
||||||
|
- **Optional Features**: Discord RPC is optional and can be disabled in settings
|
||||||
|
|
||||||
|
## Known Security Considerations
|
||||||
|
|
||||||
|
### Browser Cookie Access
|
||||||
|
|
||||||
|
Freedom Loader accesses Firefox cookies to download protected content. This is:
|
||||||
|
|
||||||
|
- **By Design**: Required for age-restricted or member-only content
|
||||||
|
- **Local Only**: Cookies are read locally and never transmitted outside your machine
|
||||||
|
- **User Controlled**: You control what content you download
|
||||||
|
|
||||||
|
### Native Dependencies
|
||||||
|
|
||||||
|
Freedom Loader bundles the following official binaries:
|
||||||
|
|
||||||
|
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp) — auto-updated on each launch
|
||||||
|
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
|
||||||
|
- **Deno**: Official builds from [Deno.com](https://deno.com/)
|
||||||
|
|
||||||
|
These dependencies are verified and updated regularly.
|
||||||
|
|
||||||
|
### Windows Defender Warnings
|
||||||
|
|
||||||
|
New releases may trigger Windows Defender warnings because:
|
||||||
|
|
||||||
|
- The application is not yet widely distributed
|
||||||
|
- Code signing certificates are expensive for open-source projects
|
||||||
|
|
||||||
|
This is expected behavior for new releases and will diminish as more users download the software.
|
||||||
|
|
||||||
|
## Vulnerability Disclosure Policy
|
||||||
|
|
||||||
|
### Our Commitment
|
||||||
|
|
||||||
|
I am committed to:
|
||||||
|
|
||||||
|
- Responding promptly to security reports
|
||||||
|
- Working with security researchers to verify and address issues
|
||||||
|
- Keeping users informed about security updates
|
||||||
|
- Crediting researchers who report vulnerabilities responsibly
|
||||||
|
|
||||||
|
### Disclosure Timeline
|
||||||
|
|
||||||
|
1. **Day 0**: Vulnerability reported privately
|
||||||
|
2. **Day 1–2**: Acknowledgment sent to reporter
|
||||||
|
3. **Day 3–7**: Vulnerability assessed and fix developed
|
||||||
|
4. **Day 7–14**: Fix released (critical vulnerabilities prioritized)
|
||||||
|
5. **Day 14+**: Public disclosure (coordinated with reporter)
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
Security issues we're most interested in:
|
||||||
|
|
||||||
|
- **Code Execution**: Arbitrary code execution vulnerabilities
|
||||||
|
- **Path Traversal**: Issues with file system access controls
|
||||||
|
- **Injection**: Command injection or similar vulnerabilities
|
||||||
|
- **Authentication**: Bypass of security controls
|
||||||
|
- **Data Exposure**: Unintended exposure of sensitive data
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
|
||||||
|
The following are generally not considered security vulnerabilities:
|
||||||
|
|
||||||
|
- Issues requiring physical access to the user's machine
|
||||||
|
- Social engineering attacks
|
||||||
|
- Denial of service against third-party services
|
||||||
|
- Issues in third-party dependencies (report to upstream projects directly)
|
||||||
|
- Missing security headers on the local HTTP server (not exposed remotely)
|
||||||
|
|
||||||
|
## Security Updates
|
||||||
|
|
||||||
|
Security updates are released as:
|
||||||
|
|
||||||
|
- **Patch Releases**: For critical security fixes (e.g., 1.6.0 → 1.6.1)
|
||||||
|
- **Automatic Updates**: Users are notified via the built-in update system
|
||||||
|
- **Release Notes**: Security fixes are clearly marked in the changelog
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Contributing Guidelines](CONTRIBUTING.md)
|
||||||
|
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
|
- [GitHub Security Advisories](https://github.com/MasterAcnolo/Freedom-Loader/security/advisories)
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
For security-related questions or concerns:
|
||||||
|
|
||||||
|
- **Email**: masteracnolo25@gmail.com
|
||||||
|
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Thank you for helping keep Freedom Loader and its users safe!**
|
||||||
157
app/autoUpdater.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Electron auto-update system using electron-updater.
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - update detection
|
||||||
|
* - user confirmation dialogs
|
||||||
|
* - download progress streaming to renderer
|
||||||
|
* - application restart and installation
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { autoUpdater } = require("electron-updater");
|
||||||
|
const { dialog } = require("electron");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures autoUpdater behavior:
|
||||||
|
* - disables automatic download
|
||||||
|
* - disables automatic installation on quit
|
||||||
|
* (manual user-controlled update flow)
|
||||||
|
*/
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes application auto-update lifecycle.
|
||||||
|
*
|
||||||
|
* Registers update event listeners and binds them to:
|
||||||
|
* - UI dialogs (Electron dialog)
|
||||||
|
* - renderer communication (webContents)
|
||||||
|
* - download/install flow control
|
||||||
|
*
|
||||||
|
* @param {BrowserWindow} mainWindow - main Electron window instance
|
||||||
|
*/
|
||||||
|
function initAutoUpdater(mainWindow) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when a new version is detected.
|
||||||
|
* Prompts user to install or defer update.
|
||||||
|
*/
|
||||||
|
autoUpdater.on("update-available", async (info) => {
|
||||||
|
logger.info(`Update available: ${info.version}`);
|
||||||
|
|
||||||
|
const { response } = await dialog.showMessageBox(mainWindow, {
|
||||||
|
type: "info",
|
||||||
|
title: "Update Available",
|
||||||
|
message: `Version ${info.version} is available.`,
|
||||||
|
detail: "Would you like to download and install it now?",
|
||||||
|
buttons: ["Install Update", "Maybe Later"],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response === 0) {
|
||||||
|
await autoUpdater.downloadUpdate();
|
||||||
|
} else {
|
||||||
|
mainWindow?.webContents.executeJavaScript(
|
||||||
|
`window.showUpdateBadge && window.showUpdateBadge("${info.version}")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams update download progress to renderer process.
|
||||||
|
* Used to update UI progress bar and status indicators.
|
||||||
|
*/
|
||||||
|
autoUpdater.on("download-progress", (progress) => {
|
||||||
|
logger.info(`Download progress: ${Math.round(progress.percent)}%`);
|
||||||
|
mainWindow?.webContents.send("update-progress", {
|
||||||
|
percent: Math.round(progress.percent),
|
||||||
|
speed: progress.bytesPerSecond,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when update has been fully downloaded.
|
||||||
|
* Prompts user to restart application and install update.
|
||||||
|
*/
|
||||||
|
autoUpdater.on("update-downloaded", async (info) => {
|
||||||
|
logger.info(`Update downloaded: ${info.version}`);
|
||||||
|
|
||||||
|
const { response } = await dialog.showMessageBox(mainWindow, {
|
||||||
|
type: "info",
|
||||||
|
title: "Update Ready",
|
||||||
|
message: `Version ${info.version} has been downloaded.`,
|
||||||
|
detail: "The application will restart to apply the update.",
|
||||||
|
buttons: ["Install Now", "Later"],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response === 0) autoUpdater.quitAndInstall();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles update system errors.
|
||||||
|
* Logs failure and displays an error dialog to user.
|
||||||
|
*/
|
||||||
|
autoUpdater.on("error", (err) => {
|
||||||
|
const msg = err?.message || "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If no update is available, I put this because there is no Linux version before 1.6.0
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
const isNoUpdateAvailable = /404/.test(msg) || /Cannot find latest.*\.yml/i.test(msg);
|
||||||
|
|
||||||
|
if (isNoUpdateAvailable) {
|
||||||
|
logger.warn("Auto update: no update metadata found (probably no previous release), ignoring", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Auto update error:", msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkForUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually triggers update check on startup.
|
||||||
|
* Separated from init for reusability and testability.
|
||||||
|
*/
|
||||||
|
async function checkForUpdates() {
|
||||||
|
|
||||||
|
if (process.env.SNAP) {
|
||||||
|
logger.info("Running as Snap, update managed by snapd, skipping electron-updater");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!require("electron").app.isPackaged) return ;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await autoUpdater.checkForUpdates();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Update check failed:", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually triggers update download.
|
||||||
|
* Used when user accepts update prompt.
|
||||||
|
*/
|
||||||
|
async function downloadUpdate() {
|
||||||
|
try {
|
||||||
|
await autoUpdater.downloadUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Download failed:", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately quits application and installs downloaded update.
|
||||||
|
*/
|
||||||
|
function installUpdate() {
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initAutoUpdater, downloadUpdate, installUpdate };
|
||||||
67
app/dependencyCheck.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { app, dialog } = require("electron");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that all required native binaries exist before the app starts.
|
||||||
|
*
|
||||||
|
* This includes:
|
||||||
|
* - yt-dlp (download engine)
|
||||||
|
* - ffmpeg (media processing)
|
||||||
|
* - ffprobe (media inspection)
|
||||||
|
* - deno (JS runtime used by yt-dlp hooks)
|
||||||
|
*
|
||||||
|
* If any dependency is missing:
|
||||||
|
* - logs the error
|
||||||
|
* - shows a blocking Electron error dialog
|
||||||
|
* - exits the application
|
||||||
|
*
|
||||||
|
* @returns {boolean} true if all dependencies are present, false otherwise
|
||||||
|
*/
|
||||||
|
function checkNativeDependencies() {
|
||||||
|
/**
|
||||||
|
* Native dependency paths are resolved at runtime from the server helpers.
|
||||||
|
* This ensures correct resolution in both dev and packaged builds.
|
||||||
|
*/
|
||||||
|
const { binaryPaths } = require("../server/helpers/path.helpers");
|
||||||
|
|
||||||
|
const isWindows = process.platform === "win32";
|
||||||
|
|
||||||
|
const addExe = isWindows ? ".exe" : "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of required external executables used by the application runtime.
|
||||||
|
* Each entry defines a binary name and its resolved absolute path.
|
||||||
|
*/
|
||||||
|
const deps = [
|
||||||
|
{ name: `yt-dlp${addExe}`, path: binaryPaths.ytDlp },
|
||||||
|
{ name: `ffmpeg${addExe}`, path: binaryPaths.ffmpeg },
|
||||||
|
{ name: `ffprobe${addExe}`, path: binaryPaths.ffprobe },
|
||||||
|
{ name: `deno${addExe}`, path: binaryPaths.deno },
|
||||||
|
];
|
||||||
|
|
||||||
|
const missing = deps.filter(d => !fs.existsSync(d.path));
|
||||||
|
if (missing.length === 0) return true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-readable list of missing binaries for logging and user dialog display.
|
||||||
|
*/
|
||||||
|
const list = missing.map(d => d.name).join(", ");
|
||||||
|
logger.error(`Missing dependencies: ${list}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defers error dialog until Electron app is fully initialized.
|
||||||
|
* Required to safely display modal dialogs before quit.
|
||||||
|
*/
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
dialog.showErrorBox(
|
||||||
|
"Missing dependencies",
|
||||||
|
`The following files are missing in the 'resources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.`
|
||||||
|
);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { checkNativeDependencies };
|
||||||
87
app/discordRPC.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const config = require('../config');
|
||||||
|
const RPC = require("discord-rpc");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord Application Client ID used to authenticate the RPC connection.
|
||||||
|
* Comes from the application configuration.
|
||||||
|
*/
|
||||||
|
const clientId = `${config.DiscordRPCID}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord RPC client instance using IPC transport.
|
||||||
|
* Maintains a persistent connection with Discord desktop client.
|
||||||
|
*/
|
||||||
|
const rpc = new RPC.Client({ transport: "ipc" });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional interval reference used for future RPC refresh logic.
|
||||||
|
* Currently reserved for potential periodic activity updates.
|
||||||
|
*/
|
||||||
|
let intervalId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes Discord Rich Presence (RPC) connection.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Connect to Discord via IPC transport
|
||||||
|
* - Set application presence (title, state, timestamps, assets)
|
||||||
|
* - Handle connection errors gracefully via logger
|
||||||
|
*
|
||||||
|
* Triggered once during application startup.
|
||||||
|
*/
|
||||||
|
function startRPC() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rich Presence payload describing current application state
|
||||||
|
* shown in Discord user profile.
|
||||||
|
*/
|
||||||
|
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.clearActivity()
|
||||||
|
rpc.setActivity(presence);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.login({ clientId }).catch(err => {
|
||||||
|
logger.error("Unable to connect to the RPC:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully stops Discord Rich Presence connection.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Clear any running intervals
|
||||||
|
* - Remove current activity from Discord
|
||||||
|
* - Destroy RPC transport connection
|
||||||
|
* - Handle cleanup errors safely
|
||||||
|
*/
|
||||||
|
async function stopRPC(){
|
||||||
|
try {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures RPC connection exists before attempting cleanup.
|
||||||
|
*/
|
||||||
|
if (rpc && rpc.transport) {
|
||||||
|
await rpc.clearActivity()
|
||||||
|
await rpc.destroy();
|
||||||
|
} else{
|
||||||
|
logger.error("Not Able to close RPC")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Error while closing the RPC:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { startRPC, stopRPC};
|
||||||
233
app/ipcHandlers.js
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Electron IPC main process registry.
|
||||||
|
*
|
||||||
|
* Centralizes all IPC channels exposed to renderer process:
|
||||||
|
* - Application info (version, features)
|
||||||
|
* - Window controls (topbar actions)
|
||||||
|
* - File system interactions (theme, config, download paths)
|
||||||
|
* - UI state synchronization (progress bar)
|
||||||
|
*
|
||||||
|
* Acts as the bridge between renderer and privileged Node/Electron APIs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { ipcMain, dialog, shell } = require("electron");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { logger, logDir } = require("../server/logger");
|
||||||
|
const { configFeatures, featuresPath } = require("../config");
|
||||||
|
const { getThemes, reloadThemes } = require("./themeManager");
|
||||||
|
const config = require("../config");
|
||||||
|
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
|
||||||
|
const { userThemesPath } = require("../server/helpers/path.helpers");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security whitelist for feature flags that can be modified at runtime.
|
||||||
|
*
|
||||||
|
* Prevents unauthorized or unexpected configuration keys from being persisted.
|
||||||
|
* Acts as a basic validation layer for IPC "set-feature".
|
||||||
|
*/
|
||||||
|
const FEATURE_WHITELIST = new Set([
|
||||||
|
"autoUpdate",
|
||||||
|
"discordRPC",
|
||||||
|
"customTopBar",
|
||||||
|
"autoCheckInfo",
|
||||||
|
"addThumbnail",
|
||||||
|
"addMetadata",
|
||||||
|
"verboseLogs",
|
||||||
|
"autoDownloadPlaylist",
|
||||||
|
"customCodec",
|
||||||
|
"theme",
|
||||||
|
"createPlaylistFolders",
|
||||||
|
"notifySystem",
|
||||||
|
"enableHardwareAcceleration"
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolute path to the configuration file storing feature flags.
|
||||||
|
*/
|
||||||
|
const configFolderPath = featuresPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory containing user-installed themes.
|
||||||
|
*/
|
||||||
|
const themeFolderPath = userThemesPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers all IPC handlers and event listeners for Electron main process.
|
||||||
|
*
|
||||||
|
* This function wires renderer → main communication channels:
|
||||||
|
* - synchronous queries (handle)
|
||||||
|
* - fire-and-forget events (on)
|
||||||
|
*
|
||||||
|
* @param {Function} getMainWindow - Function returning current BrowserWindow instance
|
||||||
|
*/
|
||||||
|
function registerIpcHandlers(getMainWindow) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns application version from config.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("version", () => config.version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns runtime feature configuration object.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("features", () => configFeatures);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens native folder selection dialog and validates selected path.
|
||||||
|
*
|
||||||
|
* Ensures:
|
||||||
|
* - user cancellation is handled safely
|
||||||
|
* - selected path is validated against security rules
|
||||||
|
* - unsafe directories are rejected
|
||||||
|
*/
|
||||||
|
ipcMain.handle("select-download-folder", async () => {
|
||||||
|
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||||
|
if (result.canceled) {
|
||||||
|
logger.info("Folder selection cancelled by user");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const validated = validateDownloadPath(result.filePaths[0]);
|
||||||
|
logger.info(`Folder selected and validated: ${validated}`);
|
||||||
|
return validated;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("validate-download-path", (_, userPath) => validateDownloadPath(userPath));
|
||||||
|
ipcMain.handle("get-default-download-path", () => getDefaultDownloadPath());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates Windows/macOS taskbar progress indicator.
|
||||||
|
*
|
||||||
|
* @param {number} percent - Progress value (0–100)
|
||||||
|
*/
|
||||||
|
ipcMain.on("set-progress", (_, percent) => {
|
||||||
|
getMainWindow()?.setProgressBar(percent / 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window minimize request from renderer.
|
||||||
|
*/
|
||||||
|
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles maximize/unmaximize state of main window.
|
||||||
|
*/
|
||||||
|
ipcMain.on("window-maximize", () => {
|
||||||
|
const win = getMainWindow();
|
||||||
|
if (!win) return;
|
||||||
|
win.isMaximized() ? win.unmaximize() : win.maximize();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the main application window.
|
||||||
|
*/
|
||||||
|
ipcMain.on("window-close", () => getMainWindow()?.close());
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Chromium DevTools in detached mode.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-devtools", () =>
|
||||||
|
getMainWindow()?.webContents.openDevTools({ mode: "detach" })
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens application logs directory in system file explorer.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens external website in default browser.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-website", () =>
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/")
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens official wiki page in external browser.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-wiki", () =>
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki")
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens workshop page in external browser.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-workshop", () =>
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Workshop")
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens configuration folder in system file explorer.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves available themes from filesystem.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("get-themes", () => getThemes());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens theme directory in file explorer.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-theme", () => shell.openPath(themeFolderPath));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads themes from disk dynamically.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("reload-themes", async () => {
|
||||||
|
return await reloadThemes();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a runtime feature flag and persists it to disk.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* - Validates key against whitelist
|
||||||
|
* - Prevents unnecessary writes if value is unchanged
|
||||||
|
* - Updates in-memory config object
|
||||||
|
* - Persists to configuration file
|
||||||
|
*
|
||||||
|
* Security note:
|
||||||
|
* Only whitelisted keys can be modified via IPC to prevent config injection.
|
||||||
|
*
|
||||||
|
* @param {Electron.IpcMainEvent} event
|
||||||
|
* @param {{key: string, value: any}} payload
|
||||||
|
* @returns {boolean} success state
|
||||||
|
*/
|
||||||
|
ipcMain.handle("set-feature", (event, { key, value }) => {
|
||||||
|
try {
|
||||||
|
if (!FEATURE_WHITELIST.has(key)) {
|
||||||
|
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { registerIpcHandlers };
|
||||||
90
app/pathValidator.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached resolved default download path.
|
||||||
|
* Initialized lazily on first access to avoid unnecessary imports.
|
||||||
|
*/
|
||||||
|
let _defaultPath = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the application's default download directory.
|
||||||
|
*
|
||||||
|
* This value is lazily loaded and cached after first call.
|
||||||
|
* The path originates from server configuration helpers.
|
||||||
|
*
|
||||||
|
* @returns {string} Absolute default download path
|
||||||
|
*/
|
||||||
|
function getDefaultDownloadPath() {
|
||||||
|
|
||||||
|
if (!_defaultPath) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-loaded dependency providing the configured default download folder.
|
||||||
|
* Required only when function is executed to reduce startup cost.
|
||||||
|
*/
|
||||||
|
const { defaultDownloadFolder } = require("../server/helpers/path.helpers");
|
||||||
|
|
||||||
|
_defaultPath = defaultDownloadFolder;
|
||||||
|
|
||||||
|
}
|
||||||
|
return _defaultPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and sanitizes a user-provided download path.
|
||||||
|
*
|
||||||
|
* Steps performed:
|
||||||
|
* - Resolves absolute real filesystem path
|
||||||
|
* - Normalizes symbolic links via realpathSync
|
||||||
|
* - Checks path safety rules (blocks system-critical directories)
|
||||||
|
* - Falls back to default path if input is empty
|
||||||
|
*
|
||||||
|
* @param {string} userPath - User-provided directory path
|
||||||
|
* @returns {string} Sanitized absolute path
|
||||||
|
* @throws Error If path is invalid, unsafe, or inaccessible
|
||||||
|
*/
|
||||||
|
function validateDownloadPath(userPath) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-loaded helper used to enforce safe directory constraints.
|
||||||
|
* Prevents writes to protected system folders.
|
||||||
|
*/
|
||||||
|
const { isSafePath } = require("../server/helpers/validation.helpers");
|
||||||
|
|
||||||
|
if (!userPath) return getDefaultDownloadPath();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves filesystem symlinks to ensure canonical absolute path.
|
||||||
|
* Required to prevent path traversal or alias bypass.
|
||||||
|
*/
|
||||||
|
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||||
|
|
||||||
|
if (!fs.existsSync(resolved)) {
|
||||||
|
fs.mkdirSync(resolved, { recursive: true });
|
||||||
|
logger.info(`Download folder created: ${resolved}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const real = fs.realpathSync(resolved);
|
||||||
|
|
||||||
|
if (!isSafePath(resolved)) {
|
||||||
|
throw new Error("Path not allowed: system folders are blocked!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return real;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles invalid filesystem paths, permissions issues, or resolution failures.
|
||||||
|
* Logs diagnostic information before propagating a sanitized error.
|
||||||
|
*/
|
||||||
|
logger.error(`Invalid download path: ${userPath} — ${err.message}`);
|
||||||
|
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { validateDownloadPath, getDefaultDownloadPath };
|
||||||
107
app/splashManager.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const { BrowserWindow, app } = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the splash screen BrowserWindow instance.
|
||||||
|
* Used during application startup before main window is ready.
|
||||||
|
*/
|
||||||
|
let splashWindow = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and displays the splash screen window.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Initializes a frameless, always-on-top splash window
|
||||||
|
* - Loads splash HTML UI
|
||||||
|
* - Dynamically injects banner image depending on dev/production mode
|
||||||
|
* - Acts as startup visual feedback before main window loads
|
||||||
|
*/
|
||||||
|
function createSplashWindow() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for splash screen BrowserWindow.
|
||||||
|
* Optimized for minimal UI and fast startup display.
|
||||||
|
*/
|
||||||
|
const splashOptions = {
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
frame: false,
|
||||||
|
transparent: true,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
resizable: false,
|
||||||
|
skipTaskbar: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the splash screen window.
|
||||||
|
*/
|
||||||
|
splashWindow = new BrowserWindow(splashOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolute path to splash HTML file used as initial UI.
|
||||||
|
* Loaded locally from application bundle or project folder.
|
||||||
|
*/
|
||||||
|
const splashPath = path.join(__dirname, "../public/splash.html");
|
||||||
|
splashWindow.loadFile(splashPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes once splash HTML is fully loaded.
|
||||||
|
* Used here to inject dynamic assets (banner image path).
|
||||||
|
*/
|
||||||
|
splashWindow.webContents.on('did-finish-load', () => {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves splash banner image path depending on runtime mode:
|
||||||
|
* - Production: bundled resources directory
|
||||||
|
* - Development: local build directory
|
||||||
|
*/
|
||||||
|
let bannerPath;
|
||||||
|
|
||||||
|
// Check if app is packaged
|
||||||
|
if (app.isPackaged) {
|
||||||
|
bannerPath = path.join(process.resourcesPath, 'banner.png');
|
||||||
|
} else {
|
||||||
|
// In dev, use build folder
|
||||||
|
bannerPath = path.join(__dirname, '../build/banner.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects runtime image source into splash DOM.
|
||||||
|
* Uses file:// protocol to load local image safely in Electron context.
|
||||||
|
*/
|
||||||
|
splashWindow.webContents.executeJavaScript(`
|
||||||
|
document.querySelector('img[alt="Freedom Loader"]').src = 'file:///${bannerPath.replace(/\\/g, '/')}';
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes and cleans up the splash screen window.
|
||||||
|
*
|
||||||
|
* Should be called once main window is ready to display.
|
||||||
|
*/
|
||||||
|
function closeSplashWindow() {
|
||||||
|
if (splashWindow) {
|
||||||
|
splashWindow.close();
|
||||||
|
splashWindow = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates splash screen progress indicator from main process.
|
||||||
|
*
|
||||||
|
* Sends a step value to renderer via injected JS function.
|
||||||
|
*
|
||||||
|
* @param {number} step - Current initialization step or progress value
|
||||||
|
*/
|
||||||
|
function setSplashProgress(step) {
|
||||||
|
if (splashWindow) {
|
||||||
|
splashWindow.webContents.executeJavaScript(`window.setProgress(${step})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createSplashWindow, closeSplashWindow, setSplashProgress };
|
||||||
337
app/themeManager.js
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
/**
|
||||||
|
* Theme loading and parsing system.
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - Folder-based themes
|
||||||
|
* - ZIP-based themes
|
||||||
|
* - Theme validation against required schema
|
||||||
|
* - Image embedding as base64 data URIs
|
||||||
|
* - Cached theme registry with sorted priority order
|
||||||
|
*
|
||||||
|
* Acts as the core theme ingestion pipeline for the application UI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
const JSZip = require("jszip");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum allowed size (in bytes) for theme images.
|
||||||
|
* Prevents memory abuse from large embedded assets.
|
||||||
|
*/
|
||||||
|
const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of supported image file extensions for theme assets.
|
||||||
|
*/
|
||||||
|
const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed base filenames for theme background/cover images.
|
||||||
|
* Restricts theme asset resolution to known conventions.
|
||||||
|
*/
|
||||||
|
const ALLOWED_IMAGE_NAMES = ["cover", "background"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priority order used to sort themes in UI.
|
||||||
|
* Themes not listed are placed at the end.
|
||||||
|
*/
|
||||||
|
const THEME_ORDER = ["dark", "light"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required JSON schema paths for a valid theme definition.
|
||||||
|
* Each entry represents a nested property path that must exist.
|
||||||
|
*/
|
||||||
|
const REQUIRED_KEYS = [
|
||||||
|
["meta", "name"],
|
||||||
|
["meta", "author"],
|
||||||
|
["meta", "version"],
|
||||||
|
["meta", "formatVersion"],
|
||||||
|
["style", "colors", "background"],
|
||||||
|
["style", "colors", "text", "default"],
|
||||||
|
["style", "form", "button", "background"],
|
||||||
|
["style", "progressBar", "fill"],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely retrieves a deeply nested value from an object using a key path array.
|
||||||
|
*
|
||||||
|
* @param {object} obj - Target object
|
||||||
|
* @param {string[]} keys - Property path segments
|
||||||
|
* @returns {*} value or undefined if path is invalid
|
||||||
|
*/
|
||||||
|
function getNestedValue(obj, keys) {
|
||||||
|
return keys.reduce((acc, key) => acc?.[key], obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates theme JSON structure against required schema.
|
||||||
|
*
|
||||||
|
* Ensures all mandatory configuration keys exist before theme is accepted.
|
||||||
|
*
|
||||||
|
* @param {object} json - Parsed theme JSON
|
||||||
|
* @returns {{valid: boolean, reason?: string}}
|
||||||
|
*/
|
||||||
|
function validateThemeJson(json) {
|
||||||
|
for (const keyPath of REQUIRED_KEYS) {
|
||||||
|
if (getNestedValue(json, keyPath) === undefined) {
|
||||||
|
return { valid: false, reason: `Missing key: ${keyPath.join(".")}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes raw theme JSON into application-ready theme object.
|
||||||
|
*
|
||||||
|
* @param {string} themeId
|
||||||
|
* @param {object} themeJson
|
||||||
|
* @param {string|null} imageData - Base64 encoded theme image
|
||||||
|
* @returns {object} normalized theme object
|
||||||
|
*/
|
||||||
|
function buildThemeObject(themeId, themeJson, imageData) {
|
||||||
|
return {
|
||||||
|
id: themeId.toLowerCase(),
|
||||||
|
name: themeJson.meta.name || themeId,
|
||||||
|
author: themeJson.meta.author || "Unknown",
|
||||||
|
version: themeJson.meta.version,
|
||||||
|
subtitle: themeJson.meta.subtitle || "",
|
||||||
|
style: themeJson.style,
|
||||||
|
image: imageData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and encodes an image from theme archive into base64 data URI.
|
||||||
|
*
|
||||||
|
* Applies safety checks:
|
||||||
|
* - Rejects images exceeding MAX_IMAGE_SIZE
|
||||||
|
* - Converts file extension into valid MIME type
|
||||||
|
*
|
||||||
|
* @param {Buffer} buffer
|
||||||
|
* @param {string} filename
|
||||||
|
* @returns {string|null} data URI or null if invalid
|
||||||
|
*/
|
||||||
|
function extractImage(buffer, filename) {
|
||||||
|
if (buffer.length > MAX_IMAGE_SIZE) {
|
||||||
|
logger.warn(`Image too large, ignoring`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ext = path.extname(filename).toLowerCase().replace(".", "");
|
||||||
|
const mime = ext === "jpg" || ext === "jpeg" ? "jpeg" : ext;
|
||||||
|
return `data:image/${mime};base64,${buffer.toString("base64")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a theme from a ZIP archive.
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* - Extract ZIP content
|
||||||
|
* - Locate .theme.json file
|
||||||
|
* - Validate JSON structure
|
||||||
|
* - Extract optional image asset
|
||||||
|
* - Build normalized theme object
|
||||||
|
*
|
||||||
|
* @param {string} zipPath
|
||||||
|
* @param {string} themeId
|
||||||
|
* @returns {Promise<object|null>}
|
||||||
|
*/
|
||||||
|
async function loadThemeFromZip(zipPath, themeId) {
|
||||||
|
try {
|
||||||
|
const zipBuffer = fs.readFileSync(zipPath);
|
||||||
|
const zip = await JSZip.loadAsync(zipBuffer);
|
||||||
|
|
||||||
|
const jsonFile = Object.keys(zip.files).find(f => f.endsWith(".theme.json"));
|
||||||
|
if (!jsonFile) { logger.warn(`Theme ${themeId}: no .theme.json, skipping`); return null; }
|
||||||
|
|
||||||
|
let themeJson;
|
||||||
|
try { themeJson = JSON.parse(await zip.files[jsonFile].async("string")); }
|
||||||
|
catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
|
||||||
|
|
||||||
|
const validation = validateThemeJson(themeJson);
|
||||||
|
if (!validation.valid) { logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`); return null; }
|
||||||
|
|
||||||
|
let imageData = null;
|
||||||
|
const imageFile = Object.keys(zip.files).find(f => {
|
||||||
|
const ext = path.extname(f).toLowerCase();
|
||||||
|
const name = path.basename(f, ext).toLowerCase();
|
||||||
|
return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (imageFile) {
|
||||||
|
const buf = await zip.files[imageFile].async("nodebuffer");
|
||||||
|
imageData = extractImage(buf, imageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Theme loaded (zip): ${themeId}`);
|
||||||
|
return buildThemeObject(themeId, themeJson, imageData);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Theme ${themeId}: failed to load zip — ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a theme from a directory structure.
|
||||||
|
*
|
||||||
|
* Equivalent to ZIP loader but works on filesystem directly.
|
||||||
|
*
|
||||||
|
* @param {string} folderPath
|
||||||
|
* @param {string} themeId
|
||||||
|
* @returns {Promise<object|null>}
|
||||||
|
*/
|
||||||
|
async function loadThemeFromFolder(folderPath, themeId) {
|
||||||
|
try {
|
||||||
|
const jsonFile = fs.readdirSync(folderPath).find(f => f.endsWith(".theme.json"));
|
||||||
|
if (!jsonFile) { logger.warn(`Theme ${themeId}: no .theme.json, skipping`); return null; }
|
||||||
|
|
||||||
|
let themeJson;
|
||||||
|
try { themeJson = JSON.parse(fs.readFileSync(path.join(folderPath, jsonFile), "utf-8")); }
|
||||||
|
catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
|
||||||
|
|
||||||
|
const validation = validateThemeJson(themeJson);
|
||||||
|
if (!validation.valid) { logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`); return null; }
|
||||||
|
|
||||||
|
let imageData = null;
|
||||||
|
const imageFile = fs.readdirSync(folderPath).find(f => {
|
||||||
|
const ext = path.extname(f).toLowerCase();
|
||||||
|
const name = path.basename(f, ext).toLowerCase();
|
||||||
|
return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (imageFile) {
|
||||||
|
const buf = fs.readFileSync(path.join(folderPath, imageFile));
|
||||||
|
imageData = extractImage(buf, imageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Theme loaded (folder): ${themeId}`);
|
||||||
|
return buildThemeObject(themeId, themeJson, imageData);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Theme ${themeId}: failed to load folder — ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory cache of loaded themes.
|
||||||
|
* Avoids re-parsing filesystem on every request.
|
||||||
|
*/
|
||||||
|
let cachedThemes = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active theme directory path used as source of truth.
|
||||||
|
*/
|
||||||
|
let themeFolderPath = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active theme directory path.
|
||||||
|
*
|
||||||
|
* @param {string} folderPath
|
||||||
|
*/
|
||||||
|
function setThemeFolderPath(folderPath) {
|
||||||
|
themeFolderPath = folderPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all themes from the configured theme directory.
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - ZIP themes (.zip)
|
||||||
|
* - Folder themes (directory-based)
|
||||||
|
*
|
||||||
|
* Iterates through filesystem entries and delegates parsing
|
||||||
|
* to the appropriate loader (ZIP or folder).
|
||||||
|
*
|
||||||
|
* @returns {Promise<object[]>} List of valid theme objects
|
||||||
|
*/
|
||||||
|
async function loadThemesFromFolder() {
|
||||||
|
const themes = [];
|
||||||
|
|
||||||
|
if (!fs.existsSync(themeFolderPath)) {
|
||||||
|
logger.warn(`Theme folder not found: ${themeFolderPath}`);
|
||||||
|
return themes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(themeFolderPath);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(themeFolderPath, file);
|
||||||
|
const themeId = path.basename(file, path.extname(file));
|
||||||
|
|
||||||
|
if (file.endsWith(".zip")) {
|
||||||
|
const theme = await loadThemeFromZip(filePath, themeId);
|
||||||
|
if (theme) themes.push(theme);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.statSync(filePath).isDirectory()) {
|
||||||
|
const theme = await loadThemeFromFolder(filePath, themeId);
|
||||||
|
if (theme) themes.push(theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return themes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns sorting priority for a theme ID.
|
||||||
|
*
|
||||||
|
* Lower index = higher priority.
|
||||||
|
* Themes not in THEME_ORDER are placed at the end.
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function getThemeOrder(id) {
|
||||||
|
const index = THEME_ORDER.indexOf(id);
|
||||||
|
return index === -1 ? Infinity : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the theme system.
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* - Sets theme folder path
|
||||||
|
* - Loads all themes from disk
|
||||||
|
* - Sorts themes by priority order
|
||||||
|
* - Caches result in memory
|
||||||
|
*
|
||||||
|
* @param {string} folderPath
|
||||||
|
*/
|
||||||
|
async function initThemes(folderPath) {
|
||||||
|
setThemeFolderPath(folderPath);
|
||||||
|
const themes = await loadThemesFromFolder();
|
||||||
|
logger.info(`Themes before sort: ${themes.map(t => t.id).join(", ")}`);
|
||||||
|
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
|
||||||
|
logger.info(`Themes after sort: ${cachedThemes.map(t => t.id).join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns cached themes.
|
||||||
|
*
|
||||||
|
* If themes are not loaded yet, returns empty array.
|
||||||
|
*
|
||||||
|
* @returns {object[]}
|
||||||
|
*/
|
||||||
|
function getThemes() {
|
||||||
|
return cachedThemes ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads themes from disk and refreshes cache.
|
||||||
|
*
|
||||||
|
* Used when themes are modified at runtime without restart.
|
||||||
|
*
|
||||||
|
* @returns {Promise<object[]>}
|
||||||
|
*/
|
||||||
|
async function reloadThemes() {
|
||||||
|
const themes = await loadThemesFromFolder();
|
||||||
|
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
|
||||||
|
logger.info(`Themes reloaded: ${cachedThemes.length} theme(s)`);
|
||||||
|
return cachedThemes;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initThemes, getThemes , reloadThemes };
|
||||||
108
app/windowManager.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Electron main window module dependencies.
|
||||||
|
* Handles BrowserWindow lifecycle and application UI shell.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { BrowserWindow, app } = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
const { configFeatures } = require("../config");
|
||||||
|
const config = require("../config");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton reference to the main Electron BrowserWindow instance.
|
||||||
|
* Ensures only one application window exists at runtime.
|
||||||
|
*/
|
||||||
|
let mainWindow = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and initializes the main Electron application window.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Ensures singleton window instance (prevents duplicates)
|
||||||
|
* - Configures window options (size, icon, preload, security settings)
|
||||||
|
* - Loads frontend application from local dev server
|
||||||
|
* - Handles lifecycle events (close, errors)
|
||||||
|
*
|
||||||
|
* @returns {Promise<BrowserWindow>} The created or existing window instance
|
||||||
|
*/
|
||||||
|
async function createMainWindow() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents multiple instances of the main window.
|
||||||
|
* Returns existing instance if already created.
|
||||||
|
*/
|
||||||
|
if (mainWindow) {
|
||||||
|
logger.warn("Window already exists, no new creation!");
|
||||||
|
return mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves application icon path depending on runtime mode:
|
||||||
|
* - Development: local build folder
|
||||||
|
* - Production: packaged Electron resources
|
||||||
|
*/
|
||||||
|
const iconPath = config.devMode
|
||||||
|
? path.join(__dirname, "../build/app-icon.ico")
|
||||||
|
: path.join(process.resourcesPath, "build/app-icon.ico");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron BrowserWindow configuration object.
|
||||||
|
* Defines UI behavior, security settings, and preload bridge.
|
||||||
|
*/
|
||||||
|
const windowOptions = {
|
||||||
|
title: `Freedom Loader ${config.version}`,
|
||||||
|
icon: iconPath,
|
||||||
|
width: 750,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 750,
|
||||||
|
minHeight: 800,
|
||||||
|
frame: !configFeatures.customTopBar,
|
||||||
|
devTools: !app.isPackaged,
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, "../preload.js"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mainWindow = new BrowserWindow(windowOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the frontend application served by the local dev server.
|
||||||
|
* In production, this could be replaced with a file:// build.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||||
|
logger.info("Window loaded");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Window loading error:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up window reference when the main window is closed.
|
||||||
|
* Prevents memory leaks and allows recreation if needed.
|
||||||
|
*/
|
||||||
|
mainWindow.on("closed", () => {
|
||||||
|
logger.info("Main window closed");
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current main window instance.
|
||||||
|
*
|
||||||
|
* Useful for IPC handlers and background services
|
||||||
|
* needing access to the renderer process.
|
||||||
|
*
|
||||||
|
* @returns {BrowserWindow | null}
|
||||||
|
*/
|
||||||
|
function getMainWindow() {
|
||||||
|
return mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createMainWindow, getMainWindow };
|
||||||
35
app/ytDlpUpdater.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to update the local yt-dlp binary using its built-in update command.
|
||||||
|
*
|
||||||
|
* This function:
|
||||||
|
* - Executes yt-dlp with the `-U` flag
|
||||||
|
* - Logs update output or warnings if update fails
|
||||||
|
* - Never throws: update failure is non-blocking by design
|
||||||
|
*
|
||||||
|
* @param {string} ytDlpPath - Absolute path to the yt-dlp executable
|
||||||
|
*/
|
||||||
|
function updateYtDlp(ytDlpPath) {
|
||||||
|
|
||||||
|
logger.info("yt-dlp update check starting...");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes yt-dlp CLI process in a non-interactive mode.
|
||||||
|
* Used here specifically for self-update via `-U` flag.
|
||||||
|
*/
|
||||||
|
execFile(ytDlpPath, ["-U"], (err, stdout) => {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles yt-dlp update result:
|
||||||
|
* - Logs warning if update fails (network, binary issues, permissions)
|
||||||
|
* - Logs stdout output when update succeeds
|
||||||
|
*/
|
||||||
|
if (err) logger.warn("yt-dlp update failed (continuing):", err.message);
|
||||||
|
|
||||||
|
else logger.info(`yt-dlp update: ${stdout.trim()}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { updateYtDlp };
|
||||||
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 88 KiB |
BIN
build/app-icon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
build/app-icon.png
Executable file
|
After Width: | Height: | Size: 107 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/error.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 5.6 KiB |
109
config.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const packageJson = require("./package.json");
|
||||||
|
const { app } = require("electron");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the application is running in development mode.
|
||||||
|
* Determined by Electron's packaging state.
|
||||||
|
*/
|
||||||
|
const devMode = !app.isPackaged;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the configuration file path depending on runtime environment.
|
||||||
|
*
|
||||||
|
* In development:
|
||||||
|
* - Uses local config/config.dev.json
|
||||||
|
* - Creates it from default if missing
|
||||||
|
*
|
||||||
|
* In production:
|
||||||
|
* - Uses app userData/config.json
|
||||||
|
* - Creates it from bundled default config if missing
|
||||||
|
*
|
||||||
|
* @returns {string} Absolute path to the active configuration file
|
||||||
|
*/
|
||||||
|
function resolveConfigPath() {
|
||||||
|
if (devMode) {
|
||||||
|
const devConfigPath = path.join(__dirname, "config", "config.dev.json");
|
||||||
|
|
||||||
|
if (!fs.existsSync(devConfigPath)) {
|
||||||
|
const defaultConfigPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"config",
|
||||||
|
"config.default.json"
|
||||||
|
);
|
||||||
|
fs.copyFileSync(defaultConfigPath, devConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return devConfigPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userConfigPath = path.join(app.getPath("userData"), "config.json");
|
||||||
|
|
||||||
|
if (!fs.existsSync(userConfigPath)) {
|
||||||
|
const defaultConfigPath = path.join(
|
||||||
|
process.resourcesPath,
|
||||||
|
"config",
|
||||||
|
"config.default.json"
|
||||||
|
);
|
||||||
|
fs.copyFileSync(defaultConfigPath, userConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userConfigPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the active configuration file used at runtime.
|
||||||
|
*/
|
||||||
|
const featuresPath = resolveConfigPath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and parses the feature configuration file.
|
||||||
|
*
|
||||||
|
* @returns {Object} Parsed feature flags configuration
|
||||||
|
* @throws {Error} If the configuration file is missing or invalid
|
||||||
|
*/
|
||||||
|
function loadFeatures() {
|
||||||
|
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||||
|
return JSON.parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory snapshot of application feature flags.
|
||||||
|
*
|
||||||
|
* Note: Changes to the config file are not automatically reflected.
|
||||||
|
* A restart or reload is required.
|
||||||
|
*/
|
||||||
|
const configFeatures = loadFeatures();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Application version from package.json
|
||||||
|
*/
|
||||||
|
version: devMode ? `dev-${packageJson.version}` : `v${packageJson.version}` ,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend Express / internal server port
|
||||||
|
*/
|
||||||
|
applicationPort: "8787",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the app is running in development mode
|
||||||
|
*/
|
||||||
|
devMode,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord RPC application identifier
|
||||||
|
*/
|
||||||
|
DiscordRPCID: "1410934537051181146",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime feature flags loaded from configuration file
|
||||||
|
*/
|
||||||
|
configFeatures,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the active configuration file
|
||||||
|
*/
|
||||||
|
featuresPath,
|
||||||
|
};
|
||||||
18
config/config.default.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"autoUpdate": true,
|
||||||
|
"discordRPC": true,
|
||||||
|
"customTopBar": true,
|
||||||
|
"autoCheckInfo": true,
|
||||||
|
"addThumbnail": true,
|
||||||
|
"addMetadata": true,
|
||||||
|
"verboseLogs": false,
|
||||||
|
"autoDownloadPlaylist": true,
|
||||||
|
"createPlaylistFolders": true,
|
||||||
|
"customCodec": "h264",
|
||||||
|
"logSystem": true,
|
||||||
|
"outputTitleCheck": true,
|
||||||
|
"downloadSystem": true,
|
||||||
|
"notifySystem": true,
|
||||||
|
"enableHardwareAcceleration": true,
|
||||||
|
"theme": "dark"
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
|
||||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
packagerConfig: {
|
|
||||||
asar: true,
|
|
||||||
},
|
|
||||||
rebuildConfig: {},
|
|
||||||
makers: [
|
|
||||||
{
|
|
||||||
name: '@electron-forge/maker-squirrel',
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '@electron-forge/maker-zip',
|
|
||||||
platforms: ['darwin'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '@electron-forge/maker-deb',
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '@electron-forge/maker-rpm',
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
name: '@electron-forge/plugin-auto-unpack-natives',
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
// Fuses are used to enable/disable various Electron functionality
|
|
||||||
// at package time, before code signing the application
|
|
||||||
new FusesPlugin({
|
|
||||||
version: FuseVersion.V1,
|
|
||||||
[FuseV1Options.RunAsNode]: false,
|
|
||||||
[FuseV1Options.EnableCookieEncryption]: true,
|
|
||||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
|
||||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
|
||||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
|
||||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
268
main.js
@@ -1,165 +1,149 @@
|
|||||||
/*
|
/**
|
||||||
This file is part of Freedom Loader.
|
* Handle crash application and avoid ghost instance
|
||||||
|
*/
|
||||||
|
const { app } = require("electron");
|
||||||
|
|
||||||
Copyright (C) 2025 MasterAcnolo
|
process.on("uncaughtException", (err) => {
|
||||||
|
console.error("Uncaught exception:", err);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
Freedom Loader is free software: you can redistribute it and/or modify
|
process.on("unhandledRejection", (reason) => {
|
||||||
it under the terms of the GNU General Public License as published by
|
console.error("Unhandled rejection:", reason);
|
||||||
the Free Software Foundation, either version 3 of the License.
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
Freedom Loader 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/>.
|
* True if this is the primary instance (lock acquired successfully)
|
||||||
*/
|
*/
|
||||||
|
const isPrimaryInstance = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
/**
|
||||||
app.disableHardwareAcceleration(); // Désactive l'accélération GPU pour éviter les erreurs GPU sur certains systèmes
|
* If we are on a second instance
|
||||||
const path = require("path");
|
*/
|
||||||
const os = require("os");
|
if (!isPrimaryInstance) {
|
||||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
app.quit();
|
||||||
|
process.exit(0);
|
||||||
let mainWindow;
|
|
||||||
// Utilise le vrai dossier de logs défini dans logger.js
|
|
||||||
const logsFolderPath = logDir;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Fonction principale qui crée la fenêtre principale de l'application.
|
|
||||||
Elle évite la création multiple et configure les dimensions et options de la fenêtre.
|
|
||||||
Charge l'URL locale du serveur Express et gère les erreurs éventuelles.
|
|
||||||
*/
|
|
||||||
async function createWindow() {
|
|
||||||
logger.info("Création de la fenêtre...");
|
|
||||||
|
|
||||||
if (mainWindow) {
|
|
||||||
logger.warn("La fenêtre existe déjà, pas de nouvelle création");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
title: "Freedom Loader",
|
|
||||||
width: 750,
|
|
||||||
height: 800,
|
|
||||||
minWidth: 750,
|
|
||||||
minHeight: 800,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
contextIsolation: true,
|
|
||||||
preload: path.join(__dirname, "preload.js"), // Chargement du preload script sécurisé
|
|
||||||
},
|
|
||||||
// titleBarStyle: 'hidden', // Option possible pour une barre de titre personnalisée
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mainWindow.loadURL("http://localhost:8080");
|
|
||||||
logger.info("Fenêtre chargée");
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Erreur chargement fenêtre:", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Événement déclenché à la fermeture de la fenêtre principale
|
|
||||||
mainWindow.on("closed", () => {
|
|
||||||
logger.info("Fenêtre principale fermée");
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Définition du chemin par défaut pour les téléchargements.
|
* Set App Name and AppID
|
||||||
Ici, on utilise le dossier 'Downloads' de l'utilisateur avec un sous-dossier spécifique à l'application.
|
*/
|
||||||
*/
|
app.setName("Freedom Loader");
|
||||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
app.setAppUserModelId("com.masteracnolo.freedomloader");
|
||||||
|
|
||||||
/*
|
|
||||||
Gestionnaire IPC qui permet au renderer de demander à l'utilisateur
|
/**
|
||||||
de sélectionner un dossier via une boîte de dialogue native.
|
* Load the app dependencies for hardware choice
|
||||||
On retourne le chemin sélectionné ou null si annulation.
|
*/
|
||||||
*/
|
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||||
ipcMain.handle("select-download-folder", async () => {
|
const { configFeatures } = require("./config");
|
||||||
logger.info("Demande de sélection d'un dossier reçue depuis le renderer");
|
|
||||||
try {
|
/**
|
||||||
const result = await dialog.showOpenDialog({
|
* On start, enable or disable hardWareAcceleration
|
||||||
properties: ["openDirectory"]
|
*/
|
||||||
});
|
if (!configFeatures.enableHardwareAcceleration){
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
app.disableHardwareAcceleration();
|
||||||
logger.info(`Dossier sélectionné : ${result.filePaths[0]}`);
|
logger.info("Disabled Hardware Acceleration")
|
||||||
return result.filePaths[0];
|
} else {
|
||||||
}
|
logger.info("Enable Hardware Acceleration")
|
||||||
return null;
|
}
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Erreur lors de la sélection de dossier : ${err.message}`);
|
/**
|
||||||
return null;
|
* In-memory snapshot of application feature flags.
|
||||||
}
|
*
|
||||||
|
* Note: Changes to the config file are not automatically reflected.
|
||||||
|
* A restart could be required.
|
||||||
|
*/
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
|
if(config.devMode){
|
||||||
|
/**
|
||||||
|
* Start devTron extensions - @see https://github.com/electron/devtron
|
||||||
|
*/
|
||||||
|
const { devtron } = require('@electron/devtron');
|
||||||
|
devtron.install();
|
||||||
|
logger.info("Loaded DevTron Extension")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the app dependencies
|
||||||
|
*/
|
||||||
|
const { initThemes } = require("./app/themeManager");
|
||||||
|
|
||||||
|
const { initAutoUpdater } = require("./app/autoUpdater");
|
||||||
|
const { startRPC, stopRPC } = require("./app/discordRPC");
|
||||||
|
const { checkNativeDependencies } = require("./app/dependencyCheck");
|
||||||
|
const { updateYtDlp } = require("./app/ytDlpUpdater");
|
||||||
|
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
||||||
|
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
||||||
|
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
|
||||||
|
const { userThemesPath, initUserThemes, isWindows, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If another instance want to run
|
||||||
|
*/
|
||||||
|
app.on("second-instance", () => {
|
||||||
|
logger.info("Second instance detected");
|
||||||
|
|
||||||
|
const mainWindow = require("./app/windowManager").getMainWindow();
|
||||||
|
|
||||||
|
if (!mainWindow) return;
|
||||||
|
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
Gestionnaire IPC pour exposer au renderer le chemin par défaut de téléchargement
|
|
||||||
afin qu'il puisse l'afficher ou l'utiliser comme valeur initiale.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("get-default-download-path", () => {
|
|
||||||
return defaultDownloadPath;
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
Événement déclenché quand l'application est prête.
|
|
||||||
On démarre le serveur Express, puis on crée la fenêtre principale.
|
|
||||||
En cas d'erreur, on log et on quitte l'application proprement.
|
|
||||||
*/
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
logSessionStart();
|
logSessionStart(logDir, defaultDownloadFolder);
|
||||||
logger.info("App prête, démarrage du serveur Express...");
|
|
||||||
const expressServer = require("./server/server.js");
|
createSplashWindow();
|
||||||
|
|
||||||
|
if (!config.devMode && !checkNativeDependencies()) return;
|
||||||
|
|
||||||
|
const { userYtDlp } = require("./server/helpers/path.helpers");
|
||||||
|
updateYtDlp(userYtDlp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await expressServer.startServer();
|
setSplashProgress(0); // Checking dependencies
|
||||||
logger.info("Serveur Express démarré");
|
await require("./server/server").startServer();
|
||||||
await createWindow();
|
|
||||||
|
|
||||||
// Ajout du menu personnalisé
|
setSplashProgress(1); // Starting server
|
||||||
const menuTemplate = [
|
registerIpcHandlers(getMainWindow);
|
||||||
// ... Menu standard Electron ...
|
|
||||||
{
|
|
||||||
label: "Logs",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Ouvrir les logs",
|
|
||||||
click: () => {
|
|
||||||
shell.openPath(logsFolderPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Fusionner avec le menu par défaut
|
setSplashProgress(2); // Loading themes
|
||||||
const defaultMenu = Menu.getApplicationMenu();
|
|
||||||
let template = menuTemplate;
|
|
||||||
if (defaultMenu) {
|
|
||||||
template = [...Menu.getApplicationMenu().items.map(item => item), ...menuTemplate];
|
|
||||||
}
|
|
||||||
const finalMenu = Menu.buildFromTemplate(template);
|
|
||||||
Menu.setApplicationMenu(finalMenu);
|
|
||||||
|
|
||||||
} catch (error) {
|
validateBinaries();
|
||||||
logger.error("Erreur serveur ou fenêtre :", error);
|
|
||||||
|
initUserThemes();
|
||||||
|
await initThemes(userThemesPath);
|
||||||
|
|
||||||
|
setSplashProgress(3); // Almost ready
|
||||||
|
await createMainWindow();
|
||||||
|
|
||||||
|
closeSplashWindow();
|
||||||
|
getMainWindow().show();
|
||||||
|
if (configFeatures.discordRPC) startRPC();
|
||||||
|
|
||||||
|
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Boot error:", err);
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
Quitte l'application lorsque toutes les fenêtres sont fermées,
|
|
||||||
sauf sous macOS où il est habituel de garder l'application active.
|
|
||||||
*/
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
logger.info("Toutes fenêtres fermées, quitte l'app");
|
logger.info("Shutting down...");
|
||||||
if (process.platform !== "darwin") app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
app.on("before-quit", async () => {
|
||||||
Avant de quitter l'application, on log la fin de session pour traçabilité.
|
await stopRPC();
|
||||||
*/
|
logger.info("All services stopped. Have a nice day!");
|
||||||
app.on("before-quit", () => {
|
|
||||||
logSessionEnd();
|
logSessionEnd();
|
||||||
});
|
});
|
||||||
5436
package-lock.json
generated
145
package.json
@@ -1,67 +1,168 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.1.3",
|
"version": "1.6.0",
|
||||||
"author": "MasterAcnolo",
|
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
|
||||||
"description": "Freedom Loader",
|
"description": "Freedom Loader",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "electron --trace-warnings .",
|
||||||
"package": "electron-forge package",
|
"postinstall": "chmod +x resources/binaries/linux/* 2>/dev/null || true",
|
||||||
"make": "electron-forge make",
|
"build": "electron-builder",
|
||||||
"dist": "electron-builder"
|
"build:win": "electron-builder --win",
|
||||||
|
"build:linux": "electron-builder --linux",
|
||||||
|
"build:all": "electron-builder -wl",
|
||||||
|
"update": "npm update"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
|
"electron-updater": "^6.6.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"webidl-conversions": "^8.0.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0"
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.8.1",
|
"@electron/devtron": "^2.1.1",
|
||||||
"@electron-forge/maker-deb": "^7.8.1",
|
"electron": "^41.2.0",
|
||||||
"@electron-forge/maker-rpm": "^7.8.1",
|
"electron-builder": "^25.1.8"
|
||||||
"@electron-forge/maker-squirrel": "^7.8.1",
|
|
||||||
"@electron-forge/maker-zip": "^7.8.1",
|
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
|
||||||
"@electron-forge/plugin-fuses": "^7.8.1",
|
|
||||||
"@electron/fuses": "^1.8.0",
|
|
||||||
"electron": "^37.1.0",
|
|
||||||
"electron-builder": "^26.0.12"
|
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.masteracnolo.freedomloader",
|
"appId": "com.masteracnolo.freedomloader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
|
"publish": [
|
||||||
|
{
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "MasterAcnolo",
|
||||||
|
"repo": "Freedom-Loader",
|
||||||
|
"releaseType": "draft"
|
||||||
|
}
|
||||||
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"**/*",
|
"**/*",
|
||||||
"node_modules/**/*",
|
"node_modules/**/*",
|
||||||
"!logs/*.json"
|
"!config/config.dev.json",
|
||||||
|
"!theme/**/*"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "build"
|
"buildResources": "build"
|
||||||
},
|
},
|
||||||
"asar": false,
|
"asar": true,
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
|
"from": "theme",
|
||||||
|
"to": "theme"
|
||||||
|
},
|
||||||
|
{
|
||||||
"from": "build/confirm-icon.png",
|
"from": "build/confirm-icon.png",
|
||||||
"to": "confirm-icon.png"
|
"to": "confirm-icon.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "build/banner.bmp",
|
||||||
|
"to": "banner.bmp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "build/banner.png",
|
||||||
|
"to": "banner.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "build/error.png",
|
||||||
|
"to": "error.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "config/config.default.json",
|
||||||
|
"to": "config/config.default.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"icon": "build/installer-icon.ico"
|
"icon": "build/app-icon.ico",
|
||||||
|
"artifactName": "${productName}-Setup-${version}.${ext}",
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/win-32/yt-dlp.exe",
|
||||||
|
"to": "binaries/yt-dlp.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/win-32/ffmpeg.exe",
|
||||||
|
"to": "binaries/ffmpeg.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/win-32/ffprobe.exe",
|
||||||
|
"to": "binaries/ffprobe.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/win-32/deno.exe",
|
||||||
|
"to": "binaries/deno.exe"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"perMachine": true,
|
"perMachine": true,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
"installerIcon": "build/installer-icon.ico",
|
"installerIcon": "build/app-icon.ico",
|
||||||
"uninstallerIcon": "build/uninstaller-icon.ico",
|
"uninstallerIcon": "build/uninstaller-icon.ico",
|
||||||
|
"installerSidebar": "build/banner.bmp",
|
||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": true,
|
"createStartMenuShortcut": true,
|
||||||
"shortcutName": "Freedom Loader"
|
"shortcutName": "Freedom Loader"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage",
|
||||||
|
"deb",
|
||||||
|
"rpm",
|
||||||
|
"snap"
|
||||||
|
],
|
||||||
|
"category": "Utility",
|
||||||
|
"icon": "build/app-icon.png",
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/linux/yt-dlp",
|
||||||
|
"to": "binaries/yt-dlp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/linux/ffmpeg",
|
||||||
|
"to": "binaries/ffmpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/linux/ffprobe",
|
||||||
|
"to": "binaries/ffprobe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/linux/deno",
|
||||||
|
"to": "binaries/deno"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deb": {
|
||||||
|
"fpm": [
|
||||||
|
"./package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rpm": {
|
||||||
|
"fpm": [
|
||||||
|
"./package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"snap": {
|
||||||
|
"confinement": "strict",
|
||||||
|
"plugs": [
|
||||||
|
"default",
|
||||||
|
"home",
|
||||||
|
"removable-media",
|
||||||
|
"network",
|
||||||
|
"audio-playback",
|
||||||
|
"desktop",
|
||||||
|
"desktop-legacy",
|
||||||
|
"x11",
|
||||||
|
"wayland"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
package/com.masteracnolo.freedomloader.metainfo.xml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>com.masteracnolo.freedomloader</id>
|
||||||
|
|
||||||
|
<name>Freedom Loader</name>
|
||||||
|
|
||||||
|
<summary>GUI for yt-dlp to download video and audio with metadata</summary>
|
||||||
|
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>GPL-3.0-only</project_license>
|
||||||
|
|
||||||
|
<developer id="com.masteracnolo">
|
||||||
|
<name>MasterAcnolo</name>
|
||||||
|
</developer>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
Freedom Loader is a free and open-source graphical interface built
|
||||||
|
around yt-dlp, the powerful command-line media downloader. It lets
|
||||||
|
you download video and audio from hundreds of supported websites
|
||||||
|
without ever touching a terminal, with no ads, no tracking, and
|
||||||
|
no questionable third-party services.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Under the hood, Freedom Loader bundles yt-dlp and ffmpeg directly,
|
||||||
|
so there is nothing extra to install: paste a link, choose a
|
||||||
|
format and quality, and the app handles fetching, converting
|
||||||
|
and tagging the file for you.
|
||||||
|
</p>
|
||||||
|
<p>Features include:</p>
|
||||||
|
<ul>
|
||||||
|
<li>MP4 video or MP3 audio downloads with embedded metadata and thumbnails</li>
|
||||||
|
<li>Quality selection from best to specific resolutions (1080p, 720p, 480p)</li>
|
||||||
|
<li>Codec choice: H.264, H.265, AV1, VP9 and Theora</li>
|
||||||
|
<li>Playlist downloads with automatic per-playlist folders</li>
|
||||||
|
<li>Cookie-based access to age-restricted and member-only content</li>
|
||||||
|
<li>Custom theme system with a web-based theme creator</li>
|
||||||
|
<li>Real-time download progress with network speed indicator</li>
|
||||||
|
<li>Optional Discord Rich Presence integration</li>
|
||||||
|
<li>Automatic application and download-engine updates</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
DRM-protected content is not supported and cannot be downloaded.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<launchable type="desktop-id">freedom-loader.desktop</launchable>
|
||||||
|
|
||||||
|
<url type="homepage">https://github.com/MasterAcnolo/Freedom-Loader</url>
|
||||||
|
<url type="bugtracker">https://github.com/MasterAcnolo/Freedom-Loader/issues</url>
|
||||||
|
<url type="vcs-browser">https://github.com/MasterAcnolo/Freedom-Loader</url>
|
||||||
|
|
||||||
|
<categories>
|
||||||
|
<category>AudioVideo</category>
|
||||||
|
<category>Utility</category>
|
||||||
|
<category>Network</category>
|
||||||
|
</categories>
|
||||||
|
|
||||||
|
<releases>
|
||||||
|
<release version="1.6.0" date="2026-06-23">
|
||||||
|
<description>
|
||||||
|
<p>Latest stable release.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
|
||||||
|
<content_rating type="oars-1.1"/>
|
||||||
|
|
||||||
|
<provides>
|
||||||
|
<binary>freedom-loader</binary>
|
||||||
|
</provides>
|
||||||
|
</component>
|
||||||
25
package/freedom-loader.spec
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
Name: freedom-loader
|
||||||
|
Version: __VERSION__
|
||||||
|
Release: 1%{?dist}
|
||||||
|
Summary: GUI for yt-dlp media downloads
|
||||||
|
License: GPL-3.0-only
|
||||||
|
Source0: freedom-loader-__VERSION__.x86_64.rpm
|
||||||
|
BuildArch: x86_64
|
||||||
|
Requires: glibc, gtk3, nss, libXScrnSaver
|
||||||
|
|
||||||
|
%description
|
||||||
|
GUI for yt-dlp media downloads.
|
||||||
|
|
||||||
|
%install
|
||||||
|
rpm2cpio %{SOURCE0} | (cd %{buildroot} && cpio -idmv)
|
||||||
|
mkdir -p %{buildroot}/usr/bin
|
||||||
|
ln -sf "/opt/Freedom Loader/freedom-loader" %{buildroot}/usr/bin/freedom-loader
|
||||||
|
|
||||||
|
%files
|
||||||
|
/opt/*
|
||||||
|
/usr/bin/freedom-loader
|
||||||
|
/usr/share/applications/*
|
||||||
|
/usr/share/metainfo/*
|
||||||
|
/usr/share/icons/hicolor
|
||||||
|
|
||||||
|
%changelog
|
||||||
148
preload.js
@@ -1,31 +1,129 @@
|
|||||||
/*
|
|
||||||
This file is part of Freedom Loader.
|
|
||||||
|
|
||||||
Copyright (C) 2025 MasterAcnolo
|
|
||||||
|
|
||||||
Freedom Loader 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.
|
|
||||||
|
|
||||||
Freedom Loader 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { contextBridge, ipcRenderer } = require("electron");
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Expose dans le contexte global (window.electronAPI) deux méthodes sécurisées
|
* Exposes the main application API to the renderer process.
|
||||||
pour que le renderer puisse interagir avec le main process via IPC.
|
*
|
||||||
|
* This API acts as a secure bridge between the renderer and the
|
||||||
- getDefaultDownloadPath : récupère le chemin de téléchargement par défaut.
|
* Electron main process using IPC (Inter-Process Communication).
|
||||||
- selectDownloadFolder : ouvre la boîte de dialogue pour choisir un dossier.
|
* All calls are explicitly whitelisted.
|
||||||
*/
|
*/
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return process.platform to renderer
|
||||||
|
*/
|
||||||
|
getProcessPlatform: () => process.platform,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default system download directory.
|
||||||
|
*/
|
||||||
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a native dialog allowing the user to select a download folder.
|
||||||
|
*/
|
||||||
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends download progress value to the main process (used for taskbar / UI sync).
|
||||||
|
*
|
||||||
|
* @param {number} percent - Download progress percentage (0–100)
|
||||||
|
*/
|
||||||
|
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves application feature flags (runtime configuration).
|
||||||
|
*/
|
||||||
|
getFeatures: () => ipcRenderer.invoke("features"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a feature flag dynamically at runtime.
|
||||||
|
*
|
||||||
|
* @param {string} key - Feature name
|
||||||
|
* @param {any} value - Feature value
|
||||||
|
*/
|
||||||
|
setFeature: (key, value) =>
|
||||||
|
ipcRenderer.invoke("set-feature", { key, value }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current application version.
|
||||||
|
*/
|
||||||
|
getVersion: () => ipcRenderer.invoke("version"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a download path before using it for file operations.
|
||||||
|
*
|
||||||
|
* @param {string} path - Path to validate
|
||||||
|
*/
|
||||||
|
getValidatedDownloadPath: (path) =>
|
||||||
|
ipcRenderer.invoke("validate-download-path", path),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves available themes from the filesystem or config layer.
|
||||||
|
*/
|
||||||
|
getThemes: () => ipcRenderer.invoke("get-themes"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces a reload of theme files (useful after modification/import).
|
||||||
|
*/
|
||||||
|
reloadThemes: () => ipcRenderer.invoke("reload-themes"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes window control and developer utilities for the custom topbar UI.
|
||||||
|
*
|
||||||
|
* These methods forward commands to the Electron main process via IPC.
|
||||||
|
*/
|
||||||
|
contextBridge.exposeInMainWorld("topbarAPI", {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimizes the application window.
|
||||||
|
*/
|
||||||
|
minimize: () => ipcRenderer.send("window-minimize"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles maximize/restore state of the application window.
|
||||||
|
*/
|
||||||
|
maximize: () => ipcRenderer.send("window-maximize"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the application window.
|
||||||
|
*/
|
||||||
|
close: () => ipcRenderer.send("window-close"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Chromium DevTools for debugging.
|
||||||
|
*/
|
||||||
|
openDevTools: () => ipcRenderer.send("open-devtools"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens application logs directory or log viewer.
|
||||||
|
*/
|
||||||
|
openLogs: () => ipcRenderer.send("open-logs"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the official website or project homepage.
|
||||||
|
*/
|
||||||
|
openWebsite: () => ipcRenderer.send("open-website"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the theme directory or theme editor interface.
|
||||||
|
*/
|
||||||
|
openTheme: () => ipcRenderer.send("open-theme"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens project wiki/documentation.
|
||||||
|
*/
|
||||||
|
openWiki: () => ipcRenderer.send("open-wiki"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the workshop or external content library.
|
||||||
|
*/
|
||||||
|
openWorkshop: () => ipcRenderer.send("open-workshop"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens configuration/settings panel.
|
||||||
|
*/
|
||||||
|
openConfig: () => ipcRenderer.send("open-config"),
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
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 |
|
Before Width: | Height: | Size: 97 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" transform="scale(64)" fill="#1B1F23"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 968 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22C269.4 401.5 288 370.9 288 336s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8l0 128c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376 80 376c-8.8 0-16-7.2-16-16l0-48c0-8.8 7.2-16 16-16l33.4 0 35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3C249.8 304.6 256 319.6 256 336s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 955 B |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 805 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 35 KiB |
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 |
@@ -7,84 +7,299 @@
|
|||||||
default-src 'self' 'unsafe-inline' data:;
|
default-src 'self' 'unsafe-inline' data:;
|
||||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||||
font-src https://fonts.gstatic.com;
|
font-src https://fonts.gstatic.com;
|
||||||
img-src 'self' data: https://i.ytimg.com;
|
img-src 'self' data: https:;
|
||||||
">
|
">
|
||||||
<title>Freedom Loader</title>
|
|
||||||
|
|
||||||
<!--CSS-->
|
|
||||||
<link rel="stylesheet" href="styles/styles.css">
|
<link rel="stylesheet" href="styles/styles.css">
|
||||||
|
<link rel="stylesheet" href="styles/layout/topbar.css">
|
||||||
|
|
||||||
<!--Icons-->
|
|
||||||
<link rel="icon" href="assets/icons/file-audio-solid.svg" type="image/svg+xml">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="container">
|
<header>
|
||||||
|
<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>
|
||||||
|
<button id="workshop-btn" title="Workshop">Theme Workshop</button>
|
||||||
|
<!-- <button id="theme-btn" title="Theme">Theme</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>
|
||||||
|
</header>
|
||||||
|
|
||||||
<header>
|
<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; align-items: center; gap: 10px;">
|
||||||
|
<span class="setting-title">Thème</span>
|
||||||
|
|
||||||
|
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
||||||
|
|
||||||
|
</select>
|
||||||
|
<button id="refresh-themes-btn" title="Refresh themes" class="refresh-themes-btn">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</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="createPlaylistFolders" id="createPlaylistFolders">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Create Playlist Folders</span>
|
||||||
|
<small>Create a dedicated folder for each playlist with its name.</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="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">
|
||||||
|
<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" 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>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="enableHardwareAcceleration" id="enableHardwareAcceleration">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Enable Hardware Acceleration</span>
|
||||||
|
<small>Uses GPU acceleration for rendering when available. <br> (Be effective on next launch)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Developer Settings -->
|
||||||
|
<details class="settings-section">
|
||||||
|
<summary>Developer Settings</summary>
|
||||||
|
|
||||||
|
<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-theme">Open Theme Folder</button>
|
||||||
|
<button id="open-json-btn">Open JSON Configuration</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container" id="container">
|
||||||
<h1 id="title"> Freedom Loader </h1>
|
<h1 id="title"> Freedom Loader </h1>
|
||||||
<p id="subtitle"> Because why not? </p>
|
<p id="subtitle"> Because why not? </p>
|
||||||
</header>
|
|
||||||
|
<form action="/download" method="POST" id="downloadForm">
|
||||||
<form action="/download" method="POST" id="downloadForm">
|
<div class="url-input-container">
|
||||||
<input name="url" placeholder="URL" id="UrlInput">
|
<input type="text" name="url" placeholder="URL" id="UrlInput">
|
||||||
|
<button type="button" id="pasteBtn" class="paste-btn" title="Paste from clipboard">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9 5H7C5.89543 5 5 5.89543 5 7V19C5 20.1046 5.89543 21 7 21H17C18.1046 21 19 20.1046 19 19V7C19 5.89543 18.1046 5 17 5H15M9 5C9 6.10457 9.89543 7 11 7H13C14.1046 7 15 6.10457 15 5M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5M12 12H15M12 16H15M9 12H9.01M9 16H9.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="checkbox-inline">
|
<label class="checkbox-inline">
|
||||||
|
|
||||||
<label class="checkbox-mp3">
|
<label class="checkbox-mp3">
|
||||||
<input type="checkbox" name="audioOnly" value="1">
|
<input type="checkbox" name="audioOnly" value="1">
|
||||||
<div class="checkmark"></div>
|
<div class="checkmark"></div>
|
||||||
|
</label>
|
||||||
|
Audio Only (MP3)
|
||||||
</label>
|
</label>
|
||||||
<!-- <input type="checkbox" name="audioOnly" value="1"> -->
|
<select name="quality">
|
||||||
Audio seulement (MP3)
|
<option value="best">Best Quality</option>
|
||||||
</label>
|
<option value="medium">Medium Quality</option>
|
||||||
<!-- <input type="checkbox" name="audioOnly" value="1"> Audio seulement (MP3) -->
|
<option value="worst">Lowest Quality</option>
|
||||||
<select name="quality">
|
<option value="1080">1080p</option>
|
||||||
<option value="best">Meilleure qualité</option>
|
<option value="720">720p</option>
|
||||||
<option value="medium">Qualité moyenne</option>
|
<option value="480">480p</option>
|
||||||
<option value="worst">Qualité minimale</option>
|
</select>
|
||||||
<option value="1080">1080p</option>
|
|
||||||
<option value="720">720p</option>
|
|
||||||
<option value="480">480p</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button type="submit">Télécharger</button>
|
<button type="submit" onclick="startProgress()">Download</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur
|
</div>
|
||||||
les données à traiter. Pour cela on définit une route vers /download, ca va
|
|
||||||
être l'espèce de "canal" attribué a ça. Ca va faire que si jamais on veut ensuite recup
|
|
||||||
les infos du form, il faut aller sur ce canal précis (/download). La méthode POST dit
|
|
||||||
que l'on envoie des données.-->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="download-path">
|
<div class="download-path">
|
||||||
Sera enregistré dans <span id="savePath"></span>
|
Will be save on <span id="savePath"></span>
|
||||||
<button id="changePath">Modifier</button>
|
<button id="changePath">Edit</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="downloadStatus"></div>
|
<div class="progress-container">
|
||||||
|
<div id="downloadStage" style="display: none; font-size: 0.85rem; color: var(--default-text-color); margin-top: 8px;"></div>
|
||||||
|
<div id="downloadProgressWrapper">
|
||||||
|
<div id="downloadProgress"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 20px; align-items: center;">
|
||||||
|
<div id="downloadProgressText"></div>
|
||||||
|
<div id="downloadSpeedText"></div>
|
||||||
|
<div id="playlistInfoText" style="display: none; font-size: 0.9rem;"></div>
|
||||||
|
<button type="button" id="stopBtn" onclick="stopDownload()" class="stop-btn" style="display:none;" title="Stop download">■</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="downloadStatus" style="display: none;"></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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
|
<h2 id="reference"> Because Why Not ? </h2>
|
||||||
|
|
||||||
<div class="infos-container">
|
|
||||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="theme-switcher">
|
<!-- Scripts -->
|
||||||
<label for="themeSelect">Thème :</label>
|
<script src="script/toast.js"></script>
|
||||||
<select id="themeSelect" aria-label="Choisir le thème">
|
<script src="script/customConfiguration.js"></script>
|
||||||
<!-- <option value="dark">Sombre</option>
|
<script src="script/clipboardPaste.js"></script>
|
||||||
<option value="light">Clair</option>
|
|
||||||
<option value="Chirac">Chirac</option>
|
|
||||||
<option value="Fanatic">Fanatic</option> -->
|
|
||||||
</select>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="script/custompath.js"></script>
|
<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/downloadstatus.js"></script>
|
||||||
<script src="script/fetchinfo.js"></script>
|
<script src="script/fetchinfo.js"></script>
|
||||||
<script src="script/chirac.js"></script>
|
<script src="script/progressBar.js"></script>
|
||||||
<script src="script/customthemes.js"></script>
|
<script src="script/customthemes.js"></script>
|
||||||
|
<script type="module" src="script/topbar.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
21
public/script/appVersion.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Retrieves the application version from the Electron main process
|
||||||
|
* and displays it in the UI version badge (bottom-right corner).
|
||||||
|
*
|
||||||
|
* This is mainly used for debugging and build identification.
|
||||||
|
*/
|
||||||
|
async function versionLabel() {
|
||||||
|
const appVersion = await window.electronAPI.getVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI element displaying the current application version.
|
||||||
|
* Updated at runtime after IPC call resolves.
|
||||||
|
*/
|
||||||
|
const versionBadge = document.getElementById("version-badge");
|
||||||
|
|
||||||
|
if (!versionBadge) return;
|
||||||
|
|
||||||
|
versionBadge.textContent = `${appVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
versionLabel();
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
This file is part of Freedom Loader.
|
|
||||||
|
|
||||||
Copyright (C) 2025 MasterAcnolo
|
|
||||||
|
|
||||||
Freedom Loader 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.
|
|
||||||
|
|
||||||
Freedom Loader 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Chirac(){
|
|
||||||
const title = document.getElementById("title");
|
|
||||||
title.innerHTML = "Chirac Loader";
|
|
||||||
|
|
||||||
const subtitle = document.getElementById("subtitle");
|
|
||||||
subtitle.innerHTML = "J'aime les pommes";
|
|
||||||
subtitle.style.color= "black";
|
|
||||||
|
|
||||||
document.body.style.backgroundImage = "url('assets/images/goat2.webp')";
|
|
||||||
document.body.style.backgroundSize = "cover";
|
|
||||||
document.body.style.backgroundPosition = "center";
|
|
||||||
document.body.style.backgroundAttachment = "fixed";
|
|
||||||
|
|
||||||
console.log("Je serai le président de tous les français");
|
|
||||||
}
|
|
||||||
40
public/script/clipboardPaste.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Initializes clipboard paste functionality for the URL input field.
|
||||||
|
*
|
||||||
|
* When the user clicks the paste button, the system attempts to read
|
||||||
|
* the clipboard content and inject it into the URL input field.
|
||||||
|
* It also triggers an input event to notify any bound listeners.
|
||||||
|
*/
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const pasteBtn = document.getElementById("pasteBtn");
|
||||||
|
const urlInput = document.getElementById("UrlInput");
|
||||||
|
|
||||||
|
if (!pasteBtn || !urlInput) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles clipboard paste action triggered by the paste button.
|
||||||
|
* Reads clipboard text and inserts it into the URL input field.
|
||||||
|
*/
|
||||||
|
pasteBtn.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
urlInput.value = await navigator.clipboard.readText();
|
||||||
|
|
||||||
|
// Notify frameworks / listeners bound to input changes
|
||||||
|
urlInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
|
||||||
|
urlInput.focus();
|
||||||
|
|
||||||
|
// Temporary UI feedback on paste action
|
||||||
|
pasteBtn.style.backgroundColor = "var(--form-button-bg-hover-color)";
|
||||||
|
setTimeout(() => {
|
||||||
|
pasteBtn.style.backgroundColor = "";
|
||||||
|
}, 200);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Clipboard access failed:", err);
|
||||||
|
|
||||||
|
alert(
|
||||||
|
"Unable to access clipboard. Please paste manually using Ctrl+V."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
27
public/script/customConfiguration.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
async function initPlatformRestrictions() {
|
||||||
|
const platform = await window.electronAPI.getProcessPlatform();
|
||||||
|
|
||||||
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
const refreshBtn = document.getElementById("refresh-themes-btn");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the OS windows
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
const isWindows = platform === "win32";
|
||||||
|
|
||||||
|
console.log(isWindows);
|
||||||
|
|
||||||
|
// If it's windows, the button is not disabled.
|
||||||
|
themeSelect.disabled = !isWindows;
|
||||||
|
refreshBtn.disabled = !isWindows;
|
||||||
|
|
||||||
|
themeSelect.classList.toggle("disabled", !isWindows);
|
||||||
|
refreshBtn.classList.toggle("disabled", !isWindows);
|
||||||
|
|
||||||
|
if (!isWindows) {
|
||||||
|
themeSelect.title = "Themes are only available on Windows";
|
||||||
|
refreshBtn.title = "Themes are only available on Windows";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,71 +1,97 @@
|
|||||||
/*
|
/**
|
||||||
This file is part of Freedom Loader.
|
* Initializes the download path system once the UI is ready.
|
||||||
|
*
|
||||||
Copyright (C) 2025 MasterAcnolo
|
* Responsibilities:
|
||||||
|
* - Restore last known user-selected download path (localStorage)
|
||||||
Freedom Loader is free software: you can redistribute it and/or modify
|
* - Validate it against backend rules
|
||||||
it under the terms of the GNU General Public License as published by
|
* - Synchronize with backend authoritative path
|
||||||
the Free Software Foundation, either version 3 of the License.
|
* - Setup folder selection UI interaction
|
||||||
|
*/
|
||||||
Freedom Loader 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Ce script attend que le DOM soit complètement chargé pour initialiser
|
|
||||||
l'affichage et la gestion du chemin de sauvegarde personnalisé.
|
|
||||||
|
|
||||||
1. Récupère le chemin de téléchargement par défaut depuis le main process via l'API exposée.
|
|
||||||
2. Met à jour le texte affiché dans l'élément avec l'id "savePath".
|
|
||||||
3. Crée un input caché nommé "savePath" dans le formulaire pour envoyer ce chemin lors du submit.
|
|
||||||
4. Ajoute un écouteur sur le bouton "changePath" pour permettre à l'utilisateur
|
|
||||||
de choisir un dossier via une boîte de dialogue native.
|
|
||||||
5. Met à jour l'affichage et la valeur cachée du chemin sélectionné.
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", async () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
const savePathElem = document.getElementById("savePath");
|
const savePathElem = document.getElementById("savePath");
|
||||||
|
const form = document.getElementById("downloadForm");
|
||||||
|
|
||||||
// 1️Essayer de charger depuis le localStorage
|
/**
|
||||||
let savedPath = localStorage.getItem("customDownloadPath");
|
* Hidden form field storing the validated download path
|
||||||
|
* used during download submission.
|
||||||
// 2️Sinon demander le chemin par défaut à l'API Electron
|
*
|
||||||
if (!savedPath) {
|
* Created dynamically if not already present in DOM.
|
||||||
savedPath = await window.electronAPI.getDefaultDownloadPath();
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
// 3 Afficher le chemin
|
|
||||||
if (savePathElem) {
|
|
||||||
savePathElem.textContent = savedPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Créer l'input caché s'il n'existe pas déjà
|
|
||||||
let hidden = document.getElementById("savePathInput");
|
let hidden = document.getElementById("savePathInput");
|
||||||
|
|
||||||
if (!hidden) {
|
if (!hidden) {
|
||||||
hidden = document.createElement("input");
|
hidden = document.createElement("input");
|
||||||
hidden.type = "hidden";
|
hidden.type = "hidden";
|
||||||
hidden.name = "savePath";
|
hidden.name = "savePath";
|
||||||
hidden.id = "savePathInput";
|
hidden.id = "savePathInput";
|
||||||
document.getElementById("downloadForm").appendChild(hidden);
|
form.appendChild(hidden);
|
||||||
}
|
}
|
||||||
hidden.value = savedPath;
|
|
||||||
|
|
||||||
// Gestion du bouton de modification
|
/**
|
||||||
document.getElementById("changePath").addEventListener("click", async () => {
|
* Applies a validated download path received from backend.
|
||||||
const selectedPath = await window.electronAPI.selectDownloadFolder();
|
*
|
||||||
if (selectedPath) {
|
* Updates:
|
||||||
// Met à jour l'affichage
|
* - UI display (visible path)
|
||||||
savePathElem.textContent = selectedPath;
|
* - tooltip (full path)
|
||||||
hidden.value = selectedPath;
|
* - hidden form input (used for submission)
|
||||||
|
* - localStorage cache (UX persistence only, not authoritative)
|
||||||
|
*
|
||||||
|
* @param {string} path - validated absolute download path
|
||||||
|
*/
|
||||||
|
async function applyPathFromBack(path) {
|
||||||
|
savePathElem.textContent = path;
|
||||||
|
savePathElem.title = path;
|
||||||
|
hidden.value = path;
|
||||||
|
localStorage.setItem("customDownloadPath", path); // UX only
|
||||||
|
}
|
||||||
|
|
||||||
// Et le stocke en localStorage pour la prochaine fois
|
/**
|
||||||
localStorage.setItem("customDownloadPath", selectedPath);
|
* Loads the initial download path on application startup.
|
||||||
|
*
|
||||||
|
* Priority order:
|
||||||
|
* 1. cached localStorage value (UX only, not trusted)
|
||||||
|
* 2. backend validated version of cached path
|
||||||
|
* 3. backend default fallback path if validation fails
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens system folder selector and updates download path.
|
||||||
|
*
|
||||||
|
* The selected folder is validated by Electron backend
|
||||||
|
* before being applied to the UI and persisted.
|
||||||
|
*/
|
||||||
|
document.getElementById("changePath").addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
// selectDownloadFolder already returns a validated path
|
||||||
|
const validatedPath = await window.electronAPI.selectDownloadFolder();
|
||||||
|
|
||||||
|
if (!validatedPath) return; // cancelled
|
||||||
|
|
||||||
|
await applyPathFromBack(validatedPath);
|
||||||
|
} catch (err) {
|
||||||
|
alert("This folder is not allowed. Only specific folders (Users, Downloads, Documents) are authorized for downloads.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,105 +1,256 @@
|
|||||||
/*
|
|
||||||
This file is part of Freedom Loader.
|
|
||||||
|
|
||||||
Copyright (C) 2025 MasterAcnolo
|
|
||||||
|
|
||||||
Freedom Loader 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.
|
|
||||||
|
|
||||||
Freedom Loader 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Liste des thèmes disponibles (clé = nom, valeur = label affiché)
|
|
||||||
const themes = {
|
|
||||||
default: "Default",
|
|
||||||
dark: "Sombre",
|
|
||||||
light: "Clair",
|
|
||||||
chirac: "Chirac",
|
|
||||||
fanatic: "Fanatic",
|
|
||||||
cyberpunk: "Cyberpunk",
|
|
||||||
spicy: "Spicy",
|
|
||||||
vilbrequin: "Vilbrequin"
|
|
||||||
};
|
|
||||||
|
|
||||||
const themeSubtitles = {
|
|
||||||
default: "Because why not?",
|
|
||||||
dark: "Darkness is my ally",
|
|
||||||
light: "Qui aime ce thème ?",
|
|
||||||
chirac: "J'aime les pommes",
|
|
||||||
fanatic: "Always Fnatic !",
|
|
||||||
cyberpunk: "Wake up, choom. We’ve got a city to burn.",
|
|
||||||
spicy: "The Spiciest One",
|
|
||||||
vilbrequin: "Rend l'argent"
|
|
||||||
};
|
|
||||||
|
|
||||||
const themeSelect = document.getElementById("themeSelect");
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
/**
|
||||||
|
* Currently selected themes loaded from backend.
|
||||||
|
* Used as cache for UI theme switching.
|
||||||
|
*/
|
||||||
|
let loadedThemes = [];
|
||||||
|
|
||||||
// Remplir le select avec les options
|
/**
|
||||||
function populateThemeSelect() {
|
* Applies a theme object to the application UI by mapping
|
||||||
for (const [key, label] of Object.entries(themes)) {
|
* theme configuration values to CSS variables and DOM properties.
|
||||||
|
*
|
||||||
|
* This function mutates:
|
||||||
|
* - document.body (background styling)
|
||||||
|
* - :root CSS variables (design system tokens)
|
||||||
|
* - subtitle DOM element (theme metadata display)
|
||||||
|
*
|
||||||
|
* @param {Object} theme - Theme definition object
|
||||||
|
* @param {Object} theme.style - Style configuration
|
||||||
|
*/
|
||||||
|
function applyTheme(theme) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const style = theme.style;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
if (theme.image) {
|
||||||
|
document.body.style.backgroundImage = `url('${theme.image}')`;
|
||||||
|
document.body.style.backgroundSize = style.background?.size || "cover";
|
||||||
|
document.body.style.backgroundPosition = style.background?.position || "center";
|
||||||
|
document.body.style.backgroundAttachment = style.background?.attachment || "fixed";
|
||||||
|
} else {
|
||||||
|
document.body.style.backgroundImage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS variables (design tokens)
|
||||||
|
root.style.setProperty("--background-color", style.colors?.background || "#121212");
|
||||||
|
root.style.setProperty("--default-text-color", style.colors?.text?.default || "#e0e0e0");
|
||||||
|
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "#2196f3");
|
||||||
|
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "#f5f5f5");
|
||||||
|
|
||||||
|
root.style.setProperty("--form-bg-color", style.form?.background || "#1e1e1e");
|
||||||
|
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "#2a2a2a");
|
||||||
|
root.style.setProperty("--form-input-border-color", style.form?.input?.border || "#444444");
|
||||||
|
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "#2196f3");
|
||||||
|
root.style.setProperty("--form-input-text-color", style.form?.input?.text || "#f5f5f5");
|
||||||
|
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "#888888");
|
||||||
|
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "#2196f3");
|
||||||
|
root.style.setProperty("--form-button-text-color", style.form?.button?.text || "#ffffff");
|
||||||
|
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "#1976d2");
|
||||||
|
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "#ffffff");
|
||||||
|
|
||||||
|
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "#555555");
|
||||||
|
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "#2196f3");
|
||||||
|
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "#ffffff");
|
||||||
|
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "rgba(33, 150, 243, 0.5)");
|
||||||
|
|
||||||
|
root.style.setProperty("--download-status-color", style.download?.status || "#2196f3");
|
||||||
|
|
||||||
|
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "#444444");
|
||||||
|
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "#2196f3");
|
||||||
|
|
||||||
|
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "#1a1a1a");
|
||||||
|
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "#e0e0e0");
|
||||||
|
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "#2196f3");
|
||||||
|
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "#cccccc");
|
||||||
|
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "#ffffff");
|
||||||
|
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "#2196f3");
|
||||||
|
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "#1976d2");
|
||||||
|
|
||||||
|
root.style.setProperty("--playlist-background-color", style.playlist?.background || "#303030");
|
||||||
|
|
||||||
|
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "#1e1e1e");
|
||||||
|
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "#e0e0e0");
|
||||||
|
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "#2196f3");
|
||||||
|
root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || "#ffffff");
|
||||||
|
root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || "#1e1e1e");
|
||||||
|
root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || "#e0e0e0");
|
||||||
|
root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || "#1e1e1e");
|
||||||
|
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "#2a2a2a");
|
||||||
|
root.style.setProperty("--settings-text-color", style.settings?.text || "#e0e0e0");
|
||||||
|
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "#aaaaaa");
|
||||||
|
|
||||||
|
const subtitleEl = document.getElementById("subtitle");
|
||||||
|
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists selected theme ID into Electron backend features storage.
|
||||||
|
*
|
||||||
|
* @param {string} themeId
|
||||||
|
*/
|
||||||
|
function saveTheme(themeId) {
|
||||||
|
window.electronAPI.setFeature("theme", themeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates theme dropdown selector with available themes.
|
||||||
|
*
|
||||||
|
* @param {Array} themes
|
||||||
|
*/
|
||||||
|
function populateThemeSelect(themes) {
|
||||||
|
themeSelect.innerHTML = "";
|
||||||
|
|
||||||
|
for (const theme of themes) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = key;
|
option.value = theme.id;
|
||||||
option.textContent = label;
|
option.textContent = theme.name;
|
||||||
themeSelect.appendChild(option);
|
themeSelect.appendChild(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appliquer le thème sur le body et changer le subtitle
|
/**
|
||||||
function applyTheme(themeName) {
|
* Initializes theme system:
|
||||||
// Supprime les classes thème précédentes
|
* - loads themes from backend
|
||||||
document.body.classList.remove(...Object.keys(themes));
|
* - restores saved theme
|
||||||
// Ajoute la classe du thème actuel
|
* - applies selected theme
|
||||||
document.body.classList.add(themeName);
|
*/
|
||||||
|
async function initThemes() {
|
||||||
|
loadedThemes = await window.electronAPI.getThemes();
|
||||||
|
|
||||||
// Modifier le subtitle
|
if (!loadedThemes.length) {
|
||||||
const subtitle = document.getElementById("subtitle");
|
const defaultTheme = {
|
||||||
if (subtitle && themeSubtitles[themeName]) {
|
id: "dark",
|
||||||
subtitle.textContent = themeSubtitles[themeName];
|
name: "Dark",
|
||||||
|
subtitle: "Darkness is my ally",
|
||||||
|
style: {
|
||||||
|
background: {
|
||||||
|
size: "cover",
|
||||||
|
position: "center",
|
||||||
|
attachment: "fixed"
|
||||||
|
},
|
||||||
|
|
||||||
|
colors: {
|
||||||
|
background: "#121212",
|
||||||
|
text: {
|
||||||
|
default: "#e0e0e0",
|
||||||
|
subtitle: "#2196f3",
|
||||||
|
audioOnly: "#f5f5f5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
form: {
|
||||||
|
background: "#1e1e1e",
|
||||||
|
input: {
|
||||||
|
background: "#2a2a2a",
|
||||||
|
border: "#444444",
|
||||||
|
borderFocus: "#2196f3",
|
||||||
|
text: "#f5f5f5",
|
||||||
|
placeholder: "#888888"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
background: "#2196f3",
|
||||||
|
text: "#ffffff",
|
||||||
|
hover: "#1976d2"
|
||||||
|
},
|
||||||
|
pasteButtonIcon: "#ffffff"
|
||||||
|
},
|
||||||
|
|
||||||
|
checkbox: {
|
||||||
|
background: {
|
||||||
|
unchecked: "#555555",
|
||||||
|
checked: "#2196f3"
|
||||||
|
},
|
||||||
|
checkmarkBorder: "#ffffff",
|
||||||
|
pulse: "rgba(33, 150, 243, 0.5)"
|
||||||
|
},
|
||||||
|
|
||||||
|
download: {
|
||||||
|
status: "#2196f3"
|
||||||
|
},
|
||||||
|
|
||||||
|
progressBar: {
|
||||||
|
background: "#444444",
|
||||||
|
fill: "#2196f3"
|
||||||
|
},
|
||||||
|
|
||||||
|
videoInfo: {
|
||||||
|
background: "#1a1a1a",
|
||||||
|
text: "#e0e0e0",
|
||||||
|
heading: "#2196f3",
|
||||||
|
list: "#cccccc",
|
||||||
|
strong: "#ffffff",
|
||||||
|
link: "#2196f3",
|
||||||
|
linkHover: "#1976d2"
|
||||||
|
},
|
||||||
|
|
||||||
|
playlist: {
|
||||||
|
background: "#303030"
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
button: {
|
||||||
|
background: "#1e1e1e",
|
||||||
|
text: "#e0e0e0"
|
||||||
|
},
|
||||||
|
openThemeButton: {
|
||||||
|
background: "#2196f3",
|
||||||
|
text: "#ffffff"
|
||||||
|
},
|
||||||
|
openJsonButton: {
|
||||||
|
background: "#2196f3",
|
||||||
|
text: "#ffffff"
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
modal: "#1e1e1e",
|
||||||
|
section: "#2a2a2a"
|
||||||
|
},
|
||||||
|
text: "#e0e0e0",
|
||||||
|
subtitle: "#aaaaaa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadedThemes = [defaultTheme];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionnel : animation spéciale Chirac
|
populateThemeSelect(loadedThemes);
|
||||||
if (themeName === "chirac") {
|
|
||||||
requestAnimationFrame(() => {
|
const features = await window.electronAPI.getFeatures();
|
||||||
document.body.classList.add("theme-active");
|
const savedId = features.theme;
|
||||||
});
|
|
||||||
} else {
|
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
|
||||||
document.body.classList.remove("theme-active");
|
|
||||||
}
|
themeSelect.value = theme.id;
|
||||||
|
applyTheme(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sauvegarder le thème en localStorage
|
/**
|
||||||
function saveTheme(themeName) {
|
* Handles theme selection change from UI dropdown.
|
||||||
localStorage.setItem("selectedTheme", themeName);
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
// Charger le thème au démarrage
|
|
||||||
function loadTheme() {
|
|
||||||
const savedTheme = localStorage.getItem("selectedTheme");
|
|
||||||
if (savedTheme && themes[savedTheme]) {
|
|
||||||
applyTheme(savedTheme);
|
|
||||||
themeSelect.value = savedTheme;
|
|
||||||
} else {
|
|
||||||
applyTheme("dark");
|
|
||||||
themeSelect.value = "dark";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quand l’utilisateur change le select
|
|
||||||
themeSelect.addEventListener("change", (e) => {
|
themeSelect.addEventListener("change", (e) => {
|
||||||
const selected = e.target.value;
|
const theme = loadedThemes.find(t => t.id === e.target.value);
|
||||||
if (themes[selected]) {
|
if (!theme) return;
|
||||||
applyTheme(selected);
|
|
||||||
saveTheme(selected);
|
applyTheme(theme);
|
||||||
}
|
saveTheme(theme.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialisation
|
/**
|
||||||
populateThemeSelect();
|
* Reloads themes from backend and reapplies current selection.
|
||||||
loadTheme();
|
*/
|
||||||
|
async function refreshThemes() {
|
||||||
|
loadedThemes = await window.electronAPI.reloadThemes();
|
||||||
|
populateThemeSelect(loadedThemes);
|
||||||
|
|
||||||
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
const savedId = features.theme;
|
||||||
|
|
||||||
|
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
|
||||||
|
|
||||||
|
themeSelect.value = theme.id;
|
||||||
|
applyTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.refreshThemes = refreshThemes;
|
||||||
|
|
||||||
|
initThemes();
|
||||||
@@ -1,73 +1,174 @@
|
|||||||
/*
|
|
||||||
This file is part of Freedom Loader.
|
|
||||||
|
|
||||||
Copyright (C) 2025 MasterAcnolo
|
|
||||||
|
|
||||||
Freedom Loader 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.
|
|
||||||
|
|
||||||
Freedom Loader 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Récupération du formulaire de téléchargement
|
|
||||||
const form = document.getElementById("downloadForm");
|
const form = document.getElementById("downloadForm");
|
||||||
|
|
||||||
// Élément où on affichera l’état du téléchargement (en cours, erreur, succès)
|
|
||||||
const statusDiv = document.getElementById("downloadStatus");
|
const statusDiv = document.getElementById("downloadStatus");
|
||||||
|
const button = form.querySelector("button[type=\"submit\"]");
|
||||||
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
|
|
||||||
// Récupération du bouton de soumission pour pouvoir le désactiver pendant la requête
|
/**
|
||||||
const button = form.querySelector("button");
|
* Indicates whether a download process is currently active.
|
||||||
|
* Used to prevent duplicate submissions and to gate UI actions
|
||||||
|
* such as disabling the form or showing progress.
|
||||||
|
*/
|
||||||
|
let isDownloading = false;
|
||||||
|
|
||||||
// Écouteur d’événement pour intercepter la soumission du formulaire
|
/**
|
||||||
|
* Indicates whether the current download was explicitly
|
||||||
|
* stopped by the user via the cancel action.
|
||||||
|
*
|
||||||
|
* Used to differentiate between:
|
||||||
|
* - user-initiated cancellation
|
||||||
|
* - failures or natural completion
|
||||||
|
*/
|
||||||
|
let userStoppedDownload = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles download form submission.
|
||||||
|
*
|
||||||
|
* - Validates user input (URL)
|
||||||
|
* - Disables UI during download
|
||||||
|
* - Sends request to backend `/download`
|
||||||
|
* - Manages success / error states
|
||||||
|
* - Restores UI state after completion
|
||||||
|
*/
|
||||||
form.addEventListener("submit", async (e) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault(); // Empêche le rechargement automatique de la page
|
e.preventDefault();
|
||||||
|
|
||||||
// Désactive le bouton pour éviter les clics multiples pendant la requête
|
const urlInput = form.querySelector("input[name='url']");
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
|
// Basic URL validation
|
||||||
|
if (!url) {
|
||||||
|
window.showError("Please enter a URL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
statusDiv.style.display = "none";
|
||||||
|
isDownloading = true;
|
||||||
|
userStoppedDownload = false;
|
||||||
|
|
||||||
// Affiche un message d’attente pour informer l’utilisateur
|
// Show progress UI immediately with initial message
|
||||||
statusDiv.textContent = "Téléchargement en cours...";
|
showInitialProgress();
|
||||||
|
|
||||||
// Prépare les données du formulaire au format URL-encoded
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const params = new URLSearchParams(formData);
|
const params = new URLSearchParams(formData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Envoie la requête POST vers /download avec les données du formulaire
|
|
||||||
const res = await fetch("/download", {
|
const res = await fetch("/download", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
body: params,
|
body: params,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Si la réponse n’est pas un succès (code HTTP 4xx ou 5xx)
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
|
if (!userStoppedDownload) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
window.showError(errorText);
|
||||||
|
resetProgressBar();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupère le texte envoyé par le serveur (message de succès ou erreur)
|
// Download successful - check if notifications are enabled
|
||||||
const text = await res.text();
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
if (features.notifySystem) {
|
||||||
|
window.showSuccess("Download completed!");
|
||||||
|
}
|
||||||
|
|
||||||
// Affiche le message retourné par le serveur
|
} catch (err) {
|
||||||
statusDiv.textContent = text;
|
if (!isDownloading && !userStoppedDownload) {
|
||||||
} catch {
|
statusDiv.style.display = "none";
|
||||||
// Gestion des erreurs réseau ou autres exceptions
|
}
|
||||||
statusDiv.textContent = "❌ Une erreur s’est produite.";
|
|
||||||
} finally {
|
} finally {
|
||||||
// Réactive le bouton pour permettre d’autres soumissions
|
isDownloading = false;
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
stopBtn.style.display = "none";
|
||||||
// Efface le message d’état après 5 secondes pour garder l’interface propre
|
|
||||||
setTimeout(() => {
|
|
||||||
statusDiv.textContent = "";
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays initial download progress UI immediately
|
||||||
|
* when a download starts.
|
||||||
|
*
|
||||||
|
* Initializes progress bar, speed indicator, stage text
|
||||||
|
* and shows the stop button.
|
||||||
|
*/
|
||||||
|
function showInitialProgress() {
|
||||||
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
|
const progressBarText = document.getElementById("downloadProgressText");
|
||||||
|
const speedElement = document.getElementById("downloadSpeedText");
|
||||||
|
const stageElement = document.getElementById("downloadStage");
|
||||||
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
|
|
||||||
|
if (progressWrapper) progressWrapper.style.display = "block";
|
||||||
|
if (progressBarText) {
|
||||||
|
progressBarText.style.display = "block";
|
||||||
|
progressBarText.innerHTML = "0%";
|
||||||
|
}
|
||||||
|
if (speedElement) {
|
||||||
|
speedElement.style.display = "block";
|
||||||
|
speedElement.textContent = "0 Mib/s";
|
||||||
|
}
|
||||||
|
if (stageElement) {
|
||||||
|
stageElement.style.display = "block";
|
||||||
|
stageElement.textContent = "Retrieving data...";
|
||||||
|
}
|
||||||
|
if (stopBtn) stopBtn.style.display = "inline-flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the current download process.
|
||||||
|
*
|
||||||
|
* Sends a request to backend `/download/cancel`,
|
||||||
|
* updates UI state and resets progress indicators.
|
||||||
|
*
|
||||||
|
* Prevents duplicate calls if no download is active.
|
||||||
|
*/
|
||||||
|
async function stopDownload() {
|
||||||
|
if (!isDownloading) return;
|
||||||
|
|
||||||
|
userStoppedDownload = true;
|
||||||
|
button.disabled = true;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch("/download/cancel", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
});
|
||||||
|
|
||||||
|
window.showWarning("Download stopped");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error stopping download:", err);
|
||||||
|
} finally {
|
||||||
|
isDownloading = false;
|
||||||
|
button.disabled = false;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
stopBtn.style.display = "none";
|
||||||
|
resetProgressBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all download progress UI elements to their
|
||||||
|
* default hidden state.
|
||||||
|
*
|
||||||
|
* Used after completion or cancellation of a download.
|
||||||
|
*/
|
||||||
|
function resetProgressBar() {
|
||||||
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
|
if (progressWrapper) progressWrapper.style.display = "none";
|
||||||
|
|
||||||
|
const progressBar = document.getElementById("downloadProgress");
|
||||||
|
if (progressBar) progressBar.style.width = "0%";
|
||||||
|
|
||||||
|
const progressBarText = document.getElementById("downloadProgressText");
|
||||||
|
if (progressBarText) progressBarText.style.display = "none";
|
||||||
|
|
||||||
|
const speedElement = document.getElementById("downloadSpeedText");
|
||||||
|
if (speedElement) speedElement.style.display = "none";
|
||||||
|
|
||||||
|
const playlistInfoElement = document.getElementById("playlistInfoText");
|
||||||
|
if (playlistInfoElement) playlistInfoElement.style.display = "none";
|
||||||
|
}
|
||||||
@@ -1,136 +1,276 @@
|
|||||||
/*
|
/**
|
||||||
This file is part of Freedom Loader.
|
* Converts a raw upload date string (format YYYYMMDD)
|
||||||
|
* into a human-readable format (DD/MM/YYYY).
|
||||||
|
*
|
||||||
|
* Returns "Inconnue" if the input is invalid or malformed.
|
||||||
|
*/
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||||
|
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
Copyright (C) 2025 MasterAcnolo
|
/**
|
||||||
|
* Converts a file size in bytes into megabytes (MB).
|
||||||
|
*
|
||||||
|
* Returns a formatted string with 2 decimals (e.g. "12.34 Mo").
|
||||||
|
* If input is falsy, returns "Inconnue".
|
||||||
|
*/
|
||||||
|
function formatSize(bytes) {
|
||||||
|
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
||||||
|
}
|
||||||
|
|
||||||
Freedom Loader is free software: you can redistribute it and/or modify
|
/**
|
||||||
it under the terms of the GNU General Public License as published by
|
* Fetches video or playlist metadata from the backend `/info` endpoint.
|
||||||
the Free Software Foundation, either version 3 of the License.
|
*
|
||||||
|
* Sends a POST request with URL-encoded body.
|
||||||
|
* Handles network errors, HTTP errors and JSON parsing safely.
|
||||||
|
*
|
||||||
|
* @param {string} url - Video or playlist URL
|
||||||
|
* @returns {Promise<Object>} Parsed metadata or error object
|
||||||
|
*/
|
||||||
|
async function fetchVideoInfo(url) {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/info", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: new URLSearchParams({ url }),
|
||||||
|
});
|
||||||
|
|
||||||
Freedom Loader is distributed in the hope that it will be useful,
|
if (!res.ok) return { error: `An Error occured when fetching info` };
|
||||||
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
|
const data = await res.json();
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
if (!data) return { error: "Data is Missing" };
|
||||||
*/
|
|
||||||
|
|
||||||
// On attend que le DOM soit complètement chargé
|
return data;
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
} catch (e) {
|
||||||
// Récupération des éléments du DOM pour interaction
|
return { error: "Network or JSON Issue" };
|
||||||
const urlInput = document.getElementById("UrlInput");
|
}
|
||||||
const infoDiv = document.getElementById("videoInfo");
|
}
|
||||||
|
|
||||||
// Stocke la dernière URL fetchée pour éviter les requêtes répétées inutiles
|
/**
|
||||||
let lastFetchedUrl = "";
|
* Initializes the video info system UI.
|
||||||
|
*
|
||||||
|
* - Listens to URL input changes
|
||||||
|
* - Automatically fetches video/playlist metadata if enabled
|
||||||
|
* - Renders either playlist UI or single video UI
|
||||||
|
* - Handles loading states and caching (lastFetchedUrl)
|
||||||
|
*
|
||||||
|
* Also manages dynamic playlist title injection into the form.
|
||||||
|
*/
|
||||||
|
async function init() {
|
||||||
|
|
||||||
// Active les logs DEBUG pour le développement
|
// Main input listener: triggers auto-fetch when URL changes
|
||||||
const DEBUG = true;
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const urlInput = document.getElementById("UrlInput");
|
||||||
|
const infoDiv = document.getElementById("videoInfo");
|
||||||
|
const loaderBox = document.getElementById("loaderBox");
|
||||||
|
const form = document.getElementById("downloadForm");
|
||||||
|
|
||||||
// Écoute les changements dans l'input URL
|
// Input caché pour stocker le titre de la playlist
|
||||||
urlInput.addEventListener("input", async () => {
|
let playlistTitleInput = document.getElementById("playlistTitle");
|
||||||
const url = urlInput.value.trim();
|
if (!playlistTitleInput) {
|
||||||
|
playlistTitleInput = document.createElement("input");
|
||||||
if (DEBUG) console.log("[DEBUG] Input détecté :", url);
|
playlistTitleInput.type = "hidden";
|
||||||
|
playlistTitleInput.name = "playlistTitle";
|
||||||
// Si l'URL est vide ou trop courte, on reset l'affichage
|
playlistTitleInput.id = "playlistTitle";
|
||||||
if (!url || url.length < 5) {
|
form.appendChild(playlistTitleInput);
|
||||||
if (DEBUG) console.log("[DEBUG] URL vide ou trop courte, reset affichage.");
|
|
||||||
infoDiv.innerHTML = "";
|
|
||||||
infoDiv.classList.remove("visible");
|
|
||||||
lastFetchedUrl = "";
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si l'URL est identique à la dernière requêtée, on ne refait pas la requête
|
let lastFetchedUrl = "";
|
||||||
if (url === lastFetchedUrl) {
|
|
||||||
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mémorise l'URL actuelle comme dernière URL traitée
|
urlInput.addEventListener("input", async () => {
|
||||||
lastFetchedUrl = url;
|
|
||||||
|
|
||||||
try {
|
const configFeatures = await window.electronAPI.getFeatures();
|
||||||
if (DEBUG) console.log("[DEBUG] Envoi requête POST vers /info avec URL:", url);
|
if (!configFeatures.autoCheckInfo) return;
|
||||||
|
|
||||||
// Envoi de la requête POST pour récupérer les infos de la vidéo
|
const url = urlInput.value.trim();
|
||||||
const res = await fetch("/info", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
||||||
body: new URLSearchParams({ url }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (DEBUG) console.log("[DEBUG] Réponse status:", res.status);
|
// IF field empty -> total reset
|
||||||
|
if (!url || url.length < 2) {
|
||||||
// Si la réponse HTTP est une erreur, on affiche un message
|
infoDiv.innerHTML = "";
|
||||||
if (!res.ok) {
|
infoDiv.classList.remove("visible", "playlist-mode");
|
||||||
console.error(`[ERROR] Réponse HTTP ${res.status}: ${res.statusText}`);
|
playlistTitleInput.value = "";
|
||||||
infoDiv.innerHTML = "❌ Impossible de récupérer les infos (erreur serveur).";
|
lastFetchedUrl = "";
|
||||||
infoDiv.classList.remove("visible");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse la réponse JSON contenant les infos vidéo
|
if (url === lastFetchedUrl) return;
|
||||||
const data = await res.json();
|
lastFetchedUrl = url;
|
||||||
if (DEBUG) console.log("[DEBUG] Données reçues:", data);
|
|
||||||
|
|
||||||
// Vérification simple que les données attendues sont présentes
|
loaderBox.style.display = "flex";
|
||||||
if (!data || !data.title || !data.thumbnail) {
|
const data = await fetchVideoInfo(url);
|
||||||
console.warn("[WARNING] Données incomplètes", data);
|
loaderBox.style.display = "none";
|
||||||
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
|
|
||||||
infoDiv.classList.remove("visible");
|
|
||||||
|
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");
|
||||||
|
playlistTitleInput.value = "";
|
||||||
|
loaderBox.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formatage de la durée en minutes et secondes
|
// ---------- PLAYLIST ----------
|
||||||
const minutes = Math.floor(data.duration / 60);
|
if (data.type === "playlist") {
|
||||||
const seconds = data.duration % 60;
|
playlistTitleInput.value = data.title;
|
||||||
const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
|
infoDiv.classList.add("playlist-mode");
|
||||||
|
infoDiv.innerHTML = `
|
||||||
|
<div class="playlist-header">
|
||||||
|
<div class="playlist-info">
|
||||||
|
<div class="playlist-badge">Detected Playlist</div>
|
||||||
|
<h3 class="playlist-title">${data.title}</h3>
|
||||||
|
<div class="playlist-meta">
|
||||||
|
<span class="playlist-count">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 6H20M4 12H20M4 18H11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
${data.count} vidéos
|
||||||
|
</span>
|
||||||
|
${data.channel ? `<span class="playlist-channel">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M5 20C5 16.134 8.134 13 12 13C15.866 13 19 16.134 19 20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
${data.channel}
|
||||||
|
</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="playlistVideos"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
// Formatage de la date d'upload au format JJ/MM/AAAA
|
const listDiv = document.getElementById("playlistVideos");
|
||||||
const date = data.upload_date || "";
|
|
||||||
const readableDate = date
|
|
||||||
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
|
|
||||||
: "Inconnue";
|
|
||||||
|
|
||||||
// Conversion taille approximative en Mo
|
data.videos.forEach(v => {
|
||||||
const sizeMB = data.filesize_approx
|
const durationStr = v.duration
|
||||||
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
|
? `${Math.floor(v.duration / 60)}:${(v.duration % 60).toString().padStart(2,"0")}` : "--:--";
|
||||||
: "Inconnue";
|
|
||||||
|
|
||||||
// Gestion des catégories, affichage par défaut si absentes
|
const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
|
||||||
const categories = data.categories
|
|
||||||
? data.categories.join(", ")
|
listDiv.innerHTML += `
|
||||||
: "Non spécifiées";
|
<div class="playlist-video-card">
|
||||||
|
<div class="video-thumbnail-wrapper">
|
||||||
|
<img src="${v.thumbnail}" alt="${v.title}" class="video-thumbnail">
|
||||||
|
<span class="video-duration">${durationStr}</span>
|
||||||
|
</div>
|
||||||
|
<div class="video-content">
|
||||||
|
<h4 class="video-title" title="${v.title}">${v.title}</h4>
|
||||||
|
${v.uploader ? `<p class="video-uploader">${v.uploader}</p>` : ''}
|
||||||
|
<div class="video-actions">
|
||||||
|
<a href="${videoUrl}" target="_blank" class="video-link">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Ouvrir
|
||||||
|
</a>
|
||||||
|
<button class="copy-btn" data-url="${videoUrl}" title="Copier l'URL">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="8" y="8" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy Button
|
||||||
|
listDiv.addEventListener("click", (event) => {
|
||||||
|
if (event.target.classList.contains("copy-btn") || event.target.closest(".copy-btn")) {
|
||||||
|
const btn = event.target.closest(".copy-btn") || event.target;
|
||||||
|
if (btn.disabled) return;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
const url = btn.dataset.url;
|
||||||
|
navigator.clipboard.writeText(url)
|
||||||
|
.then(() => {
|
||||||
|
const original = btn.innerHTML;
|
||||||
|
|
||||||
|
btn.style.opacity = 0;
|
||||||
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>`;
|
||||||
|
btn.style.opacity = 1;
|
||||||
|
btn.style.transform = "scale(1)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.style.opacity = 0;
|
||||||
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = original;
|
||||||
|
btn.style.opacity = 1;
|
||||||
|
btn.style.transform = "scale(1)";
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
}, 300);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
const original = btn.innerHTML;
|
||||||
|
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>`;
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = original;
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
infoDiv.classList.add("visible");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
infoDiv.classList.remove("playlist-mode");
|
||||||
|
playlistTitleInput.value = "";
|
||||||
|
|
||||||
|
// ---------- 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";
|
||||||
|
|
||||||
// Construction du HTML pour afficher les informations de la vidéo
|
|
||||||
infoDiv.innerHTML = `
|
infoDiv.innerHTML = `
|
||||||
<h3>${data.title}</h3>
|
<h3>${data.title}</h3>
|
||||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Durée :</strong> ${durationStr}</li>
|
<li><strong>Duration :</strong> ${durationStr}</li>
|
||||||
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
||||||
<li><strong>Date d’upload :</strong> ${readableDate}</li>
|
<li><strong>Upload Date :</strong> ${readableDate}</li>
|
||||||
<li><strong>Vues :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
<li><strong>Views :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||||
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
|
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
|
||||||
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank" rel="noopener noreferrer">${data.webpage_url}</a></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" rel="noopener noreferrer">${data.channel_url}</a></li>
|
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank">${data.channel_url}</a></li>
|
||||||
<li><strong>Taille estimée :</strong> ${sizeMB}</li>
|
<li><strong>Estimed Size :</strong> ${sizeStr}</li>
|
||||||
<li><strong>Catégories :</strong> ${categories}</li>
|
<li><strong>Category :</strong> ${categories}</li>
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Affiche la div contenant les infos
|
|
||||||
infoDiv.classList.add("visible");
|
infoDiv.classList.add("visible");
|
||||||
|
});
|
||||||
|
|
||||||
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
|
})
|
||||||
} catch (e) {
|
|
||||||
// En cas d'erreur réseau ou JSON, on affiche un message d'erreur
|
}
|
||||||
console.error("[CRITICAL] Erreur lors de la récupération:", e);
|
|
||||||
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
|
init();
|
||||||
infoDiv.classList.remove("visible");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
136
public/script/progressBar.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* DOM elements used for download progress UI rendering.
|
||||||
|
* These elements are updated in real-time via SSE streams.
|
||||||
|
*/
|
||||||
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
|
const progressBar = document.getElementById("downloadProgress");
|
||||||
|
const progressBarText = document.getElementById("downloadProgressText");
|
||||||
|
|
||||||
|
const speedElement = document.getElementById("downloadSpeedText");
|
||||||
|
const stageElement = document.getElementById("downloadStage");
|
||||||
|
const playlistInfoElement = document.getElementById("playlistInfoText");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-Sent Events streams for real-time download updates.
|
||||||
|
* Each stream is responsible for a specific aspect of the download state.
|
||||||
|
*/
|
||||||
|
const speedEvt = new EventSource("/download/speed");
|
||||||
|
const stageEvt = new EventSource("/download/stage");
|
||||||
|
const playlistInfoEvt = new EventSource("/download/playlist-info");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the progress UI to its initial state.
|
||||||
|
* Hides all dynamic elements and clears displayed values.
|
||||||
|
*/
|
||||||
|
function startProgress() {
|
||||||
|
progressBar.style.width = "0%";
|
||||||
|
progressBarText.innerHTML = "0%";
|
||||||
|
speedElement.innerHTML = "0 Mib/s";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the progress UI visible and enables related controls
|
||||||
|
* such as the stop button.
|
||||||
|
*/
|
||||||
|
function showProgress() {
|
||||||
|
progressWrapper.style.display = "block";
|
||||||
|
progressBarText.style.display = "block";
|
||||||
|
speedElement.style.display = "block";
|
||||||
|
|
||||||
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
|
if (stopBtn) stopBtn.style.display = "inline-flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the progress bar UI with the given percentage.
|
||||||
|
* Automatically triggers UI visibility if not already shown.
|
||||||
|
*
|
||||||
|
* @param {number} percent - Download progress percentage (0–100)
|
||||||
|
*/
|
||||||
|
function updateProgress(percent) {
|
||||||
|
if (percent > 0 && progressWrapper.style.display !== "block") {
|
||||||
|
showProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.style.width = `${percent}%`;
|
||||||
|
progressBarText.innerHTML = `${percent}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully resets the progress UI and hides all download indicators.
|
||||||
|
* Called after completion or cancellation.
|
||||||
|
*/
|
||||||
|
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";
|
||||||
|
|
||||||
|
stageElement.textContent = "";
|
||||||
|
stageElement.style.display = "none";
|
||||||
|
|
||||||
|
playlistInfoElement.textContent = "";
|
||||||
|
playlistInfoElement.style.display = "none";
|
||||||
|
|
||||||
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
|
if (stopBtn) stopBtn.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main SSE stream handling download progress updates.
|
||||||
|
* Supports:
|
||||||
|
* - reset signal
|
||||||
|
* - done signal
|
||||||
|
* - percentage updates
|
||||||
|
*/
|
||||||
|
const evtSource = new EventSource("/download/progress");
|
||||||
|
|
||||||
|
evtSource.onmessage = e => {
|
||||||
|
if (e.data === "reset") {
|
||||||
|
startProgress();
|
||||||
|
window.electronAPI.setProgress(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.data === "done") {
|
||||||
|
setTimeout(() => {
|
||||||
|
resetProgress();
|
||||||
|
window.electronAPI.setProgress(-1);
|
||||||
|
}, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percent = parseFloat(e.data);
|
||||||
|
|
||||||
|
if (!isNaN(percent)) {
|
||||||
|
updateProgress(percent);
|
||||||
|
window.electronAPI.setProgress(percent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE stream: download speed updates (e.g. "5.2 MiB/s").
|
||||||
|
*/
|
||||||
|
speedEvt.onmessage = e => {
|
||||||
|
speedElement.style.display = "block";
|
||||||
|
speedElement.textContent = e.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE stream: download stage updates (e.g. downloading, merging, extracting).
|
||||||
|
*/
|
||||||
|
stageEvt.onmessage = e => {
|
||||||
|
stageElement.style.display = "block";
|
||||||
|
stageElement.textContent = e.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE stream: playlist progress updates (e.g. "Item 1 of 15").
|
||||||
|
*/
|
||||||
|
playlistInfoEvt.onmessage = e => {
|
||||||
|
playlistInfoElement.style.display = "block";
|
||||||
|
playlistInfoElement.textContent = e.data;
|
||||||
|
};
|
||||||
105
public/script/settingsPanel.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Settings panel DOM elements used to control application feature flags
|
||||||
|
* and configuration UI interactions.
|
||||||
|
*/
|
||||||
|
const settingsPanel = document.getElementById("settings-panel");
|
||||||
|
const settingsModal = document.querySelector(".settings-modal");
|
||||||
|
const settingsButton = document.getElementById("settings-button");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks current settings panel state.
|
||||||
|
* NOTE: actual visibility is derived from DOM style in current implementation.
|
||||||
|
*/
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles visibility of the settings panel when clicking the settings button.
|
||||||
|
*/
|
||||||
|
settingsButton.addEventListener("click", () => {
|
||||||
|
const currentlyOpen = settingsPanel.style.display === "block";
|
||||||
|
settingsPanel.style.display = currentlyOpen ? "none" : "block";
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the settings panel and resets internal state.
|
||||||
|
*/
|
||||||
|
function closePanel() {
|
||||||
|
isOpen = false;
|
||||||
|
settingsPanel.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads feature flags from Electron backend and syncs them
|
||||||
|
* with corresponding UI controls (checkboxes, selects).
|
||||||
|
*
|
||||||
|
* Also registers change listeners to persist updates in real time.
|
||||||
|
*/
|
||||||
|
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 (!key) return;
|
||||||
|
|
||||||
|
if (features[key] !== undefined) {
|
||||||
|
if (el.type === "checkbox") el.checked = features[key];
|
||||||
|
else if (el.tagName === "SELECT") el.value = features[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
el.addEventListener("change", () => {
|
||||||
|
if (key === "theme") return;
|
||||||
|
|
||||||
|
const value = el.type === "checkbox" ? el.checked : el.value;
|
||||||
|
window.electronAPI.setFeature(key, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens configuration JSON file via Electron topbar API.
|
||||||
|
*/
|
||||||
|
document.getElementById("open-json-btn")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
|
window.topbarAPI.openConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens theme directory via Electron topbar API.
|
||||||
|
*/
|
||||||
|
document.getElementById("open-theme")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
|
window.topbarAPI.openTheme();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes theme list and updates UI selector with a short animation.
|
||||||
|
*/
|
||||||
|
document.getElementById("refresh-themes-btn")
|
||||||
|
?.addEventListener("click", function () {
|
||||||
|
this.classList.add("spinning");
|
||||||
|
window.refreshThemes();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.classList.remove("spinning");
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicking the overlay outside the modal closes the settings panel.
|
||||||
|
*/
|
||||||
|
settingsPanel?.addEventListener("click", closePanel);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents modal clicks from closing the settings panel
|
||||||
|
* by stopping event propagation to the overlay.
|
||||||
|
*/
|
||||||
|
settingsModal?.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes settings UI by loading backend feature flags.
|
||||||
|
*/
|
||||||
|
loadSettings();
|
||||||
83
public/script/toast.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Displays a toast notification in the global UI container.
|
||||||
|
*
|
||||||
|
* Toasts are automatically appended to `#toast-container`,
|
||||||
|
* support multiple visual types, and can auto-dismiss after a delay.
|
||||||
|
*
|
||||||
|
* @param {string} message - Message displayed inside the toast
|
||||||
|
* @param {"success"|"error"|"warning"|"info"} [type="info"] - Visual style of the toast
|
||||||
|
* @param {number} [duration=4000] - Auto-dismiss delay in milliseconds (0 disables auto removal)
|
||||||
|
* @returns {HTMLElement|null} The created toast element or null if container is missing
|
||||||
|
*/
|
||||||
|
window.showToast = function (message, type = "info", duration = 4000) {
|
||||||
|
const container = document.getElementById("toast-container");
|
||||||
|
if (!container) return null;
|
||||||
|
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
toast.classList.add("toast", type);
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
success: "✓",
|
||||||
|
error: "✕",
|
||||||
|
warning: "⚠",
|
||||||
|
info: "ℹ"
|
||||||
|
};
|
||||||
|
|
||||||
|
const icon = icons[type] || icons.info;
|
||||||
|
|
||||||
|
toast.innerHTML = `
|
||||||
|
<span class="toast-icon">${icon}</span>
|
||||||
|
<span class="toast-message">${message}</span>
|
||||||
|
<button class="toast-close" title="Close">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const closeBtn = toast.querySelector(".toast-close");
|
||||||
|
closeBtn.addEventListener("click", () => removeToast(toast));
|
||||||
|
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
if (duration > 0) {
|
||||||
|
setTimeout(() => removeToast(toast), duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toast;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a toast element with a small exit animation.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} toast
|
||||||
|
*/
|
||||||
|
function removeToast(toast) {
|
||||||
|
if (!toast?.parentElement) return;
|
||||||
|
|
||||||
|
toast.classList.add("removing");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut: success toast
|
||||||
|
*/
|
||||||
|
window.showSuccess = (message, duration = 4000) =>
|
||||||
|
window.showToast(message, "success", duration);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut: error toast
|
||||||
|
*/
|
||||||
|
window.showError = (message, duration = 5000) =>
|
||||||
|
window.showToast(message, "error", duration);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut: warning toast
|
||||||
|
*/
|
||||||
|
window.showWarning = (message, duration = 4000) =>
|
||||||
|
window.showToast(message, "warning", duration);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut: info toast
|
||||||
|
*/
|
||||||
|
window.showInfo = (message, duration = 4000) =>
|
||||||
|
window.showToast(message, "info", duration);
|
||||||
57
public/script/topbar.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Initializes topbar button event listeners and binds them
|
||||||
|
* to the Electron preload API exposed in `window.topbarAPI`.
|
||||||
|
*
|
||||||
|
* This function is safe to call once DOM is ready.
|
||||||
|
* It silently aborts if the API or elements are missing.
|
||||||
|
*/
|
||||||
|
function setupTopbarListeners() {
|
||||||
|
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 workshopBtn = document.getElementById("workshop-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 (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers topbar event listeners after DOM is fully loaded.
|
||||||
|
*/
|
||||||
|
document.addEventListener("DOMContentLoaded", setupTopbarListeners);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies UI layout adjustments depending on feature flags
|
||||||
|
* provided by the Electron backend.
|
||||||
|
*
|
||||||
|
* Specifically handles:
|
||||||
|
* - hiding custom topbar
|
||||||
|
* - adjusting layout spacing
|
||||||
|
* - repositioning theme switcher
|
||||||
|
*/
|
||||||
|
(async function applyFeatureLayout() {
|
||||||
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
|
if (!features.customTopBar) {
|
||||||
|
const topbar = document.getElementById("topbar");
|
||||||
|
const container = document.getElementById("container");
|
||||||
|
const themeSwitcher = document.getElementById("theme-switcher");
|
||||||
|
|
||||||
|
if (topbar) topbar.style.display = "none";
|
||||||
|
if (container) container.style.marginTop = "0";
|
||||||
|
if (themeSwitcher) themeSwitcher.style.top = "30px";
|
||||||
|
}
|
||||||
|
})();
|
||||||
94
public/splash.html
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.splash-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #D9D9D9;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 28px;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
.progress-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.progress-track {
|
||||||
|
width: 180px;
|
||||||
|
height: 3px;
|
||||||
|
background: #d0d0d0;
|
||||||
|
border-radius: 99px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 99px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6d6d6d;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="splash-container">
|
||||||
|
<img src="./banner.png" alt="Freedom Loader" style="width: 90%;">
|
||||||
|
<div class="progress-wrapper">
|
||||||
|
<div class="progress-track">
|
||||||
|
<div class="progress-bar" id="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<span class="status" id="status">Starting...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const steps = [
|
||||||
|
"Checking dependencies...",
|
||||||
|
"Starting server...",
|
||||||
|
"Loading themes...",
|
||||||
|
"Almost ready...",
|
||||||
|
];
|
||||||
|
let current = 0;
|
||||||
|
const bar = document.getElementById("progress-bar");
|
||||||
|
const status = document.getElementById("status");
|
||||||
|
|
||||||
|
function advance(label, percent) {
|
||||||
|
bar.style.width = percent + "%";
|
||||||
|
status.textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setProgress = (step) => {
|
||||||
|
if (step >= steps.length) {
|
||||||
|
advance("", 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const percent = Math.round(((step + 1) / steps.length) * 100);
|
||||||
|
advance(steps[step], percent);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.setProgress(0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.checkmark {
|
.checkmark {
|
||||||
--clr: var(--checkbox-bg-checked);
|
--clr: var(--checkbox-checked-bg-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 1.3em;
|
height: 1.3em;
|
||||||
width: 1.3em;
|
width: 1.3em;
|
||||||
background-color: var(--checkbox-bg-default);
|
background-color: var(--checkbox-unchecked-bg-color);
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
transition: background-color 300ms, border-radius 300ms;
|
transition: background-color 300ms, border-radius 300ms;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,11 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
border: 1px dashed var(--form-input-border-color);
|
border: 1px dashed var(--form-input-border-color);
|
||||||
word-break: break-all;
|
display: inline-flex;
|
||||||
display: inline-block;
|
max-width: clamp(200px, 60vw, 600px);
|
||||||
max-width: 80%;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -29,13 +31,14 @@
|
|||||||
color: var(--form-button-text-color);
|
color: var(--form-button-text-color);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.3em 0.8em;
|
padding: 0.3em 0.8em;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 550;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-path button#changePath:hover {
|
.download-path button#changePath:hover {
|
||||||
background-color: var(--form-button-bg-hover-color);
|
background-color: var(--form-button-bg-hover-color);
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
}
|
||||||
|
|||||||
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(--audio-only-label-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--audio-only-label-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); }
|
||||||
|
}
|
||||||
89
public/styles/components/progressBar.css
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/* Container pour isoler la progress bar */
|
||||||
|
.progress-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wrapper de la barre de téléchargement */
|
||||||
|
#downloadProgressWrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: var(--progress-bar-bg-color, #cccccc);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
display: none;
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barre de progression */
|
||||||
|
#downloadProgress {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--progress-bar-fill-color, #007bff);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadProgressText, #downloadSpeedText {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #fff;
|
||||||
|
content: "";
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive pour mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#downloadProgressWrapper {
|
||||||
|
max-width: 90%;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadProgressText, #downloadSpeedText {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stop Button */
|
||||||
|
.stop-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--form-button-bg-color);
|
||||||
|
color: var(--form-button-text-color);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn:hover:not(:disabled) {
|
||||||
|
background-color: var(--form-button-bg-hover-color);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn:active:not(:disabled) {
|
||||||
|
transform: scale(0.96);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
.theme-switcher {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
background: #242424;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #fff;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-switcher label {
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-switcher select {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-switcher select:hover,
|
|
||||||
.theme-switcher select:focus {
|
|
||||||
background: #474747;
|
|
||||||
color: #fff;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
136
public/styles/components/toast.css
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/* Toast Container */
|
||||||
|
#toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 140px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toast {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background-color: rgba(50, 50, 50, 0.9);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
max-width: 220px;
|
||||||
|
min-width: 100px;
|
||||||
|
animation: slideIn 0.25s ease-out;
|
||||||
|
pointer-events: auto;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
background-color: rgba(40, 167, 69, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background-color: rgba(220, 53, 69, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning {
|
||||||
|
background-color: rgba(255, 193, 7, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
background-color: rgba(23, 162, 184, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Icon */
|
||||||
|
.toast-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success .toast-icon {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error .toast-icon {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning .toast-icon {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info .toast-icon {
|
||||||
|
color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Message */
|
||||||
|
.toast-message {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Close Button */
|
||||||
|
.toast-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: currentColor;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(350px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(350px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.removing {
|
||||||
|
animation: slideOut 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#toast-container {
|
||||||
|
bottom: 12px;
|
||||||
|
right: 12px;
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
max-width: none;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,22 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
/* height: 10000px; */
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
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;
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -40,8 +40,7 @@ form button:hover {
|
|||||||
form#downloadForm {
|
form#downloadForm {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
max-width: 600px;
|
width: 350px;
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +49,7 @@ form#downloadForm {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
color: var(--form-input-text-color);
|
color: var(--audio-only-label-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#UrlInput {
|
#UrlInput {
|
||||||
@@ -62,6 +61,7 @@ form#downloadForm {
|
|||||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
background-color: var(--form-input-bg-color);
|
background-color: var(--form-input-bg-color);
|
||||||
color: var(--form-input-text-color);
|
color: var(--form-input-text-color);
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#UrlInput:focus {
|
#UrlInput:focus {
|
||||||
@@ -74,3 +74,40 @@ form#downloadForm {
|
|||||||
#UrlInput::placeholder {
|
#UrlInput::placeholder {
|
||||||
color: var(--form-input-placeholder-color);
|
color: var(--form-input-placeholder-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.url-input-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-btn {
|
||||||
|
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, transform 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-btn:hover {
|
||||||
|
background-color: var(--form-button-bg-hover-color);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-btn svg {
|
||||||
|
color: var(--paste-button-icon-color);
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
header {
|
header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
z-index: 1100;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2vw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
header h1{
|
color: var(--default-text-color);
|
||||||
margin-bottom: 0.2vw;
|
|
||||||
}
|
}
|
||||||
277
public/styles/layout/settingsPanel.css
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/* SETTINGS BUTTON */
|
||||||
|
.settings-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 60px;
|
||||||
|
right: 20px;
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: 15;
|
||||||
|
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-modal-bg-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-section-bg-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(--open-json-button-bg-color);
|
||||||
|
color: var(--open-json-button-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#open-theme {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: var(--open-theme-button-bg-color);
|
||||||
|
color: var(--open-theme-button-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* REFRESH THEMES BUTTON */
|
||||||
|
.refresh-themes-btn {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--form-input-border-color);
|
||||||
|
background-color: var(--form-input-bg-color);
|
||||||
|
color: var(--form-input-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-themes-btn:hover {
|
||||||
|
background-color: var(--form-button-bg-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-themes-btn.spinning svg {
|
||||||
|
animation: spin 0.6s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-themes-btn svg {
|
||||||
|
color: currentColor;
|
||||||
|
transition: transform 0.6s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
108
public/styles/layout/topbar.css
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 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;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding: 1.5em 2em;
|
padding: 1.5em 2em;
|
||||||
background-color: var(--infos-box-color);
|
background-color: var(--video-info-bg-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||||
color: var(--video-info-text-color);
|
color: var(--video-info-text-color);
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
#videoInfo.visible {
|
#videoInfo.visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
max-height: 1500px;
|
max-height: 1500px;
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#videoInfo h3 {
|
#videoInfo h3 {
|
||||||
@@ -68,7 +67,295 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Playlist Mode --- */
|
||||||
|
#videoInfo.playlist-mode {
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Playlist Header */
|
||||||
|
.playlist-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 2rem;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--video-info-text-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-title {
|
||||||
|
font-size: 1.9rem;
|
||||||
|
margin: 0 0 0.6rem 0;
|
||||||
|
color: var(--video-info-heading-color);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--video-info-text-color);
|
||||||
|
opacity: 0.85;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-count,
|
||||||
|
.playlist-channel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-count svg,
|
||||||
|
.playlist-channel svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout liste des vidéos */
|
||||||
|
#playlistVideos {
|
||||||
|
margin-top: 1em;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Carte vidéo playlist */
|
||||||
|
.playlist-video-card {
|
||||||
|
background: var(--playlist-background-color);
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
|
||||||
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-video-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, var(--video-info-link-color), transparent);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-video-card:hover {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
box-shadow: 0 12px 32px rgba(0,0,0,0.25);
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-video-card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thumbnail wrapper avec durée */
|
||||||
|
.video-thumbnail-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-video-card:hover .video-thumbnail {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-duration {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contenu vidéo */
|
||||||
|
.video-content {
|
||||||
|
padding: 1.2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
flex: 1;
|
||||||
|
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.02));
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--video-info-heading-color);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-height: 2.8em;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-video-card:hover .video-title {
|
||||||
|
color: var(--video-info-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-uploader {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--video-info-text-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions vidéo */
|
||||||
|
.video-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 0.8rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-link {
|
||||||
|
flex: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--video-info-link-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.5rem 0.9rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-link svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-link:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
text-decoration: none;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: var(--video-info-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#playlistVideos {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-meta {
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
#videoInfo {
|
#videoInfo {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
@@ -78,4 +365,21 @@
|
|||||||
#videoInfo h3 {
|
#videoInfo h3 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#playlistVideos {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-header {
|
||||||
|
padding: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,123 @@
|
|||||||
|
/* Layout */
|
||||||
|
@import url("layout/topbar.css");
|
||||||
|
@import url("layout/header.css");
|
||||||
@import url("layout/form.css");
|
@import url("layout/form.css");
|
||||||
@import url("layout/container.css");
|
@import url("layout/container.css");
|
||||||
@import url("layout/ctr-infos.css");
|
@import url("layout/settingsPanel.css");
|
||||||
@import url("layout/header.css");
|
|
||||||
@import url("layout/videoinfo.css");
|
@import url("layout/videoinfo.css");
|
||||||
|
|
||||||
|
/* Components */
|
||||||
@import url("components/checkbox.css");
|
@import url("components/checkbox.css");
|
||||||
@import url("components/themebutton.css");
|
@import url("components/loader.css");
|
||||||
/* @import url("variables.css"); */
|
|
||||||
@import url("theme/themeimport.css");
|
|
||||||
@import url("components/editpathbutton.css");
|
@import url("components/editpathbutton.css");
|
||||||
|
@import url("components/progressBar.css");
|
||||||
|
@import url("components/toast.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');
|
@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;
|
/* Mark element as disabled */
|
||||||
opacity: 0;
|
.disabled {
|
||||||
transition: opacity 0.6s ease;
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(40%);
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
#app.visible {
|
|
||||||
opacity: 1;
|
/* Because why not */
|
||||||
|
#reference{
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
bottom:0vh;
|
||||||
|
left: 10px;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #b0b0b02f;
|
||||||
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
html,body{
|
html,body{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
/* outline: 2px solid red; */ /*<-- Utile pour le début ça */
|
/*outline: 2px solid red; /*<-- Utile pour le debug ça */
|
||||||
font-family: "Poppins", sans-serif;
|
font-family: "Poppins", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body{
|
||||||
margin: 0;
|
background-color: var(--background-color);
|
||||||
background-color: var(--background-color);
|
color: var(--default-text-color);
|
||||||
/* background-image: linear-gradient(to left top, #838383, #a1a1a1, #bfbfbf, #dfdfdf, #ffffff);
|
}
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
header {
|
||||||
background-attachment: fixed; */
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
z-index: 1100;
|
||||||
color: var(--default-text-color);
|
height: 48px;
|
||||||
display: flex;
|
}
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
main {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 48px);
|
||||||
|
margin-top: 48px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
header p{
|
/* Scrollbar pour main */
|
||||||
|
main::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main::-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
main::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.35);
|
||||||
|
transform: scaleX(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thème clair */
|
||||||
|
body.light main::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light main::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light main::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container{
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitle{
|
||||||
color: var(--subtitle-color);
|
color: var(--subtitle-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-inline{
|
|
||||||
color: var(--mp3-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
body,
|
||||||
.container,
|
.container,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
body.chirac {
|
|
||||||
/* Background image Chirac */
|
|
||||||
background-image: url('../../assets/images/goat.webp');
|
|
||||||
background-position: right;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
background-attachment: fixed;
|
|
||||||
|
|
||||||
/* Couleurs drapeau français */
|
|
||||||
--background-color: rgba(255, 255, 255, 0.85); /* blanc légèrement transparent pour lisibilité */
|
|
||||||
--default-text-color: #002395; /* bleu foncé */
|
|
||||||
--subtitle-color: #EF4135; /* rouge vif */
|
|
||||||
--mp3-text-color: #002395; /* bleu */
|
|
||||||
|
|
||||||
/* 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; /* bleu */
|
|
||||||
--form-button-text-color: #ffffff;
|
|
||||||
--form-button-bg-hover-color: #EF4135; /* rouge */
|
|
||||||
|
|
||||||
/* 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: #002395;
|
|
||||||
--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;
|
|
||||||
|
|
||||||
/* animation d'apparition */
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.8s ease, background-color 0.8s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.chirac.theme-active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
*/
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
body.cyberpunk {
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #0d0d0d; /* noir profond */
|
|
||||||
--default-text-color: #e0e0e0; /* texte gris clair */
|
|
||||||
--subtitle-color: #ffea00; /* turquoise néon pour les sous-titres */
|
|
||||||
--mp3-text-color: #ffea00; /* jaune vif pour la checkbox */
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #141414; /* fond formulaire sombre */
|
|
||||||
--form-input-bg-color: #1a1a1a; /* inputs sombres */
|
|
||||||
--form-input-border-color: #333333; /* bordure sobre */
|
|
||||||
--form-input-border-focus-color: #ffea00; /* focus jaune néon */
|
|
||||||
--form-input-text-color: #e0e0e0; /* texte clair */
|
|
||||||
--form-input-placeholder-color: #777777; /* placeholder plus doux */
|
|
||||||
--form-button-bg-color: #ffea00; /* bouton jaune */
|
|
||||||
--form-button-text-color: #0d0d0d; /* texte bouton noir */
|
|
||||||
--form-button-bg-hover-color: #d4c000; /* hover jaune plus foncé */
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #333333; /* fond checkbox sobre */
|
|
||||||
--checkbox-bg-checked: #ffea00; /* coché en jaune vif */
|
|
||||||
--checkbox-checkmark-border-color: #0d0d0d; /* checkmark noir */
|
|
||||||
--checkbox-pulse-color: rgba(255, 234, 0, 0.5); /* pulse néon jaune */
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #fffbd1; /* turquoise néon */
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #1a1a1a; /* fond infos */
|
|
||||||
--video-info-text-color: #e0e0e0; /* texte clair */
|
|
||||||
--video-info-heading-color: #ffea00; /* titres turquoise néon */
|
|
||||||
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
|
||||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
|
||||||
--video-info-link-color: #ffea00; /* liens jaune néon */
|
|
||||||
--video-info-link-hover-color: #d4c000; /* liens hover plus foncé */
|
|
||||||
}
|
|
||||||
|
|
||||||
body.cyberpunk {
|
|
||||||
background-image: url('../../assets/images/cyberpunk4.webp');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-attachment: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
:root {
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #001224; /* body background bleu nuit */
|
|
||||||
--default-text-color: #eee; /* texte clair principal */
|
|
||||||
--subtitle-color: #007bff; /* sous-titres, header p */
|
|
||||||
--mp3-text-color: #000000; /* couleur texte checkbox inline */
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #ffffff; /* fond du formulaire */
|
|
||||||
--form-input-bg-color: #f9f9f9; /* fond inputs */
|
|
||||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
|
||||||
--form-input-border-focus-color: #0056b3; /* bordure focus */
|
|
||||||
--form-input-text-color: #333333; /* texte inputs */
|
|
||||||
--form-input-placeholder-color: #aaaaaa; /* placeholder */
|
|
||||||
--form-button-bg-color: #007bff; /* bouton bg */
|
|
||||||
--form-button-text-color: #ffffff; /* texte bouton */
|
|
||||||
--form-button-bg-hover-color: #0056b3; /* bouton hover */
|
|
||||||
|
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.default .download-path #savePath {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
body.fanatic{
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #121212; /* fond bleu nuit/noir */
|
|
||||||
--default-text-color: #eee; /* texte clair principal */
|
|
||||||
--subtitle-color: #FF5900; /* sous-titres, accent orange vif */
|
|
||||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #1e1e1e; /* fond formulaire sombre */
|
|
||||||
--form-input-bg-color: #2b2b2b; /* fond inputs sombres */
|
|
||||||
--form-input-border-color: #333333; /* bordure inputs */
|
|
||||||
--form-input-border-focus-color: #FF5900; /* bordure focus orange */
|
|
||||||
--form-input-text-color: #eeeeee; /* texte inputs clair */
|
|
||||||
--form-input-placeholder-color: #666666; /* placeholder gris foncé */
|
|
||||||
--form-button-bg-color: #FF5900; /* bouton orange */
|
|
||||||
--form-button-text-color: #121212; /* texte bouton sombre */
|
|
||||||
--form-button-bg-hover-color: #e65500; /* bouton hover orange foncé */
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #555555; /* gris moyen */
|
|
||||||
--checkbox-bg-checked: #ff6600; /* orange vif */
|
|
||||||
--checkbox-checkmark-border-color: #eee; /* blanc cassé */
|
|
||||||
--checkbox-pulse-color: rgba(255, 102, 0, 0.5); /* pulse orange translucide */
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #FF5900; /* orange */
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #222222; /* fond infos sombre */
|
|
||||||
--video-info-text-color: #eee; /* texte clair */
|
|
||||||
--video-info-heading-color: #FF5900; /* titres orange */
|
|
||||||
--video-info-list-color: #ccc; /* texte liste gris clair */
|
|
||||||
--video-info-list-strong-color: #fff; /* texte important blanc */
|
|
||||||
--video-info-link-color: #FF5900; /* liens orange */
|
|
||||||
--video-info-link-hover-color: #e65500; /* liens hover orange foncé */
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
body.light {
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #dadada; /* fond clair */
|
|
||||||
--default-text-color: #222222; /* texte sombre */
|
|
||||||
--subtitle-color: #007bff; /* bleu accent */
|
|
||||||
--mp3-text-color: #000000; /* texte checkbox */
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #ffffff; /* fond formulaire blanc */
|
|
||||||
--form-input-bg-color: #fafafa; /* fond inputs */
|
|
||||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
|
||||||
--form-input-border-focus-color: #007bff; /* focus bleu */
|
|
||||||
--form-input-text-color: #333333; /* texte inputs */
|
|
||||||
--form-input-placeholder-color: #888888; /* placeholder */
|
|
||||||
--form-button-bg-color: #007bff; /* bouton bleu */
|
|
||||||
--form-button-text-color: #ffffff; /* texte bouton */
|
|
||||||
--form-button-bg-hover-color: #0056b3; /* hover bleu foncé */
|
|
||||||
|
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
body.spicy {
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #121212; /* fond noir profond */
|
|
||||||
--default-text-color: #f5f5f5; /* texte clair */
|
|
||||||
--subtitle-color: #e29191; /* 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é */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optionnel: background piments */
|
|
||||||
body.spicy {
|
|
||||||
background-image: url('../../assets/images/spicy.webp');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-attachment: fixed;
|
|
||||||
}
|
|
||||||
8
public/styles/theme/themeimport.css
vendored
@@ -1,8 +0,0 @@
|
|||||||
@import url("chirac-theme.css");
|
|
||||||
@import url("dark-theme.css");
|
|
||||||
@import url("default-theme.css");
|
|
||||||
@import url("fanatic-theme.css");
|
|
||||||
@import url("light-theme.css");
|
|
||||||
@import url("cyberpunk-theme.css");
|
|
||||||
@import url("spicy-theme.css");
|
|
||||||
@import url("vilbrequin-theme.css");
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
body.vilbrequin {
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #0a1f3d; /* bleu Vilbrequin profond */
|
|
||||||
--default-text-color: #535353; /* texte clair */
|
|
||||||
--subtitle-color: #001696; /* bleu clair accent (turbo vibes) */
|
|
||||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #12284f; /* fond formulaire bleu foncé */
|
|
||||||
--form-input-bg-color: #1c3760; /* fond inputs bleu + clair */
|
|
||||||
--form-input-border-color: #2a4b80; /* bordure bleue */
|
|
||||||
--form-input-border-focus-color: #00bfff; /* focus bleu clair */
|
|
||||||
--form-input-text-color: #f0f0f0; /* texte inputs clair */
|
|
||||||
--form-input-placeholder-color: #b0c4de; /* placeholder gris bleuté */
|
|
||||||
--form-button-bg-color: #ffd700; /* bouton jaune turbo */
|
|
||||||
--form-button-text-color: #0a1f3d; /* texte bouton sombre */
|
|
||||||
--form-button-bg-hover-color: #e6c200; /* hover jaune foncé */
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #2a4b80; /* bleu moyen */
|
|
||||||
--checkbox-bg-checked: #00bfff; /* bleu clair accent */
|
|
||||||
--checkbox-checkmark-border-color: #f0f0f0; /* blanc cassé */
|
|
||||||
--checkbox-pulse-color: rgba(0,191,255,0.5); /* pulse bleu clair translucide */
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #ffd700; /* jaune turbo */
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #12284f; /* fond infos bleu foncé */
|
|
||||||
--video-info-text-color: #f0f0f0; /* texte clair */
|
|
||||||
--video-info-heading-color: #00bfff; /* titres bleu clair */
|
|
||||||
--video-info-list-color: #dcdcdc; /* texte liste gris clair */
|
|
||||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
|
||||||
--video-info-link-color: #00bfff; /* liens bleu clair */
|
|
||||||
--video-info-link-hover-color: #009acd; /* liens hover bleu plus foncé */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optionnel: motif turbo ou jantes */
|
|
||||||
body.vilbrequin {
|
|
||||||
background-image: url('../../assets/images/Vilbrequin.avif'); /* adapte ton image */
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-attachment: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.vilbrequin .download-path{
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,54 @@
|
|||||||
:root {
|
:root {
|
||||||
/* Couleurs générales */
|
/* Couleurs générales */
|
||||||
--background-color: #001224; /* body background bleu nuit */
|
--background-color: #001224;
|
||||||
--default-text-color: #eee; /* texte clair principal */
|
--default-text-color: #eee;
|
||||||
--subtitle-color: #007bff; /* sous-titres, header p */
|
--subtitle-color: #007bff;
|
||||||
--mp3-text-color: #000000; /* couleur texte checkbox inline */
|
--audio-only-label-color: #000000;
|
||||||
|
|
||||||
/* Formulaire */
|
/* Formulaire */
|
||||||
--form-bg-color: #ffffff; /* fond du formulaire */
|
--form-bg-color: #ffffff;
|
||||||
--form-input-bg-color: #f9f9f9; /* fond inputs */
|
--form-input-bg-color: #f9f9f9;
|
||||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
--form-input-border-color: #cccccc;
|
||||||
--form-input-border-focus-color: #0056b3; /* bordure focus */
|
--form-input-border-focus-color: #0056b3;
|
||||||
--form-input-text-color: #333333; /* texte inputs */
|
--form-input-text-color: #333333;
|
||||||
--form-input-placeholder-color: #aaaaaa; /* placeholder */
|
--form-input-placeholder-color: #aaaaaa;
|
||||||
--form-button-bg-color: #007bff; /* bouton bg */
|
--form-button-bg-color: #007bff;
|
||||||
--form-button-text-color: #ffffff; /* texte bouton */
|
--form-button-text-color: #ffffff;
|
||||||
--form-button-bg-hover-color: #0056b3; /* bouton hover */
|
--form-button-bg-hover-color: #0056b3;
|
||||||
|
--paste-button-icon-color: #ffffff;
|
||||||
|
|
||||||
/* Checkbox */
|
/* Checkbox */
|
||||||
--checkbox-bg-default: #cccccc;
|
--checkbox-unchecked-bg-color: #cccccc;
|
||||||
--checkbox-bg-checked: #3B82F6;
|
--checkbox-checked-bg-color: #3B82F6;
|
||||||
--checkbox-checkmark-border-color: #E0E0E2;
|
--checkbox-checkmark-border-color: #E0E0E2;
|
||||||
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||||
|
|
||||||
/* Download status */
|
/* Download status */
|
||||||
--download-status-color: #007bff;
|
--download-status-color: #007bff;
|
||||||
|
|
||||||
|
/* Progress bar */
|
||||||
|
--progress-bar-bg-color: #cccccc;
|
||||||
|
--progress-bar-fill-color: #007bff;
|
||||||
|
|
||||||
/* Video Info Box */
|
/* Video Info Box */
|
||||||
--infos-box-color: #f0f0f0;
|
--video-info-bg-color: #f0f0f0;
|
||||||
--video-info-text-color: #222222;
|
--video-info-text-color: #222222;
|
||||||
--video-info-heading-color: #007bff;
|
--video-info-heading-color: #007bff;
|
||||||
--video-info-list-color: #444444;
|
--video-info-list-color: #444444;
|
||||||
--video-info-list-strong-color: #000000;
|
--video-info-list-strong-color: #000000;
|
||||||
--video-info-link-color: #007bff;
|
--video-info-link-color: #007bff;
|
||||||
--video-info-link-hover-color: #0056b3;
|
--video-info-link-hover-color: #0056b3;
|
||||||
|
|
||||||
|
/* Playlist */
|
||||||
|
--playlist-background-color: #4f4f4f;
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
--settings-button-bg-color: var(--form-bg-color);
|
||||||
|
--settings-button-text-color: var(--default-text-color);
|
||||||
|
|
||||||
|
--settings-modal-bg-color: var(--form-bg-color);
|
||||||
|
--settings-section-bg-color: var(--form-bg-color);
|
||||||
|
--settings-text-color: var(--default-text-color);
|
||||||
|
--settings-subtitle-color: #aaa;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
resources/binaries/linux/yt-dlp
Executable file
20
scripts/make-copr-srpm.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
|
||||||
|
mkdir -p "$ROOT_DIR/srpm-out"
|
||||||
|
|
||||||
|
TMP_DIR="$ROOT_DIR/.copr-build-tmp"
|
||||||
|
mkdir -p "$TMP_DIR"
|
||||||
|
TMP_SPEC="$TMP_DIR/freedom-loader.spec"
|
||||||
|
sed "s/__VERSION__/$VERSION/g" "$ROOT_DIR/package/freedom-loader.spec" > "$TMP_SPEC"
|
||||||
|
|
||||||
|
rpmbuild -bs "$TMP_SPEC" \
|
||||||
|
--define "_sourcedir $ROOT_DIR/dist" \
|
||||||
|
--define "_srcrpmdir $ROOT_DIR/srpm-out"
|
||||||
|
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
echo "SRPM généré : $ROOT_DIR/srpm-out/freedom-loader-$VERSION-1*.src.rpm"
|
||||||
198
server/controller/download.controller.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
const { fetchDownload, cancelDownload } = require("../services/download.services");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
|
||||||
|
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
|
||||||
|
const { configFeatures } = require("../../config");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active Server-Sent Event subscribers receiving
|
||||||
|
* download progress updates.
|
||||||
|
*
|
||||||
|
* Each listener receives a percentage value between
|
||||||
|
* 0 and 100 during the download lifecycle.
|
||||||
|
*/
|
||||||
|
const progressListeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active Server-Sent Event subscribers receiving
|
||||||
|
* download speed updates.
|
||||||
|
*/
|
||||||
|
const speedListeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active Server-Sent Event subscribers receiving
|
||||||
|
* download stage updates.
|
||||||
|
*/
|
||||||
|
const stageListeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active Server-Sent Event subscribers receiving
|
||||||
|
* playlist progress information.
|
||||||
|
*/
|
||||||
|
const playlistListeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles download requests and starts a yt-dlp download.
|
||||||
|
*
|
||||||
|
* Validates user input, prepares download options and
|
||||||
|
* delegates the download process to the download service.
|
||||||
|
*
|
||||||
|
* Once the download completes successfully, a desktop
|
||||||
|
* notification may be displayed depending on the
|
||||||
|
* application configuration.
|
||||||
|
*
|
||||||
|
* @param {import("express").Request} req
|
||||||
|
* @param {import("express").Response} res
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
playlistTitle: req.body.playlistTitle || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("Invalid URL !");
|
||||||
|
if (options.outputFolder && !isSafePath(options.outputFolder)) {
|
||||||
|
logger.warn(`Unsafe download path rejected: ${options.outputFolder}`);
|
||||||
|
return res.status(400).send("Save Path Not Allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputFolder = await fetchDownload(
|
||||||
|
options,
|
||||||
|
progressListeners,
|
||||||
|
speedListeners,
|
||||||
|
stageListeners,
|
||||||
|
playlistListeners
|
||||||
|
);
|
||||||
|
|
||||||
|
notifyDownloadFinished(outputFolder, configFeatures.notifySystem);
|
||||||
|
res.send("Download Done !");
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Server Error in /download: ${err?.message ?? err}`);
|
||||||
|
res.status(500).send("Server Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a Server-Sent Events (SSE) connection used to
|
||||||
|
* stream download progress updates to the client.
|
||||||
|
*
|
||||||
|
* @param {import("express").Request} req
|
||||||
|
* @param {import("express").Response} res
|
||||||
|
*/
|
||||||
|
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`);
|
||||||
|
progressListeners.push(sendProgress);
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
progressListeners.splice(progressListeners.indexOf(sendProgress), 1);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a Server-Sent Events (SSE) connection used to
|
||||||
|
* stream download speed updates to the client.
|
||||||
|
*
|
||||||
|
* @param {import("express").Request} req
|
||||||
|
* @param {import("express").Response} res
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a Server-Sent Events (SSE) connection used to
|
||||||
|
* stream download stage updates such as downloading,
|
||||||
|
* merging, extracting audio or embedding metadata.
|
||||||
|
*
|
||||||
|
* @param {import("express").Request} req
|
||||||
|
* @param {import("express").Response} res
|
||||||
|
*/
|
||||||
|
function stageController(req, res) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendStage = stage => res.write(`data: ${stage}\n\n`);
|
||||||
|
stageListeners.push(sendStage);
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
stageListeners.splice(stageListeners.indexOf(sendStage), 1);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the currently running download process.
|
||||||
|
*
|
||||||
|
* Returns an error response if no download is active.
|
||||||
|
*
|
||||||
|
* @param {import("express").Request} req
|
||||||
|
* @param {import("express").Response} res
|
||||||
|
*/
|
||||||
|
function cancelDownloadController(req, res) {
|
||||||
|
const cancelled = cancelDownload();
|
||||||
|
if (cancelled) {
|
||||||
|
logger.info("Download and queue cancelled by user");
|
||||||
|
res.send("Download stopped! Queue cleared.");
|
||||||
|
} else {
|
||||||
|
logger.warn("No download to cancel");
|
||||||
|
res.status(400).send("No download in progress !");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a Server-Sent Events (SSE) connection used to
|
||||||
|
* stream playlist progress information to the client.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* - Item 1 of 20
|
||||||
|
* - Item 12 of 20
|
||||||
|
* - Item 20 of 20
|
||||||
|
*
|
||||||
|
* @param {import("express").Request} req
|
||||||
|
* @param {import("express").Response} res
|
||||||
|
*/
|
||||||
|
function playlistInfoController(req, res) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendPlaylistInfo = info => res.write(`data: ${info}\n\n`);
|
||||||
|
playlistListeners.push(sendPlaylistInfo);
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
playlistListeners.splice(playlistListeners.indexOf(sendPlaylistInfo), 1);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { downloadController, progressController, speedController, stageController, cancelDownloadController, playlistInfoController};
|
||||||
116
server/controller/info.controller.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
const { fetchInfo } = require("../services/info.services");
|
||||||
|
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
const { isValidUrl } = require("../helpers/validation.helpers");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles metadata retrieval for a video or playlist.
|
||||||
|
*
|
||||||
|
* This controller:
|
||||||
|
* - Validates incoming URL (query or body)
|
||||||
|
* - Calls yt-dlp metadata extraction service
|
||||||
|
* - Detects playlist vs single video context
|
||||||
|
* - Applies fallback strategy if playlist extraction fails
|
||||||
|
* - Returns normalized parsed response (video or playlist)
|
||||||
|
*
|
||||||
|
* @param {import("express").Request} req
|
||||||
|
* @param {import("express").Response} res
|
||||||
|
*/
|
||||||
|
async function infoController(req, res) {
|
||||||
|
|
||||||
|
const url = req.body.url || req.query.url; // Supports POST and GET requests
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic validation:
|
||||||
|
* - URL must exist
|
||||||
|
* - Must be a string
|
||||||
|
* - Must pass custom URL validation
|
||||||
|
*/
|
||||||
|
if (!url || typeof url !== "string" || !isValidUrl(url)) {
|
||||||
|
return res.status(400).send("Invalid URL Or Missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Info request received. URL: ${url}`);
|
||||||
|
|
||||||
|
// Lightweight heuristic to detect playlist URLs
|
||||||
|
const isPlaylistUrl = url.includes("&list") || url.includes("?list");
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
isPlaylistUrl
|
||||||
|
? "Estimated Data Type: Playlist"
|
||||||
|
: "Estimated Data Type: Video"
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary metadata fetch attempt
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
data = await fetchInfo(url);
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback strategy:
|
||||||
|
* If playlist extraction fails, retry as single video
|
||||||
|
*/
|
||||||
|
if (isPlaylistUrl) {
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Playlist fetch failed: ${err.message}. Retrying as single video...`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove playlist query parameter (best-effort cleanup)
|
||||||
|
const singleVideoUrl = url.split("&list")[0].split("?list")[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = await fetchInfo(singleVideoUrl);
|
||||||
|
logger.info("Single video fetch succeeded after playlist failure");
|
||||||
|
} catch (retryErr) {
|
||||||
|
logger.error(
|
||||||
|
`Single video fetch also failed: ${retryErr.message}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Preserve original error context
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize response based on yt-dlp output type
|
||||||
|
*/
|
||||||
|
if (data._type === "playlist") {
|
||||||
|
|
||||||
|
const playlist = parsePlaylist(data);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Playlist fetched: ${playlist.title} (${playlist.count} videos)`
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json(playlist);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const video = parseVideo(data);
|
||||||
|
|
||||||
|
logger.info(`Video fetched: ${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 };
|
||||||
127
server/helpers/buildArgs.helpers.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const getUserBrowser = require("./getBrowser.helpers.js");
|
||||||
|
const { ffmpegPath, denoPath} = require("./path.helpers.js");
|
||||||
|
const { configFeatures } = require("../../config.js");
|
||||||
|
const { logger } = require("../logger.js");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates whether the provided codec is supported by the application.
|
||||||
|
*
|
||||||
|
* If the codec is supported, it is returned unchanged.
|
||||||
|
* Otherwise, a warning is logged and the default codec ("h264")
|
||||||
|
* is returned as a fallback.
|
||||||
|
*
|
||||||
|
* Supported codecs:
|
||||||
|
* - av01
|
||||||
|
* - vp9.2
|
||||||
|
* - vp9
|
||||||
|
* - h265
|
||||||
|
* - h264
|
||||||
|
* - vp8
|
||||||
|
* - h263
|
||||||
|
* - theora
|
||||||
|
*
|
||||||
|
* @param {string} codec - Codec identifier to validate.
|
||||||
|
* @returns {string} A valid codec identifier.
|
||||||
|
*/
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the complete yt-dlp argument list based on the
|
||||||
|
* application configuration and download options.
|
||||||
|
*
|
||||||
|
* The generated arguments include:
|
||||||
|
* - Browser cookie extraction
|
||||||
|
* - FFmpeg integration
|
||||||
|
* - Deno runtime configuration
|
||||||
|
* - Metadata and thumbnail embedding
|
||||||
|
* - Playlist handling
|
||||||
|
* - Video/audio format selection
|
||||||
|
* - Quality filtering
|
||||||
|
* - Output file destination
|
||||||
|
*
|
||||||
|
* @param {Object} options - Download configuration.
|
||||||
|
* @param {string} options.url - Video or playlist URL.
|
||||||
|
* @param {boolean} options.audioOnly - Whether to extract audio only.
|
||||||
|
* @param {string} options.quality - Requested quality preset.
|
||||||
|
* @param {string} options.outputFolder - Download destination folder.
|
||||||
|
*
|
||||||
|
* @returns {string[]} Array of arguments ready to be passed to yt-dlp.
|
||||||
|
*/
|
||||||
|
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 };
|
||||||
|
|
||||||
102
server/helpers/getBrowser.helpers.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const notify = require("./notify.helpers")
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects the first supported browser installed on the system.
|
||||||
|
*
|
||||||
|
* The detected browser is used by yt-dlp to import authentication
|
||||||
|
* cookies when required by certain platforms.
|
||||||
|
*
|
||||||
|
* Currently, Firefox is the only browser officially supported
|
||||||
|
* by the application.
|
||||||
|
*
|
||||||
|
* If no supported browser is found:
|
||||||
|
* - A warning is logged
|
||||||
|
* - A notification is displayed to the user
|
||||||
|
* - "firefox" is returned as a fallback to allow yt-dlp to
|
||||||
|
* handle the error gracefully without crashing the application
|
||||||
|
*
|
||||||
|
* @returns {string} The detected browser identifier.
|
||||||
|
*/
|
||||||
|
function getUserBrowser() {
|
||||||
|
//const userProfile = os.homedir();
|
||||||
|
const firefoxPath = getFirefoxProfilePath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browsers supported by yt-dlp cookie extraction.
|
||||||
|
*
|
||||||
|
* Only Firefox is currently enabled and tested.
|
||||||
|
* Additional browsers can be enabled in the future
|
||||||
|
* once compatibility has been verified.
|
||||||
|
*/
|
||||||
|
const browsers = [
|
||||||
|
{ name: "firefox", path: firefoxPath },
|
||||||
|
// { 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") }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Search for the first available browser profile.
|
||||||
|
for (const browser of browsers) {
|
||||||
|
if (browser.path && fs.existsSync(browser.path)) {
|
||||||
|
logger.info(`Browser found: ${browser.name}`);
|
||||||
|
return browser.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No supported browser found => Notify User
|
||||||
|
logger.warn("No supported browser found on the system");
|
||||||
|
|
||||||
|
// If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
notify.notifyFirefoxBrowserMissing();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Firefox and let yt-dlp handle the error gracefully.
|
||||||
|
// This prevents the application from crashing
|
||||||
|
return "firefox";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFirefoxProfilePath() {
|
||||||
|
const home = os.homedir();
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return path.join(
|
||||||
|
home,
|
||||||
|
"AppData",
|
||||||
|
"Roaming",
|
||||||
|
"Mozilla",
|
||||||
|
"Firefox",
|
||||||
|
"Profiles"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
return path.join(
|
||||||
|
"usr",
|
||||||
|
"bin",
|
||||||
|
"firefox"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
return path.join(
|
||||||
|
home,
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"Firefox",
|
||||||
|
"Profiles"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getUserBrowser
|
||||||
81
server/helpers/notify.helpers.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
const { Notification, shell } = require("electron");
|
||||||
|
const { iconPaths } = require("./path.helpers");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a system notification when a download completes successfully.
|
||||||
|
*
|
||||||
|
* If enabled, clicking the notification opens the download folder
|
||||||
|
* using the system file explorer.
|
||||||
|
*
|
||||||
|
* @param {string} folder - Path to the completed download folder.
|
||||||
|
* @param {boolean} notifyEnabled - Whether notifications are enabled in config.
|
||||||
|
*/
|
||||||
|
function notifyDownloadFinished(folder, notifyEnabled = true) {
|
||||||
|
if (!notifyEnabled) return;
|
||||||
|
if (!folder) return;
|
||||||
|
|
||||||
|
const notif = new Notification({
|
||||||
|
title: "Freedom Loader",
|
||||||
|
body: "Your download is complete, click here to open it.",
|
||||||
|
icon: iconPaths.confirm,
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("click", () => shell.openPath(folder));
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a notification when browser cookies cannot be extracted.
|
||||||
|
*
|
||||||
|
* This usually indicates:
|
||||||
|
* - User is not logged in to the browser
|
||||||
|
* - Browser profile is inaccessible
|
||||||
|
*
|
||||||
|
* Clicking the notification opens a tutorial link.
|
||||||
|
*/
|
||||||
|
function notifyCookiesBrowserError(){
|
||||||
|
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: iconPaths.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("click", () =>
|
||||||
|
shell.openExternal(
|
||||||
|
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a notification when Firefox is not detected
|
||||||
|
* on the user system.
|
||||||
|
*
|
||||||
|
* This is required for cookie extraction via yt-dlp.
|
||||||
|
*
|
||||||
|
* Clicking the notification opens an installation guide.
|
||||||
|
*/
|
||||||
|
function notifyFirefoxBrowserMissing() {
|
||||||
|
const notif = new Notification({
|
||||||
|
title: "Firefox Missing",
|
||||||
|
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
||||||
|
icon: iconPaths.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("click", () =>
|
||||||
|
shell.openExternal(
|
||||||
|
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
notifyDownloadFinished,
|
||||||
|
notifyCookiesBrowserError,
|
||||||
|
notifyFirefoxBrowserMissing,
|
||||||
|
};
|
||||||