Compare commits
501 Commits
1.4.2
...
d11c0f9839
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d11c0f9839 | ||
|
|
1e0b2affee | ||
|
|
9975b2b583 | ||
|
|
b319a1e445 | ||
|
|
4a64a40226 | ||
|
|
2c0f5b315b | ||
|
|
58b6637eeb | ||
|
|
8af5a4a383 | ||
|
|
23f04bafc3 | ||
|
|
35ffd7077e | ||
|
|
408df0e2f6 | ||
|
|
7dee94da6f | ||
|
|
cd43ae10fc | ||
|
|
6978277b4b | ||
|
|
b91c20d450 | ||
|
|
014b1ad52a | ||
|
|
ec93d20a39 | ||
|
|
09c511fcb8 | ||
|
|
f343fbd148 | ||
|
|
6d99d5f91c | ||
|
|
0f0bb874bf | ||
|
|
b9267ec408 | ||
|
|
311bff6df5 | ||
|
|
e4243a01ae | ||
|
|
52be9c6b4f | ||
|
|
e541eb442a | ||
|
|
e2c2dde4a2 | ||
|
|
ed4022dfd1 | ||
|
|
fcbc3f95ac | ||
|
|
f3293ef89a | ||
|
|
05e864b93a | ||
|
|
8ec901dc07 | ||
|
|
104500a82a | ||
|
|
3444f2fc0b | ||
|
|
86b595beff | ||
|
|
ed6efcf812 | ||
|
|
2c84ac7a70 | ||
|
|
3c649173a2 | ||
|
|
b4d4ff2665 | ||
|
|
c2584b6df3 | ||
|
|
ec16b5209d | ||
|
|
f2f1649493 | ||
|
|
e5076b0e0b | ||
|
|
4573917541 | ||
|
|
62c109939a | ||
|
|
b56ec1e62e | ||
|
|
1a68c58ee3 | ||
|
|
39c4a69be1 | ||
|
|
1993566040 | ||
|
|
2d19a35ef8 | ||
|
|
02c39d53b6 | ||
|
|
26059aa436 | ||
|
|
524cee1bac | ||
|
|
4de05fcd52 | ||
|
|
c2158d99cd | ||
|
|
d84e63fb56 | ||
|
|
12d77abbd3 | ||
|
|
398de8113d | ||
|
|
5b13f5d3e0 | ||
|
|
7d27889743 | ||
|
|
dd4c93e293 | ||
|
|
e0d426aa9f | ||
|
|
c51f3eb25e | ||
|
|
4f8600dbbc | ||
|
|
63d30720a8 | ||
|
|
acf7e67d0d | ||
|
|
1a1acb53d1 | ||
|
|
c153db0244 | ||
|
|
4fda512ab5 | ||
|
|
66471ce5ce | ||
|
|
027aaf7564 | ||
|
|
7f8d018bcb | ||
|
|
6c517255e3 | ||
|
|
dc7239379d | ||
|
|
225ccf4d2d | ||
|
|
ce2a0485ab | ||
|
|
1690cc18b2 | ||
|
|
a8c2cf4c81 | ||
|
|
f1962f7cd3 | ||
|
|
245a463d52 | ||
|
|
b404e1ba8c | ||
|
|
b7e54806fd | ||
|
|
4df373d52b | ||
|
|
73aa81673b | ||
|
|
df0952c231 | ||
|
|
c67a1511a5 | ||
|
|
d3058a66cf | ||
|
|
90939e968a | ||
|
|
ec83c20e16 | ||
|
|
3f3696e54e | ||
|
|
321309e294 | ||
|
|
b9334461bf | ||
|
|
e09f1a8913 | ||
|
|
bbb0b58d17 | ||
|
|
47f5b0efef | ||
|
|
6b7645804a | ||
|
|
9f85acbb59 | ||
|
|
32eb849808 | ||
|
|
bc1c62dc35 | ||
|
|
72c152d964 | ||
|
|
596a06a59d | ||
|
|
6d4692486e | ||
|
|
7e162debcc | ||
|
|
ae6b849aac | ||
|
|
bb8ddabb24 | ||
|
|
620ce7a40c | ||
|
|
9892c6a7dc | ||
|
|
bc6283ec5c | ||
|
|
b1377fe21e | ||
|
|
e62ef6bd0c | ||
|
|
49650ecb36 | ||
|
|
4a56af2491 | ||
|
|
3cc9f90fdd | ||
|
|
3b555d465e | ||
|
|
857ff773b0 | ||
|
|
5f2bba2c85 | ||
|
|
c9a5820076 | ||
|
|
336aa33485 | ||
|
|
76ce2a8019 | ||
|
|
5bdd13ad8e | ||
|
|
14b7d944d1 | ||
|
|
3835806e1a | ||
|
|
398e9acf66 | ||
|
|
2f203a0bc1 | ||
|
|
acce91bed8 | ||
|
|
6b33662f79 | ||
|
|
2805f5f0db | ||
|
|
7ae0ef9a99 | ||
|
|
83dc0a3f2f | ||
|
|
3179b52908 | ||
|
|
9564260c57 | ||
|
|
d7c2339f61 | ||
|
|
12a6709391 | ||
|
|
6b625326a4 | ||
|
|
8c40ac2b1c | ||
|
|
71081163a6 | ||
|
|
4ab6257b39 | ||
|
|
0d0d60c692 | ||
|
|
504149a220 | ||
|
|
a6ac2333c6 | ||
|
|
eb232de34e | ||
|
|
cda79bd28f | ||
|
|
78e0bcab37 | ||
|
|
dbe313fac4 | ||
|
|
2316b73adb | ||
|
|
62140be696 | ||
|
|
aa3cb24b20 | ||
|
|
cbb847800a | ||
|
|
8643ed2867 | ||
|
|
e712163c14 | ||
|
|
edcd7c8f70 | ||
|
|
e6b1c8ba62 | ||
|
|
d90ae6f236 | ||
|
|
9d4eeca7ee | ||
|
|
68c0f24ebb | ||
|
|
ef8fe131f6 | ||
|
|
596a563e06 | ||
|
|
f583a006f2 | ||
|
|
3e2b7e8190 | ||
|
|
4b725bed8b | ||
|
|
cf596ddb13 | ||
|
|
6812296dbc | ||
|
|
94003135fd | ||
|
|
95688a5dff | ||
|
|
592cc3314a | ||
|
|
67b058e1ac | ||
|
|
264ac071df | ||
|
|
0bd3e10094 | ||
|
|
c656b30172 | ||
|
|
3d1920e6e9 | ||
|
|
372fc921f0 | ||
|
|
c32e382685 | ||
|
|
4d77e843af | ||
|
|
3c30e49523 | ||
|
|
6b4310500e | ||
|
|
252618dfd1 | ||
|
|
fb7f0329b6 | ||
|
|
4feea9c530 | ||
|
|
e8fe990e01 | ||
|
|
086247068e | ||
|
|
48a218efca | ||
|
|
24fc685bf6 | ||
|
|
731cdc38ec | ||
|
|
391c343b6a | ||
|
|
cbe689e38e | ||
|
|
1ac035e160 | ||
|
|
fb8e2acda0 | ||
|
|
3dd9f53e80 | ||
|
|
269b9e69ba | ||
|
|
5f3b2a80a9 | ||
|
|
fdda78c802 | ||
|
|
a82e8ceefd | ||
|
|
c9dd09d341 | ||
|
|
78191d27ad | ||
|
|
33290e029b | ||
|
|
245cf01a43 | ||
|
|
2689424d01 | ||
|
|
bfb0873b3d | ||
|
|
3a11395301 | ||
|
|
5dc7c229b0 | ||
|
|
95497d252f | ||
|
|
784834cbbc | ||
|
|
b6d77444e1 | ||
|
|
dd4d770102 | ||
|
|
a374993ab1 | ||
|
|
d99fad1c3d | ||
|
|
9f4563dbd8 | ||
|
|
5bb6b6c165 | ||
|
|
f8e346b3cb | ||
|
|
ee685695de | ||
|
|
3e2ff74876 | ||
|
|
bbdb6a37fb | ||
|
|
7b55a13ce6 | ||
|
|
a4efedbe19 | ||
|
|
c3d43cafbf | ||
|
|
bf91df3a27 | ||
|
|
28928ec174 | ||
|
|
f5c51e254a | ||
|
|
36deddd823 | ||
|
|
f6eca7f3f6 | ||
|
|
ccf9e5bc03 | ||
|
|
082f19bda4 | ||
|
|
67ecd28aeb | ||
|
|
02d60bddd2 | ||
|
|
5236165b47 | ||
|
|
044091a9c3 | ||
|
|
a04730243d | ||
|
|
b9fa931cd7 | ||
|
|
c1c1f9a870 | ||
|
|
538cb6b51a | ||
|
|
5319bee9ee | ||
|
|
1edec6509e | ||
|
|
22a3eb7524 | ||
|
|
b5d058902c | ||
|
|
0e4134b172 | ||
|
|
02973067b1 | ||
|
|
b1064cceba | ||
|
|
536fb3678e | ||
|
|
dc3f19e95e | ||
|
|
a575416801 | ||
|
|
e74ef502d4 | ||
|
|
ee7c4a1f09 | ||
|
|
8771291e61 | ||
|
|
769753f25e | ||
|
|
455d51e7cd | ||
|
|
5ef9f42bd7 | ||
|
|
0c8322db13 | ||
|
|
e653a7f82f | ||
|
|
e5e9d90552 | ||
|
|
6a14056804 | ||
|
|
1dd9bd59b5 | ||
|
|
fb34eb9f80 | ||
|
|
ccbfccb76a | ||
|
|
416f3ac4c5 | ||
|
|
df1047c21e | ||
|
|
0a2fe5500c | ||
|
|
698d75184c | ||
|
|
2fa8951f67 | ||
|
|
a480cecf39 | ||
|
|
558408b12b | ||
|
|
95909e7ba4 | ||
|
|
c70218a966 | ||
|
|
de352039ea | ||
|
|
0169f61614 | ||
|
|
da2fd72984 | ||
|
|
1c43c795af | ||
|
|
e7fa3d9639 | ||
|
|
ecffb3eab1 | ||
|
|
e99b16eac6 | ||
|
|
d3478b18f7 | ||
|
|
42a235d916 | ||
|
|
f716316244 | ||
|
|
f059361b4e | ||
|
|
5b4cabdbd0 | ||
|
|
429720ae06 | ||
|
|
4f1f4c948d | ||
|
|
5855d7a933 | ||
|
|
4c85c138b8 | ||
|
|
9b10fab27b | ||
|
|
0933cae8e1 | ||
|
|
d16eed54ea | ||
|
|
83f7f843b0 | ||
|
|
a02264b8c9 | ||
|
|
2eda434a78 | ||
|
|
b8542b9c51 | ||
|
|
c8376464e5 | ||
|
|
d41006e5cb | ||
|
|
b3c13e59a7 | ||
|
|
d54120ba5a | ||
|
|
f1ce53a833 | ||
|
|
2321acfe7d | ||
|
|
c07ff64177 | ||
|
|
1ffc213fdd | ||
|
|
aba339116c | ||
|
|
0e3e65d36f | ||
|
|
d4aa1f3b94 | ||
|
|
3af1542ccd | ||
|
|
bfeb4da529 | ||
|
|
28f7459d9c | ||
|
|
9a59479b5c | ||
|
|
07387351da | ||
|
|
295971ef68 | ||
|
|
967583894f | ||
|
|
170bfc13f6 | ||
|
|
54a81b8e81 | ||
|
|
34343ef692 | ||
|
|
3152ae17dd | ||
|
|
750b07b69a | ||
|
|
118e4aa6a7 | ||
|
|
776105ce48 | ||
|
|
838f5481b6 | ||
|
|
2a66728f40 | ||
|
|
07899d0a43 | ||
|
|
2dd4f016a7 | ||
|
|
fdeca41ec7 | ||
|
|
9d2ddc33df | ||
|
|
c17ab8c934 | ||
|
|
12464f84dd | ||
|
|
b6583dc7ee | ||
|
|
9fadb4339e | ||
|
|
7c3fa0f69d | ||
|
|
c27da25a2b | ||
|
|
f36e456d1d | ||
|
|
0af4c6e0a3 | ||
|
|
4ee4053faf | ||
|
|
6f91a77692 | ||
|
|
937866c60c | ||
|
|
4cc2dbe3b7 | ||
|
|
20f887d07f | ||
|
|
4dbbdb94d5 | ||
|
|
3b01c1d560 | ||
|
|
f86aaaff46 | ||
|
|
778218aaca | ||
|
|
be44762483 | ||
|
|
689aa72e55 | ||
|
|
2d03c785ac | ||
|
|
37913b4edb | ||
|
|
399a50e64d | ||
|
|
50c93493a7 | ||
|
|
9206091cda | ||
|
|
db5e3ec9ec | ||
|
|
3a9d4ecb80 | ||
|
|
57a9634ba6 | ||
|
|
fea24e4cb8 | ||
|
|
e5d06b25f4 | ||
|
|
cd71d9ecbd | ||
|
|
fcc58e54d3 | ||
|
|
8208bab1b5 | ||
|
|
67e42d6e78 | ||
|
|
c5aefaef7f | ||
|
|
3d788285d7 | ||
|
|
dec4f721cf | ||
|
|
f0e91ef0a9 | ||
|
|
c53c9b9d43 | ||
|
|
7c8b37a35d | ||
|
|
17e5157d9a | ||
|
|
027572d666 | ||
|
|
2ed4d99c6f | ||
|
|
8fd3404064 | ||
|
|
fcbc3b3b77 | ||
|
|
7ea3e316ac | ||
|
|
cb20028119 | ||
|
|
9f5a63042d | ||
|
|
0708edeabc | ||
|
|
3190b7a835 | ||
|
|
922315883d | ||
|
|
2eff372377 | ||
|
|
ccf852fc97 | ||
|
|
9139fa4545 | ||
|
|
21eccfaae2 | ||
|
|
9e7e600645 | ||
|
|
4bf0e55d70 | ||
|
|
d3191b3c3e | ||
|
|
f91cdbefca | ||
|
|
3fdefad69b | ||
|
|
f910bf7543 | ||
|
|
cfc29956d0 | ||
|
|
737ed2b14e | ||
|
|
338f712bd4 | ||
|
|
0501496ee4 | ||
|
|
05d0ad77f8 | ||
|
|
6403cb3c71 | ||
|
|
3d7eadab21 | ||
|
|
4759040daf | ||
|
|
377f41e052 | ||
|
|
581e8bc3af | ||
|
|
5428e67451 | ||
|
|
fcad2e707a | ||
|
|
ca66d9f668 | ||
|
|
6e22a01e0f | ||
|
|
d99b5ae2b3 | ||
|
|
146ee1e17f | ||
|
|
567e1dfb04 | ||
|
|
6e047fb386 | ||
|
|
f4db1381da | ||
|
|
0f84bf275f | ||
|
|
01daca1d76 | ||
|
|
c8a13dc671 | ||
|
|
15163009f6 | ||
|
|
b3b3b99a68 | ||
|
|
5d7c592758 | ||
|
|
30d7bc683a | ||
|
|
150505b8bb | ||
|
|
874007b229 | ||
|
|
7e5facdf2a | ||
|
|
4a8422d002 | ||
|
|
4d4211d288 | ||
|
|
6a945eb413 | ||
|
|
7a0122d160 | ||
|
|
c06f0355f2 | ||
|
|
7bd4c7cf68 | ||
|
|
7abdc30cb0 | ||
|
|
6b8b5434f9 | ||
|
|
54031e7ead | ||
|
|
d70e6fead7 | ||
|
|
530c7f31e6 | ||
|
|
82ad264743 | ||
|
|
699696b4cf | ||
|
|
a3355341c1 | ||
|
|
a5e0c1d685 | ||
|
|
cdb4160411 | ||
|
|
ba2b51fddc | ||
|
|
180b8f2407 | ||
|
|
f8eca37f7e | ||
|
|
c9a3c0bda2 | ||
|
|
6787970343 | ||
|
|
7eda7bbf96 | ||
|
|
7c3771e8eb | ||
|
|
35f8b5fbdd | ||
|
|
4a3a893f41 | ||
|
|
9317f7b999 | ||
|
|
30ca987bf9 | ||
|
|
296bb3762b | ||
|
|
0f712e9c42 | ||
|
|
2b96ce0038 | ||
|
|
1e3239e0d2 | ||
|
|
11e5da54e0 | ||
|
|
a0cf89481b | ||
|
|
5512e7b3ec | ||
|
|
6219d07e35 | ||
|
|
2d611ac06c | ||
|
|
abc488b566 | ||
|
|
824077a5b0 | ||
|
|
a0401623fb | ||
|
|
34d577d22a | ||
|
|
4ac97a0221 | ||
|
|
b3d11bb07c | ||
|
|
03c374493c | ||
|
|
9aef213aa4 | ||
|
|
ab53ff6661 | ||
|
|
476e5362bd | ||
|
|
1686c872de | ||
|
|
80564f59ce | ||
|
|
5ed2b79781 | ||
|
|
0504890686 | ||
|
|
9a074e817d | ||
|
|
a86ecda352 | ||
|
|
5873fdff0f | ||
|
|
0ea5f7fb57 | ||
|
|
f2c18e427c | ||
|
|
d859d04abf | ||
|
|
62da2f862e | ||
|
|
4942c9109e | ||
|
|
fedaa47d0e | ||
|
|
35fc0c370b | ||
|
|
b6203f3546 | ||
|
|
9e5c2ffddf | ||
|
|
343aa99fea | ||
|
|
7dba6d8c6a | ||
|
|
4786bcee59 | ||
|
|
191fa7627a | ||
|
|
6606a5f9c1 | ||
|
|
a2871fc8e2 | ||
|
|
97fc95eb44 | ||
|
|
55a118f388 | ||
|
|
6294c4a764 | ||
|
|
8c15ce28bc | ||
|
|
521a0e41fe | ||
|
|
90b5c14bd1 | ||
|
|
4c5b963342 | ||
|
|
c42002d5db | ||
|
|
8e12ad742f | ||
|
|
c82dda321c | ||
|
|
05f6a892e0 | ||
|
|
2bd5369524 | ||
|
|
fdd0a2fa16 | ||
|
|
d413e47d58 | ||
|
|
26949ef6e9 | ||
|
|
1063494070 | ||
|
|
844b994eb8 | ||
|
|
72b9e45366 | ||
|
|
65cf3fad21 | ||
|
|
942e4032a0 | ||
|
|
f840d327f9 | ||
|
|
1da81e10d5 | ||
|
|
d46c92d1e0 | ||
|
|
bf5d1642ed | ||
|
|
e8c1c2a5b1 | ||
|
|
b3d39b5322 | ||
|
|
e7bbc109ba | ||
|
|
4ad514b064 |
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
BUG_REPORT_URL=
|
||||||
|
BUG_REPORT_API_KEY=
|
||||||
3
.github/ISSUE_TEMPLATE/performance-issue.md
vendored
@@ -29,6 +29,9 @@ Clear description of the performance issue.
|
|||||||
- **Storage**: [e.g. SSD/HDD]
|
- **Storage**: [e.g. SSD/HDD]
|
||||||
- **Other Applications Opened**: [e.g. Discord/Opera GX]
|
- **Other Applications Opened**: [e.g. Discord/Opera GX]
|
||||||
|
|
||||||
|
> [!INFO]
|
||||||
|
> At the start of the application, SystemInfo is printed in the logs, so you can just copy and paste it!
|
||||||
|
|
||||||
## Reproduction Details
|
## Reproduction Details
|
||||||
|
|
||||||
- **Download URL**: [if applicable]
|
- **Download URL**: [if applicable]
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -9,8 +9,8 @@ assignees: 'MasterAcnolo'
|
|||||||
|
|
||||||
## Pre-submission Checklist
|
## Pre-submission Checklist
|
||||||
|
|
||||||
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/FreedomLoader/pages/faq.html)
|
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/#/faq)
|
||||||
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/#/wiki)
|
||||||
- [ ] I have searched existing issues
|
- [ ] I have searched existing issues
|
||||||
|
|
||||||
## Question
|
## Question
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Brief description of what this PR does.
|
Brief description of what this PR does.
|
||||||
|
|
||||||
## Related Issue
|
## Related Issue (if applicable)
|
||||||
|
|
||||||
Fixes #(issue number)
|
Fixes #(issue number)
|
||||||
Closes #(issue number)
|
Closes #(issue number)
|
||||||
@@ -30,7 +30,7 @@ Related to #(issue number)
|
|||||||
- [ ] I have tested with different download scenarios (video/audio, playlists)
|
- [ ] I have tested with different download scenarios (video/audio, playlists)
|
||||||
- [ ] I have checked the application logs for errors
|
- [ ] I have checked the application logs for errors
|
||||||
- [ ] I have verified the UI changes (if applicable)
|
- [ ] I have verified the UI changes (if applicable)
|
||||||
- [ ] I have tested on Windows 10 and/or Windows 11
|
- [ ] I have tested on Windows 10 and/or Windows 11 or in any Linux Distribution
|
||||||
|
|
||||||
## Screenshots (if applicable)
|
## Screenshots (if applicable)
|
||||||
|
|
||||||
|
|||||||
2
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
@@ -7,4 +7,4 @@
|
|||||||
## Found a bug or issue?
|
## Found a bug or issue?
|
||||||
- Please report it in the GitHub Issues section
|
- Please report it in the GitHub Issues section
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.4.0...1.4.1
|
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.W.X...1.Y.Z
|
||||||
46
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: "CodeQL PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '00 00 * * 1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze (${{ matrix.language }})
|
||||||
|
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
packages: read
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- language: javascript-typescript
|
||||||
|
build-mode: none
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v4
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
build-mode: ${{ matrix.build-mode }}
|
||||||
|
|
||||||
|
- name: Run manual build steps
|
||||||
|
if: matrix.build-mode == 'manual'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo 'Manual build required'
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v4
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
36
.gitignore
vendored
@@ -1,11 +1,35 @@
|
|||||||
|
# Dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/downloads
|
|
||||||
|
# Build Output Folder
|
||||||
/dist
|
/dist
|
||||||
/out
|
/out
|
||||||
|
/srpm-out
|
||||||
|
|
||||||
/ressources/ffmpeg.exe
|
# .env
|
||||||
/ressources/ffprobe.exe
|
*.env
|
||||||
/ressources/deno.exe
|
|
||||||
|
|
||||||
logs/*.json
|
# Config JSON File. Create it and fill it if you want to have dev only settings
|
||||||
logs/*.log
|
config/config.dev.json
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.zed/
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Linux executable
|
||||||
|
ffmpeg
|
||||||
|
ffprobe
|
||||||
|
deno
|
||||||
|
yt-dlp
|
||||||
|
|
||||||
|
# Windows Executable
|
||||||
|
ffmpeg.exe
|
||||||
|
ffprobe.exe
|
||||||
|
deno.exe
|
||||||
|
yt-dlp.exe
|
||||||
97
BINARIES.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# 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.
|
||||||
|
- **Yt-dlp** binaries don't need to be updated manually. At every startup of the application, the binary tries to update itself
|
||||||
@@ -6,6 +6,7 @@ Welcome! Thanks for your interest in Freedom Loader. Your contributions—bug re
|
|||||||
|
|
||||||
## Quick Links
|
## Quick Links
|
||||||
|
|
||||||
|
- [Development Guide](DEVELOPMENT.md)
|
||||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
- [Issue Templates](.github/ISSUE_TEMPLATE/)
|
- [Issue Templates](.github/ISSUE_TEMPLATE/)
|
||||||
- [Pull Request Templates](.github/PULL_REQUEST_TEMPLATE/)
|
- [Pull Request Templates](.github/PULL_REQUEST_TEMPLATE/)
|
||||||
@@ -23,7 +24,7 @@ Found a bug or unexpected behavior? Open an [**issue**](https://github.com/Maste
|
|||||||
- Clear description of the bug
|
- Clear description of the bug
|
||||||
- Steps to reproduce
|
- Steps to reproduce
|
||||||
- App version and environment (Windows version, Firefox version if relevant)
|
- App version and environment (Windows version, Firefox version if relevant)
|
||||||
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log`
|
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log` (Windows) or `~/.local/share/FreedomLoader/logs/` (Linux)
|
||||||
- Screenshots if applicable
|
- Screenshots if applicable
|
||||||
|
|
||||||
### 2. Request a Feature
|
### 2. Request a Feature
|
||||||
@@ -43,13 +44,11 @@ Have an idea to improve Freedom Loader? Open a [**Feature Request**](https://git
|
|||||||
- Follow the project's code style and conventions
|
- Follow the project's code style and conventions
|
||||||
|
|
||||||
**Process:**
|
**Process:**
|
||||||
1. Fork the repository
|
1. Fork the repository.
|
||||||
2. Create a feature branch: `git checkout -b feature/your-feature-name`
|
2. Check out our [**Developer Guide (DEVELOPMENT.md)**](./DEVELOPMENT.md) to set up your environment.
|
||||||
3. Make your changes
|
3. Create a feature branch following our branching conventions.
|
||||||
4. Test thoroughly on Windows 10/11
|
4. Make your changes and test thoroughly on Windows and/or Linux.
|
||||||
5. Commit with clear messages: `git commit -m "Add feature X"`
|
5. Open a Pull Request using the appropriate template.
|
||||||
6. Push to your fork: `git push origin feature/your-feature-name`
|
|
||||||
7. Open a Pull Request using the appropriate template
|
|
||||||
|
|
||||||
**Code Guidelines:**
|
**Code Guidelines:**
|
||||||
- Use camelCase for variables and functions
|
- Use camelCase for variables and functions
|
||||||
@@ -72,41 +71,9 @@ Small contributions matter—don't hesitate to submit documentation PRs.
|
|||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
### Prerequisites
|
Want to write some code? Awesome!
|
||||||
- Node.js 16.x or higher
|
|
||||||
- npm or yarn
|
|
||||||
- Git
|
|
||||||
- Windows 10/11 (for testing)
|
|
||||||
|
|
||||||
### Setup
|
Please read our [**Developer Guide (DEVELOPMENT.md)**](./DEVELOPMENT.md) for instructions on how to set up your local environment, our project architecture, and our Git branching rules.
|
||||||
```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)
|
|
||||||
└── ressources/ # Binaries (yt-dlp, ffmpeg, etc.)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -114,12 +81,9 @@ Freedom-Loader/
|
|||||||
|
|
||||||
Before submitting a PR, verify:
|
Before submitting a PR, verify:
|
||||||
- [ ] Download functionality works (video/audio)
|
- [ ] Download functionality works (video/audio)
|
||||||
- [ ] Playlist downloads work
|
|
||||||
- [ ] Settings panel functions correctly
|
|
||||||
- [ ] Custom path selection works
|
|
||||||
- [ ] No errors in application logs
|
- [ ] No errors in application logs
|
||||||
- [ ] UI changes work on all themes (if applicable)
|
- [ ] Any features wasn't break during the development
|
||||||
- [ ] Tested on Windows 10 and/or Windows 11
|
- [ ] Tested on Windows 10/11 and/or any Linux Distribution
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -148,8 +112,8 @@ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
|
|||||||
|
|
||||||
## Questions?
|
## Questions?
|
||||||
|
|
||||||
- Check the [FAQ](https://masteracnolo.github.io/FreedomLoader/pages/faq.html)
|
- Check the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/faq)
|
||||||
- Review the [Wiki](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki)
|
||||||
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -161,4 +125,4 @@ Check out his work: [IG @designmark_studio](https://www.instagram.com/designmark
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Thank you for contributing to Freedom Loader!**
|
**Thank you for contributing to Freedom Loader!**
|
||||||
|
|||||||
388
DEVELOPMENT.md
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
# Freedom Loader - Development Guide
|
||||||
|
|
||||||
|
Welcome to the Developer Guide! This document explains how to set up your local environment, project architecture, Git workflow, and the release process.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you begin, ensure you have the following installed on your machine:
|
||||||
|
|
||||||
|
- **Node.js** (v22 or higher recommended)
|
||||||
|
- **npm** (comes with Node.js)
|
||||||
|
- **Git**
|
||||||
|
- **yt-dlp binaries**: Freedom Loader requires native binaries to function. Read [BINARIES.md](./BINARIES.md) for download instructions and placement in `resources/binaries/`.
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/MasterAcnolo/Freedom-Loader.git
|
||||||
|
cd Freedom-Loader
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Install the Binaries
|
||||||
|
|
||||||
|
Follow the instructions in `BINARIES.md` to place `yt-dlp`, `ffmpeg`, `ffprobe`, and `deno` in the correct `resources/binaries/` folder for your OS.
|
||||||
|
|
||||||
|
### 4. Run in Development Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Development mode automatically enables detailed colored logs and the DevTron extension for debugging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
- `npm start` — Start the app in development mode (no warnings)
|
||||||
|
- `npm run start:warn` — Start with deprecation warnings visible
|
||||||
|
- `npm run start:debug` — Start with full debug logging (Wayland, Electron logging, stack dumps)
|
||||||
|
- `npm run dev` — Start with auto-reload on file changes (watches server/, app/, main.js, config.js)
|
||||||
|
- `npm run dev:warn` — Dev mode with warnings
|
||||||
|
- `npm run dev:debug` — Dev mode with full debug output and auto-reload
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
- `npm run build:linux` — Build Linux packages (AppImage, deb, snap)
|
||||||
|
- `npm run build:win` — Build Windows installer (.exe)
|
||||||
|
- `npm run build:rpm` — Build RPM and SRPM for COPR (Fedora)
|
||||||
|
- `npm run build:all` — Build both Windows and Linux (not recommended because it adds both OS binaries into a single package. The output file is 2x bigger than expected).
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- `npm test` — Run all tests (unit + integration)
|
||||||
|
- `npm run test:unit` — Run unit tests only
|
||||||
|
|
||||||
|
### Release Pipeline
|
||||||
|
|
||||||
|
- `npm run release` — Build all packages and create a draft GitHub release (no store publishing)
|
||||||
|
- `npm run release:publish` — Build all packages and publish to Snap Store + COPR
|
||||||
|
- `npm run release:dry-run` — Simulate the full release pipeline without publishing anything
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- `npm update` — Update npm dependencies
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Architecture
|
||||||
|
|
||||||
|
Freedom Loader is built with **Electron** (frontend) and **Node.js + Express** (backend), using a modular structure:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git & Branching Workflow
|
||||||
|
|
||||||
|
I use **Trunk-Based Development** (GitHub Flow):
|
||||||
|
|
||||||
|
### Core Principles
|
||||||
|
|
||||||
|
- **`main` is always deployable**: The `main` branch contains the latest stable code and must never be broken.
|
||||||
|
- **No long-lived branches**: I no longer maintain version branches like `v1.6` or `v1.7`.
|
||||||
|
- **Pull Requests for review**: All code changes go through PR review before merging to `main`.
|
||||||
|
|
||||||
|
### How to Contribute
|
||||||
|
|
||||||
|
#### 1. Create a feature branch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b feat/add-new-button
|
||||||
|
# or for fixes:
|
||||||
|
git checkout -b fix/ui-bug
|
||||||
|
```
|
||||||
|
|
||||||
|
**Branch naming convention**:
|
||||||
|
- `feat/` — New features
|
||||||
|
- `fix/` — Bug fixes
|
||||||
|
- `refactor/` — Code cleanup (no behavior change)
|
||||||
|
- `docs/` — Documentation updates
|
||||||
|
- `chore/` — Dependency updates, build config, etc.
|
||||||
|
|
||||||
|
#### 2. Make commits
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make changes
|
||||||
|
git add .
|
||||||
|
git commit -m "Brief, imperative description of the change"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit message tips**:
|
||||||
|
- Use imperative mood: "Add theme caching" not "Added theme caching"
|
||||||
|
- Keep commits logical and focused (one feature per commit if possible)
|
||||||
|
- Link to issues if relevant: "Fix crash on download (fixes #42)"
|
||||||
|
|
||||||
|
#### 3. Push and open a PR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin feat/add-new-button
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open a PR on GitHub against `main`.
|
||||||
|
|
||||||
|
#### 4. Review and merge
|
||||||
|
|
||||||
|
- Address feedback in new commits (don't rebase — easier to review)
|
||||||
|
- Once approved, merge via GitHub (use "Squash and merge" for clean history if many small commits and you find it relevant)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
**Note**: Only project maintainers release to production. This section documents the process for transparency and for future maintainers.
|
||||||
|
|
||||||
|
Releases are fully automated via the `release.sh` script. The process handles building, packaging, and publishing to **all distribution channels**:
|
||||||
|
- **GitHub Releases** (Windows .exe, Linux packages)
|
||||||
|
- **Fedora COPR** (automatic RPM builds for Fedora 43+)
|
||||||
|
- **Snap Store** (universal Linux)
|
||||||
|
|
||||||
|
### Before You Release
|
||||||
|
|
||||||
|
1. **Ensure `main` is green**: All tests pass, features are stable.
|
||||||
|
2. **Update `package.json` version**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"version": "1.6.1"
|
||||||
|
```
|
||||||
|
3. **Commit the version bump**: `git add package.json && git commit -m "chore: v1.6.1"`
|
||||||
|
4. **Push to `main`**: `git push origin main`
|
||||||
|
|
||||||
|
### Step 1: Local Build & Draft Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
This:
|
||||||
|
1. Builds Linux packages (AppImage, deb, snap)
|
||||||
|
2. Builds RPM and SRPM for COPR
|
||||||
|
3. Builds Windows installer
|
||||||
|
4. Creates a draft GitHub release
|
||||||
|
|
||||||
|
### Step 2: Edit & Publish on GitHub
|
||||||
|
|
||||||
|
1. Go to https://github.com/MasterAcnolo/Freedom-Loader/releases
|
||||||
|
2. Edit the draft release — fill in changelog
|
||||||
|
3. Click **"Publish"** to make it live
|
||||||
|
|
||||||
|
### Step 3: Publish to Distribution Channels
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release:publish
|
||||||
|
```
|
||||||
|
|
||||||
|
This automatically:
|
||||||
|
- **Uploads to Snap Store**: Makes the app available via `snap install freedom-loader`
|
||||||
|
- **Submits to Fedora COPR**: Builds and publishes RPMs for Fedora 43+ (users can `dnf install freedom-loader` from the COPR repo)
|
||||||
|
|
||||||
|
Both happen in parallel — users across all platforms get the release simultaneously.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linux Development Considerations
|
||||||
|
|
||||||
|
### Cross-Distribution Compatibility
|
||||||
|
|
||||||
|
Freedom Loader targets **Debian-based** (Ubuntu, Debian) and **Fedora-based** (Fedora, RHEL, openSUSE) distributions. When developing features, keep this in mind:
|
||||||
|
|
||||||
|
#### What to test
|
||||||
|
|
||||||
|
- **Feature works on Fedora 44+** (primary Linux development environment)
|
||||||
|
- **Feature works on Ubuntu/Debian** (via deb package or AppImage)
|
||||||
|
- **Feature doesn't break Snap confinement** (Snap has restricted filesystem/IPC access)
|
||||||
|
- **Feature gracefully degrades on missing system dependencies**
|
||||||
|
|
||||||
|
#### Desktop Environment (DE) Compatibility
|
||||||
|
|
||||||
|
Test on at least **GNOME** and **KDE** (the most common DEs). Common pain points:
|
||||||
|
|
||||||
|
- **Themes**: May render differently on KDE vs GNOME — test both if possible
|
||||||
|
- **File dialogs**: Some DEs use native file pickers, others fall back to Electron's
|
||||||
|
- **Notifications**: System notification APIs vary (D-Bus, libnotify)
|
||||||
|
- **Tray icons**: May not work identically across DEs
|
||||||
|
|
||||||
|
#### How to check locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test on Fedora (if available)
|
||||||
|
npm run build:linux
|
||||||
|
sudo dnf install dist/freedom-loader-*.x86_64.rpm
|
||||||
|
freedom-loader
|
||||||
|
|
||||||
|
# Test AppImage (works on any distro)
|
||||||
|
chmod +x dist/Freedom\ Loader-*.AppImage
|
||||||
|
./dist/Freedom\ Loader-*.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD Gap
|
||||||
|
|
||||||
|
**Currently**: No automated testing across distros or DEs. Releases rely on manual testing before publish.
|
||||||
|
|
||||||
|
**Future improvement**: Automated tests via GitHub Actions (Ubuntu) + local testing on Fedora would catch cross-distro issues early. This is planned but not yet implemented.
|
||||||
|
|
||||||
|
For now, if you fix a Linux-specific bug or add a DE-dependent feature, **please mention it in your PR description** so reviewers can test extra carefully.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
Test individual functions in isolation (no Electron required).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
Covered:
|
||||||
|
|
||||||
|
(Tests will come soon)
|
||||||
|
|
||||||
|
### End-to-End Tests (Playwright)
|
||||||
|
|
||||||
|
Test the actual Electron app launching and basic UI interactions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Covered:
|
||||||
|
|
||||||
|
(Tests will come soon)
|
||||||
|
|
||||||
|
|
||||||
|
### Running All Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Or include tests before release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test && npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Add a new feature
|
||||||
|
|
||||||
|
1. Create a feature branch: `git checkout -b feat/my-feature`
|
||||||
|
2. Make changes, commit: `git add . && git commit -m "feat: my feature"`
|
||||||
|
3. Push: `git push origin feat/my-feature`
|
||||||
|
4. Open a PR on GitHub
|
||||||
|
5. Once approved, merge to `main`
|
||||||
|
6. (Later) Cut a release when ready: `npm run release`
|
||||||
|
|
||||||
|
### Fix a bug
|
||||||
|
|
||||||
|
Same as above, but use `fix/bug-name` branch and `git commit -m "fix: description"`.
|
||||||
|
|
||||||
|
### Test the app before releasing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # Auto-reload on file changes
|
||||||
|
# Make changes, test in the UI
|
||||||
|
npm test # Run all tests
|
||||||
|
npm run release:dry-run # Simulate release (builds but doesn't publish)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm update
|
||||||
|
git add package.json package-lock.json
|
||||||
|
git commit -m "chore: update dependencies"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild only Linux (after quick fixes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:linux
|
||||||
|
# Or just the RPM:
|
||||||
|
npm run build:rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Publish to stores manually
|
||||||
|
|
||||||
|
If `npm run release:publish` fails partway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Just Snap:
|
||||||
|
snapcraft upload dist/freedom-loader_*.snap --release=stable
|
||||||
|
|
||||||
|
# Just COPR:
|
||||||
|
copr-cli build freedom-loader srpm-out/*.src.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### App won't start in dev mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start:debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the debug output for errors. Most common:
|
||||||
|
- Missing binaries in `resources/binaries/`
|
||||||
|
- Port 8787 already in use
|
||||||
|
- A dead instance is still running — open Task Manager and kill the "Freedom Loader" process
|
||||||
|
|
||||||
|
### (Linux) Binaries in the right place but app still won't launch
|
||||||
|
|
||||||
|
You probably forgot to make them executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x resources/binaries/linux/*
|
||||||
|
```
|
||||||
|
|
||||||
|
The app will fail silently if binaries lack execute permissions.
|
||||||
|
|
||||||
|
### Build fails with "permission denied"
|
||||||
|
|
||||||
|
Make sure you have write access to `dist/` and `srpm-out/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod -R u+w dist srpm-out
|
||||||
|
npm run build:linux
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests fail
|
||||||
|
|
||||||
|
Check for:
|
||||||
|
- Missing test files in `tests/unit/`
|
||||||
|
- Node modules out of sync: `rm -rf node_modules && npm install`
|
||||||
|
|
||||||
|
### GitHub release publish fails
|
||||||
|
|
||||||
|
Verify `gh` CLI is installed and authenticated:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh auth login
|
||||||
|
gh release list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
For more info:
|
||||||
|
- **Electron docs**: https://www.electronjs.org/docs
|
||||||
|
- **electron-builder**: https://www.electron.build/
|
||||||
|
- **This repo**: https://github.com/MasterAcnolo/Freedom-Loader
|
||||||
|
|
||||||
|
Happy coding! 🎉
|
||||||
343
README.md
@@ -1,27 +1,31 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<a href="https://masteracnolo.github.io/FreedomLoader/"><img
|
<a href="https://masteracnolo.github.io/Freedom-Loader-Site/"><img
|
||||||
src="./build/banner.png"
|
src="./build/banner.png"
|
||||||
alt="Banner"
|
alt="Banner"
|
||||||
style="width: 50%;"/></a>
|
style="width: 50%;"/></a>
|
||||||
|
|
||||||
### **A clean, open-source multimedia downloader for Windows**
|
### **A clean, open-source multimedia downloader for Windows and Linux**
|
||||||
|
|
||||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://masteracnolo.github.io/FreedomLoader/)
|
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
||||||
<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>
|
[](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>
|
</div>
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
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.
|
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.
|
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://github.com/MasterAcnolo/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
|
## Table of Contents
|
||||||
|
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
@@ -29,12 +33,12 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Preview](#preview)
|
- [Preview](#preview)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Project Structure](#project-structure)
|
- [Theme Workshop](#Theme-Workshop)
|
||||||
- [Technology Stack](#technology-stack)
|
- [Technology Stack](#technology-stack)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Roadmap](#roadmap)
|
- [Roadmap](#roadmap)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
- [Support](#support)
|
- [Submitting a community theme](#Submitting-a-community-theme)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -53,7 +57,9 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
### User Interface
|
### User Interface
|
||||||
|
|
||||||
- **Custom window controls** - Frameless window with custom top bar (optional)
|
- **Custom window controls** - Frameless window with custom top bar (optional)
|
||||||
- **Theme system** - Multiple pre-built themes with dynamic switching
|
- **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
|
- **Real-time progress tracking** - Live download progress bar with network speed indicator
|
||||||
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
||||||
- **Settings panel** - In-app configuration interface with live updates
|
- **Settings panel** - In-app configuration interface with live updates
|
||||||
@@ -70,38 +76,90 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
- **Rate limiting** - Express-based request throttling to prevent server overload
|
- **Rate limiting** - Express-based request throttling to prevent server overload
|
||||||
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
||||||
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
- **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
|
||||||
|
- **Keep Playlist Order** - Add an Index before the title, could be useful for an album for example.
|
||||||
|
- **System Tray** - Could minimize the application in the system Tray, allow it to run in the background.
|
||||||
|
|
||||||
## Installation
|
## 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
|
### Requirements
|
||||||
|
|
||||||
- **Operating System**: Windows 10 or later
|
- **Windows**: Windows 10 or later
|
||||||
- **Browser**: Mozilla Firefox (required for cookie extraction)
|
- **Linux**: any modern x86_64 distribution
|
||||||
- [Download Firefox](https://www.firefox.com)
|
- **Browser**: Mozilla Firefox ([download](https://www.firefox.com)) required for cookie extraction, on both platforms
|
||||||
|
|
||||||
### Standard Installation
|
### Windows
|
||||||
|
|
||||||
1. Download the latest release from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
1. Download the latest `.exe` from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
2. Run the downloaded `.exe` installer
|
2. Run the installer (click "More info" → "Run anyway" if Windows Defender warns you)
|
||||||
3. If Windows Defender shows a warning, click "More info" then "Run anyway"
|
3. Follow the installation wizard
|
||||||
4. 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
|
### File Locations
|
||||||
|
|
||||||
Downloaded files are stored in:
|
| | Windows | Linux |
|
||||||
```
|
|---------------|---------------------------------------------------------|--------------------------------------|
|
||||||
C:\Users\[USERNAME]\Downloads\Freedom Loader
|
| **Downloads** | `C:\Users\[USERNAME]\Downloads\Freedom Loader` | `~/Downloads/Freedom Loader` |
|
||||||
```
|
| **Logs** | `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\` | `~/.local/share/FreedomLoader/logs/` |
|
||||||
> [!NOTE]
|
|
||||||
> *You can change it whenever you want. Just click the "Edit" button in the UI.*
|
|
||||||
|
|
||||||
Application logs can be found at:
|
> [!NOTE]
|
||||||
```
|
> The download folder can be changed anytime via the "Edit" button in the UI, on both platforms.
|
||||||
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> On Linux, you can start Freedom Loader using `freedom-loader` command
|
||||||
|
|
||||||
### Basic Download
|
### Basic Download
|
||||||
|
|
||||||
1. Launch Freedom Loader
|
1. Launch Freedom Loader
|
||||||
@@ -118,14 +176,15 @@ The application supports various configuration options:
|
|||||||
- Custom download directory selection
|
- Custom download directory selection
|
||||||
- Discord Rich Presence activation
|
- Discord Rich Presence activation
|
||||||
- Theme customization
|
- Theme customization
|
||||||
- Log level configuration
|
- Log level
|
||||||
|
- And somes to see in the list [Configuration Options](#configuration-options) available
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
If you encounter issues:
|
If you encounter issues:
|
||||||
|
|
||||||
1. Restart the application
|
1. Restart the application
|
||||||
2. Check the logs in `AppData\Local\FreedomLoader\logs\`
|
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"
|
3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools"
|
||||||
4. Create an issue on GitHub with logs and reproduction steps
|
4. Create an issue on GitHub with logs and reproduction steps
|
||||||
|
|
||||||
@@ -151,14 +210,22 @@ If you encounter issues:
|
|||||||
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.
|
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
|
### Configuration File Location
|
||||||
|
|
||||||
|
#### Windows
|
||||||
```
|
```
|
||||||
C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
%APPDATA%\Freedom Loader\config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux
|
||||||
|
```
|
||||||
|
~/.config/Freedom\ Loader/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Options
|
### Available Options
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"autoUpdate": true,
|
"autoUpdate": true,
|
||||||
|
"systemTray": true,
|
||||||
"discordRPC": true,
|
"discordRPC": true,
|
||||||
"customTopBar": true,
|
"customTopBar": true,
|
||||||
"autoCheckInfo": true,
|
"autoCheckInfo": true,
|
||||||
@@ -166,31 +233,40 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
|||||||
"addMetadata": true,
|
"addMetadata": true,
|
||||||
"verboseLogs": false,
|
"verboseLogs": false,
|
||||||
"autoDownloadPlaylist": true,
|
"autoDownloadPlaylist": true,
|
||||||
|
"keepPlaylistOrder": false,
|
||||||
|
"createPlaylistFolders": true,
|
||||||
"customCodec": "h264",
|
"customCodec": "h264",
|
||||||
"logSystem": true,
|
"logSystem": true,
|
||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
"downloadSystem": true,
|
"downloadSystem": true,
|
||||||
"notifySystem": true
|
"notifySystem": true,
|
||||||
|
"enableHardwareAcceleration": true,
|
||||||
|
"theme": "dark"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration Options
|
### Configuration Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
|--------|------|---------|-------------|
|
|------------------------------|---------|----------|------------------------------------------------------------|
|
||||||
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
||||||
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
|
||||||
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
||||||
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
||||||
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
|
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
||||||
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
|
||||||
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||||
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||||
| `logSystem` | boolean | `true` | Enable application logging |
|
| `keepPlaylistOrder` | boolean | `false` | Add index before title, usefull for album |
|
||||||
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
| `createPlaylistFolders` | boolean | `false` | Automatically create a folder for a playlist |
|
||||||
| `downloadSystem` | boolean | `true` | Enable download system |
|
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||||
| `notifySystem` | boolean | `true` | Enable system notifications |
|
| `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
|
### Supported Codecs
|
||||||
|
|
||||||
@@ -203,26 +279,7 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
|||||||
- `theora` - Theora (legacy open codec)
|
- `theora` - Theora (legacy open codec)
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Configuration changes may require an application restart to take effect.
|
> Some configuration changes may require an application restart to take effect.
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
Freedom-Loader/
|
|
||||||
├── build/ # Build resources and assets
|
|
||||||
├── config/ # Configuration files
|
|
||||||
├── public/ # Frontend assets (HTML, CSS, JavaScript)
|
|
||||||
├── ressources/ # Internal resources (icons, binaries)
|
|
||||||
├── server/ # Express server code
|
|
||||||
│ ├── routes/ # API route handlers
|
|
||||||
│ ├── services/ # Business logic
|
|
||||||
│ └── utils/ # Server utilities
|
|
||||||
├── .github/ # GitHub configuration and workflows
|
|
||||||
├── main.js # Electron main process
|
|
||||||
├── preload.js # Electron preload script
|
|
||||||
├── config.js # Global application configuration
|
|
||||||
└── package.json # Dependencies and npm scripts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Architecture Overview
|
### Architecture Overview
|
||||||
|
|
||||||
@@ -230,10 +287,25 @@ Freedom Loader uses a client-server architecture within a single Electron applic
|
|||||||
|
|
||||||
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
|
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
|
||||||
- **Backend**: Express.js server running locally for download management
|
- **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
|
- **IPC Bridge**: Secure communication via Electron's preload script
|
||||||
- **Logging**: Winston-based structured logging with file rotation
|
- **Logging**: Winston-based structured logging with file rotation
|
||||||
- **Updates**: Automatic checking and installation via electron-updater
|
- **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
|
## Technology Stack
|
||||||
|
|
||||||
### Core Technologies
|
### Core Technologies
|
||||||
@@ -255,117 +327,69 @@ Freedom Loader uses a client-server architecture within a single Electron applic
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Prerequisites
|
If you are interested by running a local non compiled version of Freedom Loader or just interested by contributing, see [DEVELOPMENT](./DEVELOPMENT.md)
|
||||||
|
|
||||||
- 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
|
### Additional Dependencies
|
||||||
|
|
||||||
This project relies on several external binaries.
|
This project relies on several external binaries.
|
||||||
Some of them are **not included** in the public repository and must be added manually.
|
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 `ressources` folder, using the exact filenames listed below.
|
You must download the required binaries and place them in the `resources` folder, using the exact filenames listed below.
|
||||||
|
|
||||||
#### Required binaries
|
#### Required binaries
|
||||||
|
|
||||||
- **Deno**
|
See [BINARIES.md](./BINARIES.md) for how to setup the binaries.
|
||||||
- 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:
|
|
||||||
|
|
||||||
```
|
|
||||||
ressources/
|
|
||||||
├── 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
|
### Development Guidelines
|
||||||
|
|
||||||
- Follow existing code style and conventions
|
- Follow existing code style and conventions
|
||||||
- Write clear commit messages
|
- Write clear, imperative commit messages (e.g., "Add playlist indexing" not "Added")
|
||||||
- Test thoroughly before submitting changes
|
- Test thoroughly on both Windows and Linux before submitting changes
|
||||||
- Update documentation when adding features
|
- Update documentation (README, DEVELOPMENT.md) when adding features
|
||||||
- Maintain compatibility with Windows 10+
|
- Maintain compatibility with:
|
||||||
|
- Windows 10+
|
||||||
|
- Fedora 43+ / Debian 11+ / Ubuntu 20.04+
|
||||||
|
- GNOME and KDE desktop environments (when possible)
|
||||||
|
- Consider Linux distribution differences early (see [DEVELOPMENT.md](./DEVELOPMENT.md#linux-development-considerations))
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### Completed
|
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
|
||||||
|
|
||||||
- [x] ~~Website and documentation~~
|
|
||||||
- [x] ~~Automatic YT-DLP updates~~
|
|
||||||
- [x] ~~Automatic application updates~~
|
|
||||||
- [x] ~~Enhanced download status display~~
|
|
||||||
- [x] ~~Video chapter extraction~~
|
|
||||||
- [x] ~~Custom output path selection~~
|
|
||||||
- [x] ~~Custom codec selection~~
|
|
||||||
- [x] ~~Settings Panel with toggle switch for features.~~
|
|
||||||
|
|
||||||
### In Progress
|
|
||||||
|
|
||||||
- [ ] Additional format support (WebM, OGG, etc.)
|
|
||||||
- [ ] Refactoring and cleaning the code base.
|
|
||||||
- [ ] Chrome/Edge cookie support
|
|
||||||
- [ ] Improved multi-site support
|
|
||||||
|
|
||||||
### Planned
|
|
||||||
|
|
||||||
- [ ] Subtitle download and embedding
|
|
||||||
- [ ] Multiple language support
|
|
||||||
- [ ] Partial video downloads
|
|
||||||
- [ ] Custom file naming patterns
|
|
||||||
- [ ] Parallel downloads
|
|
||||||
- [ ] Automatic sponsor detection (SponsorBlock integration)
|
|
||||||
- [ ] Download history
|
|
||||||
- [ ] Linux version
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome and appreciated. This project benefits from community involvement.
|
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
|
### Bug Reports
|
||||||
|
|
||||||
Use the GitHub Issues system and include:
|
You can report bugs either using the built-in application system or via the GitHub Issues system.
|
||||||
|
|
||||||
|
#### Using the Built-in Report Tool
|
||||||
|
|
||||||
|
1. Open Freedom Loader.
|
||||||
|
2. Trigger the bug report interface.
|
||||||
|
3. Fill in the title and description of the issue.
|
||||||
|
4. Choose whether to include today's logs automatically.
|
||||||
|
5. Submit the report.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you need to attach extra context like screenshots or screen recordings, it is recommended to use the GitHub Issues
|
||||||
|
system instead.
|
||||||
|
|
||||||
|
#### Using GitHub Issues
|
||||||
|
|
||||||
|
Open a new issue on the GitHub repository and include:
|
||||||
|
|
||||||
- Clear description of the issue
|
- Clear description of the issue
|
||||||
- Steps to reproduce
|
- Steps to reproduce
|
||||||
- Expected vs actual behavior
|
- Expected vs actual behavior
|
||||||
- Relevant logs from `AppData\Local\FreedomLoader\logs\`
|
- Relevant logs from `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs/`
|
||||||
- Screenshots if applicable
|
- Screenshots or recordings if applicable
|
||||||
|
|
||||||
### Feature Requests
|
### Feature Requests
|
||||||
|
|
||||||
@@ -384,15 +408,7 @@ Open a feature request issue with:
|
|||||||
5. Update documentation as needed
|
5. Update documentation as needed
|
||||||
6. Submit a pull request with a detailed description
|
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.
|
Please read [CONTRIBUTING.md](CONTRIBUTING.md), [DEVELOPMENT.md](./DEVELOPMENT.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
|
## License
|
||||||
|
|
||||||
@@ -404,11 +420,12 @@ You are free to use, modify, and redistribute this software under the terms of t
|
|||||||
|
|
||||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
||||||
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
||||||
- [Deno](https://deno.com/) for helping me bypassing challenges
|
- [Deno](https://deno.com/) for helping me to bypass challenges
|
||||||
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
||||||
- The open-source community for continuous support and contributions
|
- The open-source community for continuous support and contributions
|
||||||
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
||||||
- Jacques Chirac to love Apples
|
- [@AureCX](https://github.com/AureCX) for being the Official Windows Tester!
|
||||||
|
- Zakaria for the website icon
|
||||||
- All users who test, report issues, and help improve the application
|
- All users who test, report issues, and help improve the application
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -417,6 +434,6 @@ You are free to use, modify, and redistribute this software under the terms of t
|
|||||||
|
|
||||||
**Freedom Loader** - Put freedom in your downloads
|
**Freedom Loader** - Put freedom in your downloads
|
||||||
|
|
||||||
[Website](https://masteracnolo.github.io/FreedomLoader/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
|
[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)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
73
SECURITY.md
@@ -2,15 +2,14 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
We actively maintain and provide security updates for the following versions of Freedom Loader:
|
Freedom Loader follows a **latest-only** support model. Security fixes are applied to the current release only — no backports to older versions.
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------------- | ------------------ |
|
||||||
| 1.4.x | :white_check_mark: |
|
| Latest stable | :white_check_mark: |
|
||||||
| 1.3.x | :white_check_mark: |
|
| Older versions | :x: |
|
||||||
| < 1.3.0 | :x: |
|
|
||||||
|
|
||||||
**Note**: We recommend always using the latest version to benefit from the most recent security patches and features.
|
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
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
@@ -32,37 +31,40 @@ Instead, please report security issues privately by:
|
|||||||
|
|
||||||
### What to Expect
|
### What to Expect
|
||||||
|
|
||||||
- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours
|
- **Acknowledgment**: I will acknowledge receipt of your report within 48 hours
|
||||||
- **Assessment**: We will assess the vulnerability and determine its severity
|
- **Assessment**: I will assess the vulnerability and determine its severity
|
||||||
- **Updates**: We will keep you informed of our progress
|
- **Updates**: I will keep you informed of our progress
|
||||||
- **Resolution**: We aim to release a fix within 7-14 days for critical vulnerabilities
|
- **Resolution**: I aim to release a fix within 7-14 days for critical vulnerabilities
|
||||||
- **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous)
|
- **Credit**: I will credit you in the release notes (unless you prefer to remain anonymous)
|
||||||
|
|
||||||
## Security Best Practices for Users
|
## Security Best Practices for Users
|
||||||
|
|
||||||
When using Freedom Loader, we recommend:
|
When using Freedom Loader, I recommend:
|
||||||
|
|
||||||
### General Security
|
### General Security
|
||||||
|
|
||||||
- **Keep Updated**: Always use the latest version of Freedom Loader
|
- **Keep Updated**: Always use the latest version of Freedom Loader — older versions receive no security patches
|
||||||
- **Official Sources**: Download only from official releases on [GitHub](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
- **Official Sources**: Download only from official sources:
|
||||||
- **Verify Downloads**: Check that installers are properly signed (Windows SmartScreen may show warnings for new releases)
|
- [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
|
- **Antivirus**: Keep your antivirus software up to date
|
||||||
|
|
||||||
### Configuration Security
|
### Configuration Security
|
||||||
|
|
||||||
- **Download Path**: Only set download paths within your user directory (`C:\Users\[USERNAME]\...`)
|
- **Download Path**: Only set download paths within your user directory
|
||||||
- **Cookies**: Be aware that Firefox cookies are used for authentication—keep Firefox secure
|
- **Cookies**: Be aware that Firefox cookies are used for authentication — keep Firefox secure
|
||||||
- **Logs**: Logs may contain sensitive information—avoid sharing them publicly without review
|
- **Logs**: Logs may contain sensitive information such as URLs or local paths — avoid sharing them publicly without review
|
||||||
|
|
||||||
### Privacy Considerations
|
### Privacy Considerations
|
||||||
|
|
||||||
Freedom Loader respects your privacy:
|
Freedom Loader respects your privacy:
|
||||||
|
|
||||||
- **No Data Collection**: We don't collect, store, or transmit your personal data
|
- **No Data Collection**: I don't collect, store, or transmit your personal data
|
||||||
- **No Telemetry**: No usage tracking or analytics
|
- **No Telemetry**: No usage tracking or analytics
|
||||||
- **Local Operation**: All downloads are processed locally on your machine
|
- **Local Operation**: All downloads are processed locally on your machine
|
||||||
- **Optional Features**: Discord RPC is optional and can be disabled
|
- **Optional Features**: Discord RPC is optional and can be disabled in settings
|
||||||
|
|
||||||
## Known Security Considerations
|
## Known Security Considerations
|
||||||
|
|
||||||
@@ -71,16 +73,16 @@ Freedom Loader respects your privacy:
|
|||||||
Freedom Loader accesses Firefox cookies to download protected content. This is:
|
Freedom Loader accesses Firefox cookies to download protected content. This is:
|
||||||
|
|
||||||
- **By Design**: Required for age-restricted or member-only content
|
- **By Design**: Required for age-restricted or member-only content
|
||||||
- **Local Only**: Cookies are read locally and never transmitted
|
- **Local Only**: Cookies are read locally and never transmitted outside your machine
|
||||||
- **User Controlled**: You can control what content you download
|
- **User Controlled**: You control what content you download
|
||||||
|
|
||||||
### Native Dependencies
|
### Native Dependencies
|
||||||
|
|
||||||
Freedom Loader bundles native binaries:
|
Freedom Loader bundles the following official binaries:
|
||||||
|
|
||||||
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
- **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/)
|
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
|
||||||
- **Deno**: Official builds from [Deno.land](https://deno.land/)
|
- **Deno**: Official builds from [Deno.com](https://deno.com/)
|
||||||
|
|
||||||
These dependencies are verified and updated regularly.
|
These dependencies are verified and updated regularly.
|
||||||
|
|
||||||
@@ -91,13 +93,13 @@ New releases may trigger Windows Defender warnings because:
|
|||||||
- The application is not yet widely distributed
|
- The application is not yet widely distributed
|
||||||
- Code signing certificates are expensive for open-source projects
|
- Code signing certificates are expensive for open-source projects
|
||||||
|
|
||||||
This is expected behavior for new releases. The warning will decrease as more users download the software.
|
This is expected behavior for new releases and will diminish as more users download the software.
|
||||||
|
|
||||||
## Vulnerability Disclosure Policy
|
## Vulnerability Disclosure Policy
|
||||||
|
|
||||||
### Our Commitment
|
### Our Commitment
|
||||||
|
|
||||||
We are committed to:
|
I am committed to:
|
||||||
|
|
||||||
- Responding promptly to security reports
|
- Responding promptly to security reports
|
||||||
- Working with security researchers to verify and address issues
|
- Working with security researchers to verify and address issues
|
||||||
@@ -107,9 +109,9 @@ We are committed to:
|
|||||||
### Disclosure Timeline
|
### Disclosure Timeline
|
||||||
|
|
||||||
1. **Day 0**: Vulnerability reported privately
|
1. **Day 0**: Vulnerability reported privately
|
||||||
2. **Day 1-2**: Acknowledgment sent to reporter
|
2. **Day 1–2**: Acknowledgment sent to reporter
|
||||||
3. **Day 3-7**: Vulnerability assessed and fix developed
|
3. **Day 3–7**: Vulnerability assessed and fix developed
|
||||||
4. **Day 7-14**: Fix released (critical vulnerabilities prioritized)
|
4. **Day 7–14**: Fix released (critical vulnerabilities prioritized)
|
||||||
5. **Day 14+**: Public disclosure (coordinated with reporter)
|
5. **Day 14+**: Public disclosure (coordinated with reporter)
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
@@ -129,16 +131,16 @@ The following are generally not considered security vulnerabilities:
|
|||||||
- Issues requiring physical access to the user's machine
|
- Issues requiring physical access to the user's machine
|
||||||
- Social engineering attacks
|
- Social engineering attacks
|
||||||
- Denial of service against third-party services
|
- Denial of service against third-party services
|
||||||
- Issues in third-party dependencies (report to upstream projects)
|
- Issues in third-party dependencies (report to upstream projects directly)
|
||||||
- Missing security headers on local HTTP server (no remote access)
|
- Missing security headers on the local HTTP server (not exposed remotely)
|
||||||
|
|
||||||
## Security Updates
|
## Security Updates
|
||||||
|
|
||||||
Security updates are released as:
|
Security updates are released as:
|
||||||
|
|
||||||
- **Patch Releases**: For critical security fixes (e.g., 1.4.1 → 1.4.2)
|
- **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
|
- **Automatic Updates**: Users are notified via the built-in update system
|
||||||
- **Release Notes**: Security fixes are clearly marked in changelog
|
- **Release Notes**: Security fixes are clearly marked in the changelog
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
@@ -150,6 +152,7 @@ Security updates are released as:
|
|||||||
|
|
||||||
For security-related questions or concerns:
|
For security-related questions or concerns:
|
||||||
|
|
||||||
|
- **Email**: masteracnolo25@gmail.com
|
||||||
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
|
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
162
app/autoUpdater.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* 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 { 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;
|
||||||
|
|
||||||
|
let updateAvailable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines how the update should be handled based on the platform
|
||||||
|
* @returns {"auto" | "snap" | "flatpak" | "linux-native"}
|
||||||
|
*/
|
||||||
|
function getUpdateEnvironment() {
|
||||||
|
if (
|
||||||
|
process.env.APPIMAGE ||
|
||||||
|
process.platform === "win32" ||
|
||||||
|
process.platform === "darwin"
|
||||||
|
) {
|
||||||
|
return "auto";
|
||||||
|
}
|
||||||
|
if (process.env.SNAP) return "snap";
|
||||||
|
if (process.env.FLATPAK_ID) return "flatpak";
|
||||||
|
|
||||||
|
return "linux-native";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}`);
|
||||||
|
|
||||||
|
mainWindow?.webContents.send("update-available", {
|
||||||
|
version: info.version,
|
||||||
|
environment: getUpdateEnvironment(),
|
||||||
|
});
|
||||||
|
updateAvailable = true;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: progress.percent,
|
||||||
|
bytesPerSecond: progress.bytesPerSecond,
|
||||||
|
transferred: progress.transferred,
|
||||||
|
total: progress.total,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}`);
|
||||||
|
|
||||||
|
mainWindow?.webContents.send("update-downloaded", info);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkForUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually triggers update check on startup.
|
||||||
|
* Separated from init for reusability and testability.
|
||||||
|
*/
|
||||||
|
async function checkForUpdates() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUpdateAvailable() {
|
||||||
|
return updateAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initAutoUpdater,
|
||||||
|
downloadUpdate,
|
||||||
|
installUpdate,
|
||||||
|
isUpdateAvailable,
|
||||||
|
};
|
||||||
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 };
|
||||||
136
app/discordRPC.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
const config = require('../config');
|
||||||
|
const RPC = require("discord-rpc");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automagically fixes Discord RPC on Linux for Sandboxed versions (Flatpak & Snap).
|
||||||
|
* It creates a symlink from the sandbox IPC socket to the standard system location.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function fixLinuxDiscordRPC() {
|
||||||
|
// Only execute on Linux
|
||||||
|
if (process.platform !== "linux") return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user's runtime directory (e.g., /run/user/1000).
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const xdgDir = process.env.XDG_RUNTIME_DIR || `/run/user/${process.getuid()}`;
|
||||||
|
const standardSocket = path.join(xdgDir, "discord-ipc-0");
|
||||||
|
|
||||||
|
// If the standard socket already exists (Native Discord), do nothing.
|
||||||
|
if (fs.existsSync(standardSocket)) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paths for known sandboxed Discord IPC sockets.
|
||||||
|
*/
|
||||||
|
const flatpakSocket = path.join(xdgDir, "app/com.discordapp.Discord/discord-ipc-0");
|
||||||
|
const snapSocket = path.join(xdgDir, "snap.discord/discord-ipc-0");
|
||||||
|
|
||||||
|
let targetSocket = null;
|
||||||
|
|
||||||
|
if (fs.existsSync(flatpakSocket)) {
|
||||||
|
targetSocket = flatpakSocket;
|
||||||
|
} else if (fs.existsSync(snapSocket)) {
|
||||||
|
targetSocket = snapSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a sandboxed socket is found, create the symlink stealthily.
|
||||||
|
if (targetSocket) {
|
||||||
|
try {
|
||||||
|
fs.symlinkSync(targetSocket, standardSocket);
|
||||||
|
logger.info(`Discord RPC bridge created automatically targeting: ${targetSocket}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Failed to create Discord RPC bridge: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch that create Simlinks for Linux environment;
|
||||||
|
*/
|
||||||
|
fixLinuxDiscordRPC();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(() => {
|
||||||
|
// Since 1.6.2, it no longer print the error. This is because if discord is not opened, it will send an error.
|
||||||
|
// logger.error("Unable to connect to the RPC:", err);
|
||||||
|
logger.info("Unable to connect to the Discord RPC. Discord is maybe not launched");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully stops Discord Rich Presence connection.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Clear any running intervals
|
||||||
|
* - Remove current activity from Discord if connected
|
||||||
|
* - Destroy RPC transport connection
|
||||||
|
* - Handle cleanup errors safely
|
||||||
|
*/
|
||||||
|
async function stopRPC() {
|
||||||
|
if (rpc) {
|
||||||
|
try {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
|
||||||
|
await rpc.destroy();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
// Since 1.6.2, it no longer print the error. This is because if discord is not opened, it will send an error.
|
||||||
|
// logger.error("Error while closing the RPC:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { startRPC, stopRPC};
|
||||||
290
app/ipcHandlers.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* 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, devMode } = require("../config");
|
||||||
|
const { getThemes, reloadThemes } = require("./themeManager");
|
||||||
|
const config = require("../config");
|
||||||
|
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
|
||||||
|
const { userThemesPath } = require("../server/helpers/path.helpers");
|
||||||
|
const { createSystemTray, destroyTray } = require("./tray");
|
||||||
|
const { sendReport } = require("./sendReport");
|
||||||
|
const { isUpdateAvailable, downloadUpdate, installUpdate } = require("./autoUpdater");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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",
|
||||||
|
"systemTray",
|
||||||
|
"discordRPC",
|
||||||
|
"customTopBar",
|
||||||
|
"autoCheckInfo",
|
||||||
|
"addThumbnail",
|
||||||
|
"addMetadata",
|
||||||
|
"verboseLogs",
|
||||||
|
"autoDownloadPlaylist",
|
||||||
|
"keepPlaylistOrder",
|
||||||
|
"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", () => {
|
||||||
|
// Minimize to tray
|
||||||
|
if (configFeatures.systemTray) {
|
||||||
|
getMainWindow()?.hide();
|
||||||
|
logger.info("Window Minimized in SystemTray (Using Minimize Button)");
|
||||||
|
} else {
|
||||||
|
// Native minimize
|
||||||
|
getMainWindow()?.minimize();
|
||||||
|
logger.info("Window minimized normally");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Front end logger
|
||||||
|
*/
|
||||||
|
ipcMain.on("log-error", (_, message) => logger.error(`[Frontend] ${message}`));
|
||||||
|
ipcMain.on("log-info", (_, message) => logger.info(`[Frontend] ${message}`));
|
||||||
|
ipcMain.on("log-warn", (_, message) => logger.warn(`[Frontend] ${message}`));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send bug report
|
||||||
|
*/
|
||||||
|
ipcMain.handle("send-report", async (_, params) => {
|
||||||
|
return await sendReport(params);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("is-update-available", () => {
|
||||||
|
return isUpdateAvailable();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("download-update", async () => {
|
||||||
|
return await downloadUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("install-update", async () => {
|
||||||
|
return installUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on("open-release-page", () => {
|
||||||
|
shell.openExternal(
|
||||||
|
"https://github.com/MasterAcnolo/Freedom-Loader/releases/latest",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically intercepts and applies System Tray state changes at runtime.
|
||||||
|
* Instantiates or destroys the tray icon immediately to prevent window
|
||||||
|
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
|
||||||
|
*/
|
||||||
|
if (key === "systemTray") {
|
||||||
|
if (value === true) {
|
||||||
|
logger.info("System Tray enabled dynamically.");
|
||||||
|
createSystemTray(devMode);
|
||||||
|
} else {
|
||||||
|
logger.info("System Tray disabled dynamically.");
|
||||||
|
destroyTray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
85
app/pathValidator.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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 {
|
||||||
|
const absolutePath = path.resolve(userPath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(absolutePath)) {
|
||||||
|
fs.mkdirSync(absolutePath, { recursive: true });
|
||||||
|
logger.info(`Download folder created: ${absolutePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const real = fs.realpathSync(absolutePath);
|
||||||
|
|
||||||
|
if (!isSafePath(real)) {
|
||||||
|
logger.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 };
|
||||||
54
app/sendReport.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const {logger, logDir} = require("../server/logger");
|
||||||
|
|
||||||
|
async function sendReport(params) {
|
||||||
|
try {
|
||||||
|
const {title, description, includeLogs} = params;
|
||||||
|
|
||||||
|
if (!title || !description) {
|
||||||
|
throw new Error("Title and description are required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("title", title);
|
||||||
|
formData.append("context", description);
|
||||||
|
|
||||||
|
if (includeLogs === "yes" && logDir) {
|
||||||
|
// YYYY-MM-DD
|
||||||
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
|
const logFilePath = path.join(logDir, `LOGS-${today}.log`);
|
||||||
|
|
||||||
|
if (fs.existsSync(logFilePath)) {
|
||||||
|
const logFileContent = fs.readFileSync(logFilePath);
|
||||||
|
const blob = new Blob([logFileContent], {type: "text/plain"});
|
||||||
|
formData.append("file", blob, `LOGS-${today}.log`);
|
||||||
|
logger.info("Logs attached to bug report.");
|
||||||
|
} else {
|
||||||
|
logger.warn(`Log file not found for today: ${logFilePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(process.env.BUG_REPORT_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-Api-Key": process.env.BUG_REPORT_API_KEY
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logger.error(`Server returned an error: ${response.status}`);
|
||||||
|
throw new Error(`Server responded with status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Bug report successfully sent.");
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Failed to send bug report: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {sendReport};
|
||||||
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 };
|
||||||
117
app/tray.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
const {app, Menu, Tray, nativeImage} = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
const {logger} = require("../server/logger"); // Ajuste le chemin si besoin
|
||||||
|
const fs = require("fs");
|
||||||
|
const {getMainWindow} = require("./windowManager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global reference to the Tray instance to prevent garbage collection.
|
||||||
|
* @type {Tray | null}
|
||||||
|
*/
|
||||||
|
let tray = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and configures the cross-platform System Tray.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Loads the appropriate icon format based on the operating system.
|
||||||
|
* - Builds the right-click context menu.
|
||||||
|
* - Handles left-click behavior to toggle main window visibility.
|
||||||
|
*
|
||||||
|
* @param {boolean} devMode - Indicates whether the application is running in development mode.
|
||||||
|
* @returns {Tray} The created Tray instance.
|
||||||
|
*/
|
||||||
|
function createSystemTray(devMode) {
|
||||||
|
// Prevent creating multiple tray instances
|
||||||
|
if (tray) return tray;
|
||||||
|
|
||||||
|
const isWin = process.platform === "win32";
|
||||||
|
const iconName = isWin ? "app-icon.ico" : "app-icon-64x64.png";
|
||||||
|
|
||||||
|
// Attempt to load the icon from the local build directory first
|
||||||
|
let iconPath = path.join(__dirname, "..", "build", iconName);
|
||||||
|
|
||||||
|
// Fallback to the extracted resources path if running in production
|
||||||
|
if (!devMode && !fs.existsSync(iconPath)) {
|
||||||
|
iconPath = path.join(process.resourcesPath, iconName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(iconPath)) {
|
||||||
|
logger.error(`System Tray icon not found at resolved path: ${iconPath}`);
|
||||||
|
} else {
|
||||||
|
logger.info("System Tray icon resolved successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = nativeImage.createFromPath(iconPath);
|
||||||
|
|
||||||
|
if (icon.isEmpty()) {
|
||||||
|
logger.error("System Tray icon file was found but could not be decoded by Electron (image is empty).");
|
||||||
|
}
|
||||||
|
|
||||||
|
tray = new Tray(icon);
|
||||||
|
tray.setToolTip("Freedom Loader");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System Tray Context Menu (Right-click action).
|
||||||
|
*/
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: "Show Freedom Loader",
|
||||||
|
click: () => {
|
||||||
|
const window = getMainWindow();
|
||||||
|
if (window && !window.isDestroyed()) {
|
||||||
|
window.show();
|
||||||
|
window.focus();
|
||||||
|
logger.info("Window Restored from SystemTray (Using Show Action)");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Quit",
|
||||||
|
click: () => {
|
||||||
|
/**
|
||||||
|
* Flags the application for a deliberate exit,
|
||||||
|
* bypassing the default "minimize to tray" behavior.
|
||||||
|
*/
|
||||||
|
app.isQuitting = true;
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
tray.setContextMenu(contextMenu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Left-click behavior: Toggles main window visibility.
|
||||||
|
* but it remains standard practice for Windows and Linux environments.
|
||||||
|
*/
|
||||||
|
tray.on("click", () => {
|
||||||
|
const window = getMainWindow();
|
||||||
|
if (!window || window.isDestroyed()) return;
|
||||||
|
|
||||||
|
if (window.isVisible()) {
|
||||||
|
window.hide();
|
||||||
|
} else {
|
||||||
|
window.show();
|
||||||
|
window.focus();
|
||||||
|
logger.info("Window Restored from SystemTray (Double Click on Tray Icon)");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely destroys the tray icon to prevent OS-level crashes (especially on Linux/Wayland).
|
||||||
|
* Should be called right before the application quits.
|
||||||
|
*/
|
||||||
|
function destroyTray() {
|
||||||
|
if (tray && !tray.isDestroyed()) {
|
||||||
|
tray.destroy();
|
||||||
|
tray = null;
|
||||||
|
logger.info("System Tray destroyed cleanly.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {createSystemTray, destroyTray};
|
||||||
123
app/windowManager.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercepts the window close event.
|
||||||
|
* If the app is not explicitly quitting (via Tray menu),
|
||||||
|
* cancels the destruction and hides the window instead.
|
||||||
|
*/
|
||||||
|
mainWindow.on('close', (event) => {
|
||||||
|
|
||||||
|
if (!app.isQuitting && config.configFeatures.systemTray) {
|
||||||
|
event.preventDefault();
|
||||||
|
mainWindow.hide();
|
||||||
|
logger.info("Window Minimized in SystemTray (Using Close Button)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: 134 KiB |
BIN
build/app-icon-64x64.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
build/app-icon.png
Executable file
|
After Width: | Height: | Size: 107 KiB |
133
config.js
@@ -3,24 +3,129 @@ const { app } = require("electron");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const localMode = !app.isPackaged;
|
/**
|
||||||
|
* Indicates whether the application is running in development mode.
|
||||||
|
* Determined by Electron's packaging state.
|
||||||
|
*/
|
||||||
|
const devMode = !app.isPackaged || process.env.NODE_ENV === "test";
|
||||||
|
|
||||||
const featuresPath = localMode ? path.join(__dirname, "./config/config.json") : path.join(path.join(process.resourcesPath,"config/" ,"config.json"));
|
/**
|
||||||
|
* 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");
|
||||||
|
|
||||||
let features = {};
|
if (!fs.existsSync(devConfigPath)) {
|
||||||
|
const defaultConfigPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"config",
|
||||||
|
"config.default.json"
|
||||||
|
);
|
||||||
|
fs.copyFileSync(defaultConfigPath, devConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
function loadFeatures() {
|
return devConfigPath;
|
||||||
const raw = fs.readFileSync(featuresPath, "utf-8");
|
}
|
||||||
features = JSON.parse(raw);
|
|
||||||
return features;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const configFeatures = loadFeatures();
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads feature flags from disk.
|
||||||
|
* Use this instead of the cached configFeatures snapshot
|
||||||
|
* when you need the latest user settings at call time.
|
||||||
|
*
|
||||||
|
* @returns {Object} Fresh feature flags from config file
|
||||||
|
*/
|
||||||
|
function reloadFeatures() {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback to cached snapshot if file read fails
|
||||||
|
return configFeatures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory snapshot of application feature flags.
|
||||||
|
*
|
||||||
|
* Note: Changes to the config file are not automatically reflected.
|
||||||
|
* A restart or reload is required.
|
||||||
|
*/
|
||||||
|
let configFeatures = loadFeatures();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
version: packageJson.version,
|
/**
|
||||||
applicationPort: "8787",
|
* Application version from package.json
|
||||||
localMode,
|
*/
|
||||||
DiscordRPCID: "1410934537051181146",
|
version: devMode ? `dev-${packageJson.version}` : `v${packageJson.version}` ,
|
||||||
configFeatures
|
|
||||||
}
|
/**
|
||||||
|
* 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,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to reload the config
|
||||||
|
*/
|
||||||
|
reloadFeatures,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the active configuration file
|
||||||
|
*/
|
||||||
|
featuresPath,
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"autoUpdate": true,
|
"autoUpdate": true,
|
||||||
|
"systemTray": false,
|
||||||
"discordRPC": true,
|
"discordRPC": true,
|
||||||
"customTopBar": true,
|
"customTopBar": true,
|
||||||
"autoCheckInfo": true,
|
"autoCheckInfo": true,
|
||||||
@@ -7,9 +8,13 @@
|
|||||||
"addMetadata": true,
|
"addMetadata": true,
|
||||||
"verboseLogs": false,
|
"verboseLogs": false,
|
||||||
"autoDownloadPlaylist": true,
|
"autoDownloadPlaylist": true,
|
||||||
|
"keepPlaylistOrder": false,
|
||||||
|
"createPlaylistFolders": false,
|
||||||
"customCodec": "h264",
|
"customCodec": "h264",
|
||||||
"logSystem": true,
|
"logSystem": true,
|
||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
"downloadSystem": true,
|
"downloadSystem": true,
|
||||||
"notifySystem": true
|
"notifySystem": true,
|
||||||
|
"enableHardwareAcceleration": true,
|
||||||
|
"theme": "dark"
|
||||||
}
|
}
|
||||||
40
eslint.config.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const js = require("@eslint/js");
|
||||||
|
const globals = require("globals");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
js.configs.recommended,
|
||||||
|
|
||||||
|
{ignores: ["node_modules/**", "srpm-out/**", "dist/**", "test/**"]},
|
||||||
|
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "commonjs",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": ["error", {argsIgnorePattern: "^_"}],
|
||||||
|
"no-console": "off",
|
||||||
|
"semi": ["error", "always"]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["app/**", "server/**", "main.js"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {...globals.node}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["public/**"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {...globals.browser}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["preload.js"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {...globals.node, ...globals.browser}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
21
jest.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
testEnvironment: "node",
|
||||||
|
testMatch: ["**/test/unit/**/*.test.js"],
|
||||||
|
collectCoverageFrom: [
|
||||||
|
"server/**/*.js",
|
||||||
|
"app/**/*.js",
|
||||||
|
"!app/autoUpdater.js",
|
||||||
|
],
|
||||||
|
|
||||||
|
modulePathIgnorePatterns: [
|
||||||
|
"<rootDir>/dist",
|
||||||
|
"<rootDir>/release",
|
||||||
|
"<rootDir>/build",
|
||||||
|
],
|
||||||
|
|
||||||
|
watchPathIgnorePatterns: [
|
||||||
|
"<rootDir>/dist",
|
||||||
|
"<rootDir>/release",
|
||||||
|
"<rootDir>/build",
|
||||||
|
],
|
||||||
|
};
|
||||||
409
main.js
@@ -1,270 +1,199 @@
|
|||||||
const config = require("./config.js");
|
/**
|
||||||
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
* Handle crash application and avoid ghost instance
|
||||||
const path = require("path");
|
*/
|
||||||
const os = require("os");
|
const { app } = require("electron");
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
|
process.on("uncaughtException", (err) => {
|
||||||
|
console.error("Uncaught exception:", err);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
console.error("Unhandled rejection:", reason);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if this is the primary instance (lock acquired successfully)
|
||||||
|
*/
|
||||||
|
const isPrimaryInstance = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we are on a second instance
|
||||||
|
*/
|
||||||
|
if (!isPrimaryInstance) {
|
||||||
|
app.quit();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set App Name and AppID
|
||||||
|
*/
|
||||||
|
app.setName("Freedom Loader");
|
||||||
|
app.setAppUserModelId("com.masteracnolo.freedomloader");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the app dependencies for hardware choice
|
||||||
|
*/
|
||||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||||
const { AutoUpdater } = require("./server/update.js");
|
const { configFeatures, devMode} = require("./config");
|
||||||
const { configFeatures } = require("./config.js");
|
|
||||||
const { startRPC, stopRPC} = require("./server/discordRPC");
|
|
||||||
|
|
||||||
let mainWindow;
|
/**
|
||||||
const logsFolderPath = logDir;
|
* In-memory snapshot of application feature flags.
|
||||||
|
*
|
||||||
const basePath = config.localMode
|
* Note: Changes to the config file are not automatically reflected.
|
||||||
? path.join(__dirname )
|
* A restart could be required.
|
||||||
: path.join(path.dirname(process.execPath), "resources");
|
*/
|
||||||
|
if (!configFeatures.enableHardwareAcceleration){
|
||||||
const configFolderPath = path.join(basePath, "config" ,"config.json");
|
app.disableHardwareAcceleration();
|
||||||
|
logger.info("Disabled Hardware Acceleration");
|
||||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
} else {
|
||||||
|
logger.info("Enable Hardware Acceleration");
|
||||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
|
||||||
app.disableHardwareAcceleration();
|
|
||||||
|
|
||||||
ipcMain.handle("version", () => config.version);
|
|
||||||
|
|
||||||
// Gestion single instance
|
|
||||||
const gotLock = app.requestSingleInstanceLock();
|
|
||||||
|
|
||||||
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
|
||||||
function checkNativeDependencies() {
|
|
||||||
const deps = [
|
|
||||||
{ name: "yt-dlp.exe", path: path.join(process.resourcesPath, "binaries","yt-dlp.exe") },
|
|
||||||
{ name: "ffmpeg.exe", path: path.join(process.resourcesPath, "binaries", "ffmpeg.exe") },
|
|
||||||
{ name: "ffprobe.exe", path: path.join(process.resourcesPath, "binaries", "ffprobe.exe") },
|
|
||||||
{ name: "deno.exe", path: path.join(process.resourcesPath, "binaries", "deno.exe") },
|
|
||||||
];
|
|
||||||
const missing = deps.filter(dep => !fs.existsSync(dep.path));
|
|
||||||
let errorMsg = "";
|
|
||||||
if (missing.length > 0) {
|
|
||||||
const missingList = missing.map(dep => dep.name).join(", ");
|
|
||||||
logger.error(`Missing dependencies: ${missingList}`);
|
|
||||||
errorMsg += `The following files are missing in the 'ressources' folder:\n${missingList}`;
|
|
||||||
}
|
|
||||||
if (errorMsg) {
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
dialog.showErrorBox(
|
|
||||||
"Missing dependencies",
|
|
||||||
`${errorMsg}\n\nThe application will now exit. Try to reinstall`
|
|
||||||
);
|
|
||||||
app.quit();
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!config.localMode){
|
if(devMode){
|
||||||
if (!gotLock) {
|
/**
|
||||||
// Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
|
* Start devTron extensions - @see https://github.com/electron/devtron
|
||||||
// Ici la nouvelle instance continue normalement
|
*/
|
||||||
} else {
|
const { devtron } = require('@electron/devtron');
|
||||||
if (!checkNativeDependencies()) {
|
devtron.install();
|
||||||
// Arrêt déjà géré dans la fonction
|
logger.info("Loaded DevTron Extension");
|
||||||
} else {
|
|
||||||
app.on("second-instance", () => {
|
|
||||||
// La vieille instance se ferme
|
|
||||||
if (mainWindow) {
|
|
||||||
logger.info("New Instance Detected, closing the older...");
|
|
||||||
mainWindow.destroy();
|
|
||||||
mainWindow = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Création fenêtre principale
|
|
||||||
async function createMainWindow() {
|
|
||||||
if (mainWindow) {
|
|
||||||
logger.warn("Window already exists, no new creation!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
/**
|
||||||
title: `Freedom Loader ${config.version}`,
|
* Load the app dependencies
|
||||||
width: 750,
|
*/
|
||||||
height: 800,
|
const { initThemes } = require("./app/themeManager");
|
||||||
minWidth: 750,
|
|
||||||
minHeight: 800,
|
|
||||||
frame: !configFeatures.customTopBar,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
contextIsolation: true,
|
|
||||||
preload: path.join(__dirname, "preload.js"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
|
||||||
|
const {createSystemTray, destroyTray} = require("./app/tray");
|
||||||
|
const { stopServer } = require("./server/server");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose .env in process.env
|
||||||
|
*/
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global flag indicating if the application is intentionally shutting down.
|
||||||
|
* Used across the app to bypass the "minimize to tray on close" behavior.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
app.isQuitting = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State flag to track the asynchronous cleanup process during shutdown.
|
||||||
|
* Prevents the application from exiting before services (RPC, Tray, Logs) are properly closed.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
let isCleanedUp = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a graceful shutdown of all application services asynchronously.
|
||||||
|
* Once finished, it re-triggers the app.quit() process.
|
||||||
|
*/
|
||||||
|
async function cleanShutdown() {
|
||||||
try {
|
try {
|
||||||
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
destroyTray();
|
||||||
logger.info("Window Loaded");
|
await stopRPC();
|
||||||
|
stopServer();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Window Loading Error:", err);
|
logger.error("Error during cleanup:", err);
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow.on("closed", () => {
|
|
||||||
logger.info("Main Window Closed");
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateDownloadPath(userPath) {
|
|
||||||
const userHome = os.homedir(); // C:\Users\<User>
|
|
||||||
|
|
||||||
if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader");
|
|
||||||
|
|
||||||
// Résolution canonique et suivi des symlinks
|
|
||||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
|
||||||
const normalizedHome = path.resolve(userHome) + path.sep;
|
|
||||||
|
|
||||||
if (!resolved.startsWith(normalizedHome)) {
|
|
||||||
throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !");
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// IPC
|
|
||||||
ipcMain.handle("select-download-folder", async () => {
|
|
||||||
try {
|
|
||||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
|
||||||
const validatedPath = validateDownloadPath(result.filePaths[0]);
|
|
||||||
logger.info(`Folder Checked and Valid : ${validatedPath}`);
|
|
||||||
return validatedPath;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`An Error Occured when validating folder : ${err.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("validate-download-path", (event, userPath) => {
|
|
||||||
return validateDownloadPath(userPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
|
|
||||||
|
|
||||||
ipcMain.on("set-progress", (event, percent) => {
|
|
||||||
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
|
|
||||||
});
|
|
||||||
|
|
||||||
// Topbar window controls
|
|
||||||
ipcMain.on("window-minimize", () => {
|
|
||||||
if (mainWindow) mainWindow.minimize();
|
|
||||||
});
|
|
||||||
ipcMain.on("window-maximize", () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
if (mainWindow.isMaximized()) {
|
|
||||||
mainWindow.unmaximize();
|
|
||||||
} else {
|
|
||||||
mainWindow.maximize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ipcMain.on("window-close", () => {
|
|
||||||
if (mainWindow) mainWindow.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Topbar custom actions
|
|
||||||
ipcMain.on("open-devtools", () => {
|
|
||||||
if (mainWindow) mainWindow.webContents.openDevTools({ mode: 'detach' });
|
|
||||||
});
|
|
||||||
ipcMain.on("open-logs", () => {
|
|
||||||
if (logsFolderPath) shell.openPath(logsFolderPath);
|
|
||||||
});
|
|
||||||
ipcMain.on("open-website", () => {
|
|
||||||
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html");
|
|
||||||
});
|
|
||||||
ipcMain.on("open-wiki", () => {
|
|
||||||
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html");
|
|
||||||
});
|
|
||||||
ipcMain.on("open-config", () => {
|
|
||||||
if (configFolderPath) shell.openPath(configFolderPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// App ready
|
|
||||||
app.whenReady().then(async () => {
|
|
||||||
logSessionStart();
|
|
||||||
logger.info("App Ready, Server Express starting...");
|
|
||||||
|
|
||||||
const serverPath = path.join(__dirname, "server", "server.js")
|
|
||||||
|
|
||||||
const expressServer = require(serverPath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await expressServer.startServer();
|
|
||||||
logger.info("Express Server Started");
|
|
||||||
|
|
||||||
ipcMain.handle("features", () => {
|
|
||||||
return configFeatures;
|
|
||||||
});
|
|
||||||
|
|
||||||
const featureWhitelist = new Set([
|
|
||||||
"autoUpdate",
|
|
||||||
"discordRPC",
|
|
||||||
"customTopBar",
|
|
||||||
"autoCheckInfo",
|
|
||||||
"addThumbnail",
|
|
||||||
"addMetadata",
|
|
||||||
"verboseLogs",
|
|
||||||
"autoDownloadPlaylist",
|
|
||||||
"customCodec"
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
logger.info("All services stopped. Have a nice day!");
|
||||||
|
logSessionEnd();
|
||||||
|
|
||||||
ipcMain.handle("set-feature", (event, { key, value }) => {
|
isCleanedUp = true;
|
||||||
try {
|
app.quit();
|
||||||
if (!featureWhitelist.has(key)) {
|
}
|
||||||
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// optionnel mais propre
|
/**
|
||||||
if (configFeatures[key] === value) {
|
* If another instance want to run
|
||||||
return true;
|
*/
|
||||||
}
|
app.on("second-instance", () => {
|
||||||
|
logger.info("Second instance detected");
|
||||||
|
|
||||||
configFeatures[key] = value;
|
const mainWindow = require("./app/windowManager").getMainWindow();
|
||||||
|
|
||||||
fs.writeFileSync(
|
if (!mainWindow) return;
|
||||||
configFolderPath,
|
|
||||||
JSON.stringify(configFeatures, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(`Feature updated: ${key} = ${value}`);
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (err) {
|
mainWindow.show();
|
||||||
logger.error(`set-feature failed (${key}): ${err.message}`);
|
mainWindow.focus();
|
||||||
return false;
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
logSessionStart(logDir, defaultDownloadFolder);
|
||||||
|
|
||||||
configFeatures.discordRPC ? startRPC() : "";
|
createSplashWindow();
|
||||||
|
|
||||||
|
if (!devMode && !checkNativeDependencies()) return;
|
||||||
|
|
||||||
|
const { userYtDlp } = require("./server/helpers/path.helpers");
|
||||||
|
updateYtDlp(userYtDlp);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSplashProgress(0); // Checking dependencies
|
||||||
|
await require("./server/server").startServer();
|
||||||
|
|
||||||
|
setSplashProgress(1); // Starting server
|
||||||
|
registerIpcHandlers(getMainWindow);
|
||||||
|
|
||||||
|
setSplashProgress(2); // Loading themes
|
||||||
|
|
||||||
|
validateBinaries();
|
||||||
|
|
||||||
|
initUserThemes();
|
||||||
|
await initThemes(userThemesPath);
|
||||||
|
|
||||||
|
setSplashProgress(3); // Almost ready
|
||||||
await createMainWindow();
|
await createMainWindow();
|
||||||
configFeatures.autoUpdate ? AutoUpdater(mainWindow) : ""; // Auto Update
|
|
||||||
|
closeSplashWindow();
|
||||||
|
getMainWindow().show();
|
||||||
|
|
||||||
|
if (configFeatures.systemTray) {
|
||||||
|
createSystemTray(devMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configFeatures.discordRPC) startRPC();
|
||||||
|
|
||||||
|
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Window or Server error :", err);
|
logger.error("Boot error:", err);
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
logger.info("All Window Closed, shuting down app");
|
if (app.isQuitting || !configFeatures.systemTray) {
|
||||||
if (process.platform !== "darwin") app.quit();
|
logger.info("Shutting down...");
|
||||||
|
app.quit();
|
||||||
|
} else if (process.platform !== "darwin") {
|
||||||
|
logger.info("Main window closed, app running in background (Tray).");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", (event) => {
|
||||||
logSessionEnd()
|
app.isQuitting = true;
|
||||||
stopRPC()
|
|
||||||
});
|
if (!isCleanedUp) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
cleanShutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
7545
package-lock.json
generated
156
package.json
@@ -1,29 +1,67 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
|
"desktopName": "com.masteracnolo.freedomloader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.4.2",
|
"version": "1.6.4",
|
||||||
"author": "MasterAcnolo",
|
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
|
||||||
"description": "Freedom Loader",
|
"description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms",
|
||||||
|
"homepage": "https://masteracnolo.github.io/Freedom-Loader-Site/",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MasterAcnolo/Freedom-Loader.git"
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/MasterAcnolo/Freedom-Loader/issues"
|
||||||
|
},
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron --trace-warnings .",
|
"start": "electron --no-warnings .",
|
||||||
|
"start:warn": "electron --trace-warnings .",
|
||||||
|
"start:debug": "WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .",
|
||||||
|
"dev": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --no-warnings .\"",
|
||||||
|
"dev:warn": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --trace-warnings .\"",
|
||||||
|
"dev:debug": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .\"",
|
||||||
"build": "electron-builder",
|
"build": "electron-builder",
|
||||||
|
"build:win": "electron-builder --win",
|
||||||
|
"build:linux": "electron-builder --linux",
|
||||||
|
"build:rpm": "bash scripts/create-copr-package.sh",
|
||||||
|
"build:all": "electron-builder -wl",
|
||||||
|
"release": "bash scripts/release.sh",
|
||||||
|
"release:publish": "bash scripts/release.sh --publish",
|
||||||
|
"release:dry-run": "bash scripts/release.sh --dry-run",
|
||||||
|
"test": "jest",
|
||||||
|
"test:unit": "jest test/unit",
|
||||||
|
"lint": "eslint . --format pretty",
|
||||||
|
"lint:fix": "eslint . --fix --format pretty",
|
||||||
"update": "npm update"
|
"update": "npm update"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
"discord-rpc": "^4.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"webidl-conversions": "^8.0.0",
|
"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": "^37.1.0",
|
"@electron/devtron": "^2.1.1",
|
||||||
"electron-builder": "^26.4.0"
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@jest/globals": "^30.4.1",
|
||||||
|
"@playwright/test": "^1.62.1",
|
||||||
|
"electron": "^44.3.0",
|
||||||
|
"electron-builder": "^26.15.3",
|
||||||
|
"eslint": "^10.10.0",
|
||||||
|
"eslint-formatter-pretty": "^7.1.0",
|
||||||
|
"globals": "^17.12.0",
|
||||||
|
"jest": "^30.4.2",
|
||||||
|
"nodemon": "^3.1.14",
|
||||||
|
"playwright": "^1.62.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.masteracnolo.freedomloader",
|
"appId": "com.masteracnolo.freedomloader",
|
||||||
@@ -32,13 +70,15 @@
|
|||||||
{
|
{
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
"owner": "MasterAcnolo",
|
"owner": "MasterAcnolo",
|
||||||
"repo": "Freedom-Loader"
|
"repo": "Freedom-Loader",
|
||||||
|
"releaseType": "draft"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"**/*",
|
"**/*",
|
||||||
"node_modules/**/*",
|
"node_modules/**/*",
|
||||||
"!logs/*.json"
|
"!config/config.dev.json",
|
||||||
|
"!theme/**/*"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "build"
|
"buildResources": "build"
|
||||||
@@ -46,45 +86,42 @@
|
|||||||
"asar": true,
|
"asar": true,
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "build/confirm-icon.png",
|
"from": "theme",
|
||||||
"to": "confirm-icon.png"
|
"to": "theme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "build/app-icon.ico",
|
"from": "config/config.default.json",
|
||||||
"to": "app-icon.ico"
|
"to": "config/config.default.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "build/banner.bmp",
|
"from": "build",
|
||||||
"to": "banner.bmp"
|
"to": ".",
|
||||||
},
|
"filter": [
|
||||||
{
|
"app-icon.ico",
|
||||||
"from": "build/error.png",
|
"app-icon-64x64.png",
|
||||||
"to": "error.png"
|
"confirm-icon.png",
|
||||||
},
|
"banner.bmp",
|
||||||
{
|
"banner.png",
|
||||||
"from": "ressources/yt-dlp.exe",
|
"error.png"
|
||||||
"to": "binaries/yt-dlp.exe"
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "ressources/ffmpeg.exe",
|
|
||||||
"to": "binaries/ffmpeg.exe"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "ressources/ffprobe.exe",
|
|
||||||
"to": "binaries/ffprobe.exe"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "ressources/deno.exe",
|
|
||||||
"to": "binaries/deno.exe"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "config/config.json",
|
|
||||||
"to": "config/config.json"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"icon": "build/app-icon.ico"
|
"icon": "build/app-icon.ico",
|
||||||
|
"artifactName": "Freedom-Loader-Setup-${version}.${ext}",
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/win-32",
|
||||||
|
"to": "binaries",
|
||||||
|
"filter": [
|
||||||
|
"yt-dlp.exe",
|
||||||
|
"ffmpeg.exe",
|
||||||
|
"ffprobe.exe",
|
||||||
|
"deno.exe"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
@@ -96,6 +133,47 @@
|
|||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": true,
|
"createStartMenuShortcut": true,
|
||||||
"shortcutName": "Freedom Loader"
|
"shortcutName": "Freedom Loader"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage",
|
||||||
|
"deb",
|
||||||
|
"snap"
|
||||||
|
],
|
||||||
|
"category": "Utility",
|
||||||
|
"synopsis": "GUI for yt-dlp to download video and audio with metadata",
|
||||||
|
"maintainer": "MasterAcnolo",
|
||||||
|
"icon": "build/app-icon.png",
|
||||||
|
"executableName": "freedom-loader",
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/linux",
|
||||||
|
"to": "binaries",
|
||||||
|
"filter": [
|
||||||
|
"yt-dlp",
|
||||||
|
"ffmpeg",
|
||||||
|
"ffprobe",
|
||||||
|
"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",
|
||||||
|
"removable-media"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
9
package/copr-build.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh -xe
|
||||||
|
git clone --depth 1 https://github.com/MasterAcnolo/Freedom-Loader.git src
|
||||||
|
cd src
|
||||||
|
npm ci
|
||||||
|
npx electron-builder --linux dir
|
||||||
|
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
tar czf "$resultdir/freedom-loader-$VERSION.tar.gz" -C dist linux-unpacked
|
||||||
|
sed "s/__VERSION__/$VERSION/" package/freedom-loader.spec > "$resultdir/freedom-loader.spec"
|
||||||
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
|
||||||
193
preload.js
@@ -1,23 +1,208 @@
|
|||||||
const { contextBridge, ipcRenderer } = require("electron");
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the main application API to the renderer process.
|
||||||
|
*
|
||||||
|
* This API acts as a secure bridge between the renderer and the
|
||||||
|
* Electron main process using IPC (Inter-Process Communication).
|
||||||
|
* All calls are explicitly whitelisted.
|
||||||
|
*/
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
/**
|
||||||
|
* Sends error log message.
|
||||||
|
*
|
||||||
|
* @param {...any} args - Values to log
|
||||||
|
*/
|
||||||
|
logError: (...args) => ipcRenderer.send("log-error", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends info log message.
|
||||||
|
*
|
||||||
|
* @param {...any} args - Values to log
|
||||||
|
*/
|
||||||
|
logInfo: (...args) => ipcRenderer.send("log-info", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends warning log message.
|
||||||
|
*
|
||||||
|
* @param {...any} args - Values to log
|
||||||
|
*/
|
||||||
|
logWarn: (...args) => ipcRenderer.send("log-warn", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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),
|
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves application feature flags (runtime configuration).
|
||||||
|
*/
|
||||||
getFeatures: () => ipcRenderer.invoke("features"),
|
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 }),
|
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current application version.
|
||||||
|
*/
|
||||||
getVersion: () => ipcRenderer.invoke("version"),
|
getVersion: () => ipcRenderer.invoke("version"),
|
||||||
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path)
|
|
||||||
|
/**
|
||||||
|
* 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"),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ==========================================
|
||||||
|
* Auto Updater IPC
|
||||||
|
* ==========================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an update has already been detected by the main process.
|
||||||
|
*
|
||||||
|
* @returns {Promise<boolean>} Resolves to true if an update is available.
|
||||||
|
*/
|
||||||
|
isUpdateAvailable: () => ipcRenderer.invoke("is-update-available"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to the update available event pushed by the main process.
|
||||||
|
*
|
||||||
|
* @param {Function} callback - Function executed with update info when an update is found.
|
||||||
|
*/
|
||||||
|
onUpdateAvailable: (callback) =>
|
||||||
|
ipcRenderer.on("update-available", (_, info) => callback(info)),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instructs the main process to start downloading the available update.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
downloadUpdate: () => ipcRenderer.invoke("download-update"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instructs the main process to quit the application and install the downloaded update.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
installUpdate: () => ipcRenderer.invoke("install-update"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to the download progress event.
|
||||||
|
*
|
||||||
|
* @param {Function} callback - Function executed with progress metrics (percent, bytesPerSecond, transferred, total).
|
||||||
|
*/
|
||||||
|
onDownloadProgress: (callback) =>
|
||||||
|
ipcRenderer.on("update-progress", (_, progress) => callback(progress)),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to the update downloaded event.
|
||||||
|
* Triggered when the update file is fully downloaded and ready to install.
|
||||||
|
*
|
||||||
|
* @param {Function} callback - Function executed with update info.
|
||||||
|
*/
|
||||||
|
onUpdateDownloaded: (callback) =>
|
||||||
|
ipcRenderer.on("update-downloaded", (_, info) => callback(info)),
|
||||||
|
|
||||||
|
openReleasePage: () => ipcRenderer.send("open-release-page"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Contrôles de fenêtre et outils custom pour la topbar
|
/**
|
||||||
|
* 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", {
|
contextBridge.exposeInMainWorld("topbarAPI", {
|
||||||
|
/**
|
||||||
|
* Minimizes the application window.
|
||||||
|
*/
|
||||||
minimize: () => ipcRenderer.send("window-minimize"),
|
minimize: () => ipcRenderer.send("window-minimize"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles maximize/restore state of the application window.
|
||||||
|
*/
|
||||||
maximize: () => ipcRenderer.send("window-maximize"),
|
maximize: () => ipcRenderer.send("window-maximize"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the application window.
|
||||||
|
*/
|
||||||
close: () => ipcRenderer.send("window-close"),
|
close: () => ipcRenderer.send("window-close"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Chromium DevTools for debugging.
|
||||||
|
*/
|
||||||
openDevTools: () => ipcRenderer.send("open-devtools"),
|
openDevTools: () => ipcRenderer.send("open-devtools"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens application logs directory or log viewer.
|
||||||
|
*/
|
||||||
openLogs: () => ipcRenderer.send("open-logs"),
|
openLogs: () => ipcRenderer.send("open-logs"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the official website or project homepage.
|
||||||
|
*/
|
||||||
openWebsite: () => ipcRenderer.send("open-website"),
|
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"),
|
openWiki: () => ipcRenderer.send("open-wiki"),
|
||||||
openConfig: () => ipcRenderer.send("open-config")
|
|
||||||
});
|
/**
|
||||||
|
* Opens the workshop or external content library.
|
||||||
|
*/
|
||||||
|
openWorkshop: () => ipcRenderer.send("open-workshop"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens configuration/settings panel.
|
||||||
|
*/
|
||||||
|
openConfig: () => ipcRenderer.send("open-config"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send Bug Report
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
sendReport: (params) => ipcRenderer.invoke("send-report", params),
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 967 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 5.3 MiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -7,30 +7,36 @@
|
|||||||
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></title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles/styles.css">
|
<link rel="stylesheet" href="styles/styles.css">
|
||||||
<link rel="stylesheet" href="styles/layout/topbar.css">
|
<link rel="stylesheet" href="styles/layout/topbar.css">
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="topbar" id="topbar">
|
<header>
|
||||||
<div class="custom-controls">
|
<div class="topbar" id="topbar">
|
||||||
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
<div class="custom-controls">
|
||||||
<button id="logs-btn" title="Logs">Logs</button>
|
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
||||||
<button id="website-btn" title="Website">Website</button>
|
<button id="logs-btn" title="Logs">Logs</button>
|
||||||
<button id="wiki-btn" title="Wiki">Wiki</button>
|
<button id="report-bug-btn" title="Report a bug">Report a Bug</button> <!-- TODO: Find a better place -->
|
||||||
|
<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>
|
</div>
|
||||||
<div class="window-controls">
|
</header>
|
||||||
<button id="minimize-btn" title="Réduire"><img src="assets/icon/minimize.svg"></button>
|
|
||||||
<button id="maximize-btn" title="Maximiser"><img src="assets/icon/maximize.svg"></button>
|
|
||||||
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="settings-button" id="settings-button">
|
<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>
|
<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>
|
</button>
|
||||||
@@ -52,12 +58,18 @@
|
|||||||
|
|
||||||
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<div style="display: flex;">
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
<span class="setting-title">Thème</span>
|
<span class="setting-title">Theme</span>
|
||||||
|
|
||||||
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
<select id="themeSelect" data-key="theme" aria-label="Choose the theme">
|
||||||
|
|
||||||
</select>
|
</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>
|
</div>
|
||||||
<small>Choose the theme of the application</small>
|
<small>Choose the theme of the application</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,27 +92,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
<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">
|
<input type="checkbox" data-key="notifySystem" id="notifySystem">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Notification at the end</span>
|
<span class="setting-title">Notification at the end</span>
|
||||||
<small>Notify when a download finishes.</small>
|
<small>Notify when a download finishes.</small>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
|
<input data-key="keepPlaylistOrder" id="keepPlaylistOrder" type="checkbox">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Auto Fetch Info</span>
|
<span class="setting-title">Keep Playlist Order</span>
|
||||||
<small>Automatically fetch information for new downloads.</small>
|
<small>Add index before output file, to keep the order.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,15 +130,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
<div class="setting-item">
|
||||||
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
<input data-key="systemTray" id="systemTray" type="checkbox">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Output title (special chars)</span>
|
<span class="setting-title">System Tray</span>
|
||||||
<small>Check for unusual characters in output titles.</small>
|
<small>Minimize app on close, in the System Tray. [EXPERIMENTAL]</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="setting-item">
|
<!-- <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 data-key="autoCheckInfo" id="autoCheckInfo" type="checkbox">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Auto Fetch Info</span>
|
||||||
|
<small>Automatically fetch information for new downloads.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Add Metadata</span>
|
<span class="setting-title">Add Metadata</span>
|
||||||
@@ -142,6 +170,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</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-item" style="margin-left: 32px; margin-top: 20px;">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Custom Codec</span>
|
<span class="setting-title">Custom Codec</span>
|
||||||
@@ -158,28 +194,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</details>
|
||||||
|
|
||||||
<!-- Developer Settings -->
|
<!-- Developer Settings -->
|
||||||
<details class="settings-section">
|
<details class="settings-section">
|
||||||
<summary>Developer Settings</summary>
|
<summary>Developer Settings</summary>
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
|
||||||
<input type="checkbox" data-key="downloadSystem" id="downloadSystem">
|
|
||||||
<div class="setting-info">
|
|
||||||
<span class="setting-title">Download System</span>
|
|
||||||
<small>Enable/disable the internal download system.</small>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
|
||||||
<input type="checkbox" data-key="logSystem" id="logSystem">
|
|
||||||
<div class="setting-info">
|
|
||||||
<span class="setting-title">Log System</span>
|
|
||||||
<small>Enable logging system.</small>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
@@ -191,6 +219,7 @@
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div class="settings-footer">
|
<div class="settings-footer">
|
||||||
|
<button id="open-theme">Open Theme Folder</button>
|
||||||
<button id="open-json-btn">Open JSON Configuration</button>
|
<button id="open-json-btn">Open JSON Configuration</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -198,71 +227,113 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container" id="container">
|
<main>
|
||||||
|
<div class="container" id="container">
|
||||||
|
<h1 id="title"> Freedom Loader </h1>
|
||||||
|
<p id="subtitle"> Because why not? </p>
|
||||||
|
|
||||||
|
<form action="/download" method="POST" id="downloadForm">
|
||||||
|
<div class="url-input-container">
|
||||||
|
<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>
|
||||||
|
|
||||||
<header>
|
<label class="checkbox-inline">
|
||||||
<h1 id="title"> Freedom Loader </h1>
|
|
||||||
<p id="subtitle"> Because why not? </p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form action="/download" method="POST" id="downloadForm">
|
|
||||||
<input name="url" placeholder="URL" id="UrlInput">
|
|
||||||
|
|
||||||
<label class="checkbox-inline">
|
<label class="checkbox-mp3">
|
||||||
|
<input type="checkbox" name="audioOnly" value="1">
|
||||||
|
<div class="checkmark"></div>
|
||||||
|
</label>
|
||||||
|
Audio Only (MP3)
|
||||||
|
</label>
|
||||||
|
<select name="quality">
|
||||||
|
<option value="best">Best Quality</option>
|
||||||
|
<option value="medium">Medium Quality</option>
|
||||||
|
<option value="worst">Lowest Quality</option>
|
||||||
|
<option value="1080">1080p</option>
|
||||||
|
<option value="720">720p</option>
|
||||||
|
<option value="480">480p</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<label class="checkbox-mp3">
|
<button type="submit" onclick="startProgress()">Download</button>
|
||||||
<input type="checkbox" name="audioOnly" value="1">
|
</form>
|
||||||
<div class="checkmark"></div>
|
|
||||||
</label>
|
|
||||||
Audio Only (MP3)
|
|
||||||
</label>
|
|
||||||
<select name="quality">
|
|
||||||
<option value="best">Best Quality</option>
|
|
||||||
<option value="medium">Medium Quality</option>
|
|
||||||
<option value="worst">Lowest Quality</option>
|
|
||||||
<option value="1080">1080p</option>
|
|
||||||
<option value="720">720p</option>
|
|
||||||
<option value="480">480p</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button type="submit" onclick="startProgress()">Download</button>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="download-path">
|
<div class="download-path">
|
||||||
Will be save on <span id="savePath"></span>
|
Will be saved in <span id="savePath"></span>
|
||||||
<button id="changePath">Edit</button>
|
<button id="changePath">Edit</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="downloadProgressWrapper">
|
<div class="progress-container">
|
||||||
<div id="downloadProgress"></div>
|
<div id="downloadStage" style="display: none; font-size: 0.85rem; color: var(--default-text-color); margin-top: 8px;"></div>
|
||||||
</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 style="display: flex; gap: 20px;">
|
<div id="downloadStatus" style="display: none;"></div>
|
||||||
<div id="downloadProgressText"></div>
|
|
||||||
<div id="downloadSpeedText"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="downloadStatus"></div>
|
<div class="infos-container">
|
||||||
|
|
||||||
<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="loaderBox" class="loader-box" style="display:none;">
|
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||||
<div></div>
|
</div>
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<span>Fetching info...</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
<span id="version-badge" class="version-badge"></span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<span id="version-badge" class="version-badge"></span>
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
<h2 id="reference"> Because Why Not ? </h2>
|
<h2 id="reference"> Because Why Not ? </h2>
|
||||||
|
|
||||||
|
<div id="updateAvailable" style="display:none">
|
||||||
|
<div class="update-banner">
|
||||||
|
<div class="banner-row">
|
||||||
|
<div class="banner-left">
|
||||||
|
<div class="icon-wrap">
|
||||||
|
<span class="material-symbols-outlined" id="update-icon">download</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-block">
|
||||||
|
<span class="update-title" id="update-title">Update available</span>
|
||||||
|
<span class="update-sub" id="update-sub">A new version is ready</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-update" id="btn-update">Download</button>
|
||||||
|
</div>
|
||||||
|
<div class="progress-wrap" id="progress-wrap" style="display:none">
|
||||||
|
<div class="progress-track">
|
||||||
|
<div class="progress-fill" id="progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-meta">
|
||||||
|
<span id="progress-pct">0%</span>
|
||||||
|
<span id="progress-eta"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
<script src="script/toast.js"></script>
|
||||||
|
<script src="script/clipboardPaste.js"></script>
|
||||||
<script src="script/custompath.js"></script>
|
<script src="script/custompath.js"></script>
|
||||||
<script src="script/settingsPanel.js"></script>
|
<script src="script/settingsPanel.js"></script>
|
||||||
<script src="script/appVersion.js"></script>
|
<script src="script/appVersion.js"></script>
|
||||||
@@ -270,6 +341,8 @@
|
|||||||
<script src="script/fetchinfo.js"></script>
|
<script src="script/fetchinfo.js"></script>
|
||||||
<script src="script/progressBar.js"></script>
|
<script src="script/progressBar.js"></script>
|
||||||
<script src="script/customthemes.js"></script>
|
<script src="script/customthemes.js"></script>
|
||||||
|
<script src="script/reportBug.js"></script>
|
||||||
|
<script src="script/autoUpdate.js"></script>
|
||||||
<script type="module" src="script/topbar.js"></script>
|
<script type="module" src="script/topbar.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,6 +1,54 @@
|
|||||||
async function versionLabel(){
|
/**
|
||||||
const appVersion = await window.electronAPI.getVersion();
|
* Retrieves the application version and build state from the backend.
|
||||||
document.getElementById("version-badge").textContent = `v${appVersion}`;
|
* Displays a formatted string indicating packaging state and preview status.
|
||||||
};
|
* Hides the "Packaged" state for production builds to keep the UI clean.
|
||||||
|
*
|
||||||
|
* Example outputs: "Not Packaged Preview v1.6.2" or "Preview v1.6.2"
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function displayVersionLabel() {
|
||||||
|
/**
|
||||||
|
* Raw version string from the backend (e.g., "dev-1.6.2-preview" or "v1.6.2")
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const appVersion = await window.electronAPI.getVersion();
|
||||||
|
|
||||||
versionLabel();
|
/**
|
||||||
|
* UI element displaying the current application version.
|
||||||
|
* @type {HTMLElement | null}
|
||||||
|
*/
|
||||||
|
const versionBadge = document.getElementById("version-badge");
|
||||||
|
|
||||||
|
if (!versionBadge) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the numeric semantic version (e.g., "1.6.2").
|
||||||
|
* @type {RegExpMatchArray | null}
|
||||||
|
*/
|
||||||
|
const versionMatch = appVersion.match(/(\d+\.\d+\.\d+)/);
|
||||||
|
const baseVersion = versionMatch ? versionMatch[0] : "Unknown";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the packaging state. The backend prepends "dev-" if unpackaged.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
const isPackaged = !appVersion.includes("dev-");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the current build is a preview version.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
const isPreview = appVersion.includes("preview");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the final display string.
|
||||||
|
* If packaged, we don't mention it. If not, we explicitly say "Not Packaged ".
|
||||||
|
*/
|
||||||
|
const packageStateStr = isPackaged ? "" : "Not Packaged ";
|
||||||
|
const previewStr = isPreview ? "Preview " : "";
|
||||||
|
|
||||||
|
versionBadge.textContent = `${packageStateStr}${previewStr}v${baseVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayVersionLabel();
|
||||||
81
public/script/autoUpdate.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
async function initUpdateUI() {
|
||||||
|
const container = document.getElementById("updateAvailable");
|
||||||
|
|
||||||
|
const hasUpdate = await window.electronAPI.isUpdateAvailable();
|
||||||
|
if (hasUpdate) {
|
||||||
|
setupAndShowUpdateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.electronAPI.onUpdateAvailable((info) => {
|
||||||
|
setupAndShowUpdateUI(info);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupAndShowUpdateUI(info) {
|
||||||
|
container.style.display = "block";
|
||||||
|
|
||||||
|
const btn = document.getElementById("btn-update");
|
||||||
|
const icon = document.getElementById("update-icon");
|
||||||
|
const title = document.getElementById("update-title");
|
||||||
|
const sub = document.getElementById("update-sub");
|
||||||
|
const progressWrap = document.getElementById("progress-wrap");
|
||||||
|
const progressFill = document.getElementById("progress-fill");
|
||||||
|
const progressPct = document.getElementById("progress-pct");
|
||||||
|
const progressEta = document.getElementById("progress-eta");
|
||||||
|
|
||||||
|
if (info && info.version) {
|
||||||
|
sub.textContent = `A new version (${info.version}) is ready`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info && info.environment && info.environment !== "auto") {
|
||||||
|
icon.textContent = "open_in_new";
|
||||||
|
|
||||||
|
if (info.environment === "snap") {
|
||||||
|
sub.textContent = `Version ${info.version} is out. Use the Snap Store or run "snap refresh".`;
|
||||||
|
btn.textContent = "View Release";
|
||||||
|
} else {
|
||||||
|
sub.textContent = `Version ${info.version} is out. Update via your package manager or download it manually.`;
|
||||||
|
btn.textContent = "Download manually";
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.onclick = () => {
|
||||||
|
window.electronAPI.openReleasePage();
|
||||||
|
container.style.display = "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.onclick = () => {
|
||||||
|
window.electronAPI.downloadUpdate();
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = "Downloading...";
|
||||||
|
icon.textContent = "download";
|
||||||
|
title.textContent = "Downloading update";
|
||||||
|
progressWrap.style.display = "flex";
|
||||||
|
};
|
||||||
|
|
||||||
|
window.electronAPI.onDownloadProgress((progress) => {
|
||||||
|
const pct = Math.round(progress.percent);
|
||||||
|
progressFill.style.width = pct + "%";
|
||||||
|
progressPct.textContent = pct + "%";
|
||||||
|
|
||||||
|
const remaining = (progress.total - progress.transferred) / progress.bytesPerSecond;
|
||||||
|
progressEta.textContent =
|
||||||
|
remaining > 0 ? `~${Math.ceil(remaining)}s left` : "almost done";
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onUpdateDownloaded(() => {
|
||||||
|
progressFill.style.width = "100%";
|
||||||
|
progressPct.textContent = "100%";
|
||||||
|
progressEta.textContent = "Done";
|
||||||
|
icon.textContent = "check";
|
||||||
|
title.textContent = "Ready to install";
|
||||||
|
sub.textContent = "Restart to apply the update";
|
||||||
|
btn.textContent = "Restart";
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.onclick = () => window.electronAPI.installUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initUpdateUI();
|
||||||
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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Initializes the download path system once the UI is ready.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Restore last known user-selected download path (localStorage)
|
||||||
|
* - Validate it against backend rules
|
||||||
|
* - Synchronize with backend authoritative path
|
||||||
|
* - Setup folder selection UI interaction
|
||||||
|
*/
|
||||||
window.addEventListener("DOMContentLoaded", async () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
const savePathElem = document.getElementById("savePath");
|
const savePathElem = document.getElementById("savePath");
|
||||||
const form = document.getElementById("downloadForm");
|
const form = document.getElementById("downloadForm");
|
||||||
|
|
||||||
// input caché = DERNIER chemin validé par le back
|
/**
|
||||||
|
* Hidden form field storing the validated download path
|
||||||
|
* used during download submission.
|
||||||
|
*
|
||||||
|
* Created dynamically if not already present in DOM.
|
||||||
|
*/
|
||||||
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";
|
||||||
@@ -12,12 +27,32 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
form.appendChild(hidden);
|
form.appendChild(hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a validated download path received from backend.
|
||||||
|
*
|
||||||
|
* Updates:
|
||||||
|
* - UI display (visible path)
|
||||||
|
* - tooltip (full path)
|
||||||
|
* - hidden form input (used for submission)
|
||||||
|
* - localStorage cache (UX persistence only, not authoritative)
|
||||||
|
*
|
||||||
|
* @param {string} path - validated absolute download path
|
||||||
|
*/
|
||||||
async function applyPathFromBack(path) {
|
async function applyPathFromBack(path) {
|
||||||
savePathElem.textContent = path;
|
savePathElem.textContent = path;
|
||||||
|
savePathElem.title = path;
|
||||||
hidden.value = path;
|
hidden.value = path;
|
||||||
localStorage.setItem("customDownloadPath", path); // UX only
|
localStorage.setItem("customDownloadPath", path); // UX only
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
async function loadInitialPath() {
|
||||||
// On affiche ce que le user a vu la dernière fois (UX)
|
// On affiche ce que le user a vu la dernière fois (UX)
|
||||||
const cached = localStorage.getItem("customDownloadPath");
|
const cached = localStorage.getItem("customDownloadPath");
|
||||||
@@ -41,22 +76,22 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
await loadInitialPath();
|
await loadInitialPath();
|
||||||
|
|
||||||
document
|
/**
|
||||||
.getElementById("changePath")
|
* Opens system folder selector and updates download path.
|
||||||
.addEventListener("click", async () => {
|
*
|
||||||
|
* The selected folder is validated by Electron backend
|
||||||
|
* before being applied to the UI and persisted.
|
||||||
|
*/
|
||||||
|
document.getElementById("changePath").addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const selectedPath =
|
// selectDownloadFolder already returns a validated path
|
||||||
await window.electronAPI.selectDownloadFolder();
|
const validatedPath = await window.electronAPI.selectDownloadFolder();
|
||||||
|
|
||||||
if (!selectedPath) return; // annulé
|
if (!validatedPath) return; // cancelled
|
||||||
|
|
||||||
// Validation back obligatoire
|
|
||||||
const validatedPath =
|
|
||||||
await window.electronAPI.getValidatedDownloadPath(selectedPath);
|
|
||||||
|
|
||||||
await applyPathFromBack(validatedPath);
|
await applyPathFromBack(validatedPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Dossier non autorisé.");
|
alert("This folder is not allowed. Only specific folders (Users, Downloads, Documents) are authorized for downloads.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,62 +1,256 @@
|
|||||||
const themes = {
|
|
||||||
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
|
|
||||||
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
|
|
||||||
neon: { label: "Purple", subtitle: "How was your day ?"},
|
|
||||||
nf: { label: "NF", subtitle: "You call it music, i call it my Therapist" },
|
|
||||||
songbird: { label: "Songbird", subtitle: "From Her to Eternity" },
|
|
||||||
drift: { label: "Drift", subtitle: "Si la route t'appelle, contre appel" },
|
|
||||||
fanatic: { label: "Fanatic", subtitle: "Always Fnatic !" },
|
|
||||||
chirac: { label: "Chirac", subtitle: "J'aime les pommes" },
|
|
||||||
spicy: { label: "Spicy", subtitle: "The Spiciest One" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const themeSelect = document.getElementById("themeSelect");
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
/**
|
||||||
|
* Currently selected themes loaded from backend.
|
||||||
|
* Used as cache for UI theme switching.
|
||||||
|
*/
|
||||||
|
let loadedThemes = [];
|
||||||
|
|
||||||
function populateThemeSelect() {
|
/**
|
||||||
for (const [themeKey, themeInfo] of Object.entries(themes)) {
|
* Applies a theme object to the application UI by mapping
|
||||||
|
* 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 = themeKey;
|
option.value = theme.id;
|
||||||
option.textContent = themeInfo.label;
|
option.textContent = theme.name;
|
||||||
themeSelect.appendChild(option);
|
themeSelect.appendChild(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTheme(themeKey) {
|
/**
|
||||||
document.body.classList.remove(...Object.keys(themes));
|
* Initializes theme system:
|
||||||
document.body.classList.add(themeKey);
|
* - loads themes from backend
|
||||||
|
* - restores saved theme
|
||||||
|
* - applies selected theme
|
||||||
|
*/
|
||||||
|
async function initThemes() {
|
||||||
|
loadedThemes = await window.electronAPI.getThemes();
|
||||||
|
|
||||||
const subtitleElement = document.getElementById("subtitle");
|
if (!loadedThemes.length) {
|
||||||
if (subtitleElement && themes[themeKey]) {
|
const defaultTheme = {
|
||||||
subtitleElement.textContent = themes[themeKey].subtitle;
|
id: "dark",
|
||||||
|
name: "Dark",
|
||||||
|
subtitle: "Because Why Not ?",
|
||||||
|
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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTheme(themeKey) {
|
/**
|
||||||
localStorage.setItem("selectedTheme", themeKey);
|
* Handles theme selection change from UI dropdown.
|
||||||
}
|
*/
|
||||||
|
themeSelect.addEventListener("change", (e) => {
|
||||||
|
const theme = loadedThemes.find(t => t.id === e.target.value);
|
||||||
|
if (!theme) return;
|
||||||
|
|
||||||
function loadTheme() {
|
applyTheme(theme);
|
||||||
const savedTheme = localStorage.getItem("selectedTheme");
|
saveTheme(theme.id);
|
||||||
|
|
||||||
if (savedTheme && themes[savedTheme]) {
|
|
||||||
applyTheme(savedTheme);
|
|
||||||
themeSelect.value = savedTheme;
|
|
||||||
} else {
|
|
||||||
applyTheme("dark"); // thème par défaut
|
|
||||||
themeSelect.value = "dark";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quand l'utilisateur change le thème depuis le select
|
|
||||||
themeSelect.addEventListener("change", (event) => {
|
|
||||||
const selectedTheme = event.target.value;
|
|
||||||
if (themes[selectedTheme]) {
|
|
||||||
applyTheme(selectedTheme);
|
|
||||||
saveTheme(selectedTheme);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialisation
|
/**
|
||||||
populateThemeSelect();
|
* 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,11 +1,55 @@
|
|||||||
const form = document.getElementById("downloadForm");
|
const form = document.getElementById("downloadForm");
|
||||||
const statusDiv = document.getElementById("downloadStatus");
|
const statusDiv = document.getElementById("downloadStatus");
|
||||||
const button = form.querySelector("button");
|
const button = form.querySelector("button[type=\"submit\"]");
|
||||||
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
e.preventDefault();
|
||||||
button.disabled = true; // Empêche les clics multiples
|
|
||||||
statusDiv.textContent = "Download in progress...";
|
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;
|
||||||
|
statusDiv.style.display = "none";
|
||||||
|
isDownloading = true;
|
||||||
|
userStoppedDownload = false;
|
||||||
|
|
||||||
|
// Show progress UI immediately with initial message
|
||||||
|
showInitialProgress();
|
||||||
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const params = new URLSearchParams(formData);
|
const params = new URLSearchParams(formData);
|
||||||
@@ -18,20 +62,113 @@ form.addEventListener("submit", async (e) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
statusDiv.textContent = res;
|
if (!userStoppedDownload) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
window.showError(errorText);
|
||||||
|
resetProgressBar();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await res.text();
|
// Download successful - check if notifications are enabled
|
||||||
statusDiv.textContent = text;
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
if (features.notifySystem) {
|
||||||
|
window.showSuccess("Download completed!");
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch (err) {
|
||||||
statusDiv.textContent = "❌ An Error has Occured.";
|
if (!isDownloading && !userStoppedDownload) {
|
||||||
|
statusDiv.style.display = "none";
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
isDownloading = false;
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
stopBtn.style.display = "none";
|
||||||
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,23 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* 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) {
|
function formatDate(dateStr) {
|
||||||
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||||
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
function formatSize(bytes) {
|
||||||
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches video or playlist metadata from the backend `/info` endpoint.
|
||||||
|
*
|
||||||
|
* 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) {
|
async function fetchVideoInfo(url) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/info", {
|
const res = await fetch(`/info?url=${encodeURIComponent(url)}`, {
|
||||||
method: "POST",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: {"Accept": "application/json"}
|
||||||
body: new URLSearchParams({ url }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) return { error: `An Error occured when fetching info` };
|
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) return {error: `An Error occured when fetching info`};
|
||||||
|
|
||||||
if (!data) return { error: "Data is Missing" };
|
if (!data) return { error: "Data is Missing" };
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -26,13 +47,34 @@ async function fetchVideoInfo(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
async function init() {
|
||||||
|
|
||||||
|
// Main input listener: triggers auto-fetch when URL changes
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const urlInput = document.getElementById("UrlInput");
|
const urlInput = document.getElementById("UrlInput");
|
||||||
const infoDiv = document.getElementById("videoInfo");
|
const infoDiv = document.getElementById("videoInfo");
|
||||||
const loaderBox = document.getElementById("loaderBox");
|
const loaderBox = document.getElementById("loaderBox");
|
||||||
|
const form = document.getElementById("downloadForm");
|
||||||
|
|
||||||
|
// Input caché pour stocker le titre de la playlist
|
||||||
|
let playlistTitleInput = document.getElementById("playlistTitle");
|
||||||
|
if (!playlistTitleInput) {
|
||||||
|
playlistTitleInput = document.createElement("input");
|
||||||
|
playlistTitleInput.type = "hidden";
|
||||||
|
playlistTitleInput.name = "playlistTitle";
|
||||||
|
playlistTitleInput.id = "playlistTitle";
|
||||||
|
form.appendChild(playlistTitleInput);
|
||||||
|
}
|
||||||
|
|
||||||
let lastFetchedUrl = "";
|
let lastFetchedUrl = "";
|
||||||
|
|
||||||
@@ -43,10 +85,11 @@ async function init() {
|
|||||||
|
|
||||||
const url = urlInput.value.trim();
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
// Si champ vide -> reset total
|
// IF field empty -> total reset
|
||||||
if (!url || url.length < 2) {
|
if (!url || url.length < 2) {
|
||||||
infoDiv.innerHTML = "";
|
infoDiv.innerHTML = "";
|
||||||
infoDiv.classList.remove("visible", "playlist-mode");
|
infoDiv.classList.remove("visible", "playlist-mode");
|
||||||
|
playlistTitleInput.value = "";
|
||||||
lastFetchedUrl = "";
|
lastFetchedUrl = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,7 +101,7 @@ async function init() {
|
|||||||
const data = await fetchVideoInfo(url);
|
const data = await fetchVideoInfo(url);
|
||||||
loaderBox.style.display = "none";
|
loaderBox.style.display = "none";
|
||||||
|
|
||||||
// Gestion des erreurs
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
infoDiv.innerHTML = `
|
infoDiv.innerHTML = `
|
||||||
<div style="
|
<div style="
|
||||||
@@ -70,17 +113,37 @@ async function init() {
|
|||||||
`;
|
`;
|
||||||
infoDiv.classList.add("visible");
|
infoDiv.classList.add("visible");
|
||||||
infoDiv.classList.remove("playlist-mode");
|
infoDiv.classList.remove("playlist-mode");
|
||||||
|
playlistTitleInput.value = "";
|
||||||
loaderBox.style.display = "none";
|
loaderBox.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- PLAYLIST ----------
|
// ---------- PLAYLIST ----------
|
||||||
if (data.type === "playlist") {
|
if (data.type === "playlist") {
|
||||||
|
playlistTitleInput.value = data.title;
|
||||||
infoDiv.classList.add("playlist-mode");
|
infoDiv.classList.add("playlist-mode");
|
||||||
infoDiv.innerHTML = `
|
infoDiv.innerHTML = `
|
||||||
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist Detected: ${data.title}</strong></h3>
|
<div class="playlist-header">
|
||||||
<h3 style="color:var(--video-info-heading-color);"><strong>Video Count: ${data.count}</strong></h3>
|
<div class="playlist-info">
|
||||||
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p>
|
<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>
|
<div id="playlistVideos"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -88,39 +151,58 @@ async function init() {
|
|||||||
|
|
||||||
data.videos.forEach(v => {
|
data.videos.forEach(v => {
|
||||||
const durationStr = v.duration
|
const durationStr = v.duration
|
||||||
? `${Math.floor(v.duration / 60)}m ${(v.duration % 60).toString().padStart(2,"0")}s` : "Inconnue";
|
? `${Math.floor(v.duration / 60)}:${(v.duration % 60).toString().padStart(2,"0")}` : "--:--";
|
||||||
|
|
||||||
const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
|
const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
|
||||||
|
|
||||||
listDiv.innerHTML += `
|
listDiv.innerHTML += `
|
||||||
<div style="margin-bottom:12px;">
|
<div class="playlist-video-card">
|
||||||
<img src="${v.thumbnail}" width="160" alt="Thumbnail">
|
<div class="video-thumbnail-wrapper">
|
||||||
<p><strong>${v.title}</strong></p>
|
<img src="${v.thumbnail}" alt="${v.title}" class="video-thumbnail">
|
||||||
<p>Duration : ${durationStr}</p>
|
<span class="video-duration">${durationStr}</span>
|
||||||
<p><a href="${videoUrl}" target="_blank">URL</a>
|
</div>
|
||||||
<button class="copy-btn" data-url="${videoUrl}">📋</button>
|
<div class="video-content">
|
||||||
</p>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestion du bouton copier
|
// Copy Button
|
||||||
listDiv.addEventListener("click", (event) => {
|
listDiv.addEventListener("click", (event) => {
|
||||||
if (event.target.classList.contains("copy-btn")) {
|
if (event.target.classList.contains("copy-btn") || event.target.closest(".copy-btn")) {
|
||||||
const btn = event.target;
|
const btn = event.target.closest(".copy-btn") || event.target;
|
||||||
if (btn.disabled) return;
|
if (btn.disabled) return;
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const url = btn.dataset.url;
|
const url = btn.dataset.url;
|
||||||
navigator.clipboard.writeText(url)
|
navigator.clipboard.writeText(url)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const original = btn.textContent;
|
const original = btn.innerHTML;
|
||||||
|
|
||||||
btn.style.opacity = 0;
|
btn.style.opacity = 0;
|
||||||
btn.style.transform = "scale(0.7)";
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.textContent = "✅";
|
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.opacity = 1;
|
||||||
btn.style.transform = "scale(1)";
|
btn.style.transform = "scale(1)";
|
||||||
|
|
||||||
@@ -129,7 +211,7 @@ async function init() {
|
|||||||
btn.style.transform = "scale(0.7)";
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.textContent = original;
|
btn.innerHTML = original;
|
||||||
btn.style.opacity = 1;
|
btn.style.opacity = 1;
|
||||||
btn.style.transform = "scale(1)";
|
btn.style.transform = "scale(1)";
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -140,10 +222,12 @@ async function init() {
|
|||||||
}, 300);
|
}, 300);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
const original = btn.textContent;
|
const original = btn.innerHTML;
|
||||||
btn.textContent = "❌";
|
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(() => {
|
setTimeout(() => {
|
||||||
btn.textContent = original;
|
btn.innerHTML = original;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
@@ -155,6 +239,7 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
infoDiv.classList.remove("playlist-mode");
|
infoDiv.classList.remove("playlist-mode");
|
||||||
|
playlistTitleInput.value = "";
|
||||||
|
|
||||||
// ---------- VIDEO NORMALE ----------
|
// ---------- VIDEO NORMALE ----------
|
||||||
const durationStr = data.duration
|
const durationStr = data.duration
|
||||||
@@ -184,8 +269,8 @@ async function init() {
|
|||||||
infoDiv.classList.add("visible");
|
infoDiv.classList.add("visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
});
|
||||||
|
|
||||||
};
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
@@ -1,25 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* DOM elements used for download progress UI rendering.
|
||||||
|
* These elements are updated in real-time via SSE streams.
|
||||||
|
*/
|
||||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
const progressBar = document.getElementById("downloadProgress");
|
const progressBar = document.getElementById("downloadProgress");
|
||||||
const progressBarText = document.getElementById("downloadProgressText")
|
const progressBarText = document.getElementById("downloadProgressText");
|
||||||
|
|
||||||
const speedElement = document.getElementById("downloadSpeedText");
|
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 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() {
|
function startProgress() {
|
||||||
progressWrapper.style.display = "block";
|
|
||||||
progressBar.style.width = "0%";
|
progressBar.style.width = "0%";
|
||||||
progressBarText.style.display = "block";
|
|
||||||
progressBarText.innerHTML = "0%";
|
progressBarText.innerHTML = "0%";
|
||||||
|
|
||||||
speedElement.style.display = "block";
|
|
||||||
speedElement.innerHTML = "0 Mib/s";
|
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) {
|
function updateProgress(percent) {
|
||||||
|
if (percent > 0 && progressWrapper.style.display !== "block") {
|
||||||
|
showProgress();
|
||||||
|
}
|
||||||
|
|
||||||
progressBar.style.width = `${percent}%`;
|
progressBar.style.width = `${percent}%`;
|
||||||
progressBarText.innerHTML = `${percent}%`;
|
progressBarText.innerHTML = `${percent}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully resets the progress UI and hides all download indicators.
|
||||||
|
* Called after completion or cancellation.
|
||||||
|
*/
|
||||||
function resetProgress() {
|
function resetProgress() {
|
||||||
progressBar.style.width = "0%";
|
progressBar.style.width = "0%";
|
||||||
progressBarText.textContent = "";
|
progressBarText.textContent = "";
|
||||||
@@ -28,10 +68,26 @@ function resetProgress() {
|
|||||||
|
|
||||||
speedElement.textContent = "0 Mib/s";
|
speedElement.textContent = "0 Mib/s";
|
||||||
speedElement.style.display = "none";
|
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";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connexion SSE
|
/**
|
||||||
|
* Main SSE stream handling download progress updates.
|
||||||
|
* Supports:
|
||||||
|
* - reset signal
|
||||||
|
* - done signal
|
||||||
|
* - percentage updates
|
||||||
|
*/
|
||||||
const evtSource = new EventSource("/download/progress");
|
const evtSource = new EventSource("/download/progress");
|
||||||
|
|
||||||
evtSource.onmessage = e => {
|
evtSource.onmessage = e => {
|
||||||
if (e.data === "reset") {
|
if (e.data === "reset") {
|
||||||
startProgress();
|
startProgress();
|
||||||
@@ -40,24 +96,41 @@ evtSource.onmessage = e => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e.data === "done") {
|
if (e.data === "done") {
|
||||||
resetProgress();
|
setTimeout(() => {
|
||||||
window.electronAPI.setProgress(-1);
|
resetProgress();
|
||||||
|
window.electronAPI.setProgress(-1);
|
||||||
|
}, 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const percent = parseFloat(e.data);
|
const percent = parseFloat(e.data);
|
||||||
|
|
||||||
if (!isNaN(percent)) {
|
if (!isNaN(percent)) {
|
||||||
updateProgress(percent);
|
updateProgress(percent);
|
||||||
window.electronAPI.setProgress(percent); // update barre des tâches
|
window.electronAPI.setProgress(percent);
|
||||||
if (percent >= 100) setTimeout(() => {
|
|
||||||
resetProgress();
|
|
||||||
window.electronAPI.setProgress(-1); // retire la barre
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE stream: download speed updates (e.g. "5.2 MiB/s").
|
||||||
|
*/
|
||||||
speedEvt.onmessage = e => {
|
speedEvt.onmessage = e => {
|
||||||
speedElement.style.display = "block";
|
speedElement.style.display = "block";
|
||||||
speedElement.textContent = e.data; // ex: "5.2MiB/s"
|
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;
|
||||||
};
|
};
|
||||||
121
public/script/reportBug.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
class BugReportModal {
|
||||||
|
constructor() {
|
||||||
|
this.modal = null;
|
||||||
|
this.form = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.modal = document.createElement("div");
|
||||||
|
this.modal.className = "bug-modal-overlay";
|
||||||
|
this.modal.style.cssText = "display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); justify-content:center; align-items:center; z-index:99999; font-family:sans-serif;";
|
||||||
|
|
||||||
|
this.modal.innerHTML = `
|
||||||
|
<div class="bug-modal-content" style="background:#14141491 !important; color:#ffffff !important; padding:24px !important; border-radius:10px !important; width:420px !important; box-shadow: 0 10px 25px rgba(0,0,0,0.8) !important; display:flex !important; flex-direction:column !important; gap:16px !important; border: 1px solid #333 !important; box-sizing: border-box !important;">
|
||||||
|
<h3 style="margin:0 !important; font-size:1.2rem !important; font-weight:600 !important; color:#ffffff !important;">Report a Bug</h3>
|
||||||
|
<form id="bugReportForm" style="display:flex !important; flex-direction:column !important; gap:12px !important; width:unset !important; margin:0 !important; background:transparent !important; box-shadow: unset !important;">
|
||||||
|
<div style="display:flex !important; flex-direction:column !important; gap:4px !important;">
|
||||||
|
<label style="font-size:0.85rem !important; color:#aaaaaa !important;">Title <span style="color: red; font-weight: bold" >*</span> </label>
|
||||||
|
<input type="text" id="bugTitle" required style="background:#2a2a2a !important; border:1px solid #444 !important; color:#ffffff !important; border-radius:6px !important; padding:8px 10px !important; outline:none !important; width:100% !important; box-sizing: border-box !important;" />
|
||||||
|
</div>
|
||||||
|
<div style="display:flex !important; flex-direction:column !important; gap:4px !important;">
|
||||||
|
<label style="font-size:0.85rem !important; color:#aaaaaa !important;">Description <span style="color: red; font-weight: bold" >*</span></label>
|
||||||
|
<textarea id="bugDescription" placeholder="Provide some details about the bug" required rows="4" style="background:#2a2a2a !important; border:1px solid #444 !important; color:#ffffff !important; border-radius:6px !important; padding:8px 10px !important; outline:none !important; resize:vertical !important; width:100% !important; box-sizing: border-box !important;"></textarea>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex !important; flex-direction:column !important; gap:4px !important;">
|
||||||
|
<label style="font-size:0.85rem !important; color:#aaaaaa !important;">Include Logs From Today</label>
|
||||||
|
<select id="bugIncludeLogs" style="background:#2a2a2a !important; border:1px solid #444 !important; color:#ffffff !important; border-radius:6px !important; padding:8px 10px !important; outline:none !important; width:100% !important; box-sizing: border-box !important;">
|
||||||
|
<option value="yes" style="background:#2a2a2a !important; color:#ffffff !important;">Yes</option>
|
||||||
|
<option value="no" style="background:#2a2a2a !important; color:#ffffff !important;">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex !important; justify-content:flex-end !important; gap:8px !important; margin-top:8px !important;">
|
||||||
|
<button type="button" id="bugCancelBtn" style="background:#333333 !important; border:none !important; color:#cccccc !important; padding:8px 14px !important; border-radius:6px !important; cursor:pointer !important;">Cancel</button>
|
||||||
|
<button type="submit" style="background:#4f46e5 !important; border:none !important; color:#ffffff !important; padding:8px 16px !important; border-radius:6px !important; cursor:pointer !important; font-weight:500 !important;">Send</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(this.modal);
|
||||||
|
this.form = this.modal.querySelector("#bugReportForm");
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
const titleInput = this.modal.querySelector("#bugTitle");
|
||||||
|
const descInput = this.modal.querySelector("#bugDescription");
|
||||||
|
|
||||||
|
titleInput.addEventListener("input", () => {
|
||||||
|
localStorage.setItem("draft_bug_title", titleInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
descInput.addEventListener("input", () => {
|
||||||
|
localStorage.setItem("draft_bug_desc", descInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.modal.querySelector("#bugCancelBtn").addEventListener("click", () => {
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.modal.addEventListener("click", (e) => {
|
||||||
|
if (e.target === this.modal) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = {
|
||||||
|
title: titleInput.value,
|
||||||
|
description: descInput.value,
|
||||||
|
includeLogs: this.modal.querySelector("#bugIncludeLogs").value
|
||||||
|
};
|
||||||
|
|
||||||
|
window.electronAPI.logInfo("Submitting:", data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await window.electronAPI.sendReport(data);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
localStorage.removeItem("draft_bug_title");
|
||||||
|
localStorage.removeItem("draft_bug_desc");
|
||||||
|
this.form.reset();
|
||||||
|
this.close();
|
||||||
|
window.showSuccess("Bug report sent successfully!");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
window.showError("Failed to send bug report.");
|
||||||
|
window.electronAPI.logError("Failed to send bug report:", err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState() {
|
||||||
|
const savedTitle = localStorage.getItem("draft_bug_title");
|
||||||
|
const savedDesc = localStorage.getItem("draft_bug_desc");
|
||||||
|
|
||||||
|
if (savedTitle) {
|
||||||
|
this.modal.querySelector("#bugTitle").value = savedTitle;
|
||||||
|
}
|
||||||
|
if (savedDesc) {
|
||||||
|
this.modal.querySelector("#bugDescription").value = savedDesc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.loadState();
|
||||||
|
this.modal.style.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.modal.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.bugModal = new BugReportModal();
|
||||||
|
|
||||||
|
document.getElementById("report-bug-btn").addEventListener("click", () => {
|
||||||
|
window.bugModal.open();
|
||||||
|
});
|
||||||
@@ -1,50 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* Settings panel DOM elements used to control application feature flags
|
||||||
|
* and configuration UI interactions.
|
||||||
|
*/
|
||||||
const settingsPanel = document.getElementById("settings-panel");
|
const settingsPanel = document.getElementById("settings-panel");
|
||||||
const settingsModal = document.querySelector(".settings-modal");
|
const settingsModal = document.querySelector(".settings-modal");
|
||||||
|
|
||||||
const settingsButton = document.getElementById("settings-button");
|
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;
|
let isOpen = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles visibility of the settings panel when clicking the settings button.
|
||||||
|
*/
|
||||||
settingsButton.addEventListener("click", () => {
|
settingsButton.addEventListener("click", () => {
|
||||||
const isOpen = settingsPanel.style.display === "block";
|
settingsPanel.classList.toggle("open");
|
||||||
settingsPanel.style.display = isOpen ? "none" : "block";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the settings panel and resets internal state.
|
||||||
|
*/
|
||||||
function closePanel() {
|
function closePanel() {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
settingsPanel.style.display = "none";
|
settingsPanel.classList.remove("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
// charge les features
|
/**
|
||||||
|
* 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() {
|
async function loadSettings() {
|
||||||
const features = await window.electronAPI.getFeatures();
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => {
|
document
|
||||||
const key = el.dataset.key;
|
.querySelectorAll(".settings-section input, .settings-section select")
|
||||||
if (features[key] !== undefined) {
|
.forEach((el) => {
|
||||||
if (el.type === "checkbox") el.checked = features[key];
|
const key = el.dataset.key;
|
||||||
else if (el.tagName === "SELECT") el.value = features[key];
|
if (!key) return;
|
||||||
}
|
|
||||||
|
|
||||||
el.addEventListener("change", () => {
|
if (features[key] !== undefined) {
|
||||||
let value = el.type === "checkbox" ? el.checked : el.value;
|
if (el.type === "checkbox") el.checked = features[key];
|
||||||
window.electronAPI.setFeature(key, value);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ouvrir le JSON
|
/**
|
||||||
document.getElementById("open-json-btn").addEventListener("click", () => {
|
* Opens configuration JSON file via Electron topbar API.
|
||||||
window.topbarAPI.openConfig(); // ton IPC existant
|
*/
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
/* clic sur l'overlay = fermer */
|
* Initializes settings UI by loading backend feature flags.
|
||||||
settingsPanel.addEventListener("click", closePanel);
|
*/
|
||||||
|
loadSettings();
|
||||||
/* clic dans la modal = stop propagation */
|
|
||||||
settingsModal.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
loadSettings();
|
|
||||||
85
public/script/toast.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* 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: "check_circle",
|
||||||
|
error: "error",
|
||||||
|
warning: "warning",
|
||||||
|
info: "info"
|
||||||
|
};
|
||||||
|
|
||||||
|
const icon = icons[type] || icons.info;
|
||||||
|
|
||||||
|
toast.style.setProperty("--toast-duration", `${duration}ms`);
|
||||||
|
toast.innerHTML = `
|
||||||
|
<span class="toast-icon material-icons-round">${icon}</span>
|
||||||
|
<span class="toast-message">${message}</span>
|
||||||
|
<button class="toast-close" title="Close">×</button>
|
||||||
|
<div class="toast-progress"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -1,34 +1,59 @@
|
|||||||
// Ajout des listeners pour la topbar
|
/**
|
||||||
function setupTopbarListeners() {
|
* Initializes topbar button event listeners and binds them
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
* to the Electron preload API exposed in `window.topbarAPI`.
|
||||||
const { topbarAPI } = window;
|
*
|
||||||
if (!topbarAPI) return;
|
* This function is safe to call once DOM is ready.
|
||||||
const minBtn = document.getElementById('minimize-btn');
|
* It silently aborts if the API or elements are missing.
|
||||||
const maxBtn = document.getElementById('maximize-btn');
|
*/
|
||||||
const closeBtn = document.getElementById('close-btn');
|
export function initTopBar() {
|
||||||
const devtoolsBtn = document.getElementById('devtools-btn');
|
const { topbarAPI } = window;
|
||||||
const logsBtn = document.getElementById('logs-btn');
|
if (!topbarAPI) return;
|
||||||
const websiteBtn = document.getElementById('website-btn');
|
|
||||||
const wikiBtn = document.getElementById('wiki-btn');
|
const minBtn = document.getElementById("minimize-btn");
|
||||||
const configBtn = document.getElementById('config-btn');
|
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||||
|
|
||||||
|
const maxBtn = document.getElementById("maximize-btn");
|
||||||
|
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
||||||
|
|
||||||
|
const closeBtn = document.getElementById("close-btn");
|
||||||
|
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
||||||
|
|
||||||
|
const devtoolsBtn = document.getElementById("devtools-btn");
|
||||||
|
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
||||||
|
|
||||||
|
const logsBtn = document.getElementById("logs-btn");
|
||||||
|
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
||||||
|
|
||||||
|
const websiteBtn = document.getElementById("website-btn");
|
||||||
|
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
||||||
|
|
||||||
|
const wikiBtn = document.getElementById("wiki-btn");
|
||||||
|
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
||||||
|
|
||||||
|
const workshopBtn = document.getElementById("workshop-btn");
|
||||||
|
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
||||||
|
|
||||||
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
|
||||||
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
|
||||||
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
|
||||||
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
|
||||||
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
|
||||||
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
|
||||||
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
|
||||||
if (configBtn) configBtn.onclick = () => topbarAPI.openConfig();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTopbarListeners(); // IF it put it the if check. It don't work. Why ?
|
async function attachListeners(){
|
||||||
|
try{
|
||||||
|
const featuresList = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
|
if (!featuresList.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";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load layout features for topbar:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const features = await window.electronAPI.getFeatures();
|
}
|
||||||
|
|
||||||
if(!features.customTopBar){
|
initTopBar()
|
||||||
document.getElementById("topbar").style.display = "none";
|
document.addEventListener("DOMContentLoaded", attachListeners)
|
||||||
document.getElementById("container").style.marginTop = "0";
|
|
||||||
document.getElementById("theme-switcher").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>
|
||||||
128
public/styles/components/autoUpdate.css
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
:root {
|
||||||
|
--border-accent: #0ea5e966;
|
||||||
|
--bg-accent: #0ea5e915;
|
||||||
|
--fill-accent: #0ea5e9;
|
||||||
|
--text-accent: #0369a1;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateAvailable {
|
||||||
|
display: none;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 420px;
|
||||||
|
position: absolute;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 5%;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-banner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0.5px solid var(--border-accent);
|
||||||
|
background: var(--bg-accent);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrap {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--fill-accent);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrap i {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--fill-accent);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update:hover:not(:disabled) {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-track {
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: var(--border-accent);
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: var(--fill-accent);
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: var(--form-bg-color);
|
background: var(--form-bg-color);
|
||||||
border: 2px solid var(--mp3-text-color);
|
border: 2px solid var(--audio-only-label-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--mp3-text-color);
|
color: var(--audio-only-label-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
animation: pulse 1.2s infinite;
|
animation: pulse 1.2s infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
|
/* 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 */
|
/* Wrapper de la barre de téléchargement */
|
||||||
#downloadProgressWrapper {
|
#downloadProgressWrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background-color: #444;
|
background-color: var(--progress-bar-bg-color, #cccccc);
|
||||||
border-radius: 6px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 10px auto;
|
margin: 0;
|
||||||
display: none;
|
display: none;
|
||||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Barre de progression */
|
/* Barre de progression */
|
||||||
#downloadProgress {
|
#downloadProgress {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0%;
|
width: 0%;
|
||||||
background-color: var(--video-info-link-color, #4caf50);
|
background-color: var(--progress-bar-fill-color, #007bff);
|
||||||
border-radius: 6px 0 0 6px;
|
border-radius: 10px;
|
||||||
transition: width 0.2s ease;
|
transition: width 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +37,7 @@
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
content: "";
|
content: "";
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive pour mobile */
|
/* Responsive pour mobile */
|
||||||
@@ -39,3 +51,39 @@
|
|||||||
font-size: 0.7rem;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
162
public/styles/components/toast.css
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/* Toast Container */
|
||||||
|
#toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 140px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toast {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background-color: rgba(30, 30, 30, 0.9);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #f3f4f6;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 320px;
|
||||||
|
min-width: 220px;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.2);
|
||||||
|
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
pointer-events: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Icon */
|
||||||
|
.toast-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success .toast-icon {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error .toast-icon {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning .toast-icon {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info .toast-icon {
|
||||||
|
color: #0ea5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Message */
|
||||||
|
.toast-message {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Close Button */
|
||||||
|
.toast-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-progress {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 3px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
animation: progress var(--toast-duration, 4000ms) linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success .toast-progress {
|
||||||
|
background-color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error .toast-progress {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress {
|
||||||
|
from {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.removing {
|
||||||
|
animation: slideOut 0.25s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#toast-container {
|
||||||
|
top: auto;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
max-width: none;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
top: 60px;
|
top: 60px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
z-index: 15;
|
||||||
background-color: var(--settings-button-bg-color);
|
background-color: var(--settings-button-bg-color);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -40,15 +41,26 @@
|
|||||||
width: 40vw;
|
width: 40vw;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
background-color: var(--settings-bg-primary-color);
|
background-color: var(--settings-modal-bg-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||||
display: none;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
animation: slideIn 0.2s ease-out;
|
/*animation: slideIn 0.2s ease-out; */
|
||||||
color: var(--settings-text-color);
|
color: var(--settings-text-color);
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(20px);
|
||||||
|
transition: opacity 0.2s ease-out, transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel.open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-panel::-webkit-scrollbar {
|
.settings-panel::-webkit-scrollbar {
|
||||||
@@ -89,7 +101,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
background: var(--settings-bg-secondary-color);
|
background: var(--settings-section-bg-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
@@ -190,8 +202,18 @@ details summary {
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: var(--form-button-bg-color);
|
background-color: var(--open-json-button-bg-color);
|
||||||
color: #fff;
|
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 {
|
#experimental-args {
|
||||||
@@ -222,6 +244,33 @@ details summary {
|
|||||||
opacity: 1;
|
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 */
|
/* ANIMATION */
|
||||||
@keyframes popIn {
|
@keyframes popIn {
|
||||||
from { transform: scale(0.95); opacity: 0; }
|
from { transform: scale(0.95); opacity: 0; }
|
||||||
@@ -231,4 +280,9 @@ details summary {
|
|||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from { transform: translateX(20px); opacity: 0; }
|
from { transform: translateX(20px); opacity: 0; }
|
||||||
to { transform: translateX(0); opacity: 1; }
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
@@ -2,20 +2,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1100;
|
|
||||||
|
|
||||||
height: 48px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
background: #1f1f1f98;
|
background: #1f1f1f98;
|
||||||
color: #eaeaea;
|
color: #eaeaea;
|
||||||
|
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* LEFT - custom buttons */
|
/* LEFT - custom buttons */
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -69,80 +69,293 @@
|
|||||||
|
|
||||||
/* --- Playlist Mode --- */
|
/* --- Playlist Mode --- */
|
||||||
#videoInfo.playlist-mode {
|
#videoInfo.playlist-mode {
|
||||||
max-width: 900px;
|
max-width: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style du bloc "Playlist détectée" */
|
/* Playlist Header */
|
||||||
#videoInfo.playlist-mode > p {
|
.playlist-header {
|
||||||
text-align: center;
|
display: flex;
|
||||||
font-size: 1.1rem;
|
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;
|
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 */
|
/* Layout liste des vidéos */
|
||||||
#playlistVideos {
|
#playlistVideos {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 18px;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Un item de playlist */
|
/* Carte vidéo playlist */
|
||||||
#playlistVideos > div {
|
.playlist-video-card {
|
||||||
background: var(--playlist-background-color);
|
background: var(--playlist-background-color);
|
||||||
border-radius: 10px;
|
border-radius: 14px;
|
||||||
padding: 10px;
|
overflow: hidden;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
|
||||||
text-align: center;
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#playlistVideos > div p {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: center;
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
flex-wrap: wrap;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#videoInfo.playlist-mode {
|
.playlist-video-card::before {
|
||||||
max-width: 95%;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlistVideos > div:hover {
|
.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);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlistVideos img {
|
.video-duration {
|
||||||
width: 100%;
|
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;
|
border-radius: 8px;
|
||||||
margin-bottom: 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 {
|
.copy-btn {
|
||||||
all: unset;
|
background: rgba(255, 255, 255, 0.06);
|
||||||
background: transparent;
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 4px;
|
padding: 0.5rem;
|
||||||
vertical-align: middle;
|
border-radius: 8px;
|
||||||
transition: transform 0.1s ease, color 0.1s ease, opacity 0.15s ease;
|
transition: all 0.2s ease;
|
||||||
font-size: 1em;
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlistVideos > div p a{
|
.copy-btn svg {
|
||||||
text-decoration: none;
|
width: 18px;
|
||||||
color: var(--video-info-link-color);
|
height: 18px;
|
||||||
|
color: var(--video-info-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlistVideos > div p a:hover{
|
.copy-btn:hover {
|
||||||
text-decoration: underline;
|
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;
|
||||||
@@ -152,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,29 +1,42 @@
|
|||||||
/* Layout */
|
/* Layout */
|
||||||
@import url("layout/form.css");
|
@import url("layout/topbar.css");
|
||||||
@import url("layout/settingsPanel.css");
|
|
||||||
@import url("layout/container.css");
|
|
||||||
@import url("layout/header.css");
|
@import url("layout/header.css");
|
||||||
|
@import url("layout/form.css");
|
||||||
|
@import url("layout/container.css");
|
||||||
|
@import url("layout/settingsPanel.css");
|
||||||
@import url("layout/videoinfo.css");
|
@import url("layout/videoinfo.css");
|
||||||
|
|
||||||
/* Components */
|
/* Components */
|
||||||
@import url("components/checkbox.css");
|
@import url("components/checkbox.css");
|
||||||
@import url("components/loader.css");
|
@import url("components/loader.css");
|
||||||
|
|
||||||
@import url("theme/themeimport.css");
|
|
||||||
@import url("components/editpathbutton.css");
|
@import url("components/editpathbutton.css");
|
||||||
@import url("components/progressBar.css");
|
@import url("components/progressBar.css");
|
||||||
|
@import url("components/toast.css");
|
||||||
|
@import url("components/autoUpdate.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;
|
||||||
@@ -34,43 +47,45 @@ html,body{
|
|||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Because why not */
|
/* Scrollbar pour main */
|
||||||
#reference{
|
main::-webkit-scrollbar {
|
||||||
position: fixed;
|
|
||||||
pointer-events: none;
|
|
||||||
bottom:0vh;
|
|
||||||
left: 10px;
|
|
||||||
font-size: 1em;
|
|
||||||
color: #b0b0b02f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar globale pour Chromium/Electron */
|
|
||||||
body::-webkit-scrollbar {
|
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::-webkit-scrollbar-track {
|
main::-webkit-scrollbar-track {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::-webkit-scrollbar-thumb {
|
main::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
@@ -78,36 +93,32 @@ body::-webkit-scrollbar-thumb {
|
|||||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::-webkit-scrollbar-thumb:hover {
|
main::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.35);
|
background-color: rgba(255, 255, 255, 0.35);
|
||||||
transform: scaleX(1.05);
|
transform: scaleX(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Thème clair */
|
/* Thème clair */
|
||||||
body.light::-webkit-scrollbar-track {
|
body.light main::-webkit-scrollbar-track {
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light::-webkit-scrollbar-thumb {
|
body.light main::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light::-webkit-scrollbar-thumb:hover {
|
body.light main::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.45);
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container{
|
.container{
|
||||||
margin-top: 50px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header p{
|
#subtitle{
|
||||||
color: var(--subtitle-color);
|
color: var(--subtitle-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-inline{
|
|
||||||
color: var(--mp3-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
body,
|
||||||
.container,
|
.container,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
body.chirac {
|
|
||||||
|
|
||||||
background-image: url('../../assets/images/Chirac.png');
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
background-attachment: fixed;
|
|
||||||
|
|
||||||
|
|
||||||
--background-color: rgba(255, 255, 255, 0.85);
|
|
||||||
--default-text-color: #002395;
|
|
||||||
--subtitle-color: #EF4135;
|
|
||||||
--mp3-text-color: #002395;
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: rgba(255, 255, 255, 0.9);
|
|
||||||
--form-input-bg-color: #f5f5f5;
|
|
||||||
--form-input-border-color: #002395;
|
|
||||||
--form-input-border-focus-color: #EF4135;
|
|
||||||
--form-input-text-color: #002395;
|
|
||||||
--form-input-placeholder-color: #999999;
|
|
||||||
--form-button-bg-color: #002395;
|
|
||||||
--form-button-text-color: #ffffff;
|
|
||||||
--form-button-bg-hover-color: #EF4135;
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #cccccc;
|
|
||||||
--checkbox-bg-checked: #002395;
|
|
||||||
--checkbox-checkmark-border-color: #ffffff;
|
|
||||||
--checkbox-pulse-color: rgba(0, 35, 149, 0.5);
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #002395;
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: rgba(255, 255, 255, 0.9);
|
|
||||||
--video-info-text-color: #000000;
|
|
||||||
--video-info-heading-color: #EF4135;
|
|
||||||
--video-info-list-color: #444444;
|
|
||||||
--video-info-list-strong-color: #000000;
|
|
||||||
--video-info-link-color: #002395;
|
|
||||||
--video-info-link-hover-color: #EF4135;
|
|
||||||
|
|
||||||
/* Playlist Color */
|
|
||||||
--playlist-background-color: #d1d1d1ca;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #00196a;
|
|
||||||
--settings-button-text-color: #dcdcdc;
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #0b0528;
|
|
||||||
--settings-bg-secondary-color: #1c1055;
|
|
||||||
|
|
||||||
--settings-text-color: #cecece;
|
|
||||||
--settings-subtitle-color: #adadad;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Je serai le président de tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Mes chers compatriotes
|
|
||||||
Je serai le président de tous les Français, de tous les Français
|
|
||||||
Je mesure la difficulté de la tâche qui nous attend
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Je veux un État, je veux les Français
|
|
||||||
Je veux un État, je veux
|
|
||||||
Je veux un État-tat-tat-tat vigoureux
|
|
||||||
Un État-tat-tat-tat impartial
|
|
||||||
Un État-tat-tat-tat vigoureux
|
|
||||||
Un État-tat-tat-tat exigeant
|
|
||||||
Un État-tat-tat-tat
|
|
||||||
Je serai le président de tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Tous les Français
|
|
||||||
Un État qui n'isole pas, tous, pas, pas
|
|
||||||
Tous, pas, tous, pas, pas
|
|
||||||
Tous, pas, tous, pas, pas (les Français)
|
|
||||||
Tous, tous, pas, tous, tous, pas, pas
|
|
||||||
Tous, tous, pas, tous, tous, pas, pas
|
|
||||||
Tous, tous, tous, pas, tous, pas, pas (les Français)
|
|
||||||
Tous, pas, tous, tous, pas, pas
|
|
||||||
Tous, tous, tous, pas, tous, tous, pas, pas
|
|
||||||
Tous, tous, pas, tous, tous, pas, pas
|
|
||||||
Tous, tous, tous-tous, tous-tous, pas, pas
|
|
||||||
Tous, tous-tous, tous-tous, pas, pas
|
|
||||||
Un État-tat-tat-tat vigoureux
|
|
||||||
Un État-tat-tat-tat impartial
|
|
||||||
Un État-tat-tat-tat exigeant
|
|
||||||
Un État-tat-tat-tat
|
|
||||||
Je serai le président de tous les Français
|
|
||||||
|
|
||||||
*/
|
|
||||||
@@ -1,49 +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;
|
|
||||||
|
|
||||||
/* Playlist Color */
|
|
||||||
--playlist-background-color: #303030;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #1e1e1e;
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #1e1e1e;
|
|
||||||
--settings-bg-secondary-color: #2a2a2a;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #aaa;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
body.drift {
|
|
||||||
|
|
||||||
background-image: url('../../assets/images/Drift.jpg');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-attachment: fixed;
|
|
||||||
|
|
||||||
/* Palette principale */
|
|
||||||
--background-color: #0f1218;
|
|
||||||
--default-text-color: #3d4b61;
|
|
||||||
--subtitle-color: #1a1a1a;
|
|
||||||
--mp3-text-color: #c0d4ff;
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #1e222a;
|
|
||||||
--form-input-bg-color: #13171d;
|
|
||||||
--form-input-border-color: #3b4b6d;
|
|
||||||
--form-input-border-focus-color: #6ea8ff;
|
|
||||||
--form-input-text-color: #f0f4ff;
|
|
||||||
--form-input-placeholder-color: #7a8fae;
|
|
||||||
--form-button-bg-color: #2a2979;
|
|
||||||
--form-button-text-color: #ffffff;
|
|
||||||
--form-button-bg-hover-color: #353393;
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #2a2e38;
|
|
||||||
--checkbox-bg-checked: #2a2979;
|
|
||||||
--checkbox-checkmark-border-color: #ffffff;
|
|
||||||
--checkbox-pulse-color: rgba(76,108,255,0.3);
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #ff8e8e;
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #1e222ad0;
|
|
||||||
--video-info-text-color: #e8e8f0;
|
|
||||||
--video-info-heading-color: #9ab6ff;
|
|
||||||
--video-info-list-color: #c0d4ff;
|
|
||||||
--video-info-list-strong-color: #ffffff;
|
|
||||||
--video-info-link-color: #4c6cff;
|
|
||||||
--video-info-link-hover-color: #6c8dff;
|
|
||||||
|
|
||||||
/* Playlist Color */
|
|
||||||
--playlist-background-color: #424b5cc2;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #1e222ace;
|
|
||||||
--settings-button-text-color: #b3b3b4;
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #1e222a;
|
|
||||||
--settings-bg-secondary-color: #272d38;
|
|
||||||
|
|
||||||
--settings-text-color: #bababa;
|
|
||||||
--settings-subtitle-color: #cacaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
body.drift .download-path{
|
|
||||||
color: #ededed;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
body.fanatic{
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #121212;
|
|
||||||
--default-text-color: #eee;
|
|
||||||
--subtitle-color: #FF5900;
|
|
||||||
--mp3-text-color: #ffffff;
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #1e1e1e;
|
|
||||||
--form-input-bg-color: #2b2b2b;
|
|
||||||
--form-input-border-color: #333333;
|
|
||||||
--form-input-border-focus-color: #FF5900;
|
|
||||||
--form-input-text-color: #eeeeee;
|
|
||||||
--form-input-placeholder-color: #666666;
|
|
||||||
--form-button-bg-color: #FF5900;
|
|
||||||
--form-button-text-color: #121212;
|
|
||||||
--form-button-bg-hover-color: #e65500;
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #555555;
|
|
||||||
--checkbox-bg-checked: #ff6600;
|
|
||||||
--checkbox-checkmark-border-color: #eee;
|
|
||||||
--checkbox-pulse-color: rgba(255, 102, 0, 0.5);
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #FF5900;
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #222222;
|
|
||||||
--video-info-text-color: #eee;
|
|
||||||
--video-info-heading-color: #FF5900;
|
|
||||||
--video-info-list-color: #ccc;
|
|
||||||
--video-info-list-strong-color: #fff;
|
|
||||||
--video-info-link-color: #FF5900;
|
|
||||||
--video-info-link-hover-color: #e65500;
|
|
||||||
|
|
||||||
/* Playlist Color */
|
|
||||||
--playlist-background-color: #2b2b2b;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #1e1e1e;
|
|
||||||
--settings-button-text-color: #dcdcdc;
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #1e1e1e;
|
|
||||||
--settings-bg-secondary-color: #2d2d2d;
|
|
||||||
|
|
||||||
--settings-text-color: #cecece;
|
|
||||||
--settings-subtitle-color: #adadad;
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
body.light {
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #dadada;
|
|
||||||
--default-text-color: #222222;
|
|
||||||
--subtitle-color: #007bff;
|
|
||||||
--mp3-text-color: #000000;
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #ffffff;
|
|
||||||
--form-input-bg-color: #fafafa;
|
|
||||||
--form-input-border-color: #cccccc;
|
|
||||||
--form-input-border-focus-color: #007bff;
|
|
||||||
--form-input-text-color: #333333;
|
|
||||||
--form-input-placeholder-color: #888888;
|
|
||||||
--form-button-bg-color: #007bff;
|
|
||||||
--form-button-text-color: #ffffff;
|
|
||||||
--form-button-bg-hover-color: #0056b3;
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #cccccc;
|
|
||||||
--checkbox-bg-checked: #007bff;
|
|
||||||
--checkbox-checkmark-border-color: #ffffff;
|
|
||||||
--checkbox-pulse-color: rgba(0, 123, 255, 0.4);
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #007bff;
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #ffffff;
|
|
||||||
--video-info-text-color: #222222;
|
|
||||||
--video-info-heading-color: #007bff;
|
|
||||||
--video-info-list-color: #444444;
|
|
||||||
--video-info-list-strong-color: #000000;
|
|
||||||
--video-info-link-color: #007bff;
|
|
||||||
--video-info-link-hover-color: #0056b3;
|
|
||||||
|
|
||||||
/* Playlist Color */
|
|
||||||
--playlist-background-color: #c5c5c5b2;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #e4e4e4;
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #cbcbcb;
|
|
||||||
--settings-bg-secondary-color: #cbcbcb;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #656565;
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
body.neon {
|
|
||||||
|
|
||||||
background: (url("../../assets/images/drift.jpg"));
|
|
||||||
background-image: linear-gradient(135deg, #0f0c29, #302b63, #24243e); /* dégradé sombre violet/bleu */
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-attachment: fixed;
|
|
||||||
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #1b1b2f;
|
|
||||||
--default-text-color: #e0e0ff;
|
|
||||||
--subtitle-color: #a3a3ff;
|
|
||||||
--mp3-text-color: #c0c0ff;
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #2c2c4a;
|
|
||||||
--form-input-bg-color: #1f1f3a;
|
|
||||||
--form-input-border-color: #5858f0;
|
|
||||||
--form-input-border-focus-color: #a3a3ff;
|
|
||||||
--form-input-text-color: #ffffff;
|
|
||||||
--form-input-placeholder-color: #8888ff;
|
|
||||||
--form-button-bg-color: #5858f0;
|
|
||||||
--form-button-text-color: #ffffff;
|
|
||||||
--form-button-bg-hover-color: #7a7aff;
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #302b63;
|
|
||||||
--checkbox-bg-checked: #5858f0;
|
|
||||||
--checkbox-checkmark-border-color: #ffffff;
|
|
||||||
--checkbox-pulse-color: rgba(88,88,240,0.3);
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #a3a3ff;
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #2c2c4a;
|
|
||||||
--video-info-text-color: #e0e0ff;
|
|
||||||
--video-info-heading-color: #a3a3ff;
|
|
||||||
--video-info-list-color: #c0c0ff;
|
|
||||||
--video-info-list-strong-color: #ffffff;
|
|
||||||
--video-info-link-color: #5858f0;
|
|
||||||
--video-info-link-hover-color: #7a7aff;
|
|
||||||
|
|
||||||
/* Playlist Color */
|
|
||||||
--playlist-background-color: #3e3e64;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #202038;
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #242454;
|
|
||||||
--settings-bg-secondary-color: #2f2f6f;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #d2d2d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.neon .download-path {
|
|
||||||
color: var(--default-text-color);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
body.nf {
|
|
||||||
|
|
||||||
background-image: url('../../assets/images/NF.jpg');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: 70% center;
|
|
||||||
background-attachment: fixed;
|
|
||||||
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #1a1a1a;
|
|
||||||
--default-text-color: #e6dfd5;
|
|
||||||
--subtitle-color: #f5f0e6;
|
|
||||||
--mp3-text-color: #d8cfc2;
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #2a2a2a;
|
|
||||||
--form-input-bg-color: #222222;
|
|
||||||
--form-input-border-color: #4a4036;
|
|
||||||
--form-input-border-focus-color: #cbb89d;
|
|
||||||
--form-input-text-color: #f0e8dd;
|
|
||||||
--form-input-placeholder-color: #9a8f80;
|
|
||||||
--form-button-bg-color: #cbb89d;
|
|
||||||
--form-button-text-color: #1a1a1a;
|
|
||||||
--form-button-bg-hover-color: #b7a588;
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #4a4036;
|
|
||||||
--checkbox-bg-checked: #cbb89d;
|
|
||||||
--checkbox-checkmark-border-color: #1a1a1a;
|
|
||||||
--checkbox-pulse-color: rgba(203,184,157,0.3);
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #cbb89d;
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #2a2a2a;
|
|
||||||
--video-info-text-color: #e6dfd5;
|
|
||||||
--video-info-heading-color: #f5f0e6;
|
|
||||||
--video-info-list-color: #d8cfc2;
|
|
||||||
--video-info-list-strong-color: #ffffff;
|
|
||||||
--video-info-link-color: #cbb89d;
|
|
||||||
--video-info-link-hover-color: #b7a588;
|
|
||||||
|
|
||||||
/* Playlist Color */
|
|
||||||
--playlist-background-color: #4f4f4f;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #2a2a2a;
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #2a2a2a;
|
|
||||||
--settings-bg-secondary-color: #313131;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #7a7979;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.nf .download-path {
|
|
||||||
color: var(--default-text-color);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
body.songbird {
|
|
||||||
|
|
||||||
background-image: url('../../assets/images/Songbird.webp');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center center;
|
|
||||||
background-attachment: fixed;
|
|
||||||
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #0a0a0a; /* noir profond */
|
|
||||||
--default-text-color: #ff2a2a; /* rouge néon */
|
|
||||||
--subtitle-color: #f2f2f2; /* rouge sombre */
|
|
||||||
--mp3-text-color: #ff4d4d; /* rouge un peu plus clair */
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: rgba(20,0,0,0.85); /* noir semi-transparent avec rouge */
|
|
||||||
--form-input-bg-color: #1a0a0a;
|
|
||||||
--form-input-border-color: #ff2a2a;
|
|
||||||
--form-input-border-focus-color: #b30000;
|
|
||||||
--form-input-text-color: #ffffff;
|
|
||||||
--form-input-placeholder-color: #950404;
|
|
||||||
--form-button-bg-color: #b30000;
|
|
||||||
--form-button-text-color: #ffffff;
|
|
||||||
--form-button-bg-hover-color: #ff2a2a;
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #630000;
|
|
||||||
--checkbox-bg-checked: #ff2a2a;
|
|
||||||
--checkbox-checkmark-border-color: #ffffff;
|
|
||||||
--checkbox-pulse-color: rgba(255,42,42,0.3);
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #ff2a2a;
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: rgba(30,0,0,0.9);
|
|
||||||
--video-info-text-color: #ff4d4d;
|
|
||||||
--video-info-heading-color: #ff2a2a;
|
|
||||||
--video-info-list-color: #b30000;
|
|
||||||
--video-info-list-strong-color: #ffffff;
|
|
||||||
--video-info-link-color: #ff2a2a;
|
|
||||||
--video-info-link-hover-color: #b30000;
|
|
||||||
|
|
||||||
/* Playlist Color */
|
|
||||||
--playlist-background-color: #3f0000;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: rgba(29, 0, 0, 0.784);
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #121212;
|
|
||||||
--settings-bg-secondary-color: #1a1a1a;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #7a7979;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.cyberpunk .download-path {
|
|
||||||
color: var(--default-text-color);
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
body.spicy {
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #121212; /* fond noir profond */
|
|
||||||
--default-text-color: #ffffff; /* texte clair */
|
|
||||||
--subtitle-color: #f87918; /* accent rouge piment */
|
|
||||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #1a1a1a; /* fond formulaire gris très foncé */
|
|
||||||
--form-input-bg-color: #2b2b2b; /* fond inputs gris foncé */
|
|
||||||
--form-input-border-color: #444444; /* bordure gris */
|
|
||||||
--form-input-border-focus-color: #ff1a1a; /* focus rouge piment */
|
|
||||||
--form-input-text-color: #eeeeee; /* texte inputs */
|
|
||||||
--form-input-placeholder-color: #888888; /* placeholder gris moyen */
|
|
||||||
--form-button-bg-color: #ff1a1a; /* bouton rouge vif */
|
|
||||||
--form-button-text-color: #ffffff; /* texte bouton blanc */
|
|
||||||
--form-button-bg-hover-color: #cc0000; /* hover rouge foncé */
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #555555; /* gris moyen */
|
|
||||||
--checkbox-bg-checked: #ff1a1a; /* rouge piment */
|
|
||||||
--checkbox-checkmark-border-color: #f5f5f5; /* blanc cassé */
|
|
||||||
--checkbox-pulse-color: rgba(255, 26, 26, 0.5); /* pulse rouge translucide */
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #ffffff; /* rouge vif */
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #1a1a1a; /* fond infos gris foncé */
|
|
||||||
--video-info-text-color: #f5f5f5; /* texte clair */
|
|
||||||
--video-info-heading-color: #ff1a1a; /* titres rouges */
|
|
||||||
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
|
||||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
|
||||||
--video-info-link-color: #ff1a1a; /* liens rouges */
|
|
||||||
--video-info-link-hover-color: #cc0000; /* liens hover rouge foncé */
|
|
||||||
|
|
||||||
/* Playlist Color */
|
|
||||||
--playlist-background-color: #353232;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #1a1a1a;
|
|
||||||
--settings-button-text-color: #dcdcdc;
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #1a1a1a;
|
|
||||||
--settings-bg-secondary-color: #292929;
|
|
||||||
|
|
||||||
--settings-text-color: #cecece;
|
|
||||||
--settings-subtitle-color: #adadad;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optionnel: background piments */
|
|
||||||
body.spicy {
|
|
||||||
background-image: url('../../assets/images/Spicy.png');
|
|
||||||
background-size:cover;
|
|
||||||
background-position: center;
|
|
||||||
background-attachment: fixed;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
9
public/styles/theme/themeimport.css
vendored
@@ -1,9 +0,0 @@
|
|||||||
@import url("chirac-theme.css");
|
|
||||||
@import url("dark-theme.css");
|
|
||||||
@import url("fanatic-theme.css");
|
|
||||||
@import url("light-theme.css");
|
|
||||||
@import url("nf-theme.css");
|
|
||||||
@import url("drift-theme.css");
|
|
||||||
@import url("neon-theme.css");
|
|
||||||
@import url("songbird-theme.css");
|
|
||||||
@import url("spicy-theme.css")
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
--background-color: #001224;
|
--background-color: #001224;
|
||||||
--default-text-color: #eee;
|
--default-text-color: #eee;
|
||||||
--subtitle-color: #007bff;
|
--subtitle-color: #007bff;
|
||||||
--mp3-text-color: #000000;
|
--audio-only-label-color: #000000;
|
||||||
|
|
||||||
/* Formulaire */
|
/* Formulaire */
|
||||||
--form-bg-color: #ffffff;
|
--form-bg-color: #ffffff;
|
||||||
@@ -15,18 +15,23 @@
|
|||||||
--form-button-bg-color: #007bff;
|
--form-button-bg-color: #007bff;
|
||||||
--form-button-text-color: #ffffff;
|
--form-button-text-color: #ffffff;
|
||||||
--form-button-bg-hover-color: #0056b3;
|
--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;
|
||||||
@@ -34,12 +39,15 @@
|
|||||||
--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 */
|
||||||
--settings-button-bg-color: var(--form-bg-color);
|
--settings-button-bg-color: var(--form-bg-color);
|
||||||
--settings-button-text-color: var(--default-text-color);
|
--settings-button-text-color: var(--default-text-color);
|
||||||
|
|
||||||
--settings-bg-primary-color: var(--form-bg-color);
|
--settings-modal-bg-color: var(--form-bg-color);
|
||||||
--settings-bg-secondary-color: var(--form-bg-color);
|
--settings-section-bg-color: var(--form-bg-color);
|
||||||
--settings-text-color: var(--default-text-color);
|
--settings-text-color: var(--default-text-color);
|
||||||
--settings-subtitle-color: #aaa;
|
--settings-subtitle-color: #aaa;
|
||||||
|
|
||||||
|
|||||||
0
resources/binaries/linux/.gitkeep
Normal file
0
resources/binaries/win-32/.gitkeep
Normal file
1
scripts/CHANGELOG.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[//]: # (This file is used for the release CHANGELOG. Please use Markdown to describe changes.)
|
||||||
116
scripts/create-copr-package.sh
Executable file
@@ -0,0 +1,116 @@
|
|||||||
|
#!/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")
|
||||||
|
RPM_OUT="$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Resolve fpm from electron-builder cache
|
||||||
|
# Excludes Ruby gem wrappers, targets standalone binaries only
|
||||||
|
# Prefers fpm@2.x over legacy fpm-1.9.x
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
FPM=$(find ~/.cache/electron-builder -name "fpm" -type f 2>/dev/null \
|
||||||
|
| grep -v "/gems/bin/fpm" \
|
||||||
|
| grep -v "/gems/gems/" \
|
||||||
|
| grep -v "/lib/app/bin/fpm" \
|
||||||
|
| grep "fpm@" \
|
||||||
|
| head -1)
|
||||||
|
|
||||||
|
# Fallback to legacy fpm-1.9.x if fpm@2.x not found
|
||||||
|
if [ -z "$FPM" ]; then
|
||||||
|
FPM=$(find ~/.cache/electron-builder -name "fpm" -type f 2>/dev/null \
|
||||||
|
| grep -v "/gems/" \
|
||||||
|
| grep -v "/lib/app/bin/fpm" \
|
||||||
|
| head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$FPM" ]; then
|
||||||
|
echo "Error: fpm not found in electron-builder cache. Run 'npm run build:linux' once first to download it."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using fpm: $FPM"
|
||||||
|
|
||||||
|
# Check that linux-unpacked exists
|
||||||
|
if [ ! -d "$ROOT_DIR/dist/linux-unpacked" ]; then
|
||||||
|
echo "Error: dist/linux-unpacked not found. Run 'npm run build:linux' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Prepare staging files
|
||||||
|
# fpm requires exact file-to-destination mappings to avoid
|
||||||
|
# scanning system directories (which causes permission errors)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
# Shell wrapper for /usr/bin/freedom-loader
|
||||||
|
TMP_WRAPPER="$ROOT_DIR/.rpm-pkg-tmp-wrapper"
|
||||||
|
mkdir -p "$TMP_WRAPPER"
|
||||||
|
cat > "$TMP_WRAPPER/freedom-loader" << 'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
exec /opt/freedom-loader/freedom-loader "$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$TMP_WRAPPER/freedom-loader"
|
||||||
|
|
||||||
|
# Desktop entry file
|
||||||
|
TMP_DESKTOP="$ROOT_DIR/.rpm-pkg-tmp-desktop"
|
||||||
|
mkdir -p "$TMP_DESKTOP"
|
||||||
|
cat > "$TMP_DESKTOP/freedom-loader.desktop" << 'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Freedom Loader
|
||||||
|
Exec=/opt/freedom-loader/freedom-loader %U
|
||||||
|
Icon=freedom-loader
|
||||||
|
Type=Application
|
||||||
|
Categories=AudioVideo;Utility;Network;
|
||||||
|
Comment=Free and open-source GUI for yt-dlp
|
||||||
|
StartupWMClass=Freedom Loader
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 1 - Build .rpm from linux-unpacked via fpm
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo "Building RPM..."
|
||||||
|
"$FPM" -s dir -t rpm --force \
|
||||||
|
--rpm-use-file-permissions \
|
||||||
|
--name freedom-loader \
|
||||||
|
--version "$VERSION" \
|
||||||
|
--architecture x86_64 \
|
||||||
|
--description "Free and open-source GUI for yt-dlp" \
|
||||||
|
--url "https://masteracnolo.github.io/Freedom-Loader-Site/" \
|
||||||
|
--maintainer "MasterAcnolo <MasterAcnolo@users.noreply.github.com>" \
|
||||||
|
--license "GPL-3.0-only" \
|
||||||
|
--depends gtk3 \
|
||||||
|
--depends nss \
|
||||||
|
--depends libXScrnSaver \
|
||||||
|
--depends at-spi2-core \
|
||||||
|
--rpm-attr "4755,root,root:/opt/freedom-loader/chrome-sandbox" \
|
||||||
|
--package "$RPM_OUT" \
|
||||||
|
"$ROOT_DIR/dist/linux-unpacked/=/opt/freedom-loader" \
|
||||||
|
"$TMP_WRAPPER/freedom-loader=/usr/bin/freedom-loader" \
|
||||||
|
"$TMP_DESKTOP/freedom-loader.desktop=/usr/share/applications/freedom-loader.desktop" \
|
||||||
|
"$ROOT_DIR/build/app-icon.png=/usr/share/icons/hicolor/512x512/apps/freedom-loader.png" \
|
||||||
|
"$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
||||||
|
|
||||||
|
rm -rf "$TMP_WRAPPER" "$TMP_DESKTOP"
|
||||||
|
echo "RPM built: $RPM_OUT"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 2 - Build SRPM for COPR
|
||||||
|
# Wraps the .rpm above into a source RPM for COPR submission
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo "Building SRPM for COPR..."
|
||||||
|
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 built: $ROOT_DIR/srpm-out/freedom-loader-$VERSION-1*.src.rpm"
|
||||||
198
scripts/release.sh
Executable file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
|
||||||
|
|
||||||
|
if [[ "$VERSION" == *"-preview"* ]]; then
|
||||||
|
echo "Error: The current version ($VERSION) is a 'preview"
|
||||||
|
echo "Unable to launch release pipeline. Please update the package.json with a stable version.."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAG="$VERSION"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
PUBLISH_SNAP=false
|
||||||
|
PUBLISH_COPR=false
|
||||||
|
DRY_RUN=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case $arg in
|
||||||
|
--snap) PUBLISH_SNAP=true ;;
|
||||||
|
--copr) PUBLISH_COPR=true ;;
|
||||||
|
--publish) PUBLISH_SNAP=true; PUBLISH_COPR=true ;;
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
*) echo "Unknown Argument : $arg" ; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo " Freedom Loader - Release Pipeline"
|
||||||
|
echo " Version : $VERSION"
|
||||||
|
echo " Tag : $TAG"
|
||||||
|
echo " Snap : $PUBLISH_SNAP"
|
||||||
|
echo " COPR : $PUBLISH_COPR"
|
||||||
|
echo " Dry run : $DRY_RUN"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 0.1 - Check Dependencies
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo "[0/5] Checking dependencies..."
|
||||||
|
|
||||||
|
command -v gh >/dev/null 2>&1 || { echo "'gh' CLI missing, need to be installed (https://cli.github.com/)."; exit 1; }
|
||||||
|
|
||||||
|
if [ "$PUBLISH_SNAP" = true ] && [ "$DRY_RUN" = false ]; then
|
||||||
|
command -v snapcraft >/dev/null 2>&1 || { echo "'snapcraft' missing, need to be installed."; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$PUBLISH_COPR" = true ] && [ "$DRY_RUN" = false ]; then
|
||||||
|
command -v copr-cli >/dev/null 2>&1 || { echo "'copr-cli' missing, need to be installed."; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "All dependencies found"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 0.2 - Clean Workspace
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo "[0/5] Cleaning workspace..."
|
||||||
|
# On supprime les dossiers générés lors des builds précédents
|
||||||
|
rm -rf "$ROOT_DIR/dist" "$ROOT_DIR/srpm-out"
|
||||||
|
echo "Workspace cleaned (dist/ and srpm-out/ removed)"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 0.3 - Retrieve Changelog
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
if [ ! -f "$ROOT_DIR/CHANGELOG.md" ]; then
|
||||||
|
echo "Warning: CHANGELOG.md not found, release notes will be empty."
|
||||||
|
CHANGELOG=""
|
||||||
|
else
|
||||||
|
CHANGELOG="$(cat "$ROOT_DIR/CHANGELOG.md")"
|
||||||
|
echo "Changelog retrieved successfully"
|
||||||
|
fi
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 1 - Build Linux packages (AppImage, deb, snap)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo "[1/5] Building Linux packages..."
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
npm run build:linux
|
||||||
|
echo "Linux packages built"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 2 - Build RPM binary + SRPM for COPR
|
||||||
|
# Uses the create-copr-package.sh script which handles:
|
||||||
|
# - fpm binary resolution from electron-builder cache
|
||||||
|
# - .rpm generation from linux-unpacked
|
||||||
|
# - .src.rpm generation for COPR submission
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo "[2/5] Building RPM and SRPM..."
|
||||||
|
bash "$ROOT_DIR/scripts/create-copr-package.sh"
|
||||||
|
echo "RPM and SRPM built"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 3 - Build Windows installer
|
||||||
|
# Note: requires Windows binaries to be present locally under
|
||||||
|
# resources/binaries/win-32/ (not committed to the repo)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo "[3/5] Building Windows installer..."
|
||||||
|
npm run build:win
|
||||||
|
echo " Windows installer built"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 4 - Create a draft release on GitHub
|
||||||
|
# Release notes are pre-filled with the standard template.
|
||||||
|
# Edit and publish manually from the GitHub web interface.
|
||||||
|
# Requires gh CLI to be installed and authenticated.
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo "[4/5] Creating GitHub draft release $TAG..."
|
||||||
|
|
||||||
|
# Utilise une méthode un peu plus robuste pour trouver l'ancien tag (ignore le HEAD actuel s'il est déjà tagué)
|
||||||
|
PREV_TAG=$(git rev-list --tags --skip=1 --max-count=1 | xargs git describe --tags 2>/dev/null || echo "previous")
|
||||||
|
|
||||||
|
RELEASE_NOTES="# Freedom Loader - $VERSION
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
$CHANGELOG
|
||||||
|
|
||||||
|
## Found a bug or issue?
|
||||||
|
Please report it in the [GitHub Issues](https://github.com/MasterAcnolo/Freedom-Loader/issues) section.
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/${PREV_TAG}...${TAG}"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = false ]; then
|
||||||
|
gh release create "$TAG" \
|
||||||
|
--title "$TAG" \
|
||||||
|
--notes "$RELEASE_NOTES" \
|
||||||
|
--draft \
|
||||||
|
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe" \
|
||||||
|
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe.blockmap" \
|
||||||
|
"$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm" \
|
||||||
|
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.deb" \
|
||||||
|
"$ROOT_DIR/dist/Freedom Loader-${VERSION}.AppImage" \
|
||||||
|
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \
|
||||||
|
"$ROOT_DIR/dist/latest.yml" \
|
||||||
|
"$ROOT_DIR/dist/latest-linux.yml"
|
||||||
|
echo "Draft release created: https://github.com/MasterAcnolo/Freedom-Loader/releases"
|
||||||
|
else
|
||||||
|
echo " [DRY RUN] gh release create $TAG (skipped)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Step 5 - Publish to package stores
|
||||||
|
# Snap and COPR are opt-in via --snap, --copr, or --publish.
|
||||||
|
# Without these flags, this step is skipped entirely.
|
||||||
|
# Run after the GitHub release is published (not the draft),
|
||||||
|
# so store users always get the same version as GitHub users.
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo "[5/5] Publishing to package stores..."
|
||||||
|
|
||||||
|
if [ "$PUBLISH_SNAP" = true ]; then
|
||||||
|
echo " Publishing to Snap Store..."
|
||||||
|
if [ "$DRY_RUN" = false ]; then
|
||||||
|
export PATH=$PATH:/var/lib/snapd/snap/bin
|
||||||
|
snapcraft upload \
|
||||||
|
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \
|
||||||
|
--release=stable
|
||||||
|
echo " Snap published"
|
||||||
|
else
|
||||||
|
echo " [DRY RUN] snapcraft upload (skipped)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Snap: skipped (use --snap or --publish to enable)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$PUBLISH_COPR" = true ]; then
|
||||||
|
echo " Publishing to COPR..."
|
||||||
|
if [ "$DRY_RUN" = false ]; then
|
||||||
|
copr-cli build freedom-loader \
|
||||||
|
"$ROOT_DIR/srpm-out/freedom-loader-${VERSION}-1"*.src.rpm
|
||||||
|
echo "COPR build triggered"
|
||||||
|
else
|
||||||
|
echo " [DRY RUN] copr-cli build (skipped)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " COPR: skipped (use --copr or --publish to enable)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Done - Summary
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo " Release pipeline complete."
|
||||||
|
echo ""
|
||||||
|
echo " Next steps:"
|
||||||
|
echo " 1. Fill in the release notes on GitHub"
|
||||||
|
echo " 2. Publish the draft release"
|
||||||
|
if [ "$PUBLISH_SNAP" = false ] || [ "$PUBLISH_COPR" = false ]; then
|
||||||
|
echo ""
|
||||||
|
echo " Store publishing (not done yet):"
|
||||||
|
[ "$PUBLISH_SNAP" = false ] && echo " - Snap : bash scripts/release.sh --snap"
|
||||||
|
[ "$PUBLISH_COPR" = false ] && echo " - COPR : bash scripts/release.sh --copr"
|
||||||
|
fi
|
||||||
|
echo "============================================"
|
||||||
@@ -1,11 +1,50 @@
|
|||||||
const { fetchDownload } = require("../services/download.services");
|
const { fetchDownload, cancelDownload } = require("../services/download.services");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
const { isValidUrl, isSafePath } = require("../helpers/validation");
|
const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
|
||||||
const { notifyDownloadFinished } = require("../helpers/notify");
|
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
|
||||||
|
const { configFeatures } = require("../../config");
|
||||||
|
|
||||||
const listeners = [];
|
/**
|
||||||
|
* 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 = [];
|
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) {
|
async function downloadController(req, res) {
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
@@ -13,21 +52,39 @@ async function downloadController(req, res) {
|
|||||||
audioOnly: req.body.audioOnly === "1",
|
audioOnly: req.body.audioOnly === "1",
|
||||||
quality: req.body.quality || "best",
|
quality: req.body.quality || "best",
|
||||||
outputFolder: req.body.savePath || undefined,
|
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.url || !isValidUrl(options.url)) return res.status(400).send("Invalid URL !");
|
||||||
if (options.outputFolder && !isSafePath(options.outputFolder)) return res.status(400).send("❌ Save Path Not Allowed.");
|
if (options.outputFolder && !isSafePath(options.outputFolder)) {
|
||||||
|
logger.warn(`Unsafe download path rejected: ${options.outputFolder}`);
|
||||||
|
return res.status(400).send("Save Path Not Allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
const filePath = await fetchDownload(options, listeners, speedListeners);
|
const outputFolder = await fetchDownload(
|
||||||
notifyDownloadFinished(filePath);
|
options,
|
||||||
res.send("✅ Download Done !");
|
progressListeners,
|
||||||
|
speedListeners,
|
||||||
|
stageListeners,
|
||||||
|
playlistListeners
|
||||||
|
);
|
||||||
|
|
||||||
|
notifyDownloadFinished(outputFolder, configFeatures.notifySystem);
|
||||||
|
res.send("Download Done !");
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Server Error in /download : ${err.message}`);
|
logger.error(`Server Error in /download: ${err?.message ?? err}`);
|
||||||
res.status(500).send(`❌ Server Error`);
|
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) {
|
function progressController(req, res) {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
@@ -36,14 +93,21 @@ function progressController(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sendProgress = percent => res.write(`data: ${percent}\n\n`);
|
const sendProgress = percent => res.write(`data: ${percent}\n\n`);
|
||||||
listeners.push(sendProgress);
|
progressListeners.push(sendProgress);
|
||||||
|
|
||||||
req.on('close', () => {
|
req.on('close', () => {
|
||||||
listeners.splice(listeners.indexOf(sendProgress), 1);
|
progressListeners.splice(progressListeners.indexOf(sendProgress), 1);
|
||||||
res.end();
|
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) {
|
function speedController(req, res) {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
@@ -60,5 +124,75 @@ function speedController(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = { downloadController, progressController, speedController};
|
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};
|
||||||
|
|||||||
@@ -1,46 +1,130 @@
|
|||||||
const { fetchInfo } = require("../services/info.services");
|
const { fetchInfo } = require("../services/info.services");
|
||||||
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo");
|
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
const { isValidUrl } = require("../helpers/validation");
|
const { isValidUrl } = require("../helpers/validation.helpers");
|
||||||
|
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
||||||
|
|
||||||
async function infoController(req, res){
|
/**
|
||||||
|
* 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; // Gérer POST et GET
|
// Previously, i support both POST and GET element, but it's kinda weird. So now it only support GET.
|
||||||
|
const url = req.query.url;
|
||||||
|
const encodedUrl = encodeURIComponent(url)
|
||||||
|
|
||||||
|
/**
|
||||||
/* Si pas d'url, url non-string ou url invalide*/
|
* Basic validation:
|
||||||
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("❌ Invalid URL Or Missing");
|
* - URL must exist
|
||||||
|
* - Must be a string
|
||||||
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
|
* - Must pass custom URL validation
|
||||||
|
*/
|
||||||
if (url.includes("?list")) {
|
if (!url || typeof url !== "string" || !isValidUrl(url)) {
|
||||||
logger.info("Estimated Data Type: Playlist")
|
return res.status(400).send("Invalid URL Or Missing");
|
||||||
|
|
||||||
} else{
|
|
||||||
logger.info("Estimated Data Type: Video")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`Info request received. RAW URL: ${url}`);
|
||||||
|
logger.info(`Info request received. ENCODED: ${encodedUrl}`);
|
||||||
|
|
||||||
|
// Lightweight heuristic to detect playlist URLs. It works for Youtube.
|
||||||
|
const isPlaylistUrl = isUrlPlaylist(url);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
isPlaylistUrl
|
||||||
|
? "Estimated Data Type: Playlist"
|
||||||
|
: "Estimated Data Type: Video"
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchInfo(url);
|
let data;
|
||||||
|
|
||||||
if (data._type === "playlist") {
|
/**
|
||||||
|
* Primary metadata fetch attempt
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
data = await fetchInfo(url);
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
const playlist = parsePlaylist(data);
|
/**
|
||||||
logger.info(`Playlist : ${playlist.title} (${playlist.count} vidéos)`);
|
* Fallback strategy:
|
||||||
return res.json(playlist);
|
* If playlist extraction fails, retry as single video
|
||||||
|
*/
|
||||||
|
if (isPlaylistUrl) {
|
||||||
|
|
||||||
} else{
|
logger.info(
|
||||||
const video = parseVideo(data);
|
`Playlist fetch failed: ${err.message}. Retrying as single video...`
|
||||||
logger.info(`Unique Video: ${video.title}`);
|
);
|
||||||
return res.json(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
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize response based on yt-dlp output type
|
||||||
|
*/
|
||||||
|
if (data._type === "playlist") {
|
||||||
|
|
||||||
|
// If the playlist got only one video, show it as a video.
|
||||||
|
if (data.entries && data.entries.length === 1) {
|
||||||
|
|
||||||
|
const singleVideo = parseVideo(data.entries[0]);
|
||||||
|
|
||||||
|
logger.info(`Playlist with 1 item converted to video: ${singleVideo.title}`);
|
||||||
|
|
||||||
|
return res.json(singleVideo);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (err) {
|
||||||
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
|
|
||||||
return res.status(500).send(`❌ Unable to fetch info`);
|
logger.error(
|
||||||
|
`Info controller error: ${err && err.message ? err.message : err}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(500).send("Unable to fetch info");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {infoController};
|
module.exports = { infoController };
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
const config = require('../config');
|
|
||||||
const RPC = require("discord-rpc");
|
|
||||||
const { logger } = require("./logger");
|
|
||||||
|
|
||||||
const clientId = `${config.DiscordRPCID}`;
|
|
||||||
const rpc = new RPC.Client({ transport: "ipc" });
|
|
||||||
|
|
||||||
let intervalId;
|
|
||||||
|
|
||||||
function startRPC() {
|
|
||||||
rpc.on("ready", () => {
|
|
||||||
const presence = {
|
|
||||||
largeImageKey: "icon",
|
|
||||||
smallImageKey: "acnolo_pfp",
|
|
||||||
smallImageText: "By MasterAcnolo",
|
|
||||||
startTimestamp: new Date(),
|
|
||||||
details: `Open Source Download Tools - ${config.version}`,
|
|
||||||
state: "masteracnolo.github.io/FreedomLoader",
|
|
||||||
};
|
|
||||||
rpc.clearActivity()
|
|
||||||
rpc.setActivity(presence);
|
|
||||||
|
|
||||||
// Met à jour la présence toutes les 15s
|
|
||||||
intervalId = setInterval(() => {
|
|
||||||
rpc.setActivity(presence);
|
|
||||||
}, 15000);
|
|
||||||
});
|
|
||||||
|
|
||||||
rpc.login({ clientId }).catch(err => {
|
|
||||||
logger.error("Unable to connect to the RPC:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopRPC(){
|
|
||||||
try {
|
|
||||||
if (intervalId) clearInterval(intervalId);
|
|
||||||
|
|
||||||
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};
|
|
||||||
@@ -1,10 +1,31 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const getUserBrowser = require("./getBrowser");
|
const {getUserBrowser} = require("./getBrowser.helpers.js");
|
||||||
const { ffmpegPath, denoPath} = require("./path");
|
const { ffmpegPath, denoPath} = require("./path.helpers.js");
|
||||||
const { configFeatures } = require("../../config.js");
|
const { configFeatures } = require("../../config.js");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger.js");
|
||||||
|
const config = require("../../config")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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){
|
function validateCodec(codec){
|
||||||
|
|
||||||
const validCodec = [
|
const validCodec = [
|
||||||
@@ -20,7 +41,29 @@ function validateCodec(codec){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
/**
|
||||||
|
* 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, isPlaylist}) {
|
||||||
|
|
||||||
logger.info("--- CONFIGURATION ---");
|
logger.info("--- CONFIGURATION ---");
|
||||||
|
|
||||||
@@ -28,6 +71,7 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
|||||||
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
||||||
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
||||||
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
||||||
|
logger.info(`CONFIG keepPlaylistOrder: ${configFeatures.keepPlaylistOrder}`);
|
||||||
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
||||||
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
||||||
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
||||||
@@ -75,7 +119,27 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
args.push("-f", qualityMap[quality] || "best");
|
args.push("-f", qualityMap[quality] || "best");
|
||||||
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
|
|
||||||
|
/**
|
||||||
|
* Default title output template
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
let outputTemplate = "%(title)s.%(ext)s";
|
||||||
|
|
||||||
|
// If it's a playlist, and we want to keep the playlist order. Add an index before the title.
|
||||||
|
if (isPlaylist && configFeatures.keepPlaylistOrder && configFeatures.autoDownloadPlaylist) {
|
||||||
|
outputTemplate = "%(playlist_index)02d - %(title)s.%(ext)s";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a custom signature in each file downloaded with the app. It's a small things but i found it cool :)
|
||||||
|
if (configFeatures.addMetadata) {
|
||||||
|
args.push(
|
||||||
|
"--postprocessor-args",
|
||||||
|
`ffmpeg:-metadata comment="Downloaded with Freedom Loader ${config.version}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push("-o", path.join(outputFolder, outputTemplate));
|
||||||
args.push(url);
|
args.push(url);
|
||||||
|
|
||||||
return args.filter(Boolean);
|
return args.filter(Boolean);
|
||||||
63
server/helpers/getBrowser.helpers.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
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 platform = process.platform;
|
||||||
|
let isFirefoxHere = false;
|
||||||
|
|
||||||
|
if (platform === "win32") {
|
||||||
|
const paths = [
|
||||||
|
"C:\\Program Files\\Mozilla Firefox\\firefox.exe",
|
||||||
|
"C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe",
|
||||||
|
`${process.env.LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`,
|
||||||
|
];
|
||||||
|
|
||||||
|
isFirefoxHere = paths.some(p => fs.existsSync(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === "linux") {
|
||||||
|
const paths = [
|
||||||
|
"/usr/bin/firefox",
|
||||||
|
"/usr/bin/firefox-esr",
|
||||||
|
"/usr/local/bin/firefox",
|
||||||
|
"/snap/bin/firefox",
|
||||||
|
"/var/lib/flatpak/exports/bin/org.mozilla.firefox",
|
||||||
|
`${os.homedir()}/.local/share/flatpak/exports/bin/org.mozilla.firefox`,
|
||||||
|
];
|
||||||
|
isFirefoxHere = paths.some(p => fs.existsSync(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFirefoxHere) {
|
||||||
|
// If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad
|
||||||
|
// No supported browser found => Notify User
|
||||||
|
logger.warn("No supported browser found on the system");
|
||||||
|
notify.notifyFirefoxBrowserMissing();
|
||||||
|
} else {
|
||||||
|
logger.info("Browser found: firefox");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Firefox and let yt-dlp handle the error gracefully.
|
||||||
|
// This prevents the application from crashing
|
||||||
|
return "firefox";
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {getUserBrowser};
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const os = require("os");
|
|
||||||
const path = require("path");
|
|
||||||
const notify = require("./notify")
|
|
||||||
|
|
||||||
function getUserBrowser() {
|
|
||||||
const userProfile = os.homedir();
|
|
||||||
|
|
||||||
// Liste des navigateurs supportés par yt-dlp (Actuellement je peux que garantir Firefox, Désolé)
|
|
||||||
const browsers = [
|
|
||||||
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
|
||||||
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
|
||||||
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
|
|
||||||
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
|
|
||||||
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
|
|
||||||
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
|
|
||||||
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
|
|
||||||
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const browser of browsers) {
|
|
||||||
if (fs.existsSync(browser.path)) {
|
|
||||||
return browser.name;
|
|
||||||
} else{
|
|
||||||
notify.notifyFirefoxBrowserMissing();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notify.notifyCookiesBrowserError()
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = getUserBrowser
|
|
||||||
117
server/helpers/notify.helpers.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
const { Notification, shell } = require("electron");
|
||||||
|
const { iconPaths } = require("./path.helpers");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set that is holding all notifications. Used to avoid Race Condition and notification crash
|
||||||
|
* @type {Set<any>}
|
||||||
|
*/
|
||||||
|
const activeNotifications = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Protect notification to garbage collection and race condition, by adding a 150ms timeout before trying to open the folder
|
||||||
|
activeNotifications.add(notif);
|
||||||
|
|
||||||
|
notif.on("click", () => {
|
||||||
|
activeNotifications.delete(notif);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
shell.openPath(folder).then((errorMessage) => {
|
||||||
|
if (errorMessage) {
|
||||||
|
logger.error(`Impossible d'ouvrir le dossier : ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
logger.error(`Erreur inattendue de shell.openPath : ${err}`);
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("close", () => {
|
||||||
|
activeNotifications.delete(notif);
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
activeNotifications.add(notif);
|
||||||
|
|
||||||
|
notif.on("click", () => {
|
||||||
|
shell.openExternal("https://www.firefox.com/en-US/download/");
|
||||||
|
activeNotifications.delete(notif);
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("close", () => {
|
||||||
|
activeNotifications.delete(notif);
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
activeNotifications.add(notif);
|
||||||
|
|
||||||
|
notif.on("click", () => {
|
||||||
|
shell.openExternal("https://www.firefox.com/en-US/download/");
|
||||||
|
activeNotifications.delete(notif);
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("close", () => {
|
||||||
|
activeNotifications.delete(notif);
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
notifyDownloadFinished,
|
||||||
|
notifyCookiesBrowserError,
|
||||||
|
notifyFirefoxBrowserMissing,
|
||||||
|
};
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const { Notification, shell } = require("electron");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
function notifyDownloadFinished(folder) {
|
|
||||||
const iconPath = path.join(process.resourcesPath, "confirm-icon.png");
|
|
||||||
const notif = new Notification({
|
|
||||||
title: "Freedom Loader",
|
|
||||||
body: "Your download is complete, click here to open it.",
|
|
||||||
icon: iconPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
notif.on("click", () => shell.openPath(folder));
|
|
||||||
notif.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyCookiesBrowserError(){
|
|
||||||
const iconPath = path.join(process.resourcesPath, "error.png");
|
|
||||||
const notif = new Notification({
|
|
||||||
title: "Cookies Error",
|
|
||||||
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
|
||||||
icon: iconPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
|
|
||||||
notif.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyFirefoxBrowserMissing(){
|
|
||||||
const iconPath = path.join(process.ressourcesPath, "error.png");
|
|
||||||
const notif = new Notification({
|
|
||||||
title: "Firefox Missing",
|
|
||||||
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
|
||||||
icon: iconPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))
|
|
||||||
notif.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
notifyDownloadFinished,
|
|
||||||
notifyCookiesBrowserError,
|
|
||||||
notifyFirefoxBrowserMissing
|
|
||||||
};
|
|
||||||
38
server/helpers/parseInfo.helpers.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Normalizes a video payload by attaching the resource type.
|
||||||
|
*
|
||||||
|
* @param {Object} data - Raw video metadata.
|
||||||
|
* @returns {Object} Normalized video object.
|
||||||
|
*/
|
||||||
|
function parseVideo(data) {
|
||||||
|
return {
|
||||||
|
type: "video",
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a raw playlist payload into a normalized structure
|
||||||
|
* containing playlist metadata and a simplified list of videos.
|
||||||
|
*
|
||||||
|
* @param {Object} data - Raw playlist metadata.
|
||||||
|
* @returns {Object} Normalized playlist object.
|
||||||
|
*/
|
||||||
|
function parsePlaylist(data) {
|
||||||
|
return {
|
||||||
|
type: "playlist",
|
||||||
|
title: data.title || data.id,
|
||||||
|
channel: data.uploader,
|
||||||
|
count: data.entries.length,
|
||||||
|
videos: (data.entries || []).map(v => ({
|
||||||
|
id: v.id,
|
||||||
|
title: v.title,
|
||||||
|
url: v.webpage_url,
|
||||||
|
duration: v.duration,
|
||||||
|
thumbnail: v.thumbnail,
|
||||||
|
uploader: v.uploader
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parseVideo, parsePlaylist };
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
function parseVideo(data) {
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "video",
|
|
||||||
// id: data.id,
|
|
||||||
// title: data.title,
|
|
||||||
// url: data.webpage_url,
|
|
||||||
// duration: data.duration,
|
|
||||||
// thumbnail: data.thumbnail,
|
|
||||||
// uploader: data.uploader
|
|
||||||
...data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePlaylist(data) {
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "playlist",
|
|
||||||
title: data.title || data.id,
|
|
||||||
channel: data.uploader,
|
|
||||||
count: data.entries.length,
|
|
||||||
videos: (data.entries || []).map(v => ({
|
|
||||||
id: v.id,
|
|
||||||
title: v.title,
|
|
||||||
url: v.webpage_url,
|
|
||||||
duration: v.duration,
|
|
||||||
thumbnail: v.thumbnail,
|
|
||||||
uploader: v.uploader
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { parseVideo, parsePlaylist };
|
|
||||||
269
server/helpers/path.helpers.js
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { app } = require("electron");
|
||||||
|
const config = require("../../config.js");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the OS windows
|
||||||
|
*/
|
||||||
|
const isWindows = process.platform === 'win32'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On windows the binaries are name.exe on Linux/macOS no
|
||||||
|
*/
|
||||||
|
const ext = isWindows ? '.exe' : ''
|
||||||
|
|
||||||
|
/** Centralized resource paths */
|
||||||
|
const resourcesPath = config.devMode
|
||||||
|
? path.join(__dirname, "../../resources")
|
||||||
|
: process.resourcesPath;
|
||||||
|
|
||||||
|
/** Default download folder (centralized) */
|
||||||
|
const defaultDownloadFolder = path.join(app.getPath("downloads"), "Freedom Loader");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current platform:
|
||||||
|
* win32 | linux | darwin
|
||||||
|
*/
|
||||||
|
const platform = process.platform;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dev source tree uses platform-named subfolders (matches resources/binaries/{win-32,linux,darwin}).
|
||||||
|
* Packaged builds flatten everything directly under binaries/, since each
|
||||||
|
* platform build only ships its own binaries — no subfolder needed.
|
||||||
|
*/
|
||||||
|
const devPlatformFolder = {
|
||||||
|
win32: "win-32",
|
||||||
|
linux: "linux",
|
||||||
|
darwin: "darwin"
|
||||||
|
}[platform];
|
||||||
|
|
||||||
|
const binariesPath = config.devMode
|
||||||
|
? path.join(resourcesPath, "binaries", devPlatformFolder)
|
||||||
|
: path.join(resourcesPath, "binaries");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime-resolved binary paths.
|
||||||
|
* These values differ between development and production builds.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let userYtDlp;
|
||||||
|
let ffmpegPath;
|
||||||
|
let denoPath;
|
||||||
|
|
||||||
|
/** Source binary bundled inside application resources (used for first-time copy) */
|
||||||
|
const sourceYtDlp = path.join(
|
||||||
|
binariesPath,
|
||||||
|
`yt-dlp${ext}`
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve binary locations depending on environment:
|
||||||
|
* - dev: local resources folder
|
||||||
|
* - prod: extracted userData + packaged resources
|
||||||
|
*/
|
||||||
|
if (config.devMode) {
|
||||||
|
userYtDlp = path.join(
|
||||||
|
binariesPath,
|
||||||
|
`yt-dlp${ext}`
|
||||||
|
);
|
||||||
|
|
||||||
|
ffmpegPath = path.join(
|
||||||
|
binariesPath,
|
||||||
|
`ffmpeg${ext}`
|
||||||
|
);
|
||||||
|
|
||||||
|
denoPath = path.join(
|
||||||
|
binariesPath,
|
||||||
|
`deno${ext}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
userYtDlp = path.join(
|
||||||
|
app.getPath("userData"),
|
||||||
|
`yt-dlp${ext}`
|
||||||
|
);
|
||||||
|
|
||||||
|
ffmpegPath = path.join(
|
||||||
|
binariesPath,
|
||||||
|
`ffmpeg${ext}`
|
||||||
|
);
|
||||||
|
|
||||||
|
denoPath = path.join(
|
||||||
|
binariesPath,
|
||||||
|
`deno${ext}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(userYtDlp) && fs.existsSync(sourceYtDlp)) {
|
||||||
|
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||||
|
|
||||||
|
// Linux/macOS executable permission
|
||||||
|
if (!isWindows) {
|
||||||
|
try {
|
||||||
|
fs.chmodSync(userYtDlp, 0o755);
|
||||||
|
} catch (err) {
|
||||||
|
getLogger().warn(`Failed to chmod yt-dlp: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized notification and UI icon paths.
|
||||||
|
* Used by Electron notifications and UI components.
|
||||||
|
*/
|
||||||
|
const iconPaths = {
|
||||||
|
confirm: path.join(resourcesPath, "confirm-icon.png"),
|
||||||
|
error: path.join(resourcesPath, "error.png"),
|
||||||
|
app: path.join(resourcesPath, "app-icon.ico")
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static reference paths to bundled binaries.
|
||||||
|
* Used mainly for verification and diagnostics.
|
||||||
|
*/
|
||||||
|
const binaryPaths = {
|
||||||
|
ytDlp: path.join(
|
||||||
|
binariesPath,
|
||||||
|
`yt-dlp${ext}`
|
||||||
|
),
|
||||||
|
|
||||||
|
ffmpeg: path.join(
|
||||||
|
binariesPath,
|
||||||
|
`ffmpeg${ext}`
|
||||||
|
),
|
||||||
|
|
||||||
|
ffprobe: path.join(
|
||||||
|
binariesPath,
|
||||||
|
`ffprobe${ext}`
|
||||||
|
),
|
||||||
|
|
||||||
|
deno: path.join(
|
||||||
|
binariesPath,
|
||||||
|
`deno${ext}`
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme storage system:
|
||||||
|
* - default themes come from app resources
|
||||||
|
* - user themes are persisted in userData to survive updates
|
||||||
|
*/
|
||||||
|
const userThemesPath = path.join(app.getPath("userData"), "themes");
|
||||||
|
const defaultThemesSourcePath = path.join("theme");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes user theme directory and ensures default themes exist.
|
||||||
|
*
|
||||||
|
* - Creates user themes folder if missing
|
||||||
|
* - Copies bundled themes from resources if not present
|
||||||
|
* - Applies permissive permissions for runtime modification
|
||||||
|
*/
|
||||||
|
function initUserThemes() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(userThemesPath)) {
|
||||||
|
fs.mkdirSync(userThemesPath, { recursive: true });
|
||||||
|
fs.chmodSync(userThemesPath, 0o777);
|
||||||
|
getLogger().info("Created user themes directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(defaultThemesSourcePath)) {
|
||||||
|
const defaultThemes = fs.readdirSync(defaultThemesSourcePath);
|
||||||
|
for (const theme of defaultThemes) {
|
||||||
|
const srcPath = path.join(defaultThemesSourcePath, theme);
|
||||||
|
const destPath = path.join(userThemesPath, theme);
|
||||||
|
const srcIsFile = !fs.statSync(srcPath).isDirectory();
|
||||||
|
|
||||||
|
if (fs.existsSync(destPath)) {
|
||||||
|
const destIsDir = fs.statSync(destPath).isDirectory();
|
||||||
|
if (srcIsFile && destIsDir) {
|
||||||
|
getLogger().warn(`Corrupted theme entry: ${theme}, fixing...`);
|
||||||
|
fs.rmSync(destPath, { recursive: true, force: true });
|
||||||
|
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (srcIsFile) {
|
||||||
|
fs.copyFileSync(srcPath, destPath);
|
||||||
|
fs.chmodSync(destPath, 0o666);
|
||||||
|
} else {
|
||||||
|
copyDirectory(srcPath, destPath);
|
||||||
|
fs.chmodSync(destPath, 0o777);
|
||||||
|
}
|
||||||
|
getLogger().info(`Copied default theme: ${theme}`);
|
||||||
|
} catch (copyErr) {
|
||||||
|
getLogger().warn(`Failed to copy theme ${theme}: ${copyErr.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getLogger().warn(`Default themes source path not found: ${defaultThemesSourcePath}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
getLogger().error(`Failed to initialize user themes: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Recursively copies a directory and its contents.
|
||||||
|
*
|
||||||
|
* @param {string} src - Source directory
|
||||||
|
* @param {string} dest - Destination directory
|
||||||
|
* @throws Error If copy operation fails
|
||||||
|
*/
|
||||||
|
function copyDirectory(src, dest) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
fs.mkdirSync(dest, {
|
||||||
|
recursive: true,
|
||||||
|
mode: 0o777
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(src);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const srcFile = path.join(src, file);
|
||||||
|
const destFile = path.join(dest, file);
|
||||||
|
|
||||||
|
if (fs.statSync(srcFile).isDirectory()) {
|
||||||
|
copyDirectory(srcFile, destFile);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(srcFile, destFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Copy error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime validation of critical binaries
|
||||||
|
// if (!userYtDlp) {
|
||||||
|
// getLogger().error("Missing yt-dlp binary");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!ffmpegPath) {
|
||||||
|
// getLogger().error("Missing ffmpeg binary");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!denoPath) {
|
||||||
|
// getLogger().error("Missing deno binary");
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
function validateBinaries() {
|
||||||
|
if (!userYtDlp) getLogger().error("Missing yt-dlp binary");
|
||||||
|
if (!ffmpegPath) getLogger().error("Missing ffmpeg binary");
|
||||||
|
if (!denoPath) getLogger().error("Missing deno binary");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to lazy load logger, avoid circular import
|
||||||
|
* @returns {import("winston").Logger}
|
||||||
|
*/
|
||||||
|
function getLogger() {
|
||||||
|
return require("../logger.js").logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes, isWindows, validateBinaries };
|
||||||