Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ff998e4df | ||
|
|
0c1e8fe899 | ||
|
|
4158e8c6e3 | ||
|
|
653319e408 | ||
|
|
0a4f14fba7 | ||
|
|
fb4571fc51 | ||
|
|
c11fd40681 | ||
|
|
cbfde8b761 | ||
|
|
c774821cc4 | ||
|
|
2ed15a32bc | ||
|
|
c252853603 | ||
|
|
cfdddec073 | ||
|
|
97a63ab791 | ||
|
|
a0e00341e1 | ||
|
|
26d42491ce | ||
|
|
d678ba2f12 | ||
|
|
55b400eb05 | ||
|
|
0a6313b1f3 | ||
|
|
b4312376bb | ||
|
|
5ffb143a1a | ||
|
|
ee24473077 | ||
|
|
2b98a63f92 | ||
|
|
2e594d87e8 | ||
|
|
b343c2869d | ||
|
|
094b93f4fa | ||
|
|
cdc8a623c0 | ||
|
|
2b43c00a3a | ||
|
|
14928798d4 | ||
|
|
e5fb44fc6b | ||
|
|
9f9e4cd1d8 | ||
|
|
45fe7336fe | ||
|
|
b909e7d917 | ||
|
|
ce62748ade | ||
|
|
d4ecc36f6b | ||
|
|
fee364610e | ||
|
|
492af8525b | ||
|
|
fb58b5ae06 | ||
|
|
577034b0bd | ||
|
|
6fc452edb4 | ||
|
|
086ca49599 | ||
|
|
be1de35da8 | ||
|
|
3c1f57667f | ||
|
|
5997cd20b4 | ||
|
|
6220c9400e | ||
|
|
48a16b927f | ||
|
|
87545d77b4 | ||
|
|
4e0fe29c20 | ||
|
|
f8e3bc843d | ||
|
|
b3f1edaabd | ||
|
|
ef784dd19c | ||
|
|
69bbcee885 | ||
|
|
60b4a99506 | ||
|
|
c1030400f5 | ||
|
|
3f508da501 | ||
|
|
0437cb61a2 | ||
|
|
1372582c54 | ||
|
|
070c3d507f | ||
|
|
3e9c545952 | ||
|
|
e3845e1849 | ||
|
|
91703d6a91 | ||
|
|
cff3101b03 | ||
|
|
c9a48ec64c | ||
|
|
e6419a9759 | ||
|
|
9d1d2fc7a8 | ||
|
|
cdca4a82ab | ||
|
|
c2e4e12512 | ||
|
|
96c1c6717d | ||
|
|
abf776175c | ||
|
|
d6de184bfd | ||
|
|
12249c4e8a | ||
|
|
f1fbc3f0c7 | ||
|
|
bb8cf54bf9 | ||
|
|
dccf8bd786 | ||
|
|
793bb79d76 | ||
|
|
f5cdf27ce6 | ||
|
|
627c56cd25 | ||
|
|
783871d131 | ||
|
|
138c05f995 | ||
|
|
5009a479af | ||
|
|
2a95a10113 | ||
|
|
9d6065c80a | ||
|
|
cd8b2bf007 | ||
|
|
1faf74fbc4 | ||
|
|
09163afbcd | ||
|
|
405d141055 | ||
|
|
599e09fa9f | ||
|
|
4954de5f1a | ||
|
|
ac854ae559 | ||
|
|
9944f05139 | ||
|
|
f6558b656a | ||
|
|
bfbf57ba38 | ||
|
|
d0af35377c | ||
|
|
59c263a246 | ||
|
|
42700bd7d8 | ||
|
|
5a9b364b4b | ||
|
|
3619550c74 | ||
|
|
31cc4012ac | ||
|
|
2e329a8584 | ||
|
|
d92dd59f86 | ||
|
|
5bb7ca377d | ||
|
|
9ee9a3574c | ||
|
|
dc50a44682 | ||
|
|
4be5c14999 | ||
|
|
b92ffebdb4 | ||
|
|
7b6e48b09e | ||
|
|
368851b4bb | ||
|
|
fa2da5712d | ||
|
|
7df97d70bb | ||
|
|
5c5f595829 | ||
|
|
5b659e3d8a | ||
|
|
1a526777cc | ||
|
|
b0d99d4c29 | ||
|
|
c7554475a9 |
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
/ressources/ffmpeg.exe
|
/ressources/ffmpeg.exe
|
||||||
/ressources/ffprobe.exe
|
/ressources/ffprobe.exe
|
||||||
|
/ressources/deno.exe
|
||||||
|
|
||||||
logs/*.json
|
logs/*.json
|
||||||
logs/*.log
|
logs/*.log
|
||||||
114
README-FR.md
@@ -1,114 +0,0 @@
|
|||||||
# Freedom Loader
|
|
||||||
|
|
||||||
<!-- Release -->
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<!-- Build (Next Release) -->
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> 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
|
|
||||||
|
|
||||||
## 🚀 Fonctionnalités
|
|
||||||
|
|
||||||
- ✅ Téléchargement de vidéos avec les métadonnées (MP4)
|
|
||||||
- ✅ Téléchargement audio uniquement, avec la couverture ainsi que les métadonnées (MP3)
|
|
||||||
- ✅ Support des playlists complètes
|
|
||||||
- ✅ Récupération automatique des métadonnées (titre, durée, etc.)
|
|
||||||
- ✅ Logs détaillés : console et fichiers rotatifs (7 jours)
|
|
||||||
- ✅ Choix du chemin de sortie personnalisé avec sauvegarde
|
|
||||||
- ✅ Plusieurs thèmes graphiques au choix pour personnaliser l’interface
|
|
||||||
- ✅ Interface simple, intuitive
|
|
||||||
- ✅ Notification quand le téléchargement est fini (Click = Ouverture Dossier Output)
|
|
||||||
- ✅ Discord Rich Presence
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Installation
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ❓ Questions fréquentes
|
|
||||||
|
|
||||||
### _Où se situent mes fichiers une fois téléchargés ?_
|
|
||||||
|
|
||||||
Dans le dossier Téléchargements (**C:\Users\VOTRE NOM\Downloads**) de votre ordinateur, dans un dossier nommé **Freedom Loader Output**
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### _Une fois le programme installé et lancé, j'ai une erreur_
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
<!--  -->
|
|
||||||
<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 d’une partie de la vidéo
|
|
||||||
- [ ] Renommage du fichier de sortie
|
|
||||||
- [x] ~~Extraction / découpage de la vidéo via chapitrage natif~~
|
|
||||||
- [ ] Téléchargements parallèles
|
|
||||||
- [ ] Suppression automatique des parties sponsorisées (skip sponsor)
|
|
||||||
- [x] ~~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/apercu1.0.1.png" alt="Exemple d'application" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> Voici un exemple de l’application lancée après avoir saisi une URL.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ℹ️ À propos
|
|
||||||
|
|
||||||
Ce projet a été réalisé en utilisant [](https://github.com/yt-dlp/yt-dlp)
|
|
||||||
Merci à eux de nous permettre d’utiliser leur API gratuitement et en open source.
|
|
||||||
|
|
||||||
Ce projet est open source et accueille volontiers vos contributions.
|
|
||||||
Voici comment participer :
|
|
||||||
|
|
||||||
- **🪲 Signalement de bugs**
|
|
||||||
Merci d’utiliser le [système d’issues 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 d’installation 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 m’encourager, 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 d’informations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
|
||||||
|
|
||||||
J’ai bien évidemment eu recours à l’IA pour m’aider sur certaines parties du projet. Utilisez l’IA avec parcimonie et soyez toujours conscient de ce que vous faites.
|
|
||||||
195
README.md
@@ -1,114 +1,151 @@
|
|||||||
# Freedom Loader
|
<div align="center">
|
||||||
|
<img
|
||||||
|
src="./build/banner.png"
|
||||||
|
alt="Banner"
|
||||||
|
style="width: 50%;;"/>
|
||||||
|
|
||||||
<!-- Release -->
|
</div>
|
||||||
|
|
||||||

|
<div align="center">
|
||||||
|
<a href="https://github.com/MasterAcnolo/Freedom-Loader/releases"><img src="https://img.shields.io/badge/Release-1.3.1-blue?style=for-the-badge"></a>
|
||||||
<!-- Build (Next Release) -->
|
<a href="https://masteracnolo.github.io/FreedomLoader/index.html"><img src="https://img.shields.io/badge/Freedom%20Loader-Open%20Page-111111?style=for-the-badge&logo=terminal&logoColor=white"></a>
|
||||||
|
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
|
||||||

|
</div>
|
||||||
|
|
||||||
> 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 with metadata(MP4)
|
|
||||||
- ✅ Audio-only download with thumbail and metadata(MP3)
|
|
||||||
- ✅ Full playlist support
|
|
||||||
- ✅ Automatic metadata retrieval (title, duration, etc.)
|
|
||||||
- ✅ Detailed logs: console and rotating files (7 days)
|
|
||||||
- ✅ Custom output path selection with saving
|
|
||||||
- ✅ Multiple graphical themes available for interface customization
|
|
||||||
- ✅ Simple, intuitive, and responsive interface
|
|
||||||
- ✅ Notification system when download finished (Click = Open Output Folder)
|
|
||||||
- ✅ Discord Rich Presence
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 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>
|
<br>
|
||||||
|
|
||||||
### _After installing and running the program, I get an error_
|
Freedom Loader is a **Windows desktop** application built with **Electron**.
|
||||||
|
It provides a simple and reliable way to download video or audio content with metadata and advanced options, without ads or questionable third-party services.
|
||||||
|
The goal is to offer a clean, user-friendly, and transparent download experience. It's important for me to make media content downloading accessible to people who love music or videos and don’t always have an internet connection.
|
||||||
|
|
||||||
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`
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Video download with metadata (MP4)
|
||||||
|
- Audio-only download with thumbnail and tags (MP3)
|
||||||
|
- Full playlist support and better display in UI
|
||||||
|
- Automatic metadata retrieval (title, duration, author, etc.)
|
||||||
|
- Detailed logs (console + rotating files for 7 days)
|
||||||
|
- Progress Bar for download and network speed indicator
|
||||||
|
- Better UX when fetching info
|
||||||
|
- Custom output path with persistence
|
||||||
|
- Automatic dependency updates (YT-DLP)
|
||||||
|
- Automatic Application Update
|
||||||
|
- Multiple UI themes
|
||||||
|
- Simple, responsive interface
|
||||||
|
- Notifications on download completion (click to open folder)
|
||||||
|
- Discord Rich Presence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Download the latest installer from the **Releases** page and run it (Defender could stop you but click on "Run Anyway" and it's good).
|
||||||
|
Actually, **Firefox Browser** is the only browser supported for get cookies. You should install it. Follow the tutorial in the wiki page if you need some help, Firefox Section. [Link](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Where are my downloaded files stored?
|
||||||
|
|
||||||
|
In your **Downloads** folder, inside **Freedom Loader**.
|
||||||
|
Example path:
|
||||||
|
`C:\Users\[YOUR NAME]\Downloads\Freedom Loader`
|
||||||
|
|
||||||
|
### I get an error after launching the app
|
||||||
|
|
||||||
|
Try restarting the application, checking the logs, or opening the developer console:
|
||||||
|
`CTRL + SHIFT + I` -> “Toggle Developer Tools”.
|
||||||
|
|
||||||
<!--  -->
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="build/example-developertools.png" alt="Example Developer Tools" />
|
<img src="build/example-developertools.png" width="20%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 🏗️ Upcoming Features (Roadmap)
|
---
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [x] ~~Website for Freedom Loader~~
|
||||||
- [ ] More format options
|
- [ ] More format options
|
||||||
|
- [ ] Linux version
|
||||||
|
- [x] ~~Auto-updating YT-DLP~~
|
||||||
|
- [x] ~~Automatic project updates~~
|
||||||
|
- [x] ~~Better download status~~
|
||||||
|
- [ ] Better website support (currently only YouTube is fully supported, other site could work but i can't prove it)
|
||||||
- [ ] Subtitle support
|
- [ ] Subtitle support
|
||||||
- [ ] Improved UI / UX
|
- [x] ~~UI/UX improvements~~
|
||||||
- [ ] Language selection
|
- [ ] Language selection
|
||||||
- [ ] Download a specific part of the video
|
- [ ] Download specific parts of a video
|
||||||
- [ ] Rename output file
|
- [ ] Custom file naming
|
||||||
- [x] ~~Extract / Split video using native chapters~~
|
|
||||||
- [ ] Parallel downloads
|
- [ ] Parallel downloads
|
||||||
- [ ] Skip sponsored parts of videos
|
- [ ] Automatic sponsor skipping
|
||||||
- [x] ~~Custom output path selection (default **C:\Users\Username\Downloads** with a **Freedom Loader Output** folder created)~~
|
- [x] ~~Extract/split video using native chapters~~
|
||||||
|
- [x] ~~Custom output path selection~~
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🖼️ Preview
|
## Preview
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="build/apercu1.0.1.png" alt="Exemple d'application" />
|
<img src="build/apercu1.2.4.png" width="40%" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> Here is an example of the app after launch and entering a URL
|
---
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
This project uses:
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/yt-dlp/yt-dlp"><img src="https://img.shields.io/badge/yt--dlp-Visit%20Repo-cf0000?style=for-the-badge&logo=github&logoColor=white"></a>
|
||||||
|
<a href="https://github.com/FFmpeg/FFmpeg"><img src="https://img.shields.io/badge/FFmpeg-Visit%20Repo-666666?style=for-the-badge&logo=ffmpeg&logoColor=white"></a>
|
||||||
|
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-Visit%20Repo-339933?style=for-the-badge&logo=node.js&logoColor=white"></a>
|
||||||
|
<a href="https://github.com/electron/electron"><img src="https://img.shields.io/badge/Electron-Visit%20Repo-47848F?style=for-the-badge&logo=electron&logoColor=white"></a>
|
||||||
|
<a href="https://github.com/denoland/deno/"><img src="https://img.shields.io/badge/Deno-Visit%20Repo-14e0cc?style=for-the-badge&logo=deno&logoColor=white"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ℹ️ About
|
## Contributing
|
||||||
|
|
||||||
This project was built using [](https://github.com/yt-dlp/yt-dlp)
|
Contributions are welcome.
|
||||||
Thanks to them for providing a free and open-source API.
|
|
||||||
|
|
||||||
This project is open-source and welcomes your contributions.
|
### Bug Reports
|
||||||
Here is how to get involved:
|
|
||||||
|
|
||||||
- **🪲 Bug Reporting**
|
Use the GitHub Issues system and include reproduction steps and logs when possible.
|
||||||
Please use the [GitHub issues system](https://github.com/MasterAcnolo/Freedom-Loader/issues) to report any bugs you encounter.
|
Logs can be found here:
|
||||||
Try to provide clear reproduction steps and logs if possible. Logs are located in the installation folder of the program. Example path:
|
`C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`
|
||||||
`C:\Users\[YOUR NAME]\Desktop\Freedom Loader\resources\app\logs`
|
|
||||||
The log file will be named something like `combined-20xx-xx-xx.log`
|
### Feature Requests
|
||||||
<br>
|
|
||||||
- **🛠️ Contribution**
|
Open a feature request issue with a clear description and use cases.
|
||||||
Pull Requests are welcome! Please fork the repo and submit your PR after ensuring you meet the contribution guidelines (tests, style, etc.).
|
|
||||||
<br>
|
### Pull Requests
|
||||||
- **💡 Feature Suggestions**
|
|
||||||
You can propose new ideas via a [feature request issue](https://github.com/MasterAcnolo/Freedom-Loader/issues).
|
Fork the repository, make your changes, and submit a PR once everything is tested and clean.
|
||||||
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
|
## Support the Project
|
||||||
|
|
||||||
This project is licensed under the **MIT License** — see the [LICENSE](./LICENSE) file for details.
|
If you'd like to support development:
|
||||||
|
|
||||||
|
<a href="https://paypal.me/axelnicolas25">
|
||||||
|
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" />
|
||||||
|
</a>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
## License
|
||||||
|
|
||||||
I have obviously used AI assistance for some parts of this project. Use AI sparingly and always be aware of what you are doing.
|
Freedom Loader is released under the **GNU General Public License v3.0 (GPLv3)**.
|
||||||
|
You are free to use, modify, and redistribute the software under the terms of this license.
|
||||||
|
|
||||||
|
See the full license in the [LICENSE](./LICENSE) file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
AI assistance was used occasionally to speed up development, but all code and decisions were reviewed and validated manually.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 88 KiB |
BIN
build/apercu1.2.4.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
build/app-icon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
build/banner.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
build/error.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 5.6 KiB |
10
config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const packageJson = require("./package.json");
|
||||||
|
const { app } = require("electron");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
version: packageJson.version,
|
||||||
|
applicationPort: "8787",
|
||||||
|
debugMode: true,
|
||||||
|
localMode: !app.isPackaged,
|
||||||
|
DiscordRPCID: "1410934537051181146",
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
227
main.js
@@ -1,165 +1,182 @@
|
|||||||
/*
|
const config = require("./config.js");
|
||||||
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 { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
||||||
app.disableHardwareAcceleration(); // Désactive l'accélération GPU pour éviter les erreurs GPU sur certains systèmes
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
const fs = require("fs");
|
||||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||||
|
const { AutoUpdater } = require("./server/update.js");
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
// Utilise le vrai dossier de logs défini dans logger.js
|
|
||||||
const logsFolderPath = logDir;
|
const logsFolderPath = logDir;
|
||||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); /* Pour la notif */
|
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||||
/*
|
|
||||||
Fonction principale qui crée la fenêtre principale de l'application.
|
|
||||||
Elle évite la création multiple et configure les dimensions et options de la fenêtre.
|
|
||||||
Charge l'URL locale du serveur Express et gère les erreurs éventuelles.
|
|
||||||
*/
|
|
||||||
async function createWindow() {
|
|
||||||
logger.info("Création de la fenêtre...");
|
|
||||||
|
|
||||||
|
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
||||||
|
app.disableHardwareAcceleration();
|
||||||
|
|
||||||
|
|
||||||
|
// Gestion single instance
|
||||||
|
const gotLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
||||||
|
function checkNativeDependencies() {
|
||||||
|
const deps = [
|
||||||
|
{ name: "yt-dlp.exe", path: path.join(process.resourcesPath, "yt-dlp.exe") },
|
||||||
|
{ name: "ffmpeg.exe", path: path.join(process.resourcesPath, "ffmpeg.exe") },
|
||||||
|
{ name: "ffprobe.exe", path: path.join(process.resourcesPath, "ffprobe.exe") },
|
||||||
|
{ name: "deno.exe", path: path.join(process.resourcesPath, "deno.exe") },
|
||||||
|
];
|
||||||
|
const missing = deps.filter(dep => !fs.existsSync(dep.path));
|
||||||
|
let errorMsg = "";
|
||||||
|
if (missing.length > 0) {
|
||||||
|
const missingList = missing.map(dep => dep.name).join(", ");
|
||||||
|
logger.error(`Missing dependencies: ${missingList}`);
|
||||||
|
errorMsg += `The following files are missing in the 'ressources' folder:\n${missingList}`;
|
||||||
|
}
|
||||||
|
if (errorMsg) {
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
dialog.showErrorBox(
|
||||||
|
"Missing dependencies",
|
||||||
|
`${errorMsg}\n\nThe application will now exit. Try to reinstall`
|
||||||
|
);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!config.localMode){
|
||||||
|
if (!gotLock) {
|
||||||
|
// Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
|
||||||
|
// Ici la nouvelle instance continue normalement
|
||||||
|
} else {
|
||||||
|
if (!checkNativeDependencies()) {
|
||||||
|
// Arrêt déjà géré dans la fonction
|
||||||
|
} else {
|
||||||
|
app.on("second-instance", () => {
|
||||||
|
// La vieille instance se ferme
|
||||||
|
if (mainWindow) {
|
||||||
|
logger.info("New Instance Detected, closing the older...");
|
||||||
|
mainWindow.destroy();
|
||||||
|
mainWindow = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Création fenêtre principale
|
||||||
|
async function createMainWindow() {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
logger.warn("La fenêtre existe déjà, pas de nouvelle création");
|
logger.warn("Window already exists, no new creation!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
title: "Freedom Loader 1.2.2", // A Changer à chaque Version
|
title: `Freedom Loader ${config.version}`,
|
||||||
width: 750,
|
width: 750,
|
||||||
height: 800,
|
height: 800,
|
||||||
minWidth: 750,
|
minWidth: 750,
|
||||||
minHeight: 800,
|
minHeight: 800,
|
||||||
|
frame:false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
preload: path.join(__dirname, "preload.js"), // Chargement du preload script sécurisé
|
preload: path.join(__dirname, "preload.js"),
|
||||||
},
|
},
|
||||||
// titleBarStyle: 'hidden', // Option possible pour une barre de titre personnalisée
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mainWindow.loadURL("http://localhost:8080");
|
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||||
logger.info("Fenêtre chargée");
|
logger.info("Window Loaded");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Erreur chargement fenêtre:", err);
|
logger.error("Window Loading Error:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Événement déclenché à la fermeture de la fenêtre principale
|
|
||||||
mainWindow.on("closed", () => {
|
mainWindow.on("closed", () => {
|
||||||
logger.info("Fenêtre principale fermée");
|
logger.info("Main Window Closed");
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// IPC
|
||||||
Définition du chemin par défaut pour les téléchargements.
|
|
||||||
Ici, on utilise le dossier 'Downloads' de l'utilisateur avec un sous-dossier spécifique à l'application.
|
|
||||||
*/
|
|
||||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
|
||||||
|
|
||||||
/*
|
|
||||||
Gestionnaire IPC qui permet au renderer de demander à l'utilisateur
|
|
||||||
de sélectionner un dossier via une boîte de dialogue native.
|
|
||||||
On retourne le chemin sélectionné ou null si annulation.
|
|
||||||
*/
|
|
||||||
ipcMain.handle("select-download-folder", async () => {
|
ipcMain.handle("select-download-folder", async () => {
|
||||||
logger.info("Demande de sélection d'un dossier reçue depuis le renderer");
|
|
||||||
try {
|
try {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||||
properties: ["openDirectory"]
|
|
||||||
});
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
logger.info(`Dossier sélectionné : ${result.filePaths[0]}`);
|
logger.info(`Dossier sélectionné : ${result.filePaths[0]}`);
|
||||||
return result.filePaths[0];
|
return result.filePaths[0];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Erreur lors de la sélection de dossier : ${err.message}`);
|
logger.error(`Error when creating Output Folder : ${err.message}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
|
||||||
Gestionnaire IPC pour exposer au renderer le chemin par défaut de téléchargement
|
|
||||||
afin qu'il puisse l'afficher ou l'utiliser comme valeur initiale.
|
ipcMain.on("set-progress", (event, percent) => {
|
||||||
*/
|
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
|
||||||
ipcMain.handle("get-default-download-path", () => {
|
|
||||||
return defaultDownloadPath;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
// Topbar window controls
|
||||||
Événement déclenché quand l'application est prête.
|
ipcMain.on("window-minimize", () => {
|
||||||
On démarre le serveur Express, puis on crée la fenêtre principale.
|
if (mainWindow) mainWindow.minimize();
|
||||||
En cas d'erreur, on log et on quitte l'application proprement.
|
});
|
||||||
*/
|
ipcMain.on("window-maximize", () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMaximized()) {
|
||||||
|
mainWindow.unmaximize();
|
||||||
|
} else {
|
||||||
|
mainWindow.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ipcMain.on("window-close", () => {
|
||||||
|
if (mainWindow) mainWindow.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Topbar custom actions
|
||||||
|
ipcMain.on("open-devtools", () => {
|
||||||
|
if (mainWindow) mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
});
|
||||||
|
ipcMain.on("open-logs", () => {
|
||||||
|
if (logsFolderPath) shell.openPath(logsFolderPath);
|
||||||
|
});
|
||||||
|
ipcMain.on("open-website", () => {
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html");
|
||||||
|
});
|
||||||
|
ipcMain.on("open-wiki", () => {
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// App ready
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
logSessionStart();
|
logSessionStart();
|
||||||
logger.info("App prête, démarrage du serveur Express...");
|
logger.info("App Ready, Server Express starting...");
|
||||||
const expressServer = require("./server/server.js");
|
|
||||||
|
const serverPath = path.join(__dirname, "server", "server.js");
|
||||||
|
const expressServer = require(serverPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await expressServer.startServer();
|
await expressServer.startServer();
|
||||||
logger.info("Serveur Express démarré");
|
logger.info("Express Server Started");
|
||||||
await createWindow();
|
|
||||||
|
|
||||||
// Ajout du menu personnalisé
|
const { startRPC } = require("./server/discordRPC");
|
||||||
const menuTemplate = [
|
startRPC();
|
||||||
// ... Menu standard Electron ...
|
|
||||||
{
|
|
||||||
label: "Logs",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Ouvrir les logs",
|
|
||||||
click: () => {
|
|
||||||
shell.openPath(logsFolderPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Fusionner avec le menu par défaut
|
await createMainWindow();
|
||||||
const defaultMenu = Menu.getApplicationMenu();
|
AutoUpdater(mainWindow);
|
||||||
let template = menuTemplate;
|
} catch (err) {
|
||||||
if (defaultMenu) {
|
logger.error("Window or Server error :", err);
|
||||||
template = [...Menu.getApplicationMenu().items.map(item => item), ...menuTemplate];
|
|
||||||
}
|
|
||||||
const finalMenu = Menu.buildFromTemplate(template);
|
|
||||||
Menu.setApplicationMenu(finalMenu);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Erreur serveur ou fenêtre :", error);
|
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
Quitte l'application lorsque toutes les fenêtres sont fermées,
|
|
||||||
sauf sous macOS où il est habituel de garder l'application active.
|
|
||||||
*/
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
logger.info("Toutes fenêtres fermées, quitte l'app");
|
logger.info("All Window Closed, shuting down app");
|
||||||
if (process.platform !== "darwin") app.quit();
|
if (process.platform !== "darwin") app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
app.on("before-quit", () => logSessionEnd());
|
||||||
Avant de quitter l'application, on log la fin de session pour traçabilité.
|
|
||||||
*/
|
|
||||||
app.on("before-quit", () => {
|
|
||||||
logSessionEnd();
|
|
||||||
});
|
|
||||||
|
|||||||
4218
package-lock.json
generated
58
package.json
@@ -1,44 +1,46 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.2.2",
|
"version": "1.3.1",
|
||||||
"author": "MasterAcnolo",
|
"author": "MasterAcnolo",
|
||||||
"description": "Freedom Loader",
|
"description": "Freedom Loader",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "electron --trace-warnings .",
|
||||||
"package": "electron-forge package",
|
"dist": "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",
|
||||||
"discord-rpc": "^4.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
"electron-updater": "^6.6.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"webidl-conversions": "^8.0.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0"
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.8.1",
|
|
||||||
"@electron-forge/maker-deb": "^7.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": "^37.1.0",
|
||||||
"electron-builder": "^26.0.12"
|
"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/**/*",
|
||||||
"!logs/*.json"
|
"!logs/*.json",
|
||||||
|
"server/**/*",
|
||||||
|
"server/node_modules/**/*"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "build"
|
"buildResources": "build"
|
||||||
@@ -50,23 +52,43 @@
|
|||||||
"to": "confirm-icon.png"
|
"to": "confirm-icon.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "ressources/yt-dlp 2025.08.27.exe",
|
"from": "build/app-icon.ico",
|
||||||
"to": "yt-dlp 2025.08.27.exe"
|
"to": "app-icon.ico"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "build/error.png",
|
||||||
|
"to": "error.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/yt-dlp.exe",
|
||||||
|
"to": "yt-dlp.exe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "ressources/ffmpeg.exe",
|
"from": "ressources/ffmpeg.exe",
|
||||||
"to": "ffmpeg.exe"
|
"to": "ffmpeg.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/ffprobe.exe",
|
||||||
|
"to": "ffprobe.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "ressources/deno.exe",
|
||||||
|
"to": "deno.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "server",
|
||||||
|
"to": "server"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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",
|
||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": true,
|
"createStartMenuShortcut": true,
|
||||||
|
|||||||
37
preload.js
@@ -1,31 +1,18 @@
|
|||||||
/*
|
|
||||||
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 { contextBridge, ipcRenderer } = require("electron");
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
/*
|
|
||||||
Expose dans le contexte global (window.electronAPI) deux méthodes sécurisées
|
|
||||||
pour que le renderer puisse interagir avec le main process via IPC.
|
|
||||||
|
|
||||||
- getDefaultDownloadPath : récupère le chemin de téléchargement par défaut.
|
|
||||||
- selectDownloadFolder : ouvre la boîte de dialogue pour choisir un dossier.
|
|
||||||
*/
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
||||||
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
||||||
|
setProgress: (percent) => ipcRenderer.send("set-progress", percent)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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"),
|
||||||
|
openWiki: () => ipcRenderer.send("open-wiki")
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB |
1
public/assets/icon/close.svg
Normal 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 |
1
public/assets/icon/maximize.svg
Normal 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 |
1
public/assets/icon/minimize.svg
Normal 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 |
|
Before Width: | Height: | Size: 97 KiB |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 805 KiB |
BIN
public/assets/images/Drift.jpg
Normal file
|
After Width: | Height: | Size: 967 KiB |
BIN
public/assets/images/Songbird.webp
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
public/assets/images/Spicy.png
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 16 KiB |
BIN
public/assets/logo/Freedom Loader White.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/assets/logo/Freedom Loader.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/assets/logo/FreedomLoader2.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -11,80 +11,99 @@
|
|||||||
">
|
">
|
||||||
<title></title>
|
<title></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">
|
<div class="topbar">
|
||||||
|
<div class="custom-controls">
|
||||||
|
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
||||||
|
<button id="logs-btn" title="Logs">Logs</button>
|
||||||
|
<button id="website-btn" title="Website">Website</button>
|
||||||
|
<button id="wiki-btn" title="Wiki">Wiki</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>
|
<div class="theme-switcher">
|
||||||
<h1 id="title"> Freedom Loader </h1>
|
<label for="themeSelect">Thème :</label>
|
||||||
<p id="subtitle"> Because why not? </p>
|
<select id="themeSelect" aria-label="Choisir le thème"></select>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<form action="/download" method="POST" id="downloadForm">
|
<div class="container">
|
||||||
<input name="url" placeholder="URL" id="UrlInput">
|
|
||||||
|
|
||||||
<label class="checkbox-inline">
|
<header>
|
||||||
|
<h1 id="title"> Freedom Loader </h1>
|
||||||
|
<p id="subtitle"> Because why not? </p>
|
||||||
|
</header>
|
||||||
|
|
||||||
<label class="checkbox-mp3">
|
<form action="/download" method="POST" id="downloadForm">
|
||||||
<input type="checkbox" name="audioOnly" value="1">
|
<input name="url" placeholder="URL" id="UrlInput">
|
||||||
<div class="checkmark"></div>
|
|
||||||
</label>
|
<label class="checkbox-inline">
|
||||||
<!-- <input type="checkbox" name="audioOnly" value="1"> -->
|
|
||||||
Audio seulement (MP3)
|
<label class="checkbox-mp3">
|
||||||
|
<input type="checkbox" name="audioOnly" value="1">
|
||||||
|
<div class="checkmark"></div>
|
||||||
</label>
|
</label>
|
||||||
<!-- <input type="checkbox" name="audioOnly" value="1"> Audio seulement (MP3) -->
|
Audio Only (MP3)
|
||||||
<select name="quality">
|
</label>
|
||||||
<option value="best">Meilleure qualité</option>
|
<select name="quality">
|
||||||
<option value="medium">Qualité moyenne</option>
|
<option value="best">Best Quality</option>
|
||||||
<option value="worst">Qualité minimale</option>
|
<option value="medium">Medium Quality</option>
|
||||||
<option value="1080">1080p</option>
|
<option value="worst">Lowest Quality</option>
|
||||||
<option value="720">720p</option>
|
<option value="1080">1080p</option>
|
||||||
<option value="480">480p</option>
|
<option value="720">720p</option>
|
||||||
</select>
|
<option value="480">480p</option>
|
||||||
|
</select>
|
||||||
<button type="submit">Télécharger</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!--Il faut utiliser un formulaire, parce que on doit envoyer a un serveur
|
|
||||||
les données à traiter. Pour cela on définit une route vers /download, ca va
|
|
||||||
être l'espèce de "canal" attribué a ça. Ca va faire que si jamais on veut ensuite recup
|
|
||||||
les infos du form, il faut aller sur ce canal précis (/download). La méthode POST dit
|
|
||||||
que l'on envoie des données.-->
|
|
||||||
|
|
||||||
|
<button type="submit" onclick="startProgress()">Download</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="download-path">
|
<div class="download-path">
|
||||||
Sera enregistré dans <span id="savePath"></span>
|
Will be save on <span id="savePath"></span>
|
||||||
<button id="changePath">Modifier</button>
|
<button id="changePath">Edit</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="downloadProgressWrapper">
|
||||||
|
<div id="downloadProgress"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 20px;">
|
||||||
|
<div id="downloadProgressText"></div>
|
||||||
|
<div id="downloadSpeedText"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="downloadStatus"></div>
|
<div id="downloadStatus"></div>
|
||||||
|
|
||||||
<div class="infos-container">
|
<div class="infos-container">
|
||||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<footer class="theme-switcher">
|
<span id="version-badge" class="version-badge">v1.3.1</span>
|
||||||
<label for="themeSelect">Thème :</label>
|
|
||||||
<select id="themeSelect" aria-label="Choisir le thème">
|
|
||||||
<!-- <option value="dark">Sombre</option>
|
|
||||||
<option value="light">Clair</option>
|
|
||||||
<option value="Chirac">Chirac</option>
|
|
||||||
<option value="Fanatic">Fanatic</option> -->
|
|
||||||
</select>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
<script src="script/custompath.js"></script>
|
<script src="script/custompath.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/chirac.js"></script>
|
<script src="script/progressBar.js"></script>
|
||||||
<script src="script/customthemes.js"></script>
|
<script src="script/customthemes.js"></script>
|
||||||
|
<script type="module" src="script/topbar.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,33 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Chirac(){
|
|
||||||
const title = document.getElementById("title");
|
|
||||||
title.innerHTML = "Chirac Loader";
|
|
||||||
|
|
||||||
const subtitle = document.getElementById("subtitle");
|
|
||||||
subtitle.innerHTML = "J'aime les pommes";
|
|
||||||
subtitle.style.color= "black";
|
|
||||||
|
|
||||||
document.body.style.backgroundImage = "url('assets/images/goat2.webp')";
|
|
||||||
document.body.style.backgroundSize = "cover";
|
|
||||||
document.body.style.backgroundPosition = "center";
|
|
||||||
document.body.style.backgroundAttachment = "fixed";
|
|
||||||
|
|
||||||
console.log("Je serai le président de tous les français");
|
|
||||||
}
|
|
||||||
@@ -1,45 +1,15 @@
|
|||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Ce script attend que le DOM soit complètement chargé pour initialiser
|
|
||||||
l'affichage et la gestion du chemin de sauvegarde personnalisé.
|
|
||||||
|
|
||||||
1. Récupère le chemin de téléchargement par défaut depuis le main process via l'API exposée.
|
|
||||||
2. Met à jour le texte affiché dans l'élément avec l'id "savePath".
|
|
||||||
3. Crée un input caché nommé "savePath" dans le formulaire pour envoyer ce chemin lors du submit.
|
|
||||||
4. Ajoute un écouteur sur le bouton "changePath" pour permettre à l'utilisateur
|
|
||||||
de choisir un dossier via une boîte de dialogue native.
|
|
||||||
5. Met à jour l'affichage et la valeur cachée du chemin sélectionné.
|
|
||||||
*/
|
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", async () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
const savePathElem = document.getElementById("savePath");
|
const savePathElem = document.getElementById("savePath");
|
||||||
|
|
||||||
// 1️Essayer de charger depuis le localStorage
|
// Essayer de charger depuis le localStorage
|
||||||
let savedPath = localStorage.getItem("customDownloadPath");
|
let savedPath = localStorage.getItem("customDownloadPath");
|
||||||
|
|
||||||
// 2️Sinon demander le chemin par défaut à l'API Electron
|
// Sinon demander le chemin par défaut à l'API Electron
|
||||||
if (!savedPath) {
|
if (!savedPath) {
|
||||||
savedPath = await window.electronAPI.getDefaultDownloadPath();
|
savedPath = await window.electronAPI.getDefaultDownloadPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3 Afficher le chemin
|
// Afficher le chemin
|
||||||
if (savePathElem) {
|
if (savePathElem) {
|
||||||
savePathElem.textContent = savedPath;
|
savePathElem.textContent = savedPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,60 @@
|
|||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Liste des thèmes disponibles (clé = nom, valeur = label affiché)
|
|
||||||
const themes = {
|
const themes = {
|
||||||
default: "Default",
|
default: { label: "Default", subtitle: "Because why not?" },
|
||||||
dark: "Sombre",
|
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
|
||||||
light: "Clair",
|
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
|
||||||
nf : "NF",
|
neon: { label: "Néon", subtitle: "How was your day ?"},
|
||||||
chirac: "Chirac",
|
nf: { label: "NF", subtitle: "You call it music, i call it my Therapist" },
|
||||||
fanatic: "Fanatic",
|
songbird: { label: "Songbird", subtitle: "From Her to Eternity" },
|
||||||
cyberpunk: "Cyberpunk",
|
drift: { label: "Drift", subtitle: "Si la route t'appelle, contre appel" },
|
||||||
spicy: "Spicy",
|
fanatic: { label: "Fanatic", subtitle: "Always Fnatic !" },
|
||||||
vilbrequin: "Vilbrequin"
|
chirac: { label: "Chirac", subtitle: "J'aime les pommes" },
|
||||||
};
|
spicy: { label: "Spicy", subtitle: "The Spiciest One" },
|
||||||
|
|
||||||
const themeSubtitles = {
|
|
||||||
default: "Because why not?",
|
|
||||||
dark: "Darkness is my ally",
|
|
||||||
light: "Qui aime ce thème ?",
|
|
||||||
nf : "You call it music, i call it my Therapist",
|
|
||||||
chirac: "J'aime les pommes",
|
|
||||||
fanatic: "Always Fnatic !",
|
|
||||||
cyberpunk: "Wake up, choom. We’ve got a city to burn.",
|
|
||||||
spicy: "The Spiciest One",
|
|
||||||
vilbrequin: "Rend l'argent"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const themeSelect = document.getElementById("themeSelect");
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
|
||||||
// Remplir le select avec les options
|
|
||||||
function populateThemeSelect() {
|
function populateThemeSelect() {
|
||||||
for (const [key, label] of Object.entries(themes)) {
|
for (const [themeKey, themeInfo] of Object.entries(themes)) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = key;
|
option.value = themeKey;
|
||||||
option.textContent = label;
|
option.textContent = themeInfo.label;
|
||||||
themeSelect.appendChild(option);
|
themeSelect.appendChild(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appliquer le thème sur le body et changer le subtitle
|
function applyTheme(themeKey) {
|
||||||
function applyTheme(themeName) {
|
|
||||||
// Supprime les classes thème précédentes
|
|
||||||
document.body.classList.remove(...Object.keys(themes));
|
document.body.classList.remove(...Object.keys(themes));
|
||||||
// Ajoute la classe du thème actuel
|
document.body.classList.add(themeKey);
|
||||||
document.body.classList.add(themeName);
|
|
||||||
|
|
||||||
// Modifier le subtitle
|
const subtitleElement = document.getElementById("subtitle");
|
||||||
const subtitle = document.getElementById("subtitle");
|
if (subtitleElement && themes[themeKey]) {
|
||||||
if (subtitle && themeSubtitles[themeName]) {
|
subtitleElement.textContent = themes[themeKey].subtitle;
|
||||||
subtitle.textContent = themeSubtitles[themeName];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionnel : animation spéciale Chirac
|
|
||||||
if (themeName === "chirac") {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
document.body.classList.add("theme-active");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove("theme-active");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sauvegarder le thème en localStorage
|
function saveTheme(themeKey) {
|
||||||
function saveTheme(themeName) {
|
localStorage.setItem("selectedTheme", themeKey);
|
||||||
localStorage.setItem("selectedTheme", themeName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charger le thème au démarrage
|
|
||||||
function loadTheme() {
|
function loadTheme() {
|
||||||
const savedTheme = localStorage.getItem("selectedTheme");
|
const savedTheme = localStorage.getItem("selectedTheme");
|
||||||
|
|
||||||
if (savedTheme && themes[savedTheme]) {
|
if (savedTheme && themes[savedTheme]) {
|
||||||
applyTheme(savedTheme);
|
applyTheme(savedTheme);
|
||||||
themeSelect.value = savedTheme;
|
themeSelect.value = savedTheme;
|
||||||
} else {
|
} else {
|
||||||
applyTheme("dark");
|
applyTheme("dark"); // thème par défaut
|
||||||
themeSelect.value = "dark";
|
themeSelect.value = "dark";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quand l’utilisateur change le select
|
// Quand l'utilisateur change le thème depuis le select
|
||||||
themeSelect.addEventListener("change", (e) => {
|
themeSelect.addEventListener("change", (event) => {
|
||||||
const selected = e.target.value;
|
const selectedTheme = event.target.value;
|
||||||
if (themes[selected]) {
|
if (themes[selectedTheme]) {
|
||||||
applyTheme(selected);
|
applyTheme(selectedTheme);
|
||||||
saveTheme(selected);
|
saveTheme(selectedTheme);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +1,35 @@
|
|||||||
/*
|
|
||||||
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");
|
||||||
|
|
||||||
// Élément où on affichera l’état du téléchargement (en cours, erreur, succès)
|
|
||||||
const statusDiv = document.getElementById("downloadStatus");
|
const statusDiv = document.getElementById("downloadStatus");
|
||||||
|
|
||||||
// Récupération du bouton de soumission pour pouvoir le désactiver pendant la requête
|
|
||||||
const button = form.querySelector("button");
|
const button = form.querySelector("button");
|
||||||
|
|
||||||
// Écouteur d’événement pour intercepter la soumission du formulaire
|
|
||||||
form.addEventListener("submit", async (e) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault(); // Empêche le rechargement automatique de la page
|
e.preventDefault();
|
||||||
|
button.disabled = true; // Empêche les clics multiples
|
||||||
|
statusDiv.textContent = "Download in progress...";
|
||||||
|
|
||||||
// Désactive le bouton pour éviter les clics multiples pendant la requête
|
|
||||||
button.disabled = true;
|
|
||||||
|
|
||||||
// Affiche un message d’attente pour informer l’utilisateur
|
|
||||||
statusDiv.textContent = "Téléchargement en cours...";
|
|
||||||
|
|
||||||
// Prépare les données du formulaire au format URL-encoded
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const params = new URLSearchParams(formData);
|
const params = new URLSearchParams(formData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Envoie 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 HTTP 4xx ou 5xx)
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
|
statusDiv.textContent = res;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupère le texte envoyé par le serveur (message de succès ou erreur)
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
|
|
||||||
// Affiche le message retourné par le serveur
|
|
||||||
statusDiv.textContent = text;
|
statusDiv.textContent = text;
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
// Gestion des erreurs réseau ou autres exceptions
|
statusDiv.textContent = "❌ An Error has Occured.";
|
||||||
statusDiv.textContent = "❌ Une erreur s’est produite.";
|
|
||||||
} finally {
|
} finally {
|
||||||
// Réactive le bouton pour permettre d’autres soumissions
|
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
|
||||||
// Efface le message d’état après 5 secondes pour garder l’interface propre
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
statusDiv.textContent = "";
|
statusDiv.textContent = "";
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|||||||
@@ -1,136 +1,180 @@
|
|||||||
/*
|
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" };
|
||||||
*/
|
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
return { error: "Network or JSON Issue" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// On attend que le DOM soit complètement chargé
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Récupération des éléments du DOM pour interaction
|
|
||||||
const urlInput = document.getElementById("UrlInput");
|
const urlInput = document.getElementById("UrlInput");
|
||||||
const infoDiv = document.getElementById("videoInfo");
|
const infoDiv = document.getElementById("videoInfo");
|
||||||
|
const loaderBox = document.getElementById("loaderBox");
|
||||||
|
|
||||||
// Stocke la dernière URL fetchée pour éviter les requêtes répétées inutiles
|
|
||||||
let lastFetchedUrl = "";
|
let lastFetchedUrl = "";
|
||||||
|
|
||||||
// Active les logs DEBUG pour le développement
|
|
||||||
const DEBUG = true;
|
|
||||||
|
|
||||||
// Écoute les changements dans l'input URL
|
|
||||||
urlInput.addEventListener("input", async () => {
|
urlInput.addEventListener("input", async () => {
|
||||||
const url = urlInput.value.trim();
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
if (DEBUG) console.log("[DEBUG] Input détecté :", url);
|
// Si champ vide -> reset total
|
||||||
|
if (!url || url.length < 2) {
|
||||||
// Si l'URL est vide ou trop courte, on reset l'affichage
|
|
||||||
if (!url || url.length < 5) {
|
|
||||||
if (DEBUG) console.log("[DEBUG] URL vide ou trop courte, reset affichage.");
|
|
||||||
infoDiv.innerHTML = "";
|
infoDiv.innerHTML = "";
|
||||||
infoDiv.classList.remove("visible");
|
infoDiv.classList.remove("visible", "playlist-mode");
|
||||||
lastFetchedUrl = "";
|
lastFetchedUrl = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si l'URL est identique à la dernière requêtée, on ne refait pas la requête
|
if (url === lastFetchedUrl) return;
|
||||||
if (url === lastFetchedUrl) {
|
lastFetchedUrl = url;
|
||||||
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
|
|
||||||
|
loaderBox.style.display = "flex";
|
||||||
|
const data = await fetchVideoInfo(url);
|
||||||
|
loaderBox.style.display = "none";
|
||||||
|
|
||||||
|
// Gestion des erreurs
|
||||||
|
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");
|
||||||
|
loaderBox.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mémorise l'URL actuelle comme dernière URL traitée
|
// ---------- PLAYLIST ----------
|
||||||
lastFetchedUrl = url;
|
if (data.type === "playlist") {
|
||||||
|
infoDiv.classList.add("playlist-mode");
|
||||||
try {
|
|
||||||
if (DEBUG) console.log("[DEBUG] Envoi requête POST vers /info avec URL:", url);
|
|
||||||
|
|
||||||
// Envoi de la requête POST pour récupérer les infos de la vidéo
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Si la réponse HTTP est une erreur, on affiche un message
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error(`[ERROR] Réponse HTTP ${res.status}: ${res.statusText}`);
|
|
||||||
infoDiv.innerHTML = "❌ Impossible de récupérer les infos (erreur serveur).";
|
|
||||||
infoDiv.classList.remove("visible");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse la réponse JSON contenant les infos vidéo
|
|
||||||
const data = await res.json();
|
|
||||||
if (DEBUG) console.log("[DEBUG] Données reçues:", data);
|
|
||||||
|
|
||||||
// Vérification simple que les données attendues sont présentes
|
|
||||||
if (!data || !data.title || !data.thumbnail) {
|
|
||||||
console.warn("[WARNING] Données incomplètes", data);
|
|
||||||
infoDiv.innerHTML = "❌ Données incomplètes reçues.";
|
|
||||||
infoDiv.classList.remove("visible");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formatage de la durée en minutes et secondes
|
|
||||||
const minutes = Math.floor(data.duration / 60);
|
|
||||||
const seconds = data.duration % 60;
|
|
||||||
const durationStr = `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
|
|
||||||
|
|
||||||
// Formatage de la date d'upload au format JJ/MM/AAAA
|
|
||||||
const date = data.upload_date || "";
|
|
||||||
const readableDate = date
|
|
||||||
? `${date.slice(6, 8)}/${date.slice(4, 6)}/${date.slice(0, 4)}`
|
|
||||||
: "Inconnue";
|
|
||||||
|
|
||||||
// Conversion taille approximative en Mo
|
|
||||||
const sizeMB = data.filesize_approx
|
|
||||||
? (data.filesize_approx / (1024 * 1024)).toFixed(2) + " Mo"
|
|
||||||
: "Inconnue";
|
|
||||||
|
|
||||||
// Gestion des catégories, affichage par défaut si absentes
|
|
||||||
const categories = data.categories
|
|
||||||
? data.categories.join(", ")
|
|
||||||
: "Non spécifiées";
|
|
||||||
|
|
||||||
// Construction du HTML pour afficher les informations de la vidéo
|
|
||||||
infoDiv.innerHTML = `
|
infoDiv.innerHTML = `
|
||||||
<h3>${data.title}</h3>
|
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist Detected: ${data.title}</strong></h3>
|
||||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
<h3 style="color:var(--video-info-heading-color);"><strong>Video Count: ${data.count}</strong></h3>
|
||||||
<ul>
|
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p>
|
||||||
<li><strong>Durée :</strong> ${durationStr}</li>
|
<div id="playlistVideos"></div>
|
||||||
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
|
||||||
<li><strong>Date d’upload :</strong> ${readableDate}</li>
|
|
||||||
<li><strong>Vues :</strong> ${data.view_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>Channel :</strong> <a href="${data.channel_url}" target="_blank" rel="noopener noreferrer">${data.channel_url}</a></li>
|
|
||||||
<li><strong>Taille estimée :</strong> ${sizeMB}</li>
|
|
||||||
<li><strong>Catégories :</strong> ${categories}</li>
|
|
||||||
</ul>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Affiche la div contenant les infos
|
const listDiv = document.getElementById("playlistVideos");
|
||||||
infoDiv.classList.add("visible");
|
|
||||||
|
|
||||||
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
|
data.videos.forEach(v => {
|
||||||
} catch (e) {
|
const durationStr = v.duration
|
||||||
// En cas d'erreur réseau ou JSON, on affiche un message d'erreur
|
? `${Math.floor(v.duration / 60)}m ${(v.duration % 60).toString().padStart(2,"0")}s` : "Inconnue";
|
||||||
console.error("[CRITICAL] Erreur lors de la récupération:", e);
|
|
||||||
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
|
const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
|
||||||
infoDiv.classList.remove("visible");
|
|
||||||
|
listDiv.innerHTML += `
|
||||||
|
<div style="margin-bottom:12px;">
|
||||||
|
<img src="${v.thumbnail}" width="160" alt="Thumbnail">
|
||||||
|
<p><strong>${v.title}</strong></p>
|
||||||
|
<p>Duration : ${durationStr}</p>
|
||||||
|
<p><a href="${videoUrl}" target="_blank">URL</a>
|
||||||
|
<button class="copy-btn" data-url="${videoUrl}">📋</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestion du bouton copier
|
||||||
|
listDiv.addEventListener("click", (event) => {
|
||||||
|
if (event.target.classList.contains("copy-btn")) {
|
||||||
|
const btn = event.target;
|
||||||
|
if (btn.disabled) return;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
const url = btn.dataset.url;
|
||||||
|
navigator.clipboard.writeText(url)
|
||||||
|
.then(() => {
|
||||||
|
const original = btn.textContent;
|
||||||
|
|
||||||
|
btn.style.opacity = 0;
|
||||||
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = "✅";
|
||||||
|
btn.style.opacity = 1;
|
||||||
|
btn.style.transform = "scale(1)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.style.opacity = 0;
|
||||||
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = original;
|
||||||
|
btn.style.opacity = 1;
|
||||||
|
btn.style.transform = "scale(1)";
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
}, 300);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
const original = btn.textContent;
|
||||||
|
btn.textContent = "❌";
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = original;
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
infoDiv.classList.add("visible");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
infoDiv.classList.remove("playlist-mode");
|
||||||
|
|
||||||
|
// ---------- 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 = `
|
||||||
|
<h3>${data.title}</h3>
|
||||||
|
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||||
|
<ul>
|
||||||
|
<li><strong>Duration :</strong> ${durationStr}</li>
|
||||||
|
<li><strong>Uploader :</strong> ${data.uploader || "Inconnu"}</li>
|
||||||
|
<li><strong>Upload Date :</strong> ${readableDate}</li>
|
||||||
|
<li><strong>Views :</strong> ${data.view_count?.toLocaleString() || "?"}</li>
|
||||||
|
<li><strong>Likes :</strong> ${data.like_count?.toLocaleString() || "?"}</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">${data.channel_url}</a></li>
|
||||||
|
<li><strong>Estimed Size :</strong> ${sizeStr}</li>
|
||||||
|
<li><strong>Category :</strong> ${categories}</li>
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
infoDiv.classList.add("visible");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
})
|
||||||
63
public/script/progressBar.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
|
const progressBar = document.getElementById("downloadProgress");
|
||||||
|
const progressBarText = document.getElementById("downloadProgressText")
|
||||||
|
|
||||||
|
const speedElement = document.getElementById("downloadSpeedText");
|
||||||
|
const speedEvt = new EventSource("/download/speed");
|
||||||
|
|
||||||
|
function startProgress() {
|
||||||
|
progressWrapper.style.display = "block";
|
||||||
|
progressBar.style.width = "0%";
|
||||||
|
progressBarText.style.display = "block";
|
||||||
|
progressBarText.innerHTML = "0%";
|
||||||
|
|
||||||
|
speedElement.style.display = "block";
|
||||||
|
speedElement.innerHTML = "0 Mib/s";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(percent) {
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connexion SSE
|
||||||
|
const evtSource = new EventSource("/download/progress");
|
||||||
|
evtSource.onmessage = e => {
|
||||||
|
if (e.data === "reset") {
|
||||||
|
startProgress();
|
||||||
|
window.electronAPI.setProgress(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.data === "done") {
|
||||||
|
resetProgress();
|
||||||
|
window.electronAPI.setProgress(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percent = parseFloat(e.data);
|
||||||
|
|
||||||
|
if (!isNaN(percent)) {
|
||||||
|
updateProgress(percent);
|
||||||
|
window.electronAPI.setProgress(percent); // update barre des tâches
|
||||||
|
if (percent >= 100) setTimeout(() => {
|
||||||
|
resetProgress();
|
||||||
|
window.electronAPI.setProgress(-1); // retire la barre
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
speedEvt.onmessage = e => {
|
||||||
|
speedElement.style.display = "block";
|
||||||
|
speedElement.textContent = e.data; // ex: "5.2MiB/s"
|
||||||
|
};
|
||||||
24
public/script/topbar.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Ajout des listeners pour la topbar
|
||||||
|
function setupTopbarListeners() {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
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');
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTopbarListeners();
|
||||||
34
public/styles/components/loader.css
Normal 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(--mp3-text-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--mp3-text-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); }
|
||||||
|
}
|
||||||
41
public/styles/components/progressBar.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/* Wrapper de la barre de téléchargement */
|
||||||
|
#downloadProgressWrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 10px auto;
|
||||||
|
display: none;
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barre de progression */
|
||||||
|
#downloadProgress {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--video-info-link-color, #4caf50);
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadProgressText, #downloadSpeedText {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #fff;
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive pour mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#downloadProgressWrapper {
|
||||||
|
max-width: 90%;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadProgressText, #downloadSpeedText {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
.theme-switcher {
|
.theme-switcher {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 60px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
background: #242424;
|
background: #242424;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
z-index: 1000;
|
z-index: 1050;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-switcher label {
|
.theme-switcher label {
|
||||||
|
|||||||
@@ -3,10 +3,23 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
/* height: 10000px; */
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
z-index: 1000;
|
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;
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
116
public/styles/layout/topbar.css
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1100;
|
||||||
|
|
||||||
|
height: 48px;
|
||||||
|
width: 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;
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
#videoInfo.visible {
|
#videoInfo.visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
max-height: 1500px;
|
max-height: 1500px;
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#videoInfo h3 {
|
#videoInfo h3 {
|
||||||
@@ -68,6 +67,81 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Playlist Mode --- */
|
||||||
|
#videoInfo.playlist-mode {
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style du bloc "Playlist détectée" */
|
||||||
|
#videoInfo.playlist-mode > p {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout liste des vidéos */
|
||||||
|
#playlistVideos {
|
||||||
|
margin-top: 1em;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Un item de playlist */
|
||||||
|
#playlistVideos > div {
|
||||||
|
background: var(--playlist-background-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos > div p {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videoInfo.playlist-mode {
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos > div:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
all: unset;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: transform 0.1s ease, color 0.1s ease, opacity 0.15s ease;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos > div p a{
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--video-info-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#playlistVideos > div p a:hover{
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
#videoInfo {
|
#videoInfo {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
/* Layout */
|
||||||
@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/header.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("components/themebutton.css");
|
@import url("components/themebutton.css");
|
||||||
/* @import url("variables.css"); */
|
@import url("components/loader.css");
|
||||||
|
|
||||||
@import url("theme/themeimport.css");
|
@import url("theme/themeimport.css");
|
||||||
@import url("components/editpathbutton.css");
|
@import url("components/editpathbutton.css");
|
||||||
|
@import url("components/progressBar.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');
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ 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;
|
||||||
@@ -33,10 +37,6 @@ html,body{
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
/* background-image: linear-gradient(to left top, #838383, #a1a1a1, #bfbfbf, #dfdfdf, #ffffff);
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-attachment: fixed; */
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: var(--default-text-color);
|
color: var(--default-text-color);
|
||||||
@@ -47,7 +47,47 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar globale pour Chromium/Electron */
|
||||||
|
body::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.35);
|
||||||
|
transform: scaleX(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thème clair */
|
||||||
|
body.light::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container{
|
||||||
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header p{
|
header p{
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
body.chirac {
|
body.chirac {
|
||||||
/* Background image Chirac */
|
|
||||||
background-image: url('../../assets/images/Chirac.png');
|
background-image: url('../../assets/images/Chirac.png');
|
||||||
/* background-position: right; */
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
|
||||||
/* Couleurs drapeau français */
|
|
||||||
--background-color: rgba(255, 255, 255, 0.85); /* blanc légèrement transparent pour lisibilité */
|
--background-color: rgba(255, 255, 255, 0.85);
|
||||||
--default-text-color: #002395; /* bleu foncé */
|
--default-text-color: #002395;
|
||||||
--subtitle-color: #EF4135; /* rouge vif */
|
--subtitle-color: #EF4135;
|
||||||
--mp3-text-color: #002395; /* bleu */
|
--mp3-text-color: #002395;
|
||||||
|
|
||||||
/* Formulaire */
|
/* Formulaire */
|
||||||
--form-bg-color: rgba(255, 255, 255, 0.9);
|
--form-bg-color: rgba(255, 255, 255, 0.9);
|
||||||
@@ -19,9 +18,9 @@ body.chirac {
|
|||||||
--form-input-border-focus-color: #EF4135;
|
--form-input-border-focus-color: #EF4135;
|
||||||
--form-input-text-color: #002395;
|
--form-input-text-color: #002395;
|
||||||
--form-input-placeholder-color: #999999;
|
--form-input-placeholder-color: #999999;
|
||||||
--form-button-bg-color: #002395; /* bleu */
|
--form-button-bg-color: #002395;
|
||||||
--form-button-text-color: #ffffff;
|
--form-button-text-color: #ffffff;
|
||||||
--form-button-bg-hover-color: #EF4135; /* rouge */
|
--form-button-bg-hover-color: #EF4135;
|
||||||
|
|
||||||
/* Checkbox */
|
/* Checkbox */
|
||||||
--checkbox-bg-default: #cccccc;
|
--checkbox-bg-default: #cccccc;
|
||||||
@@ -34,21 +33,17 @@ body.chirac {
|
|||||||
|
|
||||||
/* Video Info Box */
|
/* Video Info Box */
|
||||||
--infos-box-color: rgba(255, 255, 255, 0.9);
|
--infos-box-color: rgba(255, 255, 255, 0.9);
|
||||||
--video-info-text-color: #002395;
|
--video-info-text-color: #000000;
|
||||||
--video-info-heading-color: #EF4135;
|
--video-info-heading-color: #EF4135;
|
||||||
--video-info-list-color: #444444;
|
--video-info-list-color: #444444;
|
||||||
--video-info-list-strong-color: #000000;
|
--video-info-list-strong-color: #000000;
|
||||||
--video-info-link-color: #002395;
|
--video-info-link-color: #002395;
|
||||||
--video-info-link-hover-color: #EF4135;
|
--video-info-link-hover-color: #EF4135;
|
||||||
|
|
||||||
/* animation d'apparition */
|
/* Playlist Color */
|
||||||
opacity: 0;
|
--playlist-background-color: #d1d1d1ca;
|
||||||
transition: opacity 0.8s ease, background-color 0.8s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.chirac.theme-active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
body.cyberpunk {
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #0d0d0d; /* noir profond */
|
|
||||||
--default-text-color: #e0e0e0; /* texte gris clair */
|
|
||||||
--subtitle-color: #ffea00; /* turquoise néon pour les sous-titres */
|
|
||||||
--mp3-text-color: #ffea00; /* jaune vif pour la checkbox */
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #141414; /* fond formulaire sombre */
|
|
||||||
--form-input-bg-color: #1a1a1a; /* inputs sombres */
|
|
||||||
--form-input-border-color: #333333; /* bordure sobre */
|
|
||||||
--form-input-border-focus-color: #ffea00; /* focus jaune néon */
|
|
||||||
--form-input-text-color: #e0e0e0; /* texte clair */
|
|
||||||
--form-input-placeholder-color: #777777; /* placeholder plus doux */
|
|
||||||
--form-button-bg-color: #ffea00; /* bouton jaune */
|
|
||||||
--form-button-text-color: #0d0d0d; /* texte bouton noir */
|
|
||||||
--form-button-bg-hover-color: #d4c000; /* hover jaune plus foncé */
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #333333; /* fond checkbox sobre */
|
|
||||||
--checkbox-bg-checked: #ffea00; /* coché en jaune vif */
|
|
||||||
--checkbox-checkmark-border-color: #0d0d0d; /* checkmark noir */
|
|
||||||
--checkbox-pulse-color: rgba(255, 234, 0, 0.5); /* pulse néon jaune */
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #fffbd1; /* turquoise néon */
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #1a1a1a; /* fond infos */
|
|
||||||
--video-info-text-color: #e0e0e0; /* texte clair */
|
|
||||||
--video-info-heading-color: #ffea00; /* titres turquoise néon */
|
|
||||||
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
|
||||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
|
||||||
--video-info-link-color: #ffea00; /* liens jaune néon */
|
|
||||||
--video-info-link-hover-color: #d4c000; /* liens hover plus foncé */
|
|
||||||
}
|
|
||||||
|
|
||||||
body.cyberpunk {
|
|
||||||
background-image: url('../../assets/images/Cyberpunk.webp');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-attachment: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -33,4 +33,7 @@ body.dark {
|
|||||||
--video-info-list-strong-color: #ffffff;
|
--video-info-list-strong-color: #ffffff;
|
||||||
--video-info-link-color: #2196f3;
|
--video-info-link-color: #2196f3;
|
||||||
--video-info-link-hover-color: #1976d2;
|
--video-info-link-hover-color: #1976d2;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #303030;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,16 @@
|
|||||||
--download-status-color: #007bff;
|
--download-status-color: #007bff;
|
||||||
|
|
||||||
/* Video Info Box */
|
/* Video Info Box */
|
||||||
--infos-box-color: #f0f0f0;
|
--infos-box-color: #0c0140;
|
||||||
--video-info-text-color: #222222;
|
--video-info-text-color: #ffffff;
|
||||||
--video-info-heading-color: #007bff;
|
--video-info-heading-color: #007bff;
|
||||||
--video-info-list-color: #444444;
|
--video-info-list-color: #444444;
|
||||||
--video-info-list-strong-color: #000000;
|
--video-info-list-strong-color: #000000;
|
||||||
--video-info-link-color: #007bff;
|
--video-info-link-color: #007bff;
|
||||||
--video-info-link-hover-color: #0056b3;
|
--video-info-link-hover-color: #0056b3;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #070823a3;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.default .download-path #savePath {
|
body.default .download-path #savePath {
|
||||||
|
|||||||
51
public/styles/theme/drift-theme.css
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
body.drift {
|
||||||
|
|
||||||
|
background-image: url('../../assets/images/Drift.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
|
||||||
|
/* Palette principale */
|
||||||
|
--background-color: #0f1218;
|
||||||
|
--default-text-color: #3d4b61;
|
||||||
|
--subtitle-color: #1a1a1a;
|
||||||
|
--mp3-text-color: #c0d4ff;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #1e222a;
|
||||||
|
--form-input-bg-color: #13171d;
|
||||||
|
--form-input-border-color: #3b4b6d;
|
||||||
|
--form-input-border-focus-color: #6ea8ff;
|
||||||
|
--form-input-text-color: #f0f4ff;
|
||||||
|
--form-input-placeholder-color: #7a8fae;
|
||||||
|
--form-button-bg-color: #2a2979;
|
||||||
|
--form-button-text-color: #ffffff;
|
||||||
|
--form-button-bg-hover-color: #353393;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #2a2e38;
|
||||||
|
--checkbox-bg-checked: #2a2979;
|
||||||
|
--checkbox-checkmark-border-color: #ffffff;
|
||||||
|
--checkbox-pulse-color: rgba(76,108,255,0.3);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #ff8e8e;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #1e222ad0;
|
||||||
|
--video-info-text-color: #e8e8f0;
|
||||||
|
--video-info-heading-color: #9ab6ff;
|
||||||
|
--video-info-list-color: #c0d4ff;
|
||||||
|
--video-info-list-strong-color: #ffffff;
|
||||||
|
--video-info-link-color: #4c6cff;
|
||||||
|
--video-info-link-hover-color: #6c8dff;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #424b5cc2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
body.drift .download-path{
|
||||||
|
color: #ededed;
|
||||||
|
}
|
||||||
@@ -1,36 +1,39 @@
|
|||||||
body.fanatic{
|
body.fanatic{
|
||||||
/* Couleurs générales */
|
/* Couleurs générales */
|
||||||
--background-color: #121212; /* fond bleu nuit/noir */
|
--background-color: #121212;
|
||||||
--default-text-color: #eee; /* texte clair principal */
|
--default-text-color: #eee;
|
||||||
--subtitle-color: #FF5900; /* sous-titres, accent orange vif */
|
--subtitle-color: #FF5900;
|
||||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
--mp3-text-color: #ffffff;
|
||||||
|
|
||||||
/* Formulaire */
|
/* Formulaire */
|
||||||
--form-bg-color: #1e1e1e; /* fond formulaire sombre */
|
--form-bg-color: #1e1e1e;
|
||||||
--form-input-bg-color: #2b2b2b; /* fond inputs sombres */
|
--form-input-bg-color: #2b2b2b;
|
||||||
--form-input-border-color: #333333; /* bordure inputs */
|
--form-input-border-color: #333333;
|
||||||
--form-input-border-focus-color: #FF5900; /* bordure focus orange */
|
--form-input-border-focus-color: #FF5900;
|
||||||
--form-input-text-color: #eeeeee; /* texte inputs clair */
|
--form-input-text-color: #eeeeee;
|
||||||
--form-input-placeholder-color: #666666; /* placeholder gris foncé */
|
--form-input-placeholder-color: #666666;
|
||||||
--form-button-bg-color: #FF5900; /* bouton orange */
|
--form-button-bg-color: #FF5900;
|
||||||
--form-button-text-color: #121212; /* texte bouton sombre */
|
--form-button-text-color: #121212;
|
||||||
--form-button-bg-hover-color: #e65500; /* bouton hover orange foncé */
|
--form-button-bg-hover-color: #e65500;
|
||||||
|
|
||||||
/* Checkbox */
|
/* Checkbox */
|
||||||
--checkbox-bg-default: #555555; /* gris moyen */
|
--checkbox-bg-default: #555555;
|
||||||
--checkbox-bg-checked: #ff6600; /* orange vif */
|
--checkbox-bg-checked: #ff6600;
|
||||||
--checkbox-checkmark-border-color: #eee; /* blanc cassé */
|
--checkbox-checkmark-border-color: #eee;
|
||||||
--checkbox-pulse-color: rgba(255, 102, 0, 0.5); /* pulse orange translucide */
|
--checkbox-pulse-color: rgba(255, 102, 0, 0.5);
|
||||||
|
|
||||||
/* Download status */
|
/* Download status */
|
||||||
--download-status-color: #FF5900; /* orange */
|
--download-status-color: #FF5900;
|
||||||
|
|
||||||
/* Video Info Box */
|
/* Video Info Box */
|
||||||
--infos-box-color: #222222; /* fond infos sombre */
|
--infos-box-color: #222222;
|
||||||
--video-info-text-color: #eee; /* texte clair */
|
--video-info-text-color: #eee;
|
||||||
--video-info-heading-color: #FF5900; /* titres orange */
|
--video-info-heading-color: #FF5900;
|
||||||
--video-info-list-color: #ccc; /* texte liste gris clair */
|
--video-info-list-color: #ccc;
|
||||||
--video-info-list-strong-color: #fff; /* texte important blanc */
|
--video-info-list-strong-color: #fff;
|
||||||
--video-info-link-color: #FF5900; /* liens orange */
|
--video-info-link-color: #FF5900;
|
||||||
--video-info-link-hover-color: #e65500; /* liens hover orange foncé */
|
--video-info-link-hover-color: #e65500;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #2b2b2b;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
body.light {
|
body.light {
|
||||||
/* Couleurs générales */
|
/* Couleurs générales */
|
||||||
--background-color: #dadada; /* fond clair */
|
--background-color: #dadada;
|
||||||
--default-text-color: #222222; /* texte sombre */
|
--default-text-color: #222222;
|
||||||
--subtitle-color: #007bff; /* bleu accent */
|
--subtitle-color: #007bff;
|
||||||
--mp3-text-color: #000000; /* texte checkbox */
|
--mp3-text-color: #000000;
|
||||||
|
|
||||||
/* Formulaire */
|
/* Formulaire */
|
||||||
--form-bg-color: #ffffff; /* fond formulaire blanc */
|
--form-bg-color: #ffffff;
|
||||||
--form-input-bg-color: #fafafa; /* fond inputs */
|
--form-input-bg-color: #fafafa;
|
||||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
--form-input-border-color: #cccccc;
|
||||||
--form-input-border-focus-color: #007bff; /* focus bleu */
|
--form-input-border-focus-color: #007bff;
|
||||||
--form-input-text-color: #333333; /* texte inputs */
|
--form-input-text-color: #333333;
|
||||||
--form-input-placeholder-color: #888888; /* placeholder */
|
--form-input-placeholder-color: #888888;
|
||||||
--form-button-bg-color: #007bff; /* bouton bleu */
|
--form-button-bg-color: #007bff;
|
||||||
--form-button-text-color: #ffffff; /* texte bouton */
|
--form-button-text-color: #ffffff;
|
||||||
--form-button-bg-hover-color: #0056b3; /* hover bleu foncé */
|
--form-button-bg-hover-color: #0056b3;
|
||||||
|
|
||||||
/* Checkbox */
|
/* Checkbox */
|
||||||
--checkbox-bg-default: #cccccc;
|
--checkbox-bg-default: #cccccc;
|
||||||
@@ -33,4 +33,7 @@ body.light {
|
|||||||
--video-info-list-strong-color: #000000;
|
--video-info-list-strong-color: #000000;
|
||||||
--video-info-link-color: #007bff;
|
--video-info-link-color: #007bff;
|
||||||
--video-info-link-hover-color: #0056b3;
|
--video-info-link-hover-color: #0056b3;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #c5c5c5b2;
|
||||||
}
|
}
|
||||||
|
|||||||
50
public/styles/theme/neon-theme.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
body.neon {
|
||||||
|
|
||||||
|
background: (url("../../assets/images/drift.jpg"));
|
||||||
|
background-image: linear-gradient(135deg, #0f0c29, #302b63, #24243e); /* dégradé sombre violet/bleu */
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #1b1b2f;
|
||||||
|
--default-text-color: #e0e0ff;
|
||||||
|
--subtitle-color: #a3a3ff;
|
||||||
|
--mp3-text-color: #c0c0ff;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #2c2c4a;
|
||||||
|
--form-input-bg-color: #1f1f3a;
|
||||||
|
--form-input-border-color: #5858f0;
|
||||||
|
--form-input-border-focus-color: #a3a3ff;
|
||||||
|
--form-input-text-color: #ffffff;
|
||||||
|
--form-input-placeholder-color: #8888ff;
|
||||||
|
--form-button-bg-color: #5858f0;
|
||||||
|
--form-button-text-color: #ffffff;
|
||||||
|
--form-button-bg-hover-color: #7a7aff;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #302b63;
|
||||||
|
--checkbox-bg-checked: #5858f0;
|
||||||
|
--checkbox-checkmark-border-color: #ffffff;
|
||||||
|
--checkbox-pulse-color: rgba(88,88,240,0.3);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #a3a3ff;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #2c2c4a;
|
||||||
|
--video-info-text-color: #e0e0ff;
|
||||||
|
--video-info-heading-color: #a3a3ff;
|
||||||
|
--video-info-list-color: #c0c0ff;
|
||||||
|
--video-info-list-strong-color: #ffffff;
|
||||||
|
--video-info-link-color: #5858f0;
|
||||||
|
--video-info-link-hover-color: #7a7aff;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #3e3e64;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.neon .download-path {
|
||||||
|
color: var(--default-text-color);
|
||||||
|
}
|
||||||
@@ -1,46 +1,47 @@
|
|||||||
body.nf {
|
body.nf {
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #1a1a1a; /* fond sombre neutre */
|
|
||||||
--default-text-color: #e6dfd5; /* texte beige clair */
|
|
||||||
--subtitle-color: #f5f0e6; /* beige très clair (titres) */
|
|
||||||
--mp3-text-color: #d8cfc2; /* texte checkbox */
|
|
||||||
|
|
||||||
/* Formulaire */
|
background-image: url('../../assets/images/NF.jpg');
|
||||||
--form-bg-color: #2a2a2a; /* fond formulaire gris sombre */
|
|
||||||
--form-input-bg-color: #222222; /* fond inputs sombre */
|
|
||||||
--form-input-border-color: #4a4036; /* bordure beige/brun */
|
|
||||||
--form-input-border-focus-color: #cbb89d; /* beige doré en focus */
|
|
||||||
--form-input-text-color: #f0e8dd; /* texte clair/beige */
|
|
||||||
--form-input-placeholder-color: #9a8f80; /* placeholder beige/gris */
|
|
||||||
--form-button-bg-color: #cbb89d; /* bouton beige doré */
|
|
||||||
--form-button-text-color: #1a1a1a; /* texte sombre */
|
|
||||||
--form-button-bg-hover-color: #b7a588; /* hover beige plus foncé */
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #4a4036; /* brun/gris */
|
|
||||||
--checkbox-bg-checked: #cbb89d; /* beige doré */
|
|
||||||
--checkbox-checkmark-border-color: #1a1a1a; /* noir profond */
|
|
||||||
--checkbox-pulse-color: rgba(203,184,157,0.3); /* halo beige doux */
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #cbb89d; /* beige doré */
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #2a2a2a; /* fond sombre */
|
|
||||||
--video-info-text-color: #e6dfd5; /* texte beige clair */
|
|
||||||
--video-info-heading-color: #f5f0e6; /* titres beige clair */
|
|
||||||
--video-info-list-color: #d8cfc2; /* texte liste */
|
|
||||||
--video-info-list-strong-color: #ffffff; /* texte fort blanc cassé */
|
|
||||||
--video-info-link-color: #cbb89d; /* liens beige doré */
|
|
||||||
--video-info-link-hover-color: #b7a588; /* hover beige plus foncé */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fond inspiré de ton image beige */
|
|
||||||
body.nf {
|
|
||||||
background-image: url('../../assets/images/NF.jpg'); /* mets ton image ici */
|
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: 70% center;
|
background-position: 70% center;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #1a1a1a;
|
||||||
|
--default-text-color: #e6dfd5;
|
||||||
|
--subtitle-color: #f5f0e6;
|
||||||
|
--mp3-text-color: #d8cfc2;
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: #2a2a2a;
|
||||||
|
--form-input-bg-color: #222222;
|
||||||
|
--form-input-border-color: #4a4036;
|
||||||
|
--form-input-border-focus-color: #cbb89d;
|
||||||
|
--form-input-text-color: #f0e8dd;
|
||||||
|
--form-input-placeholder-color: #9a8f80;
|
||||||
|
--form-button-bg-color: #cbb89d;
|
||||||
|
--form-button-text-color: #1a1a1a;
|
||||||
|
--form-button-bg-hover-color: #b7a588;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #4a4036;
|
||||||
|
--checkbox-bg-checked: #cbb89d;
|
||||||
|
--checkbox-checkmark-border-color: #1a1a1a;
|
||||||
|
--checkbox-pulse-color: rgba(203,184,157,0.3);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #cbb89d;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: #2a2a2a;
|
||||||
|
--video-info-text-color: #e6dfd5;
|
||||||
|
--video-info-heading-color: #f5f0e6;
|
||||||
|
--video-info-list-color: #d8cfc2;
|
||||||
|
--video-info-list-strong-color: #ffffff;
|
||||||
|
--video-info-link-color: #cbb89d;
|
||||||
|
--video-info-link-hover-color: #b7a588;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #4f4f4f;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.nf .download-path {
|
body.nf .download-path {
|
||||||
|
|||||||
49
public/styles/theme/songbird-theme.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
body.songbird {
|
||||||
|
|
||||||
|
background-image: url('../../assets/images/Songbird.webp');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
|
||||||
|
/* Couleurs générales */
|
||||||
|
--background-color: #0a0a0a; /* noir profond */
|
||||||
|
--default-text-color: #ff2a2a; /* rouge néon */
|
||||||
|
--subtitle-color: #f2f2f2; /* rouge sombre */
|
||||||
|
--mp3-text-color: #ff4d4d; /* rouge un peu plus clair */
|
||||||
|
|
||||||
|
/* Formulaire */
|
||||||
|
--form-bg-color: rgba(20,0,0,0.85); /* noir semi-transparent avec rouge */
|
||||||
|
--form-input-bg-color: #1a0a0a;
|
||||||
|
--form-input-border-color: #ff2a2a;
|
||||||
|
--form-input-border-focus-color: #b30000;
|
||||||
|
--form-input-text-color: #ffffff;
|
||||||
|
--form-input-placeholder-color: #950404;
|
||||||
|
--form-button-bg-color: #b30000;
|
||||||
|
--form-button-text-color: #ffffff;
|
||||||
|
--form-button-bg-hover-color: #ff2a2a;
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
--checkbox-bg-default: #630000;
|
||||||
|
--checkbox-bg-checked: #ff2a2a;
|
||||||
|
--checkbox-checkmark-border-color: #ffffff;
|
||||||
|
--checkbox-pulse-color: rgba(255,42,42,0.3);
|
||||||
|
|
||||||
|
/* Download status */
|
||||||
|
--download-status-color: #ff2a2a;
|
||||||
|
|
||||||
|
/* Video Info Box */
|
||||||
|
--infos-box-color: rgba(30,0,0,0.9);
|
||||||
|
--video-info-text-color: #ff4d4d;
|
||||||
|
--video-info-heading-color: #ff2a2a;
|
||||||
|
--video-info-list-color: #b30000;
|
||||||
|
--video-info-list-strong-color: #ffffff;
|
||||||
|
--video-info-link-color: #ff2a2a;
|
||||||
|
--video-info-link-hover-color: #b30000;
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #3f0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.cyberpunk .download-path {
|
||||||
|
color: var(--default-text-color);
|
||||||
|
}
|
||||||
@@ -1,50 +1,48 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
body.spicy {
|
body.spicy {
|
||||||
/* Couleurs générales */
|
/* Couleurs générales */
|
||||||
--background-color: #121212; /* fond noir profond */
|
--background-color: #121212; /* fond noir profond */
|
||||||
--default-text-color: #581010; /* texte clair */
|
--default-text-color: #ffffff; /* texte clair */
|
||||||
--subtitle-color: #3d0909; /* accent rouge piment */
|
--subtitle-color: #f87918; /* accent rouge piment */
|
||||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
||||||
|
|
||||||
/* Formulaire */
|
/* Formulaire */
|
||||||
--form-bg-color: #1a1a1a; /* fond formulaire gris très foncé */
|
--form-bg-color: #1a1a1a; /* fond formulaire gris très foncé */
|
||||||
--form-input-bg-color: #2b2b2b; /* fond inputs gris foncé */
|
--form-input-bg-color: #2b2b2b; /* fond inputs gris foncé */
|
||||||
--form-input-border-color: #444444; /* bordure gris */
|
--form-input-border-color: #444444; /* bordure gris */
|
||||||
--form-input-border-focus-color: #ff1a1a; /* focus rouge piment */
|
--form-input-border-focus-color: #ff1a1a; /* focus rouge piment */
|
||||||
--form-input-text-color: #eeeeee; /* texte inputs */
|
--form-input-text-color: #eeeeee; /* texte inputs */
|
||||||
--form-input-placeholder-color: #888888; /* placeholder gris moyen */
|
--form-input-placeholder-color: #888888; /* placeholder gris moyen */
|
||||||
--form-button-bg-color: #ff1a1a; /* bouton rouge vif */
|
--form-button-bg-color: #ff1a1a; /* bouton rouge vif */
|
||||||
--form-button-text-color: #ffffff; /* texte bouton blanc */
|
--form-button-text-color: #ffffff; /* texte bouton blanc */
|
||||||
--form-button-bg-hover-color: #cc0000; /* hover rouge foncé */
|
--form-button-bg-hover-color: #cc0000; /* hover rouge foncé */
|
||||||
|
|
||||||
/* Checkbox */
|
/* Checkbox */
|
||||||
--checkbox-bg-default: #555555; /* gris moyen */
|
--checkbox-bg-default: #555555; /* gris moyen */
|
||||||
--checkbox-bg-checked: #ff1a1a; /* rouge piment */
|
--checkbox-bg-checked: #ff1a1a; /* rouge piment */
|
||||||
--checkbox-checkmark-border-color: #f5f5f5; /* blanc cassé */
|
--checkbox-checkmark-border-color: #f5f5f5; /* blanc cassé */
|
||||||
--checkbox-pulse-color: rgba(255, 26, 26, 0.5); /* pulse rouge translucide */
|
--checkbox-pulse-color: rgba(255, 26, 26, 0.5); /* pulse rouge translucide */
|
||||||
|
|
||||||
/* Download status */
|
/* Download status */
|
||||||
--download-status-color: #ffffff; /* rouge vif */
|
--download-status-color: #ffffff; /* rouge vif */
|
||||||
|
|
||||||
/* Video Info Box */
|
/* Video Info Box */
|
||||||
--infos-box-color: #1a1a1a; /* fond infos gris foncé */
|
--infos-box-color: #1a1a1a; /* fond infos gris foncé */
|
||||||
--video-info-text-color: #f5f5f5; /* texte clair */
|
--video-info-text-color: #f5f5f5; /* texte clair */
|
||||||
--video-info-heading-color: #ff1a1a; /* titres rouges */
|
--video-info-heading-color: #ff1a1a; /* titres rouges */
|
||||||
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
||||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
||||||
--video-info-link-color: #ff1a1a; /* liens rouges */
|
--video-info-link-color: #ff1a1a; /* liens rouges */
|
||||||
--video-info-link-hover-color: #cc0000; /* liens hover rouge foncé */
|
--video-info-link-hover-color: #cc0000; /* liens hover rouge foncé */
|
||||||
|
|
||||||
|
/* Playlist Color */
|
||||||
|
--playlist-background-color: #353232;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optionnel: background piments */
|
/* Optionnel: background piments */
|
||||||
body.spicy {
|
body.spicy {
|
||||||
background-image: url('../../assets/images/Spicy.webp');
|
background-image: url('../../assets/images/Spicy.png');
|
||||||
background-size:contain;
|
background-size:cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
7
public/styles/theme/themeimport.css
vendored
@@ -3,7 +3,8 @@
|
|||||||
@import url("default-theme.css");
|
@import url("default-theme.css");
|
||||||
@import url("fanatic-theme.css");
|
@import url("fanatic-theme.css");
|
||||||
@import url("light-theme.css");
|
@import url("light-theme.css");
|
||||||
@import url("cyberpunk-theme.css");
|
|
||||||
@import url("spicy-theme.css");
|
|
||||||
@import url("vilbrequin-theme.css");
|
|
||||||
@import url("nf-theme.css");
|
@import url("nf-theme.css");
|
||||||
|
@import url("drift-theme.css");
|
||||||
|
@import url("neon-theme.css");
|
||||||
|
@import url("songbird-theme.css");
|
||||||
|
@import url("spicy-theme.css")
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
body.vilbrequin {
|
|
||||||
/* Couleurs générales */
|
|
||||||
--background-color: #0a1f3d; /* bleu Vilbrequin profond */
|
|
||||||
--default-text-color: #535353; /* texte clair */
|
|
||||||
--subtitle-color: #001696; /* bleu clair accent (turbo vibes) */
|
|
||||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
--form-bg-color: #12284f; /* fond formulaire bleu foncé */
|
|
||||||
--form-input-bg-color: #1c3760; /* fond inputs bleu + clair */
|
|
||||||
--form-input-border-color: #2a4b80; /* bordure bleue */
|
|
||||||
--form-input-border-focus-color: #00bfff; /* focus bleu clair */
|
|
||||||
--form-input-text-color: #f0f0f0; /* texte inputs clair */
|
|
||||||
--form-input-placeholder-color: #b0c4de; /* placeholder gris bleuté */
|
|
||||||
--form-button-bg-color: #ffd700; /* bouton jaune turbo */
|
|
||||||
--form-button-text-color: #0a1f3d; /* texte bouton sombre */
|
|
||||||
--form-button-bg-hover-color: #e6c200; /* hover jaune foncé */
|
|
||||||
|
|
||||||
/* Checkbox */
|
|
||||||
--checkbox-bg-default: #2a4b80; /* bleu moyen */
|
|
||||||
--checkbox-bg-checked: #00bfff; /* bleu clair accent */
|
|
||||||
--checkbox-checkmark-border-color: #f0f0f0; /* blanc cassé */
|
|
||||||
--checkbox-pulse-color: rgba(0,191,255,0.5); /* pulse bleu clair translucide */
|
|
||||||
|
|
||||||
/* Download status */
|
|
||||||
--download-status-color: #ffd700; /* jaune turbo */
|
|
||||||
|
|
||||||
/* Video Info Box */
|
|
||||||
--infos-box-color: #12284f; /* fond infos bleu foncé */
|
|
||||||
--video-info-text-color: #f0f0f0; /* texte clair */
|
|
||||||
--video-info-heading-color: #00bfff; /* titres bleu clair */
|
|
||||||
--video-info-list-color: #dcdcdc; /* texte liste gris clair */
|
|
||||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
|
||||||
--video-info-link-color: #00bfff; /* liens bleu clair */
|
|
||||||
--video-info-link-hover-color: #009acd; /* liens hover bleu plus foncé */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optionnel: motif turbo ou jantes */
|
|
||||||
body.vilbrequin {
|
|
||||||
background-image: url('../../assets/images/Vilbrequin.webp'); /* adapte ton image */
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-attachment: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.vilbrequin .download-path{
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
:root {
|
:root {
|
||||||
/* Couleurs générales */
|
/* Couleurs générales */
|
||||||
--background-color: #001224; /* body background bleu nuit */
|
--background-color: #001224;
|
||||||
--default-text-color: #eee; /* texte clair principal */
|
--default-text-color: #eee;
|
||||||
--subtitle-color: #007bff; /* sous-titres, header p */
|
--subtitle-color: #007bff;
|
||||||
--mp3-text-color: #000000; /* couleur texte checkbox inline */
|
--mp3-text-color: #000000;
|
||||||
|
|
||||||
/* Formulaire */
|
/* Formulaire */
|
||||||
--form-bg-color: #ffffff; /* fond du formulaire */
|
--form-bg-color: #ffffff;
|
||||||
--form-input-bg-color: #f9f9f9; /* fond inputs */
|
--form-input-bg-color: #f9f9f9;
|
||||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
--form-input-border-color: #cccccc;
|
||||||
--form-input-border-focus-color: #0056b3; /* bordure focus */
|
--form-input-border-focus-color: #0056b3;
|
||||||
--form-input-text-color: #333333; /* texte inputs */
|
--form-input-text-color: #333333;
|
||||||
--form-input-placeholder-color: #aaaaaa; /* placeholder */
|
--form-input-placeholder-color: #aaaaaa;
|
||||||
--form-button-bg-color: #007bff; /* bouton bg */
|
--form-button-bg-color: #007bff;
|
||||||
--form-button-text-color: #ffffff; /* texte bouton */
|
--form-button-text-color: #ffffff;
|
||||||
--form-button-bg-hover-color: #0056b3; /* bouton hover */
|
--form-button-bg-hover-color: #0056b3;
|
||||||
|
|
||||||
/* Checkbox */
|
/* Checkbox */
|
||||||
--checkbox-bg-default: #cccccc;
|
--checkbox-bg-default: #cccccc;
|
||||||
|
|||||||
65
server/controller/download.controller.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
const { fetchDownload } = require("../services/download.services");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
const { isValidUrl, isSafePath } = require("../helpers/validation");
|
||||||
|
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
||||||
|
const { notifyDownloadFinished } = require("../helpers/notify");
|
||||||
|
|
||||||
|
const listeners = [];
|
||||||
|
const speedListeners = [];
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !");
|
||||||
|
if (options.outputFolder && !isSafePath(options.outputFolder)) return res.status(400).send("❌ Save Path Not Allowed.");
|
||||||
|
|
||||||
|
const filePath = await fetchDownload(options, listeners, speedListeners);
|
||||||
|
notifyDownloadFinished(filePath);
|
||||||
|
res.send("✅ Download Done !");
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Server Error in /download : ${err.message}`);
|
||||||
|
res.status(500).send(`❌ ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { downloadController, progressController, speedController};
|
||||||
48
server/controller/info.controller.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const { fetchInfo } = require("../services/info.services");
|
||||||
|
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
const { isValidUrl } = require("../helpers/validation");
|
||||||
|
|
||||||
|
async function infoController(req, res){
|
||||||
|
|
||||||
|
const url = req.body.url || req.query.url; // Gérer POST et GET
|
||||||
|
|
||||||
|
|
||||||
|
/* Si pas d'url ou url invalide*/
|
||||||
|
if (!url || !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")) {
|
||||||
|
logger.info("Estimated Data Type: Playlist")
|
||||||
|
|
||||||
|
} else{
|
||||||
|
logger.info("Estimated Data Type: Video")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchInfo(url);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return res.status(500).send(`❌ ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {infoController};
|
||||||
@@ -1,47 +1,51 @@
|
|||||||
/*
|
const config = require('../config');
|
||||||
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 RPC = require("discord-rpc");
|
const RPC = require("discord-rpc");
|
||||||
const clientId = "1410934537051181146";
|
const { logger } = require("./logger");
|
||||||
|
|
||||||
|
const clientId = `${config.DiscordRPCID}`;
|
||||||
const rpc = new RPC.Client({ transport: "ipc" });
|
const rpc = new RPC.Client({ transport: "ipc" });
|
||||||
|
|
||||||
|
let intervalId;
|
||||||
|
|
||||||
function startRPC() {
|
function startRPC() {
|
||||||
rpc.on("ready", () => {
|
rpc.on("ready", () => {
|
||||||
console.log("✅ Connecté à Discord !");
|
|
||||||
|
|
||||||
const presence = {
|
const presence = {
|
||||||
largeImageKey: "icon",
|
largeImageKey: "icon",
|
||||||
smallImageKey: "acnolo_pfp",
|
smallImageKey: "acnolo_pfp",
|
||||||
smallImageText: "By MasterAcnolo",
|
smallImageText: "By MasterAcnolo",
|
||||||
startTimestamp: new Date(), // compteur de temps
|
startTimestamp: new Date(),
|
||||||
details: "github.com/MasterAcnolo/Freedom-Loader",
|
details: `Open Source Download Tools - ${config.version}`,
|
||||||
|
state: "masteracnolo.github.io/FreedomLoader",
|
||||||
};
|
};
|
||||||
|
|
||||||
rpc.setActivity(presence);
|
rpc.setActivity(presence);
|
||||||
|
|
||||||
// Mise à jour toutes les 15 secondes si tu veux garder le compteur à jour
|
// Met à jour la présence toutes les 15s
|
||||||
setInterval(() => {
|
intervalId = setInterval(() => {
|
||||||
rpc.setActivity(presence);
|
rpc.setActivity(presence);
|
||||||
}, 15000);
|
}, 15000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function cleanExit() {
|
||||||
|
try {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
|
||||||
|
if (rpc && rpc.transport) {
|
||||||
|
await rpc.destroy();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Error while closing the RPC:", err);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("exit", cleanExit);
|
||||||
|
process.on("SIGINT", cleanExit);
|
||||||
|
process.on("SIGTERM", cleanExit);
|
||||||
|
|
||||||
rpc.login({ clientId }).catch(err => {
|
rpc.login({ clientId }).catch(err => {
|
||||||
console.error("Erreur Discord RPC :", err);
|
logger.error("Unable to connect to the RPC:", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
server/helpers/buildArgs.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const getUserBrowser = require("./getBrowser");
|
||||||
|
const { ffmpegPath, denoPath} = require("./path");
|
||||||
|
|
||||||
|
|
||||||
|
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
"--cookies-from-browser", `${getUserBrowser()}`,
|
||||||
|
"--no-continue",
|
||||||
|
"--no-overwrites",
|
||||||
|
"--embed-thumbnail",
|
||||||
|
"--add-metadata",
|
||||||
|
"--concurrent-fragments", "8",
|
||||||
|
"--retries", "10",
|
||||||
|
"--fragment-retries", "10",
|
||||||
|
"--ffmpeg-location", ffmpegPath,
|
||||||
|
"--extractor-args","youtube:player_client=default",
|
||||||
|
"--js-runtimes", `deno:${denoPath}`
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { buildYtDlpArgs };
|
||||||
|
|
||||||
32
server/helpers/getBrowser.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const notify = require("./notify")
|
||||||
|
|
||||||
|
function getUserBrowser() {
|
||||||
|
const userProfile = os.homedir();
|
||||||
|
|
||||||
|
// Liste des navigateurs supportés par yt-dlp (Actuellement je peux que garantir Firefox, Désolé)
|
||||||
|
const browsers = [
|
||||||
|
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
||||||
|
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
||||||
|
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
|
||||||
|
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
|
||||||
|
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
|
||||||
|
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
|
||||||
|
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
|
||||||
|
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const browser of browsers) {
|
||||||
|
if (fs.existsSync(browser.path)) {
|
||||||
|
return browser.name;
|
||||||
|
} else{
|
||||||
|
notify.notifyFirefoxBrowserMissing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notify.notifyCookiesBrowserError()
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getUserBrowser
|
||||||
44
server/helpers/notify.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const { Notification, shell } = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
function notifyDownloadFinished(folder) {
|
||||||
|
const iconPath = path.join(process.resourcesPath, "confirm-icon.png");
|
||||||
|
const notif = new Notification({
|
||||||
|
title: "Freedom Loader",
|
||||||
|
body: "Your download is complete, click here to open it.",
|
||||||
|
icon: iconPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("click", () => shell.openPath(folder));
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyCookiesBrowserError(){
|
||||||
|
const iconPath = path.join(process.resourcesPath, "error.png");
|
||||||
|
const notif = new Notification({
|
||||||
|
title: "Cookies Error",
|
||||||
|
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
||||||
|
icon: iconPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyFirefoxBrowserMissing(){
|
||||||
|
const iconPath = path.join(process.ressourcesPath, "error.png");
|
||||||
|
const notif = new Notification({
|
||||||
|
title: "Firefox Missing",
|
||||||
|
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
||||||
|
icon: iconPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))
|
||||||
|
notif.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
notifyDownloadFinished,
|
||||||
|
notifyCookiesBrowserError,
|
||||||
|
notifyFirefoxBrowserMissing
|
||||||
|
};
|
||||||
33
server/helpers/parseInfo.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
function parseVideo(data) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "video",
|
||||||
|
// id: data.id,
|
||||||
|
// title: data.title,
|
||||||
|
// url: data.webpage_url,
|
||||||
|
// duration: data.duration,
|
||||||
|
// thumbnail: data.thumbnail,
|
||||||
|
// uploader: data.uploader
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePlaylist(data) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "playlist",
|
||||||
|
title: data.title || data.id,
|
||||||
|
channel: data.uploader,
|
||||||
|
count: data.entries.length,
|
||||||
|
videos: (data.entries || []).map(v => ({
|
||||||
|
id: v.id,
|
||||||
|
title: v.title,
|
||||||
|
url: v.webpage_url,
|
||||||
|
duration: v.duration,
|
||||||
|
thumbnail: v.thumbnail,
|
||||||
|
uploader: v.uploader
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parseVideo, parsePlaylist };
|
||||||
26
server/helpers/path.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { app } = require("electron");
|
||||||
|
const config = require("../../config");
|
||||||
|
|
||||||
|
let userYtDlp;
|
||||||
|
let ffmpegPath;
|
||||||
|
let denoPath;
|
||||||
|
|
||||||
|
const sourceYtDlp = path.join(process.resourcesPath, "yt-dlp.exe");
|
||||||
|
|
||||||
|
if (config.localMode) {
|
||||||
|
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
|
||||||
|
ffmpegPath = path.join(__dirname, "../../ressources"); // <- contient ffmpeg.exe et ffprobe.exe
|
||||||
|
denoPath = path.join(__dirname, "../../ressources/deno.exe");
|
||||||
|
} else {
|
||||||
|
userYtDlp = path.join(app.getPath("userData"), "yt-dlp.exe");
|
||||||
|
ffmpegPath = path.join(process.resourcesPath, "ffmpeg.exe");
|
||||||
|
denoPath = path.join(process.resourcesPath, "deno.exe");
|
||||||
|
|
||||||
|
if (!fs.existsSync(userYtDlp)) {
|
||||||
|
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { userYtDlp, ffmpegPath, denoPath };
|
||||||
21
server/helpers/validation.js
Normal 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", "/etc", "\\Windows"];
|
||||||
|
const resolved = path.resolve(folder);
|
||||||
|
return !unsafe.some(u => resolved.includes(u));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { isValidUrl, isSafePath };
|
||||||
115
server/logger.js
@@ -1,107 +1,58 @@
|
|||||||
/*
|
|
||||||
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 { createLogger, format, transports } = require("winston");
|
const { createLogger, format, transports } = require("winston");
|
||||||
const DailyRotateFile = require("winston-daily-rotate-file");
|
const DailyRotateFile = require("winston-daily-rotate-file");
|
||||||
const fs = require("fs"); // Pour vérifier/créer le dossier de logs
|
const fs = require("fs");
|
||||||
const path = require("path"); // Pour gérer proprement les chemins
|
const path = require("path");
|
||||||
const os = require("os"); // Pour gérer les chemins aussi
|
const os = require("os");
|
||||||
|
const config = require("../config")
|
||||||
|
|
||||||
// Définition du dossier où seront stockés les logs
|
// Dossier de logs Windows
|
||||||
// const logDir = path.join(__dirname, "../logs"); Désactivé parce que cela requiert d'exec en admin mode. Donc pas pratique pour les gens
|
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||||
|
|
||||||
let logDir;
|
// Création du dossier "logs" si nécessaire
|
||||||
if (process.platform === "win32") {
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
|
||||||
} else {
|
|
||||||
logDir = path.join(os.homedir(), ".freedomloader", "logs");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Création du dossier logs s’il n’existe pas encore
|
const logFormat = format.combine(
|
||||||
//if (!fs.existsSync(logDir)) fs.mkdirSync(logDir); OBSOLETE
|
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
|
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||||
|
);
|
||||||
|
|
||||||
fs.mkdirSync(logDir, { recursive: true }); // Evite les erreurs
|
// Configuration du logger
|
||||||
|
|
||||||
|
|
||||||
// Fonction utilitaire pour générer une ligne indiquant le début d’une session de logs
|
|
||||||
function getSessionStartLine() {
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
return `--- Démarrage de la session : ${now} ---`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction utilitaire pour générer une ligne indiquant la fin d’une session de logs
|
|
||||||
function getSessionEndLine() {
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
return `--- Fin de la session : ${now} ---`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration principale de Winston
|
|
||||||
// - Niveau minimum d’écriture : info (enregistre info, warn, error, etc.)
|
|
||||||
// - Formatage des logs avec timestamp lisible et format personnalisé
|
|
||||||
// - Transports : écriture dans fichier journalier + console avec couleurs
|
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
level: "info",
|
level: "info",
|
||||||
format: format.combine(
|
format: logFormat,
|
||||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
||||||
format.printf(({ timestamp, level, message }) =>
|
|
||||||
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
|
||||||
)
|
|
||||||
),
|
|
||||||
transports: [
|
transports: [
|
||||||
// Rotation quotidienne des fichiers de logs
|
|
||||||
new DailyRotateFile({
|
new DailyRotateFile({
|
||||||
dirname: logDir, // dossier cible pour les logs
|
dirname: logDir,
|
||||||
filename: "LOGS-%DATE%.log", // nom du fichier par date
|
filename: "LOGS-%DATE%.log",
|
||||||
datePattern: "YYYY-MM-DD", // pattern de date dans le nom
|
datePattern: "YYYY-MM-DD",
|
||||||
zippedArchive: false, // ne pas compresser les archives
|
zippedArchive: false,
|
||||||
maxFiles: "7d", // conserve 14 jours d’historique
|
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 ajout à la fin (append)
|
|
||||||
}),
|
}),
|
||||||
// Affichage des logs en console avec coloration et format simple
|
|
||||||
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}`
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper pour marquer clairement le début de session dans les logs
|
function getSessionStartLine() {
|
||||||
function logSessionStart() {
|
return `--- Starting session: ${new Date().toISOString()} ---`;
|
||||||
logger.info(getSessionStartLine());
|
}
|
||||||
|
|
||||||
|
function getSessionEndLine() {
|
||||||
|
return `--- Ending session: ${new Date().toISOString()} ---`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logSessionStart() {
|
||||||
|
logger.info(getSessionStartLine());
|
||||||
|
logger.info(`Application Version: ${config.version}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper pour marquer la fin de session dans les logs
|
|
||||||
function logSessionEnd() {
|
function logSessionEnd() {
|
||||||
logger.info(getSessionEndLine());
|
logger.info(getSessionEndLine());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export du logger principal et des helpers pour usage dans d’autres modules
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
logger,
|
logger,
|
||||||
logSessionStart,
|
logSessionStart,
|
||||||
|
|||||||
@@ -1,197 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const express = require("express");
|
|
||||||
const router = express.Router();
|
|
||||||
const { execFile } = require("child_process");
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
const { Notification } = require("electron");
|
|
||||||
const logger = require("../logger").logger;
|
|
||||||
|
|
||||||
// Path vers le fichier exécutable yt-dlp (outil tiers pour le téléchargement)
|
|
||||||
// const ytDlpPath = path.join(process.resourcesPath, '../../yt-dlp.exe');
|
|
||||||
const ytDlpPath = path.join(process.resourcesPath, 'yt-dlp 2025.08.27.exe');
|
|
||||||
|
|
||||||
router.post("/", (req, res) => {
|
|
||||||
try {
|
|
||||||
// Récupération des options envoyées par le frontend via le formulaire
|
|
||||||
// On vérifie la présence de l'URL, le choix audioOnly, et la qualité demandée
|
|
||||||
const options = {
|
|
||||||
url: req.body.url,
|
|
||||||
audioOnly: req.body.audioOnly === "1",
|
|
||||||
quality: req.body.quality || "best",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validation simple : si l'URL est manquante, on rejette la requête
|
|
||||||
if (!options.url) {
|
|
||||||
logger.warn("Requête POST /download sans URL");
|
|
||||||
return res.status(400).send("❌ URL manquante !");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupération du chemin de sauvegarde personnalisé envoyé depuis Electron,
|
|
||||||
// ou on utilise un chemin par défaut stocké dans app.locals
|
|
||||||
let requestedOutputFolder = req.body.savePath || req.app.locals.outputFolder;
|
|
||||||
|
|
||||||
// On normalise le chemin pour éviter des erreurs liées aux séparateurs ou chemins relatifs
|
|
||||||
requestedOutputFolder = path.normalize(requestedOutputFolder);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Sécurité basique : On refuse certains chemins sensibles ou suspects
|
|
||||||
(ex: System32, /etc, Windows) pour éviter que l'application n'écrive dans des dossiers système.
|
|
||||||
On vérifie aussi la longueur minimale du chemin pour éviter des valeurs vides ou invalides.
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
!requestedOutputFolder ||
|
|
||||||
requestedOutputFolder.length < 3 ||
|
|
||||||
requestedOutputFolder.includes("System32") ||
|
|
||||||
requestedOutputFolder.includes("/etc") ||
|
|
||||||
requestedOutputFolder.includes("\\Windows")
|
|
||||||
) {
|
|
||||||
logger.warn(`Chemin potentiellement dangereux refusé : ${requestedOutputFolder}`);
|
|
||||||
return res.status(400).send("❌ Chemin de sauvegarde non autorisé.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifie que le dossier de sortie existe, sinon le crée récursivement
|
|
||||||
if (!fs.existsSync(requestedOutputFolder)) {
|
|
||||||
fs.mkdirSync(requestedOutputFolder, { recursive: true });
|
|
||||||
logger.info(`Dossier de sortie créé : ${requestedOutputFolder}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction du template de sortie pour yt-dlp (nom de fichier basé sur le titre)
|
|
||||||
const outputTemplate = path.join(requestedOutputFolder, "%(title)s.%(ext)s");
|
|
||||||
|
|
||||||
// Construction du tableau d'arguments à passer à yt-dlp
|
|
||||||
// --no-continue pour forcer un téléchargement propre
|
|
||||||
// --restrict-filenames pour éviter les caractères problématiques dans les noms
|
|
||||||
const args = [
|
|
||||||
"--no-continue", // pas de reprise, c'est un choix que j'ai fait voila
|
|
||||||
//"--restrict-filenames", // noms de fichiers sans caractères spéciaux (Cyrilique, accents, etc.)
|
|
||||||
"--no-overwrites", // évite d'écraser un fichier existant
|
|
||||||
"--embed-thumbnail", // ajoute la pochette
|
|
||||||
"--add-metadata", // ajoute les tags (titre, artiste, etc.)
|
|
||||||
"--concurrent-fragments", "8",// accélère le téléchargement
|
|
||||||
"--retries", "10", // réessaie jusqu'à 10 fois en cas d'erreur
|
|
||||||
"--fragment-retries", "10" , // réessaie aussi 10 fois chaque fragment
|
|
||||||
"--ffmpeg-location", path.join(process.resourcesPath, "ffmpeg.exe")
|
|
||||||
];
|
|
||||||
|
|
||||||
// Si l'option audioOnly est activée, on ajoute les flags pour extraction audio en mp3
|
|
||||||
if (options.audioOnly) {
|
|
||||||
args.push(
|
|
||||||
"-f",
|
|
||||||
"bestaudio",
|
|
||||||
"--extract-audio",
|
|
||||||
"--audio-format",
|
|
||||||
"mp3",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Correspondance entre la qualité choisie et le format à demander à yt-dlp
|
|
||||||
const qualityMap = {
|
|
||||||
best: "bestvideo+bestaudio/best",
|
|
||||||
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]",
|
|
||||||
worst: "worstvideo+worstaudio/worst",
|
|
||||||
1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]",
|
|
||||||
720: "bestvideo[height<=720]+bestaudio/best[height<=720]",
|
|
||||||
480: "bestvideo[height<=480]+bestaudio/best[height<=480]",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sélection du format basé sur la qualité, par défaut 'best' si option invalide
|
|
||||||
const format = qualityMap[options.quality] || "best";
|
|
||||||
args.push("-f", format);
|
|
||||||
|
|
||||||
// Ajout du template de sortie et de l'URL à télécharger
|
|
||||||
args.push("-o", outputTemplate);
|
|
||||||
args.push(options.url);
|
|
||||||
|
|
||||||
// Log de la commande complète pour traçabilité et débogage
|
|
||||||
logger.info(`Téléchargement demandé : url=${options.url}, audioOnly=${options.audioOnly}, quality=${options.quality}`);
|
|
||||||
logger.info(`Commande yt-dlp : ${ytDlpPath} ${args.join(" ")}`);
|
|
||||||
|
|
||||||
// Lancement du processus yt-dlp avec les arguments définis
|
|
||||||
const child = execFile(ytDlpPath, args);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Gestion des sorties standards (stdout) du processus,
|
|
||||||
on log chaque ligne pour suivre la progression ou informations diverses
|
|
||||||
*/
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
data.toString().split("\n").forEach(line => {
|
|
||||||
if (line.trim()) logger.info(`[yt-dlp stdout] ${line.trim()}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
Gestion des erreurs (stderr) du processus,
|
|
||||||
on log chaque ligne d'erreur pour diagnostic
|
|
||||||
*/
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
data.toString().split("\n").forEach(line => {
|
|
||||||
if (line.trim()) logger.error(`[yt-dlp stderr] ${line.trim()}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
En cas d'erreur lors du lancement du processus yt-dlp,
|
|
||||||
on log l'erreur et on répond au client avec un statut 500
|
|
||||||
*/
|
|
||||||
child.on("error", (err) => {
|
|
||||||
logger.error(`Erreur lancement yt-dlp : ${err.message}`);
|
|
||||||
res.status(500).send(`❌ Erreur lors de l'exécution : ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
Quand le processus se termine, on vérifie le code de sortie.
|
|
||||||
Code 0 = succès, on informe le client que le téléchargement est terminé.
|
|
||||||
Sinon, on envoie une erreur au client avec le code d'échec.
|
|
||||||
*/
|
|
||||||
child.on("close", (code) => {
|
|
||||||
logger.info(`yt-dlp terminé avec code de sortie : ${code}`);
|
|
||||||
if (code === 0) {
|
|
||||||
|
|
||||||
const iconPath = path.join(process.resourcesPath, "confirm-icon.png"); // la sécurité pour toutes les machines
|
|
||||||
|
|
||||||
console.log("Icon path pour la notif :", iconPath);
|
|
||||||
|
|
||||||
const notif = new Notification({
|
|
||||||
title: "Freedom Loader",
|
|
||||||
body: "Ton téléchargement est terminé, clique ici pour l'ouvrir.",
|
|
||||||
icon: iconPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
notif.on("click", () => {
|
|
||||||
console.log("Notification cliquée !");
|
|
||||||
//Pour pouvoir ouvrir le dossier de la vidéo
|
|
||||||
const { shell } = require("electron");
|
|
||||||
shell.openPath(requestedOutputFolder);
|
|
||||||
});
|
|
||||||
|
|
||||||
notif.show();
|
|
||||||
res.send("✅ Téléchargement terminé !");
|
|
||||||
} else {
|
|
||||||
res.status(500).send(`❌ yt-dlp a échoué avec le code : ${code}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
// Capture toute autre erreur serveur non prévue, log et réponse 500
|
|
||||||
logger.error(`Erreur serveur dans /download : ${err.message}`);
|
|
||||||
res.status(500).send(`Erreur serveur : ${err.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
11
server/routes/download.route.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { downloadController, progressController, speedController } = require("../controller/download.controller");
|
||||||
|
|
||||||
|
router.post("/", downloadController);
|
||||||
|
|
||||||
|
// SSE pour la progression du téléchargement
|
||||||
|
router.get("/progress", progressController);
|
||||||
|
router.get("/speed", speedController);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,81 +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");
|
|
||||||
const router = express.Router();
|
|
||||||
const { execFile } = require("child_process");
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
const { logger } = require("../logger"); // On récupère ton logger Winston
|
|
||||||
|
|
||||||
// Path absolu vers l'exécutable yt-dlp
|
|
||||||
const ytDlpPath = path.join(__dirname, '../../ressources/yt-dlp 2025.08.27.exe');
|
|
||||||
|
|
||||||
// Vérification que yt-dlp.exe existe bien au lancement du module
|
|
||||||
if (!fs.existsSync(ytDlpPath)) {
|
|
||||||
logger.error(`❌ yt-dlp.exe introuvable à ${ytDlpPath}`);
|
|
||||||
throw new Error(`yt-dlp.exe introuvable à ${ytDlpPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route POST pour récupérer les métadonnées d'une vidéo via yt-dlp
|
|
||||||
router.post("/", (req, res) => {
|
|
||||||
const url = req.body.url;
|
|
||||||
|
|
||||||
// Validation basique : on refuse la requête sans URL
|
|
||||||
if (!url) {
|
|
||||||
logger.warn("Requête metadata sans URL");
|
|
||||||
return res.status(400).send("❌ URL manquante");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Requête metadata reçue pour ${url}`);
|
|
||||||
|
|
||||||
// Exécution de yt-dlp avec l'option --dump-json pour récupérer les infos vidéo
|
|
||||||
execFile(
|
|
||||||
ytDlpPath,
|
|
||||||
["--dump-json", url],
|
|
||||||
{ timeout: 10_000 }, // Timeout fixé à 10 secondes pour éviter blocage
|
|
||||||
(error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
// En cas d'erreur, on log l'erreur et le stderr, puis on renvoie un code 500
|
|
||||||
logger.error(`Erreur exécution yt-dlp: ${error.message}`);
|
|
||||||
logger.debug(`stderr: ${stderr}`);
|
|
||||||
return res.status(500).send("❌ Impossible de récupérer les infos.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// yt-dlp peut retourner plusieurs JSON séparés par des sauts de ligne
|
|
||||||
// On split et parse chacun proprement
|
|
||||||
const infos = stdout
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.map(line => JSON.parse(line));
|
|
||||||
|
|
||||||
logger.info(`Infos récupérées pour ${url} (${infos.length} élément(s))`);
|
|
||||||
|
|
||||||
// Si on a un seul élément, on renvoie directement l'objet JSON, sinon un tableau
|
|
||||||
res.json(infos.length === 1 ? infos[0] : infos);
|
|
||||||
} catch (e) {
|
|
||||||
// Si parsing JSON échoue, on log et renvoie une erreur 500
|
|
||||||
logger.error(`Erreur parsing JSON: ${e.message}`);
|
|
||||||
return res.status(500).send("❌ JSON illisible.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
7
server/routes/info.route.js
Normal 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;
|
||||||
134
server/server.js
@@ -1,106 +1,70 @@
|
|||||||
/*
|
const express = require("express");
|
||||||
This file is part of Freedom Loader.
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
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"); // Framework web pour créer l’API et servir les fichiers
|
|
||||||
const { exec } = require("child_process"); // Permet d’exécuter des commandes systèmes (pas utilisé directement ici)
|
|
||||||
const fs = require("fs"); // Module système pour gérer fichiers et dossiers
|
|
||||||
const path = require("path"); // Gestion propre des chemins de fichiers et dossiers
|
|
||||||
const debug = require("debug")("freedom-loader:server");
|
|
||||||
// Module de debug coloré en console, pratique pour dev
|
|
||||||
|
|
||||||
const { logger, logSessionStart, logSessionEnd } = require("./logger");
|
const { logger, logSessionStart, logSessionEnd } = require("./logger");
|
||||||
// Import du logger Winston et helpers pour marquer début/fin de session
|
const config = require("../config");
|
||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { userYtDlp } = require("./helpers/path");
|
||||||
|
|
||||||
const app = express(); // Création de l’instance Express, notre serveur web
|
|
||||||
|
|
||||||
// Définition du dossier par défaut où enregistrer les téléchargements
|
const app = express();
|
||||||
// On prend le dossier Téléchargements de l’utilisateur Windows (USERPROFILE)
|
|
||||||
const downloadsPath = path.join(process.env.USERPROFILE, "Downloads");
|
|
||||||
const outputFolder = path.join(downloadsPath, "Freedom Loader");
|
|
||||||
|
|
||||||
// Création du dossier de sortie s’il n’existe pas déjà
|
app.use(express.json());
|
||||||
if (!fs.existsSync(outputFolder)) {
|
|
||||||
try {
|
// Dossier de téléchargement
|
||||||
fs.mkdirSync(outputFolder, { recursive: true }); // création récursive au cas où
|
const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||||
logger.info("Dossier Freedom Loader cree dans Telechargements.");
|
|
||||||
} catch (err) {
|
// Création du dossier si nécessaire
|
||||||
logger.error("Impossible de creer le dossier :", err);
|
try {
|
||||||
process.exit(1); // Arrêt du programme si dossier non créé (critique)
|
fs.mkdirSync(outputFolder, { recursive: true });
|
||||||
}
|
logger.info(`Freedom Loader folder ready in ${outputFolder}`);
|
||||||
} else {
|
} catch (err) {
|
||||||
logger.info("Dossier Freedom Loader deja existant.");
|
logger.error("Unable to create folder:", err);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On rend ce dossier accessible globalement via app.locals pour l’utiliser dans les routes
|
// Mise à jour yt-dlp au
|
||||||
app.locals.outputFolder = outputFolder;
|
execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
|
||||||
|
if (err) logger.warn("yt-dlp update error:", err);
|
||||||
|
else logger.info(`Update yt-dlp : ${stdout}`);
|
||||||
|
});
|
||||||
|
|
||||||
// Middleware pour parser le corps des requêtes POST en application/x-www-form-urlencoded
|
// Middlewares
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(express.static(path.join(__dirname, "../public")));
|
||||||
|
|
||||||
// Définition du dossier contenant les fichiers statiques (frontend)
|
// Routes
|
||||||
// Permet d’accéder au HTML, CSS, JS côté client
|
app.use("/download", require("./routes/download.route"));
|
||||||
const staticPath = path.join(__dirname, "../public");
|
app.use("/info", require("./routes/info.route"));
|
||||||
debug("Serveur statique sur", staticPath);
|
|
||||||
app.use(express.static(staticPath));
|
|
||||||
|
|
||||||
// Import et enregistrement des routes API
|
|
||||||
// Ces routes gèrent les requêtes pour les infos vidéo et téléchargement
|
|
||||||
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 / qui sert la page principale index.html
|
|
||||||
app.get("/", (req, res) => {
|
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 Express
|
// Fonction pour démarrer le serveur
|
||||||
// Retourne une Promise pour pouvoir await le démarrage dans d’autres modules
|
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); // Serveur prêt, on résout la promesse
|
|
||||||
});
|
});
|
||||||
// Gestion des erreurs serveur lors du démarrage
|
|
||||||
serverInstance.on("error", (err) => {
|
server.on("error", (err) => {
|
||||||
logger.error("Erreur serveur Express :", err);
|
logger.error("Express Server Error :", err);
|
||||||
reject(err); // Rejet de la promesse en cas d’erreur critique
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const gracefulExit = () => {
|
||||||
|
logSessionEnd();
|
||||||
|
server.close(() => {
|
||||||
|
logger.info("Express Server close cleanly.");
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGINT", gracefulExit);
|
||||||
|
process.on("SIGTERM", gracefulExit);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion propre de la fermeture du process pour logger la fin de session
|
|
||||||
process.on("SIGINT", () => { // Capture Ctrl+C (interruption)
|
|
||||||
logSessionEnd();
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
process.on("SIGTERM", () => { // Capture kill ou arrêt du process
|
|
||||||
logSessionEnd();
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Export de la fonction startServer pour permettre son appel depuis d’autres fichiers
|
|
||||||
module.exports = { startServer };
|
module.exports = { startServer };
|
||||||
|
|
||||||
const { startRPC } = require("./discordRPC");
|
|
||||||
startRPC();
|
|
||||||
|
|||||||
62
server/services/download.services.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { userYtDlp } = require("../helpers/path");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
||||||
|
const notify = require("../helpers/notify")
|
||||||
|
|
||||||
|
function fetchDownload(options, listeners, speedListeners) {
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||||
|
fs.mkdirSync(outputFolder, { recursive: true });
|
||||||
|
|
||||||
|
const args = buildYtDlpArgs({ ...options, outputFolder });
|
||||||
|
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||||
|
|
||||||
|
const child = execFile(userYtDlp, args);
|
||||||
|
|
||||||
|
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
|
||||||
|
|
||||||
|
child.on("close", code => {
|
||||||
|
listeners.forEach(fn => fn("done"));
|
||||||
|
if (code === 0) resolve(outputFolder);
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
/* Barre de Chargement*/
|
||||||
|
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { fetchDownload };
|
||||||
51
server/services/info.services.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { userYtDlp, denoPath } = require("../helpers/path");
|
||||||
|
const getUserBrowser = require("../helpers/getBrowser");
|
||||||
|
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, si jamais plus, abandon de la requête
|
||||||
|
maxBuffer: 10 * 1024 * 1024 }, // 10MO de réponse max (Par défaut: 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 };
|
||||||
47
server/update.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const { autoUpdater } = require("electron-updater");
|
||||||
|
const { app, Notification } = require("electron");
|
||||||
|
const { logger } = require("./logger");
|
||||||
|
|
||||||
|
function AutoUpdater() {
|
||||||
|
|
||||||
|
autoUpdater.on("update-available", (info) => {
|
||||||
|
logger.info(`New Version Available : ${info.version}`);
|
||||||
|
new Notification({
|
||||||
|
title: "Freedom Loader",
|
||||||
|
body: `New Version Available : ${info.version}. Application will restart`
|
||||||
|
}).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-downloaded", (info) => {
|
||||||
|
logger.info(`Update Downloaded : ${info.version}`);
|
||||||
|
new Notification({
|
||||||
|
title: "Freedom Loader",
|
||||||
|
body: `Update ${info.version} downloaded.`
|
||||||
|
}).on("click", () =>
|
||||||
|
shell.openExternal("https://github.com/MasterAcnolo/Freedom-Loader/releases/latest")
|
||||||
|
).show();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("error", (err) => {
|
||||||
|
logger.error("Auto Update Error :", err.message);
|
||||||
|
new Notification({
|
||||||
|
title: "Freedom Loader - Update Error",
|
||||||
|
body: err.message
|
||||||
|
}).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
try {
|
||||||
|
await autoUpdater.checkForUpdates();
|
||||||
|
logger.info("Update check completed");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Error during update check :", err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { AutoUpdater };
|
||||||