mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-09-28 04:21:15 +02:00
Compare commits
438 Commits
1993566040
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0bdd887e3 | ||
|
|
609ac2eadb | ||
|
|
c484d322bf | ||
|
|
b321a1d164 | ||
|
|
89e6fcf9bc | ||
|
|
ba2f317551 | ||
|
|
553e383c94 | ||
|
|
89cc0b85a3 | ||
|
|
abd0bf9006 | ||
|
|
653f73805e | ||
|
|
4f03c5876f | ||
|
|
a5f05f32af | ||
|
|
0d1b1e4919 | ||
|
|
0e77a94ec7 | ||
|
|
7f481d20b2 | ||
|
|
1ff32ca021 | ||
|
|
90d8a9f65d | ||
|
|
d5965c2ce0 | ||
|
|
15f59a6093 | ||
|
|
8b31436e25 | ||
|
|
7d5203001b | ||
|
|
60709c4fad | ||
|
|
2fb6509edc | ||
|
|
eebce83976 | ||
|
|
a56a070bc8 | ||
|
|
e1ef7032bb | ||
|
|
e7333b7b6b | ||
|
|
e2b51d32d1 | ||
|
|
81515c6ab8 | ||
|
|
c88189b44f | ||
|
|
b745ebd29f | ||
|
|
13ee4c170b | ||
|
|
0218664c5a | ||
|
|
6afc5e06d6 | ||
|
|
519dcad6e6 | ||
|
|
ac94d68224 | ||
|
|
0f876fcb70 | ||
|
|
a6c11ba7c4 | ||
|
|
44132dd7b5 | ||
|
|
15ab6892b7 | ||
|
|
725b6f0ada | ||
|
|
ad0e4f858b | ||
|
|
68108e3a66 | ||
|
|
eff2affd42 | ||
|
|
87cf05643f | ||
|
|
ce14b8abd7 | ||
|
|
44ed1bbf0e | ||
|
|
e0d017d9fd | ||
|
|
c3ea1ac0bd | ||
|
|
238d4ea717 | ||
|
|
d879a5eb1d | ||
|
|
40bea35286 | ||
|
|
83ac76381a | ||
|
|
6ff0c71be7 | ||
|
|
fd0b339bcc | ||
|
|
bc5faf38b6 | ||
|
|
e744e55475 | ||
|
|
a581c9309d | ||
|
|
61040769ee | ||
|
|
06f6a30797 | ||
|
|
a2653eef57 | ||
|
|
89573d11f7 | ||
|
|
f0c28a7f5e | ||
|
|
f3c37c2498 | ||
|
|
8fe4e7c38a | ||
|
|
912f4042e5 | ||
|
|
5dc61b4a15 | ||
|
|
5fb49b0c9d | ||
|
|
75fdf91fcd | ||
|
|
011e0a703a | ||
|
|
5aebc78d44 | ||
|
|
60137a15dc | ||
|
|
d05b9e2511 | ||
|
|
ebb48f4f8b | ||
|
|
8be47b9821 | ||
|
|
ca8b107f61 | ||
|
|
f25d07cb83 | ||
|
|
49e1e1b507 | ||
|
|
23d11d8933 | ||
|
|
9f88fe19c3 | ||
|
|
9b4b0306c7 | ||
|
|
96401a6e1f | ||
|
|
0c7103c5c9 | ||
|
|
798084e3bf | ||
|
|
5c4e6669fe | ||
|
|
00c542925c | ||
|
|
24460cf0b5 | ||
|
|
a3c1b1d753 | ||
|
|
5ade3fe144 | ||
|
|
577069b471 | ||
|
|
a4da47647a | ||
|
|
c5b29a0647 | ||
|
|
90e1a5fde7 | ||
|
|
dcfea2073c | ||
|
|
0be1ef1710 | ||
|
|
da393b19d9 | ||
|
|
1c27cc19cf | ||
|
|
ff7366c46c | ||
|
|
459d4c6df4 | ||
|
|
d2ca3e99d2 | ||
|
|
8ad3ea55cc | ||
|
|
71dbd104f8 | ||
|
|
57a5defb08 | ||
|
|
25c78ff8b7 | ||
|
|
792a59d8d7 | ||
|
|
b13c98dda2 | ||
|
|
1f103c20b9 | ||
|
|
70d62959ca | ||
|
|
83ca963502 | ||
|
|
9fb9295093 | ||
|
|
74fcdbb738 | ||
|
|
32c9a66a35 | ||
|
|
45e0840fea | ||
|
|
19b3f8b947 | ||
|
|
cc5bde5b48 | ||
|
|
9a48ac05e3 | ||
|
|
5750444655 | ||
|
|
fdb22f2342 | ||
|
|
760550e521 | ||
|
|
334e83ae1f | ||
|
|
5d776594fc | ||
|
|
adb36e3fa2 | ||
|
|
87945fba80 | ||
|
|
2234f9ed81 | ||
|
|
7cd3b4be06 | ||
|
|
a42b020b96 | ||
|
|
c1e70dae80 | ||
|
|
6877c51c0e | ||
|
|
5a41090175 | ||
|
|
5983f21759 | ||
|
|
ee9e02410f | ||
|
|
909480083e | ||
|
|
ca0492ed04 | ||
|
|
6c73dd75be | ||
|
|
556dccec08 | ||
|
|
f720d7c889 | ||
|
|
bb23f37fb5 | ||
|
|
220e81d8b2 | ||
|
|
44d8772827 | ||
|
|
335fb9652b | ||
|
|
243b98cbf9 | ||
|
|
23da75a17b | ||
|
|
756d2c048e | ||
|
|
659c1d4cf9 | ||
|
|
443fd31170 | ||
|
|
6d8095ceb2 | ||
|
|
c18cb6c8aa | ||
|
|
a3c5025434 | ||
|
|
3d7986cb87 | ||
|
|
3f8701567f | ||
|
|
ccb16c49d5 | ||
|
|
86d9ac3592 | ||
|
|
43d87c2b47 | ||
|
|
19aa8ed8fa | ||
|
|
3ddfaa999e | ||
|
|
3d7f3b7481 | ||
|
|
cfe2e17313 | ||
|
|
dedf10c6b3 | ||
|
|
ac0b4a0750 | ||
|
|
29ca2ea496 | ||
|
|
2a3905bdfb | ||
|
|
d3755ab57d | ||
|
|
9824e107fe | ||
|
|
e437722937 | ||
|
|
fd8c4ae33c | ||
|
|
967144bd55 | ||
|
|
cd8eb05020 | ||
|
|
590060e504 | ||
|
|
26c7d4d2ee | ||
|
|
caa987fa42 | ||
|
|
ccb090e4b2 | ||
|
|
8d070ed10d | ||
|
|
343dbb26a2 | ||
|
|
fef4f6de36 | ||
|
|
1a61972750 | ||
|
|
45ae5d7f98 | ||
|
|
fecb123d0e | ||
|
|
5c62ed081a | ||
|
|
8f0f31041d | ||
|
|
0e48ab3b4c | ||
|
|
d4c33bcf2d | ||
|
|
2f105f5145 | ||
|
|
b7a193b062 | ||
|
|
65364d3095 | ||
|
|
c356e2e30d | ||
|
|
e45810b54c | ||
|
|
55db30febc | ||
|
|
163faec221 | ||
|
|
5398fed928 | ||
|
|
f959171db5 | ||
|
|
01b6bcd02c | ||
|
|
0ff4715b35 | ||
|
|
745a73a5b0 | ||
|
|
a69114edce | ||
|
|
489a4766fa | ||
|
|
adcee62386 | ||
|
|
b867d819b7 | ||
|
|
31d8b20a76 | ||
|
|
42ae0e7204 | ||
|
|
43869420c0 | ||
|
|
6c9d296fbd | ||
|
|
008428f10a | ||
|
|
ae7d338474 | ||
|
|
12ff25cc4b | ||
|
|
f31767e7e7 | ||
|
|
7c4f2841e1 | ||
|
|
a27b3555f6 | ||
|
|
c451ddd042 | ||
|
|
da6311055f | ||
|
|
945efb2059 | ||
|
|
ab2881f8e9 | ||
|
|
f23811fff4 | ||
|
|
19ab77501b | ||
|
|
37eeed583a | ||
|
|
caf08efb90 | ||
|
|
7ff998e4df | ||
|
|
0c1e8fe899 | ||
|
|
4158e8c6e3 | ||
|
|
653319e408 | ||
|
|
0a4f14fba7 | ||
|
|
fb4571fc51 | ||
|
|
c11fd40681 | ||
|
|
cbfde8b761 | ||
|
|
c774821cc4 | ||
|
|
2ed15a32bc | ||
|
|
c252853603 | ||
|
|
cfdddec073 | ||
|
|
97a63ab791 | ||
|
|
a0e00341e1 | ||
|
|
26d42491ce | ||
|
|
d678ba2f12 | ||
|
|
55b400eb05 | ||
|
|
0a6313b1f3 | ||
|
|
b4312376bb | ||
|
|
5ffb143a1a | ||
|
|
ee24473077 | ||
|
|
2b98a63f92 | ||
|
|
2e594d87e8 | ||
|
|
b343c2869d | ||
|
|
094b93f4fa | ||
|
|
cdc8a623c0 | ||
|
|
2b43c00a3a | ||
|
|
14928798d4 | ||
|
|
e5fb44fc6b | ||
|
|
9f9e4cd1d8 | ||
|
|
45fe7336fe | ||
|
|
b909e7d917 | ||
|
|
ce62748ade | ||
|
|
d4ecc36f6b | ||
|
|
fee364610e | ||
|
|
492af8525b | ||
|
|
fb58b5ae06 | ||
|
|
577034b0bd | ||
|
|
6fc452edb4 | ||
|
|
086ca49599 | ||
|
|
be1de35da8 | ||
|
|
3c1f57667f | ||
|
|
5997cd20b4 | ||
|
|
6220c9400e | ||
|
|
48a16b927f | ||
|
|
87545d77b4 | ||
|
|
4e0fe29c20 | ||
|
|
f8e3bc843d | ||
|
|
b3f1edaabd | ||
|
|
ef784dd19c | ||
|
|
69bbcee885 | ||
|
|
60b4a99506 | ||
|
|
c1030400f5 | ||
|
|
3f508da501 | ||
|
|
0437cb61a2 | ||
|
|
1372582c54 | ||
|
|
070c3d507f | ||
|
|
3e9c545952 | ||
|
|
e3845e1849 | ||
|
|
91703d6a91 | ||
|
|
cff3101b03 | ||
|
|
c9a48ec64c | ||
|
|
e6419a9759 | ||
|
|
9d1d2fc7a8 | ||
|
|
cdca4a82ab | ||
|
|
c2e4e12512 | ||
|
|
96c1c6717d | ||
|
|
abf776175c | ||
|
|
d6de184bfd | ||
|
|
12249c4e8a | ||
|
|
f1fbc3f0c7 | ||
|
|
bb8cf54bf9 | ||
|
|
dccf8bd786 | ||
|
|
793bb79d76 | ||
|
|
f5cdf27ce6 | ||
|
|
627c56cd25 | ||
|
|
783871d131 | ||
|
|
138c05f995 | ||
|
|
5009a479af | ||
|
|
2a95a10113 | ||
|
|
9d6065c80a | ||
|
|
cd8b2bf007 | ||
|
|
1faf74fbc4 | ||
|
|
09163afbcd | ||
|
|
405d141055 | ||
|
|
599e09fa9f | ||
|
|
4954de5f1a | ||
|
|
ac854ae559 | ||
|
|
9944f05139 | ||
|
|
f6558b656a | ||
|
|
bfbf57ba38 | ||
|
|
d0af35377c | ||
|
|
59c263a246 | ||
|
|
42700bd7d8 | ||
|
|
5a9b364b4b | ||
|
|
3619550c74 | ||
|
|
31cc4012ac | ||
|
|
2e329a8584 | ||
|
|
d92dd59f86 | ||
|
|
5bb7ca377d | ||
|
|
9ee9a3574c | ||
|
|
dc50a44682 | ||
|
|
4be5c14999 | ||
|
|
b92ffebdb4 | ||
|
|
7b6e48b09e | ||
|
|
368851b4bb | ||
|
|
fa2da5712d | ||
|
|
7df97d70bb | ||
|
|
5c5f595829 | ||
|
|
5b659e3d8a | ||
|
|
1a526777cc | ||
|
|
b0d99d4c29 | ||
|
|
c7554475a9 | ||
|
|
cab177e8a7 | ||
|
|
f7244a69ac | ||
|
|
c6ed79ed1d | ||
|
|
00e5358bbb | ||
|
|
598c35eb54 | ||
|
|
00c74ea6c6 | ||
|
|
0c415c9106 | ||
|
|
b0495d033d | ||
|
|
d72cc6e0ca | ||
|
|
b59cb9f57f | ||
|
|
af743be5b0 | ||
|
|
024d039701 | ||
|
|
e9a91656bc | ||
|
|
3505606b44 | ||
|
|
815abae0a0 | ||
|
|
004271241e | ||
|
|
8295967ff2 | ||
|
|
053dca365c | ||
|
|
62110b1a5b | ||
|
|
aae9b2866c | ||
|
|
0d8bddbc20 | ||
|
|
2a7d61bab7 | ||
|
|
de71035fa0 | ||
|
|
5368fb9311 | ||
|
|
3d583a9b6f | ||
|
|
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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -17,10 +17,8 @@ config/config.dev.json
|
|||||||
ffmpeg
|
ffmpeg
|
||||||
ffprobe
|
ffprobe
|
||||||
deno
|
deno
|
||||||
yt-dlp
|
|
||||||
|
|
||||||
# Windows Executable
|
# Windows Executable
|
||||||
ffmpeg.exe
|
ffmpeg.exe
|
||||||
ffprobe.exe
|
ffprobe.exe
|
||||||
deno.exe
|
deno.exe
|
||||||
yt-dlp.exe
|
|
||||||
67
README.md
67
README.md
@@ -61,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
|
||||||
@@ -80,11 +80,9 @@ 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
|
||||||
|
|
||||||
@@ -107,7 +105,7 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
#### Fedora (via Copr - recommended)
|
#### Fedora (via Copr — recommended)
|
||||||
|
|
||||||
Installing via Copr lets `dnf` handle future updates automatically:
|
Installing via Copr lets `dnf` handle future updates automatically:
|
||||||
|
|
||||||
@@ -180,8 +178,7 @@ 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
|
||||||
|
|
||||||
@@ -222,14 +219,13 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
|||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
```
|
```
|
||||||
~/.config/Freedom\ Loader/config.json
|
~/.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,
|
||||||
@@ -237,7 +233,6 @@ 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,
|
||||||
@@ -254,7 +249,6 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
|||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
|------------------------------|---------|----------|------------------------------------------------------------|
|
|------------------------------|---------|----------|------------------------------------------------------------|
|
||||||
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
||||||
| `systemTray` | boolean | `true` | 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 |
|
||||||
@@ -262,7 +256,6 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
|||||||
| `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 |
|
||||||
| `keepPlaylistOrder` | boolean | `false` | Add index before title, usefull for album |
|
|
||||||
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
||||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||||
| `logSystem` | boolean | `true` | Enable application logging |
|
| `logSystem` | boolean | `true` | Enable application logging |
|
||||||
@@ -283,7 +276,7 @@ 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
|
## Project Structure
|
||||||
|
|
||||||
@@ -297,21 +290,15 @@ Freedom-Loader/
|
|||||||
│ ├── pathValidator.js
|
│ ├── pathValidator.js
|
||||||
│ ├── splashManager.js
|
│ ├── splashManager.js
|
||||||
│ ├── themeManager.js
|
│ ├── themeManager.js
|
||||||
│ ├── tray.js
|
|
||||||
│ ├── windowManager.js
|
│ ├── windowManager.js
|
||||||
│ └── ytDlpUpdater.js
|
│ └── ytDlpUpdater.js
|
||||||
├── build/ # Build resources and assets
|
├── build/ # Build resources and assets
|
||||||
│ ├── app-icon.ico
|
|
||||||
│ ├── app-icon.png
|
|
||||||
│ ├── banner.png
|
|
||||||
│ └── ...
|
|
||||||
├── config/ # Configuration files
|
├── config/ # Configuration files
|
||||||
│ ├── config.default.json
|
│ ├── config.default.json
|
||||||
│ └── config.dev.json # Configuration Used when i dev mode
|
│ └── config.dev.json
|
||||||
├── dist/ # Compiled executables (.exe, .AppImage, .deb, .rpm, .snap)
|
|
||||||
├── public/ # Frontend assets and UI
|
├── public/ # Frontend assets and UI
|
||||||
│ ├── index.html
|
│ ├── index.html
|
||||||
│ ├── splash.html # Splash Screen (Start of the application)
|
│ ├── splash.html
|
||||||
│ ├── assets/
|
│ ├── assets/
|
||||||
│ │ ├── icon/ # Application icons
|
│ │ ├── icon/ # Application icons
|
||||||
│ │ └── logo/ # Logo assets
|
│ │ └── logo/ # Logo assets
|
||||||
@@ -330,8 +317,19 @@ Freedom-Loader/
|
|||||||
│ ├── styles.css
|
│ ├── styles.css
|
||||||
│ ├── variables.css
|
│ ├── variables.css
|
||||||
│ ├── components/ # Component-specific styles
|
│ ├── components/ # Component-specific styles
|
||||||
|
│ │ ├── checkbox.css
|
||||||
|
│ │ ├── editpathbutton.css
|
||||||
|
│ │ ├── loader.css
|
||||||
|
│ │ ├── progressBar.css
|
||||||
|
│ │ └── toast.css
|
||||||
│ └── layout/ # Layout styles
|
│ └── layout/ # Layout styles
|
||||||
├── resources/ # Internal resources (binaries : yt-dlp, ffmpeg...)
|
│ ├── container.css
|
||||||
|
│ ├── form.css
|
||||||
|
│ ├── header.css
|
||||||
|
│ ├── settingsPanel.css
|
||||||
|
│ ├── topbar.css
|
||||||
|
│ └── videoinfo.css
|
||||||
|
├── resources/ # Internal resources (binaries)
|
||||||
├── server/ # Express backend server
|
├── server/ # Express backend server
|
||||||
│ ├── logger.js
|
│ ├── logger.js
|
||||||
│ ├── server.js
|
│ ├── server.js
|
||||||
@@ -355,16 +353,16 @@ Freedom-Loader/
|
|||||||
├── theme/ # Theme system
|
├── theme/ # Theme system
|
||||||
│ ├── template.theme.json # Theme template
|
│ ├── template.theme.json # Theme template
|
||||||
│ ├── Dark/ # Default dark theme
|
│ ├── Dark/ # Default dark theme
|
||||||
|
│ │ └── dark.theme.json
|
||||||
│ └── Light/ # Default light theme
|
│ └── Light/ # Default light theme
|
||||||
├── BINARIES.md # External dependencies & binaries documentation
|
│ └── light.theme.json
|
||||||
├── CODE_OF_CONDUCT.md # Community guidelines
|
|
||||||
├── config.js # Global configuration loader
|
|
||||||
├── CONTRIBUTING.md # Contribution guidelines
|
|
||||||
├── jest.config.js # Unit testing configuration
|
|
||||||
├── LICENSE # GPLv3 license
|
|
||||||
├── main.js # Electron main process entry point
|
├── main.js # Electron main process entry point
|
||||||
├── package.json # Project metadata and dependencies
|
|
||||||
├── preload.js # Electron preload script (IPC bridge)
|
├── 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
|
└── README.md # This file
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -391,7 +389,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
|
||||||
|
|
||||||
@@ -545,7 +543,6 @@ You are free to use, modify, and redistribute this software under the terms of t
|
|||||||
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
- [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
|
- 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
|
||||||
|
|||||||
@@ -121,14 +121,8 @@ function initAutoUpdater(mainWindow) {
|
|||||||
*/
|
*/
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
|
|
||||||
/**
|
if (process.env.SNAP) {
|
||||||
* Prevents electron-updater from running inside sandboxed environments.
|
logger.info("Running as Snap, update managed by snapd, skipping electron-updater");
|
||||||
* Snap and Flatpak lock the file system in read-only mode, causing the updater to crash.
|
|
||||||
* In these environments, updates are safely handled by snapd or the Flatpak runtime.
|
|
||||||
* Standalone Linux packages (.deb, .rpm, AppImage) will bypass this and update normally.
|
|
||||||
*/
|
|
||||||
if (process.env.SNAP || process.env.FLATPAK_ID) {
|
|
||||||
logger.info("Running as Snap or Flatpak. Updates are managed by the OS store. Skipping electron-updater.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -103,9 +51,7 @@ function startRPC() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rpc.login({ clientId }).catch(err => {
|
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,31 +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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures RPC connection AND the underlying socket exist
|
* Ensures RPC connection exists before attempting cleanup.
|
||||||
* before attempting to clear activity to prevent crash.
|
|
||||||
*/
|
*/
|
||||||
if (rpc.transport && rpc.transport.socket) {
|
if (rpc && rpc.transport) {
|
||||||
await rpc.clearActivity();
|
await rpc.clearActivity()
|
||||||
|
await rpc.destroy();
|
||||||
|
} else{
|
||||||
|
logger.error("Not Able to close RPC")
|
||||||
}
|
}
|
||||||
|
|
||||||
await rpc.destroy();
|
|
||||||
|
|
||||||
} 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};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Security whitelist for feature flags that can be modified at runtime.
|
* Security whitelist for feature flags that can be modified at runtime.
|
||||||
@@ -28,7 +27,6 @@ const { createSystemTray, destroyTray } = require("./tray");
|
|||||||
*/
|
*/
|
||||||
const FEATURE_WHITELIST = new Set([
|
const FEATURE_WHITELIST = new Set([
|
||||||
"autoUpdate",
|
"autoUpdate",
|
||||||
"systemTray",
|
|
||||||
"discordRPC",
|
"discordRPC",
|
||||||
"customTopBar",
|
"customTopBar",
|
||||||
"autoCheckInfo",
|
"autoCheckInfo",
|
||||||
@@ -36,7 +34,6 @@ const FEATURE_WHITELIST = new Set([
|
|||||||
"addMetadata",
|
"addMetadata",
|
||||||
"verboseLogs",
|
"verboseLogs",
|
||||||
"autoDownloadPlaylist",
|
"autoDownloadPlaylist",
|
||||||
"keepPlaylistOrder",
|
|
||||||
"customCodec",
|
"customCodec",
|
||||||
"theme",
|
"theme",
|
||||||
"createPlaylistFolders",
|
"createPlaylistFolders",
|
||||||
@@ -215,21 +212,6 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Dynamically intercepts and applies System Tray state changes at runtime.
|
|
||||||
* Instantiates or destroys the tray icon immediately to prevent window
|
|
||||||
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
|
|
||||||
*/
|
|
||||||
if (key === "systemTray") {
|
|
||||||
if (value === true) {
|
|
||||||
logger.info("System Tray enabled dynamically.");
|
|
||||||
createSystemTray(getMainWindow(), config.devMode);
|
|
||||||
} else {
|
|
||||||
logger.info("System Tray disabled dynamically.");
|
|
||||||
destroyTray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configFeatures[key] = value;
|
configFeatures[key] = value;
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -56,30 +56,35 @@ 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 (!fs.existsSync(resolved)) {
|
||||||
|
fs.mkdirSync(resolved, { recursive: true });
|
||||||
if (!isSafePath(real)) {
|
logger.info(`Download folder created: ${resolved}`);
|
||||||
throw new Error("Path not allowed: system folders are blocked!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return real;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (err) {
|
const real = fs.realpathSync(resolved);
|
||||||
/**
|
|
||||||
* Handles invalid filesystem paths, permissions issues, or resolution failures.
|
if (!isSafePath(resolved)) {
|
||||||
* Logs diagnostic information before propagating a sanitized error.
|
throw new Error("Path not allowed: system folders are blocked!");
|
||||||
*/
|
|
||||||
logger.error(`Invalid download path: ${userPath} — ${err.message}`);
|
|
||||||
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return real;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles invalid filesystem paths, permissions issues, or resolution failures.
|
||||||
|
* Logs diagnostic information before propagating a sanitized error.
|
||||||
|
*/
|
||||||
|
logger.error(`Invalid download path: ${userPath} — ${err.message}`);
|
||||||
|
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { validateDownloadPath, getDefaultDownloadPath };
|
module.exports = { validateDownloadPath, getDefaultDownloadPath };
|
||||||
108
app/tray.js
108
app/tray.js
@@ -1,108 +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");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {import('electron').BrowserWindow} mainWindow - The main application window.
|
|
||||||
* @param {boolean} devMode - Indicates whether the application is running in development mode.
|
|
||||||
* @returns {Tray} The created Tray instance.
|
|
||||||
*/
|
|
||||||
function createSystemTray(mainWindow, 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: () => {
|
|
||||||
mainWindow.show();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ 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", () => {
|
|
||||||
if (mainWindow.isVisible()) {
|
|
||||||
mainWindow.hide();
|
|
||||||
} else {
|
|
||||||
mainWindow.show();
|
|
||||||
mainWindow.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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,19 +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.systemTray ) {
|
|
||||||
event.preventDefault();
|
|
||||||
mainWindow.hide();
|
|
||||||
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 |
24
config.js
24
config.js
@@ -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,7 +7,6 @@
|
|||||||
"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,
|
||||||
|
|||||||
69
main.js
69
main.js
@@ -41,10 +41,7 @@ const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/log
|
|||||||
const { configFeatures } = require("./config");
|
const { configFeatures } = require("./config");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In-memory snapshot of application feature flags.
|
* On start, enable or disable hardWareAcceleration
|
||||||
*
|
|
||||||
* Note: Changes to the config file are not automatically reflected.
|
|
||||||
* A restart could be required.
|
|
||||||
*/
|
*/
|
||||||
if (!configFeatures.enableHardwareAcceleration){
|
if (!configFeatures.enableHardwareAcceleration){
|
||||||
app.disableHardwareAcceleration();
|
app.disableHardwareAcceleration();
|
||||||
@@ -53,7 +50,15 @@ if (!configFeatures.enableHardwareAcceleration){
|
|||||||
logger.info("Enable Hardware Acceleration")
|
logger.info("Enable Hardware Acceleration")
|
||||||
}
|
}
|
||||||
|
|
||||||
if(configFeatures.devMode){
|
/**
|
||||||
|
* In-memory snapshot of application feature flags.
|
||||||
|
*
|
||||||
|
* Note: Changes to the config file are not automatically reflected.
|
||||||
|
* A restart could be required.
|
||||||
|
*/
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
|
if(config.devMode){
|
||||||
/**
|
/**
|
||||||
* Start devTron extensions - @see https://github.com/electron/devtron
|
* Start devTron extensions - @see https://github.com/electron/devtron
|
||||||
*/
|
*/
|
||||||
@@ -75,21 +80,7 @@ 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, isWindows, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
|
const { userThemesPath, initUserThemes, isWindows, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
|
||||||
const {createSystemTray, destroyTray} = require("./app/tray");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If another instance want to run
|
* If another instance want to run
|
||||||
@@ -112,7 +103,7 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
createSplashWindow();
|
createSplashWindow();
|
||||||
|
|
||||||
if (!configFeatures.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);
|
||||||
@@ -136,11 +127,6 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
closeSplashWindow();
|
closeSplashWindow();
|
||||||
getMainWindow().show();
|
getMainWindow().show();
|
||||||
|
|
||||||
if (configFeatures.systemTray) {
|
|
||||||
createSystemTray(getMainWindow(), configFeatures.devMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configFeatures.discordRPC) startRPC();
|
if (configFeatures.discordRPC) startRPC();
|
||||||
|
|
||||||
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
||||||
@@ -152,33 +138,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();
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
destroyTray();
|
|
||||||
await stopRPC();
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Error during cleanup:", err);
|
|
||||||
} finally {
|
|
||||||
logger.info("All services stopped. Have a nice day!");
|
|
||||||
logSessionEnd();
|
|
||||||
|
|
||||||
isCleanedUp = true;
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
1736
package-lock.json
generated
1736
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.6.2",
|
"version": "1.6.0",
|
||||||
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
|
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
|
||||||
"description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms",
|
"description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms",
|
||||||
"homepage": "https://masteracnolo.github.io/Freedom-Loader-Site/",
|
"homepage": "https://masteracnolo.github.io/Freedom-Loader-Site/",
|
||||||
@@ -15,24 +15,16 @@
|
|||||||
},
|
},
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron --no-warnings .",
|
"start": "electron --trace-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:win": "electron-builder --win",
|
||||||
"build:linux": "electron-builder --linux",
|
"build:linux": "electron-builder --linux",
|
||||||
"build:rpm": "bash scripts/create-copr-package.sh",
|
"build:rpm": "bash scripts/create-copr-package.sh",
|
||||||
"build:all": "electron-builder -wl",
|
"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": "jest",
|
||||||
"test:unit": "jest test/unit",
|
"test:unit": "jest tests/unit",
|
||||||
|
|
||||||
"update": "npm update"
|
"update": "npm update"
|
||||||
},
|
},
|
||||||
@@ -52,10 +44,9 @@
|
|||||||
"@electron/devtron": "^2.1.1",
|
"@electron/devtron": "^2.1.1",
|
||||||
"@jest/globals": "^30.4.1",
|
"@jest/globals": "^30.4.1",
|
||||||
"@playwright/test": "^1.62.1",
|
"@playwright/test": "^1.62.1",
|
||||||
"electron": "^43.3.0",
|
"electron": "^41.2.0",
|
||||||
"electron-builder": "^26.15.3",
|
"electron-builder": "^26.15.3",
|
||||||
"jest": "^30.4.2",
|
"jest": "^30.4.2",
|
||||||
"nodemon": "^3.1.14",
|
|
||||||
"playwright": "^1.62.1"
|
"playwright": "^1.62.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
@@ -84,14 +75,6 @@
|
|||||||
"from": "theme",
|
"from": "theme",
|
||||||
"to": "theme"
|
"to": "theme"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"from": "build/app-icon.ico",
|
|
||||||
"to": "app-icon.ico"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "build/app-icon-64x64.png",
|
|
||||||
"to": "app-icon-64x64.png"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"from": "build/confirm-icon.png",
|
"from": "build/confirm-icon.png",
|
||||||
"to": "confirm-icon.png"
|
"to": "confirm-icon.png"
|
||||||
@@ -116,7 +99,7 @@
|
|||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"icon": "build/app-icon.ico",
|
"icon": "build/app-icon.ico",
|
||||||
"artifactName": "Freedom-Loader-Setup-${version}.${ext}",
|
"artifactName": "${productName}-Setup-${version}.${ext}",
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "resources/binaries/win-32/yt-dlp.exe",
|
"from": "resources/binaries/win-32/yt-dlp.exe",
|
||||||
|
|||||||
@@ -56,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">
|
||||||
@@ -106,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>
|
||||||
@@ -127,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>
|
||||||
@@ -306,6 +290,7 @@
|
|||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="script/toast.js"></script>
|
<script src="script/toast.js"></script>
|
||||||
|
<script src="script/customConfiguration.js"></script>
|
||||||
<script src="script/clipboardPaste.js"></script>
|
<script src="script/clipboardPaste.js"></script>
|
||||||
<script src="script/custompath.js"></script>
|
<script src="script/custompath.js"></script>
|
||||||
<script src="script/settingsPanel.js"></script>
|
<script src="script/settingsPanel.js"></script>
|
||||||
|
|||||||
@@ -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();
|
||||||
27
public/script/customConfiguration.js
Normal file
27
public/script/customConfiguration.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
async function initPlatformRestrictions() {
|
||||||
|
const platform = await window.electronAPI.getProcessPlatform();
|
||||||
|
|
||||||
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
const refreshBtn = document.getElementById("refresh-themes-btn");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the OS windows
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
const isWindows = platform === "win32";
|
||||||
|
|
||||||
|
console.log(isWindows);
|
||||||
|
|
||||||
|
// If it's windows, the button is not disabled.
|
||||||
|
themeSelect.disabled = !isWindows;
|
||||||
|
refreshBtn.disabled = !isWindows;
|
||||||
|
|
||||||
|
themeSelect.classList.toggle("disabled", !isWindows);
|
||||||
|
refreshBtn.classList.toggle("disabled", !isWindows);
|
||||||
|
|
||||||
|
if (!isWindows) {
|
||||||
|
themeSelect.title = "Themes are only available on Windows";
|
||||||
|
refreshBtn.title = "Themes are only available on Windows";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ async function initThemes() {
|
|||||||
const defaultTheme = {
|
const defaultTheme = {
|
||||||
id: "dark",
|
id: "dark",
|
||||||
name: "Dark",
|
name: "Dark",
|
||||||
subtitle: "Because Why Not ?",
|
subtitle: "Darkness is my ally",
|
||||||
style: {
|
style: {
|
||||||
background: {
|
background: {
|
||||||
size: "cover",
|
size: "cover",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
/**
|
||||||
const container = document.getElementById("container");
|
* Applies UI layout adjustments depending on feature flags
|
||||||
const themeSwitcher = document.getElementById("theme-switcher");
|
* provided by the Electron backend.
|
||||||
|
*
|
||||||
if (topbar) topbar.style.display = "none";
|
* Specifically handles:
|
||||||
if (container) container.style.marginTop = "0";
|
* - hiding custom topbar
|
||||||
if (themeSwitcher) themeSwitcher.style.top = "30px";
|
* - adjusting layout spacing
|
||||||
}
|
* - repositioning theme switcher
|
||||||
|
*/
|
||||||
} catch (error) {
|
(async function applyFeatureLayout() {
|
||||||
console.error("Failed to load layout features for topbar:", error);
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
|
if (!features.customTopBar) {
|
||||||
|
const topbar = document.getElementById("topbar");
|
||||||
|
const container = document.getElementById("container");
|
||||||
|
const themeSwitcher = document.getElementById("theme-switcher");
|
||||||
|
|
||||||
|
if (topbar) topbar.style.display = "none";
|
||||||
|
if (container) container.style.marginTop = "0";
|
||||||
|
if (themeSwitcher) themeSwitcher.style.top = "30px";
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
}
|
|
||||||
|
|
||||||
initTopBar()
|
|
||||||
document.addEventListener("DOMContentLoaded", attachListeners)
|
|
||||||
BIN
resources/binaries/linux/yt-dlp
Executable file
BIN
resources/binaries/linux/yt-dlp
Executable file
Binary file not shown.
BIN
resources/binaries/win-32/yt-dlp.exe
Normal file
BIN
resources/binaries/win-32/yt-dlp.exe
Normal file
Binary file not shown.
@@ -6,74 +6,15 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||||||
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
|
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
|
||||||
RPM_OUT="$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm"
|
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
|
# Check that linux-unpacked exists
|
||||||
if [ ! -d "$ROOT_DIR/dist/linux-unpacked" ]; then
|
if [ ! -d "$ROOT_DIR/dist/linux-unpacked" ]; then
|
||||||
echo "Error: dist/linux-unpacked not found. Run 'npm run build:linux' first."
|
echo "Error: dist/linux-unpacked not found. Run 'npm run build:linux' first."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# Step 1 — Build .rpm from linux-unpacked via fpm
|
||||||
# Prepare staging files
|
|
||||||
# fpm requires exact file-to-destination mappings to avoid
|
|
||||||
# scanning system directories (which causes permission errors)
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
# Shell wrapper for /usr/bin/freedom-loader
|
|
||||||
TMP_WRAPPER="$ROOT_DIR/.rpm-pkg-tmp-wrapper"
|
|
||||||
mkdir -p "$TMP_WRAPPER"
|
|
||||||
cat > "$TMP_WRAPPER/freedom-loader" << 'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
exec /opt/freedom-loader/freedom-loader "$@"
|
|
||||||
EOF
|
|
||||||
chmod +x "$TMP_WRAPPER/freedom-loader"
|
|
||||||
|
|
||||||
# Desktop entry file
|
|
||||||
TMP_DESKTOP="$ROOT_DIR/.rpm-pkg-tmp-desktop"
|
|
||||||
mkdir -p "$TMP_DESKTOP"
|
|
||||||
cat > "$TMP_DESKTOP/freedom-loader.desktop" << 'EOF'
|
|
||||||
[Desktop Entry]
|
|
||||||
Name=Freedom Loader
|
|
||||||
Exec=/opt/freedom-loader/freedom-loader %U
|
|
||||||
Icon=freedom-loader
|
|
||||||
Type=Application
|
|
||||||
Categories=AudioVideo;Utility;Network;
|
|
||||||
Comment=Free and open-source GUI for yt-dlp
|
|
||||||
StartupWMClass=Freedom Loader
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# Step 1 - Build .rpm from linux-unpacked via fpm
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
echo "Building RPM..."
|
echo "Building RPM..."
|
||||||
"$FPM" -s dir -t rpm --force \
|
fpm -s dir -t rpm --force \
|
||||||
--rpm-use-file-permissions \
|
|
||||||
--name freedom-loader \
|
--name freedom-loader \
|
||||||
--version "$VERSION" \
|
--version "$VERSION" \
|
||||||
--architecture x86_64 \
|
--architecture x86_64 \
|
||||||
@@ -85,21 +26,14 @@ echo "Building RPM..."
|
|||||||
--depends nss \
|
--depends nss \
|
||||||
--depends libXScrnSaver \
|
--depends libXScrnSaver \
|
||||||
--depends at-spi2-core \
|
--depends at-spi2-core \
|
||||||
--rpm-attr "4755,root,root:/opt/freedom-loader/chrome-sandbox" \
|
|
||||||
--package "$RPM_OUT" \
|
--package "$RPM_OUT" \
|
||||||
"$ROOT_DIR/dist/linux-unpacked/=/opt/freedom-loader" \
|
"$ROOT_DIR/dist/linux-unpacked/=/opt/freedom-loader" \
|
||||||
"$TMP_WRAPPER/freedom-loader=/usr/bin/freedom-loader" \
|
|
||||||
"$TMP_DESKTOP/freedom-loader.desktop=/usr/share/applications/freedom-loader.desktop" \
|
|
||||||
"$ROOT_DIR/build/app-icon.png=/usr/share/icons/hicolor/512x512/apps/freedom-loader.png" \
|
"$ROOT_DIR/build/app-icon.png=/usr/share/icons/hicolor/512x512/apps/freedom-loader.png" \
|
||||||
"$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
"$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
||||||
|
|
||||||
rm -rf "$TMP_WRAPPER" "$TMP_DESKTOP"
|
|
||||||
echo "RPM built: $RPM_OUT"
|
echo "RPM built: $RPM_OUT"
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# Step 2 — Build SRPM for COPR (wraps the .rpm above)
|
||||||
# Step 2 - Build SRPM for COPR
|
|
||||||
# Wraps the .rpm above into a source RPM for COPR submission
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
echo "Building SRPM for COPR..."
|
echo "Building SRPM for COPR..."
|
||||||
mkdir -p "$ROOT_DIR/srpm-out"
|
mkdir -p "$ROOT_DIR/srpm-out"
|
||||||
|
|
||||||
|
|||||||
@@ -1,187 +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")
|
|
||||||
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 "⚠️ Argument inconnu : $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 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
|
|
||||||
|
|
||||||
## Found a bug or issue?
|
|
||||||
Please report it in the [GitHub Issues](https://github.com/MasterAcnolo/Freedom-Loader/issues) section.
|
|
||||||
|
|
||||||
## Next Release (non-exhaustive roadmap)
|
|
||||||
- More format options
|
|
||||||
- Subtitle support
|
|
||||||
- Improved UI / UX
|
|
||||||
- Language selection
|
|
||||||
- Download specific parts of videos
|
|
||||||
- File renaming options
|
|
||||||
- Parallel downloads
|
|
||||||
- Skip sponsored parts automatically
|
|
||||||
|
|
||||||
**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 "============================================"
|
|
||||||
@@ -18,9 +18,7 @@ const { isValidUrl } = require("../helpers/validation.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:
|
||||||
@@ -32,10 +30,9 @@ 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 = url.includes("&list") || url.includes("?list");
|
const isPlaylistUrl = url.includes("&list") || url.includes("?list");
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -89,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(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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 v${config.version}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push("-o", path.join(outputFolder, outputTemplate));
|
|
||||||
args.push(url);
|
args.push(url);
|
||||||
|
|
||||||
return args.filter(Boolean);
|
return args.filter(Boolean);
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
@@ -20,34 +14,14 @@ const activeNotifications = new Set();
|
|||||||
function notifyDownloadFinished(folder, notifyEnabled = true) {
|
function notifyDownloadFinished(folder, notifyEnabled = true) {
|
||||||
if (!notifyEnabled) return;
|
if (!notifyEnabled) return;
|
||||||
if (!folder) return;
|
if (!folder) return;
|
||||||
|
|
||||||
const notif = new Notification({
|
const notif = new Notification({
|
||||||
title: "Freedom Loader",
|
title: "Freedom Loader",
|
||||||
body: "Your download is complete, click here to open it.",
|
body: "Your download is complete, click here to open it.",
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,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.
|
||||||
|
|||||||
@@ -22,31 +22,6 @@ const logFormat = format.combine(
|
|||||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Format used by the saved file
|
|
||||||
* @type {Format}
|
|
||||||
*/
|
|
||||||
const fileFormat = format.combine(
|
|
||||||
format.timestamp({format: "YYYY-MM-DD HH:mm:ss"}),
|
|
||||||
format.errors({stack: true}), // Capture la stack trace complète des erreurs
|
|
||||||
format.printf(({timestamp, level, message, stack}) => {
|
|
||||||
return `${timestamp} | ${level.toUpperCase()} | ${stack || message}`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formated used by the IDE/Console
|
|
||||||
* @type {Format}
|
|
||||||
*/
|
|
||||||
const consoleFormat = format.combine(
|
|
||||||
format.colorize({all: true}),
|
|
||||||
format.timestamp({format: "HH:mm:ss"}),
|
|
||||||
format.errors({stack: true}),
|
|
||||||
format.printf(({timestamp, level, message, stack}) => {
|
|
||||||
return `[${timestamp}] ${level}: ${stack || message}`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger Instance with differents types
|
* Logger Instance with differents types
|
||||||
* - info
|
* - info
|
||||||
@@ -58,6 +33,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,
|
||||||
@@ -65,11 +41,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,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -78,17 +54,16 @@ const logger = createLogger({
|
|||||||
* Start Log Session
|
* Start Log Session
|
||||||
*/
|
*/
|
||||||
function logSessionStart(logDir, downloadPath) {
|
function logSessionStart(logDir, downloadPath) {
|
||||||
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);
|
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;
|
||||||
@@ -13,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"));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +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");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to the currently running yt-dlp process.
|
* Reference to the currently running yt-dlp process.
|
||||||
@@ -123,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.
|
||||||
@@ -132,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;
|
||||||
|
|
||||||
@@ -161,8 +158,8 @@ 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 || isPlaylistUrl(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);
|
||||||
@@ -171,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);
|
||||||
@@ -190,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}`));
|
||||||
@@ -204,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/);
|
||||||
|
|||||||
BIN
theme/dark.zip
BIN
theme/dark.zip
Binary file not shown.
Reference in New Issue
Block a user