355 Commits

Author SHA1 Message Date
MasterAcnolo
519dcad6e6 fix: rename localMode => devMode for clarity 2026-05-29 07:42:49 +02:00
MasterAcnolo
ac94d68224 fix: rename localMode => devMode for clarity 2026-05-29 07:42:38 +02:00
MasterAcnolo
0f876fcb70 fix: update dependencies 2026-05-29 07:41:54 +02:00
MasterAcnolo
a6c11ba7c4 update: version for 1.6.0 2026-05-15 19:57:33 +02:00
MasterAcnolo
44132dd7b5 fix: update dependencies 2026-05-11 21:14:00 +02:00
MasterAcnolo
15ab6892b7 Fix: remove duplication roadmap (now full in website) 2026-05-01 12:07:39 +02:00
MasterAcnolo
725b6f0ada Fix: Configuration File Location 2026-04-15 13:55:13 +02:00
MasterAcnolo
ad0e4f858b Fix: missing options in readme 2026-04-15 13:50:39 +02:00
MasterAcnolo
68108e3a66 Update Workshop link to GitHub repository 2026-04-14 11:28:53 +02:00
MasterAcnolo
eff2affd42 1.5.0 !!!!!!
IT'S TIME
2026-04-14 11:20:48 +02:00
MasterAcnolo
87cf05643f Feat: Update README with detailed project structure and architecture overview 2026-04-14 11:04:55 +02:00
MasterAcnolo
ce14b8abd7 Fix: Remove emoji from error messages in download and info controllers 2026-04-14 09:31:23 +02:00
MasterAcnolo
44ed1bbf0e Feat: Add link to the most recent roadmap in README 2026-04-14 09:25:59 +02:00
MasterAcnolo
e0d017d9fd Fix: Remove z-index from container styles and add z-index to settings button 2026-04-14 09:22:31 +02:00
MasterAcnolo
c3ea1ac0bd Feat: Add section for submitting community themes in README 2026-04-14 09:22:24 +02:00
MasterAcnolo
238d4ea717 Fix: Save Themes inside "AppData" 2026-04-13 21:49:19 +02:00
MasterAcnolo
d879a5eb1d Feat: Add dynamic icon path for main window based on local mode 2026-04-13 20:57:56 +02:00
MasterAcnolo
40bea35286 Feat: Implement notification feature for download completion 2026-04-13 20:39:23 +02:00
MasterAcnolo
83ac76381a Fix: Update links to Freedom-Loader-Site in documentation and IPC handlers 2026-04-13 20:20:47 +02:00
MasterAcnolo
6ff0c71be7 Feat: Add related repository links for website and workshop in README 2026-04-13 19:48:09 +02:00
MasterAcnolo
fd0b339bcc Fix: Update link to Freedom Loader Workshop in README 2026-04-13 19:15:33 +02:00
MasterAcnolo
bc5faf38b6 Fix: Correct typo in Workshop badge link in README 2026-04-13 19:14:35 +02:00
MasterAcnolo
e744e55475 Feat: Add Theme Workshop section to README and update features list 2026-04-13 19:13:04 +02:00
MasterAcnolo
a581c9309d Fix: Update external links for website and wiki in IPC handlers 2026-04-13 19:13:00 +02:00
MasterAcnolo
61040769ee Feat: Add "Theme Workshop" button and associated IPC handler for workshop access 2026-04-12 21:40:13 +02:00
MasterAcnolo
06f6a30797 Feat: Enhance theme settings with new button styles for open theme and JSON options 2026-04-12 19:32:05 +02:00
MasterAcnolo
a2653eef57 Feat: Implement playlist download count tracking during downloads with UI updates 2026-04-12 18:24:19 +02:00
MasterAcnolo
89573d11f7 Fix: Adjust z-index for reference 2026-04-12 18:17:43 +02:00
MasterAcnolo
f0c28a7f5e Fix: Moving Custom Top BAr Settings into advanced 2026-04-12 18:15:19 +02:00
MasterAcnolo
f3c37c2498 Feat: Add createPlaylistFolders feature to organize downloads into dedicated folders with toggle settings 2026-04-12 18:13:38 +02:00
MasterAcnolo
8fe4e7c38a Fix: Update save path display to include title attribute for better accessibility 2026-04-12 17:40:01 +02:00
MasterAcnolo
912f4042e5 Fix: Enhance error handling for playlist fetch failures by retrying with single video 2026-04-12 17:34:17 +02:00
MasterAcnolo
5dc61b4a15 Fix: Adjust stop button hover scale effect for better user experience 2026-04-12 17:28:13 +02:00
MasterAcnolo
5fb49b0c9d Fix: Update button text 2026-04-12 17:04:20 +02:00
MasterAcnolo
75fdf91fcd Refactor: auto-update functionality with user prompts and download progress 2026-04-12 16:57:39 +02:00
MasterAcnolo
011e0a703a Feat: Enhance download experience with progress indicators and toast notifications 2026-04-12 12:04:05 +02:00
MasterAcnolo
5aebc78d44 Fix: IPC button not working 2026-04-12 11:22:31 +02:00
MasterAcnolo
60137a15dc Feat: Implement download cancellation feature with stop button and progress handling 2026-04-12 11:22:18 +02:00
MasterAcnolo
d05b9e2511 Feat: Add theme reloading functionality with refresh button in settings panel 2026-04-12 09:32:15 +02:00
MasterAcnolo
ebb48f4f8b Fix: Remove themes from the theme order array 2026-04-11 22:38:25 +02:00
MasterAcnolo
8be47b9821 Feat: Implement splash screen with progress indicators during startup 2026-04-11 22:36:42 +02:00
MasterAcnolo
ca8b107f61 Fix: Display custom these with a basic order 2026-04-11 22:16:05 +02:00
MasterAcnolo
f25d07cb83 Fix: Remove empty title tag from index.html and unused app styles from styles.css 2026-04-11 22:15:49 +02:00
MasterAcnolo
49e1e1b507 Feat: Add theme folder access functionality in UI 2026-04-11 18:25:17 +02:00
MasterAcnolo
23d11d8933 Feat: Custom Theme features and clean the old one 2026-04-11 18:10:51 +02:00
MasterAcnolo
9f88fe19c3 Fix: Theme Saving 2026-04-11 16:37:35 +02:00
MasterAcnolo
9b4b0306c7 fix: move config to AppData to prevent settings loss on update, add config.dev.json for dev environment 2026-04-11 16:33:58 +02:00
MasterAcnolo
96401a6e1f Refactor: split main.js into app/ modules 2026-04-11 16:14:21 +02:00
MasterAcnolo
0c7103c5c9 Update: Readme version and file structure 2026-04-11 16:13:36 +02:00
MasterAcnolo
798084e3bf Fix: Remove unusued element 2026-04-11 16:00:51 +02:00
MasterAcnolo
5c4e6669fe Fix: Update theme JSON structure for audio and button settings 2026-04-11 15:18:11 +02:00
MasterAcnolo
00c542925c Refactor: Update CSS variables
Fix Progress bar
2026-04-11 15:17:05 +02:00
MasterAcnolo
24460cf0b5 Refactor: Restructure HTML layout and enhance CSS styles for improved UI 2026-04-11 14:20:05 +02:00
MasterAcnolo
a3c1b1d753 Fix: Settings Secondary Color in LIght THeme 2026-04-11 14:01:39 +02:00
MasterAcnolo
5ade3fe144 Merge pull request #39 from MasterAcnolo/1.5.0
1.4.7
2026-04-09 13:46:20 +02:00
MasterAcnolo
577069b471 Fix: Remove unusued pictures 2026-04-09 13:35:04 +02:00
MasterAcnolo
a4da47647a Update: App Version in ReadME 2026-04-09 13:31:58 +02:00
MasterAcnolo
c5b29a0647 Update: Dependencies 2026-04-08 15:04:56 +02:00
MasterAcnolo
90e1a5fde7 Merge pull request #36 from MasterAcnolo/1.4.6
1.4.6
2026-03-18 10:00:17 +01:00
MasterAcnolo
dcfea2073c Fix: Wrong files Path 2026-03-18 09:20:25 +01:00
MasterAcnolo
0be1ef1710 Update: YT-DLP 2026-03-18 09:13:56 +01:00
MasterAcnolo
da393b19d9 Refactor: Suffixe in helpers/ 2026-03-18 09:13:47 +01:00
MasterAcnolo
1c27cc19cf Fix: Remove unusued modules and disable devTools when packaged 2026-03-16 21:22:41 +01:00
MasterAcnolo
ff7366c46c Fix: Update YT-DLP 2026-03-16 21:22:24 +01:00
MasterAcnolo
459d4c6df4 Fix: Increment app version 2026-03-16 21:15:35 +01:00
MasterAcnolo
d2ca3e99d2 Fix: Dependencies 2026-03-16 21:15:17 +01:00
MasterAcnolo
8ad3ea55cc Docs: Update ReadME 2026-03-16 20:57:38 +01:00
MasterAcnolo
71dbd104f8 Merge pull request #35 from MasterAcnolo/working
1.4.5
2026-02-21 13:00:36 +01:00
MasterAcnolo
57a5defb08 Fix: Update Dependencies 2026-02-21 12:43:36 +01:00
MasterAcnolo
25c78ff8b7 Fix: Increment App Version 2026-02-21 12:40:27 +01:00
MasterAcnolo
792a59d8d7 Feat: You can now fetch image from all website. 2026-02-21 12:38:57 +01:00
MasterAcnolo
b13c98dda2 Refactor: Translate all comms in English 2026-02-21 12:27:13 +01:00
MasterAcnolo
1f103c20b9 Fix: Remove Discord RPC Update. It's useless and take performance for nothing. 2026-02-19 22:23:30 +01:00
MasterAcnolo
70d62959ca Fix: Reduce Line Complexity featuresPath 2026-02-19 22:08:56 +01:00
MasterAcnolo
83ca963502 Merge with false positive
Merge with False positive
2026-02-18 15:10:01 +01:00
MasterAcnolo
9fb9295093 Fix: Try to re run workflow 2026-02-18 14:51:57 +01:00
MasterAcnolo
74fcdbb738 Fix: Undo Path Validation for coming back on a classic one 2026-02-18 14:43:40 +01:00
MasterAcnolo
32c9a66a35 Fix: Centralize default download path and enhance path validation logic 2026-02-17 23:29:31 +01:00
MasterAcnolo
45e0840fea Fix: Correct windowMs value in rate limit configuration to 15 minutes 2026-02-17 23:26:01 +01:00
MasterAcnolo
19b3f8b947 Fix: Refactor conditional statements for starting Discord RPC and Auto Update 2026-02-17 21:49:52 +01:00
MasterAcnolo
cc5bde5b48 Fix: Correct comment for rate limit window duration in rateLimit.js 2026-02-17 21:49:43 +01:00
MasterAcnolo
9a48ac05e3 Fix: Uncomplete comments and remove repeated code 2026-02-17 21:42:12 +01:00
MasterAcnolo
5750444655 Fix: Update full changelog link format in release template 2026-02-17 21:35:57 +01:00
MasterAcnolo
fdb22f2342 Fix: Improve browser detection logging and error handling 2026-02-17 21:29:51 +01:00
MasterAcnolo
760550e521 Fix: Centralize resource paths and icons 2026-02-17 21:28:46 +01:00
MasterAcnolo
334e83ae1f Fix: Small coms in AppVersion.js 2026-02-17 21:12:01 +01:00
MasterAcnolo
5d776594fc Fix: App Not Stopping correctly + Better Logs for end :) 2026-02-17 21:09:09 +01:00
MasterAcnolo
adb36e3fa2 Fix: We are not on apple system. Don't care about Darwin 2026-02-17 20:59:22 +01:00
MasterAcnolo
87945fba80 Fix: Missing semi colon, Better Folder Creation, And remove the fucking bad code who was repeat too much (wtf i did) 2026-02-17 18:05:07 +01:00
MasterAcnolo
2234f9ed81 Fix: Update version numbers to 1.4.4 in package.json and ReadME 2026-02-17 15:22:38 +01:00
MasterAcnolo
7cd3b4be06 Fix: Update playlist badge text to English 2026-02-17 15:19:23 +01:00
MasterAcnolo
a42b020b96 Fix: Update URL check to correctly identify playlist links 2026-02-17 15:19:17 +01:00
MasterAcnolo
c1e70dae80 Feat: Add Better playlist layout 2026-02-17 10:27:13 +01:00
MasterAcnolo
6877c51c0e Fix: Add Small Comms for clarity 2026-02-17 10:26:54 +01:00
MasterAcnolo
5a41090175 Fix: Remove Unusued Code 2026-02-17 10:26:39 +01:00
MasterAcnolo
5983f21759 Fix: Big mistake when estimating video format (? -> &) 2026-02-17 09:50:32 +01:00
MasterAcnolo
ee9e02410f Merge pull request #31 from MasterAcnolo/fix-input
1.4.3
2026-02-16 22:34:12 +01:00
MasterAcnolo
909480083e Fix: Update Dependencies 2026-02-16 22:00:31 +01:00
MasterAcnolo
ca0492ed04 Fix: Update ReadME App Version 2026-02-16 21:37:24 +01:00
MasterAcnolo
6c73dd75be Feat: Add Paste BTN URL Input 2026-02-16 21:34:39 +01:00
MasterAcnolo
556dccec08 Fix: Increment App version 2026-02-16 21:29:24 +01:00
MasterAcnolo
f720d7c889 Merge pull request #30 from MasterAcnolo/fix
1.4.2
2026-02-06 22:55:43 +01:00
MasterAcnolo
bb23f37fb5 Fix: Update release badge to version 1.4.2 2026-02-06 22:38:19 +01:00
MasterAcnolo
220e81d8b2 Fix: 1.4.2 Version 2026-02-06 22:37:05 +01:00
MasterAcnolo
44d8772827 Fix: yt-dlp update 2026-02-06 22:23:31 +01:00
MasterAcnolo
335fb9652b Fix: Dependencies Vulnerability 2026-02-06 22:11:43 +01:00
MasterAcnolo
243b98cbf9 Merge branch 'fix' of https://github.com/MasterAcnolo/Freedom-Loader into fix 2026-02-06 22:01:01 +01:00
MasterAcnolo
23da75a17b Potential fix for code scanning alert no. 5: Type confusion through parameter tampering
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-02-06 22:00:50 +01:00
MasterAcnolo
756d2c048e Update: YT-DLP 2026-02-05 16:05:33 +01:00
MasterAcnolo
659c1d4cf9 Update acknowledgments in README.md
Added acknowledgment for Deno and Jacques Chirac.
2026-02-03 17:48:59 +01:00
MasterAcnolo
443fd31170 Add: Additional dependencies section in README with required binaries 2026-02-03 14:01:59 +01:00
MasterAcnolo
6d8095ceb2 Mark completed tasks in the roadmap as strikethrough
Updated the roadmap to mark completed tasks as strikethrough.
2026-01-26 10:09:33 +01:00
MasterAcnolo
c18cb6c8aa Fix: ReadME 2026-01-25 21:34:40 +01:00
MasterAcnolo
a3c5025434 Fix: Bug Report Template 2026-01-25 21:31:35 +01:00
MasterAcnolo
3d7986cb87 Fix: Security 2026-01-25 21:28:23 +01:00
MasterAcnolo
3f8701567f Fix: Security, Contributing, Code Of Conduct 2026-01-25 21:27:28 +01:00
MasterAcnolo
ccb16c49d5 Feat: Templates for PR and Issues 2026-01-25 21:22:09 +01:00
MasterAcnolo
86d9ac3592 Update GitHub username for SpicyFire21 2026-01-25 21:08:33 +01:00
MasterAcnolo
43d87c2b47 Enhance README with detailed features list
Expanded features section with detailed descriptions of new functionalities including multi-format downloads, video information preview, quality selection, and more.
2026-01-25 21:04:38 +01:00
MasterAcnolo
19aa8ed8fa Revise README for improved structure and clarity
Updated README with new structure and content for clarity and organization.
2026-01-25 20:51:18 +01:00
MasterAcnolo
3ddfaa999e Fix: ReadME Version 1.4.1 2026-01-25 18:05:07 +01:00
MasterAcnolo
3d7f3b7481 1.4.1
1.4.1
2026-01-25 17:55:19 +01:00
MasterAcnolo
cfe2e17313 Add: App Version for 1.4.1 2026-01-25 17:45:14 +01:00
MasterAcnolo
dedf10c6b3 Fix: Discord RPC not Stoping #26 2026-01-25 17:32:29 +01:00
MasterAcnolo
ac0b4a0750 1.4.0 - It's time to sleep
1.4.0 - It's time to sleep
2026-01-22 22:09:24 +01:00
MasterAcnolo
29ca2ea496 Fix: Auto Update mispelling (Am a monkey) 2026-01-22 21:39:07 +01:00
MasterAcnolo
2a3905bdfb Add: Video Codec 2026-01-21 20:43:15 +01:00
MasterAcnolo
d3755ab57d Fix: Remove unusued "Settings.svg" icon 2026-01-21 20:25:34 +01:00
MasterAcnolo
9824e107fe Fix: Try to force AAC Audio Codec 2026-01-21 13:29:33 +01:00
MasterAcnolo
e437722937 Merge pull request #27 from MasterAcnolo/sync
Sync
2026-01-21 11:32:19 +01:00
MasterAcnolo
fd8c4ae33c Fix: Check Info config every request 2026-01-21 11:16:53 +01:00
MasterAcnolo
967144bd55 Fix: Config not working propely 2026-01-21 11:08:42 +01:00
MasterAcnolo
cd8eb05020 Fix: Remove Unusued Import 2026-01-21 11:08:20 +01:00
MasterAcnolo
590060e504 Fix: Reference position bottom 2026-01-20 22:34:33 +01:00
MasterAcnolo
26c7d4d2ee Because why not ? 2026-01-20 22:30:41 +01:00
MasterAcnolo
caa987fa42 Fix: Remove unusued files 2026-01-20 22:30:22 +01:00
MasterAcnolo
ccb090e4b2 Add: Settings theme color 2026-01-20 22:29:56 +01:00
MasterAcnolo
8d070ed10d Fix: Neon Theme rename 2026-01-20 22:29:22 +01:00
MasterAcnolo
343dbb26a2 Feat: Settings Panel 2026-01-20 22:28:53 +01:00
MasterAcnolo
fef4f6de36 Sync: Synchro Settings Panel 2026-01-18 14:23:17 +01:00
MasterAcnolo
1a61972750 Add: Settings Icon 2026-01-18 14:11:01 +01:00
MasterAcnolo
45ae5d7f98 Fix: Update Dependencies 2026-01-18 14:09:41 +01:00
MasterAcnolo
fecb123d0e Fix: Indent Index.html 2026-01-18 13:51:17 +01:00
MasterAcnolo
5c62ed081a Fix: Update ReadME Banner Redirection 2026-01-18 11:28:27 +01:00
MasterAcnolo
8f0f31041d Fix: Remove Unusued Dependencies 2026-01-16 09:42:32 +01:00
MasterAcnolo
0e48ab3b4c Fix: Default Theme Info Color 2026-01-16 09:42:09 +01:00
MasterAcnolo
d4c33bcf2d FIx: Updated Appliction Version because i did too much work to be a "minor release" 2026-01-15 22:54:40 +01:00
MasterAcnolo
2f105f5145 Fix: Put config.json file into config folder for better readability + Fix Yt-DLP Path 2026-01-15 22:01:26 +01:00
MasterAcnolo
b7a193b062 Fix: Remove unusued import server.js 2026-01-15 21:23:26 +01:00
MasterAcnolo
65364d3095 Fix: Update Dependencies 2026-01-15 21:22:57 +01:00
MasterAcnolo
c356e2e30d Fix: Using ASAR for every parts of the projects (the server is now in it) and i put the binaries in a /binaries folder. It's better for readability of the resources folder 2026-01-15 21:22:40 +01:00
MasterAcnolo
e45810b54c Add: Better logs. Write logs when a download is init 2026-01-15 16:22:29 +01:00
MasterAcnolo
55db30febc Fix: Remove Useless Console.log 2026-01-15 16:21:43 +01:00
MasterAcnolo
163faec221 Feat: Add Configuration Button. user can edit config.json as wanted. Linked and IPC 2026-01-15 16:01:24 +01:00
MasterAcnolo
5398fed928 Fix: Remove unusued Variable 2026-01-15 10:59:45 +01:00
MasterAcnolo
f959171db5 Feat: Add CustomVideo Codec Choice with verification in config.json 2026-01-15 10:57:37 +01:00
MasterAcnolo
01b6bcd02c Feat: Add Newline YT-DLP Args. Add Toggle Switches for verbose, autodownloadplaylist + Fix Thumbail misspell 2026-01-15 10:00:17 +01:00
MasterAcnolo
0ff4715b35 Fix: Correct Config.json file for packaged version 2026-01-15 09:28:46 +01:00
MasterAcnolo
745a73a5b0 Feat: Add version badge controlled. Before we needed to write with the hand for the front. But now there is an IPC event that provide App Version from package.json 2026-01-15 09:03:50 +01:00
MasterAcnolo
a69114edce Feat: Package Update + Add NSIS Installer Banner 2026-01-14 22:49:34 +01:00
MasterAcnolo
489a4766fa Feat: Path Verification (Security #4) 2026-01-14 21:47:38 +01:00
MasterAcnolo
adcee62386 Fix: ReadME Badge Version 2026-01-14 20:41:55 +01:00
MasterAcnolo
b867d819b7 Fix: Increment App Version for 1.3.2 2026-01-14 14:04:06 +01:00
MasterAcnolo
31d8b20a76 Feat: Implement Config File. Read by Front and Back. It aims to give possibility to disable some features quickly. Currenty: DiscordRPC, AutoCheckInfo, AutoUpdate, Custom TopBar, AddMetadata, Add Thumbail were put and implement. Other need to be implement. That's the begin of Settings Panel 2026-01-14 14:03:32 +01:00
MasterAcnolo
42ae0e7204 Feat: Add Codec H264 to vidéo output 2026-01-14 10:06:17 +01:00
MasterAcnolo
43869420c0 Update: Update Script + Dependencies 2026-01-14 10:04:38 +01:00
MasterAcnolo
6c9d296fbd Add: Some Variables in config.js. For Modules 2025-12-30 11:38:18 +01:00
MasterAcnolo
008428f10a Fix: Remove Unsued Import in Controller 2025-12-28 12:47:00 +01:00
MasterAcnolo
ae7d338474 Add: Rate Limit to front rendering. Using Components for reusability 2025-12-28 12:40:55 +01:00
MasterAcnolo
12ff25cc4b Fix: Error Handling Download Controller 2025-12-27 12:15:28 +01:00
MasterAcnolo
f31767e7e7 Fix: Error Handling Info Controller 2025-12-27 12:14:21 +01:00
MasterAcnolo
7c4f2841e1 Fix: Security #5 2025-12-27 11:41:52 +01:00
MasterAcnolo
a27b3555f6 Potential fix for code scanning alert no. 5: Type confusion through parameter tampering
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-27 11:39:30 +01:00
MasterAcnolo
c451ddd042 Add pull request template for consistent feature submissions 2025-12-20 13:51:55 +01:00
MasterAcnolo
da6311055f Replace feature and version templates in PULL_REQUEST_TEMPLATE directory 2025-12-20 13:46:47 +01:00
MasterAcnolo
945efb2059 Add feature issue template for improved feature requests 2025-12-20 13:46:00 +01:00
MasterAcnolo
ab2881f8e9 Add version template for changelog and roadmap 2025-12-20 13:45:39 +01:00
MasterAcnolo
f23811fff4 Create SECURITY.md 2025-12-20 13:39:50 +01:00
MasterAcnolo
19ab77501b Create CONTRIBUTING.md for project guidelines
Added a comprehensive contributing guide for Freedom Loader, outlining how to report issues, submit pull requests, and improve documentation.
2025-12-20 13:35:21 +01:00
MasterAcnolo
37eeed583a Add Contributor Covenant Code of Conduct
This document outlines the Contributor Covenant Code of Conduct, including our pledge, standards of behavior, enforcement responsibilities, and consequences for violations.
2025-12-20 13:29:21 +01:00
MasterAcnolo
caf08efb90 Update issue templates 2025-12-20 13:25:58 +01:00
MasterAcnolo
7ff998e4df Merge pull request #21 from MasterAcnolo/build
1.3.1
2025-12-20 11:41:36 +01:00
MasterAcnolo
0c1e8fe899 refactor: Remove setupMenu function and its invocation from app initialization 2025-12-20 11:28:04 +01:00
MasterAcnolo
4158e8c6e3 feat: Add version badge 2025-12-20 11:18:54 +01:00
MasterAcnolo
653319e408 style: Update z-index for theme switcher and topbar; add custom scrollbar styles 2025-12-20 10:23:39 +01:00
MasterAcnolo
0a4f14fba7 fix: Update topbar width to use 100% instead of 100vw for better responsiveness 2025-12-20 10:10:42 +01:00
MasterAcnolo
fb4571fc51 Fix: Update release badge to version 1.3.1 in README 2025-12-20 09:54:35 +01:00
MasterAcnolo
c11fd40681 feat: Implement topbar with custom window controls and actions 2025-12-20 09:53:17 +01:00
MasterAcnolo
cbfde8b761 Fix: Startup verif + Update Package 2025-12-20 09:11:25 +01:00
MasterAcnolo
c774821cc4 Refactor badge links in README for improved layout and consistency 2025-12-19 14:21:35 +01:00
MasterAcnolo
2ed15a32bc Refactor dependencies section in README for improved layout and styling 2025-12-19 14:20:39 +01:00
MasterAcnolo
c252853603 Refactor layout of dependencies section in README for improved styling 2025-12-19 14:18:43 +01:00
MasterAcnolo
cfdddec073 Refactor badge links in README for improved readability 2025-12-19 14:18:03 +01:00
MasterAcnolo
97a63ab791 Update version to 1.3.1 and add update script to package.json 2025-12-19 14:15:37 +01:00
MasterAcnolo
a0e00341e1 Add native dependencies check and error handling on startup 2025-12-19 14:11:37 +01:00
MasterAcnolo
26d42491ce Bump version to 1.3.1 and fix Discord RPC state URL; enhance update notification with clickable link to releases 2025-12-19 13:50:17 +01:00
MasterAcnolo
d678ba2f12 Merge Main 2025-12-19 13:47:07 +01:00
MasterAcnolo
55b400eb05 Merge branch 'main' into build 2025-12-19 13:47:06 +01:00
MasterAcnolo
0a6313b1f3 Update Freedom Loader link in README.md 2025-12-16 16:27:51 +01:00
MasterAcnolo
b4312376bb Version 1.3.0
It's Time. Remember The Goal.
2025-11-21 14:10:26 +01:00
MasterAcnolo
5ffb143a1a Size Reduce README 2025-11-21 13:54:16 +01:00
MasterAcnolo
ee24473077 Size Reduce README 2025-11-21 13:53:55 +01:00
MasterAcnolo
2b98a63f92 ReadME Firefox 2025-11-21 13:52:09 +01:00
MasterAcnolo
2e594d87e8 ReadME Firefox 2025-11-21 13:45:29 +01:00
MasterAcnolo
b343c2869d Added Website Top Menu Section 2025-11-21 13:44:32 +01:00
MasterAcnolo
094b93f4fa Update Banner 2025-11-20 21:21:45 +01:00
MasterAcnolo
cdc8a623c0 Fix Wiki Path + Icon + Bring Back Spicy Theme + New banner (Logo) 2025-11-20 21:18:04 +01:00
MasterAcnolo
2b43c00a3a Better RPC 2025-11-20 14:42:52 +01:00
MasterAcnolo
14928798d4 Bring Back Spicy Fire Theme from darkness 2025-11-20 13:53:49 +01:00
MasterAcnolo
e5fb44fc6b ReadME Change Logo 2025-11-20 12:58:08 +01:00
MasterAcnolo
9f9e4cd1d8 ReadME Fix 2025-11-20 12:48:50 +01:00
MasterAcnolo
45fe7336fe New Logo 2025-11-20 12:47:17 +01:00
MasterAcnolo
b909e7d917 main fix 2025-11-20 10:35:50 +01:00
MasterAcnolo
ce62748ade ReadME 2025-11-20 10:28:18 +01:00
MasterAcnolo
d4ecc36f6b ReadME 2025-11-20 10:27:23 +01:00
MasterAcnolo
fee364610e ReadME 2025-11-20 10:26:07 +01:00
MasterAcnolo
492af8525b ReadME 2025-11-20 10:25:19 +01:00
MasterAcnolo
fb58b5ae06 ReadME 2025-11-20 10:24:30 +01:00
MasterAcnolo
577034b0bd English Index.html audio 2025-11-20 10:23:07 +01:00
MasterAcnolo
6fc452edb4 ReadME 2025-11-20 09:55:31 +01:00
MasterAcnolo
086ca49599 ReadME 2025-11-20 09:54:45 +01:00
MasterAcnolo
be1de35da8 ReadME 2025-11-20 09:52:21 +01:00
MasterAcnolo
3c1f57667f ReadME 2025-11-20 09:51:28 +01:00
MasterAcnolo
5997cd20b4 ReadME 2025-11-20 09:51:05 +01:00
MasterAcnolo
6220c9400e ReadME 2025-11-20 09:49:01 +01:00
MasterAcnolo
48a16b927f ReadME 2025-11-20 09:48:21 +01:00
MasterAcnolo
87545d77b4 English 2025-11-20 08:32:38 +01:00
MasterAcnolo
4e0fe29c20 Anglais Fetch Info.js + Removed Files Check 2025-11-19 18:24:50 +01:00
MasterAcnolo
f8e3bc843d Change Language Main.js 2025-11-19 18:09:42 +01:00
MasterAcnolo
b3f1edaabd Better Log Components Path 2025-11-19 16:55:26 +01:00
MasterAcnolo
ef784dd19c Translate English 2025-11-19 16:48:07 +01:00
MasterAcnolo
69bbcee885 NO AID HERE 2025-11-18 21:09:25 +01:00
MasterAcnolo
60b4a99506 NO AID HERE 2025-11-18 18:06:39 +01:00
MasterAcnolo
c1030400f5 Notify Logic 2025-11-18 13:55:56 +01:00
MasterAcnolo
3f508da501 Info Loading Bar 2025-11-17 16:54:11 +01:00
MasterAcnolo
0437cb61a2 Change Error Info Box + Clean HTML 2025-11-17 16:40:05 +01:00
MasterAcnolo
1372582c54 SongBird Theme + Playlist Background For Every Theme 2025-11-17 16:25:32 +01:00
MasterAcnolo
070c3d507f Download Speed 2025-11-17 15:33:25 +01:00
MasterAcnolo
3e9c545952 Clean Code 2025-11-15 23:54:41 +01:00
MasterAcnolo
e3845e1849 Loading Bar taskbar 2025-11-15 23:52:43 +01:00
MasterAcnolo
91703d6a91 Using Download Route MVC Model 2025-11-15 23:34:20 +01:00
MasterAcnolo
cff3101b03 Push 2025-11-15 23:14:05 +01:00
MasterAcnolo
c9a48ec64c Using MVC Model + AutoDeteck Package + Fix Download/Info 2025-11-15 15:06:19 +01:00
MasterAcnolo
e6419a9759 Fix Deno Not Using on Info.js 2025-11-15 01:26:54 +01:00
MasterAcnolo
9d1d2fc7a8 Deno Integré + Progress Bar Fix + Small Clean 2025-11-15 00:50:45 +01:00
MasterAcnolo
cdca4a82ab Removed Cyberpunk | Spicy | Vilbrequin Theme, Info Fix, Fix Progress Bar. Centralized Ressources 2025-11-14 23:41:03 +01:00
MasterAcnolo
c2e4e12512 ProgressBar + Playlist 2025-11-14 18:42:15 +01:00
MasterAcnolo
96c1c6717d Playlist Card + Safe Args 2025-11-14 13:02:55 +01:00
MasterAcnolo
abf776175c UpperCase 2025-11-12 18:23:58 +01:00
MasterAcnolo
d6de184bfd Autoupdate + FreedomKiller Démarrage 2025-11-12 18:00:07 +01:00
MasterAcnolo
12249c4e8a Fix AutoUpdate 2025-11-12 15:58:05 +01:00
MasterAcnolo
f1fbc3f0c7 Version Config Depend PackageJSON 2025-11-12 15:53:06 +01:00
MasterAcnolo
bb8cf54bf9 Auto Update GitHub 2025-11-12 15:33:07 +01:00
MasterAcnolo
dccf8bd786 Change log file path for bug reports
Updated log file path in README for bug reports.
2025-11-12 15:04:01 +01:00
MasterAcnolo
793bb79d76 Fix Logs path readme 2025-11-12 15:03:45 +01:00
MasterAcnolo
f5cdf27ce6 Log Discord RPC 2025-11-12 15:01:10 +01:00
MasterAcnolo
627c56cd25 Discord RPC Fix 2025-11-12 14:59:11 +01:00
MasterAcnolo
783871d131 Documentation Bouton + Package Lock Update 2025-11-12 14:53:49 +01:00
MasterAcnolo
138c05f995 Removed Licence Preload 2025-11-09 15:16:59 +01:00
MasterAcnolo
5009a479af Removed console.log RPC 2025-11-09 15:16:15 +01:00
MasterAcnolo
2a95a10113 Removed Else Statement 2025-11-09 13:21:04 +01:00
MasterAcnolo
9d6065c80a Notify Error + Cookies Gestion 2025-11-09 13:19:54 +01:00
MasterAcnolo
cd8b2bf007 RPC Com 2025-11-09 10:46:33 +01:00
MasterAcnolo
1faf74fbc4 Port Lisible Server.js 2025-11-09 10:46:00 +01:00
MasterAcnolo
09163afbcd YTDLP Path DevMode 2025-11-09 10:44:34 +01:00
MasterAcnolo
405d141055 Fix Double Log Start 2025-11-09 10:40:06 +01:00
MasterAcnolo
599e09fa9f Helpers + Clean Download et Info 2025-11-09 10:35:19 +01:00
MasterAcnolo
4954de5f1a Push avant Clean 2025-11-09 09:04:57 +01:00
MasterAcnolo
ac854ae559 Préparation du Champ de Bataille 2025-11-08 15:11:25 +01:00
MasterAcnolo
9944f05139 ReadME Okay for 1.3 2025-11-08 15:02:05 +01:00
MasterAcnolo
f6558b656a Fix Version Mismatch (1.3.1 -> 1.3.0) 2025-11-08 14:58:29 +01:00
MasterAcnolo
bfbf57ba38 Package UPDATE For 1.3.1 2025-11-08 14:57:45 +01:00
MasterAcnolo
d0af35377c NPM Script Clear 2025-11-08 14:53:39 +01:00
MasterAcnolo
59c263a246 Change Apercu README 2025-11-08 14:12:52 +01:00
MasterAcnolo
42700bd7d8 LatestRelease No Space 2025-11-08 14:09:59 +01:00
MasterAcnolo
5a9b364b4b LatestRelease Space 2025-11-08 14:09:38 +01:00
MasterAcnolo
3619550c74 ReadME Fix 2025-11-08 14:09:04 +01:00
MasterAcnolo
31cc4012ac ReadME Fix 2025-11-08 14:08:42 +01:00
MasterAcnolo
2e329a8584 ReadME Fix 2025-11-08 14:06:57 +01:00
MasterAcnolo
d92dd59f86 ReadME Fix 2025-11-08 14:06:46 +01:00
MasterAcnolo
5bb7ca377d Removed ReadME FR and Rework ReadME 2025-11-08 14:05:15 +01:00
MasterAcnolo
9ee9a3574c Change project license from MIT to GNU License 2025-11-08 10:31:33 +01:00
MasterAcnolo
dc50a44682 Clear NPM Scripts 2025-10-31 18:00:48 +01:00
MasterAcnolo
4be5c14999 Merge pull request #16 from MasterAcnolo/build-branch
1.2.4
2025-09-27 11:15:41 +02:00
MasterAcnolo
b92ffebdb4 YT-DLP Maj Automatique 2025-09-27 11:10:34 +02:00
MasterAcnolo
7b6e48b09e Préparation 1.2.4 2025-09-26 07:50:09 +02:00
MasterAcnolo
368851b4bb Merge pull request #15 from MasterAcnolo/build-branch
1.2.3
2025-09-25 21:51:13 +02:00
MasterAcnolo
fa2da5712d ReadME Update 2025-09-25 21:48:38 +02:00
MasterAcnolo
7df97d70bb YT-DLP Update + Fix Crash 2025-09-25 21:43:14 +02:00
MasterAcnolo
5c5f595829 Fix Spicy Image 2025-09-21 09:57:47 +02:00
MasterAcnolo
5b659e3d8a ffprobe 2025-09-20 18:22:43 +02:00
MasterAcnolo
1a526777cc FFPROBE 2025-09-20 18:22:07 +02:00
MasterAcnolo
b0d99d4c29 Update project purpose in README-FR.md 2025-09-20 17:59:17 +02:00
MasterAcnolo
c7554475a9 Update README.md 2025-09-20 17:58:53 +02:00
MasterAcnolo
cab177e8a7 Merge pull request #14 from MasterAcnolo/build-branch
Build branch
2025-09-20 17:53:59 +02:00
MasterAcnolo
f7244a69ac ReadME Update 2025-09-20 17:42:15 +02:00
MasterAcnolo
c6ed79ed1d Title Window 2025-09-20 17:41:27 +02:00
MasterAcnolo
00e5358bbb Asar 2025-09-20 17:21:38 +02:00
MasterAcnolo
598c35eb54 Asar 2025-09-20 17:00:33 +02:00
MasterAcnolo
00c74ea6c6 Package Update + Notif Fix Issues#13 2025-09-20 16:51:46 +02:00
MasterAcnolo
0c415c9106 Merge pull request #12 from MasterAcnolo/build-branch
1.2.1
2025-09-17 12:29:53 +02:00
MasterAcnolo
b0495d033d FFMPEG Location 2025-09-17 11:10:48 +02:00
MasterAcnolo
d72cc6e0ca Correct Log Folder Name 2025-09-17 10:32:53 +02:00
MasterAcnolo
b59cb9f57f Fix Server.js Path 2025-09-17 10:30:48 +02:00
MasterAcnolo
af743be5b0 GitIgnore Fix 2025-09-13 16:18:17 +02:00
MasterAcnolo
024d039701 Package Updated for 1.2.1 + ReadME Fix 2025-09-13 15:41:19 +02:00
MasterAcnolo
e9a91656bc Package JSON Updated for 1.2.1 2025-08-31 18:09:16 +02:00
MasterAcnolo
3505606b44 ADD Licence 'discordRPC.js' 2025-08-31 18:04:07 +02:00
MasterAcnolo
815abae0a0 YT-DLP Moove 2025-08-31 16:30:54 +02:00
MasterAcnolo
004271241e Merge pull request #11 from MasterAcnolo/1.2
Update 1.2
2025-08-29 13:57:11 +02:00
MasterAcnolo
8295967ff2 ReadME Update 2025-08-29 13:45:52 +02:00
MasterAcnolo
053dca365c Package Update 2025-08-29 13:45:02 +02:00
MasterAcnolo
62110b1a5b Package Update 2025-08-29 13:38:20 +02:00
MasterAcnolo
aae9b2866c Discord RPC 2025-08-29 13:36:36 +02:00
MasterAcnolo
0d8bddbc20 ReadME Update 2025-08-29 12:17:46 +02:00
MasterAcnolo
2a7d61bab7 Inversion ReadME Principal 2025-08-29 12:16:12 +02:00
MasterAcnolo
de71035fa0 NF Theme + Ajustement Other Themes 2025-08-29 10:31:13 +02:00
MasterAcnolo
5368fb9311 Update YT-DLP 2025-08-28 10:37:14 +02:00
MasterAcnolo
3d583a9b6f Retrait '--Restrict Filenames' 2025-08-22 12:23:41 +02:00
MasterAcnolo
94de0a42f1 Merge pull request #6 from MasterAcnolo/1.2
1.1.3
2025-08-21 16:01:34 +02:00
MasterAcnolo
5ba2901273 ReadME Update 2025-08-21 15:50:51 +02:00
MasterAcnolo
f5888b8382 NPM Update 2025-08-21 15:49:09 +02:00
MasterAcnolo
6ece182629 Update YTDLP + New Icon 2025-08-21 11:27:03 +02:00
MasterAcnolo
777257b71d Clarification 2025-08-21 08:45:50 +02:00
MasterAcnolo
df0156d603 Add Metadata / Thumbail
Optimisation + Retries
2025-08-20 14:27:45 +02:00
MasterAcnolo
68cdad06ab Merge pull request #5 from MasterAcnolo/Notification-System
# Freedom Loader - 1.1.2

## Changelog

- Added a “Logs” menu in the toolbar to open the logs folder directly
- Fixed log path for better Windows/Linux compatibility
- Disabled hardware acceleration to improve stability on some systems
- Fixed Electron menu merging logic
- Various minor improvements and bug fixes

## Found a bug or issue?
- Please report it in the GitHub 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  
- Video extraction/splitting using native chapters  
- Parallel downloads  
- Skip sponsored parts automatically  

**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.1.1...1.1.2
2025-08-13 15:28:57 +02:00
MasterAcnolo
ee7c6520b9 ReadME Fix 2025-08-13 15:22:51 +02:00
MasterAcnolo
4ed1609e41 Logs Fix + Logs Button 2025-08-13 15:20:35 +02:00
MasterAcnolo
b64e61d45d Fix Download 2025-08-13 14:56:14 +02:00
MasterAcnolo
b4c2c865d8 Logger Error Fix 2025-08-13 14:34:00 +02:00
MasterAcnolo
e32ffd8fa0 NPM Update 2025-08-13 14:25:33 +02:00
MasterAcnolo
8be6a932e5 NPM Update 2025-08-13 14:22:51 +02:00
MasterAcnolo
6ed59fd272 Merge pull request #4 from MasterAcnolo/Notification-System
Notification system + YT DLP Update
2025-07-20 18:22:10 +02:00
MasterAcnolo
4df1cb207f ReadME Update 2025-07-20 18:17:19 +02:00
MasterAcnolo
d03e3bc7e5 Notif + Update YT DLP 2025-07-20 15:27:40 +02:00
MasterAcnolo
df02acb97f Notification + Open On CLICK 2025-07-03 18:28:56 +02:00
MasterAcnolo
6ef04700ab NPM Update 2025-07-03 15:39:03 +02:00
MasterAcnolo
766694f330 ReadME Fix 2025-07-02 22:38:38 +02:00
MasterAcnolo
93380b2f36 ReadME Fix 2025-07-02 22:38:15 +02:00
MasterAcnolo
8c969d694f ReadME Fix 2025-07-02 22:37:30 +02:00
MasterAcnolo
f2f3d42e8c ReadME Fix 2025-07-02 22:36:53 +02:00
MasterAcnolo
8d8973acfc ReadME Fix 2025-07-02 22:35:59 +02:00
MasterAcnolo
281bff2379 Merge pull request #3 from MasterAcnolo/1.0.2
1.1.1
2025-07-02 22:33:09 +02:00
MasterAcnolo
e30f54050f ReadME FIX 2025-07-02 22:28:30 +02:00
MasterAcnolo
0ae808c1a0 Merge branch '1.0.2' of https://github.com/MasterAcnolo/MP3-Downloader into 1.0.2 2025-07-02 22:27:46 +02:00
MasterAcnolo
2598573844 Package Fix 2025-07-02 22:27:44 +02:00
MasterAcnolo
27782376a3 Merge pull request #2 from MasterAcnolo/1.0.2
1.1
2025-07-02 22:26:28 +02:00
MasterAcnolo
876f7e1db0 Merge branch 'main' into 1.0.2 2025-07-02 22:26:14 +02:00
MasterAcnolo
db654c5f82 1.1 2025-07-02 22:24:46 +02:00
MasterAcnolo
751f3d5c3e Commentary + Custom PATH 2025-07-02 16:50:52 +02:00
MasterAcnolo
2a8d307bc8 Chirac Easter EGG 2025-07-02 16:17:09 +02:00
MasterAcnolo
ec6963cff3 Logs FIX + Bug FIX 2025-07-02 13:49:55 +02:00
MasterAcnolo
196a07977a Package FIX for 1.0.2 2025-07-02 10:50:10 +02:00
MasterAcnolo
f20366f0c7 Logs Fix 2025-07-01 20:34:22 +02:00
MasterAcnolo
f54dc18e85 ReadME Pictures Fix 2025-07-01 20:13:12 +02:00
91 changed files with 7143 additions and 4670 deletions

35
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: "Bug Report"
about: Report something that doesn't work fine
title: "[BUG]"
labels: bug
assignees: "MasterAcnolo"
---
## Check Before Reporting
Make sure this bug doesn't already exist in the issues.
## Describe the Bug
A clear and concise description of the problem.
## Steps to Reproduce
1. Go to…
2. Click on…
3. See the error…
## Expected Behavior
What you expected the program to do.
## Logs
Add logs if possible. Available at `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`.
## App Version
State the version of the app you are using.
## Environment
- Version: [e.g. 1.2.4]
- Browser / Other: [e.g. Chrome, Node.js 20]
- Any others information that can be usefull for me
## Screenshots (if applicable)

View File

@@ -0,0 +1,44 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: "[FEATURE] "
labels: enhancement
assignees: 'MasterAcnolo'
---
## Feature Description
A clear and concise description of the feature you'd like to see.
## Problem it Solves
Describe the problem or limitation this feature would address.
## Proposed Solution
Describe how you envision this feature working.
## Alternative Solutions
Have you considered any alternative approaches? Describe them here.
## Use Case
Provide specific examples of how you would use this feature.
## Implementation Considerations
Are there any technical challenges or dependencies you're aware of?
## Priority
How important is this feature to you?
- [ ] Critical - Blocking my workflow
- [ ] High - Would significantly improve my experience
- [ ] Medium - Nice to have
- [ ] Low - Minor improvement
## Additional Context
Add any other context, mockups, or screenshots about the feature request here.

View File

@@ -0,0 +1,56 @@
---
name: Performance Issue
about: Report slow downloads, high CPU/memory usage, or crashes
title: "[PERFORMANCE] "
labels: performance
assignees: MasterAcnolo
---
## Performance Issue Type
- [ ] Slow download speed
- [ ] High CPU usage
- [ ] High memory usage
- [ ] Application freeze/hang
- [ ] Application crash
- [ ] Other (specify below)
## Description
Clear description of the performance issue.
## System Information
- **Freedom Loader Version**: [e.g. 1.4.1]
- **Windows Version**: [e.g. Windows 11 22H2]
- **CPU**: [e.g. Intel i7-9700K]
- **RAM**: [e.g. 16GB]
- **Storage**: [e.g. SSD/HDD]
- **Other Applications Opened**: [e.g. Discord/Opera GX]
## Reproduction Details
- **Download URL**: [if applicable]
- **File Size/Length**: [e.g. 2GB / 2 hours]
- **Quality Setting**: [e.g. 1080p, Best]
- **Audio Only**: Yes / No
## Expected Performance
What performance did you expect?
## Actual Performance
What performance are you experiencing? (Include metrics if possible: download speed, CPU %, memory usage)
## Logs
Attach logs with verbose mode enabled if possible:
```
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log
```
## Additional Context
Any other relevant information (network speed, antivirus software, other running applications).

30
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: Question or Support
about: Ask a question about using Freedom Loader
title: "[QUESTION] "
labels: question
assignees: 'MasterAcnolo'
---
## Pre-submission Checklist
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/pages/faq)
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/pages/wiki)
- [ ] I have searched existing issues
## Question
What would you like to know?
## What I've Tried
Describe what you've already attempted to solve this yourself.
## Environment (if relevant)
- **Freedom Loader Version**: [e.g. 1.4.1]
## Additional Context
Any other information that might be helpful.

