mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-11-13 15:09:20 +01:00
Compare commits
411 Commits
f0971ebccc
...
1.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2b51d32d1 | ||
|
|
81515c6ab8 | ||
|
|
c88189b44f | ||
|
|
b745ebd29f | ||
|
|
13ee4c170b | ||
|
|
0218664c5a | ||
|
|
6afc5e06d6 | ||
|
|
519dcad6e6 | ||
|
|
ac94d68224 | ||
|
|
0f876fcb70 | ||
|
|
a6c11ba7c4 | ||
|
|
44132dd7b5 | ||
|
|
15ab6892b7 | ||
|
|
725b6f0ada | ||
|
|
ad0e4f858b | ||
|
|
68108e3a66 | ||
|
|
eff2affd42 | ||
|
|
87cf05643f | ||
|
|
ce14b8abd7 | ||
|
|
44ed1bbf0e | ||
|
|
e0d017d9fd | ||
|
|
c3ea1ac0bd | ||
|
|
238d4ea717 | ||
|
|
d879a5eb1d | ||
|
|
40bea35286 | ||
|
|
83ac76381a | ||
|
|
6ff0c71be7 | ||
|
|
fd0b339bcc | ||
|
|
bc5faf38b6 | ||
|
|
e744e55475 | ||
|
|
a581c9309d | ||
|
|
61040769ee | ||
|
|
06f6a30797 | ||
|
|
a2653eef57 | ||
|
|
89573d11f7 | ||
|
|
f0c28a7f5e | ||
|
|
f3c37c2498 | ||
|
|
8fe4e7c38a | ||
|
|
912f4042e5 | ||
|
|
5dc61b4a15 | ||
|
|
5fb49b0c9d | ||
|
|
75fdf91fcd | ||
|
|
011e0a703a | ||
|
|
5aebc78d44 | ||
|
|
60137a15dc | ||
|
|
d05b9e2511 | ||
|
|
ebb48f4f8b | ||
|
|
8be47b9821 | ||
|
|
ca8b107f61 | ||
|
|
f25d07cb83 | ||
|
|
49e1e1b507 | ||
|
|
23d11d8933 | ||
|
|
9f88fe19c3 | ||
|
|
9b4b0306c7 | ||
|
|
96401a6e1f | ||
|
|
0c7103c5c9 | ||
|
|
798084e3bf | ||
|
|
5c4e6669fe | ||
|
|
00c542925c | ||
|
|
24460cf0b5 | ||
|
|
a3c1b1d753 | ||
|
|
5ade3fe144 | ||
|
|
577069b471 | ||
|
|
a4da47647a | ||
|
|
c5b29a0647 | ||
|
|
90e1a5fde7 | ||
|
|
dcfea2073c | ||
|
|
0be1ef1710 | ||
|
|
da393b19d9 | ||
|
|
1c27cc19cf | ||
|
|
ff7366c46c | ||
|
|
459d4c6df4 | ||
|
|
d2ca3e99d2 | ||
|
|
8ad3ea55cc | ||
|
|
71dbd104f8 | ||
|
|
57a5defb08 | ||
|
|
25c78ff8b7 | ||
|
|
792a59d8d7 | ||
|
|
b13c98dda2 | ||
|
|
1f103c20b9 | ||
|
|
70d62959ca | ||
|
|
83ca963502 | ||
|
|
9fb9295093 | ||
|
|
74fcdbb738 | ||
|
|
32c9a66a35 | ||
|
|
45e0840fea | ||
|
|
19b3f8b947 | ||
|
|
cc5bde5b48 | ||
|
|
9a48ac05e3 | ||
|
|
5750444655 | ||
|
|
fdb22f2342 | ||
|
|
760550e521 | ||
|
|
334e83ae1f | ||
|
|
5d776594fc | ||
|
|
adb36e3fa2 | ||
|
|
87945fba80 | ||
|
|
2234f9ed81 | ||
|
|
7cd3b4be06 | ||
|
|
a42b020b96 | ||
|
|
c1e70dae80 | ||
|
|
6877c51c0e | ||
|
|
5a41090175 | ||
|
|
5983f21759 | ||
|
|
ee9e02410f | ||
|
|
909480083e | ||
|
|
ca0492ed04 | ||
|
|
6c73dd75be | ||
|
|
556dccec08 | ||
|
|
f720d7c889 | ||
|
|
bb23f37fb5 | ||
|
|
220e81d8b2 | ||
|
|
44d8772827 | ||
|
|
335fb9652b | ||
|
|
243b98cbf9 | ||
|
|
23da75a17b | ||
|
|
756d2c048e | ||
|
|
659c1d4cf9 | ||
|
|
443fd31170 | ||
|
|
6d8095ceb2 | ||
|
|
c18cb6c8aa | ||
|
|
a3c5025434 | ||
|
|
3d7986cb87 | ||
|
|
3f8701567f | ||
|
|
ccb16c49d5 | ||
|
|
86d9ac3592 | ||
|
|
43d87c2b47 | ||
|
|
19aa8ed8fa | ||
|
|
3ddfaa999e | ||
|
|
3d7f3b7481 | ||
|
|
cfe2e17313 | ||
|
|
dedf10c6b3 | ||
|
|
ac0b4a0750 | ||
|
|
29ca2ea496 | ||
|
|
2a3905bdfb | ||
|
|
d3755ab57d | ||
|
|
9824e107fe | ||
|
|
e437722937 | ||
|
|
fd8c4ae33c | ||
|
|
967144bd55 | ||
|
|
cd8eb05020 | ||
|
|
590060e504 | ||
|
|
26c7d4d2ee | ||
|
|
caa987fa42 | ||
|
|
ccb090e4b2 | ||
|
|
8d070ed10d | ||
|
|
343dbb26a2 | ||
|
|
fef4f6de36 | ||
|
|
1a61972750 | ||
|
|
45ae5d7f98 | ||
|
|
fecb123d0e | ||
|
|
5c62ed081a | ||
|
|
8f0f31041d | ||
|
|
0e48ab3b4c | ||
|
|
d4c33bcf2d | ||
|
|
2f105f5145 | ||
|
|
b7a193b062 | ||
|
|
65364d3095 | ||
|
|
c356e2e30d | ||
|
|
e45810b54c | ||
|
|
55db30febc | ||
|
|
163faec221 | ||
|
|
5398fed928 | ||
|
|
f959171db5 | ||
|
|
01b6bcd02c | ||
|
|
0ff4715b35 | ||
|
|
745a73a5b0 | ||
|
|
a69114edce | ||
|
|
489a4766fa | ||
|
|
adcee62386 | ||
|
|
b867d819b7 | ||
|
|
31d8b20a76 | ||
|
|
42ae0e7204 | ||
|
|
43869420c0 | ||
|
|
6c9d296fbd | ||
|
|
008428f10a | ||
|
|
ae7d338474 | ||
|
|
12ff25cc4b | ||
|
|
f31767e7e7 | ||
|
|
7c4f2841e1 | ||
|
|
a27b3555f6 | ||
|
|
c451ddd042 | ||
|
|
da6311055f | ||
|
|
945efb2059 | ||
|
|
ab2881f8e9 | ||
|
|
f23811fff4 | ||
|
|
19ab77501b | ||
|
|
37eeed583a | ||
|
|
caf08efb90 | ||
|
|
7ff998e4df | ||
|
|
0c1e8fe899 | ||
|
|
4158e8c6e3 | ||
|
|
653319e408 | ||
|
|
0a4f14fba7 | ||
|
|
fb4571fc51 | ||
|
|
c11fd40681 | ||
|
|
cbfde8b761 | ||
|
|
c774821cc4 | ||
|
|
2ed15a32bc | ||
|
|
c252853603 | ||
|
|
cfdddec073 | ||
|
|
97a63ab791 | ||
|
|
a0e00341e1 | ||
|
|
26d42491ce | ||
|
|
d678ba2f12 | ||
|
|
55b400eb05 | ||
|
|
0a6313b1f3 | ||
|
|
b4312376bb | ||
|
|
5ffb143a1a | ||
|
|
ee24473077 | ||
|
|
2b98a63f92 | ||
|
|
2e594d87e8 | ||
|
|
b343c2869d | ||
|
|
094b93f4fa | ||
|
|
cdc8a623c0 | ||
|
|
2b43c00a3a | ||
|
|
14928798d4 | ||
|
|
e5fb44fc6b | ||
|
|
9f9e4cd1d8 | ||
|
|
45fe7336fe | ||
|
|
b909e7d917 | ||
|
|
ce62748ade | ||
|
|
d4ecc36f6b | ||
|
|
fee364610e | ||
|
|
492af8525b | ||
|
|
fb58b5ae06 | ||
|
|
577034b0bd | ||
|
|
6fc452edb4 | ||
|
|
086ca49599 | ||
|
|
be1de35da8 | ||
|
|
3c1f57667f | ||
|
|
5997cd20b4 | ||
|
|
6220c9400e | ||
|
|
48a16b927f | ||
|
|
87545d77b4 | ||
|
|
4e0fe29c20 | ||
|
|
f8e3bc843d | ||
|
|
b3f1edaabd | ||
|
|
ef784dd19c | ||
|
|
69bbcee885 | ||
|
|
60b4a99506 | ||
|
|
c1030400f5 | ||
|
|
3f508da501 | ||
|
|
0437cb61a2 | ||
|
|
1372582c54 | ||
|
|
070c3d507f | ||
|
|
3e9c545952 | ||
|
|
e3845e1849 | ||
|
|
91703d6a91 | ||
|
|
cff3101b03 | ||
|
|
c9a48ec64c | ||
|
|
e6419a9759 | ||
|
|
9d1d2fc7a8 | ||
|
|
cdca4a82ab | ||
|
|
c2e4e12512 | ||
|
|
96c1c6717d | ||
|
|
abf776175c | ||
|
|
d6de184bfd | ||
|
|
12249c4e8a | ||
|
|
f1fbc3f0c7 | ||
|
|
bb8cf54bf9 | ||
|
|
dccf8bd786 | ||
|
|
793bb79d76 | ||
|
|
f5cdf27ce6 | ||
|
|
627c56cd25 | ||
|
|
783871d131 | ||
|
|
138c05f995 | ||
|
|
5009a479af | ||
|
|
2a95a10113 | ||
|
|
9d6065c80a | ||
|
|
cd8b2bf007 | ||
|
|
1faf74fbc4 | ||
|
|
09163afbcd | ||
|
|
405d141055 | ||
|
|
599e09fa9f | ||
|
|
4954de5f1a | ||
|
|
ac854ae559 | ||
|
|
9944f05139 | ||
|
|
f6558b656a | ||
|
|
bfbf57ba38 | ||
|
|
d0af35377c | ||
|
|
59c263a246 | ||
|
|
42700bd7d8 | ||
|
|
5a9b364b4b | ||
|
|
3619550c74 | ||
|
|
31cc4012ac | ||
|
|
2e329a8584 | ||
|
|
d92dd59f86 | ||
|
|
5bb7ca377d | ||
|
|
9ee9a3574c | ||
|
|
dc50a44682 | ||
|
|
4be5c14999 | ||
|
|
b92ffebdb4 | ||
|
|
7b6e48b09e | ||
|
|
368851b4bb | ||
|
|
fa2da5712d | ||
|
|
7df97d70bb | ||
|
|
5c5f595829 | ||
|
|
5b659e3d8a | ||
|
|
1a526777cc | ||
|
|
b0d99d4c29 | ||
|
|
c7554475a9 | ||
|
|
cab177e8a7 | ||
|
|
f7244a69ac | ||
|
|
c6ed79ed1d | ||
|
|
00e5358bbb | ||
|
|
598c35eb54 | ||
|
|
00c74ea6c6 | ||
|
|
0c415c9106 | ||
|
|
b0495d033d | ||
|
|
d72cc6e0ca | ||
|
|
b59cb9f57f | ||
|
|
af743be5b0 | ||
|
|
024d039701 | ||
|
|
e9a91656bc | ||
|
|
3505606b44 | ||
|
|
815abae0a0 | ||
|
|
004271241e | ||
|
|
8295967ff2 | ||
|
|
053dca365c | ||
|
|
62110b1a5b | ||
|
|
aae9b2866c | ||
|
|
0d8bddbc20 | ||
|
|
2a7d61bab7 | ||
|
|
de71035fa0 | ||
|
|
5368fb9311 | ||
|
|
3d583a9b6f | ||
|
|
94de0a42f1 | ||
|
|
5ba2901273 | ||
|
|
f5888b8382 | ||
|
|
6ece182629 | ||
|
|
777257b71d | ||
|
|
df0156d603 | ||
|
|
68cdad06ab | ||
|
|
ee7c6520b9 | ||
|
|
4ed1609e41 | ||
|
|
b64e61d45d | ||
|
|
b4c2c865d8 | ||
|
|
e32ffd8fa0 | ||
|
|
8be6a932e5 | ||
|
|
6ed59fd272 | ||
|
|
4df1cb207f | ||
|
|
d03e3bc7e5 | ||
|
|
df02acb97f | ||
|
|
6ef04700ab | ||
|
|
766694f330 | ||
|
|
93380b2f36 | ||
|
|
8c969d694f | ||
|
|
f2f3d42e8c | ||
|
|
8d8973acfc | ||
|
|
281bff2379 | ||
|
|
e30f54050f | ||
|
|
0ae808c1a0 | ||
|
|
2598573844 | ||
|
|
27782376a3 | ||
|
|
876f7e1db0 | ||
|
|
db654c5f82 | ||
|
|
751f3d5c3e | ||
|
|
2a8d307bc8 | ||
|
|
ec6963cff3 | ||
|
|
196a07977a | ||
|
|
f20366f0c7 | ||
|
|
f54dc18e85 | ||
|
|
251049c7fe | ||
|
|
5d347967b1 | ||
|
|
d5c506c4e0 | ||
|
|
6718f4133e | ||
|
|
9ff6678730 | ||
|
|
3957450fdb | ||
|
|
25e50a6e7a | ||
|
|
22f935f678 | ||
|
|
58b843630b | ||
|
|
dec3f3272a | ||
|
|
f3252c2548 | ||
|
|
ac28afb17a | ||
|
|
b1a42f44fa | ||
|
|
78eea2fa0f | ||
|
|
4290e6db52 | ||
|
|
0ffc1af218 | ||
|
|
10cefdba19 | ||
|
|
c888946588 | ||
|
|
9026c63979 | ||
|
|
cfc2c5bbaa | ||
|
|
a41da5c4c2 | ||
|
|
b4242e4295 | ||
|
|
1f0650a71c | ||
|
|
87fe8af6f5 | ||
|
|
592d3a5ae4 | ||
|
|
dc847a5a8c | ||
|
|
250e6c3f23 | ||
|
|
1ffe9dcea0 | ||
|
|
b40a4fc033 | ||
|
|
0900e1b485 | ||
|
|
432633af52 | ||
|
|
152ee39535 | ||
|
|
9d824f5c33 | ||
|
|
accb55e538 | ||
|
|
bfb3e0cbdf | ||
|
|
b9460dbceb | ||
|
|
1216e5f10d | ||
|
|
8b4b3c84e0 | ||
|
|
f7abca90cb | ||
|
|
83b31cb645 | ||
|
|
bb967834f0 | ||
|
|
e9f4b80b7b | ||
|
|
abd5295395 | ||
|
|
d4d36c39ca | ||
|
|
95701bf56e | ||
|
|
7b93413050 | ||
|
|
c2c69935b1 | ||
|
|
07485f39ba | ||
|
|
0c99e48950 |
3
.github/ISSUE_TEMPLATE/performance-issue.md
vendored
3
.github/ISSUE_TEMPLATE/performance-issue.md
vendored
@@ -29,9 +29,6 @@ Clear description of the performance issue.
|
|||||||
- **Storage**: [e.g. SSD/HDD]
|
- **Storage**: [e.g. SSD/HDD]
|
||||||
- **Other Applications Opened**: [e.g. Discord/Opera GX]
|
- **Other Applications Opened**: [e.g. Discord/Opera GX]
|
||||||
|
|
||||||
> [!INFO]
|
|
||||||
> At the start of the application, SystemInfo is printed in the logs, so you can just copy and paste it!
|
|
||||||
|
|
||||||
## Reproduction Details
|
## Reproduction Details
|
||||||
|
|
||||||
- **Download URL**: [if applicable]
|
- **Download URL**: [if applicable]
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/question.md
vendored
4
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -9,8 +9,8 @@ assignees: 'MasterAcnolo'
|
|||||||
|
|
||||||
## Pre-submission Checklist
|
## Pre-submission Checklist
|
||||||
|
|
||||||
- [ ] I have checked the [FAQ](https://freedomloader.acnolo.fr/#/faq)
|
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/pages/faq)
|
||||||
- [ ] I have reviewed the [Wiki](https://freedomloader.acnolo.fr/#/wiki)
|
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/pages/wiki)
|
||||||
- [ ] I have searched existing issues
|
- [ ] I have searched existing issues
|
||||||
|
|
||||||
## Question
|
## Question
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Brief description of what this PR does.
|
Brief description of what this PR does.
|
||||||
|
|
||||||
## Related Issue (if applicable)
|
## Related Issue
|
||||||
|
|
||||||
Fixes #(issue number)
|
Fixes #(issue number)
|
||||||
Closes #(issue number)
|
Closes #(issue number)
|
||||||
@@ -30,7 +30,7 @@ Related to #(issue number)
|
|||||||
- [ ] I have tested with different download scenarios (video/audio, playlists)
|
- [ ] I have tested with different download scenarios (video/audio, playlists)
|
||||||
- [ ] I have checked the application logs for errors
|
- [ ] I have checked the application logs for errors
|
||||||
- [ ] I have verified the UI changes (if applicable)
|
- [ ] I have verified the UI changes (if applicable)
|
||||||
- [ ] I have tested on Windows 10 and/or Windows 11 or in any Linux Distribution
|
- [ ] I have tested on Windows 10 and/or Windows 11
|
||||||
|
|
||||||
## Screenshots (if applicable)
|
## Screenshots (if applicable)
|
||||||
|
|
||||||
|
|||||||
46
.github/workflows/codeql.yml
vendored
46
.github/workflows/codeql.yml
vendored
@@ -1,46 +0,0 @@
|
|||||||
name: "CodeQL PR"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
schedule:
|
|
||||||
- cron: '00 00 * * 1'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze (${{ matrix.language }})
|
|
||||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
|
||||||
permissions:
|
|
||||||
security-events: write
|
|
||||||
packages: read
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- language: javascript-typescript
|
|
||||||
build-mode: none
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v4
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
build-mode: ${{ matrix.build-mode }}
|
|
||||||
|
|
||||||
- name: Run manual build steps
|
|
||||||
if: matrix.build-mode == 'manual'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo 'Manual build required'
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v4
|
|
||||||
with:
|
|
||||||
category: "/language:${{matrix.language}}"
|
|
||||||
36
.gitignore
vendored
36
.gitignore
vendored
@@ -1,35 +1,15 @@
|
|||||||
# Dependencies
|
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/downloads
|
||||||
# Build Output Folder
|
|
||||||
/dist
|
/dist
|
||||||
/out
|
/out
|
||||||
/srpm-out
|
|
||||||
|
|
||||||
# .env
|
/ressources/ffmpeg.exe
|
||||||
*.env
|
/ressources/ffprobe.exe
|
||||||
|
/ressources/deno.exe
|
||||||
|
|
||||||
|
/theme/**
|
||||||
|
|
||||||
# Config JSON File. Create it and fill it if you want to have dev only settings
|
|
||||||
config/config.dev.json
|
config/config.dev.json
|
||||||
|
|
||||||
# IDE
|
logs/*.json
|
||||||
.idea/
|
logs/*.log
|
||||||
.vscode/
|
|
||||||
.zed/
|
|
||||||
|
|
||||||
# OS generated files
|
|
||||||
.DS_Store
|
|
||||||
.DS_Store?
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Linux executable
|
|
||||||
ffmpeg
|
|
||||||
ffprobe
|
|
||||||
deno
|
|
||||||
yt-dlp
|
|
||||||
|
|
||||||
# Windows Executable
|
|
||||||
ffmpeg.exe
|
|
||||||
ffprobe.exe
|
|
||||||
deno.exe
|
|
||||||
yt-dlp.exe
|
|
||||||
97
BINARIES.md
97
BINARIES.md
@@ -1,97 +0,0 @@
|
|||||||
# External binaries
|
|
||||||
|
|
||||||
This project depends on three external tools that are **not bundled in the repository**.
|
|
||||||
You must download them manually and place each binary in the correct folder before building or running the app locally.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Required tools
|
|
||||||
|
|
||||||
| Tool | Role |
|
|
||||||
|---------|---------------------------------------------|
|
|
||||||
| yt-dlp | Video and audio download engine |
|
|
||||||
| ffmpeg | Media encoding, transcoding, muxing |
|
|
||||||
| ffprobe | Media file analysis and metadata extraction |
|
|
||||||
| deno | JavaScript / TypeScript runtime |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Folder structure
|
|
||||||
|
|
||||||
Place the binaries exactly as shown below.
|
|
||||||
Do not rename them — the application resolves them by these exact names.
|
|
||||||
|
|
||||||
```
|
|
||||||
resources/
|
|
||||||
└── binaries/
|
|
||||||
├── linux/
|
|
||||||
│ ├── yt-dlp
|
|
||||||
│ ├── ffmpeg
|
|
||||||
│ ├── ffprobe
|
|
||||||
│ └── deno
|
|
||||||
└── win-32/
|
|
||||||
├── yt-dlp.exe
|
|
||||||
├── ffmpeg.exe
|
|
||||||
├── ffprobe.exe
|
|
||||||
└── deno.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Download links
|
|
||||||
|
|
||||||
### yt-dlp
|
|
||||||
|
|
||||||
| Platform | URL |
|
|
||||||
|----------|----------------------------------------------------------------------|
|
|
||||||
| Windows | https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe |
|
|
||||||
| Linux | https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp |
|
|
||||||
|
|
||||||
Rename the downloaded file to `yt-dlp` (or `yt-dlp.exe` on Windows).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ffmpeg and ffprobe
|
|
||||||
|
|
||||||
**Windows and Linux** — both binaries are included in the same archive.
|
|
||||||
|
|
||||||
| Platform | URL |
|
|
||||||
|-------------|-----------------------------------------------------------------------------------------------------------|
|
|
||||||
| Windows x64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip |
|
|
||||||
| Linux x64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz |
|
|
||||||
| Linux arm64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz |
|
|
||||||
|
|
||||||
Extract the archive and copy `ffmpeg` and `ffprobe` from the `bin/` subfolder.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### deno
|
|
||||||
|
|
||||||
| Platform | URL |
|
|
||||||
|-------------|----------------------------------------------------------------------------------------------|
|
|
||||||
| Windows x64 | https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip |
|
|
||||||
| Linux x64 | https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip |
|
|
||||||
| Linux arm64 | https://github.com/denoland/deno/releases/latest/download/deno-aarch64-unknown-linux-gnu.zip |
|
|
||||||
|
|
||||||
Each archive contains a single binary. Rename it to `deno` (or `deno.exe` on Windows).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Permissions (Linux )
|
|
||||||
|
|
||||||
After placing the binaries, make them executable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x resources/binaries/linux/yt-dlp
|
|
||||||
chmod +x resources/binaries/linux/ffmpeg
|
|
||||||
chmod +x resources/binaries/linux/ffprobe
|
|
||||||
chmod +x resources/binaries/linux/deno
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- These binaries are excluded from version control via `.gitignore`.
|
|
||||||
- Always use the latest stable release of each tool.
|
|
||||||
- **Yt-dlp** binaries don't need to be updated manually. At every startup of the application, the binary tries to update itself
|
|
||||||
@@ -6,7 +6,6 @@ Welcome! Thanks for your interest in Freedom Loader. Your contributions—bug re
|
|||||||
|
|
||||||
## Quick Links
|
## Quick Links
|
||||||
|
|
||||||
- [Development Guide](DEVELOPMENT.md)
|
|
||||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
- [Issue Templates](.github/ISSUE_TEMPLATE/)
|
- [Issue Templates](.github/ISSUE_TEMPLATE/)
|
||||||
- [Pull Request Templates](.github/PULL_REQUEST_TEMPLATE/)
|
- [Pull Request Templates](.github/PULL_REQUEST_TEMPLATE/)
|
||||||
@@ -24,7 +23,7 @@ Found a bug or unexpected behavior? Open an [**issue**](https://github.com/Maste
|
|||||||
- Clear description of the bug
|
- Clear description of the bug
|
||||||
- Steps to reproduce
|
- Steps to reproduce
|
||||||
- App version and environment (Windows version, Firefox version if relevant)
|
- App version and environment (Windows version, Firefox version if relevant)
|
||||||
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log` (Windows) or `~/.local/share/FreedomLoader/logs/` (Linux)
|
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log`
|
||||||
- Screenshots if applicable
|
- Screenshots if applicable
|
||||||
|
|
||||||
### 2. Request a Feature
|
### 2. Request a Feature
|
||||||
@@ -44,11 +43,13 @@ Have an idea to improve Freedom Loader? Open a [**Feature Request**](https://git
|
|||||||
- Follow the project's code style and conventions
|
- Follow the project's code style and conventions
|
||||||
|
|
||||||
**Process:**
|
**Process:**
|
||||||
1. Fork the repository.
|
1. Fork the repository
|
||||||
2. Check out our [**Developer Guide (DEVELOPMENT.md)**](./DEVELOPMENT.md) to set up your environment.
|
2. Create a feature branch: `git checkout -b feature/your-feature-name`
|
||||||
3. Create a feature branch following our branching conventions.
|
3. Make your changes
|
||||||
4. Make your changes and test thoroughly on Windows and/or Linux.
|
4. Test thoroughly on Windows 10/11
|
||||||
5. Open a Pull Request using the appropriate template.
|
5. Commit with clear messages: `git commit -m "Add feature X"`
|
||||||
|
6. Push to your fork: `git push origin feature/your-feature-name`
|
||||||
|
7. Open a Pull Request using the appropriate template
|
||||||
|
|
||||||
**Code Guidelines:**
|
**Code Guidelines:**
|
||||||
- Use camelCase for variables and functions
|
- Use camelCase for variables and functions
|
||||||
@@ -71,9 +72,41 @@ Small contributions matter—don't hesitate to submit documentation PRs.
|
|||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
Want to write some code? Awesome!
|
### Prerequisites
|
||||||
|
- Node.js 16.x or higher
|
||||||
|
- npm or yarn
|
||||||
|
- Git
|
||||||
|
- Windows 10/11 (for testing)
|
||||||
|
|
||||||
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.
|
### 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)
|
||||||
|
└── ressources/ # Binaries (yt-dlp, ffmpeg, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -81,9 +114,12 @@ Please read our [**Developer Guide (DEVELOPMENT.md)**](./DEVELOPMENT.md) for ins
|
|||||||
|
|
||||||
Before submitting a PR, verify:
|
Before submitting a PR, verify:
|
||||||
- [ ] Download functionality works (video/audio)
|
- [ ] Download functionality works (video/audio)
|
||||||
|
- [ ] Playlist downloads work
|
||||||
|
- [ ] Settings panel functions correctly
|
||||||
|
- [ ] Custom path selection works
|
||||||
- [ ] No errors in application logs
|
- [ ] No errors in application logs
|
||||||
- [ ] Any features wasn't break during the development
|
- [ ] UI changes work on all themes (if applicable)
|
||||||
- [ ] Tested on Windows 10/11 and/or any Linux Distribution
|
- [ ] Tested on Windows 10 and/or Windows 11
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -112,8 +148,8 @@ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
|
|||||||
|
|
||||||
## Questions?
|
## Questions?
|
||||||
|
|
||||||
- Check the [FAQ](https://freedomloader.acnolo.fr/faq)
|
- Check the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/faq)
|
||||||
- Review the [Wiki](https://freedomloader.acnolo.fr/wiki)
|
- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki)
|
||||||
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
388
DEVELOPMENT.md
388
DEVELOPMENT.md
@@ -1,388 +0,0 @@
|
|||||||
# 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! 🎉
|
|
||||||
360
README.md
360
README.md
@@ -1,20 +1,22 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<a href="https://freedomloader.acnolo.fr/"><img
|
<a href="https://masteracnolo.github.io/Freedom-Loader-Site/"><img
|
||||||
src="./build/banner.png"
|
src="./build/banner.png"
|
||||||
alt="Banner"
|
alt="Banner"
|
||||||
style="width: 50%;"/></a>
|
style="width: 50%;"/></a>
|
||||||
|
|
||||||
### **A clean, open-source multimedia downloader for Windows and Linux**
|
### **A clean, open-source multimedia downloader for Windows**
|
||||||
|
|
||||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://freedomloader.acnolo.fr/)
|
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
||||||
[](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
|
[](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>
|
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services.
|
Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services.
|
||||||
@@ -23,7 +25,7 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
|
|
||||||
### See related repo:
|
### See related repo:
|
||||||
|
|
||||||
- **[Website](https://github.com/MasterAcnolo/Freedom-Loader-Site/)** - Official project website and documentation
|
- **[Website](https://masteracnolo.github.io/Freedom-Loader-Site/)** - Official project website and documentation
|
||||||
- **[Workshop](https://github.com/MasterAcnolo/Freedom-Loader-Workshop)** - Theme creation tool and theme browser
|
- **[Workshop](https://github.com/MasterAcnolo/Freedom-Loader-Workshop)** - Theme creation tool and theme browser
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
@@ -33,12 +35,14 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Preview](#preview)
|
- [Preview](#preview)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
- [Theme Workshop](#Theme-Workshop)
|
- [Theme Workshop](#Theme-Workshop)
|
||||||
- [Technology Stack](#technology-stack)
|
- [Technology Stack](#technology-stack)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Roadmap](#roadmap)
|
- [Roadmap](#roadmap)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
- [Submitting a community theme](#Submitting-a-community-theme)
|
- [Submitting a community theme](#Submitting-a-community-theme)
|
||||||
|
- [Support](#support)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -57,9 +61,9 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
### User Interface
|
### User Interface
|
||||||
|
|
||||||
- **Custom window controls** - Frameless window with custom top bar (optional)
|
- **Custom window controls** - Frameless window with custom top bar (optional)
|
||||||
- **Custom themes** - ZIP-based theme system with live preview
|
- **Custom themes** — ZIP-based theme system with live preview
|
||||||
- **Theme Workshop** - web-based theme creator and browser
|
- **Theme Workshop** — web-based theme creator and browser
|
||||||
- **Toast notifications** - in-app feedback system
|
- **Toast notifications** — in-app feedback system
|
||||||
- **Real-time progress tracking** - Live download progress bar with network speed indicator
|
- **Real-time progress tracking** - Live download progress bar with network speed indicator
|
||||||
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
||||||
- **Settings panel** - In-app configuration interface with live updates
|
- **Settings panel** - In-app configuration interface with live updates
|
||||||
@@ -76,90 +80,41 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
- **Rate limiting** - Express-based request throttling to prevent server overload
|
- **Rate limiting** - Express-based request throttling to prevent server overload
|
||||||
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
||||||
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
||||||
- **Splash screen** - animated loading screen on startup
|
- **Splash screen** — animated loading screen on startup
|
||||||
- **Stop download** - ability to cancel in-progress downloads
|
- **Stop download** — ability to cancel in-progress downloads
|
||||||
- **Playlist folders** - dedicated folder creation per playlist
|
- **Playlist folders** — dedicated folder creation per playlist
|
||||||
- **Keep Playlist Order** - Add an Index before the title, could be useful for an album for example.
|
|
||||||
- **System Tray** - Could minimize the application in the system Tray, allow it to run in the background.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
[](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
|
|
||||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
|
||||||
[](https://snapcraft.io/freedom-loader)
|
|
||||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- **Windows**: Windows 10 or later
|
- **Operating System**: Windows 10 or later
|
||||||
- **Linux**: any modern x86_64 distribution
|
- **Browser**: Mozilla Firefox (required for cookie extraction)
|
||||||
- **Browser**: Mozilla Firefox ([download](https://www.firefox.com)) required for cookie extraction, on both platforms
|
- [Download Firefox](https://www.firefox.com)
|
||||||
|
|
||||||
### Windows
|
### Standard Installation
|
||||||
|
|
||||||
1. Download the latest `.exe` from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
1. Download the latest release from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
2. Run the installer (click "More info" → "Run anyway" if Windows Defender warns you)
|
2. Run the downloaded `.exe` installer
|
||||||
3. Follow the installation wizard
|
3. If Windows Defender shows a warning, click "More info" then "Run anyway"
|
||||||
|
4. Follow the installation wizard
|
||||||
### Linux
|
|
||||||
|
|
||||||
#### Fedora (via Copr - recommended)
|
|
||||||
|
|
||||||
Installing via Copr lets `dnf` handle future updates automatically:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dnf copr enable masteracnolo/freedom-loader
|
|
||||||
sudo dnf install freedom-loader
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Debian / Ubuntu (.deb)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Download the latest .deb from the Releases page, then:
|
|
||||||
sudo apt install ./freedom-loader_1.x.x_amd64.deb
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Snap (any distro with snapd)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo snap install freedom-loader
|
|
||||||
```
|
|
||||||
|
|
||||||
#### RPM (Fedora/RHEL/openSUSE, without Copr)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Download the latest .rpm from the Releases page, then:
|
|
||||||
sudo dnf install ./freedom-loader-1.x.y.x86_64.rpm
|
|
||||||
```
|
|
||||||
|
|
||||||
#### AppImage (any distro, no installation needed)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Download the latest .AppImage from the Releases page, then:
|
|
||||||
chmod +x Freedom-Loader-1.x.y.AppImage
|
|
||||||
./Freedom-Loader-1.x.y.AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
Once installed via any method, launch Freedom Loader from your application menu, or from a terminal with:
|
|
||||||
```bash
|
|
||||||
freedom-loader
|
|
||||||
```
|
|
||||||
|
|
||||||
### File Locations
|
### File Locations
|
||||||
|
|
||||||
| | Windows | Linux |
|
Downloaded files are stored in:
|
||||||
|---------------|---------------------------------------------------------|--------------------------------------|
|
```
|
||||||
| **Downloads** | `C:\Users\[USERNAME]\Downloads\Freedom Loader` | `~/Downloads/Freedom Loader` |
|
C:\Users\[USERNAME]\Downloads\Freedom Loader
|
||||||
| **Logs** | `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\` | `~/.local/share/FreedomLoader/logs/` |
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The download folder can be changed anytime via the "Edit" button in the UI, on both platforms.
|
> *You can change it whenever you want. Just click the "Edit" button in the UI.*
|
||||||
|
|
||||||
|
Application logs can be found at:
|
||||||
|
```
|
||||||
|
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> On Linux, you can start Freedom Loader using `freedom-loader` command
|
|
||||||
|
|
||||||
### Basic Download
|
### Basic Download
|
||||||
|
|
||||||
1. Launch Freedom Loader
|
1. Launch Freedom Loader
|
||||||
@@ -176,15 +131,14 @@ The application supports various configuration options:
|
|||||||
- Custom download directory selection
|
- Custom download directory selection
|
||||||
- Discord Rich Presence activation
|
- Discord Rich Presence activation
|
||||||
- Theme customization
|
- Theme customization
|
||||||
- Log level
|
- Log level configuration
|
||||||
- And somes to see in the list [Configuration Options](#configuration-options) available
|
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
If you encounter issues:
|
If you encounter issues:
|
||||||
|
|
||||||
1. Restart the application
|
1. Restart the application
|
||||||
2. Check the logs in `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs`
|
2. Check the logs in `AppData\Local\FreedomLoader\logs\`
|
||||||
3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools"
|
3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools"
|
||||||
4. Create an issue on GitHub with logs and reproduction steps
|
4. Create an issue on GitHub with logs and reproduction steps
|
||||||
|
|
||||||
@@ -210,22 +164,14 @@ If you encounter issues:
|
|||||||
Freedom Loader can be configured either through the settings panel in the UI or by directly editing the `config.json` file located in the application data directory.
|
Freedom Loader can be configured either through the settings panel in the UI or by directly editing the `config.json` file located in the application data directory.
|
||||||
|
|
||||||
### Configuration File Location
|
### Configuration File Location
|
||||||
|
|
||||||
#### Windows
|
|
||||||
```
|
```
|
||||||
%APPDATA%\Freedom Loader\config.json
|
%APPDATA%\Freedom Loader\config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Linux
|
|
||||||
```
|
|
||||||
~/.config/Freedom\ Loader/config.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Available Options
|
### Available Options
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"autoUpdate": true,
|
"autoUpdate": true,
|
||||||
"systemTray": true,
|
|
||||||
"discordRPC": true,
|
"discordRPC": true,
|
||||||
"customTopBar": true,
|
"customTopBar": true,
|
||||||
"autoCheckInfo": true,
|
"autoCheckInfo": true,
|
||||||
@@ -233,40 +179,36 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
|||||||
"addMetadata": true,
|
"addMetadata": true,
|
||||||
"verboseLogs": false,
|
"verboseLogs": false,
|
||||||
"autoDownloadPlaylist": true,
|
"autoDownloadPlaylist": true,
|
||||||
"keepPlaylistOrder": false,
|
|
||||||
"createPlaylistFolders": true,
|
"createPlaylistFolders": true,
|
||||||
"customCodec": "h264",
|
"customCodec": "h264",
|
||||||
"logSystem": true,
|
"logSystem": true,
|
||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
"downloadSystem": true,
|
"downloadSystem": true,
|
||||||
"notifySystem": true,
|
"notifySystem": true,
|
||||||
"enableHardwareAcceleration": true,
|
|
||||||
"theme": "dark"
|
"theme": "dark"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration Options
|
### Configuration Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
|------------------------------|---------|----------|------------------------------------------------------------|
|
|--------|------|---------|-------------|
|
||||||
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
||||||
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
|
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
||||||
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
||||||
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
||||||
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
|
||||||
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
|
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||||
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||||
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||||
| `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 |
|
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||||
| `createPlaylistFolders` | boolean | `false` | Automatically create a folder for a playlist |
|
| `logSystem` | boolean | `true` | Enable application logging |
|
||||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
||||||
| `logSystem` | boolean | `true` | Enable application logging |
|
| `downloadSystem` | boolean | `true` | Enable download system |
|
||||||
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
| `notifySystem` | boolean | `true` | Enable system notifications |
|
||||||
| `downloadSystem` | boolean | `true` | Enable download system |
|
| `enableHardwareAcceleration` | boolean | `true` | Enable HArdware Acceleration |
|
||||||
| `notifySystem` | boolean | `true` | Enable system notifications |
|
| `theme` | string | `dark` | Current Theme Name |
|
||||||
| `enableHardwareAcceleration` | boolean | `true` | Enable Hardware Acceleration |
|
|
||||||
| `theme` | string | `dark` | Current Theme Name |
|
|
||||||
|
|
||||||
### Supported Codecs
|
### Supported Codecs
|
||||||
|
|
||||||
@@ -279,7 +221,95 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
|||||||
- `theora` - Theora (legacy open codec)
|
- `theora` - Theora (legacy open codec)
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Some configuration changes may require an application restart to take effect.
|
> 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
|
||||||
|
├── ressources/ # Internal resources (binaries)
|
||||||
|
├── server/ # Express backend server
|
||||||
|
│ ├── logger.js
|
||||||
|
│ ├── server.js
|
||||||
|
│ ├── controller/ # Request handlers
|
||||||
|
│ │ ├── download.controller.js
|
||||||
|
│ │ └── info.controller.js
|
||||||
|
│ ├── helpers/ # Utility functions
|
||||||
|
│ │ ├── buildArgs.helpers.js
|
||||||
|
│ │ ├── getBrowser.helpers.js
|
||||||
|
│ │ ├── notify.helpers.js
|
||||||
|
│ │ ├── parseInfo.helpers.js
|
||||||
|
│ │ ├── path.helpers.js
|
||||||
|
│ │ ├── rateLimit.helpers.js
|
||||||
|
│ │ └── validation.helpers.js
|
||||||
|
│ ├── routes/ # API route definitions
|
||||||
|
│ │ ├── download.route.js
|
||||||
|
│ │ └── info.route.js
|
||||||
|
│ └── services/ # Business logic layer
|
||||||
|
│ ├── download.services.js
|
||||||
|
│ └── info.services.js
|
||||||
|
├── theme/ # Theme system
|
||||||
|
│ ├── template.theme.json # Theme template
|
||||||
|
│ ├── Dark/ # Default dark theme
|
||||||
|
│ │ └── dark.theme.json
|
||||||
|
│ └── Light/ # Default light theme
|
||||||
|
│ └── light.theme.json
|
||||||
|
├── main.js # Electron main process entry point
|
||||||
|
├── preload.js # Electron preload script (IPC bridge)
|
||||||
|
├── config.js # Global configuration loader
|
||||||
|
├── package.json # Project metadata and dependencies
|
||||||
|
├── CODE_OF_CONDUCT.md # Community guidelines
|
||||||
|
├── CONTRIBUTING.md # Contribution guidelines
|
||||||
|
├── LICENSE # GPLv3 license
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
### Architecture Overview
|
### Architecture Overview
|
||||||
|
|
||||||
@@ -304,7 +334,7 @@ Freedom Loader includes a web-based theme creator available at [Freedom Loader W
|
|||||||
### Installing a custom theme
|
### Installing a custom theme
|
||||||
1. Download a `.zip` theme file
|
1. Download a `.zip` theme file
|
||||||
2. Drop it in the `theme/` folder of your Freedom Loader installation
|
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
|
## Technology Stack
|
||||||
|
|
||||||
@@ -327,34 +357,76 @@ Freedom Loader includes a web-based theme creator available at [Freedom Loader W
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
If you are interested by running a local non compiled version of Freedom Loader or just interested by contributing, see [DEVELOPMENT](./DEVELOPMENT.md)
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 16.x or higher
|
||||||
|
- npm or yarn
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/MasterAcnolo/Freedom-Loader.git
|
||||||
|
cd Freedom-Loader
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run in development mode
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
### Additional Dependencies
|
### Additional Dependencies
|
||||||
|
|
||||||
This project relies on several external binaries.
|
This project relies on several external binaries.
|
||||||
Some of them are **not included** in the public repository and must be added manually.
|
Some of them are **not included** in the public repository and must be added manually.
|
||||||
|
|
||||||
You must download the required binaries and place them in the `resources` folder, using the exact filenames listed below.
|
You must download the required binaries and place them in the `ressources` folder, using the exact filenames listed below.
|
||||||
|
|
||||||
#### Required binaries
|
#### Required binaries
|
||||||
|
|
||||||
See [BINARIES.md](./BINARIES.md) for how to setup the 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:
|
||||||
|
|
||||||
|
```
|
||||||
|
ressources/
|
||||||
|
├── deno.exe
|
||||||
|
├── ffmpeg.exe
|
||||||
|
├── ffprobe.exe
|
||||||
|
└── yt-dlp.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
> These binaries are required for the application to start correctly.
|
||||||
|
> If any of them are missing, an error message will be displayed at application startup.
|
||||||
|
|
||||||
### Development Guidelines
|
### Development Guidelines
|
||||||
|
|
||||||
- Follow existing code style and conventions
|
- Follow existing code style and conventions
|
||||||
- Write clear, imperative commit messages (e.g., "Add playlist indexing" not "Added")
|
- Write clear commit messages
|
||||||
- Test thoroughly on both Windows and Linux before submitting changes
|
- Test thoroughly before submitting changes
|
||||||
- Update documentation (README, DEVELOPMENT.md) when adding features
|
- Update documentation when adding features
|
||||||
- Maintain compatibility with:
|
- Maintain compatibility with Windows 10+
|
||||||
- Windows 10+
|
|
||||||
- Fedora 43+ / Debian 11+ / Ubuntu 20.04+
|
|
||||||
- GNOME and KDE desktop environments (when possible)
|
|
||||||
- Consider Linux distribution differences early (see [DEVELOPMENT.md](./DEVELOPMENT.md#linux-development-considerations))
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
See the most recent Roadmap [Here](https://freedomloader.acnolo.fr/Roadmap).
|
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -367,29 +439,13 @@ Head over to the [Freedom Loader Workshop repository](https://github.com/MasterA
|
|||||||
|
|
||||||
### Bug Reports
|
### Bug Reports
|
||||||
|
|
||||||
You can report bugs either using the built-in application system or via the GitHub Issues system.
|
Use the GitHub Issues system and include:
|
||||||
|
|
||||||
#### Using the Built-in Report Tool
|
|
||||||
|
|
||||||
1. Open Freedom Loader.
|
|
||||||
2. Trigger the bug report interface.
|
|
||||||
3. Fill in the title and description of the issue.
|
|
||||||
4. Choose whether to include today's logs automatically.
|
|
||||||
5. Submit the report.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> If you need to attach extra context like screenshots or screen recordings, it is recommended to use the GitHub Issues
|
|
||||||
system instead.
|
|
||||||
|
|
||||||
#### Using GitHub Issues
|
|
||||||
|
|
||||||
Open a new issue on the GitHub repository and include:
|
|
||||||
|
|
||||||
- Clear description of the issue
|
- Clear description of the issue
|
||||||
- Steps to reproduce
|
- Steps to reproduce
|
||||||
- Expected vs actual behavior
|
- Expected vs actual behavior
|
||||||
- Relevant logs from `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs/`
|
- Relevant logs from `AppData\Local\FreedomLoader\logs\`
|
||||||
- Screenshots or recordings if applicable
|
- Screenshots if applicable
|
||||||
|
|
||||||
### Feature Requests
|
### Feature Requests
|
||||||
|
|
||||||
@@ -408,7 +464,15 @@ Open a feature request issue with:
|
|||||||
5. Update documentation as needed
|
5. Update documentation as needed
|
||||||
6. Submit a pull request with a detailed description
|
6. Submit a pull request with a detailed description
|
||||||
|
|
||||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md), [DEVELOPMENT.md](./DEVELOPMENT.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing.
|
Please read [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you find this project useful and want to support its development:
|
||||||
|
|
||||||
|
[](https://paypal.me/axelnicolas25)
|
||||||
|
|
||||||
|
Your support helps maintain infrastructure, develop new features, and keep the project free and ad-free.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -420,12 +484,12 @@ You are free to use, modify, and redistribute this software under the terms of t
|
|||||||
|
|
||||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
||||||
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
||||||
- [Deno](https://deno.com/) for helping me to bypass challenges
|
- [Deno](https://deno.com/) for helping me bypassing challenges
|
||||||
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
||||||
- The open-source community for continuous support and contributions
|
- The open-source community for continuous support and contributions
|
||||||
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
||||||
- [@AureCX](https://github.com/AureCX) for being the Official Windows Tester!
|
|
||||||
- Zakaria for the website icon
|
- Zakaria for the website icon
|
||||||
|
- Jacques Chirac to love Apples
|
||||||
- All users who test, report issues, and help improve the application
|
- All users who test, report issues, and help improve the application
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -434,6 +498,6 @@ You are free to use, modify, and redistribute this software under the terms of t
|
|||||||
|
|
||||||
**Freedom Loader** - Put freedom in your downloads
|
**Freedom Loader** - Put freedom in your downloads
|
||||||
|
|
||||||
[Website](https://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)
|
[Website](https://masteracnolo.github.io/Freedom-Loader-Site/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/Freedom-Loader-Site/wiki) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
73
SECURITY.md
73
SECURITY.md
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Freedom Loader follows a **latest-only** support model. Security fixes are applied to the current release only — no backports to older versions.
|
We actively maintain and provide security updates for the following versions of Freedom Loader:
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| Latest stable | :white_check_mark: |
|
| 1.4.x | :white_check_mark: |
|
||||||
| Older versions | :x: |
|
| 1.3.x | :white_check_mark: |
|
||||||
|
| < 1.3.0 | :x: |
|
||||||
|
|
||||||
I recommend always running the latest version. The built-in auto-updater will notify you when a new release is available. Older versions will not receive security patches regardless of severity.
|
**Note**: We recommend always using the latest version to benefit from the most recent security patches and features.
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
@@ -31,40 +32,37 @@ Instead, please report security issues privately by:
|
|||||||
|
|
||||||
### What to Expect
|
### What to Expect
|
||||||
|
|
||||||
- **Acknowledgment**: I will acknowledge receipt of your report within 48 hours
|
- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours
|
||||||
- **Assessment**: I will assess the vulnerability and determine its severity
|
- **Assessment**: We will assess the vulnerability and determine its severity
|
||||||
- **Updates**: I will keep you informed of our progress
|
- **Updates**: We will keep you informed of our progress
|
||||||
- **Resolution**: I aim to release a fix within 7-14 days for critical vulnerabilities
|
- **Resolution**: We aim to release a fix within 7-14 days for critical vulnerabilities
|
||||||
- **Credit**: I will credit you in the release notes (unless you prefer to remain anonymous)
|
- **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous)
|
||||||
|
|
||||||
## Security Best Practices for Users
|
## Security Best Practices for Users
|
||||||
|
|
||||||
When using Freedom Loader, I recommend:
|
When using Freedom Loader, we recommend:
|
||||||
|
|
||||||
### General Security
|
### General Security
|
||||||
|
|
||||||
- **Keep Updated**: Always use the latest version of Freedom Loader — older versions receive no security patches
|
- **Keep Updated**: Always use the latest version of Freedom Loader
|
||||||
- **Official Sources**: Download only from official sources:
|
- **Official Sources**: Download only from official releases on [GitHub](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
- [GitHub Releases](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
- **Verify Downloads**: Check that installers are properly signed (Windows SmartScreen may show warnings for new releases)
|
||||||
- [Fedora COPR](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
|
|
||||||
- [Snap Store](https://snapcraft.io/freedom-loader)
|
|
||||||
- **Verify Downloads**: Check that installers are from one of the above sources
|
|
||||||
- **Antivirus**: Keep your antivirus software up to date
|
- **Antivirus**: Keep your antivirus software up to date
|
||||||
|
|
||||||
### Configuration Security
|
### Configuration Security
|
||||||
|
|
||||||
- **Download Path**: Only set download paths within your user directory
|
- **Download Path**: Only set download paths within your user directory (`C:\Users\[USERNAME]\...`)
|
||||||
- **Cookies**: Be aware that Firefox cookies are used for authentication — keep Firefox secure
|
- **Cookies**: Be aware that Firefox cookies are used for authentication—keep Firefox secure
|
||||||
- **Logs**: Logs may contain sensitive information such as URLs or local paths — avoid sharing them publicly without review
|
- **Logs**: Logs may contain sensitive information—avoid sharing them publicly without review
|
||||||
|
|
||||||
### Privacy Considerations
|
### Privacy Considerations
|
||||||
|
|
||||||
Freedom Loader respects your privacy:
|
Freedom Loader respects your privacy:
|
||||||
|
|
||||||
- **No Data Collection**: I don't collect, store, or transmit your personal data
|
- **No Data Collection**: We don't collect, store, or transmit your personal data
|
||||||
- **No Telemetry**: No usage tracking or analytics
|
- **No Telemetry**: No usage tracking or analytics
|
||||||
- **Local Operation**: All downloads are processed locally on your machine
|
- **Local Operation**: All downloads are processed locally on your machine
|
||||||
- **Optional Features**: Discord RPC is optional and can be disabled in settings
|
- **Optional Features**: Discord RPC is optional and can be disabled
|
||||||
|
|
||||||
## Known Security Considerations
|
## Known Security Considerations
|
||||||
|
|
||||||
@@ -73,16 +71,16 @@ Freedom Loader respects your privacy:
|
|||||||
Freedom Loader accesses Firefox cookies to download protected content. This is:
|
Freedom Loader accesses Firefox cookies to download protected content. This is:
|
||||||
|
|
||||||
- **By Design**: Required for age-restricted or member-only content
|
- **By Design**: Required for age-restricted or member-only content
|
||||||
- **Local Only**: Cookies are read locally and never transmitted outside your machine
|
- **Local Only**: Cookies are read locally and never transmitted
|
||||||
- **User Controlled**: You control what content you download
|
- **User Controlled**: You can control what content you download
|
||||||
|
|
||||||
### Native Dependencies
|
### Native Dependencies
|
||||||
|
|
||||||
Freedom Loader bundles the following official binaries:
|
Freedom Loader bundles native binaries:
|
||||||
|
|
||||||
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp) — auto-updated on each launch
|
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
||||||
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
|
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
|
||||||
- **Deno**: Official builds from [Deno.com](https://deno.com/)
|
- **Deno**: Official builds from [Deno.land](https://deno.land/)
|
||||||
|
|
||||||
These dependencies are verified and updated regularly.
|
These dependencies are verified and updated regularly.
|
||||||
|
|
||||||
@@ -93,13 +91,13 @@ New releases may trigger Windows Defender warnings because:
|
|||||||
- The application is not yet widely distributed
|
- The application is not yet widely distributed
|
||||||
- Code signing certificates are expensive for open-source projects
|
- Code signing certificates are expensive for open-source projects
|
||||||
|
|
||||||
This is expected behavior for new releases and will diminish as more users download the software.
|
This is expected behavior for new releases. The warning will decrease as more users download the software.
|
||||||
|
|
||||||
## Vulnerability Disclosure Policy
|
## Vulnerability Disclosure Policy
|
||||||
|
|
||||||
### Our Commitment
|
### Our Commitment
|
||||||
|
|
||||||
I am committed to:
|
We are committed to:
|
||||||
|
|
||||||
- Responding promptly to security reports
|
- Responding promptly to security reports
|
||||||
- Working with security researchers to verify and address issues
|
- Working with security researchers to verify and address issues
|
||||||
@@ -109,9 +107,9 @@ I am committed to:
|
|||||||
### Disclosure Timeline
|
### Disclosure Timeline
|
||||||
|
|
||||||
1. **Day 0**: Vulnerability reported privately
|
1. **Day 0**: Vulnerability reported privately
|
||||||
2. **Day 1–2**: Acknowledgment sent to reporter
|
2. **Day 1-2**: Acknowledgment sent to reporter
|
||||||
3. **Day 3–7**: Vulnerability assessed and fix developed
|
3. **Day 3-7**: Vulnerability assessed and fix developed
|
||||||
4. **Day 7–14**: Fix released (critical vulnerabilities prioritized)
|
4. **Day 7-14**: Fix released (critical vulnerabilities prioritized)
|
||||||
5. **Day 14+**: Public disclosure (coordinated with reporter)
|
5. **Day 14+**: Public disclosure (coordinated with reporter)
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
@@ -131,16 +129,16 @@ The following are generally not considered security vulnerabilities:
|
|||||||
- Issues requiring physical access to the user's machine
|
- Issues requiring physical access to the user's machine
|
||||||
- Social engineering attacks
|
- Social engineering attacks
|
||||||
- Denial of service against third-party services
|
- Denial of service against third-party services
|
||||||
- Issues in third-party dependencies (report to upstream projects directly)
|
- Issues in third-party dependencies (report to upstream projects)
|
||||||
- Missing security headers on the local HTTP server (not exposed remotely)
|
- Missing security headers on local HTTP server (no remote access)
|
||||||
|
|
||||||
## Security Updates
|
## Security Updates
|
||||||
|
|
||||||
Security updates are released as:
|
Security updates are released as:
|
||||||
|
|
||||||
- **Patch Releases**: For critical security fixes (e.g., 1.6.0 → 1.6.1)
|
- **Patch Releases**: For critical security fixes (e.g., 1.4.1 → 1.4.2)
|
||||||
- **Automatic Updates**: Users are notified via the built-in update system
|
- **Automatic Updates**: Users are notified via the built-in update system
|
||||||
- **Release Notes**: Security fixes are clearly marked in the changelog
|
- **Release Notes**: Security fixes are clearly marked in changelog
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
@@ -152,7 +150,6 @@ Security updates are released as:
|
|||||||
|
|
||||||
For security-related questions or concerns:
|
For security-related questions or concerns:
|
||||||
|
|
||||||
- **Email**: masteracnolo25@gmail.com
|
|
||||||
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
|
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -9,37 +9,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { autoUpdater } = require("electron-updater");
|
const { autoUpdater } = require("electron-updater");
|
||||||
|
const { dialog } = require("electron");
|
||||||
const { logger } = require("../server/logger");
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures autoUpdater behavior:
|
* Configures autoUpdater behavior:
|
||||||
* - disables automatic download
|
* - disables automatic download
|
||||||
* - disables automatic installation on quit
|
* - disables automatic install on quit
|
||||||
* (manual user-controlled update flow)
|
* (manual user-controlled update flow)
|
||||||
*/
|
*/
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = false;
|
||||||
autoUpdater.autoInstallOnAppQuit = 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.
|
* Initializes application auto-update lifecycle.
|
||||||
*
|
*
|
||||||
@@ -51,6 +32,7 @@ function getUpdateEnvironment() {
|
|||||||
* @param {BrowserWindow} mainWindow - main Electron window instance
|
* @param {BrowserWindow} mainWindow - main Electron window instance
|
||||||
*/
|
*/
|
||||||
function initAutoUpdater(mainWindow) {
|
function initAutoUpdater(mainWindow) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggered when a new version is detected.
|
* Triggered when a new version is detected.
|
||||||
* Prompts user to install or defer update.
|
* Prompts user to install or defer update.
|
||||||
@@ -58,12 +40,23 @@ function initAutoUpdater(mainWindow) {
|
|||||||
autoUpdater.on("update-available", async (info) => {
|
autoUpdater.on("update-available", async (info) => {
|
||||||
logger.info(`Update available: ${info.version}`);
|
logger.info(`Update available: ${info.version}`);
|
||||||
|
|
||||||
mainWindow?.webContents.send("update-available", {
|
const { response } = await dialog.showMessageBox(mainWindow, {
|
||||||
version: info.version,
|
type: "info",
|
||||||
environment: getUpdateEnvironment(),
|
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,
|
||||||
});
|
});
|
||||||
updateAvailable = true;
|
|
||||||
|
|
||||||
|
if (response === 0) {
|
||||||
|
autoUpdater.downloadUpdate();
|
||||||
|
} else {
|
||||||
|
mainWindow?.webContents.executeJavaScript(
|
||||||
|
`window.showUpdateBadge && window.showUpdateBadge("${info.version}")`
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,10 +66,8 @@ function initAutoUpdater(mainWindow) {
|
|||||||
autoUpdater.on("download-progress", (progress) => {
|
autoUpdater.on("download-progress", (progress) => {
|
||||||
logger.info(`Download progress: ${Math.round(progress.percent)}%`);
|
logger.info(`Download progress: ${Math.round(progress.percent)}%`);
|
||||||
mainWindow?.webContents.send("update-progress", {
|
mainWindow?.webContents.send("update-progress", {
|
||||||
percent: progress.percent,
|
percent: Math.round(progress.percent),
|
||||||
bytesPerSecond: progress.bytesPerSecond,
|
speed: progress.bytesPerSecond,
|
||||||
transferred: progress.transferred,
|
|
||||||
total: progress.total,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,9 +78,17 @@ function initAutoUpdater(mainWindow) {
|
|||||||
autoUpdater.on("update-downloaded", async (info) => {
|
autoUpdater.on("update-downloaded", async (info) => {
|
||||||
logger.info(`Update downloaded: ${info.version}`);
|
logger.info(`Update downloaded: ${info.version}`);
|
||||||
|
|
||||||
mainWindow?.webContents.send("update-downloaded", info);
|
const { response } = await dialog.showMessageBox(mainWindow, {
|
||||||
|
type: "info",
|
||||||
|
title: "Update Ready",
|
||||||
|
message: `Version ${info.version} has been downloaded.`,
|
||||||
|
detail: "The application will restart to apply the update.",
|
||||||
|
buttons: ["Install Now", "Later"],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response === 0) autoUpdater.quitAndInstall();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,22 +96,8 @@ function initAutoUpdater(mainWindow) {
|
|||||||
* Logs failure and displays an error dialog to user.
|
* Logs failure and displays an error dialog to user.
|
||||||
*/
|
*/
|
||||||
autoUpdater.on("error", (err) => {
|
autoUpdater.on("error", (err) => {
|
||||||
const msg = err?.message || "";
|
logger.error("Auto update error:", err.message);
|
||||||
|
dialog.showErrorBox("Update Error", err.message);
|
||||||
/**
|
|
||||||
* If no update is available, I put this because there is no Linux version before 1.6.0
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
const isNoUpdateAvailable =
|
|
||||||
/404/.test(msg) || /Cannot find latest.*\.yml/i.test(msg);
|
|
||||||
|
|
||||||
if (isNoUpdateAvailable) {
|
|
||||||
logger.warn("Auto update: no update metadata found (probably no previous release), ignoring", msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error("Auto update error:", err);
|
|
||||||
mainWindow?.webContents.send("update-error", msg);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
@@ -123,11 +108,6 @@ function initAutoUpdater(mainWindow) {
|
|||||||
* Separated from init for reusability and testability.
|
* Separated from init for reusability and testability.
|
||||||
*/
|
*/
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
|
|
||||||
// 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 {
|
try {
|
||||||
await autoUpdater.checkForUpdates();
|
await autoUpdater.checkForUpdates();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -144,7 +124,6 @@ async function downloadUpdate() {
|
|||||||
await autoUpdater.downloadUpdate();
|
await autoUpdater.downloadUpdate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Download failed:", err.message);
|
logger.error("Download failed:", err.message);
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,13 +134,4 @@ function installUpdate() {
|
|||||||
autoUpdater.quitAndInstall();
|
autoUpdater.quitAndInstall();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUpdateAvailable() {
|
module.exports = { initAutoUpdater, downloadUpdate, installUpdate };
|
||||||
return updateAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
initAutoUpdater,
|
|
||||||
downloadUpdate,
|
|
||||||
installUpdate,
|
|
||||||
isUpdateAvailable,
|
|
||||||
};
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -25,19 +25,15 @@ function checkNativeDependencies() {
|
|||||||
*/
|
*/
|
||||||
const { binaryPaths } = require("../server/helpers/path.helpers");
|
const { binaryPaths } = require("../server/helpers/path.helpers");
|
||||||
|
|
||||||
const isWindows = process.platform === "win32";
|
|
||||||
|
|
||||||
const addExe = isWindows ? ".exe" : "";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of required external executables used by the application runtime.
|
* List of required external executables used by the application runtime.
|
||||||
* Each entry defines a binary name and its resolved absolute path.
|
* Each entry defines a binary name and its resolved absolute path.
|
||||||
*/
|
*/
|
||||||
const deps = [
|
const deps = [
|
||||||
{ name: `yt-dlp${addExe}`, path: binaryPaths.ytDlp },
|
{ name: "yt-dlp.exe", path: binaryPaths.ytDlp },
|
||||||
{ name: `ffmpeg${addExe}`, path: binaryPaths.ffmpeg },
|
{ name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
|
||||||
{ name: `ffprobe${addExe}`, path: binaryPaths.ffprobe },
|
{ name: "ffprobe.exe", path: binaryPaths.ffprobe },
|
||||||
{ name: `deno${addExe}`, path: binaryPaths.deno },
|
{ name: "deno.exe", path: binaryPaths.deno },
|
||||||
];
|
];
|
||||||
|
|
||||||
const missing = deps.filter(d => !fs.existsSync(d.path));
|
const missing = deps.filter(d => !fs.existsSync(d.path));
|
||||||
@@ -56,7 +52,7 @@ function checkNativeDependencies() {
|
|||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
dialog.showErrorBox(
|
dialog.showErrorBox(
|
||||||
"Missing dependencies",
|
"Missing dependencies",
|
||||||
`The following files are missing in the 'resources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.`
|
`The following files are missing in the 'ressources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.`
|
||||||
);
|
);
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const RPC = require("discord-rpc");
|
const RPC = require("discord-rpc");
|
||||||
const { logger } = require("../server/logger");
|
const { logger } = require("../server/logger");
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discord Application Client ID used to authenticate the RPC connection.
|
* Discord Application Client ID used to authenticate the RPC connection.
|
||||||
@@ -22,51 +20,6 @@ const rpc = new RPC.Client({ transport: "ipc" });
|
|||||||
*/
|
*/
|
||||||
let intervalId;
|
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.
|
* Initializes Discord Rich Presence (RPC) connection.
|
||||||
*
|
*
|
||||||
@@ -79,11 +32,6 @@ function fixLinuxDiscordRPC() {
|
|||||||
*/
|
*/
|
||||||
function startRPC() {
|
function startRPC() {
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch that create Simlinks for Linux environment;
|
|
||||||
*/
|
|
||||||
fixLinuxDiscordRPC();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rich Presence payload describing current application state
|
* Rich Presence payload describing current application state
|
||||||
* shown in Discord user profile.
|
* shown in Discord user profile.
|
||||||
@@ -95,17 +43,15 @@ function startRPC() {
|
|||||||
smallImageText: "By MasterAcnolo",
|
smallImageText: "By MasterAcnolo",
|
||||||
startTimestamp: new Date(),
|
startTimestamp: new Date(),
|
||||||
details: `Open Source Download Tools - ${config.version}`,
|
details: `Open Source Download Tools - ${config.version}`,
|
||||||
state: "freedomloader.acnolo.fr",
|
state: "masteracnolo.github.io/FreedomLoader",
|
||||||
};
|
};
|
||||||
rpc.clearActivity();
|
rpc.clearActivity()
|
||||||
rpc.setActivity(presence);
|
rpc.setActivity(presence);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.login({ clientId }).catch(() => {
|
rpc.login({ clientId }).catch(err => {
|
||||||
// Since 1.6.2, it no longer print the error. This is because if discord is not opened, it will send an error.
|
logger.error("Unable to connect to the RPC:", err);
|
||||||
// logger.error("Unable to connect to the RPC:", err);
|
|
||||||
logger.info("Unable to connect to the Discord RPC. Discord is maybe not launched");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,23 +60,28 @@ function startRPC() {
|
|||||||
*
|
*
|
||||||
* Responsibilities:
|
* Responsibilities:
|
||||||
* - Clear any running intervals
|
* - Clear any running intervals
|
||||||
* - Remove current activity from Discord if connected
|
* - Remove current activity from Discord
|
||||||
* - Destroy RPC transport connection
|
* - Destroy RPC transport connection
|
||||||
* - Handle cleanup errors safely
|
* - Handle cleanup errors safely
|
||||||
*/
|
*/
|
||||||
async function stopRPC() {
|
async function stopRPC(){
|
||||||
if (rpc) {
|
|
||||||
try {
|
try {
|
||||||
if (intervalId) clearInterval(intervalId);
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
|
||||||
await rpc.destroy();
|
/**
|
||||||
|
* 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) {
|
} catch (err) {
|
||||||
// Since 1.6.2, it no longer print the error. This is because if discord is not opened, it will send an error.
|
logger.error("Error while closing the RPC:", err);
|
||||||
// logger.error("Error while closing the RPC:", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = { startRPC, stopRPC};
|
module.exports = { startRPC, stopRPC};
|
||||||
|
|||||||
@@ -12,16 +12,13 @@
|
|||||||
|
|
||||||
const { ipcMain, dialog, shell } = require("electron");
|
const { ipcMain, dialog, shell } = require("electron");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const { logger, logDir } = require("../server/logger");
|
const { logger, logDir } = require("../server/logger");
|
||||||
const { configFeatures, featuresPath, devMode } = require("../config");
|
const { configFeatures, featuresPath } = require("../config");
|
||||||
const { getThemes, reloadThemes } = require("./themeManager");
|
const { getThemes, reloadThemes } = require("./themeManager");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
|
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
|
||||||
const { userThemesPath } = require("../server/helpers/path.helpers");
|
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.
|
* Security whitelist for feature flags that can be modified at runtime.
|
||||||
@@ -31,7 +28,6 @@ const { getChangelog, markChangelogSeen } = require("./changelogManager");
|
|||||||
*/
|
*/
|
||||||
const FEATURE_WHITELIST = new Set([
|
const FEATURE_WHITELIST = new Set([
|
||||||
"autoUpdate",
|
"autoUpdate",
|
||||||
"systemTray",
|
|
||||||
"discordRPC",
|
"discordRPC",
|
||||||
"customTopBar",
|
"customTopBar",
|
||||||
"autoCheckInfo",
|
"autoCheckInfo",
|
||||||
@@ -39,7 +35,6 @@ const FEATURE_WHITELIST = new Set([
|
|||||||
"addMetadata",
|
"addMetadata",
|
||||||
"verboseLogs",
|
"verboseLogs",
|
||||||
"autoDownloadPlaylist",
|
"autoDownloadPlaylist",
|
||||||
"keepPlaylistOrder",
|
|
||||||
"customCodec",
|
"customCodec",
|
||||||
"theme",
|
"theme",
|
||||||
"createPlaylistFolders",
|
"createPlaylistFolders",
|
||||||
@@ -67,21 +62,12 @@ const themeFolderPath = userThemesPath;
|
|||||||
* @param {Function} getMainWindow - Function returning current BrowserWindow instance
|
* @param {Function} getMainWindow - Function returning current BrowserWindow instance
|
||||||
*/
|
*/
|
||||||
function registerIpcHandlers(getMainWindow) {
|
function registerIpcHandlers(getMainWindow) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns application version from config.
|
* Returns application version from config.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("version", () => config.version);
|
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.
|
* Returns runtime feature configuration object.
|
||||||
*/
|
*/
|
||||||
@@ -126,17 +112,7 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
/**
|
/**
|
||||||
* Window minimize request from renderer.
|
* Window minimize request from renderer.
|
||||||
*/
|
*/
|
||||||
ipcMain.on("window-minimize", () => {
|
ipcMain.on("window-minimize", () => getMainWindow()?.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.
|
* Toggles maximize/unmaximize state of main window.
|
||||||
@@ -152,6 +128,7 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
*/
|
*/
|
||||||
ipcMain.on("window-close", () => getMainWindow()?.close());
|
ipcMain.on("window-close", () => getMainWindow()?.close());
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens Chromium DevTools in detached mode.
|
* Opens Chromium DevTools in detached mode.
|
||||||
*/
|
*/
|
||||||
@@ -162,20 +139,20 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
/**
|
/**
|
||||||
* Opens application logs directory in system file explorer.
|
* Opens application logs directory in system file explorer.
|
||||||
*/
|
*/
|
||||||
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
|
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens external website in default browser.
|
* Opens external website in default browser.
|
||||||
*/
|
*/
|
||||||
ipcMain.on("open-website", () =>
|
ipcMain.on("open-website", () =>
|
||||||
shell.openExternal("https://freedomloader.acnolo.fr/")
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/")
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens official wiki page in external browser.
|
* Opens official wiki page in external browser.
|
||||||
*/
|
*/
|
||||||
ipcMain.on("open-wiki", () =>
|
ipcMain.on("open-wiki", () =>
|
||||||
shell.openExternal("https://freedomloader.acnolo.fr/wiki")
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki")
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,12 +167,7 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
*/
|
*/
|
||||||
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
|
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.
|
* Retrieves available themes from filesystem.
|
||||||
@@ -214,36 +186,6 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
return await reloadThemes();
|
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.
|
* Updates a runtime feature flag and persists it to disk.
|
||||||
*
|
*
|
||||||
@@ -261,46 +203,32 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
* @returns {boolean} success state
|
* @returns {boolean} success state
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("set-feature", (event, { key, value }) => {
|
ipcMain.handle("set-feature", (event, { key, value }) => {
|
||||||
try {
|
try {
|
||||||
if (!FEATURE_WHITELIST.has(key)) {
|
if (!FEATURE_WHITELIST.has(key)) {
|
||||||
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configFeatures[key] === value) {
|
if (configFeatures[key] === value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
configFeatures[key] = value;
|
||||||
* Dynamically intercepts and applies System Tray state changes at runtime.
|
|
||||||
* Instantiates or destroys the tray icon immediately to prevent window
|
fs.writeFileSync(
|
||||||
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
|
configFolderPath,
|
||||||
*/
|
JSON.stringify(configFeatures, null, 2),
|
||||||
if (key === "systemTray") {
|
"utf-8"
|
||||||
if (value === true) {
|
);
|
||||||
logger.info("System Tray enabled dynamically.");
|
|
||||||
createSystemTray(devMode);
|
logger.info(`Feature updated: ${key} = ${value}`);
|
||||||
} else {
|
return true;
|
||||||
logger.info("System Tray disabled dynamically.");
|
|
||||||
destroyTray();
|
} catch (err) {
|
||||||
|
logger.error(`set-feature failed (${key}): ${err.message}`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
configFeatures[key] = value;
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
configFolderPath,
|
|
||||||
JSON.stringify(configFeatures, null, 2),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(`Feature updated: ${key} = ${value}`);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`set-feature failed (${key}): ${err.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { registerIpcHandlers };
|
module.exports = { registerIpcHandlers };
|
||||||
@@ -56,30 +56,26 @@ function validateDownloadPath(userPath) {
|
|||||||
if (!userPath) return getDefaultDownloadPath();
|
if (!userPath) return getDefaultDownloadPath();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const absolutePath = path.resolve(userPath);
|
|
||||||
|
|
||||||
if (!fs.existsSync(absolutePath)) {
|
/**
|
||||||
fs.mkdirSync(absolutePath, { recursive: true });
|
* Resolves filesystem symlinks to ensure canonical absolute path.
|
||||||
logger.info(`Download folder created: ${absolutePath}`);
|
* Required to prevent path traversal or alias bypass.
|
||||||
}
|
*/
|
||||||
|
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
|
||||||
catch (err) {
|
/**
|
||||||
/**
|
* Handles invalid filesystem paths, permissions issues, or resolution failures.
|
||||||
* Handles invalid filesystem paths, permissions issues, or resolution failures.
|
* Logs diagnostic information before propagating a sanitized error.
|
||||||
* Logs diagnostic information before propagating a sanitized error.
|
*/
|
||||||
*/
|
logger.error(`Invalid download path: ${userPath} — ${err.message}`);
|
||||||
logger.error(`Invalid download path: ${userPath} — ${err.message}`);
|
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
||||||
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { validateDownloadPath, getDefaultDownloadPath };
|
module.exports = { validateDownloadPath, getDefaultDownloadPath };
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
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};
|
|
||||||
@@ -270,6 +270,7 @@ async function loadThemesFromFolder() {
|
|||||||
if (fs.statSync(filePath).isDirectory()) {
|
if (fs.statSync(filePath).isDirectory()) {
|
||||||
const theme = await loadThemeFromFolder(filePath, themeId);
|
const theme = await loadThemeFromFolder(filePath, themeId);
|
||||||
if (theme) themes.push(theme);
|
if (theme) themes.push(theme);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
117
app/tray.js
117
app/tray.js
@@ -1,117 +0,0 @@
|
|||||||
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,21 +81,6 @@ async function createMainWindow() {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Intercepts the window close event.
|
|
||||||
* If the app is not explicitly quitting (via Tray menu),
|
|
||||||
* cancels the destruction and hides the window instead.
|
|
||||||
*/
|
|
||||||
mainWindow.on('close', (event) => {
|
|
||||||
|
|
||||||
if (!app.isQuitting && config.configFeatures.systemTray) {
|
|
||||||
event.preventDefault();
|
|
||||||
mainWindow.hide();
|
|
||||||
logger.info("Window Minimized in SystemTray (Using Close Button)");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans up window reference when the main window is closed.
|
* Cleans up window reference when the main window is closed.
|
||||||
* Prevents memory leaks and allows recreation if needed.
|
* Prevents memory leaks and allows recreation if needed.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 107 KiB |
26
config.js
26
config.js
@@ -7,7 +7,7 @@ const path = require("path");
|
|||||||
* Indicates whether the application is running in development mode.
|
* Indicates whether the application is running in development mode.
|
||||||
* Determined by Electron's packaging state.
|
* Determined by Electron's packaging state.
|
||||||
*/
|
*/
|
||||||
const devMode = !app.isPackaged || process.env.NODE_ENV === "test";
|
const devMode = !app.isPackaged;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the configuration file path depending on runtime environment.
|
* Resolves the configuration file path depending on runtime environment.
|
||||||
@@ -68,30 +68,13 @@ function loadFeatures() {
|
|||||||
return JSON.parse(raw);
|
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.
|
* In-memory snapshot of application feature flags.
|
||||||
*
|
*
|
||||||
* Note: Changes to the config file are not automatically reflected.
|
* Note: Changes to the config file are not automatically reflected.
|
||||||
* A restart or reload is required.
|
* A restart or reload is required.
|
||||||
*/
|
*/
|
||||||
let configFeatures = loadFeatures();
|
const configFeatures = loadFeatures();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
@@ -119,11 +102,6 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
configFeatures,
|
configFeatures,
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to reload the config
|
|
||||||
*/
|
|
||||||
reloadFeatures,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the active configuration file
|
* Path to the active configuration file
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"autoUpdate": true,
|
"autoUpdate": true,
|
||||||
"systemTray": false,
|
|
||||||
"discordRPC": true,
|
"discordRPC": true,
|
||||||
"customTopBar": true,
|
"customTopBar": true,
|
||||||
"autoCheckInfo": true,
|
"autoCheckInfo": true,
|
||||||
@@ -8,8 +7,7 @@
|
|||||||
"addMetadata": true,
|
"addMetadata": true,
|
||||||
"verboseLogs": false,
|
"verboseLogs": false,
|
||||||
"autoDownloadPlaylist": true,
|
"autoDownloadPlaylist": true,
|
||||||
"keepPlaylistOrder": false,
|
"createPlaylistFolders": true,
|
||||||
"createPlaylistFolders": false,
|
|
||||||
"customCodec": "h264",
|
"customCodec": "h264",
|
||||||
"logSystem": true,
|
"logSystem": true,
|
||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
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}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
testEnvironment: "node",
|
|
||||||
testMatch: ["**/test/unit/**/*.test.js"],
|
|
||||||
collectCoverageFrom: [
|
|
||||||
"server/**/*.js",
|
|
||||||
"app/**/*.js",
|
|
||||||
"!app/autoUpdater.js",
|
|
||||||
],
|
|
||||||
|
|
||||||
modulePathIgnorePatterns: [
|
|
||||||
"<rootDir>/dist",
|
|
||||||
"<rootDir>/release",
|
|
||||||
"<rootDir>/build",
|
|
||||||
],
|
|
||||||
|
|
||||||
watchPathIgnorePatterns: [
|
|
||||||
"<rootDir>/dist",
|
|
||||||
"<rootDir>/release",
|
|
||||||
"<rootDir>/build",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
103
main.js
103
main.js
@@ -13,9 +13,6 @@ process.on("unhandledRejection", (reason) => {
|
|||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
|
||||||
app.setDesktopName('com.masteracnolo.freedomloader.desktop');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if this is the primary instance (lock acquired successfully)
|
* True if this is the primary instance (lock acquired successfully)
|
||||||
@@ -40,8 +37,18 @@ app.setAppUserModelId("com.masteracnolo.freedomloader");
|
|||||||
/**
|
/**
|
||||||
* Load the app dependencies for hardware choice
|
* Load the app dependencies for hardware choice
|
||||||
*/
|
*/
|
||||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
||||||
const { configFeatures, devMode} = require("./config");
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In-memory snapshot of application feature flags.
|
* In-memory snapshot of application feature flags.
|
||||||
@@ -49,20 +56,15 @@ const { configFeatures, devMode} = require("./config");
|
|||||||
* Note: Changes to the config file are not automatically reflected.
|
* Note: Changes to the config file are not automatically reflected.
|
||||||
* A restart could be required.
|
* A restart could be required.
|
||||||
*/
|
*/
|
||||||
if (!configFeatures.enableHardwareAcceleration){
|
const config = require("./config");
|
||||||
app.disableHardwareAcceleration();
|
|
||||||
logger.info("Disabled Hardware Acceleration");
|
|
||||||
} else {
|
|
||||||
logger.info("Enable Hardware Acceleration");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(devMode){
|
if(config.devMode){
|
||||||
/**
|
/**
|
||||||
* Start devTron extensions - @see https://github.com/electron/devtron
|
* Start devTron extensions - @see https://github.com/electron/devtron
|
||||||
*/
|
*/
|
||||||
const { devtron } = require('@electron/devtron');
|
const { devtron } = require('@electron/devtron');
|
||||||
devtron.install();
|
devtron.install();
|
||||||
logger.info("Loaded DevTron Extension");
|
logger.info("Loaded DevTron Extension")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,46 +79,8 @@ const { updateYtDlp } = require("./app/ytDlpUpdater");
|
|||||||
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
||||||
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
||||||
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
|
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
|
||||||
const { userThemesPath, initUserThemes, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
|
const { userThemesPath, initUserThemes } = 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
|
* If another instance want to run
|
||||||
@@ -135,14 +99,11 @@ app.on("second-instance", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
logSessionStart(logDir, defaultDownloadFolder);
|
logSessionStart();
|
||||||
checkReportService().catch((err) => {
|
|
||||||
logger.warn(`Bug report service startup check failed: ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
createSplashWindow();
|
createSplashWindow();
|
||||||
|
|
||||||
if (!devMode && !checkNativeDependencies()) return;
|
if (!config.devMode && !checkNativeDependencies()) return;
|
||||||
|
|
||||||
const { userYtDlp } = require("./server/helpers/path.helpers");
|
const { userYtDlp } = require("./server/helpers/path.helpers");
|
||||||
updateYtDlp(userYtDlp);
|
updateYtDlp(userYtDlp);
|
||||||
@@ -155,9 +116,6 @@ app.whenReady().then(async () => {
|
|||||||
registerIpcHandlers(getMainWindow);
|
registerIpcHandlers(getMainWindow);
|
||||||
|
|
||||||
setSplashProgress(2); // Loading themes
|
setSplashProgress(2); // Loading themes
|
||||||
|
|
||||||
validateBinaries();
|
|
||||||
|
|
||||||
initUserThemes();
|
initUserThemes();
|
||||||
await initThemes(userThemesPath);
|
await initThemes(userThemesPath);
|
||||||
|
|
||||||
@@ -166,11 +124,6 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
closeSplashWindow();
|
closeSplashWindow();
|
||||||
getMainWindow().show();
|
getMainWindow().show();
|
||||||
|
|
||||||
if (configFeatures.systemTray) {
|
|
||||||
createSystemTray(devMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configFeatures.discordRPC) startRPC();
|
if (configFeatures.discordRPC) startRPC();
|
||||||
|
|
||||||
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
||||||
@@ -182,20 +135,12 @@ app.whenReady().then(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
if (app.isQuitting || !configFeatures.systemTray) {
|
logger.info("Shutting down...");
|
||||||
logger.info("Shutting down...");
|
app.quit();
|
||||||
app.quit();
|
|
||||||
} else if (process.platform !== "darwin") {
|
|
||||||
logger.info("Main window closed, app running in background (Tray).");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("before-quit", (event) => {
|
app.on("before-quit", async () => {
|
||||||
app.isQuitting = true;
|
await stopRPC();
|
||||||
|
logger.info("All services stopped. Have a nice day!");
|
||||||
if (!isCleanedUp) {
|
logSessionEnd();
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
cleanShutdown();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
7110
package-lock.json
generated
7110
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
174
package.json
174
package.json
@@ -1,42 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"desktopName": "com.masteracnolo.freedomloader",
|
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.6.4",
|
"version": "1.5.1",
|
||||||
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
|
"author": "MasterAcnolo",
|
||||||
"description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms",
|
"description": "Freedom Loader",
|
||||||
"homepage": "https://freedomloader.acnolo.fr/",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/MasterAcnolo/Freedom-Loader.git"
|
|
||||||
},
|
|
||||||
"license": "GPL-3.0-only",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/MasterAcnolo/Freedom-Loader/issues"
|
|
||||||
},
|
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean:dist": "rm -rf dist/ && rm -rf srpm-out/",
|
"start": "electron --trace-warnings .",
|
||||||
"start": "electron --no-warnings .",
|
|
||||||
"start:warn": "electron --trace-warnings .",
|
|
||||||
"start:debug": "WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .",
|
|
||||||
"dev": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --no-warnings .\"",
|
|
||||||
"dev:warn": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --trace-warnings .\"",
|
|
||||||
"dev:debug": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .\"",
|
|
||||||
"build": "electron-builder",
|
"build": "electron-builder",
|
||||||
"build:win": "electron-builder --win",
|
|
||||||
"build:linux": "electron-builder --linux",
|
|
||||||
"build:rpm": "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 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"
|
"update": "npm update"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -52,41 +23,26 @@
|
|||||||
"winston-daily-rotate-file": "^5.0.0"
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/devtron": "^2.1.1",
|
"electron": "^41.2.0",
|
||||||
"@eslint/js": "^10.0.1",
|
"electron-builder": "^26.8.1",
|
||||||
"@jest/globals": "^30.4.1",
|
"@electron/devtron": "^2.1.1"
|
||||||
"@playwright/test": "^1.62.1",
|
|
||||||
"electron": "^44.3.0",
|
|
||||||
"electron-builder": "^26.15.3",
|
|
||||||
"eslint": "^10.10.0",
|
|
||||||
"eslint-formatter-pretty": "^7.1.0",
|
|
||||||
"globals": "^17.12.0",
|
|
||||||
"jest": "^30.4.2",
|
|
||||||
"nodemon": "^3.1.14",
|
|
||||||
"playwright": "^1.62.1"
|
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.masteracnolo.freedomloader",
|
"appId": "com.masteracnolo.freedomloader",
|
||||||
"artifactName": "Freedom-Loader-${version}.${ext}",
|
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"publish": [
|
"publish": [
|
||||||
{
|
{
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
"owner": "MasterAcnolo",
|
"owner": "MasterAcnolo",
|
||||||
"repo": "Freedom-Loader",
|
"repo": "Freedom-Loader"
|
||||||
"releaseType": "draft"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"app/**/*",
|
"**/*",
|
||||||
"public/**/*",
|
"node_modules/**/*",
|
||||||
"server/**/*",
|
"!config/config.dev.json",
|
||||||
"build/**/*",
|
"!theme/**/*"
|
||||||
"config/config.default.json",
|
|
||||||
"config.js",
|
|
||||||
"main.js",
|
|
||||||
"preload.js",
|
|
||||||
"package.json"
|
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "build"
|
"buildResources": "build"
|
||||||
@@ -98,38 +54,49 @@
|
|||||||
"to": "theme"
|
"to": "theme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "config/config.default.json",
|
"from": "build/confirm-icon.png",
|
||||||
"to": "config/config.default.json"
|
"to": "confirm-icon.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "build",
|
"from": "build/app-icon.ico",
|
||||||
"to": ".",
|
"to": "app-icon.ico"
|
||||||
"filter": [
|
},
|
||||||
"app-icon.ico",
|
{
|
||||||
"app-icon-64x64.png",
|
"from": "build/banner.bmp",
|
||||||
"confirm-icon.png",
|
"to": "banner.bmp"
|
||||||
"banner.bmp",
|
},
|
||||||
"banner.png",
|
{
|
||||||
"error.png"
|
"from": "build/banner.png",
|
||||||
]
|
"to": "banner.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "build/error.png",
|
||||||
|
"to": "error.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/yt-dlp.exe",
|
||||||
|
"to": "binaries/yt-dlp.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/ffmpeg.exe",
|
||||||
|
"to": "binaries/ffmpeg.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/ffprobe.exe",
|
||||||
|
"to": "binaries/ffprobe.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/deno.exe",
|
||||||
|
"to": "binaries/deno.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "config/config.default.json",
|
||||||
|
"to": "config/config.default.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"icon": "build/app-icon.ico",
|
"icon": "build/app-icon.ico"
|
||||||
"artifactName": "Freedom-Loader-Setup-${version}.${ext}",
|
|
||||||
"extraResources": [
|
|
||||||
{
|
|
||||||
"from": "resources/binaries/win-32",
|
|
||||||
"to": "binaries",
|
|
||||||
"filter": [
|
|
||||||
"yt-dlp.exe",
|
|
||||||
"ffmpeg.exe",
|
|
||||||
"ffprobe.exe",
|
|
||||||
"deno.exe"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
@@ -141,47 +108,6 @@
|
|||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": true,
|
"createStartMenuShortcut": true,
|
||||||
"shortcutName": "Freedom Loader"
|
"shortcutName": "Freedom Loader"
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": [
|
|
||||||
"AppImage",
|
|
||||||
"deb",
|
|
||||||
"snap"
|
|
||||||
],
|
|
||||||
"category": "Utility",
|
|
||||||
"synopsis": "GUI for yt-dlp to download video and audio with metadata",
|
|
||||||
"maintainer": "MasterAcnolo",
|
|
||||||
"icon": "build/app-icon.png",
|
|
||||||
"executableName": "freedom-loader",
|
|
||||||
"extraResources": [
|
|
||||||
{
|
|
||||||
"from": "resources/binaries/linux",
|
|
||||||
"to": "binaries",
|
|
||||||
"filter": [
|
|
||||||
"yt-dlp",
|
|
||||||
"ffmpeg",
|
|
||||||
"ffprobe",
|
|
||||||
"deno"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"deb": {
|
|
||||||
"fpm": [
|
|
||||||
"./package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"rpm": {
|
|
||||||
"fpm": [
|
|
||||||
"./package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"snap": {
|
|
||||||
"confinement": "strict",
|
|
||||||
"plugs": [
|
|
||||||
"default",
|
|
||||||
"removable-media"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
[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
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
<component type="desktop-application">
|
|
||||||
<id>com.masteracnolo.freedomloader</id>
|
|
||||||
|
|
||||||
<name>Freedom Loader</name>
|
|
||||||
|
|
||||||
<summary>GUI for yt-dlp to download video and audio with metadata</summary>
|
|
||||||
|
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
|
||||||
<project_license>GPL-3.0-only</project_license>
|
|
||||||
|
|
||||||
<developer id="com.masteracnolo">
|
|
||||||
<name>MasterAcnolo</name>
|
|
||||||
</developer>
|
|
||||||
|
|
||||||
<description>
|
|
||||||
<p>
|
|
||||||
Freedom Loader is a free and open-source graphical interface built
|
|
||||||
around yt-dlp, the powerful command-line media downloader. It lets
|
|
||||||
you download video and audio from hundreds of supported websites
|
|
||||||
without ever touching a terminal, with no ads, no tracking, and
|
|
||||||
no questionable third-party services.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Under the hood, Freedom Loader bundles yt-dlp and ffmpeg directly,
|
|
||||||
so there is nothing extra to install: paste a link, choose a
|
|
||||||
format and quality, and the app handles fetching, converting
|
|
||||||
and tagging the file for you.
|
|
||||||
</p>
|
|
||||||
<p>Features include:</p>
|
|
||||||
<ul>
|
|
||||||
<li>MP4 video or MP3 audio downloads with embedded metadata and thumbnails</li>
|
|
||||||
<li>Quality selection from best to specific resolutions (1080p, 720p, 480p)</li>
|
|
||||||
<li>Codec choice: H.264, H.265, AV1, VP9 and Theora</li>
|
|
||||||
<li>Playlist downloads with automatic per-playlist folders</li>
|
|
||||||
<li>Cookie-based access to age-restricted and member-only content</li>
|
|
||||||
<li>Custom theme system with a web-based theme creator</li>
|
|
||||||
<li>Real-time download progress with network speed indicator</li>
|
|
||||||
<li>Optional Discord Rich Presence integration</li>
|
|
||||||
<li>Automatic application and download-engine updates</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
DRM-protected content is not supported and cannot be downloaded.
|
|
||||||
</p>
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<launchable type="desktop-id">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>
|
|
||||||
<url type="vcs-browser">https://github.com/MasterAcnolo/Freedom-Loader</url>
|
|
||||||
|
|
||||||
<categories>
|
|
||||||
<category>AudioVideo</category>
|
|
||||||
<category>Utility</category>
|
|
||||||
<category>Network</category>
|
|
||||||
</categories>
|
|
||||||
|
|
||||||
<releases>
|
|
||||||
<release version="1.6.0" date="2026-06-23">
|
|
||||||
<description>
|
|
||||||
<p>Latest stable release.</p>
|
|
||||||
</description>
|
|
||||||
</release>
|
|
||||||
</releases>
|
|
||||||
|
|
||||||
<content_rating type="oars-1.1"/>
|
|
||||||
|
|
||||||
<provides>
|
|
||||||
<binary>freedom-loader</binary>
|
|
||||||
</provides>
|
|
||||||
</component>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/sh -xe
|
|
||||||
git clone --depth 1 https://github.com/MasterAcnolo/Freedom-Loader.git src
|
|
||||||
cd src
|
|
||||||
npm ci
|
|
||||||
npx electron-builder --linux dir
|
|
||||||
|
|
||||||
VERSION=$(node -p "require('./package.json').version")
|
|
||||||
tar czf "$resultdir/freedom-loader-$VERSION.tar.gz" -C dist linux-unpacked
|
|
||||||
sed "s/__VERSION__/$VERSION/" package/freedom-loader.spec > "$resultdir/freedom-loader.spec"
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
Name: freedom-loader
|
|
||||||
Version: __VERSION__
|
|
||||||
Release: 1%{?dist}
|
|
||||||
Summary: GUI for yt-dlp media downloads
|
|
||||||
License: GPL-3.0-only
|
|
||||||
Source0: freedom-loader-__VERSION__.x86_64.rpm
|
|
||||||
BuildArch: x86_64
|
|
||||||
Requires: glibc, gtk3, nss, libXScrnSaver
|
|
||||||
|
|
||||||
%description
|
|
||||||
GUI for yt-dlp media downloads.
|
|
||||||
|
|
||||||
%install
|
|
||||||
rpm2cpio %{SOURCE0} | (cd %{buildroot} && cpio -idmv)
|
|
||||||
mkdir -p %{buildroot}/usr/bin
|
|
||||||
ln -sf "/opt/Freedom Loader/freedom-loader" %{buildroot}/usr/bin/freedom-loader
|
|
||||||
|
|
||||||
%files
|
|
||||||
/opt/*
|
|
||||||
/usr/bin/freedom-loader
|
|
||||||
/usr/share/applications/*
|
|
||||||
/usr/share/metainfo/*
|
|
||||||
/usr/share/icons/hicolor
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
118
preload.js
118
preload.js
@@ -8,31 +8,6 @@ const { contextBridge, ipcRenderer } = require("electron");
|
|||||||
* All calls are explicitly whitelisted.
|
* All calls are explicitly whitelisted.
|
||||||
*/
|
*/
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
/**
|
|
||||||
* Sends error log message.
|
|
||||||
*
|
|
||||||
* @param {...any} args - Values to log
|
|
||||||
*/
|
|
||||||
logError: (...args) => ipcRenderer.send("log-error", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends info log message.
|
|
||||||
*
|
|
||||||
* @param {...any} args - Values to log
|
|
||||||
*/
|
|
||||||
logInfo: (...args) => ipcRenderer.send("log-info", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends warning log message.
|
|
||||||
*
|
|
||||||
* @param {...any} args - Values to log
|
|
||||||
*/
|
|
||||||
logWarn: (...args) => ipcRenderer.send("log-warn", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return process.platform to renderer
|
|
||||||
*/
|
|
||||||
getProcessPlatform: () => process.platform,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default system download directory.
|
* Returns the default system download directory.
|
||||||
@@ -62,23 +37,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
* @param {string} key - Feature name
|
* @param {string} key - Feature name
|
||||||
* @param {any} value - Feature value
|
* @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.
|
* Returns the current application version.
|
||||||
*/
|
*/
|
||||||
getVersion: () => ipcRenderer.invoke("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.
|
* Validates a download path before using it for file operations.
|
||||||
*
|
*
|
||||||
@@ -96,77 +62,16 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
* Forces a reload of theme files (useful after modification/import).
|
* Forces a reload of theme files (useful after modification/import).
|
||||||
*/
|
*/
|
||||||
reloadThemes: () => ipcRenderer.invoke("reload-themes"),
|
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.
|
* Exposes window control and developer utilities for the custom topbar UI.
|
||||||
*
|
*
|
||||||
* These methods forward commands to the Electron main process via IPC.
|
* These methods forward commands to the Electron main process via IPC.
|
||||||
*/
|
*/
|
||||||
contextBridge.exposeInMainWorld("topbarAPI", {
|
contextBridge.exposeInMainWorld("topbarAPI", {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimizes the application window.
|
* Minimizes the application window.
|
||||||
*/
|
*/
|
||||||
@@ -216,17 +121,4 @@ contextBridge.exposeInMainWorld("topbarAPI", {
|
|||||||
* Opens configuration/settings panel.
|
* Opens configuration/settings panel.
|
||||||
*/
|
*/
|
||||||
openConfig: () => ipcRenderer.send("open-config"),
|
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"),
|
|
||||||
});
|
});
|
||||||
BIN
public/assets/logo/Freedom Loader White.png
Normal file
BIN
public/assets/logo/Freedom Loader White.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
public/assets/logo/Freedom Loader.png
Normal file
BIN
public/assets/logo/Freedom Loader.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/assets/logo/FreedomLoader2.png
Normal file
BIN
public/assets/logo/FreedomLoader2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -13,8 +13,6 @@
|
|||||||
<link rel="stylesheet" href="styles/styles.css">
|
<link rel="stylesheet" href="styles/styles.css">
|
||||||
<link rel="stylesheet" href="styles/layout/topbar.css">
|
<link rel="stylesheet" href="styles/layout/topbar.css">
|
||||||
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -23,11 +21,9 @@
|
|||||||
<div class="custom-controls">
|
<div class="custom-controls">
|
||||||
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
||||||
<button id="logs-btn" title="Logs">Logs</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="website-btn" title="Website">Website</button>
|
||||||
<button id="wiki-btn" title="Wiki">Wiki</button>
|
<button id="wiki-btn" title="Wiki">Wiki</button>
|
||||||
<button id="workshop-btn" title="Workshop">Theme Workshop</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> -->
|
<!-- <button id="theme-btn" title="Theme">Theme</button> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="window-controls">
|
<div class="window-controls">
|
||||||
@@ -60,9 +56,9 @@
|
|||||||
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<div style="display: flex; align-items: center; gap: 10px;">
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
<span class="setting-title">Theme</span>
|
<span class="setting-title">Thème</span>
|
||||||
|
|
||||||
<select id="themeSelect" data-key="theme" aria-label="Choose the theme">
|
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
<button id="refresh-themes-btn" title="Refresh themes" class="refresh-themes-btn">
|
<button id="refresh-themes-btn" title="Refresh themes" class="refresh-themes-btn">
|
||||||
@@ -110,10 +106,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<input data-key="keepPlaylistOrder" id="keepPlaylistOrder" type="checkbox">
|
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Keep Playlist Order</span>
|
<span class="setting-title">Auto Fetch Info</span>
|
||||||
<small>Add index before output file, to keep the order.</small>
|
<small>Automatically fetch information for new downloads.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,31 +127,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
<!-- <div class="setting-item">
|
||||||
<input data-key="systemTray" id="systemTray" type="checkbox">
|
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">System Tray</span>
|
<span class="setting-title">Output title (special chars)</span>
|
||||||
<small>Minimize app on close, in the System Tray. [EXPERIMENTAL]</small>
|
<small>Check for unusual characters in output titles.</small>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
<div class="setting-item">
|
||||||
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
|
||||||
<div class="setting-info">
|
|
||||||
<span class="setting-title">Output title (special chars)</span>
|
|
||||||
<small>Check for unusual characters in output titles.</small>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<input data-key="autoCheckInfo" id="autoCheckInfo" type="checkbox">
|
|
||||||
<div class="setting-info">
|
|
||||||
<span class="setting-title">Auto Fetch Info</span>
|
|
||||||
<small>Automatically fetch information for new downloads.</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Add Metadata</span>
|
<span class="setting-title">Add Metadata</span>
|
||||||
@@ -266,7 +246,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="download-path">
|
<div class="download-path">
|
||||||
Will be saved in <span id="savePath"></span>
|
Will be save on <span id="savePath"></span>
|
||||||
<button id="changePath">Edit</button>
|
<button id="changePath">Edit</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -299,53 +279,14 @@
|
|||||||
|
|
||||||
<span id="version-badge" class="version-badge"></span>
|
<span id="version-badge" class="version-badge"></span>
|
||||||
|
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div id="toast-container"></div>
|
<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>
|
<h2 id="reference"> Because Why Not ? </h2>
|
||||||
|
|
||||||
<div id="updateAvailable" style="display:none">
|
|
||||||
<div class="update-banner">
|
|
||||||
<div class="banner-row">
|
|
||||||
<div class="banner-left">
|
|
||||||
<div class="icon-wrap">
|
|
||||||
<span class="material-symbols-outlined" id="update-icon">download</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-block">
|
|
||||||
<span class="update-title" id="update-title">Update available</span>
|
|
||||||
<span class="update-sub" id="update-sub">A new version is ready</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn-update" id="btn-update">Download</button>
|
|
||||||
</div>
|
|
||||||
<div class="progress-wrap" id="progress-wrap" style="display:none">
|
|
||||||
<div class="progress-track">
|
|
||||||
<div class="progress-fill" id="progress-fill"></div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-meta">
|
|
||||||
<span id="progress-pct">0%</span>
|
|
||||||
<span id="progress-eta"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="script/toast.js"></script>
|
<script src="script/toast.js"></script>
|
||||||
@@ -357,9 +298,6 @@
|
|||||||
<script src="script/fetchinfo.js"></script>
|
<script src="script/fetchinfo.js"></script>
|
||||||
<script src="script/progressBar.js"></script>
|
<script src="script/progressBar.js"></script>
|
||||||
<script src="script/customthemes.js"></script>
|
<script src="script/customthemes.js"></script>
|
||||||
<script src="script/reportBug.js"></script>
|
|
||||||
<script src="script/autoUpdate.js"></script>
|
|
||||||
<script type="module" src="script/changelog.js"></script>
|
|
||||||
<script type="module" src="script/topbar.js"></script>
|
<script type="module" src="script/topbar.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,54 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Retrieves the application version and build state from the backend.
|
* Retrieves the application version from the Electron main process
|
||||||
* Displays a formatted string indicating packaging state and preview status.
|
* and displays it in the UI version badge (bottom-right corner).
|
||||||
* Hides the "Packaged" state for production builds to keep the UI clean.
|
|
||||||
*
|
*
|
||||||
* Example outputs: "Not Packaged Preview v1.6.2" or "Preview v1.6.2"
|
* This is mainly used for debugging and build identification.
|
||||||
*
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
async function displayVersionLabel() {
|
async function versionLabel() {
|
||||||
/**
|
|
||||||
* Raw version string from the backend (e.g., "dev-1.6.2-preview" or "v1.6.2")
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const appVersion = await window.electronAPI.getVersion();
|
const appVersion = await window.electronAPI.getVersion();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI element displaying the current application version.
|
* UI element displaying the current application version.
|
||||||
* @type {HTMLElement | null}
|
* Updated at runtime after IPC call resolves.
|
||||||
*/
|
*/
|
||||||
const versionBadge = document.getElementById("version-badge");
|
const versionBadge = document.getElementById("version-badge");
|
||||||
|
|
||||||
if (!versionBadge) return;
|
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}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
displayVersionLabel();
|
versionLabel();
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
/** 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();
|
|
||||||
@@ -17,7 +17,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
*/
|
*/
|
||||||
pasteBtn.addEventListener("click", async () => {
|
pasteBtn.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
urlInput.value = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText();
|
||||||
|
|
||||||
|
urlInput.value = text;
|
||||||
|
|
||||||
// Notify frameworks / listeners bound to input changes
|
// Notify frameworks / listeners bound to input changes
|
||||||
urlInput.dispatchEvent(new Event("input", { bubbles: true }));
|
urlInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
|||||||
@@ -32,52 +32,52 @@ function applyTheme(theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CSS variables (design tokens)
|
// CSS variables (design tokens)
|
||||||
root.style.setProperty("--background-color", style.colors?.background || "#121212");
|
root.style.setProperty("--background-color", style.colors?.background || "");
|
||||||
root.style.setProperty("--default-text-color", style.colors?.text?.default || "#e0e0e0");
|
root.style.setProperty("--default-text-color", style.colors?.text?.default || "");
|
||||||
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "#2196f3");
|
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "");
|
||||||
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "#f5f5f5");
|
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "");
|
||||||
|
|
||||||
root.style.setProperty("--form-bg-color", style.form?.background || "#1e1e1e");
|
root.style.setProperty("--form-bg-color", style.form?.background || "");
|
||||||
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "#2a2a2a");
|
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "");
|
||||||
root.style.setProperty("--form-input-border-color", style.form?.input?.border || "#444444");
|
root.style.setProperty("--form-input-border-color", style.form?.input?.border || "");
|
||||||
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "#2196f3");
|
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "");
|
||||||
root.style.setProperty("--form-input-text-color", style.form?.input?.text || "#f5f5f5");
|
root.style.setProperty("--form-input-text-color", style.form?.input?.text || "");
|
||||||
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "#888888");
|
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "");
|
||||||
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "#2196f3");
|
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "");
|
||||||
root.style.setProperty("--form-button-text-color", style.form?.button?.text || "#ffffff");
|
root.style.setProperty("--form-button-text-color", style.form?.button?.text || "");
|
||||||
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "#1976d2");
|
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "");
|
||||||
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "#ffffff");
|
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "");
|
||||||
|
|
||||||
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "#555555");
|
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "");
|
||||||
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "#2196f3");
|
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "");
|
||||||
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "#ffffff");
|
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "");
|
||||||
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "rgba(33, 150, 243, 0.5)");
|
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "");
|
||||||
|
|
||||||
root.style.setProperty("--download-status-color", style.download?.status || "#2196f3");
|
root.style.setProperty("--download-status-color", style.download?.status || "");
|
||||||
|
|
||||||
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "#444444");
|
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "");
|
||||||
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "#2196f3");
|
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "");
|
||||||
|
|
||||||
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "#1a1a1a");
|
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "");
|
||||||
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "#e0e0e0");
|
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "");
|
||||||
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "#2196f3");
|
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "");
|
||||||
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "#cccccc");
|
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "");
|
||||||
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "#ffffff");
|
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "");
|
||||||
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "#2196f3");
|
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "");
|
||||||
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "#1976d2");
|
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "");
|
||||||
|
|
||||||
root.style.setProperty("--playlist-background-color", style.playlist?.background || "#303030");
|
root.style.setProperty("--playlist-background-color", style.playlist?.background || "");
|
||||||
|
|
||||||
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "#1e1e1e");
|
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "");
|
||||||
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "#e0e0e0");
|
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "");
|
||||||
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "#2196f3");
|
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "");
|
||||||
root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || "#ffffff");
|
root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || "");
|
||||||
root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || "#1e1e1e");
|
root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || "");
|
||||||
root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || "#e0e0e0");
|
root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || "");
|
||||||
root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || "#1e1e1e");
|
root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || "");
|
||||||
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "#2a2a2a");
|
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "");
|
||||||
root.style.setProperty("--settings-text-color", style.settings?.text || "#e0e0e0");
|
root.style.setProperty("--settings-text-color", style.settings?.text || "");
|
||||||
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "#aaaaaa");
|
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "");
|
||||||
|
|
||||||
const subtitleEl = document.getElementById("subtitle");
|
const subtitleEl = document.getElementById("subtitle");
|
||||||
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
|
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
|
||||||
@@ -116,102 +116,7 @@ function populateThemeSelect(themes) {
|
|||||||
*/
|
*/
|
||||||
async function initThemes() {
|
async function initThemes() {
|
||||||
loadedThemes = await window.electronAPI.getThemes();
|
loadedThemes = await window.electronAPI.getThemes();
|
||||||
|
if (!loadedThemes.length) return;
|
||||||
if (!loadedThemes.length) {
|
|
||||||
const defaultTheme = {
|
|
||||||
id: "dark",
|
|
||||||
name: "Dark",
|
|
||||||
subtitle: "Because Why Not ?",
|
|
||||||
style: {
|
|
||||||
background: {
|
|
||||||
size: "cover",
|
|
||||||
position: "center",
|
|
||||||
attachment: "fixed"
|
|
||||||
},
|
|
||||||
|
|
||||||
colors: {
|
|
||||||
background: "#121212",
|
|
||||||
text: {
|
|
||||||
default: "#e0e0e0",
|
|
||||||
subtitle: "#2196f3",
|
|
||||||
audioOnly: "#f5f5f5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
form: {
|
|
||||||
background: "#1e1e1e",
|
|
||||||
input: {
|
|
||||||
background: "#2a2a2a",
|
|
||||||
border: "#444444",
|
|
||||||
borderFocus: "#2196f3",
|
|
||||||
text: "#f5f5f5",
|
|
||||||
placeholder: "#888888"
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
background: "#2196f3",
|
|
||||||
text: "#ffffff",
|
|
||||||
hover: "#1976d2"
|
|
||||||
},
|
|
||||||
pasteButtonIcon: "#ffffff"
|
|
||||||
},
|
|
||||||
|
|
||||||
checkbox: {
|
|
||||||
background: {
|
|
||||||
unchecked: "#555555",
|
|
||||||
checked: "#2196f3"
|
|
||||||
},
|
|
||||||
checkmarkBorder: "#ffffff",
|
|
||||||
pulse: "rgba(33, 150, 243, 0.5)"
|
|
||||||
},
|
|
||||||
|
|
||||||
download: {
|
|
||||||
status: "#2196f3"
|
|
||||||
},
|
|
||||||
|
|
||||||
progressBar: {
|
|
||||||
background: "#444444",
|
|
||||||
fill: "#2196f3"
|
|
||||||
},
|
|
||||||
|
|
||||||
videoInfo: {
|
|
||||||
background: "#1a1a1a",
|
|
||||||
text: "#e0e0e0",
|
|
||||||
heading: "#2196f3",
|
|
||||||
list: "#cccccc",
|
|
||||||
strong: "#ffffff",
|
|
||||||
link: "#2196f3",
|
|
||||||
linkHover: "#1976d2"
|
|
||||||
},
|
|
||||||
|
|
||||||
playlist: {
|
|
||||||
background: "#303030"
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
button: {
|
|
||||||
background: "#1e1e1e",
|
|
||||||
text: "#e0e0e0"
|
|
||||||
},
|
|
||||||
openThemeButton: {
|
|
||||||
background: "#2196f3",
|
|
||||||
text: "#ffffff"
|
|
||||||
},
|
|
||||||
openJsonButton: {
|
|
||||||
background: "#2196f3",
|
|
||||||
text: "#ffffff"
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
modal: "#1e1e1e",
|
|
||||||
section: "#2a2a2a"
|
|
||||||
},
|
|
||||||
text: "#e0e0e0",
|
|
||||||
subtitle: "#aaaaaa"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadedThemes = [defaultTheme];
|
|
||||||
}
|
|
||||||
|
|
||||||
populateThemeSelect(loadedThemes);
|
populateThemeSelect(loadedThemes);
|
||||||
|
|
||||||
|
|||||||
@@ -30,15 +30,15 @@ function formatSize(bytes) {
|
|||||||
*/
|
*/
|
||||||
async function fetchVideoInfo(url) {
|
async function fetchVideoInfo(url) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/info?url=${encodeURIComponent(url)}`, {
|
const res = await fetch("/info", {
|
||||||
method: "GET",
|
method: "POST",
|
||||||
headers: {"Accept": "application/json"}
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: new URLSearchParams({ url }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return { error: `An Error occured when fetching info` };
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (!res.ok) return {error: `An Error occured when fetching info`};
|
|
||||||
|
|
||||||
if (!data) return { error: "Data is Missing" };
|
if (!data) return { error: "Data is Missing" };
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -269,8 +269,8 @@ async function init() {
|
|||||||
infoDiv.classList.add("visible");
|
infoDiv.classList.add("visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
})
|
||||||
|
|
||||||
}
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
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,7 +16,8 @@ let isOpen = false;
|
|||||||
* Toggles visibility of the settings panel when clicking the settings button.
|
* Toggles visibility of the settings panel when clicking the settings button.
|
||||||
*/
|
*/
|
||||||
settingsButton.addEventListener("click", () => {
|
settingsButton.addEventListener("click", () => {
|
||||||
settingsPanel.classList.toggle("open");
|
const currentlyOpen = settingsPanel.style.display === "block";
|
||||||
|
settingsPanel.style.display = currentlyOpen ? "none" : "block";
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +25,7 @@ settingsButton.addEventListener("click", () => {
|
|||||||
*/
|
*/
|
||||||
function closePanel() {
|
function closePanel() {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
settingsPanel.classList.remove("open");
|
settingsPanel.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,20 +17,18 @@ window.showToast = function (message, type = "info", duration = 4000) {
|
|||||||
toast.classList.add("toast", type);
|
toast.classList.add("toast", type);
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
success: "check_circle",
|
success: "✓",
|
||||||
error: "error",
|
error: "✕",
|
||||||
warning: "warning",
|
warning: "⚠",
|
||||||
info: "info"
|
info: "ℹ"
|
||||||
};
|
};
|
||||||
|
|
||||||
const icon = icons[type] || icons.info;
|
const icon = icons[type] || icons.info;
|
||||||
|
|
||||||
toast.style.setProperty("--toast-duration", `${duration}ms`);
|
|
||||||
toast.innerHTML = `
|
toast.innerHTML = `
|
||||||
<span class="toast-icon material-icons-round">${icon}</span>
|
<span class="toast-icon">${icon}</span>
|
||||||
<span class="toast-message">${message}</span>
|
<span class="toast-message">${message}</span>
|
||||||
<button class="toast-close" title="Close">×</button>
|
<button class="toast-close" title="Close">×</button>
|
||||||
<div class="toast-progress"></div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const closeBtn = toast.querySelector(".toast-close");
|
const closeBtn = toast.querySelector(".toast-close");
|
||||||
|
|||||||
@@ -5,55 +5,53 @@
|
|||||||
* This function is safe to call once DOM is ready.
|
* This function is safe to call once DOM is ready.
|
||||||
* It silently aborts if the API or elements are missing.
|
* It silently aborts if the API or elements are missing.
|
||||||
*/
|
*/
|
||||||
export function initTopBar() {
|
function setupTopbarListeners() {
|
||||||
const { topbarAPI } = window;
|
const { topbarAPI } = window;
|
||||||
if (!topbarAPI) return;
|
if (!topbarAPI) return;
|
||||||
|
|
||||||
const minBtn = document.getElementById("minimize-btn");
|
const minBtn = document.getElementById("minimize-btn");
|
||||||
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
|
||||||
|
|
||||||
const maxBtn = document.getElementById("maximize-btn");
|
const maxBtn = document.getElementById("maximize-btn");
|
||||||
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
|
||||||
|
|
||||||
const closeBtn = document.getElementById("close-btn");
|
const closeBtn = document.getElementById("close-btn");
|
||||||
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
|
||||||
|
|
||||||
const devtoolsBtn = document.getElementById("devtools-btn");
|
const devtoolsBtn = document.getElementById("devtools-btn");
|
||||||
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
|
||||||
|
|
||||||
const logsBtn = document.getElementById("logs-btn");
|
const logsBtn = document.getElementById("logs-btn");
|
||||||
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
|
||||||
|
|
||||||
const websiteBtn = document.getElementById("website-btn");
|
const websiteBtn = document.getElementById("website-btn");
|
||||||
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
|
||||||
|
|
||||||
const wikiBtn = document.getElementById("wiki-btn");
|
const wikiBtn = document.getElementById("wiki-btn");
|
||||||
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
|
||||||
|
|
||||||
const workshopBtn = document.getElementById("workshop-btn");
|
const workshopBtn = document.getElementById("workshop-btn");
|
||||||
|
|
||||||
|
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||||
|
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
||||||
|
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
||||||
|
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
||||||
|
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
||||||
|
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
||||||
|
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
||||||
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function attachListeners(){
|
/**
|
||||||
try{
|
* Registers topbar event listeners after DOM is fully loaded.
|
||||||
const featuresList = await window.electronAPI.getFeatures();
|
*/
|
||||||
|
document.addEventListener("DOMContentLoaded", setupTopbarListeners);
|
||||||
|
|
||||||
if (!featuresList.customTopBar) {
|
/**
|
||||||
const topbar = document.getElementById("topbar");
|
* Applies UI layout adjustments depending on feature flags
|
||||||
const container = document.getElementById("container");
|
* provided by the Electron backend.
|
||||||
const themeSwitcher = document.getElementById("theme-switcher");
|
*
|
||||||
|
* Specifically handles:
|
||||||
|
* - hiding custom topbar
|
||||||
|
* - adjusting layout spacing
|
||||||
|
* - repositioning theme switcher
|
||||||
|
*/
|
||||||
|
(async function applyFeatureLayout() {
|
||||||
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
if (topbar) topbar.style.display = "none";
|
if (!features.customTopBar) {
|
||||||
if (container) container.style.marginTop = "0";
|
const topbar = document.getElementById("topbar");
|
||||||
if (themeSwitcher) themeSwitcher.style.top = "30px";
|
const container = document.getElementById("container");
|
||||||
}
|
const themeSwitcher = document.getElementById("theme-switcher");
|
||||||
|
|
||||||
} catch (error) {
|
if (topbar) topbar.style.display = "none";
|
||||||
console.error("Failed to load layout features for topbar:", error);
|
if (container) container.style.marginTop = "0";
|
||||||
|
if (themeSwitcher) themeSwitcher.style.top = "30px";
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
}
|
|
||||||
|
|
||||||
initTopBar()
|
|
||||||
document.addEventListener("DOMContentLoaded", attachListeners)
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
: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;
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
.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,70 +5,73 @@
|
|||||||
right: 20px;
|
right: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toast */
|
/* Toast */
|
||||||
.toast {
|
.toast {
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
padding: 14px 16px;
|
padding: 10px 14px;
|
||||||
background-color: rgba(30, 30, 30, 0.9);
|
background-color: rgba(50, 50, 50, 0.9);
|
||||||
backdrop-filter: blur(12px);
|
border-radius: 6px;
|
||||||
-webkit-backdrop-filter: blur(12px);
|
color: #ffffff;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
font-size: 0.9rem;
|
||||||
border-radius: 10px;
|
max-width: 220px;
|
||||||
color: #f3f4f6;
|
min-width: 100px;
|
||||||
font-family: inherit;
|
animation: slideIn 0.25s ease-out;
|
||||||
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;
|
pointer-events: auto;
|
||||||
overflow: hidden;
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
background-color: rgba(40, 167, 69, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background-color: rgba(220, 53, 69, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning {
|
||||||
|
background-color: rgba(255, 193, 7, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
background-color: rgba(23, 162, 184, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toast Icon */
|
/* Toast Icon */
|
||||||
.toast-icon {
|
.toast-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-icon svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.success .toast-icon {
|
.toast.success .toast-icon {
|
||||||
color: #22c55e;
|
color: #28a745;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.error .toast-icon {
|
.toast.error .toast-icon {
|
||||||
color: #ef4444;
|
color: #dc3545;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.warning .toast-icon {
|
.toast.warning .toast-icon {
|
||||||
color: #f59e0b;
|
color: #ffc107;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.info .toast-icon {
|
.toast.info .toast-icon {
|
||||||
color: #0ea5e9;
|
color: #17a2b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toast Message */
|
/* Toast Message */
|
||||||
.toast-message {
|
.toast-message {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
line-height: 1.4;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toast Close Button */
|
/* Toast Close Button */
|
||||||
@@ -76,83 +79,54 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #9ca3af;
|
color: currentColor;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
width: 24px;
|
padding: 0;
|
||||||
height: 24px;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: background-color 0.15s ease, color 0.15s ease;
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-close:hover {
|
.toast-close:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
opacity: 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 */
|
/* Animations */
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
transform: translateY(-20px);
|
transform: translateX(350px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: translateY(0);
|
transform: translateX(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideOut {
|
@keyframes slideOut {
|
||||||
from {
|
from {
|
||||||
transform: translateY(0);
|
transform: translateX(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: translateY(-20px);
|
transform: translateX(350px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes progress {
|
|
||||||
from {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
width: 0%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.removing {
|
.toast.removing {
|
||||||
animation: slideOut 0.25s ease-in forwards;
|
animation: slideOut 0.25s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
#toast-container {
|
#toast-container {
|
||||||
top: auto;
|
bottom: 12px;
|
||||||
bottom: 16px;
|
right: 12px;
|
||||||
right: 16px;
|
left: 12px;
|
||||||
left: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
|
|||||||
@@ -45,22 +45,11 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||||
|
display: none;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
/*animation: slideIn 0.2s ease-out; */
|
animation: slideIn 0.2s ease-out;
|
||||||
color: var(--settings-text-color);
|
color: var(--settings-text-color);
|
||||||
|
|
||||||
display: block;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translateX(20px);
|
|
||||||
transition: opacity 0.2s ease-out, transform 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-panel.open {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-panel::-webkit-scrollbar {
|
.settings-panel::-webkit-scrollbar {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
.custom-controls {
|
.custom-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
padding-left: 10px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RIGHT - window buttons */
|
/* RIGHT - window buttons */
|
||||||
@@ -43,11 +43,11 @@
|
|||||||
|
|
||||||
/* Custom buttons (Logs / Wiki / etc.) */
|
/* Custom buttons (Logs / Wiki / etc.) */
|
||||||
.custom-controls button {
|
.custom-controls button {
|
||||||
height: 28px;
|
height: 32px;
|
||||||
padding: 0 10px;
|
padding: 0 14px;
|
||||||
|
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-controls button:hover {
|
.custom-controls button:hover {
|
||||||
|
|||||||
@@ -12,20 +12,10 @@
|
|||||||
@import url("components/editpathbutton.css");
|
@import url("components/editpathbutton.css");
|
||||||
@import url("components/progressBar.css");
|
@import url("components/progressBar.css");
|
||||||
@import url("components/toast.css");
|
@import url("components/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');
|
@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');
|
||||||
|
|
||||||
|
|
||||||
/* Mark element as disabled */
|
|
||||||
.disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.5;
|
|
||||||
filter: grayscale(40%);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Because why not */
|
/* Because why not */
|
||||||
#reference{
|
#reference{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
BIN
ressources/yt-dlp.exe
Normal file
BIN
ressources/yt-dlp.exe
Normal file
Binary file not shown.
@@ -1,21 +0,0 @@
|
|||||||
### 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
|
|
||||||
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
|
|
||||||
RPM_OUT="$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# Resolve fpm from electron-builder cache
|
|
||||||
# Excludes Ruby gem wrappers, targets standalone binaries only
|
|
||||||
# Prefers fpm@2.x over legacy fpm-1.9.x
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
FPM=$(find ~/.cache/electron-builder -name "fpm" -type f 2>/dev/null \
|
|
||||||
| grep -v "/gems/bin/fpm" \
|
|
||||||
| grep -v "/gems/gems/" \
|
|
||||||
| grep -v "/lib/app/bin/fpm" \
|
|
||||||
| grep "fpm@" \
|
|
||||||
| head -1)
|
|
||||||
|
|
||||||
# Fallback to legacy fpm-1.9.x if fpm@2.x not found
|
|
||||||
if [ -z "$FPM" ]; then
|
|
||||||
FPM=$(find ~/.cache/electron-builder -name "fpm" -type f 2>/dev/null \
|
|
||||||
| grep -v "/gems/" \
|
|
||||||
| grep -v "/lib/app/bin/fpm" \
|
|
||||||
| head -1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$FPM" ]; then
|
|
||||||
echo "Error: fpm not found in electron-builder cache. Run 'npm run build:linux' once first to download it."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Using fpm: $FPM"
|
|
||||||
|
|
||||||
# Check that linux-unpacked exists
|
|
||||||
if [ ! -d "$ROOT_DIR/dist/linux-unpacked" ]; then
|
|
||||||
echo "Error: dist/linux-unpacked not found. Run 'npm run build:linux' first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# Prepare staging files
|
|
||||||
# fpm requires exact file-to-destination mappings to avoid
|
|
||||||
# scanning system directories (which causes permission errors)
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
# Shell wrapper for /usr/bin/freedom-loader
|
|
||||||
TMP_WRAPPER="$ROOT_DIR/.rpm-pkg-tmp-wrapper"
|
|
||||||
mkdir -p "$TMP_WRAPPER"
|
|
||||||
cat > "$TMP_WRAPPER/freedom-loader" << 'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
exec /opt/freedom-loader/freedom-loader "$@"
|
|
||||||
EOF
|
|
||||||
chmod +x "$TMP_WRAPPER/freedom-loader"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# Step 1 - Build .rpm from linux-unpacked via fpm
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
echo "Building RPM..."
|
|
||||||
"$FPM" -s dir -t rpm --force \
|
|
||||||
--rpm-use-file-permissions \
|
|
||||||
--name freedom-loader \
|
|
||||||
--version "$VERSION" \
|
|
||||||
--architecture x86_64 \
|
|
||||||
--description "Free and open-source GUI for yt-dlp" \
|
|
||||||
--url "https://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.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 into a source RPM for COPR submission
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
echo "Building SRPM for COPR..."
|
|
||||||
mkdir -p "$ROOT_DIR/srpm-out"
|
|
||||||
|
|
||||||
TMP_DIR="$ROOT_DIR/.copr-build-tmp"
|
|
||||||
mkdir -p "$TMP_DIR"
|
|
||||||
TMP_SPEC="$TMP_DIR/freedom-loader.spec"
|
|
||||||
sed "s/__VERSION__/$VERSION/g" "$ROOT_DIR/package/freedom-loader.spec" > "$TMP_SPEC"
|
|
||||||
|
|
||||||
rpmbuild -bs "$TMP_SPEC" \
|
|
||||||
--define "_sourcedir $ROOT_DIR/dist" \
|
|
||||||
--define "_srcrpmdir $ROOT_DIR/srpm-out"
|
|
||||||
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
echo "SRPM built: $ROOT_DIR/srpm-out/freedom-loader-$VERSION-1*.src.rpm"
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
#!/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,7 +2,6 @@ const { fetchInfo } = require("../services/info.services");
|
|||||||
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
|
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
const { isValidUrl } = require("../helpers/validation.helpers");
|
const { isValidUrl } = require("../helpers/validation.helpers");
|
||||||
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles metadata retrieval for a video or playlist.
|
* Handles metadata retrieval for a video or playlist.
|
||||||
@@ -19,9 +18,7 @@ const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
|||||||
*/
|
*/
|
||||||
async function infoController(req, res) {
|
async function infoController(req, res) {
|
||||||
|
|
||||||
// Previously, i support both POST and GET element, but it's kinda weird. So now it only support GET.
|
const url = req.body.url || req.query.url; // Supports POST and GET requests
|
||||||
const url = req.query.url;
|
|
||||||
const encodedUrl = encodeURIComponent(url)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic validation:
|
* Basic validation:
|
||||||
@@ -33,11 +30,10 @@ async function infoController(req, res) {
|
|||||||
return res.status(400).send("Invalid URL Or Missing");
|
return res.status(400).send("Invalid URL Or Missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Info request received. RAW URL: ${url}`);
|
logger.info(`Info request received. URL: ${url}`);
|
||||||
logger.info(`Info request received. ENCODED: ${encodedUrl}`);
|
|
||||||
|
|
||||||
// Lightweight heuristic to detect playlist URLs. It works for Youtube.
|
// Lightweight heuristic to detect playlist URLs
|
||||||
const isPlaylistUrl = isUrlPlaylist(url);
|
const isPlaylistUrl = url.includes("&list") || url.includes("?list");
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
isPlaylistUrl
|
isPlaylistUrl
|
||||||
@@ -77,11 +73,11 @@ async function infoController(req, res) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Preserve original error context
|
// Preserve original error context
|
||||||
logger.error(err);
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
logger.error(err);
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,16 +86,6 @@ async function infoController(req, res) {
|
|||||||
*/
|
*/
|
||||||
if (data._type === "playlist") {
|
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);
|
const playlist = parsePlaylist(data);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const {getUserBrowser} = require("./getBrowser.helpers.js");
|
const getUserBrowser = require("./getBrowser.helpers.js");
|
||||||
const { ffmpegPath, denoPath} = require("./path.helpers.js");
|
const { ffmpegPath, denoPath} = require("./path.helpers.js");
|
||||||
const { configFeatures } = require("../../config.js");
|
const { configFeatures } = require("../../config.js");
|
||||||
const { logger } = require("../logger.js");
|
const { logger } = require("../logger.js");
|
||||||
const config = require("../../config")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates whether the provided codec is supported by the application.
|
* Validates whether the provided codec is supported by the application.
|
||||||
@@ -63,7 +62,7 @@ function validateCodec(codec){
|
|||||||
*
|
*
|
||||||
* @returns {string[]} Array of arguments ready to be passed to yt-dlp.
|
* @returns {string[]} Array of arguments ready to be passed to yt-dlp.
|
||||||
*/
|
*/
|
||||||
function buildYtDlpArgs({url, audioOnly, quality, outputFolder, isPlaylist}) {
|
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||||
|
|
||||||
logger.info("--- CONFIGURATION ---");
|
logger.info("--- CONFIGURATION ---");
|
||||||
|
|
||||||
@@ -71,7 +70,6 @@ function buildYtDlpArgs({url, audioOnly, quality, outputFolder, isPlaylist}) {
|
|||||||
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
||||||
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
||||||
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
||||||
logger.info(`CONFIG keepPlaylistOrder: ${configFeatures.keepPlaylistOrder}`);
|
|
||||||
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
||||||
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
||||||
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
||||||
@@ -119,27 +117,7 @@ function buildYtDlpArgs({url, audioOnly, quality, outputFolder, isPlaylist}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
args.push("-f", qualityMap[quality] || "best");
|
args.push("-f", qualityMap[quality] || "best");
|
||||||
|
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
|
||||||
/**
|
|
||||||
* Default title output template
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
let outputTemplate = "%(title)s.%(ext)s";
|
|
||||||
|
|
||||||
// If it's a playlist, and we want to keep the playlist order. Add an index before the title.
|
|
||||||
if (isPlaylist && configFeatures.keepPlaylistOrder && configFeatures.autoDownloadPlaylist) {
|
|
||||||
outputTemplate = "%(playlist_index)02d - %(title)s.%(ext)s";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a custom signature in each file downloaded with the app. It's a small things but i found it cool :)
|
|
||||||
if (configFeatures.addMetadata) {
|
|
||||||
args.push(
|
|
||||||
"--postprocessor-args",
|
|
||||||
`ffmpeg:-metadata comment="Downloaded with Freedom Loader ${config.version}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push("-o", path.join(outputFolder, outputTemplate));
|
|
||||||
args.push(url);
|
args.push(url);
|
||||||
|
|
||||||
return args.filter(Boolean);
|
return args.filter(Boolean);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const notify = require("./notify.helpers");
|
const path = require("path");
|
||||||
|
const notify = require("./notify.helpers")
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,43 +22,41 @@ const { logger } = require("../logger");
|
|||||||
* @returns {string} The detected browser identifier.
|
* @returns {string} The detected browser identifier.
|
||||||
*/
|
*/
|
||||||
function getUserBrowser() {
|
function getUserBrowser() {
|
||||||
const platform = process.platform;
|
const userProfile = os.homedir();
|
||||||
let isFirefoxHere = false;
|
|
||||||
|
|
||||||
if (platform === "win32") {
|
/**
|
||||||
const paths = [
|
* Browsers supported by yt-dlp cookie extraction.
|
||||||
"C:\\Program Files\\Mozilla Firefox\\firefox.exe",
|
*
|
||||||
"C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe",
|
* Only Firefox is currently enabled and tested.
|
||||||
`${process.env.LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`,
|
* Additional browsers can be enabled in the future
|
||||||
];
|
* once compatibility has been verified.
|
||||||
|
*/
|
||||||
|
const browsers = [
|
||||||
|
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
||||||
|
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
||||||
|
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
|
||||||
|
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
|
||||||
|
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
|
||||||
|
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
|
||||||
|
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
|
||||||
|
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
|
||||||
|
];
|
||||||
|
|
||||||
isFirefoxHere = paths.some(p => fs.existsSync(p));
|
// Search for the first available browser profile.
|
||||||
|
for (const browser of browsers) {
|
||||||
|
if (fs.existsSync(browser.path)) {
|
||||||
|
logger.info(`Browser found: ${browser.name}`);
|
||||||
|
return browser.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform === "linux") {
|
// No supported browser found => Notify User
|
||||||
const paths = [
|
logger.warn("No supported browser found on the system");
|
||||||
"/usr/bin/firefox",
|
notify.notifyFirefoxBrowserMissing();
|
||||||
"/usr/bin/firefox-esr",
|
|
||||||
"/usr/local/bin/firefox",
|
|
||||||
"/snap/bin/firefox",
|
|
||||||
"/var/lib/flatpak/exports/bin/org.mozilla.firefox",
|
|
||||||
`${os.homedir()}/.local/share/flatpak/exports/bin/org.mozilla.firefox`,
|
|
||||||
];
|
|
||||||
isFirefoxHere = paths.some(p => fs.existsSync(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFirefoxHere) {
|
|
||||||
// If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad
|
|
||||||
// No supported browser found => Notify User
|
|
||||||
logger.warn("No supported browser found on the system");
|
|
||||||
notify.notifyFirefoxBrowserMissing();
|
|
||||||
} else {
|
|
||||||
logger.info("Browser found: firefox");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to Firefox and let yt-dlp handle the error gracefully.
|
// Fallback to Firefox and let yt-dlp handle the error gracefully.
|
||||||
// This prevents the application from crashing
|
// This prevents the application from crashing
|
||||||
return "firefox";
|
return "firefox";
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {getUserBrowser};
|
module.exports = getUserBrowser
|
||||||
@@ -2,12 +2,6 @@ const { Notification, shell } = require("electron");
|
|||||||
const { iconPaths } = require("./path.helpers");
|
const { iconPaths } = require("./path.helpers");
|
||||||
const { logger } = require("../logger");
|
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.
|
* Displays a system notification when a download completes successfully.
|
||||||
*
|
*
|
||||||
@@ -27,27 +21,7 @@ function notifyDownloadFinished(folder, notifyEnabled = true) {
|
|||||||
icon: iconPaths.confirm,
|
icon: iconPaths.confirm,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Protect notification to garbage collection and race condition, by adding a 150ms timeout before trying to open the folder
|
notif.on("click", () => shell.openPath(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();
|
notif.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,16 +41,11 @@ function notifyCookiesBrowserError(){
|
|||||||
icon: iconPaths.error,
|
icon: iconPaths.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
activeNotifications.add(notif);
|
notif.on("click", () =>
|
||||||
|
shell.openExternal(
|
||||||
notif.on("click", () => {
|
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||||
shell.openExternal("https://www.firefox.com/en-US/download/");
|
)
|
||||||
activeNotifications.delete(notif);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
notif.on("close", () => {
|
|
||||||
activeNotifications.delete(notif);
|
|
||||||
});
|
|
||||||
|
|
||||||
notif.show();
|
notif.show();
|
||||||
}
|
}
|
||||||
@@ -96,16 +65,11 @@ function notifyFirefoxBrowserMissing() {
|
|||||||
icon: iconPaths.error,
|
icon: iconPaths.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
activeNotifications.add(notif);
|
notif.on("click", () =>
|
||||||
|
shell.openExternal(
|
||||||
notif.on("click", () => {
|
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||||
shell.openExternal("https://www.firefox.com/en-US/download/");
|
)
|
||||||
activeNotifications.delete(notif);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
notif.on("close", () => {
|
|
||||||
activeNotifications.delete(notif);
|
|
||||||
});
|
|
||||||
|
|
||||||
notif.show();
|
notif.show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,18 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
const { app } = require("electron");
|
const { app } = require("electron");
|
||||||
const config = require("../../config.js");
|
const config = require("../../config.js");
|
||||||
|
|
||||||
/**
|
const { logger } = require("../logger.js");
|
||||||
* Is the OS windows
|
|
||||||
*/
|
|
||||||
const isWindows = process.platform === 'win32'
|
|
||||||
|
|
||||||
/**
|
// Centralized resource paths
|
||||||
* On windows the binaries are name.exe on Linux/macOS no
|
|
||||||
*/
|
|
||||||
const ext = isWindows ? '.exe' : ''
|
|
||||||
|
|
||||||
/** Centralized resource paths */
|
|
||||||
const resourcesPath = config.devMode
|
const resourcesPath = config.devMode
|
||||||
? path.join(__dirname, "../../resources")
|
? path.join(__dirname, "../../ressources")
|
||||||
: process.resourcesPath;
|
: process.resourcesPath;
|
||||||
|
|
||||||
/** Default download folder (centralized) */
|
// Default download folder (centralized)
|
||||||
const defaultDownloadFolder = path.join(app.getPath("downloads"), "Freedom Loader");
|
const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||||
|
|
||||||
/**
|
|
||||||
* Current platform:
|
|
||||||
* win32 | linux | darwin
|
|
||||||
*/
|
|
||||||
const platform = process.platform;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dev source tree uses platform-named subfolders (matches resources/binaries/{win-32,linux,darwin}).
|
|
||||||
* Packaged builds flatten everything directly under binaries/, since each
|
|
||||||
* platform build only ships its own binaries — no subfolder needed.
|
|
||||||
*/
|
|
||||||
const devPlatformFolder = {
|
|
||||||
win32: "win-32",
|
|
||||||
linux: "linux",
|
|
||||||
darwin: "darwin"
|
|
||||||
}[platform];
|
|
||||||
|
|
||||||
const binariesPath = config.devMode
|
|
||||||
? path.join(resourcesPath, "binaries", devPlatformFolder)
|
|
||||||
: path.join(resourcesPath, "binaries");
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime-resolved binary paths.
|
* Runtime-resolved binary paths.
|
||||||
@@ -53,10 +24,7 @@ let ffmpegPath;
|
|||||||
let denoPath;
|
let denoPath;
|
||||||
|
|
||||||
/** Source binary bundled inside application resources (used for first-time copy) */
|
/** Source binary bundled inside application resources (used for first-time copy) */
|
||||||
const sourceYtDlp = path.join(
|
const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe");
|
||||||
binariesPath,
|
|
||||||
`yt-dlp${ext}`
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve binary locations depending on environment:
|
* Resolve binary locations depending on environment:
|
||||||
@@ -64,47 +32,16 @@ const sourceYtDlp = path.join(
|
|||||||
* - prod: extracted userData + packaged resources
|
* - prod: extracted userData + packaged resources
|
||||||
*/
|
*/
|
||||||
if (config.devMode) {
|
if (config.devMode) {
|
||||||
userYtDlp = path.join(
|
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
|
||||||
binariesPath,
|
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe
|
||||||
`yt-dlp${ext}`
|
denoPath = path.join(__dirname, "../../ressources/deno.exe");
|
||||||
);
|
|
||||||
|
|
||||||
ffmpegPath = path.join(
|
|
||||||
binariesPath,
|
|
||||||
`ffmpeg${ext}`
|
|
||||||
);
|
|
||||||
|
|
||||||
denoPath = path.join(
|
|
||||||
binariesPath,
|
|
||||||
`deno${ext}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
userYtDlp = path.join(
|
userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe");
|
||||||
app.getPath("userData"),
|
ffmpegPath = path.join(resourcesPath, "binaries","ffmpeg.exe");
|
||||||
`yt-dlp${ext}`
|
denoPath = path.join(resourcesPath, "binaries","deno.exe");
|
||||||
);
|
|
||||||
|
|
||||||
ffmpegPath = path.join(
|
if (!fs.existsSync(userYtDlp)) {
|
||||||
binariesPath,
|
|
||||||
`ffmpeg${ext}`
|
|
||||||
);
|
|
||||||
|
|
||||||
denoPath = path.join(
|
|
||||||
binariesPath,
|
|
||||||
`deno${ext}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fs.existsSync(userYtDlp) && fs.existsSync(sourceYtDlp)) {
|
|
||||||
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||||
|
|
||||||
// Linux/macOS executable permission
|
|
||||||
if (!isWindows) {
|
|
||||||
try {
|
|
||||||
fs.chmodSync(userYtDlp, 0o755);
|
|
||||||
} catch (err) {
|
|
||||||
getLogger().warn(`Failed to chmod yt-dlp: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,25 +60,10 @@ const iconPaths = {
|
|||||||
* Used mainly for verification and diagnostics.
|
* Used mainly for verification and diagnostics.
|
||||||
*/
|
*/
|
||||||
const binaryPaths = {
|
const binaryPaths = {
|
||||||
ytDlp: path.join(
|
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
|
||||||
binariesPath,
|
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
|
||||||
`yt-dlp${ext}`
|
ffprobe: path.join(resourcesPath, "binaries", "ffprobe.exe"),
|
||||||
),
|
deno: path.join(resourcesPath, "binaries", "deno.exe")
|
||||||
|
|
||||||
ffmpeg: path.join(
|
|
||||||
binariesPath,
|
|
||||||
`ffmpeg${ext}`
|
|
||||||
),
|
|
||||||
|
|
||||||
ffprobe: path.join(
|
|
||||||
binariesPath,
|
|
||||||
`ffprobe${ext}`
|
|
||||||
),
|
|
||||||
|
|
||||||
deno: path.join(
|
|
||||||
binariesPath,
|
|
||||||
`deno${ext}`
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,7 +72,7 @@ const binaryPaths = {
|
|||||||
* - user themes are persisted in userData to survive updates
|
* - user themes are persisted in userData to survive updates
|
||||||
*/
|
*/
|
||||||
const userThemesPath = path.join(app.getPath("userData"), "themes");
|
const userThemesPath = path.join(app.getPath("userData"), "themes");
|
||||||
const defaultThemesSourcePath = path.join("theme");
|
const defaultThemesSourcePath = path.join(resourcesPath, "theme");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes user theme directory and ensures default themes exist.
|
* Initializes user theme directory and ensures default themes exist.
|
||||||
@@ -163,48 +85,42 @@ function initUserThemes() {
|
|||||||
try {
|
try {
|
||||||
if (!fs.existsSync(userThemesPath)) {
|
if (!fs.existsSync(userThemesPath)) {
|
||||||
fs.mkdirSync(userThemesPath, { recursive: true });
|
fs.mkdirSync(userThemesPath, { recursive: true });
|
||||||
|
// Ensure folder is writable
|
||||||
fs.chmodSync(userThemesPath, 0o777);
|
fs.chmodSync(userThemesPath, 0o777);
|
||||||
getLogger().info("Created user themes directory");
|
logger.info("Created user themes directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy default themes from resources to user directory if they don't exist
|
||||||
if (fs.existsSync(defaultThemesSourcePath)) {
|
if (fs.existsSync(defaultThemesSourcePath)) {
|
||||||
const defaultThemes = fs.readdirSync(defaultThemesSourcePath);
|
const defaultThemes = fs.readdirSync(defaultThemesSourcePath);
|
||||||
for (const theme of defaultThemes) {
|
for (const theme of defaultThemes) {
|
||||||
const srcPath = path.join(defaultThemesSourcePath, theme);
|
const srcPath = path.join(defaultThemesSourcePath, theme);
|
||||||
const destPath = path.join(userThemesPath, theme);
|
const destPath = path.join(userThemesPath, theme);
|
||||||
const srcIsFile = !fs.statSync(srcPath).isDirectory();
|
|
||||||
|
|
||||||
if (fs.existsSync(destPath)) {
|
if (!fs.existsSync(destPath)) {
|
||||||
const destIsDir = fs.statSync(destPath).isDirectory();
|
try {
|
||||||
if (srcIsFile && destIsDir) {
|
|
||||||
getLogger().warn(`Corrupted theme entry: ${theme}, fixing...`);
|
|
||||||
fs.rmSync(destPath, { recursive: true, force: true });
|
|
||||||
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (srcIsFile) {
|
|
||||||
fs.copyFileSync(srcPath, destPath);
|
|
||||||
fs.chmodSync(destPath, 0o666);
|
|
||||||
} else {
|
|
||||||
copyDirectory(srcPath, destPath);
|
copyDirectory(srcPath, destPath);
|
||||||
fs.chmodSync(destPath, 0o777);
|
fs.chmodSync(destPath, 0o777);
|
||||||
|
logger.info(`Copied default theme: ${theme}`);
|
||||||
|
} catch (copyErr) {
|
||||||
|
logger.warn(`Failed to copy theme ${theme}: ${copyErr.message}. Theme folder will be created, add theme files manually.`);
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(destPath, { recursive: true });
|
||||||
|
fs.chmodSync(destPath, 0o777);
|
||||||
|
} catch (mkdirErr) {
|
||||||
|
logger.warn(`Could not create theme directory ${theme}: ${mkdirErr.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
getLogger().info(`Copied default theme: ${theme}`);
|
|
||||||
} catch (copyErr) {
|
|
||||||
getLogger().warn(`Failed to copy theme ${theme}: ${copyErr.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getLogger().warn(`Default themes source path not found: ${defaultThemesSourcePath}`);
|
logger.warn(`Default themes source path not found: ${defaultThemesSourcePath}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
getLogger().error(`Failed to initialize user themes: ${err.message}`);
|
logger.error(`Failed to initialize user themes: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively copies a directory and its contents.
|
* Recursively copies a directory and its contents.
|
||||||
*
|
*
|
||||||
@@ -215,14 +131,10 @@ function initUserThemes() {
|
|||||||
function copyDirectory(src, dest) {
|
function copyDirectory(src, dest) {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(dest)) {
|
if (!fs.existsSync(dest)) {
|
||||||
fs.mkdirSync(dest, {
|
fs.mkdirSync(dest, { recursive: true, mode: 0o777 });
|
||||||
recursive: true,
|
|
||||||
mode: 0o777
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(src);
|
const files = fs.readdirSync(src);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const srcFile = path.join(src, file);
|
const srcFile = path.join(src, file);
|
||||||
const destFile = path.join(dest, file);
|
const destFile = path.join(dest, file);
|
||||||
@@ -239,31 +151,8 @@ function copyDirectory(src, dest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Runtime validation of critical binaries
|
// Runtime validation of critical binaries
|
||||||
// if (!userYtDlp) {
|
if (!userYtDlp){ logger.error("Missing yt-dlp binary")}
|
||||||
// getLogger().error("Missing yt-dlp binary");
|
if (!ffmpegPath){ logger.error("Missing ffmpeg binary")}
|
||||||
// }
|
if (!denoPath){ logger.error("Missing deno binary")}
|
||||||
|
|
||||||
// if (!ffmpegPath) {
|
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes };
|
||||||
// getLogger().error("Missing ffmpeg binary");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!denoPath) {
|
|
||||||
// getLogger().error("Missing deno binary");
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
function validateBinaries() {
|
|
||||||
if (!userYtDlp) getLogger().error("Missing yt-dlp binary");
|
|
||||||
if (!ffmpegPath) getLogger().error("Missing ffmpeg binary");
|
|
||||||
if (!denoPath) getLogger().error("Missing deno binary");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to lazy load logger, avoid circular import
|
|
||||||
* @returns {import("winston").Logger}
|
|
||||||
*/
|
|
||||||
function getLogger() {
|
|
||||||
return require("../logger.js").logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes, isWindows, validateBinaries };
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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("@");
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,9 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
const {isWindows} = require("./helpers/path.helpers");
|
|
||||||
const { logSystemInfo } = require("./sysinfo");
|
|
||||||
|
|
||||||
// Logs folder in Windows
|
// Logs folder in Windows
|
||||||
const logDir = isWindows ? path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs") : path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"), "FreedomLoader", "logs");
|
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||||
|
|
||||||
// Create "Logs" folder if needed
|
// Create "Logs" folder if needed
|
||||||
try {
|
try {
|
||||||
@@ -17,29 +15,9 @@ try {
|
|||||||
console.error(`Failed to create log directory: ${error.message}`);
|
console.error(`Failed to create log directory: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const logFormat = format.combine(
|
||||||
* Format used by the saved file
|
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
* @type {Format}
|
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||||
*/
|
|
||||||
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}`;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,6 +31,7 @@ const consoleFormat = format.combine(
|
|||||||
*/
|
*/
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
level: "info",
|
level: "info",
|
||||||
|
format: logFormat,
|
||||||
transports: [
|
transports: [
|
||||||
new DailyRotateFile({
|
new DailyRotateFile({
|
||||||
dirname: logDir,
|
dirname: logDir,
|
||||||
@@ -60,11 +39,11 @@ const logger = createLogger({
|
|||||||
datePattern: "YYYY-MM-DD",
|
datePattern: "YYYY-MM-DD",
|
||||||
zippedArchive: false,
|
zippedArchive: false,
|
||||||
maxFiles: "7d",
|
maxFiles: "7d",
|
||||||
format: fileFormat,
|
format: logFormat,
|
||||||
options: { flags: "a" },
|
options: { flags: "a" },
|
||||||
}),
|
}),
|
||||||
new transports.Console({
|
new transports.Console({
|
||||||
format: consoleFormat,
|
format: logFormat,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -72,18 +51,16 @@ const logger = createLogger({
|
|||||||
/**
|
/**
|
||||||
* Start Log Session
|
* Start Log Session
|
||||||
*/
|
*/
|
||||||
function logSessionStart(logDir, downloadPath) {
|
function logSessionStart() {
|
||||||
logger.info(`--- Starting session: ${new Date().toLocaleString('sv-SE')} ---`);
|
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
|
||||||
logger.info("============================================================")
|
|
||||||
logger.info(`Application Version: ${config.version}`)
|
logger.info(`Application Version: ${config.version}`)
|
||||||
if (typeof logSystemInfo === 'function') logSystemInfo(logger, logDir, downloadPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop Log Session
|
* Stop Log Session
|
||||||
*/
|
*/
|
||||||
function logSessionEnd() {
|
function logSessionEnd() {
|
||||||
logger.info(`--- Ending session: ${new Date().toLocaleString('sv-SE')} ---`);
|
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ const express = require("express");
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { infoController } = require("../controller/info.controller");
|
const { infoController } = require("../controller/info.controller");
|
||||||
|
|
||||||
router.get("/", infoController);
|
router.post("/", infoController);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { logger, logSessionEnd } = require("./logger");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
|
|
||||||
const {logger} = require("./logger");
|
|
||||||
const { rateLimit } = require("./helpers/rateLimit.helpers");
|
const { rateLimit } = require("./helpers/rateLimit.helpers");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the state of the server
|
|
||||||
*/
|
|
||||||
let serverInstance = null;
|
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
@@ -19,9 +13,7 @@ app.use(express.static(path.join(__dirname, "../public")));
|
|||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use("/download", require("./routes/download.route"));
|
app.use("/download", require("./routes/download.route"));
|
||||||
app.use("/info", require("./routes/info.route"));
|
app.use("/info", require("./routes/info.route"));
|
||||||
|
|
||||||
// Interface
|
|
||||||
app.get("/", rateLimit, (req, res) => {
|
app.get("/", rateLimit, (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||||
});
|
});
|
||||||
@@ -36,32 +28,33 @@ app.get("/", rateLimit, (req, res) => {
|
|||||||
*/
|
*/
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const serverInstance = app.listen(config.applicationPort, () => {
|
const server = app.listen(config.applicationPort, () => {
|
||||||
logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
|
logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
|
||||||
resolve(serverInstance);
|
resolve(server);
|
||||||
});
|
});
|
||||||
|
|
||||||
serverInstance.on("error", (err) => {
|
server.on("error", (err) => {
|
||||||
logger.error("Express server error:", err);
|
logger.error("Express server error:", err);
|
||||||
reject(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,8 +6,7 @@ const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
|
|||||||
const notify = require("../helpers/notify.helpers");
|
const notify = require("../helpers/notify.helpers");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { isSafePath } = require("../helpers/validation.helpers");
|
const { isSafePath } = require("../helpers/validation.helpers");
|
||||||
const {reloadFeatures} = require("../../config");
|
const { configFeatures } = require("../../config.js");
|
||||||
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to the currently running yt-dlp process.
|
* Reference to the currently running yt-dlp process.
|
||||||
@@ -19,6 +18,19 @@ const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
|||||||
*/
|
*/
|
||||||
let currentDownloadProcess = null;
|
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.
|
* Creates a dedicated download folder for a playlist.
|
||||||
*
|
*
|
||||||
@@ -37,8 +49,8 @@ let currentDownloadProcess = null;
|
|||||||
*/
|
*/
|
||||||
function createPlaylistFolder(basePath, playlistTitle) {
|
function createPlaylistFolder(basePath, playlistTitle) {
|
||||||
const sanitizedTitle = playlistTitle
|
const sanitizedTitle = playlistTitle
|
||||||
.replace(/[<>:"|?*]/g, '')
|
.replace(/[<>:"|?*]/g, '')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.trim()
|
.trim()
|
||||||
.substring(0, 200);
|
.substring(0, 200);
|
||||||
|
|
||||||
@@ -65,7 +77,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.error(`Could not find available playlist folder after 1000 attempts`);
|
logger.error(`Could not find available playlist folder after 1000 attempts`);
|
||||||
logger.error("Unable to create playlist folder");
|
throw new Error("Unable to create playlist folder");
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
|
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
|
||||||
@@ -111,7 +123,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
|||||||
* @param {string} [options.outputFolder] - Destination folder.
|
* @param {string} [options.outputFolder] - Destination folder.
|
||||||
* @param {string} [options.playlistTitle] - Playlist title.
|
* @param {string} [options.playlistTitle] - Playlist title.
|
||||||
*
|
*
|
||||||
* @param {Function[]} progressListeners - Progress listeners.
|
* @param {Function[]} listeners - Progress listeners.
|
||||||
* @param {Function[]} speedListeners - Download speed listeners.
|
* @param {Function[]} speedListeners - Download speed listeners.
|
||||||
* @param {Function[]} stageListeners - Processing stage listeners.
|
* @param {Function[]} stageListeners - Processing stage listeners.
|
||||||
* @param {Function[]} playlistInfoListeners - Playlist progress listeners.
|
* @param {Function[]} playlistInfoListeners - Playlist progress listeners.
|
||||||
@@ -120,13 +132,10 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
|||||||
* Resolves with the final output folder path once the
|
* Resolves with the final output folder path once the
|
||||||
* download has completed successfully.
|
* download has completed successfully.
|
||||||
*/
|
*/
|
||||||
function fetchDownload(options, progressListeners, speedListeners, stageListeners, playlistInfoListeners) {
|
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
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;
|
let outputFolder = options.outputFolder || defaultDownloadFolder;
|
||||||
|
|
||||||
@@ -148,9 +157,9 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Détecte si c'est une playlist et crée un dossier approprié
|
// Détecte si c'est une playlist et crée un dossier approprié
|
||||||
const isPlaylist = options.playlistTitle || isUrlPlaylist(options.url);
|
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url);
|
||||||
|
|
||||||
if (isPlaylist && userConfig.createPlaylistFolders) {
|
if (isPlaylist && configFeatures.createPlaylistFolders) {
|
||||||
try {
|
try {
|
||||||
const playlistName = options.playlistTitle || "Untitled Playlist";
|
const playlistName = options.playlistTitle || "Untitled Playlist";
|
||||||
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
|
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
|
||||||
@@ -159,16 +168,11 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
|
|||||||
logger.error(`Failed to create playlist folder: ${err.message}`);
|
logger.error(`Failed to create playlist folder: ${err.message}`);
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
} else if (isPlaylist && !userConfig.createPlaylistFolders) {
|
} else if (isPlaylist && !configFeatures.createPlaylistFolders) {
|
||||||
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
|
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = buildYtDlpArgs({
|
const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder });
|
||||||
...options,
|
|
||||||
outputFolder: safeOutputFolder,
|
|
||||||
isPlaylist
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||||
|
|
||||||
const child = execFile(userYtDlp, args);
|
const child = execFile(userYtDlp, args);
|
||||||
@@ -178,7 +182,7 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
|
|||||||
|
|
||||||
child.on("close", code => {
|
child.on("close", code => {
|
||||||
currentDownloadProcess = null;
|
currentDownloadProcess = null;
|
||||||
progressListeners.forEach(fn => fn("done"));
|
listeners.forEach(fn => fn("done"));
|
||||||
if (code === 0) resolve(safeOutputFolder);
|
if (code === 0) resolve(safeOutputFolder);
|
||||||
else if (code === null) reject(new Error(`Download cancelled by user`));
|
else if (code === null) reject(new Error(`Download cancelled by user`));
|
||||||
else reject(new Error(`YT-DLP failed with code : ${code}`));
|
else reject(new Error(`YT-DLP failed with code : ${code}`));
|
||||||
@@ -192,11 +196,11 @@ function fetchDownload(options, progressListeners, speedListeners, stageListener
|
|||||||
|
|
||||||
// Progress Bar
|
// Progress Bar
|
||||||
if (line.startsWith("[download] Destination:")) {
|
if (line.startsWith("[download] Destination:")) {
|
||||||
progressListeners.forEach(fn => fn("reset"));
|
listeners.forEach(fn => fn("reset"));
|
||||||
stageListeners.forEach(fn => fn("Downloading..."));
|
stageListeners.forEach(fn => fn("Downloading..."));
|
||||||
}
|
}
|
||||||
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
||||||
if (match) progressListeners.forEach(fn => fn(parseFloat(match[1])));
|
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
|
||||||
|
|
||||||
if (line.includes("MiB/s") || line.includes("KiB/s")) {
|
if (line.includes("MiB/s") || line.includes("KiB/s")) {
|
||||||
const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/);
|
const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { execFile } = require("child_process");
|
const { execFile } = require("child_process");
|
||||||
const { userYtDlp, denoPath } = require("../helpers/path.helpers");
|
const { userYtDlp, denoPath } = require("../helpers/path.helpers");
|
||||||
const {getUserBrowser} = require("../helpers/getBrowser.helpers");
|
const getUserBrowser = require("../helpers/getBrowser.helpers");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,7 +61,6 @@ function fetchInfo(url) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(stdout);
|
const data = JSON.parse(stdout);
|
||||||
|
|
||||||
resolve(data);
|
resolve(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`JSON Parsing Error: ${e.message}`);
|
logger.error(`JSON Parsing Error: ${e.message}`);
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
const os = require("os");
|
|
||||||
const fs = require("fs");
|
|
||||||
const { execSync } = require("child_process");
|
|
||||||
const { app } = require("electron");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads /etc/os-release to get the Linux distribution name
|
|
||||||
*/
|
|
||||||
function getLinuxDistro() {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync("/etc/os-release", "utf-8");
|
|
||||||
const pretty = content.match(/^PRETTY_NAME="?(.+?)"?$/m);
|
|
||||||
if (pretty) return pretty[1];
|
|
||||||
const name = content.match(/^NAME="?(.+?)"?$/m);
|
|
||||||
return name ? name[1] : "Unknown Linux";
|
|
||||||
} catch {
|
|
||||||
return "Unknown Linux";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the package-type file written by electron-builder
|
|
||||||
* to determine how the app was distributed (rpm, snap, AppImage, deb...)
|
|
||||||
*/
|
|
||||||
function getPackageType() {
|
|
||||||
try {
|
|
||||||
const pkgTypePath = require("path").join(process.resourcesPath, "package-type");
|
|
||||||
if (fs.existsSync(pkgTypePath)) {
|
|
||||||
return fs.readFileSync(pkgTypePath, "utf-8").trim();
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if Firefox is installed on the current platform
|
|
||||||
*/
|
|
||||||
function detectFirefox() {
|
|
||||||
const platform = process.platform;
|
|
||||||
|
|
||||||
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`,
|
|
||||||
];
|
|
||||||
return 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`,
|
|
||||||
];
|
|
||||||
if (paths.some(p => fs.existsSync(p))) return true;
|
|
||||||
try { execSync("which firefox", { stdio: "ignore" }); return true; } catch { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the system info object logged at startup
|
|
||||||
*
|
|
||||||
* @param {string} logDir - path to the log directory
|
|
||||||
* @param {string} downloadPath - current configured download path
|
|
||||||
*/
|
|
||||||
function getSystemInfo(logDir, downloadPath) {
|
|
||||||
const platform = process.platform;
|
|
||||||
const cpus = os.cpus();
|
|
||||||
const totalMem = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1);
|
|
||||||
const freeMem = (os.freemem() / 1024 / 1024 / 1024).toFixed(1);
|
|
||||||
|
|
||||||
let platformLabel;
|
|
||||||
if (platform === "win32") {
|
|
||||||
platformLabel = `Windows (${os.release()})`;
|
|
||||||
} else if (platform === "linux") {
|
|
||||||
platformLabel = `Linux — ${getLinuxDistro()}`;
|
|
||||||
} else {
|
|
||||||
platformLabel = `${platform} ${os.release()} (unsupported)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = {
|
|
||||||
"Platform": platformLabel,
|
|
||||||
"Architecture": os.arch(),
|
|
||||||
"CPU": cpus.length ? `${cpus[0].model.trim()} (${cpus.length} cores)` : "Unknown",
|
|
||||||
"Memory": `${freeMem} GB free / ${totalMem} GB total`,
|
|
||||||
"Locale": app.getLocale(),
|
|
||||||
"Electron": process.versions.electron,
|
|
||||||
"Node": process.versions.node,
|
|
||||||
"Packaged": String(app.isPackaged),
|
|
||||||
"Package type": app.isPackaged ? getPackageType() : "dev",
|
|
||||||
"Firefox present": detectFirefox() ? "Yes" : "No — cookie features unavailable",
|
|
||||||
"Log directory": logDir,
|
|
||||||
"Download path": downloadPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Linux-specific extras
|
|
||||||
if (platform === "linux") {
|
|
||||||
const sessionType = process.env.XDG_SESSION_TYPE || "unknown";
|
|
||||||
const waylandDisplay = process.env.WAYLAND_DISPLAY;
|
|
||||||
const x11Display = process.env.DISPLAY;
|
|
||||||
|
|
||||||
info["Display server"] = waylandDisplay
|
|
||||||
? `Wayland (${waylandDisplay})`
|
|
||||||
: x11Display
|
|
||||||
? `X11 (${x11Display})`
|
|
||||||
: sessionType !== "unknown"
|
|
||||||
? sessionType
|
|
||||||
: "unknown";
|
|
||||||
|
|
||||||
info["Desktop env"] = process.env.XDG_CURRENT_DESKTOP || process.env.DESKTOP_SESSION || "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs system info at application startup
|
|
||||||
*
|
|
||||||
* @param {object} logger - winston logger instance
|
|
||||||
* @param {string} logDir - path to the log directory
|
|
||||||
* @param {string} downloadPath - current configured download path
|
|
||||||
*/
|
|
||||||
function logSystemInfo(logger, logDir, downloadPath) {
|
|
||||||
const info = getSystemInfo(logDir, downloadPath);
|
|
||||||
const maxKeyLen = Math.max(...Object.keys(info).map(k => k.length));
|
|
||||||
|
|
||||||
logger.info("=".repeat(60));
|
|
||||||
logger.info("SYSTEM INFO");
|
|
||||||
logger.info("=".repeat(60));
|
|
||||||
for (const [key, val] of Object.entries(info)) {
|
|
||||||
logger.info(` ${key.padEnd(maxKeyLen)} : ${val}`);
|
|
||||||
}
|
|
||||||
logger.info("=".repeat(60));
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { logSystemInfo };
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
jest.mock("../../server/helpers/getBrowser.helpers.js", () =>
|
|
||||||
jest.fn(() => "firefox")
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.mock("../../server/helpers/path.helpers.js", () => ({
|
|
||||||
ffmpegPath: "/mock/ffmpeg",
|
|
||||||
denoPath: "/mock/deno",
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("../../config.js", () => ({
|
|
||||||
configFeatures: {
|
|
||||||
autoUpdate: false,
|
|
||||||
discordRPC: false,
|
|
||||||
customTopBar: false,
|
|
||||||
autoCheckInfo: false,
|
|
||||||
addThumbnail: true,
|
|
||||||
addMetadata: true,
|
|
||||||
verboseLogs: false,
|
|
||||||
autoDownloadPlaylist: false,
|
|
||||||
customCodec: "h264",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("../../server/logger.js", () => ({
|
|
||||||
logger: {
|
|
||||||
info: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { buildYtDlpArgs } = require("../../server/helpers/buildArgs.helpers");
|
|
||||||
const { configFeatures } = require("../../config");
|
|
||||||
const { logger } = require("../../server/logger");
|
|
||||||
|
|
||||||
describe("buildYtDlpArgs", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("builds default video arguments", () => {
|
|
||||||
const args = buildYtDlpArgs({
|
|
||||||
url: "https://youtube.com/watch?v=123",
|
|
||||||
audioOnly: false,
|
|
||||||
quality: "best",
|
|
||||||
outputFolder: "/downloads",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(args).toContain("--cookies-from-browser");
|
|
||||||
expect(args).toContain("firefox");
|
|
||||||
|
|
||||||
expect(args).toContain("--ffmpeg-location");
|
|
||||||
expect(args).toContain("/mock/ffmpeg");
|
|
||||||
|
|
||||||
expect(args).toContain("--js-runtimes");
|
|
||||||
expect(args).toContain("deno:/mock/deno");
|
|
||||||
|
|
||||||
expect(args).toContain("--merge-output");
|
|
||||||
expect(args).toContain("mp4");
|
|
||||||
|
|
||||||
expect(args).toContain("-f");
|
|
||||||
expect(args).toContain("bestvideo+bestaudio/best/mp4");
|
|
||||||
|
|
||||||
expect(args).toContain(
|
|
||||||
path.join("/downloads", "%(title)s.%(ext)s")
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(args.at(-1)).toBe("https://youtube.com/watch?v=123");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("builds audio-only arguments", () => {
|
|
||||||
const args = buildYtDlpArgs({
|
|
||||||
url: "https://youtube.com/watch?v=123",
|
|
||||||
audioOnly: true,
|
|
||||||
quality: "best",
|
|
||||||
outputFolder: "/downloads",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(args).toContain("--extract-audio");
|
|
||||||
expect(args).toContain("--audio-format");
|
|
||||||
expect(args).toContain("mp3");
|
|
||||||
expect(args).toContain("bestaudio");
|
|
||||||
|
|
||||||
expect(args).not.toContain("--merge-output");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses requested quality", () => {
|
|
||||||
const args = buildYtDlpArgs({
|
|
||||||
url: "https://youtube.com/watch?v=123",
|
|
||||||
audioOnly: false,
|
|
||||||
quality: "720",
|
|
||||||
outputFolder: "/downloads",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(args).toContain(
|
|
||||||
"bestvideo[height<=720]+bestaudio/best[height<=720]/mp4"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("falls back to best for unknown quality", () => {
|
|
||||||
const args = buildYtDlpArgs({
|
|
||||||
url: "https://youtube.com/watch?v=123",
|
|
||||||
audioOnly: false,
|
|
||||||
quality: "unknown",
|
|
||||||
outputFolder: "/downloads",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(args).toContain("best");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses --no-playlist by default", () => {
|
|
||||||
const args = buildYtDlpArgs({
|
|
||||||
url: "https://youtube.com/watch?v=123",
|
|
||||||
audioOnly: false,
|
|
||||||
quality: "best",
|
|
||||||
outputFolder: "/downloads",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(args).toContain("--no-playlist");
|
|
||||||
expect(args).not.toContain("--yes-playlist");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses --yes-playlist when enabled", () => {
|
|
||||||
configFeatures.autoDownloadPlaylist = true;
|
|
||||||
|
|
||||||
const args = buildYtDlpArgs({
|
|
||||||
url: "https://youtube.com/watch?v=123",
|
|
||||||
audioOnly: false,
|
|
||||||
quality: "best",
|
|
||||||
outputFolder: "/downloads",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(args).toContain("--yes-playlist");
|
|
||||||
expect(args).not.toContain("--no-playlist");
|
|
||||||
|
|
||||||
configFeatures.autoDownloadPlaylist = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("embeds thumbnail and metadata when enabled", () => {
|
|
||||||
const args = buildYtDlpArgs({
|
|
||||||
url: "https://youtube.com/watch?v=123",
|
|
||||||
audioOnly: false,
|
|
||||||
quality: "best",
|
|
||||||
outputFolder: "/downloads",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(args).toContain("--embed-thumbnail");
|
|
||||||
expect(args).toContain("--add-metadata");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("logs codec validation", () => {
|
|
||||||
buildYtDlpArgs({
|
|
||||||
url: "https://youtube.com/watch?v=123",
|
|
||||||
audioOnly: false,
|
|
||||||
quality: "best",
|
|
||||||
outputFolder: "/downloads",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(logger.info).toHaveBeenCalledWith("Codec Valid: h264");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("falls back to h264 when codec is invalid", () => {
|
|
||||||
configFeatures.customCodec = "invalid";
|
|
||||||
|
|
||||||
const args = buildYtDlpArgs({
|
|
||||||
url: "https://youtube.com/watch?v=123",
|
|
||||||
audioOnly: false,
|
|
||||||
quality: "best",
|
|
||||||
outputFolder: "/downloads",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
|
||||||
"Codec not valid: invalid. Using default codec"
|
|
||||||
);
|
|
||||||
|
|
||||||
const sortIndex = args.indexOf("-S");
|
|
||||||
expect(args[sortIndex + 1]).toContain("vcodec:h264");
|
|
||||||
|
|
||||||
configFeatures.customCodec = "h264";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
const mockShow = jest.fn();
|
|
||||||
const mockOn = jest.fn();
|
|
||||||
const mockOpenPath = jest.fn();
|
|
||||||
const mockOpenExternal = jest.fn();
|
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
|
||||||
Notification: jest.fn().mockImplementation(() => ({
|
|
||||||
show: mockShow,
|
|
||||||
on: mockOn,
|
|
||||||
})),
|
|
||||||
shell: {
|
|
||||||
openPath: mockOpenPath,
|
|
||||||
openExternal: mockOpenExternal,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("../../server/helpers/path.helpers", () => ({
|
|
||||||
iconPaths: {
|
|
||||||
confirm: "/icons/confirm.png",
|
|
||||||
error: "/icons/error.png",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("../../server/logger", () => ({
|
|
||||||
logger: {
|
|
||||||
info: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { Notification, shell } = require("electron");
|
|
||||||
|
|
||||||
const {
|
|
||||||
notifyDownloadFinished,
|
|
||||||
notifyCookiesBrowserError,
|
|
||||||
notifyFirefoxBrowserMissing,
|
|
||||||
} = require("../../server/helpers/notify.helpers");
|
|
||||||
|
|
||||||
describe("notify.helpers", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("notifyDownloadFinished", () => {
|
|
||||||
test("does nothing when notifications are disabled", () => {
|
|
||||||
notifyDownloadFinished("/downloads", false);
|
|
||||||
|
|
||||||
expect(Notification).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does nothing when folder is missing", () => {
|
|
||||||
notifyDownloadFinished(undefined, true);
|
|
||||||
|
|
||||||
expect(Notification).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("creates and shows notification", () => {
|
|
||||||
notifyDownloadFinished("/downloads");
|
|
||||||
|
|
||||||
expect(Notification).toHaveBeenCalledWith({
|
|
||||||
title: "Freedom Loader",
|
|
||||||
body: "Your download is complete, click here to open it.",
|
|
||||||
icon: "/icons/confirm.png",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockOn).toHaveBeenCalledWith(
|
|
||||||
"click",
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockShow).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("opens folder when notification is clicked", () => {
|
|
||||||
notifyDownloadFinished("/downloads");
|
|
||||||
|
|
||||||
const callback = mockOn.mock.calls[0][1];
|
|
||||||
callback();
|
|
||||||
|
|
||||||
expect(shell.openPath).toHaveBeenCalledWith("/downloads");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("notifyCookiesBrowserError", () => {
|
|
||||||
test("creates notification", () => {
|
|
||||||
notifyCookiesBrowserError();
|
|
||||||
|
|
||||||
expect(Notification).toHaveBeenCalledWith({
|
|
||||||
title: "Cookies Error",
|
|
||||||
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
|
||||||
icon: "/icons/error.png",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockShow).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("opens tutorial when clicked", () => {
|
|
||||||
notifyCookiesBrowserError();
|
|
||||||
|
|
||||||
const callback = mockOn.mock.calls[0][1];
|
|
||||||
callback();
|
|
||||||
|
|
||||||
expect(shell.openExternal).toHaveBeenCalledWith(
|
|
||||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("notifyFirefoxBrowserMissing", () => {
|
|
||||||
test("creates notification", () => {
|
|
||||||
notifyFirefoxBrowserMissing();
|
|
||||||
|
|
||||||
expect(Notification).toHaveBeenCalledWith({
|
|
||||||
title: "Firefox Missing",
|
|
||||||
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
|
||||||
icon: "/icons/error.png",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockShow).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("opens installation guide when clicked", () => {
|
|
||||||
notifyFirefoxBrowserMissing();
|
|
||||||
|
|
||||||
const callback = mockOn.mock.calls[0][1];
|
|
||||||
callback();
|
|
||||||
|
|
||||||
expect(shell.openExternal).toHaveBeenCalledWith(
|
|
||||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
BIN
theme/dark.zip
BIN
theme/dark.zip
Binary file not shown.
BIN
theme/light.zip
BIN
theme/light.zip
Binary file not shown.
Reference in New Issue
Block a user