mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-11-13 23:19:21 +01:00
Compare commits
521 Commits
testing
...
f0971ebccc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0971ebccc | ||
|
|
18c8621095 | ||
|
|
b2eb3b3b40 | ||
|
|
7af6898301 | ||
|
|
dc8d0a62d7 | ||
|
|
cf65069496 | ||
|
|
a308352ea0 | ||
|
|
3712813833 | ||
|
|
9398329959 | ||
|
|
8614b0e32e | ||
|
|
c212302550 | ||
|
|
0835e13840 | ||
|
|
1c0dd32732 | ||
|
|
1e099f6bb6 | ||
|
|
1937738ab8 | ||
|
|
ac1d74ff23 | ||
|
|
7f86d46ae7 | ||
|
|
187e3af559 | ||
|
|
e8c499fe23 | ||
|
|
99b7164ff8 | ||
|
|
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 |
3
.github/ISSUE_TEMPLATE/performance-issue.md
vendored
3
.github/ISSUE_TEMPLATE/performance-issue.md
vendored
@@ -29,6 +29,9 @@ Clear description of the performance issue.
|
||||
- **Storage**: [e.g. SSD/HDD]
|
||||
- **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
|
||||
|
||||
- **Download URL**: [if applicable]
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/question.md
vendored
4
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -9,8 +9,8 @@ assignees: 'MasterAcnolo'
|
||||
|
||||
## Pre-submission Checklist
|
||||
|
||||
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/pages/faq)
|
||||
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/pages/wiki)
|
||||
- [ ] I have checked the [FAQ](https://freedomloader.acnolo.fr/#/faq)
|
||||
- [ ] I have reviewed the [Wiki](https://freedomloader.acnolo.fr/#/wiki)
|
||||
- [ ] I have searched existing issues
|
||||
|
||||
## Question
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Brief description of what this PR does.
|
||||
|
||||
## Related Issue
|
||||
## Related Issue (if applicable)
|
||||
|
||||
Fixes #(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 checked the application logs for errors
|
||||
- [ ] 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)
|
||||
|
||||
|
||||
46
.github/workflows/codeql.yml
vendored
Normal file
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}}"
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -6,19 +6,30 @@
|
||||
/out
|
||||
/srpm-out
|
||||
|
||||
# .env
|
||||
*.env
|
||||
|
||||
# Config JSON File. Create it and fill it if you want to have dev only settings
|
||||
config/config.dev.json
|
||||
|
||||
# IDE
|
||||
/.idea
|
||||
/.vscode
|
||||
.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
|
||||
@@ -94,3 +94,4 @@ chmod +x resources/binaries/linux/deno
|
||||
|
||||
- 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
|
||||
|
||||
- [Development Guide](DEVELOPMENT.md)
|
||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
- [Issue Templates](.github/ISSUE_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
|
||||
- Steps to reproduce
|
||||
- App version and environment (Windows version, Firefox version if relevant)
|
||||
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log`
|
||||
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log` (Windows) or `~/.local/share/FreedomLoader/logs/` (Linux)
|
||||
- Screenshots if applicable
|
||||
|
||||
### 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
|
||||
|
||||
**Process:**
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/your-feature-name`
|
||||
3. Make your changes
|
||||
4. Test thoroughly on Windows 10/11
|
||||
5. Commit with clear messages: `git commit -m "Add feature X"`
|
||||
6. Push to your fork: `git push origin feature/your-feature-name`
|
||||
7. Open a Pull Request using the appropriate template
|
||||
1. Fork the repository.
|
||||
2. Check out our [**Developer Guide (DEVELOPMENT.md)**](./DEVELOPMENT.md) to set up your environment.
|
||||
3. Create a feature branch following our branching conventions.
|
||||
4. Make your changes and test thoroughly on Windows and/or Linux.
|
||||
5. Open a Pull Request using the appropriate template.
|
||||
|
||||
**Code Guidelines:**
|
||||
- Use camelCase for variables and functions
|
||||
@@ -72,41 +71,9 @@ Small contributions matter—don't hesitate to submit documentation PRs.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 16.x or higher
|
||||
- npm or yarn
|
||||
- Git
|
||||
- Windows 10/11 (for testing)
|
||||
Want to write some code? Awesome!
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
# Clone your fork
|
||||
git clone https://github.com/YOUR-USERNAME/Freedom-Loader.git
|
||||
cd Freedom-Loader
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
Freedom-Loader/
|
||||
├── main.js # Electron main process
|
||||
├── preload.js # Electron preload script
|
||||
├── config.js # Global configuration
|
||||
├── server/ # Express backend
|
||||
│ ├── routes/ # API routes
|
||||
│ ├── controller/ # Business logic
|
||||
│ └── helpers/ # Utility functions
|
||||
├── public/ # Frontend (HTML, CSS, JS)
|
||||
└── resources/ # Binaries (yt-dlp, ffmpeg, etc.)
|
||||
```
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -114,12 +81,9 @@ Freedom-Loader/
|
||||
|
||||
Before submitting a PR, verify:
|
||||
- [ ] Download functionality works (video/audio)
|
||||
- [ ] Playlist downloads work
|
||||
- [ ] Settings panel functions correctly
|
||||
- [ ] Custom path selection works
|
||||
- [ ] No errors in application logs
|
||||
- [ ] UI changes work on all themes (if applicable)
|
||||
- [ ] Tested on Windows 10 and/or Windows 11
|
||||
- [ ] Any features wasn't break during the development
|
||||
- [ ] 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?
|
||||
|
||||
- Check the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/faq)
|
||||
- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki)
|
||||
- Check the [FAQ](https://freedomloader.acnolo.fr/faq)
|
||||
- Review the [Wiki](https://freedomloader.acnolo.fr/wiki)
|
||||
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
||||
|
||||
---
|
||||
|
||||
388
DEVELOPMENT.md
Normal file
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! 🎉
|
||||
227
README.md
227
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
<a href="https://masteracnolo.github.io/Freedom-Loader-Site/"><img
|
||||
<a href="https://freedomloader.acnolo.fr/"><img
|
||||
src="./build/banner.png"
|
||||
alt="Banner"
|
||||
style="width: 50%;"/></a>
|
||||
@@ -9,14 +9,12 @@
|
||||
|
||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
||||
[](https://freedomloader.acnolo.fr/)
|
||||
[](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>
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -25,7 +23,7 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
|
||||
### See related repo:
|
||||
|
||||
- **[Website](https://masteracnolo.github.io/Freedom-Loader-Site/)** - Official project website and documentation
|
||||
- **[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
|
||||
@@ -35,14 +33,12 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
- [Usage](#usage)
|
||||
- [Preview](#preview)
|
||||
- [Configuration](#configuration)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Theme Workshop](#Theme-Workshop)
|
||||
- [Technology Stack](#technology-stack)
|
||||
- [Development](#development)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Contributing](#contributing)
|
||||
- [Submitting a community theme](#Submitting-a-community-theme)
|
||||
- [Support](#support)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
@@ -61,9 +57,9 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
### User Interface
|
||||
|
||||
- **Custom window controls** - Frameless window with custom top bar (optional)
|
||||
- **Custom themes** — ZIP-based theme system with live preview
|
||||
- **Theme Workshop** — web-based theme creator and browser
|
||||
- **Toast notifications** — in-app feedback system
|
||||
- **Custom themes** - ZIP-based theme system with live preview
|
||||
- **Theme Workshop** - web-based theme creator and browser
|
||||
- **Toast notifications** - in-app feedback system
|
||||
- **Real-time progress tracking** - Live download progress bar with network speed indicator
|
||||
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
||||
- **Settings panel** - In-app configuration interface with live updates
|
||||
@@ -80,9 +76,11 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
- **Rate limiting** - Express-based request throttling to prevent server overload
|
||||
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
||||
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
||||
- **Splash screen** — animated loading screen on startup
|
||||
- **Stop download** — ability to cancel in-progress downloads
|
||||
- **Playlist folders** — dedicated folder creation per playlist
|
||||
- **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
|
||||
|
||||
@@ -105,7 +103,7 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
|
||||
### Linux
|
||||
|
||||
#### Fedora (via Copr — recommended)
|
||||
#### Fedora (via Copr - recommended)
|
||||
|
||||
Installing via Copr lets `dnf` handle future updates automatically:
|
||||
|
||||
@@ -178,7 +176,8 @@ The application supports various configuration options:
|
||||
- Custom download directory selection
|
||||
- Discord Rich Presence activation
|
||||
- Theme customization
|
||||
- Log level configuration
|
||||
- Log level
|
||||
- And somes to see in the list [Configuration Options](#configuration-options) available
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
@@ -219,13 +218,14 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
|
||||
#### Linux
|
||||
```
|
||||
~/.config/Freedom Loader/config.json
|
||||
~/.config/Freedom\ Loader/config.json
|
||||
```
|
||||
|
||||
### Available Options
|
||||
```json
|
||||
{
|
||||
"autoUpdate": true,
|
||||
"systemTray": true,
|
||||
"discordRPC": true,
|
||||
"customTopBar": true,
|
||||
"autoCheckInfo": true,
|
||||
@@ -233,6 +233,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"keepPlaylistOrder": false,
|
||||
"createPlaylistFolders": true,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
@@ -249,6 +250,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
| Option | Type | Default | Description |
|
||||
|------------------------------|---------|----------|------------------------------------------------------------|
|
||||
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
||||
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
|
||||
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
||||
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
||||
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
||||
@@ -256,7 +258,8 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
||||
| `keepPlaylistOrder` | boolean | `false` | Add index before title, usefull for album |
|
||||
| `createPlaylistFolders` | boolean | `false` | Automatically create a folder for a playlist |
|
||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||
| `logSystem` | boolean | `true` | Enable application logging |
|
||||
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
||||
@@ -276,95 +279,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
- `theora` - Theora (legacy open codec)
|
||||
|
||||
> [!NOTE]
|
||||
> Configuration changes may require an application restart to take effect.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Freedom-Loader/
|
||||
├── app/ # Electron main process modules
|
||||
│ ├── autoUpdater.js
|
||||
│ ├── dependencyCheck.js
|
||||
│ ├── discordRPC.js
|
||||
│ ├── ipcHandlers.js
|
||||
│ ├── pathValidator.js
|
||||
│ ├── splashManager.js
|
||||
│ ├── themeManager.js
|
||||
│ ├── windowManager.js
|
||||
│ └── ytDlpUpdater.js
|
||||
├── build/ # Build resources and assets
|
||||
├── config/ # Configuration files
|
||||
│ ├── config.default.json
|
||||
│ └── config.dev.json
|
||||
├── public/ # Frontend assets and UI
|
||||
│ ├── index.html
|
||||
│ ├── splash.html
|
||||
│ ├── assets/
|
||||
│ │ ├── icon/ # Application icons
|
||||
│ │ └── logo/ # Logo assets
|
||||
│ ├── script/ # Frontend JavaScript modules
|
||||
│ │ ├── appVersion.js
|
||||
│ │ ├── clipboardPaste.js
|
||||
│ │ ├── custompath.js
|
||||
│ │ ├── customthemes.js
|
||||
│ │ ├── downloadstatus.js
|
||||
│ │ ├── fetchinfo.js
|
||||
│ │ ├── progressBar.js
|
||||
│ │ ├── settingsPanel.js
|
||||
│ │ ├── toast.js
|
||||
│ │ └── topbar.js
|
||||
│ └── styles/ # CSS stylesheets
|
||||
│ ├── styles.css
|
||||
│ ├── variables.css
|
||||
│ ├── components/ # Component-specific styles
|
||||
│ │ ├── checkbox.css
|
||||
│ │ ├── editpathbutton.css
|
||||
│ │ ├── loader.css
|
||||
│ │ ├── progressBar.css
|
||||
│ │ └── toast.css
|
||||
│ └── layout/ # Layout styles
|
||||
│ ├── container.css
|
||||
│ ├── form.css
|
||||
│ ├── header.css
|
||||
│ ├── settingsPanel.css
|
||||
│ ├── topbar.css
|
||||
│ └── videoinfo.css
|
||||
├── resources/ # Internal resources (binaries)
|
||||
├── server/ # Express backend server
|
||||
│ ├── logger.js
|
||||
│ ├── server.js
|
||||
│ ├── controller/ # Request handlers
|
||||
│ │ ├── download.controller.js
|
||||
│ │ └── info.controller.js
|
||||
│ ├── helpers/ # Utility functions
|
||||
│ │ ├── buildArgs.helpers.js
|
||||
│ │ ├── getBrowser.helpers.js
|
||||
│ │ ├── notify.helpers.js
|
||||
│ │ ├── parseInfo.helpers.js
|
||||
│ │ ├── path.helpers.js
|
||||
│ │ ├── rateLimit.helpers.js
|
||||
│ │ └── validation.helpers.js
|
||||
│ ├── routes/ # API route definitions
|
||||
│ │ ├── download.route.js
|
||||
│ │ └── info.route.js
|
||||
│ └── services/ # Business logic layer
|
||||
│ ├── download.services.js
|
||||
│ └── info.services.js
|
||||
├── theme/ # Theme system
|
||||
│ ├── template.theme.json # Theme template
|
||||
│ ├── Dark/ # Default dark theme
|
||||
│ │ └── dark.theme.json
|
||||
│ └── Light/ # Default light theme
|
||||
│ └── light.theme.json
|
||||
├── main.js # Electron main process entry point
|
||||
├── preload.js # Electron preload script (IPC bridge)
|
||||
├── config.js # Global configuration loader
|
||||
├── package.json # Project metadata and dependencies
|
||||
├── CODE_OF_CONDUCT.md # Community guidelines
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
├── LICENSE # GPLv3 license
|
||||
└── README.md # This file
|
||||
```
|
||||
> Some configuration changes may require an application restart to take effect.
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
@@ -389,7 +304,7 @@ Freedom Loader includes a web-based theme creator available at [Freedom Loader W
|
||||
### 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
|
||||
3. Restart the app - your theme appears in the settings panel
|
||||
|
||||
## Technology Stack
|
||||
|
||||
@@ -412,28 +327,7 @@ Freedom Loader includes a web-based theme creator available at [Freedom Loader W
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 16.x or higher
|
||||
- npm or yarn
|
||||
- Git
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/MasterAcnolo/Freedom-Loader.git
|
||||
cd Freedom-Loader
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
```
|
||||
If you are interested by running a local non compiled version of Freedom Loader or just interested by contributing, see [DEVELOPMENT](./DEVELOPMENT.md)
|
||||
|
||||
### Additional Dependencies
|
||||
|
||||
@@ -444,44 +338,23 @@ You must download the required binaries and place them in the `resources` folder
|
||||
|
||||
#### Required binaries
|
||||
|
||||
- **Deno**
|
||||
- Download from: https://sourceforge.net/projects/deno.mirror/files/latest/download
|
||||
- Rename to: `deno.exe`
|
||||
|
||||
- **FFmpeg**
|
||||
- Download from: https://www.ffmpeg.org/download.html
|
||||
- Required files:
|
||||
- `ffmpeg.exe`
|
||||
- `ffprobe.exe`
|
||||
|
||||
- **yt-dlp**
|
||||
- Already bundled with the project
|
||||
- No manual installation required
|
||||
|
||||
Final folder structure:
|
||||
|
||||
```
|
||||
resources/
|
||||
├── deno.exe
|
||||
├── ffmpeg.exe
|
||||
├── ffprobe.exe
|
||||
└── yt-dlp.exe
|
||||
```
|
||||
|
||||
> These binaries are required for the application to start correctly.
|
||||
> If any of them are missing, an error message will be displayed at application startup.
|
||||
See [BINARIES.md](./BINARIES.md) for how to setup the binaries.
|
||||
|
||||
### Development Guidelines
|
||||
|
||||
- Follow existing code style and conventions
|
||||
- Write clear commit messages
|
||||
- Test thoroughly before submitting changes
|
||||
- Update documentation when adding features
|
||||
- Maintain compatibility with Windows 10+ and Linux
|
||||
- Write clear, imperative commit messages (e.g., "Add playlist indexing" not "Added")
|
||||
- Test thoroughly on both Windows and Linux before submitting changes
|
||||
- Update documentation (README, DEVELOPMENT.md) when adding features
|
||||
- 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
|
||||
|
||||
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
|
||||
See the most recent Roadmap [Here](https://freedomloader.acnolo.fr/Roadmap).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -494,13 +367,29 @@ Head over to the [Freedom Loader Workshop repository](https://github.com/MasterA
|
||||
|
||||
### 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
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Relevant logs from `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs/`
|
||||
- Screenshots if applicable
|
||||
- Screenshots or recordings if applicable
|
||||
|
||||
### Feature Requests
|
||||
|
||||
@@ -519,15 +408,7 @@ Open a feature request issue with:
|
||||
5. Update documentation as needed
|
||||
6. Submit a pull request with a detailed description
|
||||
|
||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing.
|
||||
|
||||
## Support
|
||||
|
||||
If you find this project useful and want to support its development:
|
||||
|
||||
[](https://paypal.me/axelnicolas25)
|
||||
|
||||
Your support helps maintain infrastructure, develop new features, and keep the project free and ad-free.
|
||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md), [DEVELOPMENT.md](./DEVELOPMENT.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing.
|
||||
|
||||
## License
|
||||
|
||||
@@ -543,8 +424,8 @@ You are free to use, modify, and redistribute this software under the terms of t
|
||||
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
||||
- The open-source community for continuous support and contributions
|
||||
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
||||
- [@AureCX](https://github.com/AureCX) for being the Official Windows Tester!
|
||||
- Zakaria for the website icon
|
||||
- Jacques Chirac to love Apples
|
||||
- All users who test, report issues, and help improve the application
|
||||
|
||||
---
|
||||
@@ -553,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
|
||||
|
||||
[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)
|
||||
[Website](https://freedomloader.acnolo.fr/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://freedomloader.acnolo.fr/wiki) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
const { dialog } = require("electron");
|
||||
const { logger } = require("../server/logger");
|
||||
|
||||
/**
|
||||
@@ -21,6 +20,26 @@ const { logger } = require("../server/logger");
|
||||
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.
|
||||
*
|
||||
@@ -32,7 +51,6 @@ autoUpdater.autoInstallOnAppQuit = false;
|
||||
* @param {BrowserWindow} mainWindow - main Electron window instance
|
||||
*/
|
||||
function initAutoUpdater(mainWindow) {
|
||||
|
||||
/**
|
||||
* Triggered when a new version is detected.
|
||||
* Prompts user to install or defer update.
|
||||
@@ -40,23 +58,12 @@ function initAutoUpdater(mainWindow) {
|
||||
autoUpdater.on("update-available", async (info) => {
|
||||
logger.info(`Update available: ${info.version}`);
|
||||
|
||||
const { response } = await dialog.showMessageBox(mainWindow, {
|
||||
type: "info",
|
||||
title: "Update Available",
|
||||
message: `Version ${info.version} is available.`,
|
||||
detail: "Would you like to download and install it now?",
|
||||
buttons: ["Install Update", "Maybe Later"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
mainWindow?.webContents.send("update-available", {
|
||||
version: info.version,
|
||||
environment: getUpdateEnvironment(),
|
||||
});
|
||||
updateAvailable = true;
|
||||
|
||||
if (response === 0) {
|
||||
await autoUpdater.downloadUpdate();
|
||||
} else {
|
||||
mainWindow?.webContents.executeJavaScript(
|
||||
`window.showUpdateBadge && window.showUpdateBadge("${info.version}")`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -66,8 +73,10 @@ function initAutoUpdater(mainWindow) {
|
||||
autoUpdater.on("download-progress", (progress) => {
|
||||
logger.info(`Download progress: ${Math.round(progress.percent)}%`);
|
||||
mainWindow?.webContents.send("update-progress", {
|
||||
percent: Math.round(progress.percent),
|
||||
speed: progress.bytesPerSecond,
|
||||
percent: progress.percent,
|
||||
bytesPerSecond: progress.bytesPerSecond,
|
||||
transferred: progress.transferred,
|
||||
total: progress.total,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,17 +87,9 @@ function initAutoUpdater(mainWindow) {
|
||||
autoUpdater.on("update-downloaded", async (info) => {
|
||||
logger.info(`Update downloaded: ${info.version}`);
|
||||
|
||||
const { response } = await dialog.showMessageBox(mainWindow, {
|
||||
type: "info",
|
||||
title: "Update Ready",
|
||||
message: `Version ${info.version} has been downloaded.`,
|
||||
detail: "The application will restart to apply the update.",
|
||||
buttons: ["Install Now", "Later"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
mainWindow?.webContents.send("update-downloaded", info);
|
||||
|
||||
|
||||
if (response === 0) autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -102,14 +103,16 @@ function initAutoUpdater(mainWindow) {
|
||||
* 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);
|
||||
const isNoUpdateAvailable =
|
||||
/404/.test(msg) || /Cannot find latest.*\.yml/i.test(msg);
|
||||
|
||||
if (isNoUpdateAvailable) {
|
||||
logger.warn("Auto update: no update metadata found (probably no previous release), ignoring", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error("Auto update error:", msg);
|
||||
logger.error("Auto update error:", err);
|
||||
mainWindow?.webContents.send("update-error", msg);
|
||||
});
|
||||
|
||||
checkForUpdates();
|
||||
@@ -121,12 +124,9 @@ function initAutoUpdater(mainWindow) {
|
||||
*/
|
||||
async function checkForUpdates() {
|
||||
|
||||
if (process.env.SNAP) {
|
||||
logger.info("Running as Snap, update managed by snapd, skipping electron-updater");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!require("electron").app.isPackaged) return ;
|
||||
// If app is was installed from SNAP or Flatpak.
|
||||
if (process.env.SNAP || process.env.FLATPAK_ID) return;
|
||||
if (!require("electron").app.isPackaged) return;
|
||||
|
||||
try {
|
||||
await autoUpdater.checkForUpdates();
|
||||
@@ -144,6 +144,7 @@ async function downloadUpdate() {
|
||||
await autoUpdater.downloadUpdate();
|
||||
} catch (err) {
|
||||
logger.error("Download failed:", err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,4 +155,13 @@ function installUpdate() {
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
|
||||
module.exports = { initAutoUpdater, downloadUpdate, installUpdate };
|
||||
function isUpdateAvailable() {
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initAutoUpdater,
|
||||
downloadUpdate,
|
||||
installUpdate,
|
||||
isUpdateAvailable,
|
||||
};
|
||||
|
||||
177
app/changelogManager.js
Normal file
177
app/changelogManager.js
Normal file
@@ -0,0 +1,177 @@
|
||||
const { app } = require("electron");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const https = require("https");
|
||||
const config = require("../config");
|
||||
const { logger } = require("../server/logger");
|
||||
|
||||
/** Maximum age of the cached release list before it must be refreshed. */
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/** Filename used to persist changelog data in the Electron user data folder. */
|
||||
const CACHE_FILE_NAME = "changelog-cache.json";
|
||||
|
||||
/** GitHub API endpoint containing the application's published releases. */
|
||||
const RELEASES_URL = "https://api.github.com/repos/MasterAcnolo/Freedom-Loader/releases";
|
||||
|
||||
/** Returns the path of the local changelog cache file. */
|
||||
function getCachePath() {
|
||||
return path.join(app.getPath("userData"), CACHE_FILE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the numeric major, minor, and patch parts from a version string.
|
||||
*
|
||||
* @param {string} version - Version string or release tag
|
||||
* @returns {number[]|null} Parsed version parts, or null when invalid
|
||||
*/
|
||||
function normalizeVersion(version) {
|
||||
const match = String(version || "").match(/(\d+)\.(\d+)\.(\d+)/);
|
||||
return match ? match.slice(1).map(Number) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two semantic version strings.
|
||||
*
|
||||
* @param {string} left - First version to compare
|
||||
* @param {string} right - Second version to compare
|
||||
* @returns {number} Negative, zero, or positive comparison result
|
||||
*/
|
||||
function compareVersions(left, right) {
|
||||
const leftParts = normalizeVersion(left) || [0, 0, 0];
|
||||
const rightParts = normalizeVersion(right) || [0, 0, 0];
|
||||
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
if (leftParts[index] !== rightParts[index]) {
|
||||
return leftParts[index] - rightParts[index];
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Reads and parses the cached changelog, if one is available. */
|
||||
function readCache() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(getCachePath(), "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Persists the fetched releases together with their cache timestamp. */
|
||||
function writeCache(releases) {
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
getCachePath(),
|
||||
JSON.stringify({ cachedAt: Date.now(), releases }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn(`Unable to cache changelog: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches published releases from GitHub and maps them to the UI data shape.
|
||||
*
|
||||
* @returns {Promise<object[]>} Published releases
|
||||
*/
|
||||
function fetchReleases() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = https.get(
|
||||
RELEASES_URL,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "Freedom-Loader",
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
let body = "";
|
||||
response.setEncoding("utf8");
|
||||
response.on("data", (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
response.on("end", () => {
|
||||
// Do not attempt to parse an error response as a release list.
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`GitHub returned HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Draft releases are not visible to users and should not appear in the changelog.
|
||||
const releases = JSON.parse(body)
|
||||
.filter((release) => !release.draft)
|
||||
.map((release) => ({
|
||||
version: release.tag_name,
|
||||
name: release.name || release.tag_name,
|
||||
date: release.published_at || release.created_at,
|
||||
body: release.body || "No changelog available.",
|
||||
url: release.html_url,
|
||||
}));
|
||||
resolve(releases);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
request.setTimeout(10000, () => {
|
||||
request.destroy(new Error("Changelog request timed out"));
|
||||
});
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cached or freshly fetched releases and identifies unread versions.
|
||||
*
|
||||
* @returns {Promise<object>} Changelog data for the renderer
|
||||
*/
|
||||
async function getChangelog() {
|
||||
const cache = readCache();
|
||||
let releases = cache?.releases || [];
|
||||
const cacheIsFresh = cache && cache.releases?.length > 0 && Date.now() - cache.cachedAt < CACHE_TTL_MS;
|
||||
|
||||
// Refresh stale or missing data, while keeping the last cache as a fallback.
|
||||
if (!cacheIsFresh) {
|
||||
try {
|
||||
releases = await fetchReleases();
|
||||
writeCache(releases);
|
||||
} catch (error) {
|
||||
logger.warn(`Unable to retrieve changelog: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const lastSeenVersion = cache?.lastSeenVersion || null;
|
||||
|
||||
// Without a previous version, all available releases are considered unread.
|
||||
const unreadReleases = lastSeenVersion
|
||||
? releases.filter((release) => compareVersions(release.version, lastSeenVersion) > 0)
|
||||
: releases;
|
||||
|
||||
return {
|
||||
// An empty release list means that the fetch failed or returned no releases.
|
||||
error: releases.length === 0,
|
||||
currentVersion: config.version,
|
||||
lastSeenVersion,
|
||||
hasUnread: unreadReleases.length > 0,
|
||||
releases,
|
||||
unreadReleases,
|
||||
};
|
||||
}
|
||||
|
||||
/** Marks the current application version as seen in the local cache. */
|
||||
function markChangelogSeen() {
|
||||
const cache = readCache() || { releases: [] };
|
||||
cache.lastSeenVersion = config.version;
|
||||
cache.cachedAt = cache.cachedAt || 0 ;
|
||||
fs.mkdirSync(path.dirname(getCachePath()), { recursive: true });
|
||||
fs.writeFileSync(getCachePath(), JSON.stringify(cache, null, 2), "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = { getChangelog, markChangelogSeen };
|
||||
@@ -1,6 +1,8 @@
|
||||
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.
|
||||
@@ -20,6 +22,51 @@ const rpc = new RPC.Client({ transport: "ipc" });
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@@ -32,6 +79,11 @@ let intervalId;
|
||||
*/
|
||||
function startRPC() {
|
||||
|
||||
/**
|
||||
* Patch that create Simlinks for Linux environment;
|
||||
*/
|
||||
fixLinuxDiscordRPC();
|
||||
|
||||
/**
|
||||
* Rich Presence payload describing current application state
|
||||
* shown in Discord user profile.
|
||||
@@ -43,15 +95,17 @@ function startRPC() {
|
||||
smallImageText: "By MasterAcnolo",
|
||||
startTimestamp: new Date(),
|
||||
details: `Open Source Download Tools - ${config.version}`,
|
||||
state: "masteracnolo.github.io/FreedomLoader",
|
||||
state: "freedomloader.acnolo.fr",
|
||||
};
|
||||
rpc.clearActivity()
|
||||
rpc.clearActivity();
|
||||
rpc.setActivity(presence);
|
||||
|
||||
});
|
||||
|
||||
rpc.login({ clientId }).catch(err => {
|
||||
logger.error("Unable to connect to the RPC:", err);
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,28 +114,23 @@ function startRPC() {
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Clear any running intervals
|
||||
* - Remove current activity from Discord
|
||||
* - Remove current activity from Discord if connected
|
||||
* - Destroy RPC transport connection
|
||||
* - Handle cleanup errors safely
|
||||
*/
|
||||
async function stopRPC(){
|
||||
async function stopRPC() {
|
||||
if (rpc) {
|
||||
try {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
|
||||
/**
|
||||
* Ensures RPC connection exists before attempting cleanup.
|
||||
*/
|
||||
if (rpc && rpc.transport) {
|
||||
await rpc.clearActivity()
|
||||
await rpc.destroy();
|
||||
} else{
|
||||
logger.error("Not Able to close RPC")
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.error("Error while closing the RPC:", err);
|
||||
// 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};
|
||||
|
||||
@@ -13,11 +13,15 @@
|
||||
const { ipcMain, dialog, shell } = require("electron");
|
||||
const fs = require("fs");
|
||||
const { logger, logDir } = require("../server/logger");
|
||||
const { configFeatures, featuresPath } = require("../config");
|
||||
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, checkReportService, getReportServiceStatus} = require("./sendReport");
|
||||
const { isUpdateAvailable, downloadUpdate, installUpdate } = require("./autoUpdater");
|
||||
const { getChangelog, markChangelogSeen } = require("./changelogManager");
|
||||
|
||||
/**
|
||||
* Security whitelist for feature flags that can be modified at runtime.
|
||||
@@ -27,6 +31,7 @@ const { userThemesPath } = require("../server/helpers/path.helpers");
|
||||
*/
|
||||
const FEATURE_WHITELIST = new Set([
|
||||
"autoUpdate",
|
||||
"systemTray",
|
||||
"discordRPC",
|
||||
"customTopBar",
|
||||
"autoCheckInfo",
|
||||
@@ -34,6 +39,7 @@ const FEATURE_WHITELIST = new Set([
|
||||
"addMetadata",
|
||||
"verboseLogs",
|
||||
"autoDownloadPlaylist",
|
||||
"keepPlaylistOrder",
|
||||
"customCodec",
|
||||
"theme",
|
||||
"createPlaylistFolders",
|
||||
@@ -61,12 +67,21 @@ const themeFolderPath = userThemesPath;
|
||||
* @param {Function} getMainWindow - Function returning current BrowserWindow instance
|
||||
*/
|
||||
function registerIpcHandlers(getMainWindow) {
|
||||
|
||||
/**
|
||||
* Returns application version from config.
|
||||
*/
|
||||
ipcMain.handle("version", () => config.version);
|
||||
|
||||
/**
|
||||
* Retrieves the changelog from cache or GitHub API.
|
||||
*/
|
||||
ipcMain.handle("get-changelog", () => getChangelog());
|
||||
|
||||
/**
|
||||
* Marks all changelog entries as seen and updates cache.
|
||||
*/
|
||||
ipcMain.handle("mark-changelog-seen", () => markChangelogSeen());
|
||||
|
||||
/**
|
||||
* Returns runtime feature configuration object.
|
||||
*/
|
||||
@@ -111,7 +126,17 @@ function registerIpcHandlers(getMainWindow) {
|
||||
/**
|
||||
* Window minimize request from renderer.
|
||||
*/
|
||||
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
|
||||
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.
|
||||
@@ -127,7 +152,6 @@ function registerIpcHandlers(getMainWindow) {
|
||||
*/
|
||||
ipcMain.on("window-close", () => getMainWindow()?.close());
|
||||
|
||||
|
||||
/**
|
||||
* Opens Chromium DevTools in detached mode.
|
||||
*/
|
||||
@@ -144,14 +168,14 @@ function registerIpcHandlers(getMainWindow) {
|
||||
* Opens external website in default browser.
|
||||
*/
|
||||
ipcMain.on("open-website", () =>
|
||||
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/")
|
||||
shell.openExternal("https://freedomloader.acnolo.fr/")
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens official wiki page in external browser.
|
||||
*/
|
||||
ipcMain.on("open-wiki", () =>
|
||||
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki")
|
||||
shell.openExternal("https://freedomloader.acnolo.fr/wiki")
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -166,7 +190,12 @@ function registerIpcHandlers(getMainWindow) {
|
||||
*/
|
||||
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.
|
||||
@@ -185,6 +214,36 @@ function registerIpcHandlers(getMainWindow) {
|
||||
return await reloadThemes();
|
||||
});
|
||||
|
||||
/**
|
||||
* Send bug report
|
||||
*/
|
||||
ipcMain.handle("send-report", async (_, params) => {
|
||||
return await sendReport(params);
|
||||
});
|
||||
|
||||
ipcMain.handle("get-report-service-status", async () => {
|
||||
await checkReportService();
|
||||
return getReportServiceStatus();
|
||||
});
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -212,17 +271,31 @@ function registerIpcHandlers(getMainWindow) {
|
||||
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"
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
logger.info(`Feature updated: ${key} = ${value}`);
|
||||
return true;
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`set-feature failed (${key}): ${err.message}`);
|
||||
return false;
|
||||
|
||||
@@ -56,28 +56,23 @@ function validateDownloadPath(userPath) {
|
||||
if (!userPath) return getDefaultDownloadPath();
|
||||
|
||||
try {
|
||||
const absolutePath = path.resolve(userPath);
|
||||
|
||||
/**
|
||||
* Resolves filesystem symlinks to ensure canonical absolute path.
|
||||
* Required to prevent path traversal or alias bypass.
|
||||
*/
|
||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||
|
||||
if (!fs.existsSync(resolved)) {
|
||||
fs.mkdirSync(resolved, { recursive: true });
|
||||
logger.info(`Download folder created: ${resolved}`);
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
fs.mkdirSync(absolutePath, { recursive: true });
|
||||
logger.info(`Download folder created: ${absolutePath}`);
|
||||
}
|
||||
|
||||
const real = fs.realpathSync(resolved);
|
||||
const real = fs.realpathSync(absolutePath);
|
||||
|
||||
if (!isSafePath(resolved)) {
|
||||
throw new Error("Path not allowed: system folders are blocked!");
|
||||
if (!isSafePath(real)) {
|
||||
logger.error("Path not allowed: system folders are blocked!");
|
||||
}
|
||||
|
||||
return real;
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
catch (err) {
|
||||
/**
|
||||
* Handles invalid filesystem paths, permissions issues, or resolution failures.
|
||||
* Logs diagnostic information before propagating a sanitized error.
|
||||
|
||||
108
app/sendReport.js
Normal file
108
app/sendReport.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const {logger, logDir} = require("../server/logger");
|
||||
|
||||
const REPORT_URL = "https://freedom-loader-report.acnolo.fr";
|
||||
const REPORT_SERVICE_TIMEOUT_MS = 5000;
|
||||
let reportServiceStatus = {
|
||||
status: "checking",
|
||||
message: "Checking bug report service..."
|
||||
};
|
||||
let reportServiceCheck = null;
|
||||
|
||||
async function checkReportService() {
|
||||
if (reportServiceCheck) {
|
||||
return reportServiceCheck;
|
||||
}
|
||||
|
||||
reportServiceCheck = (async () => {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), REPORT_SERVICE_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(REPORT_URL, {
|
||||
method: "GET",
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
if (response.status >= 500) {
|
||||
throw new Error(`Server responded with status ${response.status}`);
|
||||
}
|
||||
|
||||
reportServiceStatus = {
|
||||
status: "online",
|
||||
message: "Bug report service is available."
|
||||
};
|
||||
logger.info(`Bug report service is reachable (status ${response.status}).`);
|
||||
} catch (err) {
|
||||
reportServiceStatus = {
|
||||
status: "offline",
|
||||
message: "Bug report service is unavailable. Reports may fail to send."
|
||||
};
|
||||
logger.warn(`Bug report service is unreachable: ${err.message}`);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
return reportServiceStatus;
|
||||
})();
|
||||
|
||||
return reportServiceCheck;
|
||||
}
|
||||
|
||||
function getReportServiceStatus() {
|
||||
return reportServiceStatus;
|
||||
}
|
||||
|
||||
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 now = new Date();
|
||||
const today = [
|
||||
now.getFullYear(),
|
||||
String(now.getMonth() + 1).padStart(2, "0"),
|
||||
String(now.getDate()).padStart(2, "0")
|
||||
].join("-");
|
||||
|
||||
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(REPORT_URL, {
|
||||
method: "POST",
|
||||
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, checkReportService, getReportServiceStatus};
|
||||
117
app/tray.js
Normal file
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};
|
||||
@@ -81,6 +81,21 @@ async function createMainWindow() {
|
||||
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.
|
||||
|
||||
BIN
build/app-icon-64x64.png
Normal file
BIN
build/app-icon-64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
26
config.js
26
config.js
@@ -7,7 +7,7 @@ const path = require("path");
|
||||
* Indicates whether the application is running in development mode.
|
||||
* Determined by Electron's packaging state.
|
||||
*/
|
||||
const devMode = process.env.NODE_ENV === "test" || !app?.isPackaged;
|
||||
const devMode = !app.isPackaged || process.env.NODE_ENV === "test";
|
||||
|
||||
/**
|
||||
* Resolves the configuration file path depending on runtime environment.
|
||||
@@ -68,13 +68,30 @@ function loadFeatures() {
|
||||
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.
|
||||
*/
|
||||
const configFeatures = loadFeatures();
|
||||
let configFeatures = loadFeatures();
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -102,6 +119,11 @@ module.exports = {
|
||||
*/
|
||||
configFeatures,
|
||||
|
||||
/**
|
||||
* Function to reload the config
|
||||
*/
|
||||
reloadFeatures,
|
||||
|
||||
/**
|
||||
* Path to the active configuration file
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"autoUpdate": true,
|
||||
"systemTray": false,
|
||||
"discordRPC": true,
|
||||
"customTopBar": true,
|
||||
"autoCheckInfo": true,
|
||||
@@ -7,7 +8,8 @@
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"createPlaylistFolders": true,
|
||||
"keepPlaylistOrder": false,
|
||||
"createPlaylistFolders": false,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
"outputTitleCheck": true,
|
||||
|
||||
40
eslint.config.js
Normal file
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}
|
||||
}
|
||||
}
|
||||
];
|
||||
92
main.js
92
main.js
@@ -13,6 +13,9 @@ process.on("unhandledRejection", (reason) => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
app.setDesktopName('com.masteracnolo.freedomloader.desktop');
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this is the primary instance (lock acquired successfully)
|
||||
@@ -38,17 +41,7 @@ app.setAppUserModelId("com.masteracnolo.freedomloader");
|
||||
* Load the app dependencies for hardware choice
|
||||
*/
|
||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||
const { configFeatures } = require("./config");
|
||||
|
||||
/**
|
||||
* On start, enable or disable hardWareAcceleration
|
||||
*/
|
||||
if (!configFeatures.enableHardwareAcceleration){
|
||||
app.disableHardwareAcceleration();
|
||||
logger.info("Disabled Hardware Acceleration")
|
||||
} else {
|
||||
logger.info("Enable Hardware Acceleration")
|
||||
}
|
||||
const { configFeatures, devMode} = require("./config");
|
||||
|
||||
/**
|
||||
* In-memory snapshot of application feature flags.
|
||||
@@ -56,15 +49,20 @@ if (!configFeatures.enableHardwareAcceleration){
|
||||
* Note: Changes to the config file are not automatically reflected.
|
||||
* A restart could be required.
|
||||
*/
|
||||
const config = require("./config");
|
||||
if (!configFeatures.enableHardwareAcceleration){
|
||||
app.disableHardwareAcceleration();
|
||||
logger.info("Disabled Hardware Acceleration");
|
||||
} else {
|
||||
logger.info("Enable Hardware Acceleration");
|
||||
}
|
||||
|
||||
if(config.devMode){
|
||||
if(devMode){
|
||||
/**
|
||||
* Start devTron extensions - @see https://github.com/electron/devtron
|
||||
*/
|
||||
const { devtron } = require('@electron/devtron');
|
||||
devtron.install();
|
||||
logger.info("Loaded DevTron Extension")
|
||||
logger.info("Loaded DevTron Extension");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,8 +77,46 @@ const { updateYtDlp } = require("./app/ytDlpUpdater");
|
||||
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
||||
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
||||
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
|
||||
const { userThemesPath, initUserThemes, isWindows, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
|
||||
const { userThemesPath, initUserThemes, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
|
||||
const {createSystemTray, destroyTray} = require("./app/tray");
|
||||
const { stopServer } = require("./server/server");
|
||||
const {checkReportService} = require("./app/sendReport");
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
destroyTray();
|
||||
await stopRPC();
|
||||
stopServer();
|
||||
|
||||
} catch (err) {
|
||||
logger.error("Error during cleanup:", err);
|
||||
|
||||
} finally {
|
||||
logger.info("All services stopped. Have a nice day!");
|
||||
logSessionEnd();
|
||||
|
||||
isCleanedUp = true;
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If another instance want to run
|
||||
@@ -100,10 +136,13 @@ app.on("second-instance", () => {
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
logSessionStart(logDir, defaultDownloadFolder);
|
||||
checkReportService().catch((err) => {
|
||||
logger.warn(`Bug report service startup check failed: ${err.message}`);
|
||||
});
|
||||
|
||||
createSplashWindow();
|
||||
|
||||
if (!config.devMode && !checkNativeDependencies()) return;
|
||||
if (!devMode && !checkNativeDependencies()) return;
|
||||
|
||||
const { userYtDlp } = require("./server/helpers/path.helpers");
|
||||
updateYtDlp(userYtDlp);
|
||||
@@ -127,6 +166,11 @@ app.whenReady().then(async () => {
|
||||
|
||||
closeSplashWindow();
|
||||
getMainWindow().show();
|
||||
|
||||
if (configFeatures.systemTray) {
|
||||
createSystemTray(devMode);
|
||||
}
|
||||
|
||||
if (configFeatures.discordRPC) startRPC();
|
||||
|
||||
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
||||
@@ -138,12 +182,20 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (app.isQuitting || !configFeatures.systemTray) {
|
||||
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", async () => {
|
||||
await stopRPC();
|
||||
logger.info("All services stopped. Have a nice day!");
|
||||
logSessionEnd();
|
||||
app.on("before-quit", (event) => {
|
||||
app.isQuitting = true;
|
||||
|
||||
if (!isCleanedUp) {
|
||||
event.preventDefault();
|
||||
|
||||
cleanShutdown();
|
||||
}
|
||||
});
|
||||
2785
package-lock.json
generated
2785
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
124
package.json
124
package.json
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "freedom-loader",
|
||||
"desktopName": "com.masteracnolo.freedomloader",
|
||||
"productName": "Freedom Loader",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.4",
|
||||
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
|
||||
"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/",
|
||||
"homepage": "https://freedomloader.acnolo.fr/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MasterAcnolo/Freedom-Loader.git"
|
||||
@@ -15,17 +16,27 @@
|
||||
},
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron --trace-warnings .",
|
||||
|
||||
"clean:dist": "rm -rf dist/ && rm -rf srpm-out/",
|
||||
"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:win": "electron-builder --win",
|
||||
"build:linux": "electron-builder --linux",
|
||||
"build:rpm": "bash scripts/create-copr-package.sh",
|
||||
"build:rpm": "npm run clean:dist && npm run build:linux && 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 tests/unit",
|
||||
|
||||
"test:unit": "jest test/unit",
|
||||
"test:metainfo": "appstreamcli validate package/com.masteracnolo.freedomloader.metainfo.xml",
|
||||
"test:desktop": "desktop-file-validate package/com.masteracnolo.freedomloader.desktop",
|
||||
"lint": "eslint . --format pretty",
|
||||
"lint:fix": "eslint . --fix --format pretty",
|
||||
"update": "npm update"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -42,15 +53,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/devtron": "^2.1.1",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@jest/globals": "^30.4.1",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"electron": "^41.2.0",
|
||||
"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": {
|
||||
"appId": "com.masteracnolo.freedomloader",
|
||||
"artifactName": "Freedom-Loader-${version}.${ext}",
|
||||
"productName": "Freedom Loader",
|
||||
"publish": [
|
||||
{
|
||||
@@ -61,10 +78,15 @@
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"**/*",
|
||||
"node_modules/**/*",
|
||||
"!config/config.dev.json",
|
||||
"!theme/**/*"
|
||||
"app/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"build/**/*",
|
||||
"config/config.default.json",
|
||||
"config.js",
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"package.json"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "build"
|
||||
@@ -75,47 +97,37 @@
|
||||
"from": "theme",
|
||||
"to": "theme"
|
||||
},
|
||||
{
|
||||
"from": "build/confirm-icon.png",
|
||||
"to": "confirm-icon.png"
|
||||
},
|
||||
{
|
||||
"from": "build/banner.bmp",
|
||||
"to": "banner.bmp"
|
||||
},
|
||||
{
|
||||
"from": "build/banner.png",
|
||||
"to": "banner.png"
|
||||
},
|
||||
{
|
||||
"from": "build/error.png",
|
||||
"to": "error.png"
|
||||
},
|
||||
{
|
||||
"from": "config/config.default.json",
|
||||
"to": "config/config.default.json"
|
||||
},
|
||||
{
|
||||
"from": "build",
|
||||
"to": ".",
|
||||
"filter": [
|
||||
"app-icon.ico",
|
||||
"app-icon-64x64.png",
|
||||
"confirm-icon.png",
|
||||
"banner.bmp",
|
||||
"banner.png",
|
||||
"error.png"
|
||||
]
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "build/app-icon.ico",
|
||||
"artifactName": "${productName}-Setup-${version}.${ext}",
|
||||
"artifactName": "Freedom-Loader-Setup-${version}.${ext}",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "resources/binaries/win-32/yt-dlp.exe",
|
||||
"to": "binaries/yt-dlp.exe"
|
||||
},
|
||||
{
|
||||
"from": "resources/binaries/win-32/ffmpeg.exe",
|
||||
"to": "binaries/ffmpeg.exe"
|
||||
},
|
||||
{
|
||||
"from": "resources/binaries/win-32/ffprobe.exe",
|
||||
"to": "binaries/ffprobe.exe"
|
||||
},
|
||||
{
|
||||
"from": "resources/binaries/win-32/deno.exe",
|
||||
"to": "binaries/deno.exe"
|
||||
"from": "resources/binaries/win-32",
|
||||
"to": "binaries",
|
||||
"filter": [
|
||||
"yt-dlp.exe",
|
||||
"ffmpeg.exe",
|
||||
"ffprobe.exe",
|
||||
"deno.exe"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -137,24 +149,20 @@
|
||||
"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/yt-dlp",
|
||||
"to": "binaries/yt-dlp"
|
||||
},
|
||||
{
|
||||
"from": "resources/binaries/linux/ffmpeg",
|
||||
"to": "binaries/ffmpeg"
|
||||
},
|
||||
{
|
||||
"from": "resources/binaries/linux/ffprobe",
|
||||
"to": "binaries/ffprobe"
|
||||
},
|
||||
{
|
||||
"from": "resources/binaries/linux/deno",
|
||||
"to": "binaries/deno"
|
||||
"from": "resources/binaries/linux",
|
||||
"to": "binaries",
|
||||
"filter": [
|
||||
"yt-dlp",
|
||||
"ffmpeg",
|
||||
"ffprobe",
|
||||
"deno"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
8
package/com.masteracnolo.freedomloader.desktop
Normal file
8
package/com.masteracnolo.freedomloader.desktop
Normal file
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Name=Freedom Loader
|
||||
Exec=/opt/freedom-loader/freedom-loader %U
|
||||
Icon=freedom-loader
|
||||
Type=Application
|
||||
Categories=AudioVideo;
|
||||
Comment=Free and open-source GUI for yt-dlp
|
||||
StartupWMClass=com.masteracnolo.freedomloader
|
||||
@@ -45,7 +45,7 @@
|
||||
</p>
|
||||
</description>
|
||||
|
||||
<launchable type="desktop-id">freedom-loader.desktop</launchable>
|
||||
<launchable type="desktop-id">com.masteracnolo.freedomloader.desktop</launchable>
|
||||
|
||||
<url type="homepage">https://github.com/MasterAcnolo/Freedom-Loader</url>
|
||||
<url type="bugtracker">https://github.com/MasterAcnolo/Freedom-Loader/issues</url>
|
||||
|
||||
111
preload.js
111
preload.js
@@ -8,6 +8,26 @@ const { contextBridge, ipcRenderer } = require("electron");
|
||||
* All calls are explicitly whitelisted.
|
||||
*/
|
||||
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
|
||||
@@ -42,14 +62,23 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
* @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"),
|
||||
|
||||
/**
|
||||
* Returns the changelog data
|
||||
*/
|
||||
getChangelog: () => ipcRenderer.invoke("get-changelog"),
|
||||
|
||||
/**
|
||||
* Marks the current changelog version as seen by the user.
|
||||
*/
|
||||
markChangelogSeen: () => ipcRenderer.invoke("mark-changelog-seen"),
|
||||
|
||||
/**
|
||||
* Validates a download path before using it for file operations.
|
||||
*
|
||||
@@ -67,8 +96,70 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
* 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 update error event.
|
||||
*
|
||||
* @param {Function} callback - Function executed with error information when an update error occurs.
|
||||
* @returns {Electron.IpcRenderer}
|
||||
*/
|
||||
onUpdateError: (callback) => ipcRenderer.on("update-error", (_, err) => callback(err)),
|
||||
|
||||
/**
|
||||
* Subscribes to the download progress event.
|
||||
*
|
||||
* @param {Function} callback - Function executed with progress metrics (percent, bytesPerSecond, transferred, total).
|
||||
* @returns {Electron.IpcRenderer}
|
||||
*/
|
||||
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"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Exposes window control and developer utilities for the custom topbar UI.
|
||||
@@ -76,7 +167,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
* These methods forward commands to the Electron main process via IPC.
|
||||
*/
|
||||
contextBridge.exposeInMainWorld("topbarAPI", {
|
||||
|
||||
/**
|
||||
* Minimizes the application window.
|
||||
*/
|
||||
@@ -126,4 +216,17 @@ contextBridge.exposeInMainWorld("topbarAPI", {
|
||||
* Opens configuration/settings panel.
|
||||
*/
|
||||
openConfig: () => ipcRenderer.send("open-config"),
|
||||
|
||||
/**
|
||||
* Send Bug Report
|
||||
*
|
||||
* @param params
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
sendReport: (params) => ipcRenderer.invoke("send-report", params),
|
||||
|
||||
/**
|
||||
* Returns the startup health status of the bug report service.
|
||||
*/
|
||||
getReportServiceStatus: () => ipcRenderer.invoke("get-report-service-status"),
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
@@ -13,6 +13,8 @@
|
||||
<link rel="stylesheet" href="styles/styles.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>
|
||||
<body>
|
||||
|
||||
@@ -21,9 +23,11 @@
|
||||
<div class="custom-controls">
|
||||
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
||||
<button id="logs-btn" title="Logs">Logs</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="changelog-btn" title="Changelog">Changelog</button>
|
||||
<!-- <button id="theme-btn" title="Theme">Theme</button> -->
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
@@ -56,9 +60,9 @@
|
||||
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
||||
<div class="setting-info">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span class="setting-title">Thème</span>
|
||||
<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>
|
||||
<button id="refresh-themes-btn" title="Refresh themes" class="refresh-themes-btn">
|
||||
@@ -106,10 +110,10 @@
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
|
||||
<input data-key="keepPlaylistOrder" id="keepPlaylistOrder" type="checkbox">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Auto Fetch Info</span>
|
||||
<small>Automatically fetch information for new downloads.</small>
|
||||
<span class="setting-title">Keep Playlist Order</span>
|
||||
<small>Add index before output file, to keep the order.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,6 +131,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input data-key="systemTray" id="systemTray" type="checkbox">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">System Tray</span>
|
||||
<small>Minimize app on close, in the System Tray. [EXPERIMENTAL]</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
||||
<div class="setting-info">
|
||||
@@ -135,6 +147,14 @@
|
||||
</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">
|
||||
<div class="setting-info">
|
||||
@@ -246,7 +266,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -279,18 +299,56 @@
|
||||
|
||||
<span id="version-badge" class="version-badge"></span>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<div class="changelog-overlay" id="changelog-overlay" hidden>
|
||||
<section class="changelog-modal" role="dialog" aria-modal="true" aria-labelledby="changelog-title">
|
||||
<div class="changelog-header">
|
||||
<div>
|
||||
<span class="changelog-eyebrow">Freedom Loader</span>
|
||||
<h2 id="changelog-title">What's new</h2>
|
||||
</div>
|
||||
<button class="changelog-close" id="changelog-close" type="button" title="Close changelog" aria-label="Close changelog">×</button>
|
||||
</div>
|
||||
<div class="changelog-list" id="changelog-list">
|
||||
<p class="changelog-loading">Loading changelog...</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<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 -->
|
||||
<script src="script/toast.js"></script>
|
||||
<script src="script/customConfiguration.js"></script>
|
||||
<script src="script/clipboardPaste.js"></script>
|
||||
<script src="script/custompath.js"></script>
|
||||
<script src="script/settingsPanel.js"></script>
|
||||
@@ -299,6 +357,9 @@
|
||||
<script src="script/fetchinfo.js"></script>
|
||||
<script src="script/progressBar.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/changelog.js"></script>
|
||||
<script type="module" src="script/topbar.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,21 +1,54 @@
|
||||
/**
|
||||
* Retrieves the application version from the Electron main process
|
||||
* and displays it in the UI version badge (bottom-right corner).
|
||||
* Retrieves the application version and build state from the backend.
|
||||
* Displays a formatted string indicating packaging state and preview status.
|
||||
* Hides the "Packaged" state for production builds to keep the UI clean.
|
||||
*
|
||||
* This is mainly used for debugging and build identification.
|
||||
* 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}
|
||||
*/
|
||||
async function versionLabel() {
|
||||
const appVersion = await window.electronAPI.getVersion();
|
||||
|
||||
/**
|
||||
* UI element displaying the current application version.
|
||||
* Updated at runtime after IPC call resolves.
|
||||
* @type {HTMLElement | null}
|
||||
*/
|
||||
const versionBadge = document.getElementById("version-badge");
|
||||
|
||||
if (!versionBadge) return;
|
||||
|
||||
versionBadge.textContent = `${appVersion}`;
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
|
||||
versionLabel();
|
||||
displayVersionLabel();
|
||||
109
public/script/autoUpdate.js
Normal file
109
public/script/autoUpdate.js
Normal file
@@ -0,0 +1,109 @@
|
||||
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;
|
||||
}
|
||||
|
||||
const initialButtonText = btn.textContent;
|
||||
const initialIconText = icon.textContent;
|
||||
const initialTitleText = title.textContent;
|
||||
const initialSubText = sub.textContent;
|
||||
|
||||
const resetUI = () => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = initialButtonText;
|
||||
icon.textContent = initialIconText;
|
||||
title.textContent = initialTitleText;
|
||||
sub.textContent = initialSubText;
|
||||
progressWrap.style.display = "none";
|
||||
progressFill.style.width = "0%";
|
||||
progressPct.textContent = "0%";
|
||||
progressEta.textContent = "";
|
||||
};
|
||||
|
||||
window.electronAPI.onUpdateError((err) => {
|
||||
console.error("Update failed asynchronously:", err);
|
||||
resetUI();
|
||||
});
|
||||
|
||||
btn.onclick = async () => {
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Downloading...";
|
||||
icon.textContent = "download";
|
||||
title.textContent = "Downloading update";
|
||||
progressWrap.style.display = "flex";
|
||||
|
||||
try {
|
||||
await window.electronAPI.downloadUpdate();
|
||||
} catch {
|
||||
resetUI();
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
277
public/script/changelog.js
Normal file
277
public/script/changelog.js
Normal file
@@ -0,0 +1,277 @@
|
||||
/** DOM elements used by the changelog modal. */
|
||||
const UI = {
|
||||
overlay: document.getElementById("changelog-overlay"),
|
||||
list: document.getElementById("changelog-list"),
|
||||
closeBtn: document.getElementById("changelog-close"),
|
||||
openBtn: document.getElementById("changelog-btn")
|
||||
};
|
||||
|
||||
/** Changelog payload loaded from the Electron main process. */
|
||||
let changelogData = null;
|
||||
|
||||
/**
|
||||
* Formats an ISO release date for the current locale.
|
||||
*
|
||||
* @param {string} dateString - Release date returned by GitHub
|
||||
* @returns {string} Localized date or an empty string
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return "";
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(new Date(dateString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses inline Markdown and appends safe DOM nodes to a parent element.
|
||||
*
|
||||
* @param {HTMLElement} parent - Element receiving the parsed content
|
||||
* @param {string} value - Inline Markdown text
|
||||
*/
|
||||
function parseInline(parent, value) {
|
||||
const tokenPattern = /(\*\*|__)(.+?)\1|(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_)|`([^`]+)`|~~([^~]+)~~|\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = tokenPattern.exec(value)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parent.appendChild(document.createTextNode(value.slice(lastIndex, match.index)));
|
||||
}
|
||||
|
||||
const element = match[7]
|
||||
? document.createElement("a")
|
||||
: document.createElement(match[1] ? "strong" : match[3] || match[4] ? "em" : match[5] ? "code" : "del");
|
||||
|
||||
element.textContent = match[7] || match[2] || match[3] || match[4] || match[5] || match[6];
|
||||
|
||||
if (match[7]) {
|
||||
element.href = match[8];
|
||||
element.target = "_blank";
|
||||
element.rel = "noopener noreferrer";
|
||||
}
|
||||
|
||||
parent.appendChild(element);
|
||||
lastIndex = tokenPattern.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < value.length) {
|
||||
parent.appendChild(document.createTextNode(value.slice(lastIndex)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses block-level Markdown such as headings, lists, quotes and code blocks.
|
||||
*
|
||||
* @param {HTMLElement} parent - Element receiving the parsed content
|
||||
* @param {string} markdown - Release body in Markdown format
|
||||
*/
|
||||
function parseBlock(parent, markdown) {
|
||||
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
||||
let paragraph = [];
|
||||
let list = null;
|
||||
let codeBlock = null;
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (!paragraph.length) return;
|
||||
const element = document.createElement("p");
|
||||
paragraph.forEach((line, index) => {
|
||||
if (index) element.appendChild(document.createElement("br"));
|
||||
parseInline(element, line);
|
||||
});
|
||||
parent.appendChild(element);
|
||||
paragraph = [];
|
||||
};
|
||||
|
||||
const closeList = () => {
|
||||
if (list) {
|
||||
parent.appendChild(list);
|
||||
list = null;
|
||||
}
|
||||
};
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.trim().startsWith("```")) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
if (codeBlock) {
|
||||
const code = document.createElement("code");
|
||||
code.textContent = codeBlock.join("\n");
|
||||
const pre = document.createElement("pre");
|
||||
pre.appendChild(code);
|
||||
parent.appendChild(pre);
|
||||
codeBlock = null;
|
||||
} else {
|
||||
codeBlock = [];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (codeBlock) {
|
||||
codeBlock.push(line);
|
||||
return;
|
||||
}
|
||||
|
||||
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
const unorderedItem = line.match(/^\s*[-*+]\s+(.+)$/);
|
||||
const orderedItem = line.match(/^\s*\d+[.]\s+(.+)$/);
|
||||
const quote = line.match(/^\s*>\s?(.*)$/);
|
||||
|
||||
if (!line.trim()) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
} else if (heading) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
const element = document.createElement(`h${heading[1].length}`);
|
||||
parseInline(element, heading[2]);
|
||||
parent.appendChild(element);
|
||||
} else if (unorderedItem || orderedItem) {
|
||||
flushParagraph();
|
||||
const isOrdered = Boolean(orderedItem);
|
||||
if (!list || list.tagName.toLowerCase() !== (isOrdered ? "ol" : "ul")) {
|
||||
closeList();
|
||||
list = document.createElement(isOrdered ? "ol" : "ul");
|
||||
}
|
||||
const item = document.createElement("li");
|
||||
parseInline(item, (unorderedItem || orderedItem)[1]);
|
||||
list.appendChild(item);
|
||||
} else if (quote) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
const element = document.createElement("blockquote");
|
||||
parseInline(element, quote[1]);
|
||||
parent.appendChild(element);
|
||||
} else if (/^\s*([-*_])\s*\1\s*\1\s*$/.test(line)) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
parent.appendChild(document.createElement("hr"));
|
||||
} else {
|
||||
paragraph.push(line);
|
||||
}
|
||||
});
|
||||
|
||||
flushParagraph();
|
||||
closeList();
|
||||
if (codeBlock) {
|
||||
const code = document.createElement("code");
|
||||
code.textContent = codeBlock.join("\n");
|
||||
const pre = document.createElement("pre");
|
||||
pre.appendChild(code);
|
||||
parent.appendChild(pre);
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders the empty state when no releases are available. */
|
||||
function renderEmpty() {
|
||||
const emptyMessage = document.createElement("p");
|
||||
emptyMessage.className = "changelog-empty";
|
||||
emptyMessage.textContent = "No changelog available.";
|
||||
UI.list.appendChild(emptyMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders one release entry in the changelog list.
|
||||
*
|
||||
* @param {object} release - Release data from the backend
|
||||
*/
|
||||
function renderEntry(release) {
|
||||
const article = document.createElement("article");
|
||||
article.className = "changelog-entry";
|
||||
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "changelog-entry-heading";
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.textContent = release.name;
|
||||
|
||||
const metadata = document.createElement("span");
|
||||
metadata.textContent = [release.version, formatDate(release.date)]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
heading.append(title, metadata);
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "changelog-body";
|
||||
parseBlock(body, release.body);
|
||||
|
||||
article.append(heading, body);
|
||||
UI.list.appendChild(article);
|
||||
}
|
||||
|
||||
/** Renders all releases, replacing the current modal contents. */
|
||||
function renderReleases(releases) {
|
||||
UI.list.replaceChildren();
|
||||
if (!releases.length) {
|
||||
renderEmpty();
|
||||
return;
|
||||
}
|
||||
releases.forEach((release) => renderEntry(release));
|
||||
}
|
||||
|
||||
/** Opens the changelog modal and moves focus to its close button. */
|
||||
function openChangelog(releases) {
|
||||
renderReleases(releases);
|
||||
UI.overlay.hidden = false;
|
||||
UI.closeBtn?.focus();
|
||||
}
|
||||
|
||||
/** Closes the modal and records the current version as seen. */
|
||||
async function closeChangelog() {
|
||||
UI.overlay.hidden = true;
|
||||
await window.electronAPI.markChangelogSeen();
|
||||
}
|
||||
|
||||
/** Loads changelog data and opens the modal when unread releases exist. */
|
||||
async function loadChangelog() {
|
||||
try {
|
||||
changelogData = await window.electronAPI.getChangelog();
|
||||
if (!changelogData.error && changelogData.hasUnread) {
|
||||
openChangelog(changelogData.unreadReleases);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unable to load changelog:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles a click on the button that opens the changelog. */
|
||||
async function handleOpenClick() {
|
||||
if (!changelogData || changelogData.error) {
|
||||
changelogData = await window.electronAPI.getChangelog();
|
||||
}
|
||||
|
||||
if (changelogData.error) {
|
||||
window.showError("Unable to load changelog. Please check your network connection.");
|
||||
return;
|
||||
}
|
||||
|
||||
openChangelog(changelogData.releases);
|
||||
}
|
||||
|
||||
/** Closes the modal when the user clicks on the overlay background. */
|
||||
function handleOverlayClick(event) {
|
||||
if (event.target === UI.overlay) closeChangelog();
|
||||
}
|
||||
|
||||
/** Closes the modal when Escape is pressed. */
|
||||
function handleKeydown(event) {
|
||||
if (event.key === "Escape" && !UI.overlay.hidden) closeChangelog();
|
||||
}
|
||||
|
||||
/** Registers all changelog modal event listeners. */
|
||||
function bindEvents() {
|
||||
UI.openBtn?.addEventListener("click", handleOpenClick);
|
||||
UI.closeBtn?.addEventListener("click", closeChangelog);
|
||||
UI.overlay?.addEventListener("click", handleOverlayClick);
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
}
|
||||
|
||||
/** Initializes the changelog data flow and modal interactions. */
|
||||
function init() {
|
||||
bindEvents();
|
||||
loadChangelog();
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -1,27 +0,0 @@
|
||||
async function initPlatformRestrictions() {
|
||||
const platform = await window.electronAPI.getProcessPlatform();
|
||||
|
||||
const themeSelect = document.getElementById("themeSelect");
|
||||
const refreshBtn = document.getElementById("refresh-themes-btn");
|
||||
|
||||
/**
|
||||
* Is the OS windows
|
||||
* @type {boolean}
|
||||
*/
|
||||
const isWindows = platform === "win32";
|
||||
|
||||
console.log(isWindows);
|
||||
|
||||
// If it's windows, the button is not disabled.
|
||||
themeSelect.disabled = !isWindows;
|
||||
refreshBtn.disabled = !isWindows;
|
||||
|
||||
themeSelect.classList.toggle("disabled", !isWindows);
|
||||
refreshBtn.classList.toggle("disabled", !isWindows);
|
||||
|
||||
if (!isWindows) {
|
||||
themeSelect.title = "Themes are only available on Windows";
|
||||
refreshBtn.title = "Themes are only available on Windows";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ async function initThemes() {
|
||||
const defaultTheme = {
|
||||
id: "dark",
|
||||
name: "Dark",
|
||||
subtitle: "Darkness is my ally",
|
||||
subtitle: "Because Why Not ?",
|
||||
style: {
|
||||
background: {
|
||||
size: "cover",
|
||||
|
||||
@@ -30,15 +30,15 @@ function formatSize(bytes) {
|
||||
*/
|
||||
async function fetchVideoInfo(url) {
|
||||
try {
|
||||
const res = await fetch("/info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ url }),
|
||||
const res = await fetch(`/info?url=${encodeURIComponent(url)}`, {
|
||||
method: "GET",
|
||||
headers: {"Accept": "application/json"}
|
||||
});
|
||||
|
||||
if (!res.ok) return { error: `An Error occured when fetching info` };
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) return {error: `An Error occured when fetching info`};
|
||||
|
||||
if (!data) return { error: "Data is Missing" };
|
||||
|
||||
return data;
|
||||
@@ -269,7 +269,7 @@ async function init() {
|
||||
infoDiv.classList.add("visible");
|
||||
});
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
165
public/script/reportBug.js
Normal file
165
public/script/reportBug.js
Normal file
@@ -0,0 +1,165 @@
|
||||
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 id="bugReportServiceStatus" role="status" aria-live="polite" style="display:none !important; align-items:center !important; gap:8px !important; color:#aaaaaa !important; font-size:0.8rem !important;">
|
||||
<span id="bugReportServiceStatusDot" style="width:8px !important; height:8px !important; border-radius:50% !important; background:#f59e0b !important; flex-shrink:0 !important;"></span>
|
||||
<span id="bugReportServiceStatusText">Checking bug report service...</span>
|
||||
</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" id="bugSendBtn" disabled style="background:#4f46e5 !important; border:none !important; color:#ffffff !important; padding:8px 16px !important; border-radius:6px !important; cursor:not-allowed !important; opacity:0.5 !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");
|
||||
const sendButton = this.modal.querySelector("#bugSendBtn");
|
||||
|
||||
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();
|
||||
if (sendButton.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
title: titleInput.value,
|
||||
description: descInput.value,
|
||||
includeLogs: this.modal.querySelector("#bugIncludeLogs").value
|
||||
};
|
||||
|
||||
window.electronAPI.logInfo("Submitting:", data);
|
||||
|
||||
try {
|
||||
const success = await window.topbarAPI.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";
|
||||
this.updateServiceStatus();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.style.display = "none";
|
||||
}
|
||||
|
||||
async updateServiceStatus() {
|
||||
const statusText = this.modal.querySelector("#bugReportServiceStatusText");
|
||||
const statusDot = this.modal.querySelector("#bugReportServiceStatusDot");
|
||||
const sendButton = this.modal.querySelector("#bugSendBtn");
|
||||
|
||||
sendButton.disabled = true;
|
||||
sendButton.style.setProperty("cursor", "not-allowed", "important");
|
||||
sendButton.style.setProperty("opacity", "0.5", "important");
|
||||
|
||||
try {
|
||||
const serviceStatus = await window.topbarAPI.getReportServiceStatus();
|
||||
const serviceAvailable = serviceStatus.status === "online";
|
||||
const statusVisible = !serviceAvailable;
|
||||
statusText.textContent = serviceStatus.message;
|
||||
statusDot.style.background = "#ef4444";
|
||||
statusText.style.color = "#fca5a5";
|
||||
this.modal.querySelector("#bugReportServiceStatus").style.setProperty(
|
||||
"display",
|
||||
statusVisible ? "flex" : "none",
|
||||
"important"
|
||||
);
|
||||
sendButton.disabled = !serviceAvailable;
|
||||
sendButton.style.setProperty("cursor", serviceAvailable ? "pointer" : "not-allowed", "important");
|
||||
sendButton.style.setProperty("opacity", serviceAvailable ? "1" : "0.5", "important");
|
||||
} catch (err) {
|
||||
statusText.textContent = "Unable to check bug report service.";
|
||||
statusDot.style.background = "#ef4444";
|
||||
statusText.style.color = "#fca5a5";
|
||||
this.modal.querySelector("#bugReportServiceStatus").style.setProperty("display", "flex", "important");
|
||||
sendButton.disabled = true;
|
||||
window.electronAPI.logError("Failed to retrieve bug report service status:", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.bugModal = new BugReportModal();
|
||||
|
||||
document.getElementById("report-bug-btn").addEventListener("click", () => {
|
||||
window.bugModal.open();
|
||||
});
|
||||
@@ -16,8 +16,7 @@ let isOpen = false;
|
||||
* Toggles visibility of the settings panel when clicking the settings button.
|
||||
*/
|
||||
settingsButton.addEventListener("click", () => {
|
||||
const currentlyOpen = settingsPanel.style.display === "block";
|
||||
settingsPanel.style.display = currentlyOpen ? "none" : "block";
|
||||
settingsPanel.classList.toggle("open");
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -25,7 +24,7 @@ settingsButton.addEventListener("click", () => {
|
||||
*/
|
||||
function closePanel() {
|
||||
isOpen = false;
|
||||
settingsPanel.style.display = "none";
|
||||
settingsPanel.classList.remove("open");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,18 +17,20 @@ window.showToast = function (message, type = "info", duration = 4000) {
|
||||
toast.classList.add("toast", type);
|
||||
|
||||
const icons = {
|
||||
success: "✓",
|
||||
error: "✕",
|
||||
warning: "⚠",
|
||||
info: "ℹ"
|
||||
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">${icon}</span>
|
||||
<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");
|
||||
|
||||
@@ -5,47 +5,41 @@
|
||||
* This function is safe to call once DOM is ready.
|
||||
* It silently aborts if the API or elements are missing.
|
||||
*/
|
||||
function setupTopbarListeners() {
|
||||
export function initTopBar() {
|
||||
const { topbarAPI } = window;
|
||||
if (!topbarAPI) return;
|
||||
|
||||
const minBtn = document.getElementById("minimize-btn");
|
||||
const maxBtn = document.getElementById("maximize-btn");
|
||||
const closeBtn = document.getElementById("close-btn");
|
||||
const devtoolsBtn = document.getElementById("devtools-btn");
|
||||
const logsBtn = document.getElementById("logs-btn");
|
||||
const websiteBtn = document.getElementById("website-btn");
|
||||
const wikiBtn = document.getElementById("wiki-btn");
|
||||
const workshopBtn = document.getElementById("workshop-btn");
|
||||
|
||||
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers topbar event listeners after DOM is fully loaded.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", setupTopbarListeners);
|
||||
async function attachListeners(){
|
||||
try{
|
||||
const featuresList = await window.electronAPI.getFeatures();
|
||||
|
||||
/**
|
||||
* Applies UI layout adjustments depending on feature flags
|
||||
* provided by the Electron backend.
|
||||
*
|
||||
* Specifically handles:
|
||||
* - hiding custom topbar
|
||||
* - adjusting layout spacing
|
||||
* - repositioning theme switcher
|
||||
*/
|
||||
(async function applyFeatureLayout() {
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
if (!features.customTopBar) {
|
||||
if (!featuresList.customTopBar) {
|
||||
const topbar = document.getElementById("topbar");
|
||||
const container = document.getElementById("container");
|
||||
const themeSwitcher = document.getElementById("theme-switcher");
|
||||
@@ -54,4 +48,12 @@ document.addEventListener("DOMContentLoaded", setupTopbarListeners);
|
||||
if (container) container.style.marginTop = "0";
|
||||
if (themeSwitcher) themeSwitcher.style.top = "30px";
|
||||
}
|
||||
})();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to load layout features for topbar:", error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
initTopBar()
|
||||
document.addEventListener("DOMContentLoaded", attachListeners)
|
||||
128
public/styles/components/autoUpdate.css
Normal file
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;
|
||||
}
|
||||
229
public/styles/components/changelog.css
Normal file
229
public/styles/components/changelog.css
Normal file
@@ -0,0 +1,229 @@
|
||||
.changelog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(5, 10, 16, 0.72);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.changelog-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.changelog-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(680px, 100%);
|
||||
max-height: min(720px, 88vh);
|
||||
overflow: hidden;
|
||||
color: var(--settings-text-color, #f8fafc);
|
||||
background: var(--settings-modal-bg-color, #18212b);
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.changelog-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px 28px 18px;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.changelog-eyebrow {
|
||||
color: var(--fill-accent, #0ea5e9);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.changelog-header h2 {
|
||||
margin: 5px 0 0;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.changelog-close {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.changelog-close:hover {
|
||||
background: rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.changelog-list {
|
||||
overflow-y: auto;
|
||||
padding: 20px 28px 28px;
|
||||
scrollbar-color: var(--fill-accent, #0ea5e9) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.changelog-list::-webkit-scrollbar,
|
||||
.changelog-body pre::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.changelog-list::-webkit-scrollbar-track,
|
||||
.changelog-body pre::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.changelog-list::-webkit-scrollbar-thumb,
|
||||
.changelog-body pre::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--fill-accent, #0ea5e9) 70%, transparent);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 999px;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.changelog-list::-webkit-scrollbar-thumb:hover,
|
||||
.changelog-body pre::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--fill-accent, #0ea5e9);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.changelog-entry {
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.16);
|
||||
}
|
||||
|
||||
.changelog-entry:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.changelog-entry:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.changelog-entry-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.changelog-entry-heading h3 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.changelog-entry-heading span {
|
||||
flex-shrink: 0;
|
||||
color: var(--settings-subtitle-color, #94a3b8);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.changelog-body {
|
||||
margin: 12px 0 0;
|
||||
color: var(--settings-subtitle-color, #cbd5e1);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.changelog-body p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.changelog-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.changelog-body h1,
|
||||
.changelog-body h2,
|
||||
.changelog-body h3,
|
||||
.changelog-body h4,
|
||||
.changelog-body h5,
|
||||
.changelog-body h6 {
|
||||
margin: 14px 0 8px;
|
||||
color: var(--settings-text-color, #f8fafc);
|
||||
}
|
||||
|
||||
.changelog-body h1 { font-size: 20px; }
|
||||
.changelog-body h2 { font-size: 18px; }
|
||||
.changelog-body h3 { font-size: 16px; }
|
||||
.changelog-body h4,
|
||||
.changelog-body h5,
|
||||
.changelog-body h6 { font-size: 14px; }
|
||||
|
||||
.changelog-body ul,
|
||||
.changelog-body ol {
|
||||
margin: 8px 0 12px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.changelog-body li + li {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.changelog-body code {
|
||||
padding: 2px 5px;
|
||||
color: var(--settings-text-color, #f8fafc);
|
||||
background: rgba(148, 163, 184, 0.16);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.changelog-body pre {
|
||||
padding: 10px 12px;
|
||||
overflow-x: auto;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.changelog-body pre code {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.changelog-body blockquote {
|
||||
margin: 10px 0;
|
||||
padding-left: 12px;
|
||||
color: var(--settings-subtitle-color, #cbd5e1);
|
||||
border-left: 3px solid var(--fill-accent, #0ea5e9);
|
||||
}
|
||||
|
||||
.changelog-body a {
|
||||
color: var(--fill-accent, #0ea5e9);
|
||||
}
|
||||
|
||||
.changelog-loading,
|
||||
.changelog-empty {
|
||||
margin: 0;
|
||||
color: var(--settings-subtitle-color, #cbd5e1);
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.changelog-overlay {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.changelog-header,
|
||||
.changelog-list {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.changelog-entry-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
@@ -5,73 +5,70 @@
|
||||
right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Toast */
|
||||
.toast {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background-color: rgba(50, 50, 50, 0.9);
|
||||
border-radius: 6px;
|
||||
color: #ffffff;
|
||||
font-size: 0.9rem;
|
||||
max-width: 220px;
|
||||
min-width: 100px;
|
||||
animation: slideIn 0.25s ease-out;
|
||||
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;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background-color: rgba(40, 167, 69, 0.15);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background-color: rgba(220, 53, 69, 0.15);
|
||||
}
|
||||
|
||||
.toast.warning {
|
||||
background-color: rgba(255, 193, 7, 0.15);
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
background-color: rgba(23, 162, 184, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Toast Icon */
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toast.success .toast-icon {
|
||||
color: #28a745;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.toast.error .toast-icon {
|
||||
color: #dc3545;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.toast.warning .toast-icon {
|
||||
color: #ffc107;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.toast.info .toast-icon {
|
||||
color: #17a2b8;
|
||||
color: #0ea5e9;
|
||||
}
|
||||
|
||||
/* Toast Message */
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Toast Close Button */
|
||||
@@ -79,54 +76,83 @@
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: currentColor;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
opacity: 1;
|
||||
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: translateX(350px);
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(350px);
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.removing {
|
||||
animation: slideOut 0.25s ease-out;
|
||||
@keyframes progress {
|
||||
from {
|
||||
width: 100%;
|
||||
}
|
||||
to {
|
||||
width: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.removing {
|
||||
animation: slideOut 0.25s ease-in forwards;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
#toast-container {
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
top: auto;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
|
||||
@@ -45,11 +45,22 @@
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.2s ease-out;
|
||||
/*animation: slideIn 0.2s ease-out; */
|
||||
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 {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
.custom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-left: 12px;
|
||||
gap: 4px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* RIGHT - window buttons */
|
||||
@@ -43,11 +43,11 @@
|
||||
|
||||
/* Custom buttons (Logs / Wiki / etc.) */
|
||||
.custom-controls button {
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
|
||||
font-size: 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.custom-controls button:hover {
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
@import url("components/editpathbutton.css");
|
||||
@import url("components/progressBar.css");
|
||||
@import url("components/toast.css");
|
||||
@import url("components/autoUpdate.css");
|
||||
@import url("components/changelog.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');
|
||||
|
||||
|
||||
0
resources/binaries/linux/.gitkeep
Normal file
0
resources/binaries/linux/.gitkeep
Normal file
Binary file not shown.
0
resources/binaries/win-32/.gitkeep
Normal file
0
resources/binaries/win-32/.gitkeep
Normal file
Binary file not shown.
21
scripts/RELEASE-CHANGELOG.md
Normal file
21
scripts/RELEASE-CHANGELOG.md
Normal file
@@ -0,0 +1,21 @@
|
||||
### Features
|
||||
- **In-App Feedback**: Added a built-in feedback form allowing users to send feedback, bug reports, and daily logs directly to the development team.
|
||||
- **Community Contributions**: GitHub issues are now better maintained on the repository. If you want to contribute to the project, feel free to drop by and check out the `good first issue` tags!
|
||||
|
||||
### Improvements
|
||||
- **Bundle Size**: Optimized assets and dependencies, reducing the overall application bundle size by approximately 50% on specific version.
|
||||
- **Updater UI**: Reworked the update notification for better clarity. Windows and AppImage users can download updates seamlessly in-app, while unsupported Linux package users are gracefully redirected to the manual download page. Flatpak (.rpm) and Snap (.snap) users are not informed, because that updates are handled by their respective package managers.
|
||||
- **Toast Notifications**: Refactored the style and overall UX of toast alerts.
|
||||
- **Code Quality**: Implemented a linter to enforce coding standards.
|
||||
|
||||
### Bug Fixes
|
||||
- **Auto-Updater**: Fixed a major issue causing 404 Not Found errors when attempting to download updates for `.AppImage` releases.
|
||||
- **Linux Updates**: Fixed update crashes on `.rpm` and `.snap` packages. In-app update flows are now disabled for these formats to let the OS natively handle the process.
|
||||
- **Windows Logs**: Fixed a file naming issue with the log transport system on Windows.
|
||||
- **Desktop Entry Name**: Fixed the desktop entry name for Linux users, ensuring it displays correctly in application menus.
|
||||
|
||||
### Chores
|
||||
- **Dependencies**: Removed `dotenv` from the project.
|
||||
- **URL**: A new URL for the website is now available ! [freedomloader.acnolo.fr](https://freedomloader.acnolo.fr)
|
||||
- **Dev**: Add new npm script. To check linux file quality
|
||||
|
||||
@@ -6,34 +6,85 @@ 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
|
||||
|
||||
# Step 1 — Build .rpm from linux-unpacked via fpm
|
||||
# ------------------------------------------------------------
|
||||
# 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"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Step 1 - Build .rpm from linux-unpacked via fpm
|
||||
# ------------------------------------------------------------
|
||||
echo "Building RPM..."
|
||||
fpm -s dir -t rpm --force \
|
||||
"$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/" \
|
||||
--url "https://freedomloader.acnolo.fr/" \
|
||||
--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" \
|
||||
"$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"
|
||||
|
||||
"$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml" \
|
||||
"$ROOT_DIR/package/com.masteracnolo.freedomloader.desktop=/usr/share/applications/com.masteracnolo.freedomloader.desktop"
|
||||
rm -rf "$TMP_WRAPPER"
|
||||
echo "RPM built: $RPM_OUT"
|
||||
|
||||
# Step 2 — Build SRPM for COPR (wraps the .rpm above)
|
||||
# ------------------------------------------------------------
|
||||
# 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"
|
||||
|
||||
|
||||
198
scripts/release.sh
Executable file
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/scripts/RELEASE-CHANGELOG.md" ]; then
|
||||
echo "Warning: RELEASE-CHANGELOG.md not found, release notes will be empty."
|
||||
CHANGELOG=""
|
||||
else
|
||||
CHANGELOG="$(cat "$ROOT_DIR/scripts/RELEASE-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 "============================================"
|
||||
@@ -2,6 +2,7 @@ const { fetchInfo } = require("../services/info.services");
|
||||
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
|
||||
const { logger } = require("../logger");
|
||||
const { isValidUrl } = require("../helpers/validation.helpers");
|
||||
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
||||
|
||||
/**
|
||||
* Handles metadata retrieval for a video or playlist.
|
||||
@@ -18,7 +19,9 @@ const { isValidUrl } = require("../helpers/validation.helpers");
|
||||
*/
|
||||
async function infoController(req, res) {
|
||||
|
||||
const url = req.body.url || req.query.url; // Supports POST and GET requests
|
||||
// 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)
|
||||
|
||||
/**
|
||||
* Basic validation:
|
||||
@@ -30,10 +33,11 @@ async function infoController(req, res) {
|
||||
return res.status(400).send("Invalid URL Or Missing");
|
||||
}
|
||||
|
||||
logger.info(`Info request received. URL: ${url}`);
|
||||
logger.info(`Info request received. RAW URL: ${url}`);
|
||||
logger.info(`Info request received. ENCODED: ${encodedUrl}`);
|
||||
|
||||
// Lightweight heuristic to detect playlist URLs
|
||||
const isPlaylistUrl = url.includes("&list") || url.includes("?list");
|
||||
// Lightweight heuristic to detect playlist URLs. It works for Youtube.
|
||||
const isPlaylistUrl = isUrlPlaylist(url);
|
||||
|
||||
logger.info(
|
||||
isPlaylistUrl
|
||||
@@ -73,11 +77,11 @@ async function infoController(req, res) {
|
||||
);
|
||||
|
||||
// Preserve original error context
|
||||
throw err;
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw err;
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +90,16 @@ async function infoController(req, res) {
|
||||
*/
|
||||
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(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const path = require("path");
|
||||
|
||||
const getUserBrowser = require("./getBrowser.helpers.js");
|
||||
const {getUserBrowser} = require("./getBrowser.helpers.js");
|
||||
const { ffmpegPath, denoPath} = require("./path.helpers.js");
|
||||
const { configFeatures } = require("../../config.js");
|
||||
const { logger } = require("../logger.js");
|
||||
const config = require("../../config")
|
||||
|
||||
/**
|
||||
* Validates whether the provided codec is supported by the application.
|
||||
@@ -62,7 +63,7 @@ function validateCodec(codec){
|
||||
*
|
||||
* @returns {string[]} Array of arguments ready to be passed to yt-dlp.
|
||||
*/
|
||||
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
function buildYtDlpArgs({url, audioOnly, quality, outputFolder, isPlaylist}) {
|
||||
|
||||
logger.info("--- CONFIGURATION ---");
|
||||
|
||||
@@ -70,6 +71,7 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
||||
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
||||
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
||||
logger.info(`CONFIG keepPlaylistOrder: ${configFeatures.keepPlaylistOrder}`);
|
||||
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
||||
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
||||
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
||||
@@ -117,7 +119,27 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
return args.filter(Boolean);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const notify = require("./notify.helpers")
|
||||
const notify = require("./notify.helpers");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
/**
|
||||
@@ -22,41 +21,38 @@ const { logger } = require("../logger");
|
||||
* @returns {string} The detected browser identifier.
|
||||
*/
|
||||
function getUserBrowser() {
|
||||
//const userProfile = os.homedir();
|
||||
const firefoxPath = getFirefoxProfilePath();
|
||||
const platform = process.platform;
|
||||
let isFirefoxHere = false;
|
||||
|
||||
/**
|
||||
* Browsers supported by yt-dlp cookie extraction.
|
||||
*
|
||||
* Only Firefox is currently enabled and tested.
|
||||
* Additional browsers can be enabled in the future
|
||||
* once compatibility has been verified.
|
||||
*/
|
||||
const browsers = [
|
||||
{ name: "firefox", path: firefoxPath },
|
||||
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
||||
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
|
||||
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
|
||||
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
|
||||
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
|
||||
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
|
||||
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
|
||||
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`,
|
||||
];
|
||||
|
||||
// Search for the first available browser profile.
|
||||
for (const browser of browsers) {
|
||||
if (browser.path && fs.existsSync(browser.path)) {
|
||||
logger.info(`Browser found: ${browser.name}`);
|
||||
return browser.name;
|
||||
}
|
||||
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");
|
||||
|
||||
// If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad
|
||||
if (process.platform === "win32") {
|
||||
notify.notifyFirefoxBrowserMissing();
|
||||
} else {
|
||||
logger.info("Browser found: firefox");
|
||||
}
|
||||
|
||||
// Fallback to Firefox and let yt-dlp handle the error gracefully.
|
||||
@@ -64,39 +60,4 @@ function getUserBrowser() {
|
||||
return "firefox";
|
||||
}
|
||||
|
||||
function getFirefoxProfilePath() {
|
||||
const home = os.homedir();
|
||||
|
||||
if (process.platform === "win32") {
|
||||
return path.join(
|
||||
home,
|
||||
"AppData",
|
||||
"Roaming",
|
||||
"Mozilla",
|
||||
"Firefox",
|
||||
"Profiles"
|
||||
);
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
return path.join(
|
||||
"usr",
|
||||
"bin",
|
||||
"firefox"
|
||||
);
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
return path.join(
|
||||
home,
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Firefox",
|
||||
"Profiles"
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = getUserBrowser
|
||||
module.exports = {getUserBrowser};
|
||||
@@ -2,6 +2,12 @@ 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.
|
||||
*
|
||||
@@ -21,7 +27,27 @@ function notifyDownloadFinished(folder, notifyEnabled = true) {
|
||||
icon: iconPaths.confirm,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openPath(folder));
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -41,11 +67,16 @@ function notifyCookiesBrowserError(){
|
||||
icon: iconPaths.error,
|
||||
});
|
||||
|
||||
notif.on("click", () =>
|
||||
shell.openExternal(
|
||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||
)
|
||||
);
|
||||
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();
|
||||
}
|
||||
@@ -65,11 +96,16 @@ function notifyFirefoxBrowserMissing() {
|
||||
icon: iconPaths.error,
|
||||
});
|
||||
|
||||
notif.on("click", () =>
|
||||
shell.openExternal(
|
||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||
)
|
||||
);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ const binaryPaths = {
|
||||
* - user themes are persisted in userData to survive updates
|
||||
*/
|
||||
const userThemesPath = path.join(app.getPath("userData"), "themes");
|
||||
const defaultThemesSourcePath = path.join(resourcesPath, "theme");
|
||||
const defaultThemesSourcePath = path.join("theme");
|
||||
|
||||
/**
|
||||
* Initializes user theme directory and ensures default themes exist.
|
||||
@@ -260,7 +260,7 @@ function validateBinaries() {
|
||||
|
||||
/**
|
||||
* Helper to lazy load logger, avoid circular import
|
||||
* @returns {winston.Logger}
|
||||
* @returns {import("winston").Logger}
|
||||
*/
|
||||
function getLogger() {
|
||||
return require("../logger.js").logger;
|
||||
|
||||
12
server/helpers/playlist.helpers.js
Normal file
12
server/helpers/playlist.helpers.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Determines whether a URL targets a playlist.
|
||||
*
|
||||
* The detection is based on the presence of the YouTube
|
||||
* playlist query parameter (`list`).
|
||||
*
|
||||
* @param {string} url - URL to inspect.
|
||||
* @returns {boolean} True if the URL appears to be a playlist.
|
||||
*/
|
||||
export function isUrlPlaylist(url) {
|
||||
return url.includes("?list=") || url.includes("&list=") || url.includes("@");
|
||||
}
|
||||
@@ -17,9 +17,29 @@ try {
|
||||
console.error(`Failed to create log directory: ${error.message}`);
|
||||
}
|
||||
|
||||
const logFormat = format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||
/**
|
||||
* Format used by the saved file
|
||||
* @type {Format}
|
||||
*/
|
||||
const fileFormat = format.combine(
|
||||
format.timestamp({format: "YYYY-MM-DD HH:mm:ss"}),
|
||||
format.errors({stack: true}), // Capture la stack trace complète des erreurs
|
||||
format.printf(({timestamp, level, message, stack}) => {
|
||||
return `${timestamp} | ${level.toUpperCase()} | ${stack || message}`;
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Formated used by the IDE/Console
|
||||
* @type {Format}
|
||||
*/
|
||||
const consoleFormat = format.combine(
|
||||
format.colorize({all: true}),
|
||||
format.timestamp({format: "HH:mm:ss"}),
|
||||
format.errors({stack: true}),
|
||||
format.printf(({timestamp, level, message, stack}) => {
|
||||
return `[${timestamp}] ${level}: ${stack || message}`;
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -33,7 +53,6 @@ const logFormat = format.combine(
|
||||
*/
|
||||
const logger = createLogger({
|
||||
level: "info",
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new DailyRotateFile({
|
||||
dirname: logDir,
|
||||
@@ -41,11 +60,11 @@ const logger = createLogger({
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: false,
|
||||
maxFiles: "7d",
|
||||
format: logFormat,
|
||||
format: fileFormat,
|
||||
options: { flags: "a" },
|
||||
}),
|
||||
new transports.Console({
|
||||
format: logFormat,
|
||||
format: consoleFormat,
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -54,16 +73,17 @@ const logger = createLogger({
|
||||
* Start Log Session
|
||||
*/
|
||||
function logSessionStart(logDir, downloadPath) {
|
||||
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
|
||||
logger.info(`--- Starting session: ${new Date().toLocaleString('sv-SE')} ---`);
|
||||
logger.info("============================================================")
|
||||
logger.info(`Application Version: ${config.version}`)
|
||||
logSystemInfo(logger, logDir, downloadPath)
|
||||
if (typeof logSystemInfo === 'function') logSystemInfo(logger, logDir, downloadPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Log Session
|
||||
*/
|
||||
function logSessionEnd() {
|
||||
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
|
||||
logger.info(`--- Ending session: ${new Date().toLocaleString('sv-SE')} ---`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -2,6 +2,6 @@ const express = require("express");
|
||||
const router = express.Router();
|
||||
const { infoController } = require("../controller/info.controller");
|
||||
|
||||
router.post("/", infoController);
|
||||
router.get("/", infoController);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,11 +1,17 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const { logger, logSessionEnd } = require("./logger");
|
||||
const config = require("../config");
|
||||
|
||||
const {logger} = require("./logger");
|
||||
const { rateLimit } = require("./helpers/rateLimit.helpers");
|
||||
|
||||
const app = express();
|
||||
|
||||
/**
|
||||
* Store the state of the server
|
||||
*/
|
||||
let serverInstance = null;
|
||||
|
||||
// Middlewares
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
@@ -14,6 +20,8 @@ app.use(express.static(path.join(__dirname, "../public")));
|
||||
// Routes
|
||||
app.use("/download", require("./routes/download.route"));
|
||||
app.use("/info", require("./routes/info.route"));
|
||||
|
||||
// Interface
|
||||
app.get("/", rateLimit, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||
});
|
||||
@@ -28,33 +36,32 @@ app.get("/", rateLimit, (req, res) => {
|
||||
*/
|
||||
async function startServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = app.listen(config.applicationPort, () => {
|
||||
const serverInstance = app.listen(config.applicationPort, () => {
|
||||
logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
|
||||
resolve(server);
|
||||
resolve(serverInstance);
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
serverInstance.on("error", (err) => {
|
||||
logger.error("Express server error:", err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
/**
|
||||
* Clean exit function
|
||||
* - Stop Log
|
||||
* - Stop Express Server
|
||||
* - Stop Electron App
|
||||
*/
|
||||
const gracefulExit = () => {
|
||||
logSessionEnd();
|
||||
server.close(() => {
|
||||
logger.info("Express server closed cleanly.");
|
||||
process.exit();
|
||||
});
|
||||
};
|
||||
|
||||
process.on("SIGINT", gracefulExit);
|
||||
process.on("SIGTERM", gracefulExit);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { startServer };
|
||||
/**
|
||||
* Clean exit function called by Electron
|
||||
*/
|
||||
function stopServer() {
|
||||
if (serverInstance) {
|
||||
try{
|
||||
serverInstance.close();
|
||||
logger.info("Express server closed cleanly");
|
||||
} catch (err){
|
||||
logger.error("Express server was not closed cleanly:", err)
|
||||
}
|
||||
} else {
|
||||
logger.info("Express server was already closed");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { startServer, stopServer };
|
||||
@@ -6,7 +6,8 @@ const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
|
||||
const notify = require("../helpers/notify.helpers");
|
||||
const path = require("path");
|
||||
const { isSafePath } = require("../helpers/validation.helpers");
|
||||
const { configFeatures } = require("../../config.js");
|
||||
const {reloadFeatures} = require("../../config");
|
||||
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
||||
|
||||
/**
|
||||
* Reference to the currently running yt-dlp process.
|
||||
@@ -18,19 +19,6 @@ const { configFeatures } = require("../../config.js");
|
||||
*/
|
||||
let currentDownloadProcess = null;
|
||||
|
||||
/**
|
||||
* Determines whether a URL targets a playlist.
|
||||
*
|
||||
* The detection is based on the presence of the YouTube
|
||||
* playlist query parameter (`list`).
|
||||
*
|
||||
* @param {string} url - URL to inspect.
|
||||
* @returns {boolean} True if the URL appears to be a playlist.
|
||||
*/
|
||||
function isPlaylistUrl(url) {
|
||||
return url.includes("?list=") || url.includes("&list=");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dedicated download folder for a playlist.
|
||||
*
|
||||
@@ -77,7 +65,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
||||
}
|
||||
|
||||
logger.error(`Could not find available playlist folder after 1000 attempts`);
|
||||
throw new Error("Unable to create playlist folder");
|
||||
logger.error("Unable to create playlist folder");
|
||||
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
|
||||
@@ -123,7 +111,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
||||
* @param {string} [options.outputFolder] - Destination folder.
|
||||
* @param {string} [options.playlistTitle] - Playlist title.
|
||||
*
|
||||
* @param {Function[]} listeners - Progress listeners.
|
||||
* @param {Function[]} progressListeners - Progress listeners.
|
||||
* @param {Function[]} speedListeners - Download speed listeners.
|
||||
* @param {Function[]} stageListeners - Processing stage listeners.
|
||||
* @param {Function[]} playlistInfoListeners - Playlist progress listeners.
|
||||
@@ -132,10 +120,13 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
||||
* Resolves with the final output folder path once the
|
||||
* download has completed successfully.
|
||||
*/
|
||||
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
|
||||
function fetchDownload(options, progressListeners, speedListeners, stageListeners, playlistInfoListeners) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info(`CONFIG createPlaylistFolders: ${configFeatures.createPlaylistFolders}`);
|
||||
|
||||
const userConfig = reloadFeatures();
|
||||
|
||||
logger.info(`CONFIG createPlaylistFolders: ${userConfig.createPlaylistFolders}`);
|
||||
|
||||
let outputFolder = options.outputFolder || defaultDownloadFolder;
|
||||
|
||||
@@ -157,9 +148,9 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
}
|
||||
|
||||
// Détecte si c'est une playlist et crée un dossier approprié
|
||||
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url);
|
||||
const isPlaylist = options.playlistTitle || isUrlPlaylist(options.url);
|
||||
|
||||
if (isPlaylist && configFeatures.createPlaylistFolders) {
|
||||
if (isPlaylist && userConfig.createPlaylistFolders) {
|
||||
try {
|
||||
const playlistName = options.playlistTitle || "Untitled Playlist";
|
||||
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
|
||||
@@ -168,11 +159,16 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
logger.error(`Failed to create playlist folder: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
} else if (isPlaylist && !configFeatures.createPlaylistFolders) {
|
||||
} else if (isPlaylist && !userConfig.createPlaylistFolders) {
|
||||
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
|
||||
}
|
||||
|
||||
const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder });
|
||||
const args = buildYtDlpArgs({
|
||||
...options,
|
||||
outputFolder: safeOutputFolder,
|
||||
isPlaylist
|
||||
});
|
||||
|
||||
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||
|
||||
const child = execFile(userYtDlp, args);
|
||||
@@ -182,7 +178,7 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
|
||||
child.on("close", code => {
|
||||
currentDownloadProcess = null;
|
||||
listeners.forEach(fn => fn("done"));
|
||||
progressListeners.forEach(fn => fn("done"));
|
||||
if (code === 0) resolve(safeOutputFolder);
|
||||
else if (code === null) reject(new Error(`Download cancelled by user`));
|
||||
else reject(new Error(`YT-DLP failed with code : ${code}`));
|
||||
@@ -196,11 +192,11 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
|
||||
// Progress Bar
|
||||
if (line.startsWith("[download] Destination:")) {
|
||||
listeners.forEach(fn => fn("reset"));
|
||||
progressListeners.forEach(fn => fn("reset"));
|
||||
stageListeners.forEach(fn => fn("Downloading..."));
|
||||
}
|
||||
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
||||
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
|
||||
if (match) progressListeners.forEach(fn => fn(parseFloat(match[1])));
|
||||
|
||||
if (line.includes("MiB/s") || line.includes("KiB/s")) {
|
||||
const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp, denoPath } = require("../helpers/path.helpers");
|
||||
const getUserBrowser = require("../helpers/getBrowser.helpers");
|
||||
const {getUserBrowser} = require("../helpers/getBrowser.helpers");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
/**
|
||||
@@ -61,6 +61,7 @@ function fetchInfo(url) {
|
||||
|
||||
try {
|
||||
const data = JSON.parse(stdout);
|
||||
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
logger.error(`JSON Parsing Error: ${e.message}`);
|
||||
|
||||
BIN
theme/dark.zip
BIN
theme/dark.zip
Binary file not shown.
Reference in New Issue
Block a user