View File

@@ -0,0 +1,60 @@
## Description
Brief description of what this PR does.
## Related Issue
Fixes #(issue number)
Closes #(issue number)
Related to #(issue number)
## Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Code refactoring (no functional changes)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] UI/UX enhancement
## Changes Made
- Change 1
- Change 2
- Change 3
## Testing
- [ ] I have tested this code locally
- [ ] I have tested with different download scenarios (video/audio, playlists)
- [ ] I have checked the application logs for errors
- [ ] I have verified the UI changes (if applicable)
- [ ] I have tested on Windows 10 and/or Windows 11
## Screenshots (if applicable)
**Before:**
[Add screenshot]
**After:**
[Add screenshot]
## Checklist
- [ ] My code follows the project's code style
- [ ] I have performed a self-review of my code
- [ ] I have commented my code where necessary
- [ ] I have updated the documentation (if applicable)
- [ ] My changes generate no new warnings or errors
- [ ] I have not modified the version number (this will be done during release)
## Additional Notes
Any additional information or context about the PR.
---
**Note for Maintainer:** This is a contribution PR and should NOT include version bumps.

View File

@@ -0,0 +1,10 @@
# Freedom Loader - 1.X.Y
## Changelog
-
## Found a bug or issue?
- Please report it in the GitHub Issues section
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.W.X...1.Y.Z

11
.gitignore vendored
View File

