Compare commits
219 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71dbd104f8 | ||
|
|
57a5defb08 | ||
|
|
25c78ff8b7 | ||
|
|
792a59d8d7 | ||
|
|
b13c98dda2 | ||
|
|
1f103c20b9 | ||
|
|
70d62959ca | ||
|
|
83ca963502 | ||
|
|
9fb9295093 | ||
|
|
74fcdbb738 | ||
|
|
32c9a66a35 | ||
|
|
45e0840fea | ||
|
|
19b3f8b947 | ||
|
|
cc5bde5b48 | ||
|
|
9a48ac05e3 | ||
|
|
5750444655 | ||
|
|
fdb22f2342 | ||
|
|
760550e521 | ||
|
|
334e83ae1f | ||
|
|
5d776594fc | ||
|
|
adb36e3fa2 | ||
|
|
87945fba80 | ||
|
|
2234f9ed81 | ||
|
|
7cd3b4be06 | ||
|
|
a42b020b96 | ||
|
|
c1e70dae80 | ||
|
|
6877c51c0e | ||
|
|
5a41090175 | ||
|
|
5983f21759 | ||
|
|
ee9e02410f | ||
|
|
909480083e | ||
|
|
ca0492ed04 | ||
|
|
6c73dd75be | ||
|
|
556dccec08 | ||
|
|
f720d7c889 | ||
|
|
bb23f37fb5 | ||
|
|
220e81d8b2 | ||
|
|
44d8772827 | ||
|
|
335fb9652b | ||
|
|
243b98cbf9 | ||
|
|
23da75a17b | ||
|
|
756d2c048e | ||
|
|
659c1d4cf9 | ||
|
|
443fd31170 | ||
|
|
6d8095ceb2 | ||
|
|
c18cb6c8aa | ||
|
|
a3c5025434 | ||
|
|
3d7986cb87 | ||
|
|
3f8701567f | ||
|
|
ccb16c49d5 | ||
|
|
86d9ac3592 | ||
|
|
43d87c2b47 | ||
|
|
19aa8ed8fa | ||
|
|
3ddfaa999e | ||
|
|
3d7f3b7481 | ||
|
|
cfe2e17313 | ||
|
|
dedf10c6b3 | ||
|
|
ac0b4a0750 | ||
|
|
29ca2ea496 | ||
|
|
2a3905bdfb | ||
|
|
d3755ab57d | ||
|
|
9824e107fe | ||
|
|
e437722937 | ||
|
|
fd8c4ae33c | ||
|
|
967144bd55 | ||
|
|
cd8eb05020 | ||
|
|
590060e504 | ||
|
|
26c7d4d2ee | ||
|
|
caa987fa42 | ||
|
|
ccb090e4b2 | ||
|
|
8d070ed10d | ||
|
|
343dbb26a2 | ||
|
|
fef4f6de36 | ||
|
|
1a61972750 | ||
|
|
45ae5d7f98 | ||
|
|
fecb123d0e | ||
|
|
5c62ed081a | ||
|
|
8f0f31041d | ||
|
|
0e48ab3b4c | ||
|
|
d4c33bcf2d | ||
|
|
2f105f5145 | ||
|
|
b7a193b062 | ||
|
|
65364d3095 | ||
|
|
c356e2e30d | ||
|
|
e45810b54c | ||
|
|
55db30febc | ||
|
|
163faec221 | ||
|
|
5398fed928 | ||
|
|
f959171db5 | ||
|
|
01b6bcd02c | ||
|
|
0ff4715b35 | ||
|
|
745a73a5b0 | ||
|
|
a69114edce | ||
|
|
489a4766fa | ||
|
|
adcee62386 | ||
|
|
b867d819b7 | ||
|
|
31d8b20a76 | ||
|
|
42ae0e7204 | ||
|
|
43869420c0 | ||
|
|
6c9d296fbd | ||
|
|
008428f10a | ||
|
|
ae7d338474 | ||
|
|
12ff25cc4b | ||
|
|
f31767e7e7 | ||
|
|
7c4f2841e1 | ||
|
|
a27b3555f6 | ||
|
|
c451ddd042 | ||
|
|
da6311055f | ||
|
|
945efb2059 | ||
|
|
ab2881f8e9 | ||
|
|
f23811fff4 | ||
|
|
19ab77501b | ||
|
|
37eeed583a | ||
|
|
caf08efb90 | ||
|
|
7ff998e4df | ||
|
|
0c1e8fe899 | ||
|
|
4158e8c6e3 | ||
|
|
653319e408 | ||
|
|
0a4f14fba7 | ||
|
|
fb4571fc51 | ||
|
|
c11fd40681 | ||
|
|
cbfde8b761 | ||
|
|
c774821cc4 | ||
|
|
2ed15a32bc | ||
|
|
c252853603 | ||
|
|
cfdddec073 | ||
|
|
97a63ab791 | ||
|
|
a0e00341e1 | ||
|
|
26d42491ce | ||
|
|
d678ba2f12 | ||
|
|
55b400eb05 | ||
|
|
0a6313b1f3 | ||
|
|
b4312376bb | ||
|
|
5ffb143a1a | ||
|
|
ee24473077 | ||
|
|
2b98a63f92 | ||
|
|
2e594d87e8 | ||
|
|
b343c2869d | ||
|
|
094b93f4fa | ||
|
|
cdc8a623c0 | ||
|
|
2b43c00a3a | ||
|
|
14928798d4 | ||
|
|
e5fb44fc6b | ||
|
|
9f9e4cd1d8 | ||
|
|
45fe7336fe | ||
|
|
b909e7d917 | ||
|
|
ce62748ade | ||
|
|
d4ecc36f6b | ||
|
|
fee364610e | ||
|
|
492af8525b | ||
|
|
fb58b5ae06 | ||
|
|
577034b0bd | ||
|
|
6fc452edb4 | ||
|
|
086ca49599 | ||
|
|
be1de35da8 | ||
|
|
3c1f57667f | ||
|
|
5997cd20b4 | ||
|
|
6220c9400e | ||
|
|
48a16b927f | ||
|
|
87545d77b4 | ||
|
|
4e0fe29c20 | ||
|
|
f8e3bc843d | ||
|
|
b3f1edaabd | ||
|
|
ef784dd19c | ||
|
|
69bbcee885 | ||
|
|
60b4a99506 | ||
|
|
c1030400f5 | ||
|
|
3f508da501 | ||
|
|
0437cb61a2 | ||
|
|
1372582c54 | ||
|
|
070c3d507f | ||
|
|
3e9c545952 | ||
|
|
e3845e1849 | ||
|
|
91703d6a91 | ||
|
|
cff3101b03 | ||
|
|
c9a48ec64c | ||
|
|
e6419a9759 | ||
|
|
9d1d2fc7a8 | ||
|
|
cdca4a82ab | ||
|
|
c2e4e12512 | ||
|
|
96c1c6717d | ||
|
|
abf776175c | ||
|
|
d6de184bfd | ||
|
|
12249c4e8a | ||
|
|
f1fbc3f0c7 | ||
|
|
bb8cf54bf9 | ||
|
|
dccf8bd786 | ||
|
|
793bb79d76 | ||
|
|
f5cdf27ce6 | ||
|
|
627c56cd25 | ||
|
|
783871d131 | ||
|
|
138c05f995 | ||
|
|
5009a479af | ||
|
|
2a95a10113 | ||
|
|
9d6065c80a | ||
|
|
cd8b2bf007 | ||
|
|
1faf74fbc4 | ||
|
|
09163afbcd | ||
|
|
405d141055 | ||
|
|
599e09fa9f | ||
|
|
4954de5f1a | ||
|
|
ac854ae559 | ||
|
|
9944f05139 | ||
|
|
f6558b656a | ||
|
|
bfbf57ba38 | ||
|
|
d0af35377c | ||
|
|
59c263a246 | ||
|
|
42700bd7d8 | ||
|
|
5a9b364b4b | ||
|
|
3619550c74 | ||
|
|
31cc4012ac | ||
|
|
2e329a8584 | ||
|
|
d92dd59f86 | ||
|
|
5bb7ca377d | ||
|
|
9ee9a3574c | ||
|
|
dc50a44682 | ||
|
|
4be5c14999 | ||
|
|
b92ffebdb4 | ||
|
|
7b6e48b09e |
35
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: "Bug Report"
|
||||
about: Report something that doesn't work fine
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: "MasterAcnolo"
|
||||
|
||||
---
|
||||
|
||||
## Check Before Reporting
|
||||
Make sure this bug doesn't already exist in the issues.
|
||||
|
||||
## Describe the Bug
|
||||
A clear and concise description of the problem.
|
||||
|
||||
## Steps to Reproduce
|
||||
1. Go to…
|
||||
2. Click on…
|
||||
3. See the error…
|
||||
|
||||
## Expected Behavior
|
||||
What you expected the program to do.
|
||||
|
||||
## Logs
|
||||
Add logs if possible. Available at `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`.
|
||||
|
||||
## App Version
|
||||
State the version of the app you are using.
|
||||
|
||||
## Environment
|
||||
- Version: [e.g. 1.2.4]
|
||||
- Browser / Other: [e.g. Chrome, Node.js 20]
|
||||
- Any others information that can be usefull for me
|
||||
|
||||
## Screenshots (if applicable)
|
||||
44
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature or enhancement
|
||||
title: "[FEATURE] "
|
||||
labels: enhancement
|
||||
assignees: 'MasterAcnolo'
|
||||
|
||||
---
|
||||
|
||||
## Feature Description
|
||||
|
||||
A clear and concise description of the feature you'd like to see.
|
||||
|
||||
## Problem it Solves
|
||||
|
||||
Describe the problem or limitation this feature would address.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
Describe how you envision this feature working.
|
||||
|
||||
## Alternative Solutions
|
||||
|
||||
Have you considered any alternative approaches? Describe them here.
|
||||
|
||||
## Use Case
|
||||
|
||||
Provide specific examples of how you would use this feature.
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
Are there any technical challenges or dependencies you're aware of?
|
||||
|
||||
## Priority
|
||||
|
||||
How important is this feature to you?
|
||||
- [ ] Critical - Blocking my workflow
|
||||
- [ ] High - Would significantly improve my experience
|
||||
- [ ] Medium - Nice to have
|
||||
- [ ] Low - Minor improvement
|
||||
|
||||
## Additional Context
|
||||
|
||||
Add any other context, mockups, or screenshots about the feature request here.
|
||||
56
.github/ISSUE_TEMPLATE/performance-issue.md
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: Performance Issue
|
||||
about: Report slow downloads, high CPU/memory usage, or crashes
|
||||
title: "[PERFORMANCE] "
|
||||
labels: performance
|
||||
assignees: MasterAcnolo
|
||||
|
||||
---
|
||||
|
||||
## Performance Issue Type
|
||||
|
||||
- [ ] Slow download speed
|
||||
- [ ] High CPU usage
|
||||
- [ ] High memory usage
|
||||
- [ ] Application freeze/hang
|
||||
- [ ] Application crash
|
||||
- [ ] Other (specify below)
|
||||
|
||||
## Description
|
||||
|
||||
Clear description of the performance issue.
|
||||
|
||||
## System Information
|
||||
|
||||
- **Freedom Loader Version**: [e.g. 1.4.1]
|
||||
- **Windows Version**: [e.g. Windows 11 22H2]
|
||||
- **CPU**: [e.g. Intel i7-9700K]
|
||||
- **RAM**: [e.g. 16GB]
|
||||
- **Storage**: [e.g. SSD/HDD]
|
||||
- **Other Applications Opened**: [e.g. Discord/Opera GX]
|
||||
|
||||
## Reproduction Details
|
||||
|
||||
- **Download URL**: [if applicable]
|
||||
- **File Size/Length**: [e.g. 2GB / 2 hours]
|
||||
- **Quality Setting**: [e.g. 1080p, Best]
|
||||
- **Audio Only**: Yes / No
|
||||
|
||||
## Expected Performance
|
||||
|
||||
What performance did you expect?
|
||||
|
||||
## Actual Performance
|
||||
|
||||
What performance are you experiencing? (Include metrics if possible: download speed, CPU %, memory usage)
|
||||
|
||||
## Logs
|
||||
|
||||
Attach logs with verbose mode enabled if possible:
|
||||
```
|
||||
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log
|
||||
```
|
||||
|
||||
## Additional Context
|
||||
|
||||
Any other relevant information (network speed, antivirus software, other running applications).
|
||||
30
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Question or Support
|
||||
about: Ask a question about using Freedom Loader
|
||||
title: "[QUESTION] "
|
||||
labels: question
|
||||
assignees: 'MasterAcnolo'
|
||||
|
||||
---
|
||||
|
||||
## Pre-submission Checklist
|
||||
|
||||
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/FreedomLoader/pages/faq.html)
|
||||
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
||||
- [ ] I have searched existing issues
|
||||
|
||||
## Question
|
||||
|
||||
What would you like to know?
|
||||
|
||||
## What I've Tried
|
||||
|
||||
Describe what you've already attempted to solve this yourself.
|
||||
|
||||
## Environment (if relevant)
|
||||
|
||||
- **Freedom Loader Version**: [e.g. 1.4.1]
|
||||
|
||||
## Additional Context
|
||||
|
||||
Any other information that might be helpful.
|
||||
60
.github/PULL_REQUEST_TEMPLATE/contribution.md
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
## Description
|
||||
|
||||
Brief description of what this PR does.
|
||||
|
||||
## Related Issue
|
||||
|
||||
Fixes #(issue number)
|
||||
Closes #(issue number)
|
||||
Related to #(issue number)
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Code refactoring (no functional changes)
|
||||
- [ ] Documentation update
|
||||
- [ ] Performance improvement
|
||||
- [ ] UI/UX enhancement
|
||||
|
||||
## Changes Made
|
||||
|
||||
- Change 1
|
||||
- Change 2
|
||||
- Change 3
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] I have tested this code locally
|
||||
- [ ] I have tested with different download scenarios (video/audio, playlists)
|
||||
- [ ] I have checked the application logs for errors
|
||||
- [ ] I have verified the UI changes (if applicable)
|
||||
- [ ] I have tested on Windows 10 and/or Windows 11
|
||||
|
||||
## Screenshots (if applicable)
|
||||
|
||||
**Before:**
|
||||
|
||||
[Add screenshot]
|
||||
|
||||
**After:**
|
||||
|
||||
[Add screenshot]
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the project's code style
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have commented my code where necessary
|
||||
- [ ] I have updated the documentation (if applicable)
|
||||
- [ ] My changes generate no new warnings or errors
|
||||
- [ ] I have not modified the version number (this will be done during release)
|
||||
|
||||
## Additional Notes
|
||||
|
||||
Any additional information or context about the PR.
|
||||
|
||||
---
|
||||
|
||||
**Note for Maintainer:** This is a contribution PR and should NOT include version bumps.
|
||||
10
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Freedom Loader - 1.X.Y
|
||||
|
||||
## Changelog
|
||||
|
||||
-
|
||||
|
||||
## Found a bug or issue?
|
||||
- Please report it in the GitHub Issues section
|
||||
|
||||
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.W.X...1.Y.Z
|
||||
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
||||
|
||||
/ressources/ffmpeg.exe
|
||||
/ressources/ffprobe.exe
|
||||
/ressources/deno.exe
|
||||
|
||||
logs/*.json
|
||||
logs/*.log
|
||||
111
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in the Freedom Loader community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall community
|
||||
* Supporting fellow contributors and helping newcomers feel welcome
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address, without their explicit permission
|
||||
* Sustained disruption of discussions, issues, or pull requests
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders and project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, including:
|
||||
|
||||
- GitHub repository (issues, pull requests, discussions, wiki)
|
||||
- Project documentation and website
|
||||
- Official communication channels (Discord, email, etc.)
|
||||
- Social media accounts representing the project
|
||||
- Project events and meetings (online or offline)
|
||||
|
||||
This Code of Conduct also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at **masteracnolo25@gmail.com**.
|
||||
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Our Commitment to Contributors
|
||||
|
||||
At Freedom Loader, we value every contribution—whether it's a bug report, feature suggestion, code improvement, or documentation fix. We strive to:
|
||||
|
||||
- Respond to issues and pull requests in a timely manner
|
||||
- Provide constructive feedback on contributions
|
||||
- Help new contributors get started
|
||||
- Maintain a welcoming environment for all skill levels
|
||||
- Credit contributors appropriately for their work
|
||||
|
||||
## Questions and Clarifications
|
||||
|
||||
If you have questions about this Code of Conduct or need clarification on what constitutes acceptable behavior, please:
|
||||
|
||||
- Open a [discussion](https://github.com/MasterAcnolo/Freedom-Loader/discussions) on GitHub
|
||||
- Contact the maintainers at masteracnolo25@gmail.com
|
||||
- Review the [Contributing Guidelines](CONTRIBUTING.md)
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Freedom Loader is built by people who care. Let's keep this community a place where everyone feels welcome to contribute.
|
||||
164
CONTRIBUTING.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Contributing to Freedom Loader
|
||||
|
||||
Welcome! Thanks for your interest in Freedom Loader. Your contributions—bug reports, feature ideas, or code improvements—are always appreciated. Every contribution makes Freedom Loader better.
|
||||
|
||||
---
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
- [Issue Templates](.github/ISSUE_TEMPLATE/)
|
||||
- [Pull Request Templates](.github/PULL_REQUEST_TEMPLATE/)
|
||||
- [Security Policy](SECURITY.md)
|
||||
|
||||
---
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
### 1. Report a Bug
|
||||
|
||||
Found a bug or unexpected behavior? Open an [**issue**](https://github.com/MasterAcnolo/Freedom-Loader/issues) using the **Bug Report** template.
|
||||
|
||||
**Please include:**
|
||||
- Clear description of the bug
|
||||
- Steps to reproduce
|
||||
- App version and environment (Windows version, Firefox version if relevant)
|
||||
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log`
|
||||
- Screenshots if applicable
|
||||
|
||||
### 2. Request a Feature
|
||||
|
||||
Have an idea to improve Freedom Loader? Open a [**Feature Request**](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose).
|
||||
|
||||
**Tips:**
|
||||
- Describe the problem it solves
|
||||
- Explain your proposed solution
|
||||
- Include use cases and examples
|
||||
|
||||
### 3. Submit Code Changes
|
||||
|
||||
**Before starting:**
|
||||
- Check existing issues and PRs to avoid duplicates
|
||||
- For major changes, open an issue first to discuss
|
||||
- Follow the project's code style and conventions
|
||||
|
||||
**Process:**
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/your-feature-name`
|
||||
3. Make your changes
|
||||
4. Test thoroughly on Windows 10/11
|
||||
5. Commit with clear messages: `git commit -m "Add feature X"`
|
||||
6. Push to your fork: `git push origin feature/your-feature-name`
|
||||
7. Open a Pull Request using the appropriate template
|
||||
|
||||
**Code Guidelines:**
|
||||
- Use camelCase for variables and functions
|
||||
- Comment complex logic
|
||||
- Keep commits focused and atomic
|
||||
- Update documentation if needed
|
||||
- Don't modify version numbers (done during release)
|
||||
|
||||
### 4. Improve Documentation
|
||||
|
||||
Documentation improvements are always welcome:
|
||||
- Fix typos or unclear sections
|
||||
- Add missing examples
|
||||
- Update outdated information
|
||||
- Improve README, wiki, or guides
|
||||
|
||||
Small contributions matter—don't hesitate to submit documentation PRs.
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 16.x or higher
|
||||
- npm or yarn
|
||||
- Git
|
||||
- Windows 10/11 (for testing)
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
# Clone your fork
|
||||
git clone https://github.com/YOUR-USERNAME/Freedom-Loader.git
|
||||
cd Freedom-Loader
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
Freedom-Loader/
|
||||
├── main.js # Electron main process
|
||||
├── preload.js # Electron preload script
|
||||
├── config.js # Global configuration
|
||||
├── server/ # Express backend
|
||||
│ ├── routes/ # API routes
|
||||
│ ├── controller/ # Business logic
|
||||
│ └── helpers/ # Utility functions
|
||||
├── public/ # Frontend (HTML, CSS, JS)
|
||||
└── ressources/ # Binaries (yt-dlp, ffmpeg, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Before submitting a PR, verify:
|
||||
- [ ] Download functionality works (video/audio)
|
||||
- [ ] Playlist downloads work
|
||||
- [ ] Settings panel functions correctly
|
||||
- [ ] Custom path selection works
|
||||
- [ ] No errors in application logs
|
||||
- [ ] UI changes work on all themes (if applicable)
|
||||
- [ ] Tested on Windows 10 and/or Windows 11
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Review Process
|
||||
|
||||
1. **Submission**: Open PR with clear description using the template
|
||||
2. **Review**: Maintainers review code and provide feedback
|
||||
3. **Updates**: Address feedback and push updates
|
||||
4. **Approval**: Once approved, PR will be merged
|
||||
5. **Release**: Changes included in next release
|
||||
|
||||
**Response times:**
|
||||
- Bug fixes: Usually reviewed within 2-3 days
|
||||
- Features: May take longer depending on complexity
|
||||
- Documentation: Often reviewed quickly
|
||||
|
||||
---
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Be respectful and constructive in all interactions. We welcome everyone as long as discussions remain polite and productive.
|
||||
|
||||
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
- Check the [FAQ](https://masteracnolo.github.io/FreedomLoader/pages/faq.html)
|
||||
- Review the [Wiki](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
||||
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to **Zakaria** for the website icon design.
|
||||
Check out his work: [IG @designmark_studio](https://www.instagram.com/designmark_studio/)
|
||||
|
||||
---
|
||||
|
||||
**Thank you for contributing to Freedom Loader!**
|
||||
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.
|
||||
|
||||
## 🚀 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.
|
||||
488
README.md
@@ -1,114 +1,422 @@
|
||||
# Freedom Loader
|
||||
<div align="center">
|
||||
|
||||
<!-- Release -->
|
||||
<a href="https://masteracnolo.github.io/FreedomLoader/"><img
|
||||
src="./build/banner.png"
|
||||
alt="Banner"
|
||||
style="width: 50%;"/></a>
|
||||
|
||||

|
||||
### **A clean, open-source multimedia downloader for Windows**
|
||||
|
||||
<!-- Build (Next Release) -->
|
||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://masteracnolo.github.io/FreedomLoader/)
|
||||
<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>
|
||||
|
||||

|
||||
|
||||
> This project aims to enable free, open-source, and unrestricted downloading of Video/Audio content.
|
||||
|
||||
## 🚀 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
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Installation
|
||||
## Overview
|
||||
|
||||
To install the application, simply download the installer from the releases section. Take the latest version and run it.
|
||||
Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services.
|
||||
|
||||
---
|
||||
The primary goal is to make media downloading accessible to users who want offline access to their favorite content, particularly in situations where internet connectivity is unreliable or unavailable.
|
||||
|
||||
## ❓ Frequently Asked Questions
|
||||
## Table of Contents
|
||||
|
||||
### _Where are my downloaded files located?_
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Preview](#preview)
|
||||
- [Configuration](#configuration)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Technology Stack](#technology-stack)
|
||||
- [Development](#development)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Contributing](#contributing)
|
||||
- [Support](#support)
|
||||
- [License](#license)
|
||||
|
||||
In your computer's Downloads folder (**C:\Users\YOUR NAME\Downloads**), inside a folder named **Freedom Loader Output**
|
||||
<br>
|
||||
## Features
|
||||
|
||||
### _After installing and running the program, I get an error_
|
||||
### Core Functionality
|
||||
|
||||
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`
|
||||
- **Multi-format downloads** - MP4 video with full metadata or MP3 audio with embedded artwork
|
||||
- **Video information preview** - Fetch and display video metadata before downloading
|
||||
- **Quality selection** - Choose from best, medium, lowest quality or specific resolutions (1080p, 720p, 480p)
|
||||
- **Playlist support** - Download entire playlists with optional auto-download mode
|
||||
- **Metadata embedding** - Automatic retrieval and embedding of title, artist, duration, and thumbnail
|
||||
- **Video codec options** - Support for H.264, H.265, AV1, VP9, VP9.2, and Theora codecs
|
||||
- **Custom output directory** - User-configurable download location with path validation
|
||||
- **Cookie integration** - Firefox cookie support for accessing age-restricted and member-only content
|
||||
|
||||
<!--  -->
|
||||
<p align="center">
|
||||
<img src="build/example-developertools.png" alt="Example Developer Tools" />
|
||||
</p>
|
||||
### User Interface
|
||||
|
||||
## 🏗️ Upcoming Features (Roadmap)
|
||||
- **Custom window controls** - Frameless window with custom top bar (optional)
|
||||
- **Theme system** - Multiple pre-built themes with dynamic switching
|
||||
- **Real-time progress tracking** - Live download progress bar with network speed indicator
|
||||
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
||||
- **Settings panel** - In-app configuration interface with live updates
|
||||
- **Quick access toolbar** - Direct links to logs, website, wiki, and developer tools
|
||||
|
||||
- [ ] More format options
|
||||
- [ ] Subtitle support
|
||||
- [ ] Improved UI / UX
|
||||
- [ ] Language selection
|
||||
- [ ] Download a specific part of the video
|
||||
- [ ] Rename output file
|
||||
- [x] ~~Extract / Split video using native chapters~~
|
||||
### System Features
|
||||
|
||||
- **Automatic updates** - Electron-updater integration with GitHub releases
|
||||
- **YT-DLP auto-update** - Keeps the download engine up-to-date on startup
|
||||
- **Structured logging** - Winston-based logging with daily file rotation (7-day retention)
|
||||
- **Discord Rich Presence** - Optional integration showing current download activity
|
||||
- **Single instance enforcement** - Prevents multiple app instances from running simultaneously
|
||||
- **Native dependency validation** - Startup checks for yt-dlp, FFmpeg, FFprobe, and Deno binaries
|
||||
- **Rate limiting** - Express-based request throttling to prevent server overload
|
||||
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
||||
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Operating System**: Windows 10 or later
|
||||
- **Browser**: Mozilla Firefox (required for cookie extraction)
|
||||
- [Download Firefox](https://www.firefox.com)
|
||||
|
||||
### Standard Installation
|
||||
|
||||
1. Download the latest release from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
2. Run the downloaded `.exe` installer
|
||||
3. If Windows Defender shows a warning, click "More info" then "Run anyway"
|
||||
4. Follow the installation wizard
|
||||
|
||||
### File Locations
|
||||
|
||||
Downloaded files are stored in:
|
||||
```
|
||||
C:\Users\[USERNAME]\Downloads\Freedom Loader
|
||||
```
|
||||
> [!NOTE]
|
||||
> *You can change it whenever you want. Just click the "Edit" button in the UI.*
|
||||
|
||||
Application logs can be found at:
|
||||
```
|
||||
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Download
|
||||
|
||||
1. Launch Freedom Loader
|
||||
2. Paste a video or playlist URL into the input field
|
||||
3. Select the desired format (Video MP4 or Audio MP3)
|
||||
4. Click the download button
|
||||
5. Monitor progress in real-time
|
||||
6. Receive a notification when the download completes
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
The application supports various configuration options:
|
||||
|
||||
- Custom download directory selection
|
||||
- Discord Rich Presence activation
|
||||
- Theme customization
|
||||
- Log level configuration
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Restart the application
|
||||
2. Check the logs in `AppData\Local\FreedomLoader\logs\`
|
||||
3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools"
|
||||
4. Create an issue on GitHub with logs and reproduction steps
|
||||
|
||||
## Preview
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" align="center">
|
||||
<img src="https://github.com/user-attachments/assets/d4de9079-7c82-44d3-ba22-8246bb467db3" alt="Download Preview" width="75%" />
|
||||
<p><em>Real-time download progress with network speed indicator</em></p>
|
||||
</td>
|
||||
<td width="50%" align="center">
|
||||
<img src="https://github.com/user-attachments/assets/d59aff95-6754-486a-b880-d70730146dc3" alt="Settings Panel" width="40%" />
|
||||
<p><em>Settings panel with theme and feature customization</em></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
## Configuration
|
||||
|
||||
Freedom Loader can be configured either through the settings panel in the UI or by directly editing the `config.json` file located in the application data directory.
|
||||
|
||||
### Configuration File Location
|
||||
```
|
||||
C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
||||
```
|
||||
|
||||
### Available Options
|
||||
```json
|
||||
{
|
||||
"autoUpdate": true,
|
||||
"discordRPC": true,
|
||||
"customTopBar": true,
|
||||
"autoCheckInfo": true,
|
||||
"addThumbnail": true,
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
"outputTitleCheck": true,
|
||||
"downloadSystem": true,
|
||||
"notifySystem": true
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
||||
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
||||
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
||||
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
||||
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
|
||||
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||
| `logSystem` | boolean | `true` | Enable application logging |
|
||||
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
||||
| `downloadSystem` | boolean | `true` | Enable download system |
|
||||
| `notifySystem` | boolean | `true` | Enable system notifications |
|
||||
|
||||
### Supported Codecs
|
||||
|
||||
- `h264` - H.264/AVC (widely compatible, recommended default)
|
||||
- `h265` - H.265/HEVC (better compression, less compatible)
|
||||
- `av1` - AV1 (modern codec, excellent compression)
|
||||
- `av01` - AV1 variant
|
||||
- `vp9` - VP9 (open format, good quality)
|
||||
- `vp9.2` - VP9 Profile 2 (HDR support)
|
||||
- `theora` - Theora (legacy open codec)
|
||||
|
||||
> [!NOTE]
|
||||
> Configuration changes may require an application restart to take effect.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Freedom-Loader/
|
||||
├── build/ # Build resources and assets
|
||||
├── config/ # Configuration files
|
||||
├── public/ # Frontend assets (HTML, CSS, JavaScript)
|
||||
├── ressources/ # Internal resources (icons, binaries)
|
||||
├── server/ # Express server code
|
||||
│ ├── routes/ # API route handlers
|
||||
│ ├── services/ # Business logic
|
||||
│ └── utils/ # Server utilities
|
||||
├── .github/ # GitHub configuration and workflows
|
||||
├── main.js # Electron main process
|
||||
├── preload.js # Electron preload script
|
||||
├── config.js # Global application configuration
|
||||
└── package.json # Dependencies and npm scripts
|
||||
```
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
Freedom Loader uses a client-server architecture within a single Electron application:
|
||||
|
||||
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
|
||||
- **Backend**: Express.js server running locally for download management
|
||||
- **IPC Bridge**: Secure communication via Electron's preload script
|
||||
- **Logging**: Winston-based structured logging with file rotation
|
||||
- **Updates**: Automatic checking and installation via electron-updater
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Core Technologies
|
||||
|
||||
[](https://github.com/yt-dlp/yt-dlp)
|
||||
[](https://ffmpeg.org/)
|
||||
[](https://www.electronjs.org/)
|
||||
[](https://nodejs.org/)
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
- **Electron**: Cross-platform desktop application framework
|
||||
- **Express**: Web framework for local HTTP server
|
||||
- **yt-dlp**: Media download engine
|
||||
- **FFmpeg**: Media processing and conversion
|
||||
- **Winston**: Logging library
|
||||
- **discord-rpc**: Discord Rich Presence integration
|
||||
- **electron-updater**: Automatic application updates
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 16.x or higher
|
||||
- npm or yarn
|
||||
- Git
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/MasterAcnolo/Freedom-Loader.git
|
||||
cd Freedom-Loader
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Additional Dependencies
|
||||
|
||||
This project relies on several external binaries.
|
||||
Some of them are **not included** in the public repository and must be added manually.
|
||||
|
||||
You must download the required binaries and place them in the `ressources` folder, using the exact filenames listed below.
|
||||
|
||||
#### Required binaries
|
||||
|
||||
- **Deno**
|
||||
- Download from: https://sourceforge.net/projects/deno.mirror/files/latest/download
|
||||
- Rename to: `deno.exe`
|
||||
|
||||
- **FFmpeg**
|
||||
- Download from: https://www.ffmpeg.org/download.html
|
||||
- Required files:
|
||||
- `ffmpeg.exe`
|
||||
- `ffprobe.exe`
|
||||
|
||||
- **yt-dlp**
|
||||
- Already bundled with the project
|
||||
- No manual installation required
|
||||
|
||||
Final folder structure:
|
||||
|
||||
```
|
||||
ressources/
|
||||
├── deno.exe
|
||||
├── ffmpeg.exe
|
||||
├── ffprobe.exe
|
||||
└── yt-dlp.exe
|
||||
```
|
||||
|
||||
> These binaries are required for the application to start correctly.
|
||||
> If any of them are missing, an error message will be displayed at application startup.
|
||||
|
||||
### Development Guidelines
|
||||
|
||||
- Follow existing code style and conventions
|
||||
- Write clear commit messages
|
||||
- Test thoroughly before submitting changes
|
||||
- Update documentation when adding features
|
||||
- Maintain compatibility with Windows 10+
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Completed
|
||||
|
||||
- [x] ~~Website and documentation~~
|
||||
- [x] ~~Automatic YT-DLP updates~~
|
||||
- [x] ~~Automatic application updates~~
|
||||
- [x] ~~Enhanced download status display~~
|
||||
- [x] ~~Video chapter extraction~~
|
||||
- [x] ~~Custom output path selection~~
|
||||
- [x] ~~Custom codec selection~~
|
||||
- [x] ~~Settings Panel with toggle switch for features.~~
|
||||
|
||||
### In Progress
|
||||
|
||||
- [ ] Additional format support (WebM, OGG, etc.)
|
||||
- [ ] Refactoring and cleaning the code base.
|
||||
- [ ] Chrome/Edge cookie support
|
||||
- [ ] Improved multi-site support
|
||||
|
||||
### Planned
|
||||
|
||||
- [ ] Subtitle download and embedding
|
||||
- [ ] Multiple language support
|
||||
- [ ] Partial video downloads
|
||||
- [ ] Custom file naming patterns
|
||||
- [ ] Parallel downloads
|
||||
- [ ] Skip sponsored parts of videos
|
||||
- [x] ~~Custom output path selection (default **C:\Users\Username\Downloads** with a **Freedom Loader Output** folder created)~~
|
||||
- [ ] Automatic sponsor detection (SponsorBlock integration)
|
||||
- [ ] Download history
|
||||
- [ ] Linux version
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome and appreciated. This project benefits from community involvement.
|
||||
|
||||
### Bug Reports
|
||||
|
||||
Use the GitHub Issues system and include:
|
||||
|
||||
- Clear description of the issue
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Relevant logs from `AppData\Local\FreedomLoader\logs\`
|
||||
- Screenshots if applicable
|
||||
|
||||
### Feature Requests
|
||||
|
||||
Open a feature request issue with:
|
||||
|
||||
- Clear description of the proposed feature
|
||||
- Use cases and benefits
|
||||
- Potential implementation approach (if applicable)
|
||||
|
||||
### Pull Requests
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/your-feature`)
|
||||
3. Make your changes with clear, descriptive commits
|
||||
4. Test thoroughly
|
||||
5. Update documentation as needed
|
||||
6. Submit a pull request with a detailed description
|
||||
|
||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing.
|
||||
|
||||
## Support
|
||||
|
||||
If you find this project useful and want to support its development:
|
||||
|
||||
[](https://paypal.me/axelnicolas25)
|
||||
|
||||
Your support helps maintain infrastructure, develop new features, and keep the project free and ad-free.
|
||||
|
||||
## License
|
||||
|
||||
Freedom Loader is released under the **GNU General Public License v3.0 (GPLv3)**.
|
||||
|
||||
You are free to use, modify, and redistribute this software under the terms of the GPL-3.0 license. See the [LICENSE](LICENSE) file for complete details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
||||
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
||||
- [Deno](https://deno.com/) for helping me bypassing challenges
|
||||
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
||||
- The open-source community for continuous support and contributions
|
||||
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
||||
- Jacques Chirac to love Apples
|
||||
- All users who test, report issues, and help improve the application
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Preview
|
||||
<div align="center">
|
||||
|
||||
<p align="center">
|
||||
<img src="build/apercu1.0.1.png" alt="Exemple d'application" />
|
||||
</p>
|
||||
**Freedom Loader** - Put freedom in your downloads
|
||||
|
||||
> Here is an example of the app after launch and entering a URL
|
||||
[Website](https://masteracnolo.github.io/FreedomLoader/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
|
||||
|
||||
---
|
||||
|
||||
## ℹ️ About
|
||||
|
||||
This project was built using [](https://github.com/yt-dlp/yt-dlp)
|
||||
Thanks to them for providing a free and open-source API.
|
||||
|
||||
This project is open-source and welcomes your contributions.
|
||||
Here is how to get involved:
|
||||
|
||||
- **🪲 Bug Reporting**
|
||||
Please use the [GitHub issues system](https://github.com/MasterAcnolo/Freedom-Loader/issues) to report any bugs you encounter.
|
||||
Try to provide clear reproduction steps and logs if possible. Logs are located in the installation folder of the program. Example path:
|
||||
`C:\Users\[YOUR NAME]\Desktop\Freedom Loader\resources\app\logs`
|
||||
The log file will be named something like `combined-20xx-xx-xx.log`
|
||||
<br>
|
||||
- **🛠️ Contribution**
|
||||
Pull Requests are welcome! Please fork the repo and submit your PR after ensuring you meet the contribution guidelines (tests, style, etc.).
|
||||
<br>
|
||||
- **💡 Feature Suggestions**
|
||||
You can propose new ideas via a [feature request issue](https://github.com/MasterAcnolo/Freedom-Loader/issues).
|
||||
Any clear and detailed proposal will be reviewed.
|
||||
<br>
|
||||
- **💰 Support the Project**
|
||||
<p>
|
||||
If you'd like to support me, you can donate via
|
||||
<a href="https://paypal.me/axelnicolas25">
|
||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="PayPal">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License** — see the [LICENSE](./LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
I have obviously used AI assistance for some parts of this project. Use AI sparingly and always be aware of what you are doing.
|
||||
</div>
|
||||
157
SECURITY.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We actively maintain and provide security updates for the following versions of Freedom Loader:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.4.x | :white_check_mark: |
|
||||
| 1.3.x | :white_check_mark: |
|
||||
| < 1.3.0 | :x: |
|
||||
|
||||
**Note**: We recommend always using the latest version to benefit from the most recent security patches and features.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in Freedom Loader, please help us keep the project and its users safe by reporting it responsibly.
|
||||
|
||||
### How to Report
|
||||
|
||||
**DO NOT** open a public issue for security vulnerabilities.
|
||||
|
||||
Instead, please report security issues privately by:
|
||||
|
||||
1. **Email**: Send details to **masteracnolo25@gmail.com** with the subject line: `[SECURITY] Vulnerability Report`
|
||||
2. **Include**:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce the issue
|
||||
- Potential impact and severity
|
||||
- Suggested fix (if available)
|
||||
- Your contact information for follow-up
|
||||
|
||||
### What to Expect
|
||||
|
||||
- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours
|
||||
- **Assessment**: We will assess the vulnerability and determine its severity
|
||||
- **Updates**: We will keep you informed of our progress
|
||||
- **Resolution**: We aim to release a fix within 7-14 days for critical vulnerabilities
|
||||
- **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous)
|
||||
|
||||
## Security Best Practices for Users
|
||||
|
||||
When using Freedom Loader, we recommend:
|
||||
|
||||
### General Security
|
||||
|
||||
- **Keep Updated**: Always use the latest version of Freedom Loader
|
||||
- **Official Sources**: Download only from official releases on [GitHub](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
- **Verify Downloads**: Check that installers are properly signed (Windows SmartScreen may show warnings for new releases)
|
||||
- **Antivirus**: Keep your antivirus software up to date
|
||||
|
||||
### Configuration Security
|
||||
|
||||
- **Download Path**: Only set download paths within your user directory (`C:\Users\[USERNAME]\...`)
|
||||
- **Cookies**: Be aware that Firefox cookies are used for authentication—keep Firefox secure
|
||||
- **Logs**: Logs may contain sensitive information—avoid sharing them publicly without review
|
||||
|
||||
### Privacy Considerations
|
||||
|
||||
Freedom Loader respects your privacy:
|
||||
|
||||
- **No Data Collection**: We don't collect, store, or transmit your personal data
|
||||
- **No Telemetry**: No usage tracking or analytics
|
||||
- **Local Operation**: All downloads are processed locally on your machine
|
||||
- **Optional Features**: Discord RPC is optional and can be disabled
|
||||
|
||||
## Known Security Considerations
|
||||
|
||||
### Browser Cookie Access
|
||||
|
||||
Freedom Loader accesses Firefox cookies to download protected content. This is:
|
||||
|
||||
- **By Design**: Required for age-restricted or member-only content
|
||||
- **Local Only**: Cookies are read locally and never transmitted
|
||||
- **User Controlled**: You can control what content you download
|
||||
|
||||
### Native Dependencies
|
||||
|
||||
Freedom Loader bundles native binaries:
|
||||
|
||||
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
||||
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
|
||||
- **Deno**: Official builds from [Deno.land](https://deno.land/)
|
||||
|
||||
These dependencies are verified and updated regularly.
|
||||
|
||||
### Windows Defender Warnings
|
||||
|
||||
New releases may trigger Windows Defender warnings because:
|
||||
|
||||
- The application is not yet widely distributed
|
||||
- Code signing certificates are expensive for open-source projects
|
||||
|
||||
This is expected behavior for new releases. The warning will decrease as more users download the software.
|
||||
|
||||
## Vulnerability Disclosure Policy
|
||||
|
||||
### Our Commitment
|
||||
|
||||
We are committed to:
|
||||
|
||||
- Responding promptly to security reports
|
||||
- Working with security researchers to verify and address issues
|
||||
- Keeping users informed about security updates
|
||||
- Crediting researchers who report vulnerabilities responsibly
|
||||
|
||||
### Disclosure Timeline
|
||||
|
||||
1. **Day 0**: Vulnerability reported privately
|
||||
2. **Day 1-2**: Acknowledgment sent to reporter
|
||||
3. **Day 3-7**: Vulnerability assessed and fix developed
|
||||
4. **Day 7-14**: Fix released (critical vulnerabilities prioritized)
|
||||
5. **Day 14+**: Public disclosure (coordinated with reporter)
|
||||
|
||||
### Scope
|
||||
|
||||
Security issues we're most interested in:
|
||||
|
||||
- **Code Execution**: Arbitrary code execution vulnerabilities
|
||||
- **Path Traversal**: Issues with file system access controls
|
||||
- **Injection**: Command injection or similar vulnerabilities
|
||||
- **Authentication**: Bypass of security controls
|
||||
- **Data Exposure**: Unintended exposure of sensitive data
|
||||
|
||||
### Out of Scope
|
||||
|
||||
The following are generally not considered security vulnerabilities:
|
||||
|
||||
- Issues requiring physical access to the user's machine
|
||||
- Social engineering attacks
|
||||
- Denial of service against third-party services
|
||||
- Issues in third-party dependencies (report to upstream projects)
|
||||
- Missing security headers on local HTTP server (no remote access)
|
||||
|
||||
## Security Updates
|
||||
|
||||
Security updates are released as:
|
||||
|
||||
- **Patch Releases**: For critical security fixes (e.g., 1.4.1 → 1.4.2)
|
||||
- **Automatic Updates**: Users are notified via the built-in update system
|
||||
- **Release Notes**: Security fixes are clearly marked in changelog
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Contributing Guidelines](CONTRIBUTING.md)
|
||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
- [GitHub Security Advisories](https://github.com/MasterAcnolo/Freedom-Loader/security/advisories)
|
||||
|
||||
## Contact
|
||||
|
||||
For security-related questions or concerns:
|
||||
|
||||
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
|
||||
|
||||
---
|
||||
|
||||
**Thank you for helping keep Freedom Loader and its users safe!**
|
||||
|
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.bmp
Normal file
|
After Width: | Height: | Size: 151 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 |
27
config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const packageJson = require("./package.json");
|
||||
const { app } = require("electron");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const localMode = !app.isPackaged;
|
||||
|
||||
// Change path for JSON file
|
||||
const featuresPath = path.join(path.join(`${localMode ? __dirname : process.resourcesPath}`,"config/" , "config.json"));
|
||||
|
||||
let features = {};
|
||||
|
||||
function loadFeatures() {
|
||||
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||
features = JSON.parse(raw);
|
||||
return features;
|
||||
}
|
||||
|
||||
const configFeatures = loadFeatures();
|
||||
|
||||
module.exports = {
|
||||
version: packageJson.version,
|
||||
applicationPort: "8787",
|
||||
localMode,
|
||||
DiscordRPCID: "1410934537051181146",
|
||||
configFeatures
|
||||
}
|
||||
15
config/config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"autoUpdate": true,
|
||||
"discordRPC": true,
|
||||
"customTopBar": true,
|
||||
"autoCheckInfo": true,
|
||||
"addThumbnail": true,
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
"outputTitleCheck": true,
|
||||
"downloadSystem": true,
|
||||
"notifySystem": true
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
};
|
||||
346
main.js
@@ -1,165 +1,301 @@
|
||||
/*
|
||||
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 config = require("./config.js");
|
||||
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 os = require("os");
|
||||
const fs = require("fs");
|
||||
|
||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||
const { AutoUpdater } = require("./server/update.js");
|
||||
const { configFeatures } = require("./config.js");
|
||||
const { startRPC, stopRPC} = require("./server/discordRPC");
|
||||
|
||||
let mainWindow;
|
||||
// Utilise le vrai dossier de logs défini dans logger.js
|
||||
const logsFolderPath = logDir;
|
||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); /* Pour la notif */
|
||||
/*
|
||||
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...");
|
||||
|
||||
const basePath = config.localMode
|
||||
? path.join(__dirname )
|
||||
: path.join(path.dirname(process.execPath), "resources");
|
||||
|
||||
const configFolderPath = path.join(basePath, "config" ,"config.json");
|
||||
|
||||
// Default download path (centralized, lazy loaded)
|
||||
let defaultDownloadPath;
|
||||
|
||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
ipcMain.handle("version", () => config.version);
|
||||
|
||||
// Gestion single instance
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
|
||||
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
||||
function checkNativeDependencies() {
|
||||
// Import centralized paths after app initialization
|
||||
const { binaryPaths } = require("./server/helpers/path");
|
||||
|
||||
const deps = [
|
||||
{ name: "yt-dlp.exe", path: binaryPaths.ytDlp },
|
||||
{ name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
|
||||
{ name: "ffprobe.exe", path: binaryPaths.ffprobe },
|
||||
{ name: "deno.exe", path: binaryPaths.deno },
|
||||
];
|
||||
const missing = deps.filter(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.warn("La fenêtre existe déjà, pas de nouvelle création");
|
||||
logger.info("New Instance Detected, closing the older...");
|
||||
mainWindow.destroy();
|
||||
mainWindow = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Création fenêtre principale
|
||||
async function createMainWindow() {
|
||||
if (mainWindow) {
|
||||
logger.warn("Window already exists, no new creation!");
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
title: "Freedom Loader 1.2.3", // A Changer à chaque Version
|
||||
title: `Freedom Loader ${config.version}`,
|
||||
width: 750,
|
||||
height: 800,
|
||||
minWidth: 750,
|
||||
minHeight: 800,
|
||||
frame: !configFeatures.customTopBar,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
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 {
|
||||
await mainWindow.loadURL("http://localhost:8080");
|
||||
logger.info("Fenêtre chargée");
|
||||
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||
logger.info("Window Loaded");
|
||||
} 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", () => {
|
||||
logger.info("Fenêtre principale fermée");
|
||||
logger.info("Main Window Closed");
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
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");
|
||||
function validateDownloadPath(userPath) {
|
||||
const { isSafePath } = require("./server/helpers/validation");
|
||||
|
||||
// Lazy load default path
|
||||
if (!defaultDownloadPath) {
|
||||
const { defaultDownloadFolder } = require("./server/helpers/path");
|
||||
defaultDownloadPath = defaultDownloadFolder;
|
||||
}
|
||||
|
||||
if (!userPath) return defaultDownloadPath;
|
||||
|
||||
/*
|
||||
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 () => {
|
||||
logger.info("Demande de sélection d'un dossier reçue depuis le renderer");
|
||||
try {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory"]
|
||||
});
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
logger.info(`Dossier sélectionné : ${result.filePaths[0]}`);
|
||||
return result.filePaths[0];
|
||||
// Canonical resolution and symlink following
|
||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||
|
||||
// Use the same validation as backend (allows all drives except system folders)
|
||||
if (!isSafePath(resolved)) {
|
||||
throw new Error("Path not allowed: system folders are blocked!");
|
||||
}
|
||||
return null;
|
||||
|
||||
return resolved;
|
||||
} catch (err) {
|
||||
logger.error(`Erreur lors de la sélection de dossier : ${err.message}`);
|
||||
logger.error(`Invalid download path: ${userPath} - ${err.message}`);
|
||||
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// IPC
|
||||
ipcMain.handle("select-download-folder", async () => {
|
||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||
if (result.canceled) {
|
||||
logger.info("Folder selection cancelled by user");
|
||||
return null;
|
||||
}
|
||||
if (result.filePaths.length > 0) {
|
||||
const selectedPath = result.filePaths[0];
|
||||
try {
|
||||
const validatedPath = validateDownloadPath(selectedPath);
|
||||
logger.info(`Folder selected and validated: ${validatedPath}`);
|
||||
return validatedPath;
|
||||
} catch (err) {
|
||||
logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
|
||||
throw err; // Propagate error to UI
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
/*
|
||||
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.handle("validate-download-path", (event, userPath) => {
|
||||
return validateDownloadPath(userPath);
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle("get-default-download-path", () => {
|
||||
if (!defaultDownloadPath) {
|
||||
const { defaultDownloadFolder } = require("./server/helpers/path");
|
||||
defaultDownloadPath = defaultDownloadFolder;
|
||||
}
|
||||
return defaultDownloadPath;
|
||||
});
|
||||
|
||||
/*
|
||||
Événement déclenché quand l'application est prête.
|
||||
On démarre le serveur Express, puis on crée la fenêtre principale.
|
||||
En cas d'erreur, on log et on quitte l'application proprement.
|
||||
*/
|
||||
ipcMain.on("set-progress", (event, percent) => {
|
||||
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1
|
||||
});
|
||||
|
||||
// Topbar window controls
|
||||
ipcMain.on("window-minimize", () => {
|
||||
if (mainWindow) mainWindow.minimize();
|
||||
});
|
||||
|
||||
// Toggle Maximize -> UnMaximize
|
||||
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");
|
||||
});
|
||||
ipcMain.on("open-config", () => {
|
||||
if (configFolderPath) shell.openPath(configFolderPath);
|
||||
});
|
||||
|
||||
|
||||
// App ready
|
||||
app.whenReady().then(async () => {
|
||||
logSessionStart();
|
||||
logger.info("App prête, démarrage du serveur Express...");
|
||||
const expressServer = require("./server/server.js");
|
||||
logger.info("App Ready, Server Express starting...");
|
||||
|
||||
const serverPath = path.join(__dirname, "server", "server.js")
|
||||
|
||||
const expressServer = require(serverPath);
|
||||
|
||||
try {
|
||||
await expressServer.startServer();
|
||||
logger.info("Serveur Express démarré");
|
||||
await createWindow();
|
||||
logger.info("Express Server Started");
|
||||
|
||||
// Ajout du menu personnalisé
|
||||
const menuTemplate = [
|
||||
// ... Menu standard Electron ...
|
||||
{
|
||||
label: "Logs",
|
||||
submenu: [
|
||||
{
|
||||
label: "Ouvrir les logs",
|
||||
click: () => {
|
||||
shell.openPath(logsFolderPath);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
ipcMain.handle("features", () => {
|
||||
return configFeatures;
|
||||
});
|
||||
|
||||
// Fusionner avec le menu par défaut
|
||||
const defaultMenu = Menu.getApplicationMenu();
|
||||
let template = menuTemplate;
|
||||
if (defaultMenu) {
|
||||
template = [...Menu.getApplicationMenu().items.map(item => item), ...menuTemplate];
|
||||
}
|
||||
const finalMenu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(finalMenu);
|
||||
const featureWhitelist = new Set([
|
||||
"autoUpdate",
|
||||
"discordRPC",
|
||||
"customTopBar",
|
||||
"autoCheckInfo",
|
||||
"addThumbnail",
|
||||
"addMetadata",
|
||||
"verboseLogs",
|
||||
"autoDownloadPlaylist",
|
||||
"customCodec"
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Erreur serveur ou fenêtre :", error);
|
||||
|
||||
ipcMain.handle("set-feature", (event, { key, value }) => {
|
||||
try {
|
||||
if (!featureWhitelist.has(key)) {
|
||||
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// optionnel mais propre
|
||||
if (configFeatures[key] === value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
configFeatures[key] = value;
|
||||
|
||||
fs.writeFileSync(
|
||||
configFolderPath,
|
||||
JSON.stringify(configFeatures, null, 2),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
logger.info(`Feature updated: ${key} = ${value}`);
|
||||
return true;
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`set-feature failed (${key}): ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (configFeatures.discordRPC) startRPC(); // Discord RPC
|
||||
|
||||
await createMainWindow();
|
||||
|
||||
if (configFeatures.autoUpdate) AutoUpdater(mainWindow); // Auto Update
|
||||
|
||||
} catch (err) {
|
||||
logger.error("Window or Server error :", err);
|
||||
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", () => {
|
||||
logger.info("Toutes fenêtres fermées, quitte l'app");
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
logger.info("Shuting Down App...");
|
||||
app.quit();
|
||||
});
|
||||
|
||||
/*
|
||||
Avant de quitter l'application, on log la fin de session pour traçabilité.
|
||||
*/
|
||||
app.on("before-quit", () => {
|
||||
app.on("before-quit", async () => {
|
||||
await stopRPC();
|
||||
logger.info("All Services Stopped. Have a nice day!")
|
||||
logSessionEnd();
|
||||
});
|
||||
|
||||
5034
package-lock.json
generated
63
package.json
@@ -1,40 +1,40 @@
|
||||
{
|
||||
"name": "freedom-loader",
|
||||
"productName": "Freedom Loader",
|
||||
"version": "1.2.3",
|
||||
"version": "1.4.5",
|
||||
"author": "MasterAcnolo",
|
||||
"description": "Freedom Loader",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"dist": "electron-builder"
|
||||
"start": "electron --trace-warnings .",
|
||||
"build": "electron-builder",
|
||||
"update": "npm update"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"debug": "^4.4.1",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"webidl-conversions": "^8.0.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"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-builder": "^26.0.12"
|
||||
"electron-builder": "^25.1.8"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.masteracnolo.freedomloader",
|
||||
"productName": "Freedom Loader",
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "MasterAcnolo",
|
||||
"repo": "Freedom-Loader"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"**/*",
|
||||
"node_modules/**/*",
|
||||
@@ -50,28 +50,49 @@
|
||||
"to": "confirm-icon.png"
|
||||
},
|
||||
{
|
||||
"from": "ressources/yt-dlp 2025.09.23.exe",
|
||||
"to": "yt-dlp 2025.09.23.exe"
|
||||
"from": "build/app-icon.ico",
|
||||
"to": "app-icon.ico"
|
||||
},
|
||||
{
|
||||
"from": "build/banner.bmp",
|
||||
"to": "banner.bmp"
|
||||
},
|
||||
{
|
||||
"from": "build/error.png",
|
||||
"to": "error.png"
|
||||
},
|
||||
{
|
||||
"from": "ressources/yt-dlp.exe",
|
||||
"to": "binaries/yt-dlp.exe"
|
||||
},
|
||||
{
|
||||
"from": "ressources/ffmpeg.exe",
|
||||
"to": "ffmpeg.exe"
|
||||
"to": "binaries/ffmpeg.exe"
|
||||
},
|
||||
{
|
||||
"from": "ressources/ffprobe.exe",
|
||||
"to": "ffprobe.exe"
|
||||
"to": "binaries/ffprobe.exe"
|
||||
},
|
||||
{
|
||||
"from": "ressources/deno.exe",
|
||||
"to": "binaries/deno.exe"
|
||||
},
|
||||
{
|
||||
"from": "config/config.json",
|
||||
"to": "config/config.json"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "build/installer-icon.ico"
|
||||
"icon": "build/app-icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": true,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"installerIcon": "build/installer-icon.ico",
|
||||
"installerIcon": "build/app-icon.ico",
|
||||
"uninstallerIcon": "build/uninstaller-icon.ico",
|
||||
"installerSidebar": "build/banner.bmp",
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "Freedom Loader"
|
||||
|
||||
42
preload.js
@@ -1,31 +1,23 @@
|
||||
/*
|
||||
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");
|
||||
|
||||
/*
|
||||
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", {
|
||||
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
||||
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
||||
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
|
||||
getFeatures: () => ipcRenderer.invoke("features"),
|
||||
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
|
||||
getVersion: () => ipcRenderer.invoke("version"),
|
||||
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path)
|
||||
});
|
||||
|
||||
// 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"),
|
||||
openConfig: () => ipcRenderer.send("open-config")
|
||||
});
|
||||
|
||||
|
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 |
@@ -7,19 +7,198 @@
|
||||
default-src 'self' 'unsafe-inline' data:;
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||
font-src https://fonts.gstatic.com;
|
||||
img-src 'self' data: https://i.ytimg.com;
|
||||
img-src 'self' data: https:;
|
||||
">
|
||||
<title></title>
|
||||
|
||||
<!--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>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="topbar" id="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>
|
||||
|
||||
<button class="settings-button" id="settings-button">
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-q</title><path d="M262.29,192.31a64,64,0,1,0,57.4,57.4A64.13,64.13,0,0,0,262.29,192.31ZM416.39,256a154.34,154.34,0,0,1-1.53,20.79l45.21,35.46A10.81,10.81,0,0,1,462.52,326l-42.77,74a10.81,10.81,0,0,1-13.14,4.59l-44.9-18.08a16.11,16.11,0,0,0-15.17,1.75A164.48,164.48,0,0,1,325,400.8a15.94,15.94,0,0,0-8.82,12.14l-6.73,47.89A11.08,11.08,0,0,1,298.77,470H213.23a11.11,11.11,0,0,1-10.69-8.87l-6.72-47.82a16.07,16.07,0,0,0-9-12.22,155.3,155.3,0,0,1-21.46-12.57,16,16,0,0,0-15.11-1.71l-44.89,18.07a10.81,10.81,0,0,1-13.14-4.58l-42.77-74a10.8,10.8,0,0,1,2.45-13.75l38.21-30a16.05,16.05,0,0,0,6-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16,16,0,0,0-6.07-13.94l-38.19-30A10.81,10.81,0,0,1,49.48,186l42.77-74a10.81,10.81,0,0,1,13.14-4.59l44.9,18.08a16.11,16.11,0,0,0,15.17-1.75A164.48,164.48,0,0,1,187,111.2a15.94,15.94,0,0,0,8.82-12.14l6.73-47.89A11.08,11.08,0,0,1,213.23,42h85.54a11.11,11.11,0,0,1,10.69,8.87l6.72,47.82a16.07,16.07,0,0,0,9,12.22,155.3,155.3,0,0,1,21.46,12.57,16,16,0,0,0,15.11,1.71l44.89-18.07a10.81,10.81,0,0,1,13.14,4.58l42.77,74a10.8,10.8,0,0,1-2.45,13.75l-38.21,30a16.05,16.05,0,0,0-6.05,14.08C416.17,247.67,416.39,251.83,416.39,256Z" style="fill:none;stroke:var(--settings-button-text-color);stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
|
||||
</button>
|
||||
|
||||
|
||||
<div class="settings-panel" id="settings-panel">
|
||||
<div class="settings-modal">
|
||||
|
||||
<button class="close-btn" onclick="closePanel()">✕</button>
|
||||
|
||||
<div class="settings-title">
|
||||
<h2>SETTINGS</h2>
|
||||
<p style="font-size: 0.8em;">Changes are saved automatically. <br> Somes changes could need a restart</p>
|
||||
</div>
|
||||
|
||||
<!-- Basic Settings visibles -->
|
||||
<div class="settings-section">
|
||||
<h3>Basic Settings</h3>
|
||||
|
||||
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
||||
<div class="setting-info">
|
||||
<div style="display: flex;">
|
||||
<span class="setting-title">Thème</span>
|
||||
|
||||
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
||||
|
||||
</select>
|
||||
</div>
|
||||
<small>Choose the theme of the application</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="discordRPC" id="discordRPC">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Discord RPC</span>
|
||||
<small>Enable/disable Discord Rich Presence for your app.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoDownloadPlaylist" id="autoDownloadPlaylist">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Download Playlist automatically?</span>
|
||||
<small>If enabled, playlists will download without confirmation.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="notifySystem" id="notifySystem">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Notification at the end</span>
|
||||
<small>Notify when a download finishes.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="customTopBar" id="customTopBar">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Custom Top bar</span>
|
||||
<small>Use this if the topbar is missing or you prefer a custom one.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Auto Fetch Info</span>
|
||||
<small>Automatically fetch information for new downloads.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Advanced Settings -->
|
||||
<details class="settings-section">
|
||||
<summary>Advanced Settings</summary>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoUpdate" id="autoUpdate">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Auto Update</span>
|
||||
<small>Enable automatic app updates.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Output title (special chars)</span>
|
||||
<small>Check for unusual characters in output titles.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Add Metadata</span>
|
||||
<small>Include metadata in downloaded files.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="addThumbnail" id="addThumbnail">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Add Thumbnail</span>
|
||||
<small>Include thumbnails in downloaded files.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item" style="margin-left: 32px; margin-top: 20px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Custom Codec</span>
|
||||
<select data-key="customCodec">
|
||||
<option value="h264">H264</option>
|
||||
<option value="av1">AV1</option>
|
||||
<option value="av01">AV01</option>
|
||||
<option value="vp9.2">VP9.2</option>
|
||||
<option value="vp9">VP9</option>
|
||||
<option value="h265">H265</option>
|
||||
<option value="theora">Theora</option>
|
||||
</select>
|
||||
<small>Choose your preferred video codec.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<!-- Developer Settings -->
|
||||
<details class="settings-section">
|
||||
<summary>Developer Settings</summary>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="downloadSystem" id="downloadSystem">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Download System</span>
|
||||
<small>Enable/disable the internal download system.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="logSystem" id="logSystem">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Log System</span>
|
||||
<small>Enable logging system.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Verbose Logs</span>
|
||||
<small>Show verbose logs for debugging purposes.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<div class="settings-footer">
|
||||
<button id="open-json-btn">Open JSON Configuration</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container" id="container">
|
||||
|
||||
<header>
|
||||
<h1 id="title"> Freedom Loader </h1>
|
||||
@@ -27,7 +206,14 @@
|
||||
</header>
|
||||
|
||||
<form action="/download" method="POST" id="downloadForm">
|
||||
<input name="url" placeholder="URL" id="UrlInput">
|
||||
<div class="url-input-container">
|
||||
<input type="text" name="url" placeholder="URL" id="UrlInput">
|
||||
<button type="button" id="pasteBtn" class="paste-btn" title="Paste from clipboard">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 5H7C5.89543 5 5 5.89543 5 7V19C5 20.1046 5.89543 21 7 21H17C18.1046 21 19 20.1046 19 19V7C19 5.89543 18.1046 5 17 5H15M9 5C9 6.10457 9.89543 7 11 7H13C14.1046 7 15 6.10457 15 5M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5M12 12H15M12 16H15M9 12H9.01M9 16H9.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="checkbox-inline">
|
||||
|
||||
@@ -35,56 +221,64 @@
|
||||
<input type="checkbox" name="audioOnly" value="1">
|
||||
<div class="checkmark"></div>
|
||||
</label>
|
||||
<!-- <input type="checkbox" name="audioOnly" value="1"> -->
|
||||
Audio seulement (MP3)
|
||||
Audio Only (MP3)
|
||||
</label>
|
||||
<!-- <input type="checkbox" name="audioOnly" value="1"> Audio seulement (MP3) -->
|
||||
<select name="quality">
|
||||
<option value="best">Meilleure qualité</option>
|
||||
<option value="medium">Qualité moyenne</option>
|
||||
<option value="worst">Qualité minimale</option>
|
||||
<option value="best">Best Quality</option>
|
||||
<option value="medium">Medium Quality</option>
|
||||
<option value="worst">Lowest Quality</option>
|
||||
<option value="1080">1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value="480">480p</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Télécharger</button>
|
||||
<button type="submit" onclick="startProgress()">Download</button>
|
||||
</form>
|
||||
|
||||
<!--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.-->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="download-path">
|
||||
Sera enregistré dans <span id="savePath"></span>
|
||||
<button id="changePath">Modifier</button>
|
||||
Will be save on <span id="savePath"></span>
|
||||
<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 id="downloadStatus"></div>
|
||||
|
||||
<div class="infos-container">
|
||||
|
||||
<div id="loaderBox" class="loader-box" style="display:none;">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<span>Fetching info...</span>
|
||||
</div>
|
||||
|
||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<footer class="theme-switcher">
|
||||
<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>
|
||||
<span id="version-badge" class="version-badge"></span>
|
||||
|
||||
<h2 id="reference"> Because Why Not ? </h2>
|
||||
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="script/clipboardPaste.js"></script>
|
||||
<script src="script/custompath.js"></script>
|
||||
<script src="script/settingsPanel.js"></script>
|
||||
<script src="script/appVersion.js"></script>
|
||||
<script src="script/downloadstatus.js"></script>
|
||||
<script src="script/fetchinfo.js"></script>
|
||||
<script src="script/chirac.js"></script>
|
||||
<script src="script/progressBar.js"></script>
|
||||
<script src="script/customthemes.js"></script>
|
||||
<script type="module" src="script/topbar.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
8
public/script/appVersion.js
Normal file
@@ -0,0 +1,8 @@
|
||||
async function versionLabel(){
|
||||
const appVersion = await window.electronAPI.getVersion();
|
||||
|
||||
// Write in front the app version for debugging (bottom right)
|
||||
document.getElementById("version-badge").textContent = `v${appVersion}`;
|
||||
};
|
||||
|
||||
versionLabel();
|
||||
@@ -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");
|
||||
}
|
||||
35
public/script/clipboardPaste.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// Clipboard paste functionality for URL input
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pasteBtn = document.getElementById('pasteBtn');
|
||||
const urlInput = document.getElementById('UrlInput');
|
||||
|
||||
if (pasteBtn && urlInput) {
|
||||
pasteBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
// Read text from clipboard
|
||||
const text = await navigator.clipboard.readText();
|
||||
|
||||
// Paste into the URL input
|
||||
urlInput.value = text;
|
||||
|
||||
// Trigger input event to ensure any listeners are notified
|
||||
urlInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
// Optional: Focus the input
|
||||
urlInput.focus();
|
||||
|
||||
// Visual feedback
|
||||
pasteBtn.style.backgroundColor = 'var(--form-button-bg-hover-color)';
|
||||
setTimeout(() => {
|
||||
pasteBtn.style.backgroundColor = '';
|
||||
}, 200);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard:', err);
|
||||
|
||||
// Fallback: prompt user if clipboard API fails
|
||||
alert('Unable to access clipboard. Please paste manually using Ctrl+V.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,71 +1,59 @@
|
||||
/*
|
||||
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 () => {
|
||||
const savePathElem = document.getElementById("savePath");
|
||||
const form = document.getElementById("downloadForm");
|
||||
|
||||
// 1️Essayer de charger depuis le localStorage
|
||||
let savedPath = localStorage.getItem("customDownloadPath");
|
||||
|
||||
// 2️Sinon demander le chemin par défaut à l'API Electron
|
||||
if (!savedPath) {
|
||||
savedPath = await window.electronAPI.getDefaultDownloadPath();
|
||||
}
|
||||
|
||||
// 3 Afficher le chemin
|
||||
if (savePathElem) {
|
||||
savePathElem.textContent = savedPath;
|
||||
}
|
||||
|
||||
// Créer l'input caché s'il n'existe pas déjà
|
||||
// input caché = DERNIER chemin validé par le back
|
||||
let hidden = document.getElementById("savePathInput");
|
||||
if (!hidden) {
|
||||
hidden = document.createElement("input");
|
||||
hidden.type = "hidden";
|
||||
hidden.name = "savePath";
|
||||
hidden.id = "savePathInput";
|
||||
document.getElementById("downloadForm").appendChild(hidden);
|
||||
form.appendChild(hidden);
|
||||
}
|
||||
hidden.value = savedPath;
|
||||
|
||||
// Gestion du bouton de modification
|
||||
document.getElementById("changePath").addEventListener("click", async () => {
|
||||
const selectedPath = await window.electronAPI.selectDownloadFolder();
|
||||
if (selectedPath) {
|
||||
// Met à jour l'affichage
|
||||
savePathElem.textContent = selectedPath;
|
||||
hidden.value = selectedPath;
|
||||
async function applyPathFromBack(path) {
|
||||
savePathElem.textContent = path;
|
||||
hidden.value = path;
|
||||
localStorage.setItem("customDownloadPath", path); // UX only
|
||||
}
|
||||
|
||||
// Et le stocke en localStorage pour la prochaine fois
|
||||
localStorage.setItem("customDownloadPath", selectedPath);
|
||||
async function loadInitialPath() {
|
||||
// On affiche ce que le user a vu la dernière fois (UX)
|
||||
const cached = localStorage.getItem("customDownloadPath");
|
||||
if (cached) {
|
||||
savePathElem.textContent = cached;
|
||||
}
|
||||
|
||||
// MAIS la source de vérité reste le back
|
||||
try {
|
||||
const validatedPath = await window.electronAPI.getValidatedDownloadPath(
|
||||
cached
|
||||
);
|
||||
await applyPathFromBack(validatedPath);
|
||||
} catch {
|
||||
// fallback sûr
|
||||
const defaultPath =
|
||||
await window.electronAPI.getDefaultDownloadPath();
|
||||
await applyPathFromBack(defaultPath);
|
||||
}
|
||||
}
|
||||
|
||||
await loadInitialPath();
|
||||
|
||||
document
|
||||
.getElementById("changePath")
|
||||
.addEventListener("click", async () => {
|
||||
try {
|
||||
// selectDownloadFolder already returns a validated path
|
||||
const validatedPath =
|
||||
await window.electronAPI.selectDownloadFolder();
|
||||
|
||||
if (!validatedPath) return; // cancelled
|
||||
|
||||
await applyPathFromBack(validatedPath);
|
||||
} catch (err) {
|
||||
alert("This folder is not allowed. Only specific folders (Users, Downloads, Documents) are authorized for downloads.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,89 +1,44 @@
|
||||
/*
|
||||
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 = {
|
||||
default: "Default",
|
||||
dark: "Sombre",
|
||||
light: "Clair",
|
||||
nf : "NF",
|
||||
chirac: "Chirac",
|
||||
fanatic: "Fanatic",
|
||||
cyberpunk: "Cyberpunk",
|
||||
spicy: "Spicy",
|
||||
vilbrequin: "Vilbrequin"
|
||||
};
|
||||
|
||||
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"
|
||||
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
|
||||
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
|
||||
neon: { label: "Purple", subtitle: "How was your day ?"},
|
||||
nf: { label: "NF", subtitle: "You call it music, i call it my Therapist" },
|
||||
songbird: { label: "Songbird", subtitle: "From Her to Eternity" },
|
||||
drift: { label: "Drift", subtitle: "Si la route t'appelle, contre appel" },
|
||||
fanatic: { label: "Fanatic", subtitle: "Always Fnatic !" },
|
||||
chirac: { label: "Chirac", subtitle: "J'aime les pommes" },
|
||||
spicy: { label: "Spicy", subtitle: "The Spiciest One" },
|
||||
};
|
||||
|
||||
const themeSelect = document.getElementById("themeSelect");
|
||||
|
||||
// Remplir le select avec les options
|
||||
function populateThemeSelect() {
|
||||
for (const [key, label] of Object.entries(themes)) {
|
||||
for (const [themeKey, themeInfo] of Object.entries(themes)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = key;
|
||||
option.textContent = label;
|
||||
option.value = themeKey;
|
||||
option.textContent = themeInfo.label;
|
||||
themeSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// Appliquer le thème sur le body et changer le subtitle
|
||||
function applyTheme(themeName) {
|
||||
// Supprime les classes thème précédentes
|
||||
function applyTheme(themeKey) {
|
||||
document.body.classList.remove(...Object.keys(themes));
|
||||
// Ajoute la classe du thème actuel
|
||||
document.body.classList.add(themeName);
|
||||
document.body.classList.add(themeKey);
|
||||
|
||||
// Modifier le subtitle
|
||||
const subtitle = document.getElementById("subtitle");
|
||||
if (subtitle && themeSubtitles[themeName]) {
|
||||
subtitle.textContent = themeSubtitles[themeName];
|
||||
const subtitleElement = document.getElementById("subtitle");
|
||||
if (subtitleElement && themes[themeKey]) {
|
||||
subtitleElement.textContent = themes[themeKey].subtitle;
|
||||
}
|
||||
|
||||
// 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(themeName) {
|
||||
localStorage.setItem("selectedTheme", themeName);
|
||||
function saveTheme(themeKey) {
|
||||
localStorage.setItem("selectedTheme", themeKey);
|
||||
}
|
||||
|
||||
// Charger le thème au démarrage
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem("selectedTheme");
|
||||
|
||||
if (savedTheme && themes[savedTheme]) {
|
||||
applyTheme(savedTheme);
|
||||
themeSelect.value = savedTheme;
|
||||
@@ -93,12 +48,12 @@ function loadTheme() {
|
||||
}
|
||||
}
|
||||
|
||||
// Quand l’utilisateur change le select
|
||||
themeSelect.addEventListener("change", (e) => {
|
||||
const selected = e.target.value;
|
||||
if (themes[selected]) {
|
||||
applyTheme(selected);
|
||||
saveTheme(selected);
|
||||
// When the user changes the theme from the select
|
||||
themeSelect.addEventListener("change", (event) => {
|
||||
const selectedTheme = event.target.value;
|
||||
if (themes[selectedTheme]) {
|
||||
applyTheme(selectedTheme);
|
||||
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");
|
||||
|
||||
// Élément où on affichera l’état du téléchargement (en cours, erreur, succès)
|
||||
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");
|
||||
|
||||
// Écouteur d’événement pour intercepter la soumission du formulaire
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault(); // Empêche le rechargement automatique de la page
|
||||
e.preventDefault();
|
||||
button.disabled = true; // Avoid multiple clicks
|
||||
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 params = new URLSearchParams(formData);
|
||||
|
||||
try {
|
||||
// Envoie la requête POST vers /download avec les données du formulaire
|
||||
const res = await fetch("/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params,
|
||||
});
|
||||
|
||||
// Si la réponse n’est pas un succès (code HTTP 4xx ou 5xx)
|
||||
if (!res.ok) {
|
||||
statusDiv.textContent = "❌ Erreur pendant le téléchargement.";
|
||||
statusDiv.textContent = res;
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupère le texte envoyé par le serveur (message de succès ou erreur)
|
||||
const text = await res.text();
|
||||
|
||||
// Affiche le message retourné par le serveur
|
||||
statusDiv.textContent = text;
|
||||
|
||||
} catch {
|
||||
// Gestion des erreurs réseau ou autres exceptions
|
||||
statusDiv.textContent = "❌ Une erreur s’est produite.";
|
||||
statusDiv.textContent = "❌ An Error has Occured.";
|
||||
} finally {
|
||||
// Réactive le bouton pour permettre d’autres soumissions
|
||||
button.disabled = false;
|
||||
|
||||
// Efface le message d’état après 5 secondes pour garder l’interface propre
|
||||
setTimeout(() => {
|
||||
statusDiv.textContent = "";
|
||||
}, 5000);
|
||||
|
||||
@@ -1,136 +1,230 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
// On attend que le DOM soit complètement chargé
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Récupération des éléments du DOM pour interaction
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
const infoDiv = document.getElementById("videoInfo");
|
||||
|
||||
// Stocke la dernière URL fetchée pour éviter les requêtes répétées inutiles
|
||||
let lastFetchedUrl = "";
|
||||
|
||||
// Active les logs DEBUG pour le développement
|
||||
const DEBUG = true;
|
||||
|
||||
// Écoute les changements dans l'input URL
|
||||
urlInput.addEventListener("input", async () => {
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
if (DEBUG) console.log("[DEBUG] Input détecté :", url);
|
||||
|
||||
// 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.classList.remove("visible");
|
||||
lastFetchedUrl = "";
|
||||
return;
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr || dateStr.length !== 8) return "Inconnue";
|
||||
return `${dateStr.slice(6,8)}/${dateStr.slice(4,6)}/${dateStr.slice(0,4)}`;
|
||||
}
|
||||
|
||||
// Si l'URL est identique à la dernière requêtée, on ne refait pas la requête
|
||||
if (url === lastFetchedUrl) {
|
||||
if (DEBUG) console.log("[DEBUG] Même URL que précédemment, pas de fetch.");
|
||||
return;
|
||||
function formatSize(bytes) {
|
||||
return bytes ? (bytes / (1024*1024)).toFixed(2) + " Mo" : "Inconnue";
|
||||
}
|
||||
|
||||
// Mémorise l'URL actuelle comme dernière URL traitée
|
||||
lastFetchedUrl = url;
|
||||
|
||||
async function fetchVideoInfo(url) {
|
||||
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);
|
||||
if (!res.ok) return { error: `An Error occured when fetching info` };
|
||||
|
||||
// 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);
|
||||
if (!data) return { error: "Data is Missing" };
|
||||
|
||||
// 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 data;
|
||||
} catch (e) {
|
||||
return { error: "Network or JSON Issue" };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function init() {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
const infoDiv = document.getElementById("videoInfo");
|
||||
const loaderBox = document.getElementById("loaderBox");
|
||||
|
||||
let lastFetchedUrl = "";
|
||||
|
||||
urlInput.addEventListener("input", async () => {
|
||||
|
||||
const configFeatures = await window.electronAPI.getFeatures();
|
||||
if (!configFeatures.autoCheckInfo) return;
|
||||
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
// IF field empty -> total reset
|
||||
if (!url || url.length < 2) {
|
||||
infoDiv.innerHTML = "";
|
||||
infoDiv.classList.remove("visible", "playlist-mode");
|
||||
lastFetchedUrl = "";
|
||||
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`;
|
||||
if (url === lastFetchedUrl) return;
|
||||
lastFetchedUrl = url;
|
||||
|
||||
// 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";
|
||||
loaderBox.style.display = "flex";
|
||||
const data = await fetchVideoInfo(url);
|
||||
loaderBox.style.display = "none";
|
||||
|
||||
// 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";
|
||||
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;
|
||||
}
|
||||
|
||||
// ---------- PLAYLIST ----------
|
||||
if (data.type === "playlist") {
|
||||
infoDiv.classList.add("playlist-mode");
|
||||
infoDiv.innerHTML = `
|
||||
<div class="playlist-header">
|
||||
<div class="playlist-info">
|
||||
<div class="playlist-badge">Detected Playlist</div>
|
||||
<h3 class="playlist-title">${data.title}</h3>
|
||||
<div class="playlist-meta">
|
||||
<span class="playlist-count">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 6H20M4 12H20M4 18H11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
${data.count} vidéos
|
||||
</span>
|
||||
${data.channel ? `<span class="playlist-channel">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M5 20C5 16.134 8.134 13 12 13C15.866 13 19 16.134 19 20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
${data.channel}
|
||||
</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="playlistVideos"></div>
|
||||
`;
|
||||
|
||||
const listDiv = document.getElementById("playlistVideos");
|
||||
|
||||
data.videos.forEach(v => {
|
||||
const durationStr = v.duration
|
||||
? `${Math.floor(v.duration / 60)}:${(v.duration % 60).toString().padStart(2,"0")}` : "--:--";
|
||||
|
||||
const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
|
||||
|
||||
listDiv.innerHTML += `
|
||||
<div class="playlist-video-card">
|
||||
<div class="video-thumbnail-wrapper">
|
||||
<img src="${v.thumbnail}" alt="${v.title}" class="video-thumbnail">
|
||||
<span class="video-duration">${durationStr}</span>
|
||||
</div>
|
||||
<div class="video-content">
|
||||
<h4 class="video-title" title="${v.title}">${v.title}</h4>
|
||||
${v.uploader ? `<p class="video-uploader">${v.uploader}</p>` : ''}
|
||||
<div class="video-actions">
|
||||
<a href="${videoUrl}" target="_blank" class="video-link">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
Ouvrir
|
||||
</a>
|
||||
<button class="copy-btn" data-url="${videoUrl}" title="Copier l'URL">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="8" y="8" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Copy Button
|
||||
listDiv.addEventListener("click", (event) => {
|
||||
if (event.target.classList.contains("copy-btn") || event.target.closest(".copy-btn")) {
|
||||
const btn = event.target.closest(".copy-btn") || event.target;
|
||||
if (btn.disabled) return;
|
||||
|
||||
btn.disabled = true;
|
||||
const url = btn.dataset.url;
|
||||
navigator.clipboard.writeText(url)
|
||||
.then(() => {
|
||||
const original = btn.innerHTML;
|
||||
|
||||
btn.style.opacity = 0;
|
||||
btn.style.transform = "scale(0.7)";
|
||||
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>`;
|
||||
btn.style.opacity = 1;
|
||||
btn.style.transform = "scale(1)";
|
||||
|
||||
setTimeout(() => {
|
||||
btn.style.opacity = 0;
|
||||
btn.style.transform = "scale(0.7)";
|
||||
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = original;
|
||||
btn.style.opacity = 1;
|
||||
btn.style.transform = "scale(1)";
|
||||
btn.disabled = false;
|
||||
}, 300);
|
||||
|
||||
}, 1000);
|
||||
|
||||
}, 300);
|
||||
})
|
||||
.catch(() => {
|
||||
const original = btn.innerHTML;
|
||||
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = original;
|
||||
btn.disabled = false;
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
infoDiv.classList.add("visible");
|
||||
return;
|
||||
}
|
||||
|
||||
infoDiv.classList.remove("playlist-mode");
|
||||
|
||||
// ---------- 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";
|
||||
|
||||
// Construction du HTML pour afficher les informations de la vidéo
|
||||
infoDiv.innerHTML = `
|
||||
<h3>${data.title}</h3>
|
||||
<img src="${data.thumbnail}" width="320" alt="Thumbnail">
|
||||
<ul>
|
||||
<li><strong>Durée :</strong> ${durationStr}</li>
|
||||
<li><strong>Duration :</strong> ${durationStr}</li>
|
||||
<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>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" 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>
|
||||
<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>
|
||||
`;
|
||||
|
||||
// Affiche la div contenant les infos
|
||||
infoDiv.classList.add("visible");
|
||||
});
|
||||
|
||||
if (DEBUG) console.log("[DEBUG] Info affichée avec succès.");
|
||||
} catch (e) {
|
||||
// En cas d'erreur réseau ou JSON, on affiche un message d'erreur
|
||||
console.error("[CRITICAL] Erreur lors de la récupération:", e);
|
||||
infoDiv.innerHTML = "❌ Erreur réseau ou JSON.";
|
||||
infoDiv.classList.remove("visible");
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
init();
|
||||
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";
|
||||
}
|
||||
|
||||
// SSE Connexion
|
||||
const evtSource = new EventSource("/download/progress");
|
||||
evtSource.onmessage = e => {
|
||||
if (e.data === "reset") {
|
||||
startProgress();
|
||||
window.electronAPI.setProgress(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.data === "done") {
|
||||
resetProgress();
|
||||
window.electronAPI.setProgress(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
const percent = parseFloat(e.data);
|
||||
|
||||
if (!isNaN(percent)) {
|
||||
updateProgress(percent);
|
||||
window.electronAPI.setProgress(percent); // Update Task Bar
|
||||
if (percent >= 100) setTimeout(() => {
|
||||
resetProgress();
|
||||
window.electronAPI.setProgress(-1); // Remove the bar
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
speedEvt.onmessage = e => {
|
||||
speedElement.style.display = "block";
|
||||
speedElement.textContent = e.data; // ex: "5.2MiB/s"
|
||||
};
|
||||
50
public/script/settingsPanel.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const settingsPanel = document.getElementById("settings-panel");
|
||||
const settingsModal = document.querySelector(".settings-modal");
|
||||
|
||||
const settingsButton = document.getElementById("settings-button");
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
settingsButton.addEventListener("click", () => {
|
||||
const isOpen = settingsPanel.style.display === "block";
|
||||
settingsPanel.style.display = isOpen ? "none" : "block";
|
||||
});
|
||||
|
||||
function closePanel() {
|
||||
isOpen = false;
|
||||
settingsPanel.style.display = "none";
|
||||
}
|
||||
|
||||
|
||||
async function loadSettings() {
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => {
|
||||
const key = el.dataset.key;
|
||||
if (features[key] !== undefined) {
|
||||
if (el.type === "checkbox") el.checked = features[key];
|
||||
else if (el.tagName === "SELECT") el.value = features[key];
|
||||
}
|
||||
|
||||
el.addEventListener("change", () => {
|
||||
let value = el.type === "checkbox" ? el.checked : el.value;
|
||||
window.electronAPI.setFeature(key, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Open The JSON
|
||||
document.getElementById("open-json-btn").addEventListener("click", () => {
|
||||
window.topbarAPI.openConfig(); // ton IPC existant
|
||||
});
|
||||
|
||||
|
||||
/* clic sur l'overlay = fermer */
|
||||
settingsPanel.addEventListener("click", closePanel);
|
||||
|
||||
/* clic dans la modal = stop propagation */
|
||||
settingsModal.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
loadSettings();
|
||||
34
public/script/topbar.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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');
|
||||
const configBtn = document.getElementById('config-btn');
|
||||
|
||||
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
||||
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
||||
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
||||
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
||||
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
||||
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
||||
if (configBtn) configBtn.onclick = () => topbarAPI.openConfig();
|
||||
});
|
||||
}
|
||||
|
||||
setupTopbarListeners(); // IF it put it the if check. It don't work. Why ?
|
||||
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
if(!features.customTopBar){
|
||||
document.getElementById("topbar").style.display = "none";
|
||||
document.getElementById("container").style.marginTop = "0";
|
||||
document.getElementById("theme-switcher").style.top = "30px";
|
||||
}
|
||||
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,38 +0,0 @@
|
||||
.theme-switcher {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: #242424;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-family: sans-serif;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.theme-switcher label {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.theme-switcher select {
|
||||
background: #fff;
|
||||
border: 1px solid #242424;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-switcher select:hover,
|
||||
.theme-switcher select:focus {
|
||||
background: #474747;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
@@ -3,10 +3,23 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* height: 10000px; */
|
||||
padding: 1em;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
}
|
||||
@@ -62,6 +62,7 @@ form#downloadForm {
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
background-color: var(--form-input-bg-color);
|
||||
color: var(--form-input-text-color);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#UrlInput:focus {
|
||||
@@ -74,3 +75,39 @@ form#downloadForm {
|
||||
#UrlInput::placeholder {
|
||||
color: var(--form-input-placeholder-color);
|
||||
}
|
||||
|
||||
.url-input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.paste-btn {
|
||||
background-color: var(--form-button-bg-color);
|
||||
color: var(--form-button-text-color);
|
||||
border: none;
|
||||
padding: 0.75em;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.paste-btn:hover {
|
||||
background-color: var(--form-button-bg-hover-color);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.paste-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.paste-btn svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
234
public/styles/layout/settingsPanel.css
Normal file
@@ -0,0 +1,234 @@
|
||||
/* SETTINGS BUTTON */
|
||||
.settings-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
opacity: 0.8;
|
||||
background-color: var(--settings-button-bg-color);
|
||||
border-radius: 14px;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-button svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
stroke: var(--settings-button-text-color);
|
||||
}
|
||||
|
||||
.settings-button:hover {
|
||||
transform: scale(1.1);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.settings-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
|
||||
/* SETTINGS PANEL */
|
||||
.settings-panel {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 40px;
|
||||
width: 40vw;
|
||||
max-width: 320px;
|
||||
max-height: 80vh;
|
||||
background-color: var(--settings-bg-primary-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.2s ease-out;
|
||||
color: var(--settings-text-color);
|
||||
}
|
||||
|
||||
.settings-panel::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.settings-modal {
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
max-height: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: none;
|
||||
|
||||
}
|
||||
|
||||
/* TITLE */
|
||||
.settings-title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings-title h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.settings-title p {
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* SECTION */
|
||||
.settings-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px 15px;
|
||||
background: var(--settings-bg-secondary-color);
|
||||
border-radius: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* SETTING ITEM */
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.setting-item input[type="checkbox"] {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.setting-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.setting-info small {
|
||||
font-size: 12px;
|
||||
color: var(--settings-subtitle-color);
|
||||
margin-top: 2px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#theme .settings-title{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.setting-item select {
|
||||
width: 70%;
|
||||
background: #fff;
|
||||
border: 1px solid #242424;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.setting-item select:hover,
|
||||
.setting-item select:focus {
|
||||
background: #474747;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* SELECT */
|
||||
.settings-section select {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* DETAILS / DROPDOWN */
|
||||
details {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px 15px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
details[open] {
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* FOOTER */
|
||||
.settings-footer {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#open-json-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
background-color: var(--form-button-bg-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#experimental-args {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
border: 1px solid #333;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* CLOSE BTN */
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ANIMATION */
|
||||
@keyframes popIn {
|
||||
from { transform: scale(0.95); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
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 {
|
||||
opacity: 1;
|
||||
max-height: 1500px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#videoInfo h3 {
|
||||
@@ -68,7 +67,295 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* --- Playlist Mode --- */
|
||||
#videoInfo.playlist-mode {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
/* Playlist Header */
|
||||
.playlist-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
|
||||
border-radius: 16px;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.playlist-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
padding: 0.4rem 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--video-info-text-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.playlist-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.playlist-title {
|
||||
font-size: 1.9rem;
|
||||
margin: 0 0 0.6rem 0;
|
||||
color: var(--video-info-heading-color);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.playlist-meta {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
color: var(--video-info-text-color);
|
||||
opacity: 0.85;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.playlist-count,
|
||||
.playlist-channel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.playlist-count svg,
|
||||
.playlist-channel svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Layout liste des vidéos */
|
||||
#playlistVideos {
|
||||
margin-top: 1em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Carte vidéo playlist */
|
||||
.playlist-video-card {
|
||||
background: var(--playlist-background-color);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.playlist-video-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--video-info-link-color), transparent);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.playlist-video-card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 32px rgba(0,0,0,0.25);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.playlist-video-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Thumbnail wrapper avec durée */
|
||||
.video-thumbnail-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.video-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.playlist-video-card:hover .video-thumbnail {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.video-duration {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
|
||||
letter-spacing: 0.5px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Contenu vidéo */
|
||||
.video-content {
|
||||
padding: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
flex: 1;
|
||||
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.02));
|
||||
}
|
||||
|
||||
.video-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--video-info-heading-color);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-height: 2.8em;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.playlist-video-card:hover .video-title {
|
||||
color: var(--video-info-link-color);
|
||||
}
|
||||
|
||||
.video-uploader {
|
||||
font-size: 0.9rem;
|
||||
color: var(--video-info-text-color);
|
||||
opacity: 0.7;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Actions vidéo */
|
||||
.video-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: auto;
|
||||
padding-top: 0.8rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.video-link {
|
||||
flex: 1;
|
||||
text-decoration: none;
|
||||
color: var(--video-info-link-color);
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.video-link svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.video-link:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
text-decoration: none;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.copy-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--video-info-text-color);
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.copy-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.copy-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
#playlistVideos {
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.playlist-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.playlist-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.playlist-meta {
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#videoInfo {
|
||||
padding: 1em;
|
||||
@@ -78,4 +365,21 @@
|
||||
#videoInfo h3 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
#playlistVideos {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.playlist-header {
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
.playlist-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.playlist-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
/* Layout */
|
||||
@import url("layout/form.css");
|
||||
@import url("layout/settingsPanel.css");
|
||||
@import url("layout/container.css");
|
||||
@import url("layout/ctr-infos.css");
|
||||
@import url("layout/header.css");
|
||||
@import url("layout/videoinfo.css");
|
||||
|
||||
/* Components */
|
||||
@import url("components/checkbox.css");
|
||||
@import url("components/themebutton.css");
|
||||
/* @import url("variables.css"); */
|
||||
@import url("components/loader.css");
|
||||
|
||||
@import url("theme/themeimport.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');
|
||||
|
||||
@@ -24,7 +28,7 @@ html,body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
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-weight: 500;
|
||||
font-style: normal;
|
||||
@@ -33,10 +37,6 @@ html,body{
|
||||
body {
|
||||
margin: 0;
|
||||
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%;
|
||||
height: 100%;
|
||||
color: var(--default-text-color);
|
||||
@@ -47,7 +47,57 @@ body {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
/* Because why not */
|
||||
#reference{
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
bottom:0vh;
|
||||
left: 10px;
|
||||
font-size: 1em;
|
||||
color: #b0b0b02f;
|
||||
}
|
||||
|
||||
/* 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{
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
body.chirac {
|
||||
/* Background image Chirac */
|
||||
|
||||
background-image: url('../../assets/images/Chirac.png');
|
||||
/* background-position: right; */
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs drapeau français */
|
||||
--background-color: rgba(255, 255, 255, 0.85); /* blanc légèrement transparent pour lisibilité */
|
||||
--default-text-color: #002395; /* bleu foncé */
|
||||
--subtitle-color: #EF4135; /* rouge vif */
|
||||
--mp3-text-color: #002395; /* bleu */
|
||||
|
||||
--background-color: rgba(255, 255, 255, 0.85);
|
||||
--default-text-color: #002395;
|
||||
--subtitle-color: #EF4135;
|
||||
--mp3-text-color: #002395;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: rgba(255, 255, 255, 0.9);
|
||||
@@ -19,9 +18,9 @@ body.chirac {
|
||||
--form-input-border-focus-color: #EF4135;
|
||||
--form-input-text-color: #002395;
|
||||
--form-input-placeholder-color: #999999;
|
||||
--form-button-bg-color: #002395; /* bleu */
|
||||
--form-button-bg-color: #002395;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #EF4135; /* rouge */
|
||||
--form-button-bg-hover-color: #EF4135;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
@@ -34,21 +33,27 @@ body.chirac {
|
||||
|
||||
/* Video Info Box */
|
||||
--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-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #002395;
|
||||
--video-info-link-hover-color: #EF4135;
|
||||
|
||||
/* animation d'apparition */
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s ease, background-color 0.8s ease;
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #d1d1d1ca;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #00196a;
|
||||
--settings-button-text-color: #dcdcdc;
|
||||
|
||||
--settings-bg-primary-color: #0b0528;
|
||||
--settings-bg-secondary-color: #1c1055;
|
||||
|
||||
--settings-text-color: #cecece;
|
||||
--settings-subtitle-color: #adadad;
|
||||
}
|
||||
|
||||
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,17 @@ body.dark {
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #2196f3;
|
||||
--video-info-link-hover-color: #1976d2;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #303030;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #1e1e1e;
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #1e1e1e;
|
||||
--settings-bg-secondary-color: #2a2a2a;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #aaa;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
:root {
|
||||
/* Couleurs générales */
|
||||
--background-color: #001224; /* body background bleu nuit */
|
||||
--default-text-color: #eee; /* texte clair principal */
|
||||
--subtitle-color: #007bff; /* sous-titres, header p */
|
||||
--mp3-text-color: #000000; /* couleur texte checkbox inline */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff; /* fond du formulaire */
|
||||
--form-input-bg-color: #f9f9f9; /* fond inputs */
|
||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
||||
--form-input-border-focus-color: #0056b3; /* bordure focus */
|
||||
--form-input-text-color: #333333; /* texte inputs */
|
||||
--form-input-placeholder-color: #aaaaaa; /* placeholder */
|
||||
--form-button-bg-color: #007bff; /* bouton bg */
|
||||
--form-button-text-color: #ffffff; /* texte bouton */
|
||||
--form-button-bg-hover-color: #0056b3; /* bouton hover */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #3B82F6;
|
||||
--checkbox-checkmark-border-color: #E0E0E2;
|
||||
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #f0f0f0;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
}
|
||||
|
||||
body.default .download-path #savePath {
|
||||
color: #ffffff;
|
||||
}
|
||||
61
public/styles/theme/drift-theme.css
Normal file
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #1e222ace;
|
||||
--settings-button-text-color: #b3b3b4;
|
||||
|
||||
--settings-bg-primary-color: #1e222a;
|
||||
--settings-bg-secondary-color: #272d38;
|
||||
|
||||
--settings-text-color: #bababa;
|
||||
--settings-subtitle-color: #cacaca;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body.drift .download-path{
|
||||
color: #ededed;
|
||||
}
|
||||
@@ -1,36 +1,49 @@
|
||||
body.fanatic{
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212; /* fond bleu nuit/noir */
|
||||
--default-text-color: #eee; /* texte clair principal */
|
||||
--subtitle-color: #FF5900; /* sous-titres, accent orange vif */
|
||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
||||
--background-color: #121212;
|
||||
--default-text-color: #eee;
|
||||
--subtitle-color: #FF5900;
|
||||
--mp3-text-color: #ffffff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1e1e1e; /* fond formulaire sombre */
|
||||
--form-input-bg-color: #2b2b2b; /* fond inputs sombres */
|
||||
--form-input-border-color: #333333; /* bordure inputs */
|
||||
--form-input-border-focus-color: #FF5900; /* bordure focus orange */
|
||||
--form-input-text-color: #eeeeee; /* texte inputs clair */
|
||||
--form-input-placeholder-color: #666666; /* placeholder gris foncé */
|
||||
--form-button-bg-color: #FF5900; /* bouton orange */
|
||||
--form-button-text-color: #121212; /* texte bouton sombre */
|
||||
--form-button-bg-hover-color: #e65500; /* bouton hover orange foncé */
|
||||
--form-bg-color: #1e1e1e;
|
||||
--form-input-bg-color: #2b2b2b;
|
||||
--form-input-border-color: #333333;
|
||||
--form-input-border-focus-color: #FF5900;
|
||||
--form-input-text-color: #eeeeee;
|
||||
--form-input-placeholder-color: #666666;
|
||||
--form-button-bg-color: #FF5900;
|
||||
--form-button-text-color: #121212;
|
||||
--form-button-bg-hover-color: #e65500;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555; /* gris moyen */
|
||||
--checkbox-bg-checked: #ff6600; /* orange vif */
|
||||
--checkbox-checkmark-border-color: #eee; /* blanc cassé */
|
||||
--checkbox-pulse-color: rgba(255, 102, 0, 0.5); /* pulse orange translucide */
|
||||
--checkbox-bg-default: #555555;
|
||||
--checkbox-bg-checked: #ff6600;
|
||||
--checkbox-checkmark-border-color: #eee;
|
||||
--checkbox-pulse-color: rgba(255, 102, 0, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #FF5900; /* orange */
|
||||
--download-status-color: #FF5900;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #222222; /* fond infos sombre */
|
||||
--video-info-text-color: #eee; /* texte clair */
|
||||
--video-info-heading-color: #FF5900; /* titres orange */
|
||||
--video-info-list-color: #ccc; /* texte liste gris clair */
|
||||
--video-info-list-strong-color: #fff; /* texte important blanc */
|
||||
--video-info-link-color: #FF5900; /* liens orange */
|
||||
--video-info-link-hover-color: #e65500; /* liens hover orange foncé */
|
||||
--infos-box-color: #222222;
|
||||
--video-info-text-color: #eee;
|
||||
--video-info-heading-color: #FF5900;
|
||||
--video-info-list-color: #ccc;
|
||||
--video-info-list-strong-color: #fff;
|
||||
--video-info-link-color: #FF5900;
|
||||
--video-info-link-hover-color: #e65500;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #2b2b2b;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #1e1e1e;
|
||||
--settings-button-text-color: #dcdcdc;
|
||||
|
||||
--settings-bg-primary-color: #1e1e1e;
|
||||
--settings-bg-secondary-color: #2d2d2d;
|
||||
|
||||
--settings-text-color: #cecece;
|
||||
--settings-subtitle-color: #adadad;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
body.light {
|
||||
/* Couleurs générales */
|
||||
--background-color: #dadada; /* fond clair */
|
||||
--default-text-color: #222222; /* texte sombre */
|
||||
--subtitle-color: #007bff; /* bleu accent */
|
||||
--mp3-text-color: #000000; /* texte checkbox */
|
||||
--background-color: #dadada;
|
||||
--default-text-color: #222222;
|
||||
--subtitle-color: #007bff;
|
||||
--mp3-text-color: #000000;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff; /* fond formulaire blanc */
|
||||
--form-input-bg-color: #fafafa; /* fond inputs */
|
||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
||||
--form-input-border-focus-color: #007bff; /* focus bleu */
|
||||
--form-input-text-color: #333333; /* texte inputs */
|
||||
--form-input-placeholder-color: #888888; /* placeholder */
|
||||
--form-button-bg-color: #007bff; /* bouton bleu */
|
||||
--form-button-text-color: #ffffff; /* texte bouton */
|
||||
--form-button-bg-hover-color: #0056b3; /* hover bleu foncé */
|
||||
--form-bg-color: #ffffff;
|
||||
--form-input-bg-color: #fafafa;
|
||||
--form-input-border-color: #cccccc;
|
||||
--form-input-border-focus-color: #007bff;
|
||||
--form-input-text-color: #333333;
|
||||
--form-input-placeholder-color: #888888;
|
||||
--form-button-bg-color: #007bff;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #0056b3;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
@@ -33,4 +33,16 @@ body.light {
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #c5c5c5b2;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #e4e4e4;
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #cbcbcb;
|
||||
--settings-bg-secondary-color: #cbcbcb;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #656565;
|
||||
}
|
||||
|
||||
59
public/styles/theme/neon-theme.css
Normal file
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #202038;
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #242454;
|
||||
--settings-bg-secondary-color: #2f2f6f;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #d2d2d2;
|
||||
}
|
||||
|
||||
body.neon .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
@@ -1,46 +1,56 @@
|
||||
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 */
|
||||
--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-image: url('../../assets/images/NF.jpg');
|
||||
background-size: cover;
|
||||
background-position: 70% center;
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #2a2a2a;
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #2a2a2a;
|
||||
--settings-bg-secondary-color: #313131;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #7a7979;
|
||||
}
|
||||
|
||||
body.nf .download-path {
|
||||
|
||||
58
public/styles/theme/songbird-theme.css
Normal file
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: rgba(29, 0, 0, 0.784);
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: #121212;
|
||||
--settings-bg-secondary-color: #1a1a1a;
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #7a7979;
|
||||
}
|
||||
|
||||
body.cyberpunk .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
/*
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
body.spicy {
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212; /* fond noir profond */
|
||||
--default-text-color: #581010; /* texte clair */
|
||||
--subtitle-color: #3d0909; /* accent rouge piment */
|
||||
--default-text-color: #ffffff; /* texte clair */
|
||||
--subtitle-color: #f87918; /* accent rouge piment */
|
||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
||||
|
||||
/* Formulaire */
|
||||
@@ -39,12 +33,26 @@ body.spicy {
|
||||
--video-info-list-strong-color: #ffffff; /* texte important blanc */
|
||||
--video-info-link-color: #ff1a1a; /* liens rouges */
|
||||
--video-info-link-hover-color: #cc0000; /* liens hover rouge foncé */
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #353232;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: #1a1a1a;
|
||||
--settings-button-text-color: #dcdcdc;
|
||||
|
||||
--settings-bg-primary-color: #1a1a1a;
|
||||
--settings-bg-secondary-color: #292929;
|
||||
|
||||
--settings-text-color: #cecece;
|
||||
--settings-subtitle-color: #adadad;
|
||||
|
||||
}
|
||||
|
||||
/* Optionnel: background piments */
|
||||
body.spicy {
|
||||
background-image: url('../../assets/images/spicy.webp');
|
||||
background-size:contain;
|
||||
background-image: url('../../assets/images/Spicy.png');
|
||||
background-size:cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
8
public/styles/theme/themeimport.css
vendored
@@ -1,9 +1,9 @@
|
||||
@import url("chirac-theme.css");
|
||||
@import url("dark-theme.css");
|
||||
@import url("default-theme.css");
|
||||
@import url("fanatic-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("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 {
|
||||
/* Couleurs générales */
|
||||
--background-color: #001224; /* body background bleu nuit */
|
||||
--default-text-color: #eee; /* texte clair principal */
|
||||
--subtitle-color: #007bff; /* sous-titres, header p */
|
||||
--mp3-text-color: #000000; /* couleur texte checkbox inline */
|
||||
--background-color: #001224;
|
||||
--default-text-color: #eee;
|
||||
--subtitle-color: #007bff;
|
||||
--mp3-text-color: #000000;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff; /* fond du formulaire */
|
||||
--form-input-bg-color: #f9f9f9; /* fond inputs */
|
||||
--form-input-border-color: #cccccc; /* bordure inputs */
|
||||
--form-input-border-focus-color: #0056b3; /* bordure focus */
|
||||
--form-input-text-color: #333333; /* texte inputs */
|
||||
--form-input-placeholder-color: #aaaaaa; /* placeholder */
|
||||
--form-button-bg-color: #007bff; /* bouton bg */
|
||||
--form-button-text-color: #ffffff; /* texte bouton */
|
||||
--form-button-bg-hover-color: #0056b3; /* bouton hover */
|
||||
--form-bg-color: #ffffff;
|
||||
--form-input-bg-color: #f9f9f9;
|
||||
--form-input-border-color: #cccccc;
|
||||
--form-input-border-focus-color: #0056b3;
|
||||
--form-input-text-color: #333333;
|
||||
--form-input-placeholder-color: #aaaaaa;
|
||||
--form-button-bg-color: #007bff;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #0056b3;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
@@ -33,4 +33,14 @@
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: var(--form-bg-color);
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-bg-primary-color: var(--form-bg-color);
|
||||
--settings-bg-secondary-color: var(--form-bg-color);
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #aaa;
|
||||
|
||||
}
|
||||
|
||||
68
server/controller/download.controller.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const { fetchDownload } = require("../services/download.services");
|
||||
const { logger } = require("../logger");
|
||||
const { isValidUrl, isSafePath } = require("../helpers/validation");
|
||||
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)) {
|
||||
logger.warn(`Unsafe download path rejected: ${options.outputFolder}`);
|
||||
return res.status(400).send("❌ Save Path Not Allowed.");
|
||||
}
|
||||
|
||||
// Get output folder when the download is finished
|
||||
const outputFolder = await fetchDownload(options, listeners, speedListeners);
|
||||
notifyDownloadFinished(outputFolder);
|
||||
res.send("✅ Download Done !");
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`Server Error in /download : ${err.message}`);
|
||||
res.status(500).send(`❌ Server Error`);
|
||||
}
|
||||
}
|
||||
|
||||
function progressController(req, res) {
|
||||
res.set({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
});
|
||||
|
||||
const sendProgress = percent => res.write(`data: ${percent}\n\n`);
|
||||
listeners.push(sendProgress);
|
||||
|
||||
req.on('close', () => {
|
||||
listeners.splice(listeners.indexOf(sendProgress), 1);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
function speedController(req, res) {
|
||||
res.set({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
});
|
||||
|
||||
const sendSpeed = speed => res.write(`data: ${speed}\n\n`);
|
||||
speedListeners.push(sendSpeed);
|
||||
|
||||
req.on('close', () => {
|
||||
speedListeners.splice(speedListeners.indexOf(sendSpeed), 1);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = { downloadController, progressController, speedController};
|
||||
47
server/controller/info.controller.js
Normal file
@@ -0,0 +1,47 @@
|
||||
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; // POST et GET
|
||||
|
||||
|
||||
// If no url, non-string url or invalid url
|
||||
|
||||
if (!url || typeof url !== "string" || !isValidUrl(url)) return res.status(400).send("❌ Invalid URL Or Missing");
|
||||
|
||||
logger.info(`/Info Request receive by the Info Controller. URL: ${url}`);
|
||||
|
||||
if (url.includes("&list") || url.includes("?list")) {
|
||||
logger.info("Estimated Data Type: Playlist")
|
||||
|
||||
} else{
|
||||
logger.info("Estimated Data Type: Video")
|
||||
}
|
||||
|
||||
try {
|
||||
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) {
|
||||
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
|
||||
return res.status(500).send(`❌ Unable to fetch info`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {infoController};
|
||||
@@ -1,48 +1,47 @@
|
||||
/*
|
||||
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 config = require('../config');
|
||||
const RPC = require("discord-rpc");
|
||||
const clientId = "1410934537051181146";
|
||||
const { logger } = require("./logger");
|
||||
|
||||
const clientId = `${config.DiscordRPCID}`;
|
||||
const rpc = new RPC.Client({ transport: "ipc" });
|
||||
|
||||
let intervalId;
|
||||
|
||||
function startRPC() {
|
||||
rpc.on("ready", () => {
|
||||
console.log("✅ Connecté à Discord !");
|
||||
|
||||
const presence = {
|
||||
largeImageKey: "icon",
|
||||
smallImageKey: "acnolo_pfp",
|
||||
smallImageText: "By MasterAcnolo",
|
||||
startTimestamp: new Date(), // compteur de temps
|
||||
details: "github.com/MasterAcnolo/Freedom-Loader",
|
||||
startTimestamp: new Date(),
|
||||
details: `Open Source Download Tools - ${config.version}`,
|
||||
state: "masteracnolo.github.io/FreedomLoader",
|
||||
};
|
||||
|
||||
rpc.clearActivity()
|
||||
rpc.setActivity(presence);
|
||||
|
||||
// Mise à jour toutes les 15 secondes si tu veux garder le compteur à jour
|
||||
setInterval(() => {
|
||||
rpc.setActivity(presence);
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
rpc.login({ clientId }).catch(err => {
|
||||
console.error("Erreur Discord RPC :", err);
|
||||
logger.error("Unable to connect to the RPC:", err);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { startRPC };
|
||||
async function stopRPC(){
|
||||
try {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
|
||||
if (rpc && rpc.transport) {
|
||||
await rpc.clearActivity()
|
||||
await rpc.destroy();
|
||||
} else{
|
||||
logger.error("Not Able to close RPC")
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.error("Error while closing the RPC:", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = { startRPC, stopRPC};
|
||||
|
||||
85
server/helpers/buildArgs.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const path = require("path");
|
||||
|
||||
const getUserBrowser = require("./getBrowser");
|
||||
const { ffmpegPath, denoPath} = require("./path");
|
||||
const { configFeatures } = require("../../config.js");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
function validateCodec(codec){
|
||||
|
||||
const validCodec = [
|
||||
"av01","vp9.2", "vp9" , "h265" , "h264" , "vp8" , "h263", "theora"
|
||||
]
|
||||
|
||||
if(validCodec.includes(codec)){
|
||||
logger.info(`Codec Valid: ${codec}`)
|
||||
return codec
|
||||
} else{
|
||||
logger.error(`Codec not valid: ${codec}. Using default codec`)
|
||||
return "h264"
|
||||
}
|
||||
}
|
||||
|
||||
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
|
||||
logger.info("--- CONFIGURATION ---");
|
||||
|
||||
logger.info(`CONFIG autoUpdate: ${configFeatures.autoUpdate}`);
|
||||
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
||||
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
||||
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
||||
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
||||
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
||||
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
||||
logger.info(`CONFIG autoDownloadPlaylist: ${configFeatures.autoDownloadPlaylist}`);
|
||||
logger.info(`CONFIG customCodec: ${configFeatures.customCodec}`);
|
||||
|
||||
// ACTIVATE THEM WHEN THE FEATURE IS IMPLEMENTED
|
||||
// logger.info(`CONFIG logSystem: ${configFeatures.logSystem}`);
|
||||
// logger.info(`CONFIG outputTitleCheck: ${configFeatures.outputTitleCheck}`);
|
||||
// logger.info(`CONFIG downloadSystem: ${configFeatures.downloadSystem}`);
|
||||
// logger.info(`CONFIG notifySystem: ${configFeatures.notifySystem}`);
|
||||
|
||||
logger.info("---------------------");
|
||||
|
||||
const args = [
|
||||
configFeatures.verboseLogs ? "--verbose" : null, // Verbose Logs
|
||||
"--cookies-from-browser", `${getUserBrowser()}`,
|
||||
"--no-continue",
|
||||
"--no-overwrites",
|
||||
"--newline", // YT-DLP Logs Format
|
||||
configFeatures.addThumbnail ? "--embed-thumbnail" : null,
|
||||
configFeatures.addMetadata ? "--add-metadata" : null,
|
||||
"--concurrent-fragments", "8",
|
||||
"--retries", "10",
|
||||
"--fragment-retries", "10",
|
||||
"--ffmpeg-location", ffmpegPath,
|
||||
"--extractor-args","youtube:player_client=default",
|
||||
"--js-runtimes", `deno:${denoPath}`,
|
||||
"-S",
|
||||
`vcodec:${validateCodec(configFeatures.customCodec) || "h264"}`+
|
||||
`,acodec:aac`,
|
||||
configFeatures.autoDownloadPlaylist ? "--yes-playlist" : "--no-playlist"
|
||||
];
|
||||
|
||||
if (audioOnly) args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3");
|
||||
else args.push("--merge-output", "mp4");
|
||||
|
||||
const qualityMap = {
|
||||
best: "bestvideo+bestaudio/best/mp4",
|
||||
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
worst: "worstvideo+worstaudio/worst/mp4",
|
||||
1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]/mp4",
|
||||
720: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
480: "bestvideo[height<=480]+bestaudio/best[height<=480]/mp4",
|
||||
};
|
||||
|
||||
args.push("-f", qualityMap[quality] || "best");
|
||||
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
|
||||
args.push(url);
|
||||
|
||||
return args.filter(Boolean);
|
||||
}
|
||||
|
||||
module.exports = { buildYtDlpArgs };
|
||||
|
||||
39
server/helpers/getBrowser.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const notify = require("./notify")
|
||||
const { logger } = require("../logger");
|
||||
|
||||
function getUserBrowser() {
|
||||
const userProfile = os.homedir();
|
||||
|
||||
// List of browsers supported by yt-dlp (Currently I can only guarantee Firefox, Sorry)
|
||||
const browsers = [
|
||||
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
||||
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
||||
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
|
||||
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
|
||||
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
|
||||
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
|
||||
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
|
||||
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
|
||||
];
|
||||
|
||||
// Search for an available browser
|
||||
for (const browser of browsers) {
|
||||
if (fs.existsSync(browser.path)) {
|
||||
logger.info(`Browser found: ${browser.name}`);
|
||||
return browser.name;
|
||||
}
|
||||
}
|
||||
|
||||
// No browser found - notify user
|
||||
logger.warn("No supported browser found on the system");
|
||||
notify.notifyFirefoxBrowserMissing();
|
||||
|
||||
// Fallback: return "Firefox" to let YT-DLP manage error
|
||||
// this avoid app crash
|
||||
return "firefox";
|
||||
}
|
||||
|
||||
module.exports = getUserBrowser
|
||||
41
server/helpers/notify.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { Notification, shell } = require("electron");
|
||||
const { iconPaths } = require("./path");
|
||||
|
||||
function notifyDownloadFinished(folder) {
|
||||
const notif = new Notification({
|
||||
title: "Freedom Loader",
|
||||
body: "Your download is complete, click here to open it.",
|
||||
icon: iconPaths.confirm,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openPath(folder));
|
||||
notif.show();
|
||||
}
|
||||
|
||||
function notifyCookiesBrowserError(){
|
||||
const notif = new Notification({
|
||||
title: "Cookies Error",
|
||||
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
||||
icon: iconPaths.error,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
|
||||
notif.show();
|
||||
}
|
||||
|
||||
function notifyFirefoxBrowserMissing(){
|
||||
const notif = new Notification({
|
||||
title: "Firefox Missing",
|
||||
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
||||
icon: iconPaths.error,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))
|
||||
notif.show();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
notifyDownloadFinished,
|
||||
notifyCookiesBrowserError,
|
||||
notifyFirefoxBrowserMissing
|
||||
};
|
||||
27
server/helpers/parseInfo.js
Normal file
@@ -0,0 +1,27 @@
|
||||
function parseVideo(data) {
|
||||
|
||||
return {
|
||||
type: "video",
|
||||
...data
|
||||
};
|
||||
}
|
||||
|
||||
function parsePlaylist(data) {
|
||||
|
||||
return {
|
||||
type: "playlist",
|
||||
title: data.title || data.id,
|
||||
channel: data.uploader,
|
||||
count: data.entries.length,
|
||||
videos: (data.entries || []).map(v => ({
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
url: v.webpage_url,
|
||||
duration: v.duration,
|
||||
thumbnail: v.thumbnail,
|
||||
uploader: v.uploader
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { parseVideo, parsePlaylist };
|
||||
57
server/helpers/path.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const { app } = require("electron");
|
||||
const config = require("../../config");
|
||||
|
||||
const { logger } = require("../logger.js");
|
||||
|
||||
// Centralized resource paths
|
||||
const resourcesPath = config.localMode
|
||||
? path.join(__dirname, "../../ressources")
|
||||
: process.resourcesPath;
|
||||
|
||||
// Default download folder (centralized)
|
||||
const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||
|
||||
// Binary paths
|
||||
let userYtDlp;
|
||||
let ffmpegPath;
|
||||
let denoPath;
|
||||
|
||||
const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe");
|
||||
|
||||
if (config.localMode) {
|
||||
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
|
||||
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe
|
||||
denoPath = path.join(__dirname, "../../ressources/deno.exe");
|
||||
} else {
|
||||
userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe");
|
||||
ffmpegPath = path.join(resourcesPath, "binaries","ffmpeg.exe");
|
||||
denoPath = path.join(resourcesPath, "binaries","deno.exe");
|
||||
|
||||
if (!fs.existsSync(userYtDlp)) {
|
||||
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||
}
|
||||
}
|
||||
|
||||
// Notification icon paths
|
||||
const iconPaths = {
|
||||
confirm: path.join(resourcesPath, "confirm-icon.png"),
|
||||
error: path.join(resourcesPath, "error.png"),
|
||||
app: path.join(resourcesPath, "app-icon.ico")
|
||||
};
|
||||
|
||||
// Binary paths for verification
|
||||
const binaryPaths = {
|
||||
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
|
||||
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
|
||||
ffprobe: path.join(resourcesPath, "binaries", "ffprobe.exe"),
|
||||
deno: path.join(resourcesPath, "binaries", "deno.exe")
|
||||
};
|
||||
|
||||
if (!userYtDlp){ logger.error("Missing YT-DLP")}
|
||||
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
|
||||
if (!denoPath){ logger.error("Missing DENO")}
|
||||
|
||||
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder };
|
||||
14
server/helpers/rateLimit.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const RateLimit = require("express-rate-limit");
|
||||
|
||||
|
||||
/* Rate Limite */
|
||||
const rateLimite = RateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // limit each IP to 100 requests per windowMs on the root path
|
||||
message: "Too many requests, please try again later.",
|
||||
statusCode: 429, // HTTP status code for "Too Many Requests"
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
rateLimite
|
||||
}
|
||||
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", "\\Windows"];
|
||||
const resolved = path.resolve(folder);
|
||||
return !unsafe.some(u => resolved.includes(u));
|
||||
}
|
||||
|
||||
module.exports = { isValidUrl, isSafePath };
|
||||
109
server/logger.js
@@ -1,107 +1,54 @@
|
||||
/*
|
||||
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 DailyRotateFile = require("winston-daily-rotate-file");
|
||||
const fs = require("fs"); // Pour vérifier/créer le dossier de logs
|
||||
const path = require("path"); // Pour gérer proprement les chemins
|
||||
const os = require("os"); // Pour gérer les chemins aussi
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const config = require("../config");
|
||||
|
||||
// Définition du dossier où seront stockés les logs
|
||||
// const logDir = path.join(__dirname, "../logs"); Désactivé parce que cela requiert d'exec en admin mode. Donc pas pratique pour les gens
|
||||
// Logs folder in Windows
|
||||
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||
|
||||
let logDir;
|
||||
if (process.platform === "win32") {
|
||||
logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||
} else {
|
||||
logDir = path.join(os.homedir(), ".freedomloader", "logs");
|
||||
// Create "Logs" folder if needed
|
||||
try {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error(`Failed to create log directory: ${error.message}`);
|
||||
}
|
||||
|
||||
// Création du dossier logs s’il n’existe pas encore
|
||||
//if (!fs.existsSync(logDir)) fs.mkdirSync(logDir); OBSOLETE
|
||||
const logFormat = format.combine(
|
||||
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
|
||||
|
||||
|
||||
// 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
|
||||
// Logger configuration
|
||||
const logger = createLogger({
|
||||
level: "info",
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
||||
)
|
||||
),
|
||||
format: logFormat,
|
||||
transports: [
|
||||
// Rotation quotidienne des fichiers de logs
|
||||
new DailyRotateFile({
|
||||
dirname: logDir, // dossier cible pour les logs
|
||||
filename: "LOGS-%DATE%.log", // nom du fichier par date
|
||||
datePattern: "YYYY-MM-DD", // pattern de date dans le nom
|
||||
zippedArchive: false, // ne pas compresser les archives
|
||||
maxFiles: "7d", // conserve 14 jours d’historique
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level.toUpperCase()} | ${message}`
|
||||
)
|
||||
),
|
||||
options: { flags: "a" }, // mode ajout à la fin (append)
|
||||
dirname: logDir,
|
||||
filename: "LOGS-%DATE%.log",
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: false,
|
||||
maxFiles: "7d",
|
||||
format: logFormat,
|
||||
options: { flags: "a" },
|
||||
}),
|
||||
// Affichage des logs en console avec coloration et format simple
|
||||
new transports.Console({
|
||||
format: format.combine(
|
||||
format.colorize(),
|
||||
format.timestamp({ format: "HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) =>
|
||||
`${timestamp} | ${level} | ${message}`
|
||||
)
|
||||
),
|
||||
format: logFormat,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Helper pour marquer clairement le début de session dans les logs
|
||||
function logSessionStart() {
|
||||
logger.info(getSessionStartLine());
|
||||
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
|
||||
logger.info(`Application Version: ${config.version}`)
|
||||
}
|
||||
|
||||
// Helper pour marquer la fin de session dans les logs
|
||||
function logSessionEnd() {
|
||||
logger.info(getSessionEndLine());
|
||||
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
|
||||
}
|
||||
|
||||
// Export du logger principal et des helpers pour usage dans d’autres modules
|
||||
module.exports = {
|
||||
logger,
|
||||
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.09.23.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 for download progress
|
||||
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.09.23.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;
|
||||
129
server/server.js
@@ -1,106 +1,57 @@
|
||||
/*
|
||||
This file is part of Freedom Loader.
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const { logger, logSessionEnd } = require("./logger");
|
||||
const config = require("../config");
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp } = require("./helpers/path");
|
||||
const { rateLimite } = require("./helpers/rateLimit")
|
||||
|
||||
Copyright (C) 2025 MasterAcnolo
|
||||
const app = express();
|
||||
|
||||
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.
|
||||
app.use(express.json());
|
||||
|
||||
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.
|
||||
// Update YT-DLP on Startup
|
||||
execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
|
||||
if (err) logger.warn("yt-dlp update error:", err);
|
||||
else logger.info(`Update yt-dlp : ${stdout}`);
|
||||
});
|
||||
|
||||
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");
|
||||
// Import du logger Winston et helpers pour marquer début/fin de session
|
||||
|
||||
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
|
||||
// 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à
|
||||
if (!fs.existsSync(outputFolder)) {
|
||||
try {
|
||||
fs.mkdirSync(outputFolder, { recursive: true }); // création récursive au cas où
|
||||
logger.info("Dossier Freedom Loader cree dans Telechargements.");
|
||||
} catch (err) {
|
||||
logger.error("Impossible de creer le dossier :", err);
|
||||
process.exit(1); // Arrêt du programme si dossier non créé (critique)
|
||||
}
|
||||
} else {
|
||||
logger.info("Dossier Freedom Loader deja existant.");
|
||||
}
|
||||
|
||||
// On rend ce dossier accessible globalement via app.locals pour l’utiliser dans les routes
|
||||
app.locals.outputFolder = outputFolder;
|
||||
|
||||
// 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.static(path.join(__dirname, "../public")));
|
||||
|
||||
// Définition du dossier contenant les fichiers statiques (frontend)
|
||||
// Permet d’accéder au HTML, CSS, JS côté client
|
||||
const staticPath = path.join(__dirname, "../public");
|
||||
debug("Serveur statique sur", staticPath);
|
||||
app.use(express.static(staticPath));
|
||||
// Routes
|
||||
app.use("/download", require("./routes/download.route"));
|
||||
app.use("/info", require("./routes/info.route"));
|
||||
|
||||
// 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) => {
|
||||
debug("Requete GET / servie");
|
||||
// When we get API Root, it return the front pages.
|
||||
app.get("/", rateLimite ,(req, res) => {
|
||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||
});
|
||||
|
||||
// Fonction pour démarrer le serveur Express
|
||||
// Retourne une Promise pour pouvoir await le démarrage dans d’autres modules
|
||||
function startServer() {
|
||||
async function startServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info("Demarrage du serveur Express...");
|
||||
const serverInstance = app.listen(8080, () => {
|
||||
logger.info("Serveur Express pret sur http://localhost:8080");
|
||||
resolve(serverInstance); // Serveur prêt, on résout la promesse
|
||||
const server = app.listen(config.applicationPort, () => {
|
||||
logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
|
||||
resolve(server);
|
||||
});
|
||||
// Gestion des erreurs serveur lors du démarrage
|
||||
serverInstance.on("error", (err) => {
|
||||
logger.error("Erreur serveur Express :", err);
|
||||
reject(err); // Rejet de la promesse en cas d’erreur critique
|
||||
|
||||
server.on("error", (err) => {
|
||||
logger.error("Express Server Error :", err);
|
||||
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 };
|
||||
|
||||
const { startRPC } = require("./discordRPC");
|
||||
startRPC();
|
||||
|
||||
79
server/services/download.services.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp, defaultDownloadFolder } = require("../helpers/path");
|
||||
const fs = require("fs");
|
||||
const { logger } = require("../logger");
|
||||
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
||||
const notify = require("../helpers/notify");
|
||||
const path = require("path");
|
||||
const { isSafePath } = require("../helpers/validation");
|
||||
|
||||
function fetchDownload(options, listeners, speedListeners) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const outputFolder = options.outputFolder || defaultDownloadFolder;
|
||||
|
||||
// Normalize path and validate it's safe (within Users folder)
|
||||
let safeOutputFolder = path.resolve(outputFolder);
|
||||
|
||||
if (!isSafePath(safeOutputFolder)) {
|
||||
logger.warn(`Path not allowed, using default instead: ${safeOutputFolder}`);
|
||||
safeOutputFolder = path.resolve(defaultDownloadFolder);
|
||||
}
|
||||
|
||||
// Create download folder if it doesn't exist
|
||||
try {
|
||||
fs.mkdirSync(safeOutputFolder, { recursive: true });
|
||||
logger.info(`Output folder ready: ${safeOutputFolder}`);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to create output folder: ${err.message}`);
|
||||
return reject(new Error(`Unable to create download folder: ${err.message}`));
|
||||
}
|
||||
|
||||
const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder });
|
||||
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(safeOutputFolder);
|
||||
else reject(new Error(`YT-DLP failed with code : ${code}`));
|
||||
});
|
||||
|
||||
|
||||
child.stdout.on("data", data => {
|
||||
data.toString().split("\n").forEach(line => {
|
||||
if (!line.trim()) return;
|
||||
logger.info(`[yt-dlp] ${line}`);
|
||||
|
||||
// Progress Bar
|
||||
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
|
||||
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, if more timeout
|
||||
maxBuffer: 10 * 1024 * 1024 }, // 10MO max response (Default: 200ko)
|
||||
|
||||
(error, stdout, stderr) => {
|
||||
|
||||
if (stderr) {
|
||||
const lower = stderr.toLowerCase();
|
||||
if (lower.includes("sign in to confirm") || lower.includes("failed to decrypt") || lower.includes("could not copy")) {
|
||||
return reject(new Error(`Unable to extract cookies from ${getUserBrowser()}. Please log in to this browser and try again.`));
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
logger.error(`YT-DLP Error: ${error.message}`);
|
||||
if (stderr) logger.debug(`stderr: ${stderr}`);
|
||||
return reject(new Error("Unable to fetch Info."));
|
||||
}
|
||||
|
||||
if (!stdout) return reject(new Error("No Data Received."));
|
||||
|
||||
try {
|
||||
const data = JSON.parse(stdout);
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
logger.error(`JSON Parsing Error: ${e.message}`);
|
||||
reject(new Error("JSON unreadable."));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { fetchInfo };
|
||||
47
server/update.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
const { app, Notification, shell} = 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 };
|
||||