@@ -1,8 +1,15 @@
/node_modules /node_modules
/downloads /downloads
/dist /dist
/logs
/out /out
/ressources/ffmpeg.exe
/ressources/ffprobe.exe
/ressources/deno.exe
yt-dlp.exe /theme/**
config/config.dev.json
logs/*.json
logs/*.log

111
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,111 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in the Freedom Loader community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
* Supporting fellow contributors and helping newcomers feel welcome
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address, without their explicit permission
* Sustained disruption of discussions, issues, or pull requests
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Community leaders and project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, including:
- GitHub repository (issues, pull requests, discussions, wiki)
- Project documentation and website
- Official communication channels (Discord, email, etc.)
- Social media accounts representing the project
- Project events and meetings (online or offline)
This Code of Conduct also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at **masteracnolo25@gmail.com**.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of actions.
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Our Commitment to Contributors
At Freedom Loader, we value every contribution—whether it's a bug report, feature suggestion, code improvement, or documentation fix. We strive to:
- Respond to issues and pull requests in a timely manner
- Provide constructive feedback on contributions
- Help new contributors get started
- Maintain a welcoming environment for all skill levels
- Credit contributors appropriately for their work
## Questions and Clarifications
If you have questions about this Code of Conduct or need clarification on what constitutes acceptable behavior, please:
- Open a [discussion](https://github.com/MasterAcnolo/Freedom-Loader/discussions) on GitHub
- Contact the maintainers at masteracnolo25@gmail.com
- Review the [Contributing Guidelines](CONTRIBUTING.md)
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
---
**Remember**: Freedom Loader is built by people who care. Let's keep this community a place where everyone feels welcome to contribute.

164
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,164 @@
# Contributing to Freedom Loader
Welcome! Thanks for your interest in Freedom Loader. Your contributions—bug reports, feature ideas, or code improvements—are always appreciated. Every contribution makes Freedom Loader better.
---
## Quick Links
- [Code of Conduct](CODE_OF_CONDUCT.md)
- [Issue Templates](.github/ISSUE_TEMPLATE/)
- [Pull Request Templates](.github/PULL_REQUEST_TEMPLATE/)
- [Security Policy](SECURITY.md)
---
## Ways to Contribute
### 1. Report a Bug
Found a bug or unexpected behavior? Open an [**issue**](https://github.com/MasterAcnolo/Freedom-Loader/issues) using the **Bug Report** template.
**Please include:**
- Clear description of the bug
- Steps to reproduce
- App version and environment (Windows version, Firefox version if relevant)
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log`
- Screenshots if applicable
### 2. Request a Feature
Have an idea to improve Freedom Loader? Open a [**Feature Request**](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose).
**Tips:**
- Describe the problem it solves
- Explain your proposed solution
- Include use cases and examples
### 3. Submit Code Changes
**Before starting:**
- Check existing issues and PRs to avoid duplicates
- For major changes, open an issue first to discuss
- Follow the project's code style and conventions
**Process:**
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/your-feature-name`
3. Make your changes
4. Test thoroughly on Windows 10/11
5. Commit with clear messages: `git commit -m "Add feature X"`
6. Push to your fork: `git push origin feature/your-feature-name`
7. Open a Pull Request using the appropriate template
**Code Guidelines:**
- Use camelCase for variables and functions
- Comment complex logic
- Keep commits focused and atomic
- Update documentation if needed
- Don't modify version numbers (done during release)
### 4. Improve Documentation
Documentation improvements are always welcome:
- Fix typos or unclear sections
- Add missing examples
- Update outdated information
- Improve README, wiki, or guides
Small contributions matter—don't hesitate to submit documentation PRs.
---
## Development Setup
### Prerequisites
- Node.js 16.x or higher
- npm or yarn
- Git
- Windows 10/11 (for testing)
### Setup
```bash
# Clone your fork
git clone https://github.com/YOUR-USERNAME/Freedom-Loader.git
cd Freedom-Loader
# Install dependencies
npm install
# Run in development mode
npm start
# Build for production
npm run build
```
### Project Structure
```
Freedom-Loader/
├── main.js # Electron main process
├── preload.js # Electron preload script
├── config.js # Global configuration
├── server/ # Express backend
│ ├── routes/ # API routes
│ ├── controller/ # Business logic
│ └── helpers/ # Utility functions
├── public/ # Frontend (HTML, CSS, JS)
└── ressources/ # Binaries (yt-dlp, ffmpeg, etc.)
```
---
## Testing
Before submitting a PR, verify:
- [ ] Download functionality works (video/audio)
- [ ] Playlist downloads work
- [ ] Settings panel functions correctly
- [ ] Custom path selection works
- [ ] No errors in application logs
- [ ] UI changes work on all themes (if applicable)
- [ ] Tested on Windows 10 and/or Windows 11
---
## Pull Request Review Process
1. **Submission**: Open PR with clear description using the template
2. **Review**: Maintainers review code and provide feedback
3. **Updates**: Address feedback and push updates
4. **Approval**: Once approved, PR will be merged
5. **Release**: Changes included in next release
**Response times:**
- Bug fixes: Usually reviewed within 2-3 days
- Features: May take longer depending on complexity
- Documentation: Often reviewed quickly
---
## Code of Conduct
Be respectful and constructive in all interactions. We welcome everyone as long as discussions remain polite and productive.
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
---
## Questions?
- Check the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/faq)
- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki)
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
---
## Credits
Thanks to **Zakaria** for the website icon design.
Check out his work: [IG @designmark_studio](https://www.instagram.com/designmark_studio/)
---
**Thank you for contributing to Freedom Loader!**

View File

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

577
README.md
View File

@@ -1,103 +1,502 @@
# Freedom Loader <div align="center">
![Static Badge](https://img.shields.io/badge/Latest%20Release_-1.0.1-blue?style=flat) ![Static Badge](https://img.shields.io/badge/Build-1.0.2%20Beta-yellow?style=flat) <a href="https://masteracnolo.github.io/Freedom-Loader-Site/"><img
src="./build/banner.png"
alt="Banner"
style="width: 50%;"/></a>
> Ce projet a pour but de permettre le téléchargement de contenu Vidéo/Audio de manière gratuite, open source et libre. Il permettra à terme de choisir ### **A clean, open-source multimedia downloader for Windows**
## 🚀 Fonctionnalités [![Release](https://img.shields.io/badge/Release-1.6.0-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0)
[![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Site/)
[![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
✅ Téléchargement Vidéo (MP4) </div>
✅ Téléchargement Audio (MP3)
✅ Logs détaillés (console + fichiers logs)
✅ Récupération automatique des informations
✅ Téléchargement des playlists
--- ---
## 🔧 Installation ## Overview
Pour installer l'application, il vous suffit de télécharger l'installateur dans la section releases, de prendre la dernière version et de l'exécuter. Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services.
The primary goal is to make media downloading accessible to users who want offline access to their favorite content, particularly in situations where internet connectivity is unreliable or unavailable.
### See related repo:
- **[Website](https://masteracnolo.github.io/Freedom-Loader-Site/)** - Official project website and documentation
- **[Workshop](https://github.com/MasterAcnolo/Freedom-Loader-Workshop)** - Theme creation tool and theme browser
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [Preview](#preview)
- [Configuration](#configuration)
- [Project Structure](#project-structure)
- [Theme Workshop](#Theme-Workshop)
- [Technology Stack](#technology-stack)
- [Development](#development)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [Submitting a community theme](#Submitting-a-community-theme)
- [Support](#support)
- [License](#license)
## Features
### Core Functionality
- **Multi-format downloads** - MP4 video with full metadata or MP3 audio with embedded artwork
- **Video information preview** - Fetch and display video metadata before downloading
- **Quality selection** - Choose from best, medium, lowest quality or specific resolutions (1080p, 720p, 480p)
- **Playlist support** - Download entire playlists with optional auto-download mode
- **Metadata embedding** - Automatic retrieval and embedding of title, artist, duration, and thumbnail
- **Video codec options** - Support for H.264, H.265, AV1, VP9, VP9.2, and Theora codecs
- **Custom output directory** - User-configurable download location with path validation
- **Cookie integration** - Firefox cookie support for accessing age-restricted and member-only content
### User Interface
- **Custom window controls** - Frameless window with custom top bar (optional)
- **Custom themes** — ZIP-based theme system with live preview
- **Theme Workshop** — web-based theme creator and browser
- **Toast notifications** — in-app feedback system
- **Real-time progress tracking** - Live download progress bar with network speed indicator
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
- **Settings panel** - In-app configuration interface with live updates
- **Quick access toolbar** - Direct links to logs, website, wiki, and developer tools
### System Features
- **Automatic updates** - Electron-updater integration with GitHub releases
- **YT-DLP auto-update** - Keeps the download engine up-to-date on startup
- **Structured logging** - Winston-based logging with daily file rotation (7-day retention)
- **Discord Rich Presence** - Optional integration showing current download activity
- **Single instance enforcement** - Prevents multiple app instances from running simultaneously
- **Native dependency validation** - Startup checks for yt-dlp, FFmpeg, FFprobe, and Deno binaries
- **Rate limiting** - Express-based request throttling to prevent server overload
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
- **System notifications** - Windows notifications on download completion (clickable to open folder)
- **Splash screen** — animated loading screen on startup
- **Stop download** — ability to cancel in-progress downloads
- **Playlist folders** — dedicated folder creation per playlist
## Installation
### Requirements
- **Operating System**: Windows 10 or later
- **Browser**: Mozilla Firefox (required for cookie extraction)
- [Download Firefox](https://www.firefox.com)
### Standard Installation
1. Download the latest release from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
2. Run the downloaded `.exe` installer
3. If Windows Defender shows a warning, click "More info" then "Run anyway"
4. Follow the installation wizard
### File Locations
Downloaded files are stored in:
```
C:\Users\[USERNAME]\Downloads\Freedom Loader
```
> [!NOTE]
> *You can change it whenever you want. Just click the "Edit" button in the UI.*
Application logs can be found at:
```
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\
```
## Usage
### Basic Download
1. Launch Freedom Loader
2. Paste a video or playlist URL into the input field
3. Select the desired format (Video MP4 or Audio MP3)
4. Click the download button
5. Monitor progress in real-time
6. Receive a notification when the download completes
### Advanced Configuration
The application supports various configuration options:
- Custom download directory selection
- Discord Rich Presence activation
- Theme customization
- Log level configuration
### Troubleshooting
If you encounter issues:
1. Restart the application
2. Check the logs in `AppData\Local\FreedomLoader\logs\`
3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools"
4. Create an issue on GitHub with logs and reproduction steps
## Preview
<div align="center">
<table>
<tr>
<td width="50%" align="center">
<img src="https://github.com/user-attachments/assets/d4de9079-7c82-44d3-ba22-8246bb467db3" alt="Download Preview" width="75%" />
<p><em>Real-time download progress with network speed indicator</em></p>
</td>
<td width="50%" align="center">
<img src="https://github.com/user-attachments/assets/d59aff95-6754-486a-b880-d70730146dc3" alt="Settings Panel" width="40%" />
<p><em>Settings panel with theme and feature customization</em></p>
</td>
</tr>
</table>
</div>
## Configuration
Freedom Loader can be configured either through the settings panel in the UI or by directly editing the `config.json` file located in the application data directory.
### Configuration File Location
```
%APPDATA%\Freedom Loader\config.json
```
### Available Options
```json
{
"autoUpdate": true,
"discordRPC": true,
"customTopBar": true,
"autoCheckInfo": true,
"addThumbnail": true,
"addMetadata": true,
"verboseLogs": false,
"autoDownloadPlaylist": true,
"createPlaylistFolders": true,
"customCodec": "h264",
"logSystem": true,
"outputTitleCheck": true,
"downloadSystem": true,
"notifySystem": true,
"theme": "dark"
}
```
### Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
| `customTopBar` | boolean | `true` | Use custom application top bar |
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
| `logSystem` | boolean | `true` | Enable application logging |
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
| `downloadSystem` | boolean | `true` | Enable download system |
| `notifySystem` | boolean | `true` | Enable system notifications |
| `theme` | string | `dark` | Current Theme Name |
### Supported Codecs
- `h264` - H.264/AVC (widely compatible, recommended default)
- `h265` - H.265/HEVC (better compression, less compatible)
- `av1` - AV1 (modern codec, excellent compression)
- `av01` - AV1 variant
- `vp9` - VP9 (open format, good quality)
- `vp9.2` - VP9 Profile 2 (HDR support)
- `theora` - Theora (legacy open codec)
> [!NOTE]
> Configuration changes may require an application restart to take effect.
## Project Structure
```
Freedom-Loader/
├── app/ # Electron main process modules
│ ├── autoUpdater.js
│ ├── dependencyCheck.js
│ ├── discordRPC.js
│ ├── ipcHandlers.js
│ ├── pathValidator.js
│ ├── splashManager.js
│ ├── themeManager.js
│ ├── windowManager.js
│ └── ytDlpUpdater.js
├── build/ # Build resources and assets
├── config/ # Configuration files
│ ├── config.default.json
│ └── config.dev.json
├── public/ # Frontend assets and UI
│ ├── index.html
│ ├── splash.html
│ ├── assets/
│ │ ├── icon/ # Application icons
│ │ └── logo/ # Logo assets
│ ├── script/ # Frontend JavaScript modules
│ │ ├── appVersion.js
│ │ ├── clipboardPaste.js
│ │ ├── custompath.js
│ │ ├── customthemes.js
│ │ ├── downloadstatus.js
│ │ ├── fetchinfo.js
│ │ ├── progressBar.js
│ │ ├── settingsPanel.js
│ │ ├── toast.js
│ │ └── topbar.js
│ └── styles/ # CSS stylesheets
│ ├── styles.css
│ ├── variables.css
│ ├── components/ # Component-specific styles
│ │ ├── checkbox.css
│ │ ├── editpathbutton.css
│ │ ├── loader.css
│ │ ├── progressBar.css
│ │ └── toast.css
│ └── layout/ # Layout styles
│ ├── container.css
│ ├── form.css
│ ├── header.css
│ ├── settingsPanel.css
│ ├── topbar.css
│ └── videoinfo.css
├── ressources/ # Internal resources (binaries)
├── server/ # Express backend server
│ ├── logger.js
│ ├── server.js
│ ├── controller/ # Request handlers
│ │ ├── download.controller.js
│ │ └── info.controller.js
│ ├── helpers/ # Utility functions
│ │ ├── buildArgs.helpers.js
│ │ ├── getBrowser.helpers.js
│ │ ├── notify.helpers.js
│ │ ├── parseInfo.helpers.js
│ │ ├── path.helpers.js
│ │ ├── rateLimit.helpers.js
│ │ └── validation.helpers.js
│ ├── routes/ # API route definitions
│ │ ├── download.route.js
│ │ └── info.route.js
│ └── services/ # Business logic layer
│ ├── download.services.js
│ └── info.services.js
├── theme/ # Theme system
│ ├── template.theme.json # Theme template
│ ├── Dark/ # Default dark theme
│ │ └── dark.theme.json
│ └── Light/ # Default light theme
│ └── light.theme.json
├── main.js # Electron main process entry point
├── preload.js # Electron preload script (IPC bridge)
├── config.js # Global configuration loader
├── package.json # Project metadata and dependencies
├── CODE_OF_CONDUCT.md # Community guidelines
├── CONTRIBUTING.md # Contribution guidelines
├── LICENSE # GPLv3 license
└── README.md # This file
```
### Architecture Overview
Freedom Loader uses a client-server architecture within a single Electron application:
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
- **Backend**: Express.js server running locally for download management
- **App modules**: Electron-specific logic (window, IPC, updates, RPC) isolated in `app/`
- **IPC Bridge**: Secure communication via Electron's preload script
- **Logging**: Winston-based structured logging with file rotation
- **Updates**: Automatic checking and installation via electron-updater
## Theme Workshop
Freedom Loader includes a web-based theme creator available at [Freedom Loader Workshop](https://masteracnolo.github.io/Freedom-Loader-Workshop/).
- Create custom themes with a live preview of the actual UI
- Browse and download community themes
- Export themes as `.zip` files ready to drop into the `theme/` folder
- Supports background images, custom colors for every UI element
### Installing a custom theme
1. Download a `.zip` theme file
2. Drop it in the `theme/` folder of your Freedom Loader installation
3. Restart the app — your theme appears in the settings panel
## Technology Stack
### Core Technologies
[![yt-dlp](https://img.shields.io/badge/yt--dlp-cf0000?style=for-the-badge&logo=youtube&logoColor=white)](https://github.com/yt-dlp/yt-dlp)
[![FFmpeg](https://img.shields.io/badge/FFmpeg-007808?style=for-the-badge&logo=ffmpeg&logoColor=white)](https://ffmpeg.org/)
[![Electron](https://img.shields.io/badge/Electron-47848F?style=for-the-badge&logo=electron&logoColor=white)](https://www.electronjs.org/)
[![Node.js](https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/)
### Key Dependencies
- **Electron**: Cross-platform desktop application framework
- **Express**: Web framework for local HTTP server
- **yt-dlp**: Media download engine
- **FFmpeg**: Media processing and conversion
- **Winston**: Logging library
- **discord-rpc**: Discord Rich Presence integration
- **electron-updater**: Automatic application updates
## Development
### Prerequisites
- Node.js 16.x or higher
- npm or yarn
- Git
### Setup
```bash
# Clone the repository
git clone https://github.com/MasterAcnolo/Freedom-Loader.git
cd Freedom-Loader
# Install dependencies
npm install
# Run in development mode
npm start
# Build for production
npm run build
```
### Additional Dependencies
This project relies on several external binaries.
Some of them are **not included** in the public repository and must be added manually.
You must download the required binaries and place them in the `ressources` folder, using the exact filenames listed below.
#### Required binaries
- **Deno**
- Download from: https://sourceforge.net/projects/deno.mirror/files/latest/download
- Rename to: `deno.exe`
- **FFmpeg**
- Download from: https://www.ffmpeg.org/download.html
- Required files:
- `ffmpeg.exe`
- `ffprobe.exe`
- **yt-dlp**
- Already bundled with the project
- No manual installation required
Final folder structure:
```
ressources/
├── deno.exe
├── ffmpeg.exe
├── ffprobe.exe
└── yt-dlp.exe
```
> These binaries are required for the application to start correctly.
> If any of them are missing, an error message will be displayed at application startup.
### Development Guidelines
- Follow existing code style and conventions
- Write clear commit messages
- Test thoroughly before submitting changes
- Update documentation when adding features
- Maintain compatibility with Windows 10+
## Roadmap
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
## Contributing
Contributions are welcome and appreciated. This project benefits from community involvement.
## Submitting a community theme
Want your theme featured in the Workshop? Please **do not open an issue on this repository** for theme submissions.
Head over to the [Freedom Loader Workshop repository](https://github.com/MasterAcnolo/Freedom-Loader-Workshop) and open an issue there with your theme files, preview screenshots, and description.
### Bug Reports
Use the GitHub Issues system and include:
- Clear description of the issue
- Steps to reproduce
- Expected vs actual behavior
- Relevant logs from `AppData\Local\FreedomLoader\logs\`
- Screenshots if applicable
### Feature Requests
Open a feature request issue with:
- Clear description of the proposed feature
- Use cases and benefits
- Potential implementation approach (if applicable)
### Pull Requests
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/your-feature`)
3. Make your changes with clear, descriptive commits
4. Test thoroughly
5. Update documentation as needed
6. Submit a pull request with a detailed description
Please read [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing.
## Support
If you find this project useful and want to support its development:
[![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://paypal.me/axelnicolas25)
Your support helps maintain infrastructure, develop new features, and keep the project free and ad-free.
## License
Freedom Loader is released under the **GNU General Public License v3.0 (GPLv3)**.
You are free to use, modify, and redistribute this software under the terms of the GPL-3.0 license. See the [LICENSE](LICENSE) file for complete details.
## Acknowledgments
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
- [Deno](https://deno.com/) for helping me bypassing challenges
- [Electron](https://www.electronjs.org/) for the desktop application framework
- The open-source community for continuous support and contributions
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
- Zakaria for the website icon
- Jacques Chirac to love Apples
- All users who test, report issues, and help improve the application
--- ---
## ❓ Questions fréquentes <div align="center">
### _Où se situent mes fichiers une fois téléchargés ?_ **Freedom Loader** - Put freedom in your downloads
Dans le dossier Téléchargements (**C:\Users\VOTRE NOM\Downloads**) de votre ordinateur, dans un dossier nommé **Freedom Loader Output** [Website](https://masteracnolo.github.io/Freedom-Loader-Site/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/Freedom-Loader-Site/wiki) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
<br>
### _Une fois le programme installé et lancé, j'ai une erreur_ </div>
Essayez de relancer l'application, de consulter les logs ou d'ouvrir la console de développement du navigateur en faisant le raccourci `CTRL + SHIFT + I` ou via `View > Toggle Developer Tools`.
<!-- ![Exemple Chemin](build/example-developertools.png) -->
<p align="center">
<img src="build/example-developertools.png" alt="Exemple Developer Tools" />
</p>
## 🏗️ Fonctionnalités à venir (Roadmap)
- [ ] Plus de choix de formats
- [ ] Support des sous-titres
- [ ] Meilleure UI / UX
- [ ] Choix des langues
- [ ] Téléchargement dune partie de la vidéo
- [ ] Renommage du fichier de sortie
- [ ] Extraction / découpage de la vidéo via chapitrage natif
- [ ] Téléchargements parallèles
- [ ] Suppression automatique des parties sponsorisées (skip sponsor)
- [ ] Choix personnalisé du chemin de sortie (par défaut **C:\Users\Utilisateur\Downloads**, où un dossier **Freedom Loader Output** sera créé)
---
## 🖼️ Aperçu
<p align="center">
<img src="build/exemple.png" alt="Exemple d'application" />
</p>
> Voici un exemple de lapplication lancée après avoir saisi une URL.
---
## À propos
Ce projet a été réalisé en utilisant [![yt-dlp](https://img.shields.io/badge/yt--dlp-Visit%20Repo-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/yt-dlp/yt-dlp)
Merci à eux de nous permettre dutiliser leur API gratuitement et en open source.
Ce projet est open source et accueille volontiers vos contributions.
Voici comment participer :
- **🪲 Signalement de bugs**
Merci dutiliser le [système dissues GitHub](https://github.com/MasterAcnolo/Freedom-Loader/issues) pour remonter les bugs rencontrés.
Essayez de fournir une reproduction claire et les logs si possible. Les logs se trouvent dans le dossier dinstallation du programme. Exemple de chemin :
`C:\Users\[VOTRE NOM]\Desktop\Freedom Loader\resources\app\logs`
Le fichier aura un nom du type `combined-20xx-xx-xx.log`.
<br>
- **🛠️ Contribution**
Les Pull Requests sont les bienvenues ! Merci de forker le repo et de soumettre votre PR après avoir vérifié le respect des guidelines (tests, style, etc.).
<br>
- **💡 Suggestions de fonctionnalités**
Vous pouvez proposer de nouvelles idées via une [issue de type feature request](https://github.com/MasterAcnolo/Freedom-Loader/issues).
Toute proposition claire et détaillée sera étudiée.
<br>
- **💰 Soutenir le projet**
<p>
Si vous souhaitez mencourager, vous pouvez faire un don sur
<a href="https://paypal.me/axelnicolas25">
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="PayPal">
</a>
</p>
---
## 📄 Licence
Ce projet est sous licence **MIT** — voir le fichier [LICENSE](./LICENSE) pour plus dinformations.
---
## ⚠️ Disclaimer
Jai bien évidemment eu recours à lIA pour maider sur certaines parties du projet. Utilisez lIA avec parcimonie et soyez toujours conscient de ce que vous faites.

157
SECURITY.md Normal file
View File

@@ -0,0 +1,157 @@
# Security Policy
## Supported Versions
We actively maintain and provide security updates for the following versions of Freedom Loader:
| Version | Supported |
| ------- | ------------------ |
| 1.4.x | :white_check_mark: |
| 1.3.x | :white_check_mark: |
| < 1.3.0 | :x: |
**Note**: We recommend always using the latest version to benefit from the most recent security patches and features.
## Reporting a Vulnerability
If you discover a security vulnerability in Freedom Loader, please help us keep the project and its users safe by reporting it responsibly.
### How to Report
**DO NOT** open a public issue for security vulnerabilities.
Instead, please report security issues privately by:
1. **Email**: Send details to **masteracnolo25@gmail.com** with the subject line: `[SECURITY] Vulnerability Report`
2. **Include**:
- Description of the vulnerability
- Steps to reproduce the issue
- Potential impact and severity
- Suggested fix (if available)
- Your contact information for follow-up
### What to Expect
- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours
- **Assessment**: We will assess the vulnerability and determine its severity
- **Updates**: We will keep you informed of our progress
- **Resolution**: We aim to release a fix within 7-14 days for critical vulnerabilities
- **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous)
## Security Best Practices for Users
When using Freedom Loader, we recommend:
### General Security
- **Keep Updated**: Always use the latest version of Freedom Loader
- **Official Sources**: Download only from official releases on [GitHub](https://github.com/MasterAcnolo/Freedom-Loader/releases)
- **Verify Downloads**: Check that installers are properly signed (Windows SmartScreen may show warnings for new releases)
- **Antivirus**: Keep your antivirus software up to date
### Configuration Security
- **Download Path**: Only set download paths within your user directory (`C:\Users\[USERNAME]\...`)
- **Cookies**: Be aware that Firefox cookies are used for authenticationkeep Firefox secure
- **Logs**: Logs may contain sensitive informationavoid sharing them publicly without review
### Privacy Considerations
Freedom Loader respects your privacy:
- **No Data Collection**: We don't collect, store, or transmit your personal data
- **No Telemetry**: No usage tracking or analytics
- **Local Operation**: All downloads are processed locally on your machine
- **Optional Features**: Discord RPC is optional and can be disabled
## Known Security Considerations
### Browser Cookie Access
Freedom Loader accesses Firefox cookies to download protected content. This is:
- **By Design**: Required for age-restricted or member-only content
- **Local Only**: Cookies are read locally and never transmitted
- **User Controlled**: You can control what content you download
### Native Dependencies
Freedom Loader bundles native binaries:
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp)
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
- **Deno**: Official builds from [Deno.land](https://deno.land/)
These dependencies are verified and updated regularly.
### Windows Defender Warnings
New releases may trigger Windows Defender warnings because:
- The application is not yet widely distributed
- Code signing certificates are expensive for open-source projects
This is expected behavior for new releases. The warning will decrease as more users download the software.
## Vulnerability Disclosure Policy
### Our Commitment
We are committed to:
- Responding promptly to security reports
- Working with security researchers to verify and address issues
- Keeping users informed about security updates
- Crediting researchers who report vulnerabilities responsibly
### Disclosure Timeline
1. **Day 0**: Vulnerability reported privately
2. **Day 1-2**: Acknowledgment sent to reporter
3. **Day 3-7**: Vulnerability assessed and fix developed
4. **Day 7-14**: Fix released (critical vulnerabilities prioritized)
5. **Day 14+**: Public disclosure (coordinated with reporter)
### Scope
Security issues we're most interested in:
- **Code Execution**: Arbitrary code execution vulnerabilities
- **Path Traversal**: Issues with file system access controls
- **Injection**: Command injection or similar vulnerabilities
- **Authentication**: Bypass of security controls
- **Data Exposure**: Unintended exposure of sensitive data
### Out of Scope
The following are generally not considered security vulnerabilities:
- Issues requiring physical access to the user's machine
- Social engineering attacks
- Denial of service against third-party services
- Issues in third-party dependencies (report to upstream projects)
- Missing security headers on local HTTP server (no remote access)
## Security Updates
Security updates are released as:
- **Patch Releases**: For critical security fixes (e.g., 1.4.1 1.4.2)
- **Automatic Updates**: Users are notified via the built-in update system
- **Release Notes**: Security fixes are clearly marked in changelog
## Additional Resources
- [Contributing Guidelines](CONTRIBUTING.md)
- [Code of Conduct](CODE_OF_CONDUCT.md)
- [GitHub Security Advisories](https://github.com/MasterAcnolo/Freedom-Loader/security/advisories)
## Contact
For security-related questions or concerns:
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
---
**Thank you for helping keep Freedom Loader and its users safe!**

83
app/autoUpdater.js Normal file
View File

@@ -0,0 +1,83 @@
const { autoUpdater } = require("electron-updater");
const { dialog } = require("electron");
const { logger } = require("../server/logger");
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
function initAutoUpdater(mainWindow) {
autoUpdater.on("update-available", async (info) => {
logger.info(`Update available: ${info.version}`);
const { response } = await dialog.showMessageBox(mainWindow, {
type: "info",
title: "Update Available",
message: `Version ${info.version} is available.`,
detail: "Would you like to download and install it now?",
buttons: ["Install Update", "Maybe Later"],
defaultId: 0,
cancelId: 1,
});
if (response === 0) {
autoUpdater.downloadUpdate();
} else {
mainWindow?.webContents.executeJavaScript(
`window.showUpdateBadge && window.showUpdateBadge("${info.version}")`
);
}
});
autoUpdater.on("download-progress", (progress) => {
logger.info(`Download progress: ${Math.round(progress.percent)}%`);
mainWindow?.webContents.send("update-progress", {
percent: Math.round(progress.percent),
speed: progress.bytesPerSecond,
});
});
autoUpdater.on("update-downloaded", async (info) => {
logger.info(`Update downloaded: ${info.version}`);
const { response } = await dialog.showMessageBox(mainWindow, {
type: "info",
title: "Update Ready",
message: `Version ${info.version} has been downloaded.`,
detail: "The application will restart to apply the update.",
buttons: ["Install Now", "Later"],
defaultId: 0,
cancelId: 1,
});
if (response === 0) autoUpdater.quitAndInstall();
});
autoUpdater.on("error", (err) => {
logger.error("Auto update error:", err.message);
dialog.showErrorBox("Update Error", err.message);
});
checkForUpdates();
}
async function checkForUpdates() {
try {
await autoUpdater.checkForUpdates();
} catch (err) {
logger.error("Update check failed:", err.message);
}
}
async function downloadUpdate() {
try {
await autoUpdater.downloadUpdate();
} catch (err) {
logger.error("Download failed:", err.message);
}
}
function installUpdate() {
autoUpdater.quitAndInstall();
}
module.exports = { initAutoUpdater, downloadUpdate, installUpdate };

32
app/dependencyCheck.js Normal file
View File

@@ -0,0 +1,32 @@
const fs = require("fs");
const { app, dialog } = require("electron");
const { logger } = require("../server/logger");
function checkNativeDependencies() {
const { binaryPaths } = require("../server/helpers/path.helpers");
const deps = [
{ name: "yt-dlp.exe", path: binaryPaths.ytDlp },
{ name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
{ name: "ffprobe.exe", path: binaryPaths.ffprobe },
{ name: "deno.exe", path: binaryPaths.deno },
];
const missing = deps.filter(d => !fs.existsSync(d.path));
if (missing.length === 0) return true;
const list = missing.map(d => d.name).join(", ");
logger.error(`Missing dependencies: ${list}`);
app.whenReady().then(() => {
dialog.showErrorBox(
"Missing dependencies",
`The following files are missing in the 'ressources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.`
);
app.quit();
});
return false;
}
module.exports = { checkNativeDependencies };

47
app/discordRPC.js Normal file
View File

@@ -0,0 +1,47 @@
const config = require('../config');
const RPC = require("discord-rpc");
const { logger } = require("../server/logger");
const clientId = `${config.DiscordRPCID}`;
const rpc = new RPC.Client({ transport: "ipc" });
let intervalId;
function startRPC() {
rpc.on("ready", () => {
const presence = {
largeImageKey: "icon",
smallImageKey: "acnolo_pfp",
smallImageText: "By MasterAcnolo",
startTimestamp: new Date(),
details: `Open Source Download Tools - ${config.version}`,
state: "masteracnolo.github.io/FreedomLoader",
};
rpc.clearActivity()
rpc.setActivity(presence);
});
rpc.login({ clientId }).catch(err => {
logger.error("Unable to connect to the RPC:", err);
});
}
async function stopRPC(){
try {
if (intervalId) clearInterval(intervalId);
if (rpc && rpc.transport) {
await rpc.clearActivity()
await rpc.destroy();
} else{
logger.error("Not Able to close RPC")
}
} catch (err) {
logger.error("Error while closing the RPC:", err);
}
}
module.exports = { startRPC, stopRPC};

127
app/ipcHandlers.js Normal file
View File

@@ -0,0 +1,127 @@
const { ipcMain, dialog, shell } = require("electron");
const fs = require("fs");
const path = require("path");
const { logger, logDir } = require("../server/logger");
const { configFeatures, featuresPath } = require("../config");
const { getThemes, reloadThemes } = require("./themeManager");
const config = require("../config");
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
const { userThemesPath } = require("../server/helpers/path.helpers");
const FEATURE_WHITELIST = new Set([
"autoUpdate",
"discordRPC",
"customTopBar",
"autoCheckInfo",
"addThumbnail",
"addMetadata",
"verboseLogs",
"autoDownloadPlaylist",
"customCodec",
"theme",
"createPlaylistFolders",
"notifySystem"
]);
const configFolderPath = featuresPath;
const themeFolderPath = userThemesPath;
function registerIpcHandlers(getMainWindow) {
// Infos générales
ipcMain.handle("version", () => config.version);
ipcMain.handle("features", () => configFeatures);
// Sélection et validation de dossier
ipcMain.handle("select-download-folder", async () => {
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
if (result.canceled) {
logger.info("Folder selection cancelled by user");
return null;
}
try {
const validated = validateDownloadPath(result.filePaths[0]);
logger.info(`Folder selected and validated: ${validated}`);
return validated;
} catch (err) {
logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
throw err;
}
});
ipcMain.handle("validate-download-path", (_, userPath) => validateDownloadPath(userPath));
ipcMain.handle("get-default-download-path", () => getDefaultDownloadPath());
// Progression dans la taskbar
ipcMain.on("set-progress", (_, percent) => {
getMainWindow()?.setProgressBar(percent / 100);
});
// TOPBAR ACTION
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
ipcMain.on("window-maximize", () => {
const win = getMainWindow();
if (!win) return;
win.isMaximized() ? win.unmaximize() : win.maximize();
});
ipcMain.on("window-close", () => getMainWindow()?.close());
ipcMain.on("open-devtools", () =>
getMainWindow()?.webContents.openDevTools({ mode: "detach" })
);
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
ipcMain.on("open-website", () =>
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/")
);
ipcMain.on("open-wiki", () =>
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki")
);
ipcMain.on("open-workshop", () =>
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Workshop")
);
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
// THEME
ipcMain.handle("get-themes", () => getThemes());
ipcMain.on("open-theme", () => shell.openPath(themeFolderPath));
ipcMain.handle("reload-themes", async () => {
return await reloadThemes();
});
// Modification des features
ipcMain.handle("set-feature", (event, { key, value }) => {
try {
if (!FEATURE_WHITELIST.has(key)) {
logger.warn(`Rejected feature (not whitelisted): ${key}`);
return false;
}
if (configFeatures[key] === value) {
return true;
}
configFeatures[key] = value;
fs.writeFileSync(
configFolderPath,
JSON.stringify(configFeatures, null, 2),
"utf-8"
);
logger.info(`Feature updated: ${key} = ${value}`);
return true;
} catch (err) {
logger.error(`set-feature failed (${key}): ${err.message}`);
return false;
}
});
}
module.exports = { registerIpcHandlers };

32
app/pathValidator.js Normal file
View File

@@ -0,0 +1,32 @@
const fs = require("fs");
const path = require("path");
const { logger } = require("../server/logger");
let _defaultPath = null;
function getDefaultDownloadPath() {
if (!_defaultPath) {
const { defaultDownloadFolder } = require("../server/helpers/path.helpers");
_defaultPath = defaultDownloadFolder;
}
return _defaultPath;
}
function validateDownloadPath(userPath) {
const { isSafePath } = require("../server/helpers/validation.helpers");
if (!userPath) return getDefaultDownloadPath();
try {
const resolved = fs.realpathSync(path.resolve(userPath));
if (!isSafePath(resolved)) {
throw new Error("Path not allowed: system folders are blocked!");
}
return resolved;
} catch (err) {
logger.error(`Invalid download path: ${userPath}${err.message}`);
throw new Error(`Invalid or inaccessible path: ${err.message}`);
}
}
module.exports = { validateDownloadPath, getDefaultDownloadPath };

58
app/splashManager.js Normal file
View File

@@ -0,0 +1,58 @@
const { BrowserWindow, app } = require("electron");
const path = require("path");
let splashWindow = null;
function createSplashWindow() {
const splashOptions = {
width: 400,
height: 300,
frame: false,
transparent: true,
alwaysOnTop: true,
resizable: false,
skipTaskbar: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
};
splashWindow = new BrowserWindow(splashOptions);
const splashPath = path.join(__dirname, "../public/splash.html");
splashWindow.loadFile(splashPath);
// Inject banner path for both dev and packaged app
splashWindow.webContents.on('did-finish-load', () => {
let bannerPath;
// Check if app is packaged
if (app.isPackaged) {
bannerPath = path.join(process.resourcesPath, 'banner.png');
} else {
// In dev, use build folder
bannerPath = path.join(__dirname, '../build/banner.png');
}
splashWindow.webContents.executeJavaScript(`
document.querySelector('img[alt="Freedom Loader"]').src = 'file:///${bannerPath.replace(/\\/g, '/')}';
`);
});
}
function closeSplashWindow() {
if (splashWindow) {
splashWindow.close();
splashWindow = null;
}
}
function setSplashProgress(step) {
if (splashWindow) {
splashWindow.webContents.executeJavaScript(`window.setProgress(${step})`);
}
}
module.exports = { createSplashWindow, closeSplashWindow, setSplashProgress };

188
app/themeManager.js Normal file
View File

@@ -0,0 +1,188 @@
const fs = require("fs");
const path = require("path");
const { logger } = require("../server/logger");
const JSZip = require("jszip");
const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"];
const ALLOWED_IMAGE_NAMES = ["cover", "background"];
const THEME_ORDER = ["dark", "light"];
const REQUIRED_KEYS = [
["meta", "name"],
["meta", "author"],
["meta", "version"],
["meta", "formatVersion"],
["style", "colors", "background"],
["style", "colors", "text", "default"],
["style", "form", "button", "background"],
["style", "progressBar", "fill"],
];
function getNestedValue(obj, keys) {
return keys.reduce((acc, key) => acc?.[key], obj);
}
function validateThemeJson(json) {
for (const keyPath of REQUIRED_KEYS) {
if (getNestedValue(json, keyPath) === undefined) {
return { valid: false, reason: `Missing key: ${keyPath.join(".")}` };
}
}
return { valid: true };
}
function buildThemeObject(themeId, themeJson, imageData) {
return {
id: themeId.toLowerCase(),
name: themeJson.meta.name || themeId,
author: themeJson.meta.author || "Unknown",
version: themeJson.meta.version,
subtitle: themeJson.meta.subtitle || "",
style: themeJson.style,
image: imageData,
};
}
function extractImage(buffer, filename) {
if (buffer.length > MAX_IMAGE_SIZE) {
logger.warn(`Image too large, ignoring`);
return null;
}
const ext = path.extname(filename).toLowerCase().replace(".", "");
const mime = ext === "jpg" || ext === "jpeg" ? "jpeg" : ext;
return `data:image/${mime};base64,${buffer.toString("base64")}`;
}
async function loadThemeFromZip(zipPath, themeId) {
try {
const zipBuffer = fs.readFileSync(zipPath);
const zip = await JSZip.loadAsync(zipBuffer);
const jsonFile = Object.keys(zip.files).find(f => f.endsWith(".theme.json"));
if (!jsonFile) { logger.warn(`Theme ${themeId}: no .theme.json, skipping`); return null; }
let themeJson;
try { themeJson = JSON.parse(await zip.files[jsonFile].async("string")); }
catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
const validation = validateThemeJson(themeJson);
if (!validation.valid) { logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`); return null; }
let imageData = null;
const imageFile = Object.keys(zip.files).find(f => {
const ext = path.extname(f).toLowerCase();
const name = path.basename(f, ext).toLowerCase();
return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
});
if (imageFile) {
const buf = await zip.files[imageFile].async("nodebuffer");
imageData = extractImage(buf, imageFile);
}
logger.info(`Theme loaded (zip): ${themeId}`);
return buildThemeObject(themeId, themeJson, imageData);
} catch (err) {
logger.warn(`Theme ${themeId}: failed to load zip — ${err.message}`);
return null;
}
}
async function loadThemeFromFolder(folderPath, themeId) {
try {
const jsonFile = fs.readdirSync(folderPath).find(f => f.endsWith(".theme.json"));
if (!jsonFile) { logger.warn(`Theme ${themeId}: no .theme.json, skipping`); return null; }
let themeJson;
try { themeJson = JSON.parse(fs.readFileSync(path.join(folderPath, jsonFile), "utf-8")); }
catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
const validation = validateThemeJson(themeJson);
if (!validation.valid) { logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`); return null; }
let imageData = null;
const imageFile = fs.readdirSync(folderPath).find(f => {
const ext = path.extname(f).toLowerCase();
const name = path.basename(f, ext).toLowerCase();
return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
});
if (imageFile) {
const buf = fs.readFileSync(path.join(folderPath, imageFile));
imageData = extractImage(buf, imageFile);
}
logger.info(`Theme loaded (folder): ${themeId}`);
return buildThemeObject(themeId, themeJson, imageData);
} catch (err) {
logger.warn(`Theme ${themeId}: failed to load folder — ${err.message}`);
return null;
}
}
let cachedThemes = null;
let themeFolderPath = null;
function setThemeFolderPath(folderPath) {
themeFolderPath = folderPath;
}
async function loadThemesFromFolder() {
const themes = [];
if (!fs.existsSync(themeFolderPath)) {
logger.warn(`Theme folder not found: ${themeFolderPath}`);
return themes;
}
const files = fs.readdirSync(themeFolderPath);
for (const file of files) {
const filePath = path.join(themeFolderPath, file);
const themeId = path.basename(file, path.extname(file));
if (file.endsWith(".zip")) {
const theme = await loadThemeFromZip(filePath, themeId);
if (theme) themes.push(theme);
continue;
}
if (fs.statSync(filePath).isDirectory()) {
const theme = await loadThemeFromFolder(filePath, themeId);
if (theme) themes.push(theme);
continue;
}
}
return themes;
}
function getThemeOrder(id) {
const index = THEME_ORDER.indexOf(id);
return index === -1 ? Infinity : index;
}
async function initThemes(folderPath) {
setThemeFolderPath(folderPath);
const themes = await loadThemesFromFolder();
logger.info(`Themes before sort: ${themes.map(t => t.id).join(", ")}`);
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
logger.info(`Themes after sort: ${cachedThemes.map(t => t.id).join(", ")}`);
}
function getThemes() {
return cachedThemes ?? [];
}
async function reloadThemes() {
const themes = await loadThemesFromFolder();
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
logger.info(`Themes reloaded: ${cachedThemes.length} theme(s)`);
return cachedThemes;
}
module.exports = { initThemes, getThemes , reloadThemes };

58
app/windowManager.js Normal file
View File

@@ -0,0 +1,58 @@
const { BrowserWindow, app } = require("electron");
const path = require("path");
const { logger } = require("../server/logger");
const { configFeatures } = require("../config");
const config = require("../config");
let mainWindow = null;
async function createMainWindow() {
if (mainWindow) {
logger.warn("Window already exists, no new creation!");
return mainWindow;
}
const iconPath = config.devMode
? path.join(__dirname, "../build/app-icon.ico")
: path.join(process.resourcesPath, "build/app-icon.ico");
const windowOptions = {
title: `Freedom Loader ${config.version}`,
icon: iconPath,
width: 750,
height: 800,
minWidth: 750,
minHeight: 800,
frame: !configFeatures.customTopBar,
devTools: !app.isPackaged,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, "../preload.js"),
},
};
mainWindow = new BrowserWindow(windowOptions);
try {
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
logger.info("Window loaded");
} catch (err) {
logger.error("Window loading error:", err);
throw err;
}
mainWindow.on("closed", () => {
logger.info("Main window closed");
mainWindow = null;
});
return mainWindow;
}
function getMainWindow() {
return mainWindow;
}
module.exports = { createMainWindow, getMainWindow };

12
app/ytDlpUpdater.js Normal file
View File

@@ -0,0 +1,12 @@
const { execFile } = require("child_process");
const { logger } = require("../server/logger");
function updateYtDlp(ytDlpPath) {
logger.info("yt-dlp update check starting...");
execFile(ytDlpPath, ["-U"], (err, stdout) => {
if (err) logger.warn("yt-dlp update failed (continuing):", err.message);
else logger.info(`yt-dlp update: ${stdout.trim()}`);
});
}
module.exports = { updateYtDlp };

BIN
build/app-icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
build/banner.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
build/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
build/confirm-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
build/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

45
config.js Normal file
View File

@@ -0,0 +1,45 @@
const packageJson = require("./package.json");
const { app } = require("electron");
const fs = require("fs");
const path = require("path");
const devMode = !app.isPackaged;
function resolveConfigPath() {
if (devMode) {
const devConfigPath = path.join(__dirname, "config", "config.dev.json");
if (!fs.existsSync(devConfigPath)) {
const defaultConfigPath = path.join(__dirname, "config", "config.default.json");
fs.copyFileSync(defaultConfigPath, devConfigPath);
}
return devConfigPath;
}
const userConfigPath = path.join(app.getPath("userData"), "config.json");
if (!fs.existsSync(userConfigPath)) {
const defaultConfigPath = path.join(process.resourcesPath, "config", "config.default.json");
fs.copyFileSync(defaultConfigPath, userConfigPath);
}
return userConfigPath;
}
const featuresPath = resolveConfigPath();
function loadFeatures() {
const raw = fs.readFileSync(featuresPath, "utf-8");
return JSON.parse(raw);
}
const configFeatures = loadFeatures();
module.exports = {
version: packageJson.version,
applicationPort: "8787",
devMode,
DiscordRPCID: "1410934537051181146",
configFeatures,
featuresPath
}

View File

@@ -0,0 +1,17 @@
{
"autoUpdate": true,
"discordRPC": true,
"customTopBar": true,
"autoCheckInfo": true,
"addThumbnail": true,
"addMetadata": true,
"verboseLogs": false,
"autoDownloadPlaylist": true,
"createPlaylistFolders": true,
"customCodec": "h264",
"logSystem": true,
"outputTitleCheck": true,
"downloadSystem": true,
"notifySystem": true,
"theme": "dark"
}

View File

@@ -1,44 +0,0 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
module.exports = {
packagerConfig: {
asar: true,
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {},
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};

133
main.js
View File

@@ -1,79 +1,90 @@
/* process.on("uncaughtException", (err) => {
This file is part of Freedom Loader. console.error("Uncaught exception:", err);
app.quit();
});
Copyright (C) 2025 MasterAcnolo process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
app.quit();
});
Freedom Loader is free software: you can redistribute it and/or modify const { app } = require("electron");
it under the terms of the GNU General Public License as published by const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
the Free Software Foundation, either version 3 of the License.
Freedom Loader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { app, BrowserWindow } = require("electron");
const path = require("path"); const path = require("path");
const { logger, logSessionStart, logSessionEnd } = require("./server/logger"); const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
const { initAutoUpdater } = require("./app/autoUpdater");
const { startRPC, stopRPC } = require("./app/discordRPC");
let mainWindow; const { configFeatures } = require("./config");
const config = require("./config");
const { initThemes } = require("./app/themeManager");
async function createWindow() { const { checkNativeDependencies } = require("./app/dependencyCheck");
logger.info("Creation de la fenetre..."); const { updateYtDlp } = require("./app/ytDlpUpdater");
const { createMainWindow, getMainWindow } = require("./app/windowManager");
if (mainWindow) { const { registerIpcHandlers } = require("./app/ipcHandlers");
logger.warn("La fenetre existe deja, pas de nouvelle creation"); const { userThemesPath, initUserThemes } = require("./server/helpers/path.helpers");
return;
app.setName("Freedom Loader");
app.setAppUserModelId("com.masteracnolo.freedomloader");
app.disableHardwareAcceleration();
if (!config.devMode) {
const gotLock = app.requestSingleInstanceLock();
if (gotLock) {
app.on("second-instance", () => {
logger.info("New instance detected, closing older...");
getMainWindow()?.destroy();
});
} }
mainWindow = new BrowserWindow({
title: "Freedom Loader",
width: 750,
height: 800,
minWidth: 750,
minHeight: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
// titleBarStyle: 'hidden',
});
try {
await mainWindow.loadURL("http://localhost:8080");
logger.info("Fenetre chargee");
} catch (err) {
logger.error("Erreur chargement fenetre:", err);
}
mainWindow.on("closed", () => {
logger.info("Fenetre principale fermee");
mainWindow = null;
});
} }
app.whenReady().then(async () => { app.whenReady().then(async () => {
logSessionStart(); logSessionStart();
logger.info("App prete, demarrage du serveur Express...");
const expressServer = require("./server/server.js"); createSplashWindow();
if (!config.devMode && !checkNativeDependencies()) return;
const { userYtDlp } = require("./server/helpers/path.helpers");
updateYtDlp(userYtDlp);
try { try {
await expressServer.startServer(); setSplashProgress(0); // Checking dependencies
logger.info("Serveur Express demarre"); await require("./server/server").startServer();
await createWindow();
} catch (error) { setSplashProgress(1); // Starting server
logger.error("Erreur serveur ou fenetre :", error); registerIpcHandlers(getMainWindow);
setSplashProgress(2); // Loading themes
initUserThemes();
await initThemes(userThemesPath);
setSplashProgress(3); // Almost ready
await createMainWindow();
await new Promise(resolve => setTimeout(resolve, 2000));
closeSplashWindow();
getMainWindow().show();
if (configFeatures.discordRPC) startRPC();
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
} catch (err) {
logger.error("Boot error:", err);
app.quit(); app.quit();
} }
}); });
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
logger.info("Toutes fenetres fermees, quitte l'app"); logger.info("Shutting down...");
if (process.platform !== "darwin") app.quit(); app.quit();
}); });
app.on("before-quit", () => { app.on("before-quit", async () => {
logSessionEnd(); await stopRPC();
}); logger.info("All services stopped. Have a nice day!");
logSessionEnd();
});

5378
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +1,108 @@
{ {
"name": "freedom-loader", "name": "freedom-loader",
"productName": "Freedom Loader", "productName": "Freedom Loader",
"version": "1.0.1", "version": "1.6.0",
"author": "MasterAcnolo", "author": "MasterAcnolo",
"description": "Freedom Loader, parce que nous le valons bien", "description": "Freedom Loader",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "electron-forge start", "start": "electron --trace-warnings .",
"package": "electron-forge package", "build": "electron-builder",
"make": "electron-forge make", "update": "npm update"
"dist": "electron-builder"
}, },
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
"debug": "^4.4.1", "debug": "^4.4.1",
"electron-squirrel-startup": "^1.0.1", "discord-rpc": "^4.0.1",
"electron-updater": "^6.6.2",
"express": "^5.1.0", "express": "^5.1.0",
"express-rate-limit": "^8.2.1",
"jszip": "^3.10.1",
"webidl-conversions": "^8.0.0",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.8.1", "electron": "^41.2.0",
"@electron-forge/maker-deb": "^7.8.1", "electron-builder": "^26.8.1"
"@electron-forge/maker-rpm": "^7.8.1",
"@electron-forge/maker-squirrel": "^7.8.1",
"@electron-forge/maker-zip": "^7.8.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
"@electron-forge/plugin-fuses": "^7.8.1",
"@electron/fuses": "^1.8.0",
"electron": "^37.1.0",
"electron-builder": "^26.0.12"
}, },
"build": { "build": {
"appId": "com.masteracnolo.freedomloader", "appId": "com.masteracnolo.freedomloader",
"productName": "Freedom Loader", "productName": "Freedom Loader",
"publish": [
{
"provider": "github",
"owner": "MasterAcnolo",
"repo": "Freedom-Loader"
}
],
"files": [ "files": [
"**/*", "**/*",
"node_modules/**/*" "node_modules/**/*",
"!config/config.dev.json",
"!theme/**/*"
], ],
"directories": { "directories": {
"buildResources": "build" "buildResources": "build"
}, },
"asar": false, "asar": true,
"extraResources": [
{
"from": "theme",
"to": "theme"
},
{
"from": "build/confirm-icon.png",
"to": "confirm-icon.png"
},
{
"from": "build/app-icon.ico",
"to": "app-icon.ico"
},
{
"from": "build/banner.bmp",
"to": "banner.bmp"
},
{
"from": "build/banner.png",
"to": "banner.png"
},
{
"from": "build/error.png",
"to": "error.png"
},
{
"from": "ressources/yt-dlp.exe",
"to": "binaries/yt-dlp.exe"
},
{
"from": "ressources/ffmpeg.exe",
"to": "binaries/ffmpeg.exe"
},
{
"from": "ressources/ffprobe.exe",
"to": "binaries/ffprobe.exe"
},
{
"from": "ressources/deno.exe",
"to": "binaries/deno.exe"
},
{
"from": "config/config.default.json",
"to": "config/config.default.json"
}
],
"win": { "win": {
"target": "nsis", "target": "nsis",
"icon": "build/installer-icon.ico" "icon": "build/app-icon.ico"
}, },
"nsis": { "nsis": {
"oneClick": false, "oneClick": false,
"perMachine": true, "perMachine": true,
"allowToChangeInstallationDirectory": true, "allowToChangeInstallationDirectory": true,
"installerIcon": "build/installer-icon.ico", "installerIcon": "build/app-icon.ico",
"uninstallerIcon": "build/uninstaller-icon.ico", "uninstallerIcon": "build/uninstaller-icon.ico",
"installerSidebar": "build/banner.bmp",
"createDesktopShortcut": true, "createDesktopShortcut": true,
"createStartMenuShortcut": true, "createStartMenuShortcut": true,
"shortcutName": "Freedom Loader" "shortcutName": "Freedom Loader"

27
preload.js Normal file
View File

@@ -0,0 +1,27 @@
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
getFeatures: () => ipcRenderer.invoke("features"),
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
getVersion: () => ipcRenderer.invoke("version"),
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path),
getThemes: () => ipcRenderer.invoke("get-themes"),
reloadThemes: () => ipcRenderer.invoke("reload-themes")
});
// Contrôles de fenêtre et outils custom pour la topbar
contextBridge.exposeInMainWorld("topbarAPI", {
minimize: () => ipcRenderer.send("window-minimize"),
maximize: () => ipcRenderer.send("window-maximize"),
close: () => ipcRenderer.send("window-close"),
openDevTools: () => ipcRenderer.send("open-devtools"),
openLogs: () => ipcRenderer.send("open-logs"),
openWebsite: () => ipcRenderer.send("open-website"),
openTheme: () => ipcRenderer.send("open-theme"),
openWiki: () => ipcRenderer.send("open-wiki"),
openWorkshop: () => ipcRenderer.send("open-workshop"),
openConfig: () => ipcRenderer.send("open-config")
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 128C92.7 128 64 156.7 64 192L64 448C64 483.3 92.7 512 128 512L512 512C547.3 512 576 483.3 576 448L576 192C576 156.7 547.3 128 512 128L128 128zM231 231C240.4 221.6 255.6 221.6 264.9 231L319.9 286L374.9 231C384.3 221.6 399.5 221.6 408.8 231C418.1 240.4 418.2 255.6 408.8 264.9L353.8 319.9L408.8 374.9C418.2 384.3 418.2 399.5 408.8 408.8C399.4 418.1 384.2 418.2 374.9 408.8L319.9 353.8L264.9 408.8C255.5 418.2 240.3 418.2 231 408.8C221.7 399.4 221.6 384.2 231 374.9L286 319.9L231 264.9C221.6 255.5 221.6 240.3 231 231z"/></svg>

After

Width:  |  Height:  |  Size: 751 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 128C92.7 128 64 156.7 64 192L64 448C64 483.3 92.7 512 128 512L512 512C547.3 512 576 483.3 576 448L576 192C576 156.7 547.3 128 512 128L128 128zM152 192L488 192C501.3 192 512 202.7 512 216C512 229.3 501.3 240 488 240L152 240C138.7 240 128 229.3 128 216C128 202.7 138.7 192 152 192z"/></svg>

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 480C64 462.3 78.3 448 96 448L544 448C561.7 448 576 462.3 576 480C576 497.7 561.7 512 544 512L96 512C78.3 512 64 497.7 64 480z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -1,3 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" transform="scale(64)" fill="#1B1F23"/>
</svg>

Before

Width:  |  Height:  |  Size: 968 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22C269.4 401.5 288 370.9 288 336s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8l0 128c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376 80 376c-8.8 0-16-7.2-16-16l0-48c0-8.8 7.2-16 16-16l33.4 0 35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3C249.8 304.6 256 319.6 256 336s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6z"/></svg>

Before

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -7,68 +7,306 @@
default-src 'self' 'unsafe-inline' data:; default-src 'self' 'unsafe-inline' data:;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com; font-src https://fonts.gstatic.com;
img-src 'self' data: https://i.ytimg.com; img-src 'self' data: https:;
"> ">
<title>Freedom Loader</title>
<!--CSS-->
<link rel="stylesheet" href="styles/styles.css"> <link rel="stylesheet" href="styles/styles.css">
<link rel="stylesheet" href="styles/layout/topbar.css">
<!--Icons-->
<link rel="icon" href="assets/icons/file-audio-solid.svg" type="image/svg+xml">
</head> </head>
<body> <body>
<div class="container">
<header> <header>
<h1> Freedom Loader </h1> <div class="topbar" id="topbar">
<p> Because why not? </p> <div class="custom-controls">
</header> <button id="devtools-btn" title="Dev Tools">Tools</button>
<button id="logs-btn" title="Logs">Logs</button>
<form action="/download" method="POST" id="downloadForm"> <button id="website-btn" title="Website">Website</button>
<input name="url" placeholder="URL" id="UrlInput"> <button id="wiki-btn" title="Wiki">Wiki</button>
<button id="workshop-btn" title="Workshop">Theme Workshop</button>
<!-- <button id="theme-btn" title="Theme">Theme</button> -->
</div>
<div class="window-controls">
<button id="minimize-btn" title="Réduire"><img src="assets/icon/minimize.svg"></button>
<button id="maximize-btn" title="Maximiser"><img src="assets/icon/maximize.svg"></button>
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
</div>
</div>
</header>
<label class="checkbox-inline"> <button class="settings-button" id="settings-button">
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-q</title><path d="M262.29,192.31a64,64,0,1,0,57.4,57.4A64.13,64.13,0,0,0,262.29,192.31ZM416.39,256a154.34,154.34,0,0,1-1.53,20.79l45.21,35.46A10.81,10.81,0,0,1,462.52,326l-42.77,74a10.81,10.81,0,0,1-13.14,4.59l-44.9-18.08a16.11,16.11,0,0,0-15.17,1.75A164.48,164.48,0,0,1,325,400.8a15.94,15.94,0,0,0-8.82,12.14l-6.73,47.89A11.08,11.08,0,0,1,298.77,470H213.23a11.11,11.11,0,0,1-10.69-8.87l-6.72-47.82a16.07,16.07,0,0,0-9-12.22,155.3,155.3,0,0,1-21.46-12.57,16,16,0,0,0-15.11-1.71l-44.89,18.07a10.81,10.81,0,0,1-13.14-4.58l-42.77-74a10.8,10.8,0,0,1,2.45-13.75l38.21-30a16.05,16.05,0,0,0,6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16,16,0,0,0-6.07-13.94l-38.19-30A10.81,10.81,0,0,1,49.48,186l42.77-74a10.81,10.81,0,0,1,13.14-4.59l44.9,18.08a16.11,16.11,0,0,0,15.17-1.75A164.48,164.48,0,0,1,187,111.2a15.94,15.94,0,0,0,8.82-12.14l6.73-47.89A11.08,11.08,0,0,1,213.23,42h85.54a11.11,11.11,0,0,1,10.69,8.87l6.72,47.82a16.07,16.07,0,0,0,9,12.22,155.3,155.3,0,0,1,21.46,12.57,16,16,0,0,0,15.11,1.71l44.89-18.07a10.81,10.81,0,0,1,13.14,4.58l42.77,74a10.8,10.8,0,0,1-2.45,13.75l-38.21,30a16.05,16.05,0,0,0-6.05,14.08C416.17,247.67,416.39,251.83,416.39,256Z" style="fill:none;stroke:var(--settings-button-text-color);stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
</button>
<label class="checkbox-mp3">
<input type="checkbox" name="audioOnly" value="1"> <div class="settings-panel" id="settings-panel">
<div class="checkmark"></div> <div class="settings-modal">
<button class="close-btn" onclick="closePanel()"></button>
<div class="settings-title">
<h2>SETTINGS</h2>
<p style="font-size: 0.8em;">Changes are saved automatically. <br> Somes changes could need a restart</p>
</div>
<!-- Basic Settings visibles -->
<div class="settings-section">
<h3>Basic Settings</h3>
<div class="setting-item" id="theme" style="margin-left: 32px;">
<div class="setting-info">
<div style="display: flex; align-items: center; gap: 10px;">
<span class="setting-title">Thème</span>
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
</select>
<button id="refresh-themes-btn" title="Refresh themes" class="refresh-themes-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<small>Choose the theme of the application</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="discordRPC" id="discordRPC">
<div class="setting-info">
<span class="setting-title">Discord RPC</span>
<small>Enable/disable Discord Rich Presence for your app.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="autoDownloadPlaylist" id="autoDownloadPlaylist">
<div class="setting-info">
<span class="setting-title">Download Playlist automatically?</span>
<small>If enabled, playlists will download without confirmation.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="createPlaylistFolders" id="createPlaylistFolders">
<div class="setting-info">
<span class="setting-title">Create Playlist Folders</span>
<small>Create a dedicated folder for each playlist with its name.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="notifySystem" id="notifySystem">
<div class="setting-info">
<span class="setting-title">Notification at the end</span>
<small>Notify when a download finishes.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
<div class="setting-info">
<span class="setting-title">Auto Fetch Info</span>
<small>Automatically fetch information for new downloads.</small>
</div>
</div>
</div>
<!-- Advanced Settings -->
<details class="settings-section">
<summary>Advanced Settings</summary>
<div class="setting-item">
<input type="checkbox" data-key="autoUpdate" id="autoUpdate">
<div class="setting-info">
<span class="setting-title">Auto Update</span>
<small>Enable automatic app updates.</small>
</div>
</div>
<!-- <div class="setting-item">
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
<div class="setting-info">
<span class="setting-title">Output title (special chars)</span>
<small>Check for unusual characters in output titles.</small>
</div>
</div> -->
<div class="setting-item">
<input type="checkbox" data-key="addMetadata" id="addMetadata">
<div class="setting-info">
<span class="setting-title">Add Metadata</span>
<small>Include metadata in downloaded files.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="addThumbnail" id="addThumbnail">
<div class="setting-info">
<span class="setting-title">Add Thumbnail</span>
<small>Include thumbnails in downloaded files.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="customTopBar" id="customTopBar">
<div class="setting-info">
<span class="setting-title">Custom Top bar</span>
<small>Use this if the topbar is missing or you prefer a custom one.</small>
</div>
</div>
<div class="setting-item" style="margin-left: 32px; margin-top: 20px;">
<div class="setting-info">
<span class="setting-title">Custom Codec</span>
<select data-key="customCodec">
<option value="h264">H264</option>
<option value="av1">AV1</option>
<option value="av01">AV01</option>
<option value="vp9.2">VP9.2</option>
<option value="vp9">VP9</option>
<option value="h265">H265</option>
<option value="theora">Theora</option>
</select>
<small>Choose your preferred video codec.</small>
</div>
</div>
</details>
<!-- Developer Settings -->
<details class="settings-section">
<summary>Developer Settings</summary>
<!-- <div class="setting-item">
<input type="checkbox" data-key="downloadSystem" id="downloadSystem">
<div class="setting-info">
<span class="setting-title">Download System</span>
<small>Enable/disable the internal download system.</small>
</div>
</div> -->
<!-- <div class="setting-item">
<input type="checkbox" data-key="logSystem" id="logSystem">
<div class="setting-info">
<span class="setting-title">Log System</span>
<small>Enable logging system.</small>
</div>
</div> -->
<div class="setting-item">
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
<div class="setting-info">
<span class="setting-title">Verbose Logs</span>
<small>Show verbose logs for debugging purposes.</small>
</div>
</div>
</details>
<div class="settings-footer">
<button id="open-theme">Open Theme Folder</button>
<button id="open-json-btn">Open JSON Configuration</button>
</div>
</div>
</div>
<main>
<div class="container" id="container">
<h1 id="title"> Freedom Loader </h1>
<p id="subtitle"> Because why not? </p>
<form action="/download" method="POST" id="downloadForm">
<div class="url-input-container">
<input type="text" name="url" placeholder="URL" id="UrlInput">
<button type="button" id="pasteBtn" class="paste-btn" title="Paste from clipboard">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5H7C5.89543 5 5 5.89543 5 7V19C5 20.1046 5.89543 21 7 21H17C18.1046 21 19 20.1046 19 19V7C19 5.89543 18.1046 5 17 5H15M9 5C9 6.10457 9.89543 7 11 7H13C14.1046 7 15 6.10457 15 5M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5M12 12H15M12 16H15M9 12H9.01M9 16H9.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<label class="checkbox-inline">
<label class="checkbox-mp3">
<input type="checkbox" name="audioOnly" value="1">
<div class="checkmark"></div>
</label>
Audio Only (MP3)
</label> </label>
<!-- <input type="checkbox" name="audioOnly" value="1"> --> <select name="quality">
Audio seulement (MP3) <option value="best">Best Quality</option>
</label> <option value="medium">Medium Quality</option>
<!-- <input type="checkbox" name="audioOnly" value="1"> Audio seulement (MP3) --> <option value="worst">Lowest Quality</option>
<select name="quality"> <option value="1080">1080p</option>
<option value="best">Meilleure qualité</option> <option value="720">720p</option>
<option value="medium">Qualité moyenne</option> <option value="480">480p</option>
<option value="worst">Qualité minimale</option> </select>
<option value="1080">1080p</option>
<option value="720">720p</option>
<option value="480">480p</option>
</select>
<button type="submit">Télécharger</button> <button type="submit" onclick="startProgress()">Download</button>
</form> </form>
<div id="downloadStatus"></div> </div>
<!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur <div class="download-path">
les données à traiter. Pour cela on définit une route vers /download, ca va Will be save on <span id="savePath"></span>
être l'espèce de "canal" attribué a ça. Ca va faire que si jamais on veut ensuite recup <button id="changePath">Edit</button>
les infos du form, il faut aller sur ce canal précis (/download). La méthode POST dit </div>
que l'on envoie des données.-->
<div class="progress-container">
<div id="downloadStage" style="display: none; font-size: 0.85rem; color: var(--default-text-color); margin-top: 8px;"></div>
<div id="downloadProgressWrapper">
<div id="downloadProgress"></div>
</div>
<div style="display: flex; gap: 20px; align-items: center;">
<div id="downloadProgressText"></div>
<div id="downloadSpeedText"></div>
<div id="playlistInfoText" style="display: none; font-size: 0.9rem;"></div>
<button type="button" id="stopBtn" onclick="stopDownload()" class="stop-btn" style="display:none;" title="Stop download"></button>
</div>
</div>
</div> <div id="downloadStatus" style="display: none;"></div>
<div class="infos-container">
<div id="loaderBox" class="loader-box" style="display:none;">
<div></div>
<div></div>
<div></div>
<span>Fetching info...</span>
</div>
<div id="videoInfo" style="margin-top: 20px;"></div>
</div>
<span id="version-badge" class="version-badge"></span>
</div>
</main>
<!-- Toast Container -->
<div id="toast-container"></div>
<h2 id="reference"> Because Why Not ? </h2>
<div class="infos-container">
<div id="videoInfo" style="margin-top: 20px;"></div>
</div>
<!-- Scripts -->
<script src="script/toast.js"></script>
<script src="script/clipboardPaste.js"></script>
<script src="script/custompath.js"></script>
<script src="script/settingsPanel.js"></script>
<script src="script/appVersion.js"></script>
<script src="script/downloadstatus.js"></script> <script src="script/downloadstatus.js"></script>
<script src="script/fetchinfo.js"></script> <script src="script/fetchinfo.js"></script>
<script src="script/progressBar.js"></script>
<script src="script/customthemes.js"></script>
<script type="module" src="script/topbar.js"></script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,8 @@
async function versionLabel(){
const appVersion = await window.electronAPI.getVersion();
// Write in front the app version for debugging (bottom right)
document.getElementById("version-badge").textContent = `v${appVersion}`;
};
versionLabel();

View File

@@ -0,0 +1,35 @@
// Clipboard paste functionality for URL input
document.addEventListener('DOMContentLoaded', () => {
const pasteBtn = document.getElementById('pasteBtn');
const urlInput = document.getElementById('UrlInput');
if (pasteBtn && urlInput) {
pasteBtn.addEventListener('click', async () => {
try {
// Read text from clipboard
const text = await navigator.clipboard.readText();
// Paste into the URL input
urlInput.value = text;
// Trigger input event to ensure any listeners are notified
urlInput.dispatchEvent(new Event('input', { bubbles: true }));
// Optional: Focus the input
urlInput.focus();
// Visual feedback
pasteBtn.style.backgroundColor = 'var(--form-button-bg-hover-color)';
setTimeout(() => {
pasteBtn.style.backgroundColor = '';
}, 200);
} catch (err) {
console.error('Failed to read clipboard:', err);
// Fallback: prompt user if clipboard API fails
alert('Unable to access clipboard. Please paste manually using Ctrl+V.');
}
});
}
});

View File

@@ -0,0 +1,60 @@
window.addEventListener("DOMContentLoaded", async () => {
const savePathElem = document.getElementById("savePath");
const form = document.getElementById("downloadForm");
// input caché = DERNIER chemin validé par le back
let hidden = document.getElementById("savePathInput");
if (!hidden) {
hidden = document.createElement("input");
hidden.type = "hidden";
hidden.name = "savePath";
hidden.id = "savePathInput";
form.appendChild(hidden);
}
async function applyPathFromBack(path) {
savePathElem.textContent = path;
savePathElem.title = path;
hidden.value = path;
localStorage.setItem("customDownloadPath", path); // UX only
}
async function loadInitialPath() {
// On affiche ce que le user a vu la dernière fois (UX)
const cached = localStorage.getItem("customDownloadPath");
if (cached) {
savePathElem.textContent = cached;
}
// MAIS la source de vérité reste le back
try {
const validatedPath = await window.electronAPI.getValidatedDownloadPath(
cached
);
await applyPathFromBack(validatedPath);
} catch {
// fallback sûr
const defaultPath =
await window.electronAPI.getDefaultDownloadPath();
await applyPathFromBack(defaultPath);
}
}
await loadInitialPath();
document
.getElementById("changePath")
.addEventListener("click", async () => {
try {
// selectDownloadFolder already returns a validated path
const validatedPath =
await window.electronAPI.selectDownloadFolder();
if (!validatedPath) return; // cancelled
await applyPathFromBack(validatedPath);
} catch (err) {
alert("This folder is not allowed. Only specific folders (Users, Downloads, Documents) are authorized for downloads.");
}
});
});

View File

@@ -0,0 +1,129 @@
const themeSelect = document.getElementById("themeSelect");
let loadedThemes = [];
function applyTheme(theme) {
const root = document.documentElement;
const style = theme.style;
// Background
if (theme.image) {
document.body.style.backgroundImage = `url('${theme.image}')`;
document.body.style.backgroundSize = style.background?.size || "cover";
document.body.style.backgroundPosition = style.background?.position || "center";
document.body.style.backgroundAttachment = style.background?.attachment || "fixed";
} else {
document.body.style.backgroundImage = "";
}
// Colors
root.style.setProperty("--background-color", style.colors?.background || "");
root.style.setProperty("--default-text-color", style.colors?.text?.default || "");
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "");
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "");
// Form
root.style.setProperty("--form-bg-color", style.form?.background || "");
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "");
root.style.setProperty("--form-input-border-color", style.form?.input?.border || "");
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "");
root.style.setProperty("--form-input-text-color", style.form?.input?.text || "");
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "");
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "");
root.style.setProperty("--form-button-text-color", style.form?.button?.text || "");
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "");
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "");
// Checkbox
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "");
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "");
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "");
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "");
// Download
root.style.setProperty("--download-status-color", style.download?.status || "");
// Progress bar
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "");
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "");
// Video info
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "");
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "");
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "");
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "");
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "");
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "");
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "");
// Playlist
root.style.setProperty("--playlist-background-color", style.playlist?.background || "");
// Settings
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "");
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "");
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "");
root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || "");
root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || "");
root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || "");
root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || "");
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "");
root.style.setProperty("--settings-text-color", style.settings?.text || "");
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "");
// Subtitle
const subtitleEl = document.getElementById("subtitle");
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
}
function saveTheme(themeId) {
window.electronAPI.setFeature("theme", themeId);
}
function populateThemeSelect(themes) {
themeSelect.innerHTML = "";
for (const theme of themes) {
const option = document.createElement("option");
option.value = theme.id;
option.textContent = theme.name;
themeSelect.appendChild(option);
}
}
async function initThemes() {
loadedThemes = await window.electronAPI.getThemes();
if (!loadedThemes.length) return;
populateThemeSelect(loadedThemes);
const features = await window.electronAPI.getFeatures();
const savedId = features.theme;
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
themeSelect.value = theme.id;
applyTheme(theme);
}
themeSelect.addEventListener("change", (e) => {
const theme = loadedThemes.find(t => t.id === e.target.value);
if (theme) {
applyTheme(theme);
saveTheme(theme.id);
}
});
async function refreshThemes() {
loadedThemes = await window.electronAPI.reloadThemes();
populateThemeSelect(loadedThemes);
const features = await window.electronAPI.getFeatures();
const savedId = features.theme;
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
themeSelect.value = theme.id;
applyTheme(theme);
}
window.refreshThemes = refreshThemes;
initThemes();

View File

@@ -1,73 +1,127 @@
/*
This file is part of Freedom Loader.
Copyright (C) 2025 MasterAcnolo
Freedom Loader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License.
Freedom Loader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Récupération du formulaire de téléchargement
const form = document.getElementById("downloadForm"); const form = document.getElementById("downloadForm");
// Div où on affiche l'état de progression (en cours, erreur, succès)
const statusDiv = document.getElementById("downloadStatus"); const statusDiv = document.getElementById("downloadStatus");
const button = form.querySelector("button[type=\"submit\"]");
const stopBtn = document.getElementById("stopBtn");
let isDownloading = false;
let userStoppedDownload = false;
// Récupération du bouton de validation pour pouvoir le désactiver pendant la requête
const button = form.querySelector("button");
// On intercepte la soumission du formulaire
form.addEventListener("submit", async (e) => { form.addEventListener("submit", async (e) => {
e.preventDefault(); // empêche le rechargement de la page par le navigateur e.preventDefault();
// On désactive le bouton pour éviter les clics multiples const urlInput = form.querySelector("input[name='url']");
const url = urlInput.value.trim();
// Basic URL validation
if (!url) {
window.showError("Please enter a URL");
return;
}
button.disabled = true; button.disabled = true;
statusDiv.style.display = "none";
isDownloading = true;
userStoppedDownload = false;
// Affiche un message d'attente à l'utilisateur // Show progress UI immediately with initial message
statusDiv.textContent = "Téléchargement en cours..."; showInitialProgress();
// Prépare les données du formulaire sous forme URL-encoded
const formData = new FormData(form); const formData = new FormData(form);
const params = new URLSearchParams(formData); const params = new URLSearchParams(formData);
try { try {
// Envoi de la requête POST vers /download avec les données du formulaire
const res = await fetch("/download", { const res = await fetch("/download", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params, body: params,
}); });
// Si la réponse n'est pas un succès (code 4xx ou 5xx)
if (!res.ok) { if (!res.ok) {
statusDiv.textContent = "❌ Erreur pendant le téléchargement."; if (!userStoppedDownload) {
const errorText = await res.text();
window.showError(errorText);
resetProgressBar();
}
return; return;
} }
// On récupère le texte de la réponse du serveur // Download successful - check if notifications are enabled
const text = await res.text(); const features = await window.electronAPI.getFeatures();
if (features.notifySystem) {
window.showSuccess("Download completed!");
}
// Affiche le message de fin de téléchargement (par exemple « Téléchargement terminé ») } catch (err) {
statusDiv.textContent = text; if (!isDownloading && !userStoppedDownload) {
} catch { statusDiv.style.display = "none";
// Gestion d'erreur en cas de problème réseau ou autre }
statusDiv.textContent = "❌ Une erreur sest produite.";
} finally { } finally {
// Réactive le bouton pour autoriser d'autres soumissions isDownloading = false;
button.disabled = false; button.disabled = false;
stopBtn.style.display = "none";
// Efface le message de status après 5 secondes pour garder l'UI propre
setTimeout(() => {
statusDiv.textContent = "";
}, 5000);
} }
}); });
function showInitialProgress() {
const progressWrapper = document.getElementById("downloadProgressWrapper");
const progressBarText = document.getElementById("downloadProgressText");
const speedElement = document.getElementById("downloadSpeedText");
const stageElement = document.getElementById("downloadStage");
const stopBtn = document.getElementById("stopBtn");
if (progressWrapper) progressWrapper.style.display = "block";
if (progressBarText) {
progressBarText.style.display = "block";
progressBarText.innerHTML = "0%";
}
if (speedElement) {
speedElement.style.display = "block";
speedElement.textContent = "0 Mib/s";
}
if (stageElement) {
stageElement.style.display = "block";
stageElement.textContent = "Retrieving data...";
}
if (stopBtn) stopBtn.style.display = "inline-flex";
}
async function stopDownload() {
if (!isDownloading) return;
userStoppedDownload = true;
button.disabled = true;
stopBtn.disabled = true;
try {
await fetch("/download/cancel", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
window.showWarning("Download stopped");
} catch (err) {
console.error("Error stopping download:", err);
} finally {
isDownloading = false;
button.disabled = false;
stopBtn.disabled = false;
stopBtn.style.display = "none";
resetProgressBar();
}
}
function resetProgressBar() {
const progressWrapper = document.getElementById("downloadProgressWrapper");
if (progressWrapper) progressWrapper.style.display = "none";
const progressBar = document.getElementById("downloadProgress");
if (progressBar) progressBar.style.width = "0%";
const progressBarText = document.getElementById("downloadProgressText");
if (progressBarText) progressBarText.style.display = "none";
const speedElement = document.getElementById("downloadSpeedText");
if (speedElement) speedElement.style.display = "none";
const playlistInfoElement = document.getElementById("playlistInfoText");
if (playlistInfoElement) playlistInfoElement.style.display = "none";
}

View File

@@ -1,114 +1,245 @@
/* function formatDate(dateStr) {
This file is part of Freedom Loader. if (!dateStr || dateStr.length !== 8) return "Inconnue";
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
}
Copyright (C) 2025 MasterAcnolo function formatSize(bytes) {
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
}
Freedom Loader is free software: you can redistribute it and/or modify async function fetchVideoInfo(url) {
it under the terms of the GNU General Public License as published by try {
the Free Software Foundation, either version 3 of the License. const res = await fetch("/info", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ url }),
});
Freedom Loader is distributed in the hope that it will be useful, if (!res.ok) return { error: `An Error occured when fetching info` };
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License const data = await res.json();
along with this program. If not, see <https://www.gnu.org/licenses/>. if (!data) return { error: "Data is Missing" };
*/
document.addEventListener("DOMContentLoaded", () => { return data;
const urlInput = document.getElementById("UrlInput"); } catch (e) {
const infoDiv = document.getElementById("videoInfo"); return { error: "Network or JSON Issue" };
}
}
let lastFetchedUrl = "";
const DEBUG = true;
urlInput.addEventListener("input", async () => { async function init() {
const url = urlInput.value.trim();
if (DEBUG) console.log("[DEBUG] Input détecté :", url);
if (!url || url.length < 5) { document.addEventListener("DOMContentLoaded", () => {
if (DEBUG) console.log("[DEBUG] URL vide ou trop courte, reset affichage."); const urlInput = document.getElementById("UrlInput");
infoDiv.innerHTML = ""; const infoDiv = document.getElementById("videoInfo");
infoDiv.classList.remove("visible"); const loaderBox = document.getElementById("loaderBox");
lastFetchedUrl = ""; const form = document.getElementById("downloadForm");
return;
// Input caché pour stocker le titre de la playlist
let playlistTitleInput = document.getElementById("playlistTitle");
if (!playlistTitleInput) {
playlistTitleInput = document.createElement("input");
playlistTitleInput.type = "hidden";
playlistTitleInput.name = "playlistTitle";
playlistTitleInput.id = "playlistTitle";
form.appendChild(playlistTitleInput);
} }
if (url === lastFetchedUrl) { let lastFetchedUrl = "";
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
return;
}
lastFetchedUrl = url; urlInput.addEventListener("input", async () => {
try { const configFeatures = await window.electronAPI.getFeatures();
if (DEBUG) console.log("[DEBUG] Envoi requête POST vers /info avec URL:", url); if (!configFeatures.autoCheckInfo) return;
const res = await fetch("/info", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ url }),
});
if (DEBUG) console.log("[DEBUG] Réponse status:", res.status); const url = urlInput.value.trim();
if (!res.ok) { // IF field empty -> total reset
console.error(`[ERROR] Réponse HTTP ${res.status}: ${res.statusText}`); if (!url || url.length < 2) {
infoDiv.innerHTML = "❌ Impossible de récupérer les infos (erreur serveur)."; infoDiv.innerHTML = "";
infoDiv.classList.remove("visible"); infoDiv.classList.remove("visible", "playlist-mode");
playlistTitleInput.value = "";
lastFetchedUrl = "";
return; return;
} }
const data = await res.json(); if (url === lastFetchedUrl) return;
if (DEBUG) console.log("[DEBUG] Données reçues:", data); lastFetchedUrl = url;
// Vérification minimaliste loaderBox.style.display = "flex";
if (!data || !data.title || !data.thumbnail) { const data = await fetchVideoInfo(url);
console.warn("[WARNING] Données incomplètes", data); loaderBox.style.display = "none";
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
infoDiv.classList.remove("visible");
if (data.error) {
infoDiv.innerHTML = `
<div style="
padding:7px;
background:var(----infos-box-color);
">
<strong>${data.error}</strong>
</div>
`;
infoDiv.classList.add("visible");
infoDiv.classList.remove("playlist-mode");
playlistTitleInput.value = "";
loaderBox.style.display = "none";
return; return;
} }
const minutes = Math.floor(data.duration / 60); // ---------- PLAYLIST ----------
const seconds = data.duration % 60; if (data.type === "playlist") {
const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`; playlistTitleInput.value = data.title;
infoDiv.classList.add("playlist-mode");
infoDiv.innerHTML = `
<div class="playlist-header">
<div class="playlist-info">
<div class="playlist-badge">Detected Playlist</div>
<h3 class="playlist-title">${data.title}</h3>
<div class="playlist-meta">
<span class="playlist-count">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 6H20M4 12H20M4 18H11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
${data.count} vidéos
</span>
${data.channel ? `<span class="playlist-channel">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="2"/>
<path d="M5 20C5 16.134 8.134 13 12 13C15.866 13 19 16.134 19 20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
${data.channel}
</span>` : ''}
</div>
</div>
</div>
<div id="playlistVideos"></div>
`;
const date = data.upload_date || ""; const listDiv = document.getElementById("playlistVideos");
const readableDate = date
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
: "Inconnue";
const sizeMB = data.filesize_approx data.videos.forEach(v => {
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo" const durationStr = v.duration
: "Inconnue"; ? `${Math.floor(v.duration / 60)}:${(v.duration % 60).toString().padStart(2,"0")}` : "--:--";
const categories = data.categories const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
? data.categories.join(", ")
: "Non spécifiées"; listDiv.innerHTML += `
<div class="playlist-video-card">
<div class="video-thumbnail-wrapper">
<img src="${v.thumbnail}" alt="${v.title}" class="video-thumbnail">
<span class="video-duration">${durationStr}</span>
</div>
<div class="video-content">
<h4 class="video-title" title="${v.title}">${v.title}</h4>
${v.uploader ? `<p class="video-uploader">${v.uploader}</p>` : ''}
<div class="video-actions">
<a href="${videoUrl}" target="_blank" class="video-link">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Ouvrir
</a>
<button class="copy-btn" data-url="${videoUrl}" title="Copier l'URL">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="8" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
</div>
</div>
`;
});
// Copy Button
listDiv.addEventListener("click", (event) => {
if (event.target.classList.contains("copy-btn") || event.target.closest(".copy-btn")) {
const btn = event.target.closest(".copy-btn") || event.target;
if (btn.disabled) return;
btn.disabled = true;
const url = btn.dataset.url;
navigator.clipboard.writeText(url)
.then(() => {
const original = btn.innerHTML;
btn.style.opacity = 0;
btn.style.transform = "scale(0.7)";
setTimeout(() => {
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
btn.style.opacity = 1;
btn.style.transform = "scale(1)";
setTimeout(() => {
btn.style.opacity = 0;
btn.style.transform = "scale(0.7)";
setTimeout(() => {
btn.innerHTML = original;
btn.style.opacity = 1;
btn.style.transform = "scale(1)";
btn.disabled = false;
}, 300);
}, 1000);
}, 300);
})
.catch(() => {
const original = btn.innerHTML;
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
</svg>`;
setTimeout(() => {
btn.innerHTML = original;
btn.disabled = false;
}, 1500);
});
}
});
infoDiv.classList.add("visible");
return;
}
infoDiv.classList.remove("playlist-mode");
playlistTitleInput.value = "";
// ---------- VIDEO NORMALE ----------
const durationStr = data.duration
? `${Math.floor(data.duration/60)}m ${(data.duration%60).toString().padStart(2,"0")}s`
: "Unknown";
const sizeStr = formatSize(data.filesize_approx);
const readableDate = formatDate(data.upload_date);
const categories = data.categories?.join(", ") || "Non spécifiées";
infoDiv.innerHTML = ` infoDiv.innerHTML = `
<h3>${data.title}</h3> <h3>${data.title}</h3>
<img src="${data.thumbnail}" width="320" alt="Thumbnail"> <img src="${data.thumbnail}" width="320" alt="Thumbnail">
<ul> <ul>
<li><strong>Durée :</strong> ${durationStr}</li> <li><strong>Duration :</strong> ${durationStr}</li>
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li> <li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
<li><strong>Date dupload :</strong> ${readableDate}</li> <li><strong>Upload Date :</strong> ${readableDate}</li>
<li><strong>Vues :</strong> ${data.view_count?.toLocaleString() || "?"}</li> <li><strong>Views :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li> <li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</li>
<li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank" rel="noopener noreferrer">${data.webpage_url}</a></li> <li><strong>URL :</strong> <a href="${data.webpage_url}" target="_blank">${data.webpage_url}</a></li>
<li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank" rel="noopener noreferrer">${data.channel_url}</a></li> <li><strong>Channel :</strong> <a href="${data.channel_url}" target="_blank">${data.channel_url}</a></li>
<li><strong>Taille estimée :</strong> ${sizeMB}</li> <li><strong>Estimed Size :</strong> ${sizeStr}</li>
<li><strong>Catégories :</strong> ${categories}</li> <li><strong>Category :</strong> ${categories}</li>
</ul> </ul>
`; `;
infoDiv.classList.add("visible");
if (DEBUG) console.log("[DEBUG] Info affichée avec succès."); infoDiv.classList.add("visible");
} catch (e) { });
console.error("[CRITICAL] Erreur lors de la récupération:", e);
infoDiv.innerHTML = "❌ Erreur réseau ou JSON."; })
infoDiv.classList.remove("visible");
} };
});
}); init();

View File

@@ -0,0 +1,98 @@
const progressWrapper = document.getElementById("downloadProgressWrapper");
const progressBar = document.getElementById("downloadProgress");
const progressBarText = document.getElementById("downloadProgressText")
const speedElement = document.getElementById("downloadSpeedText");
const stageElement = document.getElementById("downloadStage");
const playlistInfoElement = document.getElementById("playlistInfoText");
const speedEvt = new EventSource("/download/speed");
const stageEvt = new EventSource("/download/stage");
const playlistInfoEvt = new EventSource("/download/playlist-info");
function startProgress() {
progressBar.style.width = "0%";
progressBarText.innerHTML = "0%";
speedElement.innerHTML = "0 Mib/s";
}
function showProgress() {
progressWrapper.style.display = "block";
progressBarText.style.display = "block";
speedElement.style.display = "block";
// Show stop button
const stopBtn = document.getElementById("stopBtn");
if (stopBtn) stopBtn.style.display = "inline-flex";
}
function updateProgress(percent) {
// Show progress div and stop button only when percent > 0
if (percent > 0 && progressWrapper.style.display !== "block") {
showProgress();
}
progressBar.style.width = `${percent}%`;
progressBarText.innerHTML = `${percent}%`;
}
function resetProgress() {
progressBar.style.width = "0%";
progressBarText.textContent = "";
progressBarText.style.display = "none";
progressWrapper.style.display = "none";
speedElement.textContent = "0 Mib/s";
speedElement.style.display = "none";
stageElement.textContent = "";
stageElement.style.display = "none";
playlistInfoElement.textContent = "";
playlistInfoElement.style.display = "none";
// Hide stop button
const stopBtn = document.getElementById("stopBtn");
if (stopBtn) stopBtn.style.display = "none";
}
// SSE Connexion
const evtSource = new EventSource("/download/progress");
evtSource.onmessage = e => {
if (e.data === "reset") {
startProgress();
window.electronAPI.setProgress(0);
return;
}
if (e.data === "done") {
// Keep progress bar visible for better UX, let toast handle the feedback
setTimeout(() => {
resetProgress();
window.electronAPI.setProgress(-1);
}, 2000); // Wait 2s so user sees 100% progress
return;
}
const percent = parseFloat(e.data);
if (!isNaN(percent)) {
updateProgress(percent);
window.electronAPI.setProgress(percent); // Update Task Bar
// Don't hide at 100%, wait for "done" signal instead
}
};
speedEvt.onmessage = e => {
speedElement.style.display = "block";
speedElement.textContent = e.data; // ex: "5.2MiB/s"
};
stageEvt.onmessage = e => {
stageElement.style.display = "block";
stageElement.textContent = e.data; // ex: "📥 Downloading..."
};
playlistInfoEvt.onmessage = e => {
playlistInfoElement.style.display = "block";
playlistInfoElement.textContent = e.data; // ex: "Item 1 of 15"
};

View File

@@ -0,0 +1,66 @@
const settingsPanel = document.getElementById("settings-panel");
const settingsModal = document.querySelector(".settings-modal");
const settingsButton = document.getElementById("settings-button");
let isOpen = false;
settingsButton.addEventListener("click", () => {
const isOpen = settingsPanel.style.display === "block";
settingsPanel.style.display = isOpen ? "none" : "block";
});
function closePanel() {
isOpen = false;
settingsPanel.style.display = "none";
}
async function loadSettings() {
const features = await window.electronAPI.getFeatures();
document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => {
const key = el.dataset.key;
if (features[key] !== undefined) {
if (el.type === "checkbox") el.checked = features[key];
else if (el.tagName === "SELECT") el.value = features[key];
}
el.addEventListener("change", () => {
if (key === "theme") return;
let value = el.type === "checkbox" ? el.checked : el.value;
window.electronAPI.setFeature(key, value);
});
});
}
// Open The JSON
document.getElementById("open-json-btn").addEventListener("click", () => {
window.topbarAPI.openConfig();
});
// Open Theme Folder
document.getElementById("open-theme").addEventListener("click", () => {
window.topbarAPI.openTheme();
});
// Refresh Themes
document.getElementById("refresh-themes-btn").addEventListener("click", function() {
this.classList.add("spinning");
window.refreshThemes();
setTimeout(() => {
this.classList.remove("spinning");
}, 600);
});
/* clic sur l'overlay = fermer */
settingsPanel.addEventListener("click", closePanel);
/* clic dans la modal = stop propagation */
settingsModal.addEventListener("click", (e) => {
e.stopPropagation();
});
loadSettings();

54
public/script/toast.js Normal file
View File

@@ -0,0 +1,54 @@
window.showToast = function(message, type = 'info', duration = 4000) {
const container = document.getElementById('toast-container');
if (!container) return;
// Create toast element
const toast = document.createElement('div');
toast.classList.add('toast', type);
// Icons for different types
const icons = {
success: '✓',
error: '✕',
warning: '⚠',
info: ''
};
const icon = icons[type] || icons.info;
toast.innerHTML = `
<span class="toast-icon">${icon}</span>
<span class="toast-message">${message}</span>
<button class="toast-close" title="Close">×</button>
`;
// Add close functionality
const closeBtn = toast.querySelector('.toast-close');
closeBtn.addEventListener('click', () => removeToast(toast));
// Add to container
container.appendChild(toast);
// Auto remove after duration
if (duration > 0) {
setTimeout(() => removeToast(toast), duration);
}
return toast;
};
function removeToast(toast) {
if (!toast.parentElement) return;
toast.classList.add('removing');
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 300);
}
window.showSuccess = (message, duration = 4000) => window.showToast(message, 'success', duration);
window.showError = (message, duration = 5000) => window.showToast(message, 'error', duration);
window.showWarning = (message, duration = 4000) => window.showToast(message, 'warning', duration);
window.showInfo = (message, duration = 4000) => window.showToast(message, 'info', duration);

33
public/script/topbar.js Normal file
View File

@@ -0,0 +1,33 @@
// Ajout des listeners pour la topbar
function setupTopbarListeners() {
const { topbarAPI } = window;
if (!topbarAPI) return;
const minBtn = document.getElementById('minimize-btn');
const maxBtn = document.getElementById('maximize-btn');
const closeBtn = document.getElementById('close-btn');
const devtoolsBtn = document.getElementById('devtools-btn');
const logsBtn = document.getElementById('logs-btn');
const websiteBtn = document.getElementById('website-btn');
const wikiBtn = document.getElementById('wiki-btn');
const workshopBtn = document.getElementById('workshop-btn');
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
}
document.addEventListener('DOMContentLoaded', setupTopbarListeners);
const features = await window.electronAPI.getFeatures();
if(!features.customTopBar){
document.getElementById("topbar").style.display = "none";
document.getElementById("container").style.marginTop = "0";
document.getElementById("theme-switcher").style.top = "30px";
}

94
public/splash.html Normal file
View File

@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
background: transparent;
overflow: hidden;
}
.splash-container {
width: 100%;
height: 100%;
background: #D9D9D9;
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
font-family: "Segoe UI", sans-serif;
}
.progress-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.progress-track {
width: 180px;
height: 3px;
background: #d0d0d0;
border-radius: 99px;
overflow: hidden;
}
.progress-bar {
height: 100%;
width: 0%;
background: #1a1a1a;
border-radius: 99px;
transition: width 0.3s ease;
}
.status {
font-size: 14px;
color: #6d6d6d;
letter-spacing: 0.5px;
}
</style>
</head>
<body>
<div class="splash-container">
<img src="./banner.png" alt="Freedom Loader" style="width: 90%;">
<div class="progress-wrapper">
<div class="progress-track">
<div class="progress-bar" id="progress-bar"></div>
</div>
<span class="status" id="status">Starting...</span>
</div>
</div>
<script>
const steps = [
"Checking dependencies...",
"Starting server...",
"Loading themes...",
"Almost ready...",
];
let current = 0;
const bar = document.getElementById("progress-bar");
const status = document.getElementById("status");
function advance(label, percent) {
bar.style.width = percent + "%";
status.textContent = label;
}
window.setProgress = (step) => {
if (step >= steps.length) {
advance("", 100);
return;
}
const percent = Math.round(((step + 1) / steps.length) * 100);
advance(steps[step], percent);
};
window.setProgress(0);
</script>
</body>
</html>

View File

@@ -15,15 +15,15 @@
} }
.checkmark { .checkmark {
--clr: #3B82F6 ; --clr: var(--checkbox-checked-bg-color);
position: relative; position: relative;
top: 0; top: 0;
left: 0; left: 0;
height: 1.3em; height: 1.3em;
width: 1.3em; width: 1.3em;
background-color: #ccc; background-color: var(--checkbox-unchecked-bg-color);
border-radius: 50%; border-radius: .5rem;
transition: 300ms; transition: background-color 300ms, border-radius 300ms;
} }
.checkbox-mp3 input:checked ~ .checkmark { .checkbox-mp3 input:checked ~ .checkmark {
@@ -47,7 +47,7 @@
top: 0.25em; top: 0.25em;
width: 0.25em; width: 0.25em;
height: 0.5em; height: 0.5em;
border: solid #E0E0E2; border: solid var(--checkbox-checkmark-border-color);
border-width: 0 0.15em 0.15em 0; border-width: 0 0.15em 0.15em 0;
transform: rotate(45deg); transform: rotate(45deg);
} }
@@ -70,4 +70,4 @@
box-shadow: 0 0 0 13px var(--checkbox-pulse-color); box-shadow: 0 0 0 13px var(--checkbox-pulse-color);
rotate: 0; rotate: 0;
} }
} }

View File

@@ -0,0 +1,44 @@
.download-path {
margin-top: 1em;
font-size: 1rem;
color: var(--default-text-color);
}
.download-path #savePath:empty {
display: none;
}
.download-path #savePath {
font-size: 1rem;
font-weight: 600;
background-color: rgba(0, 0, 0, 0.1);
color: var(--form-input-text-color);
padding: 0.3em 0.6em;
border-radius: 6px;
font-family: monospace;
border: 1px dashed var(--form-input-border-color);
display: inline-flex;
max-width: clamp(200px, 60vw, 600px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.download-path button#changePath {
margin-left: 0.5em;
background-color: var(--form-button-bg-color);
color: var(--form-button-text-color);
border: none;
padding: 0.3em 0.8em;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
font-size: 0.9rem;
font-weight: 550;
}
.download-path button#changePath:hover {
background-color: var(--form-button-bg-hover-color);
}

View File

@@ -0,0 +1,34 @@
.loader-box {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
background: var(--form-bg-color);
border: 2px solid var(--audio-only-label-color);
border-radius: 8px;
color: var(--audio-only-label-color);
font-weight: bold;
animation: pulse 1.2s infinite;
}
.loader-box div {
width: 10px;
height: 10px;
background: var(--form-button-bg-color);
border-radius: 3px;
animation: bounce 1.2s infinite;
}
.loader-box div:nth-child(1) { animation-delay: 0s; }
.loader-box div:nth-child(2) { animation-delay: 0.2s; }
.loader-box div:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 5px var(--form-button-bg-color); }
50% { box-shadow: 0 0 20px var(--form-button-bg-color); }
}

View File

@@ -0,0 +1,89 @@
/* Container pour isoler la progress bar */
.progress-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 10px 0;
margin-top: 20px;
}
/* Wrapper de la barre de téléchargement */
#downloadProgressWrapper {
width: 100%;
max-width: 400px;
height: 12px;
background-color: var(--progress-bar-bg-color, #cccccc);
border-radius: 10px;
overflow: hidden;
margin: 0;
display: none;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
box-sizing: border-box;
}
/* Barre de progression */
#downloadProgress {
height: 100%;
width: 0%;
background-color: var(--progress-bar-fill-color, #007bff);
border-radius: 10px;
transition: width 0.2s ease;
}
#downloadProgressText, #downloadSpeedText {
text-align: center;
font-size: 0.8rem;
color: #fff;
content: "";
display: none;
}
/* Responsive pour mobile */
@media (max-width: 480px) {
#downloadProgressWrapper {
max-width: 90%;
height: 10px;
}
#downloadProgressText, #downloadSpeedText {
font-size: 0.7rem;
}
}
/* Stop Button */
.stop-btn {
width: 40px;
height: 40px;
min-width: 40px;
padding: 0;
border: none;
border-radius: 8px;
background-color: var(--form-button-bg-color);
color: var(--form-button-text-color);
font-size: 20px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.stop-btn:hover:not(:disabled) {
background-color: var(--form-button-bg-hover-color);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
transform: scale(1.05);
}
.stop-btn:active:not(:disabled) {
transform: scale(0.96);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.stop-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

View File

@@ -0,0 +1,136 @@
/* Toast Container */
#toast-container {
position: fixed;
top: 140px;
right: 20px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 9999;
pointer-events: none;
}
/* Toast */
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background-color: rgba(50, 50, 50, 0.9);
border-radius: 6px;
color: #ffffff;
font-size: 0.9rem;
max-width: 220px;
min-width: 100px;
animation: slideIn 0.25s ease-out;
pointer-events: auto;
backdrop-filter: blur(8px);
}
.toast.success {
background-color: rgba(40, 167, 69, 0.15);
}
.toast.error {
background-color: rgba(220, 53, 69, 0.15);
}
.toast.warning {
background-color: rgba(255, 193, 7, 0.15);
}
.toast.info {
background-color: rgba(23, 162, 184, 0.15);
}
/* Toast Icon */
.toast-icon {
flex-shrink: 0;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.toast.success .toast-icon {
color: #28a745;
}
.toast.error .toast-icon {
color: #dc3545;
}
.toast.warning .toast-icon {
color: #ffc107;
}
.toast.info .toast-icon {
color: #17a2b8;
}
/* Toast Message */
.toast-message {
flex: 1;
font-weight: 500;
}
/* Toast Close Button */
.toast-close {
flex-shrink: 0;
background: none;
border: none;
color: currentColor;
cursor: pointer;
font-size: 18px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.5;
transition: opacity 0.2s ease;
}
.toast-close:hover {
opacity: 1;
}
/* Animations */
@keyframes slideIn {
from {
transform: translateX(350px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(350px);
opacity: 0;
}
}
.toast.removing {
animation: slideOut 0.25s ease-out;
}
/* Responsive */
@media (max-width: 480px) {
#toast-container {
bottom: 12px;
right: 12px;
left: 12px;
}
.toast {
max-width: none;
min-width: auto;
}
}

View File

@@ -3,10 +3,22 @@
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
/* height: 10000px; */
padding: 1em; padding: 1em;
z-index: 1000;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
#downloadStatus {
margin-top: 1em;
min-height: 1.5em;
font-weight: bold;
text-align: center;
color: var(--download-status-color);
}
label {
display: flex;
align-items: center;
font-size: 0.9em;
}

View File

@@ -1,14 +0,0 @@
#downloadStatus {
margin-top: 1em;
min-height: 1.5em;
font-weight: bold;
text-align: center;
color: #007bff;
}
label {
display: flex;
align-items: center;
font-size: 0.9em;
}

View File

@@ -1,5 +1,5 @@
form { form {
background-color: #fff; background-color: var(--form-bg-color);
padding: 2em; padding: 2em;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1);
@@ -13,8 +13,10 @@ form input[type="text"],
form input[type="url"], form input[type="url"],
form select { form select {
padding: 0.75em; padding: 0.75em;
border: 1px solid #ccc; border: 1px solid var(--form-input-border-color);
border-radius: 8px; border-radius: 8px;
background-color: var(--form-input-bg-color);
color: var(--form-input-text-color);
} }
form input[type="checkbox"] { form input[type="checkbox"] {
@@ -22,8 +24,8 @@ form input[type="checkbox"] {
} }
form button { form button {
background-color: #007bff; background-color: var(--form-button-bg-color);
color: #fff; color: var(--form-button-text-color);
border: none; border: none;
padding: 0.75em; padding: 0.75em;
border-radius: 8px; border-radius: 8px;
@@ -32,42 +34,81 @@ form button {
} }
form button:hover { form button:hover {
background-color: #0056b3; background-color: var(--form-button-bg-hover-color);
} }
form#downloadForm { form#downloadForm {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
max-width: 600px; max-width: 600px;
width: 100%; width: 100%;
justify-content: center; justify-content: center;
} }
.checkbox-inline { .checkbox-inline {
display: flex; display: flex;
align-items: center; align-items: center;
align-self: center; align-self: center;
gap: 0.5em; gap: 0.5em;
color: var(--audio-only-label-color);
} }
#UrlInput { #UrlInput {
padding: 0.75em 1em; padding: 0.75em 1em;
border-color: #ccc; border-color: var(--form-input-border-color);
box-shadow: none; box-shadow: none;
border-radius: 8px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
transition: border-color 0.3s ease, box-shadow 0.3s ease; transition: border-color 0.3s ease, box-shadow 0.3s ease;
background-color: #f9f9f9; background-color: var(--form-input-bg-color);
color: #333; color: var(--form-input-text-color);
flex: 1;
} }
#UrlInput:focus { #UrlInput:focus {
border-color: #0056b3; border-color: var(--form-input-border-focus-color);
box-shadow: 0 0 5px rgba(0,123,255,0.4); box-shadow: 0 0 5px rgba(0,123,255,0.4);
outline: none; outline: none;
background-color: #fff; background-color: var(--form-input-bg-color);
} }
#UrlInput::placeholder { #UrlInput::placeholder {
color: #aaa; color: var(--form-input-placeholder-color);
}
.url-input-container {
display: flex;
align-items: center;
gap: 0.5em;
width: 100%;
}
.paste-btn {
background-color: var(--form-button-bg-color);
color: var(--form-button-text-color);
border: none;
padding: 0.75em;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
height: 44px;
}
.paste-btn:hover {
background-color: var(--form-button-bg-hover-color);
transform: scale(1.05);
}
.paste-btn:active {
transform: scale(0.95);
}
.paste-btn svg {
color: var(--paste-button-icon-color);
width: 30px;
height: 30px;
} }

View File

@@ -1,11 +1,13 @@
header { header {
position: fixed;
top: 0;
width: 100%;
height: 48px;
z-index: 1100;
display: flex; display: flex;
flex-direction: column; align-items: center;
align-items: center;
margin-bottom: 2vw;
} }
#title {
header h1{ color: var(--default-text-color);
margin-bottom: 0.2vw;
} }

View File

@@ -0,0 +1,277 @@
/* SETTINGS BUTTON */
.settings-button {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 60px;
right: 20px;
opacity: 0.8;
z-index: 15;
background-color: var(--settings-button-bg-color);
border-radius: 14px;
padding: 8px;
border: none;
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.settings-button svg {
width: 40px;
height: 40px;
stroke: var(--settings-button-text-color);
}
.settings-button:hover {
transform: scale(1.1);
cursor: pointer;
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
}
.settings-button:active {
transform: scale(0.95);
}
/* SETTINGS PANEL */
.settings-panel {
position: fixed;
top: 60px;
right: 40px;
width: 40vw;
max-width: 320px;
max-height: 80vh;
background-color: var(--settings-modal-bg-color);
border-radius: 12px;
padding: 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
display: none;
overflow-y: auto;
z-index: 9999;
animation: slideIn 0.2s ease-out;
color: var(--settings-text-color);
}
.settings-panel::-webkit-scrollbar {
width: 0px;
background: transparent;
}
.settings-modal {
width: 100%;
min-height: auto;
max-height: none;
padding: 0;
background: none;
box-shadow: none;
border-radius: none;
}
/* TITLE */
.settings-title {
margin-bottom: 20px;
}
.settings-title h2 {
margin: 0;
font-size: 24px;
}
.settings-title p {
font-size: 14px;
color: #ccc;
margin-top: 4px;
}
/* SECTION */
.settings-section {
display: flex;
flex-direction: column;
margin-bottom: 20px;
padding: 10px 15px;
background: var(--settings-section-bg-color);
border-radius: 12px;
gap: 6px;
}
.settings-section h3 {
margin: 0 0 10px 0;
font-size: 18px;
}
/* SETTING ITEM */
.setting-item {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 8px;
margin-top: 8px;
}
.setting-item input[type="checkbox"] {
margin: 0;
flex-shrink: 0;
width: 20px;
height: 20px;
}
.setting-info {
display: flex;
flex-direction: column;
}
.setting-title {
font-size: 14px;
font-weight: 500;
}
.setting-info small {
font-size: 12px;
color: var(--settings-subtitle-color);
margin-top: 2px;
font-style: italic;
}
#theme .settings-title{
display: flex;
}
.setting-item select {
width: 70%;
background: #fff;
border: 1px solid #242424;
border-radius: 6px;
padding: 4px 8px;
color: #000;
cursor: pointer;
font-weight: 600;
transition: background 0.3s ease, color 0.3s ease;
}
.setting-item select:hover,
.setting-item select:focus {
background: #474747;
color: #fff;
outline: none;
}
/* SELECT */
.settings-section select {
margin-left: 10px;
}
/* DETAILS / DROPDOWN */
details {
margin-bottom: 20px;
padding: 10px 15px;
background: #2a2a2a;
border-radius: 12px;
}
details[open] {
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
details summary {
cursor: pointer;
}
/* FOOTER */
.settings-footer {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
#open-json-btn {
padding: 6px 12px;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 600;
background-color: var(--open-json-button-bg-color);
color: var(--open-json-button-text-color);
}
#open-theme {
padding: 10px 14px;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 600;
background-color: var(--open-theme-button-bg-color);
color: var(--open-theme-button-text-color);
}
#experimental-args {
width: 100%;
min-height: 80px;
background-color: #1e1e1e;
border-radius: 8px;
padding: 8px;
color: #fff;
border: 1px solid #333;
resize: vertical;
}
/* CLOSE BTN */
.close-btn {
position: absolute;
top: 20px;
right: 20px;
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
opacity: 0.6;
}
.close-btn:hover {
opacity: 1;
}
/* REFRESH THEMES BUTTON */
.refresh-themes-btn {
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--form-input-border-color);
background-color: var(--form-input-bg-color);
color: var(--form-input-text-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.refresh-themes-btn:hover {
background-color: var(--form-button-bg-hover-color);
}
.refresh-themes-btn.spinning svg {
animation: spin 0.6s ease-in-out;
}
.refresh-themes-btn svg {
color: currentColor;
transition: transform 0.6s ease-in-out;
}
/* ANIMATION */
@keyframes popIn {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes slideIn {
from { transform: translateX(20px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

View File

@@ -0,0 +1,108 @@
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 100%;
background: #1f1f1f98;
color: #eaeaea;
-webkit-app-region: drag;
user-select: none;
}
/* LEFT - custom buttons */
.custom-controls {
display: flex;
align-items: center;
gap: 6px;
padding-left: 12px;
}
/* RIGHT - window buttons */
.window-controls {
display: flex;
align-items: center;
}
/* GLOBAL BUTTON RESET */
.topbar button {
border: none;
background: transparent;
color: inherit;
cursor: pointer;
-webkit-app-region: no-drag;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s ease, transform 0.1s ease;
}
/* Custom buttons (Logs / Wiki / etc.) */
.custom-controls button {
height: 32px;
padding: 0 14px;
font-size: 15px;
border-radius: 8px;
}
.custom-controls button:hover {
background: rgba(255, 255, 255, 0.12);
}
/* Window buttons (Windows style) */
.window-controls button {
width: 46px;
height: 48px;
font-size: 12px;
}
.window-controls button:hover {
background: rgba(255, 255, 255, 0.1);
}
.window-controls img {
width: 27px;
height: 27px;
pointer-events: none;
filter: invert(100%);
}
#close-btn:hover {
background: #e81123;
color: #fff;
}
/* Active effect */
.topbar button:active {
background: rgba(255, 255, 255, 0.18);
transform: scale(0.9);
}
#close-btn:active {
background: #c50f1f;
transform: scale(0.9);
}
.version-badge {
position: fixed;
right: 18px;
bottom: 12px;
z-index: 2000;
font-size: 0.85em;
color: #b0b0b0;
background: none;
font-weight: 500;
border-radius: 6px;
padding: 2px 8px;
opacity: 0.7;
pointer-events: none;
box-shadow: none;
user-select: none;
}

View File

@@ -4,10 +4,10 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding: 1.5em 2em; padding: 1.5em 2em;
background-color: var(--infos-box-color); background-color: var(--video-info-bg-color);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1); box-shadow: 0 8px 16px rgba(0,0,0,0.1);
color: #222; color: var(--video-info-text-color);
font-size: 1rem; font-size: 1rem;
line-height: 1.5; line-height: 1.5;
opacity: 0; opacity: 0;
@@ -21,13 +21,12 @@
#videoInfo.visible { #videoInfo.visible {
opacity: 1; opacity: 1;
max-height: 1500px; max-height: 1500px;
overflow: visible;
} }
#videoInfo h3 { #videoInfo h3 {
font-size: 1.8rem; font-size: 1.8rem;
margin-bottom: 0.6em; margin-bottom: 0.6em;
color: #007bff; color: var(--video-info-heading-color);
font-weight: 700; font-weight: 700;
word-break: break-word; word-break: break-word;
} }
@@ -51,15 +50,15 @@
#videoInfo ul li { #videoInfo ul li {
margin-bottom: 0.5em; margin-bottom: 0.5em;
font-weight: 500; font-weight: 500;
color: #444; color: var(--video-info-list-color);
} }
#videoInfo ul li strong { #videoInfo ul li strong {
color: #000; color: var(--video-info-list-strong-color);
} }
#videoInfo ul li a { #videoInfo ul li a {
color: #007bff; color: var(--video-info-link-color);
text-decoration: none; text-decoration: none;
} }
@@ -68,7 +67,295 @@
text-decoration: underline; text-decoration: underline;
} }
/* --- Playlist Mode --- */
#videoInfo.playlist-mode {
max-width: 95%;
}
/* Playlist Header */
.playlist-header {
display: flex;
flex-direction: column;
padding: 2rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
border-radius: 16px;
margin-bottom: 2rem;
border: 1px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(10px);
}
.playlist-badge {
display: inline-flex;
align-items: center;
width: fit-content;
padding: 0.4rem 0.9rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--video-info-text-color);
margin-bottom: 1rem;
}
.playlist-info {
flex: 1;
}
.playlist-title {
font-size: 1.9rem;
margin: 0 0 0.6rem 0;
color: var(--video-info-heading-color);
font-weight: 700;
line-height: 1.2;
}
.playlist-meta {
display: flex;
gap: 1.5rem;
font-size: 0.95rem;
color: var(--video-info-text-color);
opacity: 0.85;
flex-wrap: wrap;
}
.playlist-count,
.playlist-channel {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.8rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
font-weight: 500;
}
.playlist-count svg,
.playlist-channel svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
/* Layout liste des vidéos */
#playlistVideos {
margin-top: 1em;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
/* Carte vidéo playlist */
.playlist-video-card {
background: var(--playlist-background-color);
border-radius: 14px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
}
.playlist-video-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--video-info-link-color), transparent);
opacity: 0;
transition: opacity 0.25s ease;
}
.playlist-video-card:hover {
transform: translateY(-6px);
box-shadow: 0 12px 32px rgba(0,0,0,0.25);
border-color: rgba(255, 255, 255, 0.15);
}
.playlist-video-card:hover::before {
opacity: 1;
}
/* Thumbnail wrapper avec durée */
.video-thumbnail-wrapper {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
background: #000;
}
.video-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.3s ease;
}
.playlist-video-card:hover .video-thumbnail {
transform: scale(1.05);
}
.video-duration {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 4px 8px;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 700;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
letter-spacing: 0.5px;
backdrop-filter: blur(4px);
}
/* Contenu vidéo */
.video-content {
padding: 1.2rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
flex: 1;
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.02));
}
.video-title {
font-size: 1rem;
font-weight: 600;
margin: 0;
color: var(--video-info-heading-color);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
min-height: 2.8em;
transition: color 0.2s ease;
}
.playlist-video-card:hover .video-title {
color: var(--video-info-link-color);
}
.video-uploader {
font-size: 0.9rem;
color: var(--video-info-text-color);
opacity: 0.7;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Actions vidéo */
.video-actions {
display: flex;
align-items: center;
gap: 0.6rem;
margin-top: auto;
padding-top: 0.8rem;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.video-link {
flex: 1;
text-decoration: none;
color: var(--video-info-link-color);
font-size: 0.9rem;
padding: 0.5rem 0.9rem;
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.2s ease;
text-align: center;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
}
.video-link svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.video-link:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.15);
text-decoration: none;
transform: translateY(-1px);
}
.copy-btn {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
border-radius: 8px;
transition: all 0.2s ease;
width: 40px;
height: 40px;
flex-shrink: 0;
}
.copy-btn svg {
width: 18px;
height: 18px;
color: var(--video-info-text-color);
}
.copy-btn:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
.copy-btn:active {
transform: scale(0.95);
}
.copy-btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* Responsive */ /* Responsive */
@media (max-width: 768px) {
#playlistVideos {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1rem;
}
.playlist-header {
padding: 1.5rem;
}
.playlist-title {
font-size: 1.5rem;
}
.playlist-meta {
gap: 0.8rem;
}
}
@media (max-width: 480px) { @media (max-width: 480px) {
#videoInfo { #videoInfo {
padding: 1em; padding: 1em;
@@ -78,4 +365,21 @@
#videoInfo h3 { #videoInfo h3 {
font-size: 1.4rem; font-size: 1.4rem;
} }
#playlistVideos {
grid-template-columns: 1fr;
}
.playlist-header {
padding: 1.2rem;
}
.playlist-title {
font-size: 1.3rem;
}
.playlist-badge {
font-size: 0.75rem;
padding: 0.4rem 0.8rem;
}
} }

View File

@@ -1,66 +1,120 @@
/* Layout */
@import url("layout/topbar.css");
@import url("layout/header.css");
@import url("layout/form.css"); @import url("layout/form.css");
@import url("layout/container.css"); @import url("layout/container.css");
@import url("layout/ctr-infos.css"); @import url("layout/settingsPanel.css");
@import url("layout/header.css");
@import url("layout/videoinfo.css"); @import url("layout/videoinfo.css");
/* Components */
@import url("components/checkbox.css"); @import url("components/checkbox.css");
@import url("variables.css"); @import url("components/loader.css");
@import url("components/editpathbutton.css");
@import url("components/progressBar.css");
@import url("components/toast.css");
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
/* Because why not */
#reference{
position: fixed;
pointer-events: none;
bottom:0vh;
left: 10px;
font-size: 1em;
color: #b0b0b02f;
z-index: 9999;
}
html,body{ html,body{
margin: 0; margin: 0;
padding: 0; padding: 0;
scroll-behavior: smooth; scroll-behavior: smooth;
/* outline: 2px solid red; */ /*<-- Utile pour le début ça */ /*outline: 2px solid red; /*<-- Utile pour le debug ça */
font-family: "Poppins", sans-serif; font-family: "Poppins", sans-serif;
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
} }
body { body{
margin: 0; background-color: var(--background-color);
background-color: var(--background-color); color: var(--default-text-color);
/* background-image: linear-gradient(to left top, #838383, #a1a1a1, #bfbfbf, #dfdfdf, #ffffff);
background-size: cover;
background-position: center;
background-attachment: fixed; */
width: 100%;
height: 100%;
color: var(--default-text-color);
display: flex;
justify-content: center;
align-items: center;
display: flex;
flex-direction: column;
align-items: center;
} }
header p{ header {
position: fixed;
top: 0;
width: 100%;
z-index: 1100;
height: 48px;
}
main {
width: 100%;
height: calc(100vh - 48px);
margin-top: 48px;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 0;
gap: 0;
}
/* Scrollbar pour main */
main::-webkit-scrollbar {
width: 10px;
height: 10px;
}
main::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.03);
border-radius: 5px;
}
main::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 5px;
border: 2px solid transparent;
background-clip: padding-box;
transition: background-color 0.2s ease, transform 0.2s ease;
}
main::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.35);
transform: scaleX(1.05);
}
/* Thème clair */
body.light main::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
}
body.light main::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.25);
}
body.light main::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.45);
}
.container{
margin-top: 0px;
}
#subtitle{
color: var(--subtitle-color); color: var(--subtitle-color);
} }
.checkbox-inline{ body,
color: var(--mp3-text-color); .container,
} input,
button,
/* #videoInfo { select,
margin-top: 100px; label {
padding: 1em; transition: background-color 0.2s ease, color 0.5s ease, border-color 0.2s ease;
max-width: 600px; }
margin-left: auto;
margin-right: auto;
border-radius: 8px;
color: rgb(0, 0, 0);
box-shadow: 0 0 10px rgba(0,0,0,0.5);
transition: opacity 0.5s ease, max-height 0.5s ease;
opacity: 0;
max-height: 0;
overflow: hidden;
}
#videoInfo.visible {
opacity: 1;
max-height: 1000px;
} */

View File

@@ -1,11 +1,54 @@
:root{ :root {
--background-color : #0F172A; /* Couleurs générales */
--infos-box-color: #f1f1f1 ; --background-color: #001224;
--title-color: ; --default-text-color: #eee;
--subtitle-color : #a3aab4; --subtitle-color: #007bff;
--mp3-text-color: #0F172A; --audio-only-label-color: #000000;
--default-text-color: #F8FAFC;
--checkbox-pulse-color:#9bbcf1; /* Formulaire */
--form-bg-color: #ffffff;
--form-input-bg-color: #f9f9f9;
--form-input-border-color: #cccccc;
--form-input-border-focus-color: #0056b3;
--form-input-text-color: #333333;
--form-input-placeholder-color: #aaaaaa;
--form-button-bg-color: #007bff;
--form-button-text-color: #ffffff;
--form-button-bg-hover-color: #0056b3;
--paste-button-icon-color: #ffffff;
} /* Checkbox */
--checkbox-unchecked-bg-color: #cccccc;
--checkbox-checked-bg-color: #3B82F6;
--checkbox-checkmark-border-color: #E0E0E2;
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
/* Download status */
--download-status-color: #007bff;
/* Progress bar */
--progress-bar-bg-color: #cccccc;
--progress-bar-fill-color: #007bff;
/* Video Info Box */
--video-info-bg-color: #f0f0f0;
--video-info-text-color: #222222;
--video-info-heading-color: #007bff;
--video-info-list-color: #444444;
--video-info-list-strong-color: #000000;
--video-info-link-color: #007bff;
--video-info-link-hover-color: #0056b3;
/* Playlist */
--playlist-background-color: #4f4f4f;
/* Settings */
--settings-button-bg-color: var(--form-bg-color);
--settings-button-text-color: var(--default-text-color);
--settings-modal-bg-color: var(--form-bg-color);
--settings-section-bg-color: var(--form-bg-color);
--settings-text-color: var(--default-text-color);
--settings-subtitle-color: #aaa;
}

BIN
ressources/yt-dlp.exe Normal file

Binary file not shown.

View File

@@ -0,0 +1,114 @@
const { fetchDownload, cancelDownload } = require("../services/download.services");
const { logger } = require("../logger");
const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
const { configFeatures } = require("../../config");
const listeners = [];
const speedListeners = [];
const stageListeners = [];
const playlistInfoListeners = [];
async function downloadController(req, res) {
try {
const options = {
url: req.body.url,
audioOnly: req.body.audioOnly === "1",
quality: req.body.quality || "best",
outputFolder: req.body.savePath || undefined,
playlistTitle: req.body.playlistTitle || undefined,
};
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("Invalid URL !");
if (options.outputFolder && !isSafePath(options.outputFolder)) {
logger.warn(`Unsafe download path rejected: ${options.outputFolder}`);
return res.status(400).send("Save Path Not Allowed.");
}
// Get output folder when the download is finished
const outputFolder = await fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners);
notifyDownloadFinished(outputFolder, configFeatures.notifySystem);
res.send("Download Done !");
} catch (err) {
logger.error(`Server Error in /download : ${err.message}`);
res.status(500).send(`Server Error`);
}
}
function progressController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const sendProgress = percent => res.write(`data: ${percent}\n\n`);
listeners.push(sendProgress);
req.on('close', () => {
listeners.splice(listeners.indexOf(sendProgress), 1);
res.end();
});
}
function speedController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const sendSpeed = speed => res.write(`data: ${speed}\n\n`);
speedListeners.push(sendSpeed);
req.on('close', () => {
speedListeners.splice(speedListeners.indexOf(sendSpeed), 1);
res.end();
});
}
function stageController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const sendStage = stage => res.write(`data: ${stage}\n\n`);
stageListeners.push(sendStage);
req.on('close', () => {
stageListeners.splice(stageListeners.indexOf(sendStage), 1);
res.end();
});
}
function cancelDownloadController(req, res) {
const cancelled = cancelDownload();
if (cancelled) {
logger.info("Download and queue cancelled by user");
res.send("Download stopped! Queue cleared.");
} else {
logger.warn("No download to cancel");
res.status(400).send("No download in progress !");
}
}
function playlistInfoController(req, res) {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const sendPlaylistInfo = info => res.write(`data: ${info}\n\n`);
playlistInfoListeners.push(sendPlaylistInfo);
req.on('close', () => {
playlistInfoListeners.splice(playlistInfoListeners.indexOf(sendPlaylistInfo), 1);
res.end();
});
}
module.exports = { downloadController, progressController, speedController, stageController, cancelDownloadController, playlistInfoController};

View File

@@ -0,0 +1,72 @@
const { fetchInfo } = require("../services/info.services");
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
const { logger } = require("../logger");
const { isValidUrl } = require("../helpers/validation.helpers");
async function infoController(req, res){
const url = req.body.url || req.query.url; // POST et GET
// If no url, non-string url or invalid url
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("Invalid URL Or Missing");
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
if (url.includes("&list") || url.includes("?list")) {
logger.info("Estimated Data Type: Playlist")
} else{
logger.info("Estimated Data Type: Video")
}
try {
let data;
let fetchError = null;
try {
data = await fetchInfo(url);
} catch (err) {
fetchError = err;
// If it's a playlist URL and fetch failed, try fetching just the single video
if ((url.includes("&list") || url.includes("?list"))) {
logger.info(`Playlist fetch failed: ${err.message}. Retrying with single video...`);
// Remove the &list parameter
const singleVideoUrl = url.split("&list")[0].split("?list")[0];
try {
data = await fetchInfo(singleVideoUrl);
logger.info(`Single video fetch succeeded after playlist failure`);
} catch (retryErr) {
logger.error(`Single video fetch also failed: ${retryErr.message}`);
throw err; // Throw the original error
}
} else {
throw err;
}
}
if (data._type === "playlist") {
const playlist = parsePlaylist(data);
logger.info(`Playlist : ${playlist.title} (${playlist.count} vidéos)`);
return res.json(playlist);
} else{
const video = parseVideo(data);
logger.info(`Unique Video: ${video.title}`);
return res.json(video);
}
} catch (err) {
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
return res.status(500).send(`Unable to fetch info`);
}
}
module.exports = {infoController};

View File

@@ -0,0 +1,85 @@
const path = require("path");
const getUserBrowser = require("./getBrowser.helpers.js");
const { ffmpegPath, denoPath} = require("./path.helpers.js");
const { configFeatures } = require("../../config.js");
const { logger } = require("../logger.js");
function validateCodec(codec){
const validCodec = [
"av01","vp9.2", "vp9" , "h265" , "h264" , "vp8" , "h263", "theora"
]
if(validCodec.includes(codec)){
logger.info(`Codec Valid: ${codec}`)
return codec
} else{
logger.error(`Codec not valid: ${codec}. Using default codec`)
return "h264"
}
}
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
logger.info("--- CONFIGURATION ---");
logger.info(`CONFIG autoUpdate: ${configFeatures.autoUpdate}`);
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
logger.info(`CONFIG autoDownloadPlaylist: ${configFeatures.autoDownloadPlaylist}`);
logger.info(`CONFIG customCodec: ${configFeatures.customCodec}`);
// ACTIVATE THEM WHEN THE FEATURE IS IMPLEMENTED
// logger.info(`CONFIG logSystem: ${configFeatures.logSystem}`);
// logger.info(`CONFIG outputTitleCheck: ${configFeatures.outputTitleCheck}`);
// logger.info(`CONFIG downloadSystem: ${configFeatures.downloadSystem}`);
// logger.info(`CONFIG notifySystem: ${configFeatures.notifySystem}`);
logger.info("---------------------");
const args = [
configFeatures.verboseLogs ? "--verbose" : null, // Verbose Logs
"--cookies-from-browser", `${getUserBrowser()}`,
"--no-continue",
"--no-overwrites",
"--newline", // YT-DLP Logs Format
configFeatures.addThumbnail ? "--embed-thumbnail" : null,
configFeatures.addMetadata ? "--add-metadata" : null,
"--concurrent-fragments", "8",
"--retries", "10",
"--fragment-retries", "10",
"--ffmpeg-location", ffmpegPath,
"--extractor-args","youtube:player_client=default",
"--js-runtimes", `deno:${denoPath}`,
"-S",
`vcodec:${validateCodec(configFeatures.customCodec) || "h264"}`+
`,acodec:aac`,
configFeatures.autoDownloadPlaylist ? "--yes-playlist" : "--no-playlist"
];
if (audioOnly) args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3");
else args.push("--merge-output", "mp4");
const qualityMap = {
best: "bestvideo+bestaudio/best/mp4",
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
worst: "worstvideo+worstaudio/worst/mp4",
1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]/mp4",
720: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
480: "bestvideo[height<=480]+bestaudio/best[height<=480]/mp4",
};
args.push("-f", qualityMap[quality] || "best");
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
args.push(url);
return args.filter(Boolean);
}
module.exports = { buildYtDlpArgs };

View File

@@ -0,0 +1,39 @@
const fs = require("fs");
const os = require("os");
const path = require("path");
const notify = require("./notify.helpers")
const { logger } = require("../logger");
function getUserBrowser() {
const userProfile = os.homedir();
// List of browsers supported by yt-dlp (Currently I can only guarantee Firefox, Sorry)
const browsers = [
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
];
// Search for an available browser
for (const browser of browsers) {
if (fs.existsSync(browser.path)) {
logger.info(`Browser found: ${browser.name}`);
return browser.name;
}
}
// No browser found - notify user
logger.warn("No supported browser found on the system");
notify.notifyFirefoxBrowserMissing();
// Fallback: return "Firefox" to let YT-DLP manage error
// this avoid app crash
return "firefox";
}
module.exports = getUserBrowser

View File

@@ -0,0 +1,43 @@
const { Notification, shell } = require("electron");
const { iconPaths } = require("./path.helpers");
function notifyDownloadFinished(folder, notifyEnabled = true) {
if (!notifyEnabled) return;
const notif = new Notification({
title: "Freedom Loader",
body: "Your download is complete, click here to open it.",
icon: iconPaths.confirm,
});
notif.on("click", () => shell.openPath(folder));
notif.show();
}
function notifyCookiesBrowserError(){
const notif = new Notification({
title: "Cookies Error",
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
icon: iconPaths.error,
});
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
notif.show();
}
function notifyFirefoxBrowserMissing(){
const notif = new Notification({
title: "Firefox Missing",
body: "Firefox was not found on your system. Click here to follow the installation guide",
icon: iconPaths.error,
});
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))
notif.show();
}
module.exports = {
notifyDownloadFinished,
notifyCookiesBrowserError,
notifyFirefoxBrowserMissing
};

View File

@@ -0,0 +1,27 @@
function parseVideo(data) {
return {
type: "video",
...data
};
}
function parsePlaylist(data) {
return {
type: "playlist",
title: data.title || data.id,
channel: data.uploader,
count: data.entries.length,
videos: (data.entries || []).map(v => ({
id: v.id,
title: v.title,
url: v.webpage_url,
duration: v.duration,
thumbnail: v.thumbnail,
uploader: v.uploader
}))
};
}
module.exports = { parseVideo, parsePlaylist };

View File

@@ -0,0 +1,123 @@
const path = require("path");
const fs = require("fs");
const os = require("os");
const { app } = require("electron");
const config = require("../../config.js");
const { logger } = require("../logger.js");
// Centralized resource paths
const resourcesPath = config.devMode
? path.join(__dirname, "../../ressources")
: process.resourcesPath;
// Default download folder (centralized)
const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader");
// Binary paths
let userYtDlp;
let ffmpegPath;
let denoPath;
const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe");
if (config.devMode) {
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe
denoPath = path.join(__dirname, "../../ressources/deno.exe");
} else {
userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe");
ffmpegPath = path.join(resourcesPath, "binaries","ffmpeg.exe");
denoPath = path.join(resourcesPath, "binaries","deno.exe");
if (!fs.existsSync(userYtDlp)) {
fs.copyFileSync(sourceYtDlp, userYtDlp);
}
}
// Notification icon paths
const iconPaths = {
confirm: path.join(resourcesPath, "confirm-icon.png"),
error: path.join(resourcesPath, "error.png"),
app: path.join(resourcesPath, "app-icon.ico")
};
// Binary paths for verification
const binaryPaths = {
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
ffprobe: path.join(resourcesPath, "binaries", "ffprobe.exe"),
deno: path.join(resourcesPath, "binaries", "deno.exe")
};
// Theme paths - themes stored in AppData to survive updates
const userThemesPath = path.join(app.getPath("userData"), "themes");
const defaultThemesSourcePath = path.join(resourcesPath, "theme");
function initUserThemes() {
try {
if (!fs.existsSync(userThemesPath)) {
fs.mkdirSync(userThemesPath, { recursive: true });
// Ensure folder is writable
fs.chmodSync(userThemesPath, 0o777);
logger.info("Created user themes directory");
}
// Copy default themes from resources to user directory if they don't exist
if (fs.existsSync(defaultThemesSourcePath)) {
const defaultThemes = fs.readdirSync(defaultThemesSourcePath);
for (const theme of defaultThemes) {
const srcPath = path.join(defaultThemesSourcePath, theme);
const destPath = path.join(userThemesPath, theme);
if (!fs.existsSync(destPath)) {
try {
copyDirectory(srcPath, destPath);
fs.chmodSync(destPath, 0o777);
logger.info(`Copied default theme: ${theme}`);
} catch (copyErr) {
logger.warn(`Failed to copy theme ${theme}: ${copyErr.message}. Theme folder will be created, add theme files manually.`);
try {
fs.mkdirSync(destPath, { recursive: true });
fs.chmodSync(destPath, 0o777);
} catch (mkdirErr) {
logger.warn(`Could not create theme directory ${theme}: ${mkdirErr.message}`);
}
}
}
}
} else {
logger.warn(`Default themes source path not found: ${defaultThemesSourcePath}`);
}
} catch (err) {
logger.error(`Failed to initialize user themes: ${err.message}`);
}
}
function copyDirectory(src, dest) {
try {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true, mode: 0o777 });
}
const files = fs.readdirSync(src);
for (const file of files) {
const srcFile = path.join(src, file);
const destFile = path.join(dest, file);
if (fs.statSync(srcFile).isDirectory()) {
copyDirectory(srcFile, destFile);
} else {
fs.copyFileSync(srcFile, destFile);
}
}
} catch (err) {
throw new Error(`Copy error: ${err.message}`);
}
}
if (!userYtDlp){ logger.error("Missing YT-DLP")}
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
if (!denoPath){ logger.error("Missing DENO")}
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes };

View File

@@ -0,0 +1,14 @@
const RateLimit = require("express-rate-limit");
/* Rate Limite */
const rateLimite = RateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 100 requests per windowMs on the root path
message: "Too many requests, please try again later.",
statusCode: 429, // HTTP status code for "Too Many Requests"
});
module.exports = {
rateLimite
}

View File

@@ -0,0 +1,21 @@
const path = require("path");
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
function isSafePath(folder) {
if (!folder || folder.length < 3) return false;
const unsafe = ["System32", "\\Windows"];
const resolved = path.resolve(folder);
return !unsafe.some(u => resolved.includes(u));
}
module.exports = { isValidUrl, isSafePath };

View File

@@ -1,93 +1,57 @@
/* const { createLogger, format, transports } = require("winston");
This file is part of Freedom Loader. const DailyRotateFile = require("winston-daily-rotate-file");
const fs = require("fs");
const path = require("path");
const os = require("os");
const config = require("../config");
Copyright (C) 2025 MasterAcnolo // Logs folder in Windows
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
Freedom Loader is free software: you can redistribute it and/or modify // Create "Logs" folder if needed
it under the terms of the GNU General Public License as published by try {
the Free Software Foundation, either version 3 of the License. fs.mkdirSync(logDir, { recursive: true });
} catch (error) {
Freedom Loader is distributed in the hope that it will be useful, console.error(`Failed to create log directory: ${error.message}`);
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { createLogger, format, transports } = require("winston");
const DailyRotateFile = require("winston-daily-rotate-file");
const fs = require("fs"); // pour vérifier/créer le dossier de logs
const path = require("path"); // pour gérer proprement les chemins
// on définit le dossier de logs
const logDir = path.join(__dirname, "../logs");
// on le crée si inexistant
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir);
// fonction pour générer une ligne de début de session
function getSessionStartLine() {
const now = new Date().toISOString();
return `--- Démarrage de la session : ${now} ---`;
} }
// fonction pour générer une ligne de fin de session const logFormat = format.combine(
function getSessionEndLine() { format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
const now = new Date().toISOString(); format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
return `--- Fin de la session : ${now} ---`; );
}
// configuration principale de Winston // Logger configuration
const logger = createLogger({ const logger = createLogger({
level: "info", // niveau de log minimum (info, warn, error, etc.) level: "info",
format: format.combine( format: logFormat,
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), // timestamp lisible
format.printf(({ timestamp, level, message }) =>
`${timestamp} | ${level.toUpperCase()} | ${message}` // format de ligne de log
)
),
transports: [ transports: [
new DailyRotateFile({ new DailyRotateFile({
dirname: logDir, // dossier de logs dirname: logDir,
filename: "LOGS-%DATE%.log", // nom de fichier journalier filename: "LOGS-%DATE%.log",
datePattern: "YYYY-MM-DD", // pattern de rotation datePattern: "YYYY-MM-DD",
zippedArchive: false, // true = compression des archives zippedArchive: false,
maxFiles: "14d", // garde 14 jours dhistorique maxFiles: "7d",
format: format.combine( format: logFormat,
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), options: { flags: "a" },
format.printf(({ timestamp, level, message }) =>
`${timestamp} | ${level.toUpperCase()} | ${message}`
)
),
options: { flags: "a" }, // mode append
}), }),
new transports.Console({ new transports.Console({
format: format.combine( format: logFormat,
format.colorize(),
format.timestamp({ format: "HH:mm:ss" }),
format.printf(({ timestamp, level, message }) =>
`${timestamp} | ${level} | ${message}`
)
),
}), }),
], ],
}); });
// fonction d'utilitaire pour marquer le début de la session
function logSessionStart() { function logSessionStart() {
logger.info(getSessionStartLine()); logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
logger.info(`Application Version: ${config.version}`)
} }
// pour la fin de la session
function logSessionEnd() { function logSessionEnd() {
logger.info(getSessionEndLine()); logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
} }
// exporte le logger et les helpers pour les autres modules
module.exports = { module.exports = {
logger, logger,
logSessionStart, logSessionStart,
logSessionEnd, logSessionEnd,
}; logDir,
};

View File

@@ -1,165 +0,0 @@
/*
This file is part of Freedom Loader.
Copyright (C) 2025 MasterAcnolo
Freedom Loader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License.
Freedom Loader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const express = require("express"); // importe le framework Express
const router = express.Router(); // crée un routeur Express pour modulariser les routes
const { execFile } = require("child_process"); // permet dexécuter un binaire externe sans passer par un shell
const path = require("path"); // module Node.js pour gérer les chemins de fichiers
const fs = require("fs"); // module Node.js pour manipuler le système de fichiers
const logger = require("../logger").logger; // récupère l'instance de logger Winston personnalisée
// chemin vers lexécutable yt-dlp packagé avec lappli
const ytDlpPath = path.join(__dirname, '../../yt-dlp.exe');
// route POST sur la racine (ex: POST /download)
router.post("/", (req, res) => {
try {
// récupère les options envoyées depuis le frontend (via formulaire ou requête AJAX)
const options = {
url: req.body.url, // URL à télécharger
audioOnly: req.body.audioOnly === "1", // conversion audio uniquement (mp3)
quality: req.body.quality || "best", // qualité vidéo à télécharger
subtitles: req.body.subs === "1", // récupérer les sous-titres anglais
};
if (!options.url) {
// si lURL est absente, on log et on renvoie 400
logger.warn("Requête POST /download sans URL");
return res.status(400).send("❌ URL manquante !");
}
// on récupère le dossier de sortie depuis app.locals (défini ailleurs dans le serveur)
const outputFolder = req.app.locals.outputFolder;
// s'il n'existe pas, on le crée automatiquement
if (!fs.existsSync(outputFolder)) {
fs.mkdirSync(outputFolder, { recursive: true });
logger.info("Dossier Freedom Loader Output créé à la volée dans Téléchargements.");
}
// template du nom de fichier de sortie (ex: MonTitre.mp4 ou MonTitre.mp3)
const outputTemplate = path.join(outputFolder, "%(title)s.%(ext)s");
// tableau d'arguments pour yt-dlp
const args = [];
if (options.audioOnly) {
// si audioOnly, on force extraction audio en mp3
args.push("--extract-audio", "--audio-format", "mp3");
}
if (options.subtitles) {
// si lutilisateur veut des sous-titres
args.push("--write-subs", "--sub-lang", "en");
}
// // qualité souhaitée
// args.push("-f", options.quality);
// let format;
// if (options.quality === "best") {
// format = "bestvideo+bestaudio/best";
// } else if (options.quality === "worst") {
// format = "worstvideo+worstaudio/worst";
// } else {
// format = "best"; // fallback
// }
// args.push("-f", format);
args.push("--no-continue");
let format;
switch (options.quality) {
case "best":
format = "bestvideo+bestaudio/best";
break;
case "medium":
// qualité moyenne, ex: 720p vidéo + meilleur audio
format = "bestvideo[height<=720]+bestaudio/best[height<=720]";
break;
case "worst":
format = "worstvideo+worstaudio/worst";
break;
case "1080":
format = "bestvideo[height<=1080]+bestaudio/best[height<=1080]";
break;
case "720":
format = "bestvideo[height<=720]+bestaudio/best[height<=720]";
break;
case "480":
format = "bestvideo[height<=480]+bestaudio/best[height<=480]";
break;
default:
format = "best";
}
args.push("-f", format);
// modèle de sortie
args.push("-o", outputTemplate);
// enfin, l'URL à télécharger
args.push(options.url);
// log de la commande complète pour debug
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
// exécution du binaire yt-dlp
const child = execFile(ytDlpPath, args);
// gestion des logs stdout de yt-dlp
child.stdout.on("data", (data) => {
data.toString().split("\n").forEach(line => {
if(line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`);
});
});
// gestion des logs stderr de yt-dlp (les erreurs)
child.stderr.on("data", (data) => {
data.toString().split("\n").forEach(line => {
if(line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`);
});
});
// si une erreur système survient (par ex : yt-dlp.exe introuvable)
child.on("error", (err) => {
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
res.status(500).send(`❌ Erreur lors de l'exécution : ${err.message}`);
});
// callback quand le processus se termine
child.on("close", (code) => {
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
if(code === 0) {
// succès
res.send("✅ Téléchargement terminé !");
} else {
// yt-dlp a échoué
res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`);
}
});
} catch (err) {
// en cas d'erreur JavaScript
logger.error(`Erreur serveur dans /download : ${err.message}`);
res.status(500).send(`Erreur serveur : ${err.message}`);
}
});
// on exporte ce routeur pour qu'il soit utilisé par l'app principale
module.exports = router;

View File

@@ -0,0 +1,14 @@
const express = require("express");
const router = express.Router();
const { downloadController, progressController, speedController, stageController, cancelDownloadController, playlistInfoController } = require("../controller/download.controller");
router.post("/", downloadController);
router.post("/cancel", cancelDownloadController);
// SSE for download progress
router.get("/progress", progressController);
router.get("/speed", speedController);
router.get("/stage", stageController);
router.get("/playlist-info", playlistInfoController);
module.exports = router;

View File

@@ -1,55 +0,0 @@
/*
This file is part of Freedom Loader.
Copyright (C) 2025 MasterAcnolo
Freedom Loader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License.
Freedom Loader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const express = require("express"); // importe Express
const router = express.Router(); // crée un routeur Express indépendant
const { execFile } = require("child_process"); // permet dexécuter un programme externe
const path = require("path"); // module pour gérer les chemins de fichiers
// chemin vers le binaire yt-dlp embarqué
const ytDlpPath = path.join(__dirname, "../../yt-dlp.exe");
// route POST sur la racine de ce routeur
router.post("/", (req, res) => {
const url = req.body.url; // récupère l'URL depuis la requête POST
if (!url) return res.status(400).send("❌ URL manquante"); // validation simple
// exécute yt-dlp avec --dump-json pour récupérer les métadonnées de la vidéo
execFile(ytDlpPath, ["--dump-json", url], (error, stdout, stderr) => {
if (error) {
// gestion derreur : par exemple yt-dlp plante ou lURL est invalide
console.error("Erreur lors de l'exécution de yt-dlp :", error);
console.error("stderr :", stderr);
return res.status(500).send("❌ Impossible de récupérer les infos.");
}
try {
// on parse le JSON envoyé par yt-dlp
const info = JSON.parse(stdout);
// on renvoie lobjet JSON directement au frontend
res.json(info);
} catch (e) {
// le JSON est mal formé
console.error("Erreur lors du parsing JSON :", e);
res.status(500).send("❌ JSON illisible.");
}
});
});
// on exporte le routeur pour être utilisé dans app.js ou ailleurs
module.exports = router;

View File

@@ -0,0 +1,7 @@
const express = require("express");
const router = express.Router();
const { infoController } = require("../controller/info.controller");
router.post("/", infoController);
module.exports = router;

View File

@@ -1,103 +1,46 @@
/* const express = require("express");
This file is part of Freedom Loader. const path = require("path");
const { logger, logSessionEnd } = require("./logger");
const config = require("../config");
const { rateLimite } = require("./helpers/rateLimit.helpers");
Copyright (C) 2025 MasterAcnolo const app = express();
Freedom Loader is free software: you can redistribute it and/or modify // Middlewares
it under the terms of the GNU General Public License as published by app.use(express.json());
the Free Software Foundation, either version 3 of the License.
Freedom Loader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const express = require("express"); // framework web
const { exec } = require("child_process"); // si besoin dexécuter des commandes (pas utilisé ici directement)
const fs = require("fs"); // accès au système de fichiers
const path = require("path"); // gestion de chemins
const debug = require("debug")("freedom-loader:server");
// petit debugger coloré en console
const { logger, logSessionStart, logSessionEnd } = require("./logger");
// récupère notre logger Winston + helpers de session
const app = express(); // instancie l'application Express
// logSessionStart(); // désactivé, mais pourrait logguer le début de session
// on définit le dossier de téléchargement dans le dossier Téléchargements de lutilisateur
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
const outputFolder = path.join(downloadsPath, "Freedom Loader Output");
// création du dossier sil nexiste pas
if (!fs.existsSync(outputFolder)) {
try {
fs.mkdirSync(outputFolder, { recursive: true });
logger.info("Dossier Freedom Loader Output cree dans Telechargements.");
} catch (err) {
logger.error("Impossible de creer le dossier :", err);
process.exit(1); // arrêt du programme en cas déchec
}
} else {
logger.info("Dossier Freedom Loader Output deja existant.");
}
// on rend ce dossier dispo via app.locals
app.locals.outputFolder = outputFolder;
// configuration pour parser les requêtes POST en urlencoded
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, "../public")));
// sert les fichiers statiques (frontend) // Routes
const staticPath = path.join(__dirname, "../public"); app.use("/download", require("./routes/download.route"));
debug("Serveur statique sur", staticPath); app.use("/info", require("./routes/info.route"));
app.use(express.static(staticPath)); app.get("/", rateLimite, (req, res) => {
// enregistre les routes API
const infoRoute = require("./routes/info");
const downloadRoute = require("./routes/download");
debug("Routes /download et /info installees");
app.use("/download", downloadRoute);
app.use("/info", infoRoute);
// route GET / de base
app.get("/", (req, res) => {
debug("Requete GET / servie");
res.sendFile(path.join(__dirname, "../public/index.html")); res.sendFile(path.join(__dirname, "../public/index.html"));
}); });
// fonction pour démarrer le serveur (Promise pour pouvoir await ailleurs) async function startServer() {
function startServer() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logger.info("Demarrage du serveur Express..."); const server = app.listen(config.applicationPort, () => {
const serverInstance = app.listen(8080, () => { logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
logger.info("Serveur Express pret sur http://localhost:8080"); resolve(server);
resolve(serverInstance); // succès
}); });
serverInstance.on("error", (err) => {
logger.error("Erreur serveur Express :", err); server.on("error", (err) => {
logger.error("Express server error:", err);
reject(err); reject(err);
}); });
const gracefulExit = () => {
logSessionEnd();
server.close(() => {
logger.info("Express server closed cleanly.");
process.exit();
});
};
process.on("SIGINT", gracefulExit);
process.on("SIGTERM", gracefulExit);
}); });
} }
// gestion de la fermeture propre du process module.exports = { startServer };
// process.on("exit", () => {
// logSessionEnd();
// });
process.on("SIGINT", () => { // Ctrl+C
logSessionEnd();
process.exit();
});
process.on("SIGTERM", () => { // kill
logSessionEnd();
process.exit();
});
// on exporte la fonction startServer pour que dautres modules puissent la lancer
module.exports = { startServer };

View File

@@ -0,0 +1,221 @@
const { execFile } = require("child_process");
const { userYtDlp, defaultDownloadFolder } = require("../helpers/path.helpers");
const fs = require("fs");
const { logger } = require("../logger");
const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
const notify = require("../helpers/notify.helpers");
const path = require("path");
const { isSafePath } = require("../helpers/validation.helpers");
const { configFeatures } = require("../../config.js");
let currentDownloadProcess = null;
// Détecte si une URL est une playlist
function isPlaylistUrl(url) {
return url.includes("?list=") || url.includes("&list=");
}
function createPlaylistFolder(basePath, playlistTitle) {
const sanitizedTitle = playlistTitle
.replace(/[<>:"|?*]/g, '')
.replace(/\s+/g, ' ')
.trim()
.substring(0, 200);
const playlistPath = path.join(basePath, sanitizedTitle);
try {
if (!fs.existsSync(playlistPath)) {
fs.mkdirSync(playlistPath, { recursive: true });
logger.info(`Playlist folder created: ${playlistPath}`);
return playlistPath;
}
let counter = 1;
let newPath;
while (counter <= 1000) {
newPath = path.join(basePath, `${sanitizedTitle} ${counter}`);
if (!fs.existsSync(newPath)) {
fs.mkdirSync(newPath, { recursive: true });
logger.info(`Playlist folder created with increment: ${newPath}`);
return newPath;
}
counter++;
}
logger.error(`Could not find available playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder");
} catch (err) {
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
// Fallback: create folder "Untitled Playlist X"
let counter = 1;
let fallbackPath;
while (counter <= 1000) {
fallbackPath = path.join(basePath, `Untitled Playlist ${counter}`);
if (!fs.existsSync(fallbackPath)) {
try {
fs.mkdirSync(fallbackPath, { recursive: true });
logger.info(`Fallback playlist folder created: ${fallbackPath}`);
return fallbackPath;
} catch (mkErr) {
counter++;
continue;
}
}
counter++;
}
logger.error(`Could not create playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder");
}
}
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
return new Promise((resolve, reject) => {
logger.info(`CONFIG createPlaylistFolders: ${configFeatures.createPlaylistFolders}`);
let outputFolder = options.outputFolder || defaultDownloadFolder;
// Normalize path and validate it's safe (within Users folder)
let safeOutputFolder = path.resolve(outputFolder);
if (!isSafePath(safeOutputFolder)) {
logger.warn(`Path not allowed, using default instead: ${safeOutputFolder}`);
safeOutputFolder = path.resolve(defaultDownloadFolder);
}
// Create download folder if it doesn't exist
try {
fs.mkdirSync(safeOutputFolder, { recursive: true });
logger.info(`Output folder ready: ${safeOutputFolder}`);
} catch (err) {
logger.error(`Failed to create output folder: ${err.message}`);
return reject(new Error(`Unable to create download folder: ${err.message}`));
}
// Détecte si c'est une playlist et crée un dossier approprié
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url);
if (isPlaylist && configFeatures.createPlaylistFolders) {
try {
const playlistName = options.playlistTitle || "Untitled Playlist";
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
logger.info(`Playlist detected, using folder: ${safeOutputFolder}`);
} catch (err) {
logger.error(`Failed to create playlist folder: ${err.message}`);
return reject(err);
}
} else if (isPlaylist && !configFeatures.createPlaylistFolders) {
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
}
const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder });
logger.info(`[yt-dlp args] ${args.join(" ")}`);
const child = execFile(userYtDlp, args);
currentDownloadProcess = child;
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
child.on("close", code => {
currentDownloadProcess = null;
listeners.forEach(fn => fn("done"));
if (code === 0) resolve(safeOutputFolder);
else if (code === null) reject(new Error(`Download cancelled by user`));
else reject(new Error(`YT-DLP failed with code : ${code}`));
});
child.stdout.on("data", data => {
data.toString().split("\n").forEach(line => {
if (!line.trim()) return;
logger.info(`[yt-dlp] ${line}`);
// Progress Bar
if (line.startsWith("[download] Destination:")) {
listeners.forEach(fn => fn("reset"));
stageListeners.forEach(fn => fn("Downloading..."));
}
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
if (line.includes("MiB/s") || line.includes("KiB/s")) {
const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/);
if (match) {
const speed = match[0];
speedListeners.forEach(fn => fn(speed));
}
}
// Playlist item tracking
const itemMatch = line.match(/Downloading\s+(?:item|video)?\s*(\d+)\s+of\s+(\d+)/i);
if (itemMatch) {
const itemInfo = `Item ${itemMatch[1]} of ${itemMatch[2]}`;
playlistInfoListeners.forEach(fn => fn(itemInfo));
}
// Stage messages
if (line.includes("[Merger]")) {
stageListeners.forEach(fn => fn("Merging formats..."));
}
if (line.includes("[ExtractAudio]")) {
stageListeners.forEach(fn => fn("Extracting audio..."));
}
if (line.includes("[Metadata]")) {
stageListeners.forEach(fn => fn("Adding metadata..."));
}
if (line.includes("[ThumbnailsConvertor]")) {
stageListeners.forEach(fn => fn("Converting thumbnail..."));
}
if (line.includes("[EmbedThumbnail]")) {
stageListeners.forEach(fn => fn("Embedding thumbnail..."));
}
if (line.match(/ERROR: Could not copy .* cookie database/i)) {
notify.notifyCookiesBrowserError();
}
});
});
child.stderr.on("data", data => {
data.toString().split("\n").forEach(line => line.trim() && logger.error(`[yt-dlp stderr] ${line}`));
});
});
}
function cancelDownload() {
if (currentDownloadProcess) {
logger.info("Cancelling download and killing all child processes...");
// Force kill the process and all its children with SIGKILL
try {
// Try to kill with SIGKILL on Windows (process group) or Unix
if (process.platform === 'win32') {
// On Windows, kill the process tree
require('child_process').execSync(`taskkill /PID ${currentDownloadProcess.pid} /T /F`, { stdio: 'ignore' });
} else {
// On Unix, send SIGKILL to process group
process.kill(-currentDownloadProcess.pid, 'SIGKILL');
}
} catch (err) {
logger.warn(`Error killing process: ${err.message}`);
// Fallback to regular kill
currentDownloadProcess.kill('SIGKILL');
}
currentDownloadProcess = null;
logger.info("Download cancelled successfully");
return true;
}
return false;
}
module.exports = { fetchDownload, cancelDownload };

View File

@@ -0,0 +1,51 @@
const { execFile } = require("child_process");
const { userYtDlp, denoPath } = require("../helpers/path.helpers");
const getUserBrowser = require("../helpers/getBrowser.helpers");
const { logger } = require("../logger");
function fetchInfo(url) {
const args = [
"--js-runtimes", `deno:${denoPath}`,
"--dump-single-json",
"--ignore-errors",
"--yes-playlist",
"--cookies-from-browser",`${getUserBrowser()}`,
"--extractor-args","youtube:player_client=default",
"--ignore-no-formats-error",
];
return new Promise((resolve, reject) => {
execFile(userYtDlp,[...args, url],
{ timeout: 30_000, // 30s, if more timeout
maxBuffer: 10 * 1024 * 1024 }, // 10MO max response (Default: 200ko)
(error, stdout, stderr) => {
if (stderr) {
const lower = stderr.toLowerCase();
if (lower.includes("sign in to confirm") || lower.includes("failed to decrypt") || lower.includes("could not copy")) {
return reject(new Error(`Unable to extract cookies from ${getUserBrowser()}. Please log in to this browser and try again.`));
}
}
if (error) {
logger.error(`YT-DLP Error: ${error.message}`);
if (stderr) logger.debug(`stderr: ${stderr}`);
return reject(new Error("Unable to fetch Info."));
}
if (!stdout) return reject(new Error("No Data Received."));
try {
const data = JSON.parse(stdout);
resolve(data);
} catch (e) {
logger.error(`JSON Parsing Error: ${e.message}`);
reject(new Error("JSON unreadable."));
}
});
});
}
module.exports = { fetchInfo };

96
theme/template.theme.json Normal file
View File

@@ -0,0 +1,96 @@
{
"meta": {
"name": "",
"author": "",
"version": "1.0.0",
"formatVersion": "1.0"
},
"style": {
"background": {
"image": "",
"size": "cover",
"position": "center",
"attachment": "fixed"
},
"colors": {
"background": "",
"text": {
"default": "",
"subtitle": "",
"audioOnly": ""
}
},
"form": {
"background": "",
"input": {
"background": "",
"border": "",
"borderFocus": "",
"text": "",
"placeholder": ""
},
"button": {
"background": "",
"text": "",
"hover": ""
},
"pasteButtonIcon": ""
},
"checkbox": {
"background": {
"unchecked": "",
"checked": ""
},
"checkmarkBorder": "",
"pulse": ""
},
"download": {
"status": ""
},
"progressBar": {
"background": "",
"fill": ""
},
"videoInfo": {
"background": "",
"text": "",
"heading": "",
"list": "",
"strong": "",
"link": "",
"linkHover": ""
},
"playlist": {
"background": ""
},
"settings": {
"button": {
"background": "",
"text": ""
},
"openThemeButton": {
"background": "",
"text": ""
},
"openJsonButton": {
"background": "",
"text": ""
},
"background": {
"modal": "",
"section": ""
},
"text": "",
"subtitle": ""
}
}
}