Compare commits
181 Commits
1.3.1
...
migration-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
519dcad6e6 | ||
|
|
ac94d68224 | ||
|
|
0f876fcb70 | ||
|
|
a6c11ba7c4 | ||
|
|
44132dd7b5 | ||
|
|
15ab6892b7 | ||
|
|
725b6f0ada | ||
|
|
ad0e4f858b | ||
|
|
68108e3a66 | ||
|
|
eff2affd42 | ||
|
|
87cf05643f | ||
|
|
ce14b8abd7 | ||
|
|
44ed1bbf0e | ||
|
|
e0d017d9fd | ||
|
|
c3ea1ac0bd | ||
|
|
238d4ea717 | ||
|
|
d879a5eb1d | ||
|
|
40bea35286 | ||
|
|
83ac76381a | ||
|
|
6ff0c71be7 | ||
|
|
fd0b339bcc | ||
|
|
bc5faf38b6 | ||
|
|
e744e55475 | ||
|
|
a581c9309d | ||
|
|
61040769ee | ||
|
|
06f6a30797 | ||
|
|
a2653eef57 | ||
|
|
89573d11f7 | ||
|
|
f0c28a7f5e | ||
|
|
f3c37c2498 | ||
|
|
8fe4e7c38a | ||
|
|
912f4042e5 | ||
|
|
5dc61b4a15 | ||
|
|
5fb49b0c9d | ||
|
|
75fdf91fcd | ||
|
|
011e0a703a | ||
|
|
5aebc78d44 | ||
|
|
60137a15dc | ||
|
|
d05b9e2511 | ||
|
|
ebb48f4f8b | ||
|
|
8be47b9821 | ||
|
|
ca8b107f61 | ||
|
|
f25d07cb83 | ||
|
|
49e1e1b507 | ||
|
|
23d11d8933 | ||
|
|
9f88fe19c3 | ||
|
|
9b4b0306c7 | ||
|
|
96401a6e1f | ||
|
|
0c7103c5c9 | ||
|
|
798084e3bf | ||
|
|
5c4e6669fe | ||
|
|
00c542925c | ||
|
|
24460cf0b5 | ||
|
|
a3c1b1d753 | ||
|
|
5ade3fe144 | ||
|
|
577069b471 | ||
|
|
a4da47647a | ||
|
|
c5b29a0647 | ||
|
|
90e1a5fde7 | ||
|
|
dcfea2073c | ||
|
|
0be1ef1710 | ||
|
|
da393b19d9 | ||
|
|
1c27cc19cf | ||
|
|
ff7366c46c | ||
|
|
459d4c6df4 | ||
|
|
d2ca3e99d2 | ||
|
|
8ad3ea55cc | ||
|
|
71dbd104f8 | ||
|
|
57a5defb08 | ||
|
|
25c78ff8b7 | ||
|
|
792a59d8d7 | ||
|
|
b13c98dda2 | ||
|
|
1f103c20b9 | ||
|
|
70d62959ca | ||
|
|
83ca963502 | ||
|
|
9fb9295093 | ||
|
|
74fcdbb738 | ||
|
|
32c9a66a35 | ||
|
|
45e0840fea | ||
|
|
19b3f8b947 | ||
|
|
cc5bde5b48 | ||
|
|
9a48ac05e3 | ||
|
|
5750444655 | ||
|
|
fdb22f2342 | ||
|
|
760550e521 | ||
|
|
334e83ae1f | ||
|
|
5d776594fc | ||
|
|
adb36e3fa2 | ||
|
|
87945fba80 | ||
|
|
2234f9ed81 | ||
|
|
7cd3b4be06 | ||
|
|
a42b020b96 | ||
|
|
c1e70dae80 | ||
|
|
6877c51c0e | ||
|
|
5a41090175 | ||
|
|
5983f21759 | ||
|
|
ee9e02410f | ||
|
|
909480083e | ||
|
|
ca0492ed04 | ||
|
|
6c73dd75be | ||
|
|
556dccec08 | ||
|
|
f720d7c889 | ||
|
|
bb23f37fb5 | ||
|
|
220e81d8b2 | ||
|
|
44d8772827 | ||
|
|
335fb9652b | ||
|
|
243b98cbf9 | ||
|
|
23da75a17b | ||
|
|
756d2c048e | ||
|
|
659c1d4cf9 | ||
|
|
443fd31170 | ||
|
|
6d8095ceb2 | ||
|
|
c18cb6c8aa | ||
|
|
a3c5025434 | ||
|
|
3d7986cb87 | ||
|
|
3f8701567f | ||
|
|
ccb16c49d5 | ||
|
|
86d9ac3592 | ||
|
|
43d87c2b47 | ||
|
|
19aa8ed8fa | ||
|
|
3ddfaa999e | ||
|
|
3d7f3b7481 | ||
|
|
cfe2e17313 | ||
|
|
dedf10c6b3 | ||
|
|
ac0b4a0750 | ||
|
|
29ca2ea496 | ||
|
|
2a3905bdfb | ||
|
|
d3755ab57d | ||
|
|
9824e107fe | ||
|
|
e437722937 | ||
|
|
fd8c4ae33c | ||
|
|
967144bd55 | ||
|
|
cd8eb05020 | ||
|
|
590060e504 | ||
|
|
26c7d4d2ee | ||
|
|
caa987fa42 | ||
|
|
ccb090e4b2 | ||
|
|
8d070ed10d | ||
|
|
343dbb26a2 | ||
|
|
fef4f6de36 | ||
|
|
1a61972750 | ||
|
|
45ae5d7f98 | ||
|
|
fecb123d0e | ||
|
|
5c62ed081a | ||
|
|
8f0f31041d | ||
|
|
0e48ab3b4c | ||
|
|
d4c33bcf2d | ||
|
|
2f105f5145 | ||
|
|
b7a193b062 | ||
|
|
65364d3095 | ||
|
|
c356e2e30d | ||
|
|
e45810b54c | ||
|
|
55db30febc | ||
|
|
163faec221 | ||
|
|
5398fed928 | ||
|
|
f959171db5 | ||
|
|
01b6bcd02c | ||
|
|
0ff4715b35 | ||
|
|
745a73a5b0 | ||
|
|
a69114edce | ||
|
|
489a4766fa | ||
|
|
adcee62386 | ||
|
|
b867d819b7 | ||
|
|
31d8b20a76 | ||
|
|
42ae0e7204 | ||
|
|
43869420c0 | ||
|
|
6c9d296fbd | ||
|
|
008428f10a | ||
|
|
ae7d338474 | ||
|
|
12ff25cc4b | ||
|
|
f31767e7e7 | ||
|
|
7c4f2841e1 | ||
|
|
a27b3555f6 | ||
|
|
c451ddd042 | ||
|
|
da6311055f | ||
|
|
945efb2059 | ||
|
|
ab2881f8e9 | ||
|
|
f23811fff4 | ||
|
|
19ab77501b | ||
|
|
37eeed583a | ||
|
|
caf08efb90 |
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/Freedom-Loader-Site/pages/faq)
|
||||
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/pages/wiki)
|
||||
- [ ] I have searched existing issues
|
||||
|
||||
## Question
|
||||
|
||||
What would you like to know?
|
||||
|
||||
## What I've Tried
|
||||
|
||||
Describe what you've already attempted to solve this yourself.
|
||||
|
||||
## Environment (if relevant)
|
||||
|
||||
- **Freedom Loader Version**: [e.g. 1.4.1]
|
||||
|
||||
## Additional Context
|
||||
|
||||
Any other information that might be helpful.
|
||||
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
|
||||
4
.gitignore
vendored
@@ -7,5 +7,9 @@
|
||||
/ressources/ffprobe.exe
|
||||
/ressources/deno.exe
|
||||
|
||||
/theme/**
|
||||
|
||||
config/config.dev.json
|
||||
|
||||
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/Freedom-Loader-Site/faq)
|
||||
- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki)
|
||||
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to **Zakaria** for the website icon design.
|
||||
Check out his work: [IG @designmark_studio](https://www.instagram.com/designmark_studio/)
|
||||
|
||||
---
|
||||
|
||||
**Thank you for contributing to Freedom Loader!**
|
||||
535
README.md
@@ -1,151 +1,502 @@
|
||||
<div align="center">
|
||||
<img
|
||||
|
||||
<a href="https://masteracnolo.github.io/Freedom-Loader-Site/"><img
|
||||
src="./build/banner.png"
|
||||
alt="Banner"
|
||||
style="width: 50%;;"/>
|
||||
style="width: 50%;"/></a>
|
||||
|
||||
### **A clean, open-source multimedia downloader for Windows**
|
||||
|
||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
||||
[](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
|
||||
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/MasterAcnolo/Freedom-Loader/releases"><img src="https://img.shields.io/badge/Release-1.3.1-blue?style=for-the-badge"></a>
|
||||
<a href="https://masteracnolo.github.io/FreedomLoader/index.html"><img src="https://img.shields.io/badge/Freedom%20Loader-Open%20Page-111111?style=for-the-badge&logo=terminal&logoColor=white"></a>
|
||||
<a href="https://www.firefox.com/fr/?utm_campaign=SET_DEFAULT_BROWSER"><img src="https://img.shields.io/badge/Require Firefox-E66000?style=for-the-badge&logo=Firefox-Browser&logoColor=white"></a>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
Freedom Loader is a **Windows desktop** application built with **Electron**.
|
||||
It provides a simple and reliable way to download video or audio content with metadata and advanced options, without ads or questionable third-party services.
|
||||
The goal is to offer a clean, user-friendly, and transparent download experience. It's important for me to make media content downloading accessible to people who love music or videos and don’t always have an internet connection.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services.
|
||||
|
||||
The primary goal is to make media downloading accessible to users who want offline access to their favorite content, particularly in situations where internet connectivity is unreliable or unavailable.
|
||||
|
||||
### See related repo:
|
||||
|
||||
- **[Website](https://masteracnolo.github.io/Freedom-Loader-Site/)** - Official project website and documentation
|
||||
- **[Workshop](https://github.com/MasterAcnolo/Freedom-Loader-Workshop)** - Theme creation tool and theme browser
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Preview](#preview)
|
||||
- [Configuration](#configuration)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Theme Workshop](#Theme-Workshop)
|
||||
- [Technology Stack](#technology-stack)
|
||||
- [Development](#development)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Contributing](#contributing)
|
||||
- [Submitting a community theme](#Submitting-a-community-theme)
|
||||
- [Support](#support)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
|
||||
- Video download with metadata (MP4)
|
||||
- Audio-only download with thumbnail and tags (MP3)
|
||||
- Full playlist support and better display in UI
|
||||
- Automatic metadata retrieval (title, duration, author, etc.)
|
||||
- Detailed logs (console + rotating files for 7 days)
|
||||
- Progress Bar for download and network speed indicator
|
||||
- Better UX when fetching info
|
||||
- Custom output path with persistence
|
||||
- Automatic dependency updates (YT-DLP)
|
||||
- Automatic Application Update
|
||||
- Multiple UI themes
|
||||
- Simple, responsive interface
|
||||
- Notifications on download completion (click to open folder)
|
||||
- Discord Rich Presence
|
||||
### Core Functionality
|
||||
|
||||
---
|
||||
- **Multi-format downloads** - MP4 video with full metadata or MP3 audio with embedded artwork
|
||||
- **Video information preview** - Fetch and display video metadata before downloading
|
||||
- **Quality selection** - Choose from best, medium, lowest quality or specific resolutions (1080p, 720p, 480p)
|
||||
- **Playlist support** - Download entire playlists with optional auto-download mode
|
||||
- **Metadata embedding** - Automatic retrieval and embedding of title, artist, duration, and thumbnail
|
||||
- **Video codec options** - Support for H.264, H.265, AV1, VP9, VP9.2, and Theora codecs
|
||||
- **Custom output directory** - User-configurable download location with path validation
|
||||
- **Cookie integration** - Firefox cookie support for accessing age-restricted and member-only content
|
||||
|
||||
### User Interface
|
||||
|
||||
- **Custom window controls** - Frameless window with custom top bar (optional)
|
||||
- **Custom themes** — ZIP-based theme system with live preview
|
||||
- **Theme Workshop** — web-based theme creator and browser
|
||||
- **Toast notifications** — in-app feedback system
|
||||
- **Real-time progress tracking** - Live download progress bar with network speed indicator
|
||||
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
||||
- **Settings panel** - In-app configuration interface with live updates
|
||||
- **Quick access toolbar** - Direct links to logs, website, wiki, and developer tools
|
||||
|
||||
### System Features
|
||||
|
||||
- **Automatic updates** - Electron-updater integration with GitHub releases
|
||||
- **YT-DLP auto-update** - Keeps the download engine up-to-date on startup
|
||||
- **Structured logging** - Winston-based logging with daily file rotation (7-day retention)
|
||||
- **Discord Rich Presence** - Optional integration showing current download activity
|
||||
- **Single instance enforcement** - Prevents multiple app instances from running simultaneously
|
||||
- **Native dependency validation** - Startup checks for yt-dlp, FFmpeg, FFprobe, and Deno binaries
|
||||
- **Rate limiting** - Express-based request throttling to prevent server overload
|
||||
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
||||
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
||||
- **Splash screen** — animated loading screen on startup
|
||||
- **Stop download** — ability to cancel in-progress downloads
|
||||
- **Playlist folders** — dedicated folder creation per playlist
|
||||
|
||||
## Installation
|
||||
|
||||
Download the latest installer from the **Releases** page and run it (Defender could stop you but click on "Run Anyway" and it's good).
|
||||
Actually, **Firefox Browser** is the only browser supported for get cookies. You should install it. Follow the tutorial in the wiki page if you need some help, Firefox Section. [Link](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
||||
### Requirements
|
||||
|
||||
---
|
||||
- **Operating System**: Windows 10 or later
|
||||
- **Browser**: Mozilla Firefox (required for cookie extraction)
|
||||
- [Download Firefox](https://www.firefox.com)
|
||||
|
||||
## FAQ
|
||||
### Standard Installation
|
||||
|
||||
### Where are my downloaded files stored?
|
||||
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
|
||||
|
||||
In your **Downloads** folder, inside **Freedom Loader**.
|
||||
Example path:
|
||||
`C:\Users\[YOUR NAME]\Downloads\Freedom Loader`
|
||||
### File Locations
|
||||
|
||||
### I get an error after launching the app
|
||||
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.*
|
||||
|
||||
Try restarting the application, checking the logs, or opening the developer console:
|
||||
`CTRL + SHIFT + I` -> “Toggle Developer Tools”.
|
||||
Application logs can be found at:
|
||||
```
|
||||
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="build/example-developertools.png" width="20%" />
|
||||
</p>
|
||||
## Usage
|
||||
|
||||
---
|
||||
### Basic Download
|
||||
|
||||
## Roadmap
|
||||
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
|
||||
|
||||
- [x] ~~Website for Freedom Loader~~
|
||||
- [ ] More format options
|
||||
- [ ] Linux version
|
||||
- [x] ~~Auto-updating YT-DLP~~
|
||||
- [x] ~~Automatic project updates~~
|
||||
- [x] ~~Better download status~~
|
||||
- [ ] Better website support (currently only YouTube is fully supported, other site could work but i can't prove it)
|
||||
- [ ] Subtitle support
|
||||
- [x] ~~UI/UX improvements~~
|
||||
- [ ] Language selection
|
||||
- [ ] Download specific parts of a video
|
||||
- [ ] Custom file naming
|
||||
- [ ] Parallel downloads
|
||||
- [ ] Automatic sponsor skipping
|
||||
- [x] ~~Extract/split video using native chapters~~
|
||||
- [x] ~~Custom output path selection~~
|
||||
### 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
|
||||
|
||||
<p align="center">
|
||||
<img src="build/apercu1.2.4.png" width="40%" />
|
||||
</p>
|
||||
<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
|
||||
|
||||
## Technologies
|
||||
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.
|
||||
|
||||
This project uses:
|
||||
### Configuration File Location
|
||||
```
|
||||
%APPDATA%\Freedom Loader\config.json
|
||||
```
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/yt-dlp/yt-dlp"><img src="https://img.shields.io/badge/yt--dlp-Visit%20Repo-cf0000?style=for-the-badge&logo=github&logoColor=white"></a>
|
||||
<a href="https://github.com/FFmpeg/FFmpeg"><img src="https://img.shields.io/badge/FFmpeg-Visit%20Repo-666666?style=for-the-badge&logo=ffmpeg&logoColor=white"></a>
|
||||
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-Visit%20Repo-339933?style=for-the-badge&logo=node.js&logoColor=white"></a>
|
||||
<a href="https://github.com/electron/electron"><img src="https://img.shields.io/badge/Electron-Visit%20Repo-47848F?style=for-the-badge&logo=electron&logoColor=white"></a>
|
||||
<a href="https://github.com/denoland/deno/"><img src="https://img.shields.io/badge/Deno-Visit%20Repo-14e0cc?style=for-the-badge&logo=deno&logoColor=white"></a>
|
||||
</p>
|
||||
### Available Options
|
||||
```json
|
||||
{
|
||||
"autoUpdate": true,
|
||||
"discordRPC": true,
|
||||
"customTopBar": true,
|
||||
"autoCheckInfo": true,
|
||||
"addThumbnail": true,
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"createPlaylistFolders": true,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
"outputTitleCheck": true,
|
||||
"downloadSystem": true,
|
||||
"notifySystem": true,
|
||||
"theme": "dark"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
||||
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
||||
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
||||
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
||||
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
|
||||
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||
| `logSystem` | boolean | `true` | Enable application logging |
|
||||
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
||||
| `downloadSystem` | boolean | `true` | Enable download system |
|
||||
| `notifySystem` | boolean | `true` | Enable system notifications |
|
||||
| `theme` | string | `dark` | Current Theme Name |
|
||||
|
||||
### Supported Codecs
|
||||
|
||||
- `h264` - H.264/AVC (widely compatible, recommended default)
|
||||
- `h265` - H.265/HEVC (better compression, less compatible)
|
||||
- `av1` - AV1 (modern codec, excellent compression)
|
||||
- `av01` - AV1 variant
|
||||
- `vp9` - VP9 (open format, good quality)
|
||||
- `vp9.2` - VP9 Profile 2 (HDR support)
|
||||
- `theora` - Theora (legacy open codec)
|
||||
|
||||
> [!NOTE]
|
||||
> Configuration changes may require an application restart to take effect.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Freedom-Loader/
|
||||
├── app/ # Electron main process modules
|
||||
│ ├── autoUpdater.js
|
||||
│ ├── dependencyCheck.js
|
||||
│ ├── discordRPC.js
|
||||
│ ├── ipcHandlers.js
|
||||
│ ├── pathValidator.js
|
||||
│ ├── splashManager.js
|
||||
│ ├── themeManager.js
|
||||
│ ├── windowManager.js
|
||||
│ └── ytDlpUpdater.js
|
||||
├── build/ # Build resources and assets
|
||||
├── config/ # Configuration files
|
||||
│ ├── config.default.json
|
||||
│ └── config.dev.json
|
||||
├── public/ # Frontend assets and UI
|
||||
│ ├── index.html
|
||||
│ ├── splash.html
|
||||
│ ├── assets/
|
||||
│ │ ├── icon/ # Application icons
|
||||
│ │ └── logo/ # Logo assets
|
||||
│ ├── script/ # Frontend JavaScript modules
|
||||
│ │ ├── appVersion.js
|
||||
│ │ ├── clipboardPaste.js
|
||||
│ │ ├── custompath.js
|
||||
│ │ ├── customthemes.js
|
||||
│ │ ├── downloadstatus.js
|
||||
│ │ ├── fetchinfo.js
|
||||
│ │ ├── progressBar.js
|
||||
│ │ ├── settingsPanel.js
|
||||
│ │ ├── toast.js
|
||||
│ │ └── topbar.js
|
||||
│ └── styles/ # CSS stylesheets
|
||||
│ ├── styles.css
|
||||
│ ├── variables.css
|
||||
│ ├── components/ # Component-specific styles
|
||||
│ │ ├── checkbox.css
|
||||
│ │ ├── editpathbutton.css
|
||||
│ │ ├── loader.css
|
||||
│ │ ├── progressBar.css
|
||||
│ │ └── toast.css
|
||||
│ └── layout/ # Layout styles
|
||||
│ ├── container.css
|
||||
│ ├── form.css
|
||||
│ ├── header.css
|
||||
│ ├── settingsPanel.css
|
||||
│ ├── topbar.css
|
||||
│ └── videoinfo.css
|
||||
├── ressources/ # Internal resources (binaries)
|
||||
├── server/ # Express backend server
|
||||
│ ├── logger.js
|
||||
│ ├── server.js
|
||||
│ ├── controller/ # Request handlers
|
||||
│ │ ├── download.controller.js
|
||||
│ │ └── info.controller.js
|
||||
│ ├── helpers/ # Utility functions
|
||||
│ │ ├── buildArgs.helpers.js
|
||||
│ │ ├── getBrowser.helpers.js
|
||||
│ │ ├── notify.helpers.js
|
||||
│ │ ├── parseInfo.helpers.js
|
||||
│ │ ├── path.helpers.js
|
||||
│ │ ├── rateLimit.helpers.js
|
||||
│ │ └── validation.helpers.js
|
||||
│ ├── routes/ # API route definitions
|
||||
│ │ ├── download.route.js
|
||||
│ │ └── info.route.js
|
||||
│ └── services/ # Business logic layer
|
||||
│ ├── download.services.js
|
||||
│ └── info.services.js
|
||||
├── theme/ # Theme system
|
||||
│ ├── template.theme.json # Theme template
|
||||
│ ├── Dark/ # Default dark theme
|
||||
│ │ └── dark.theme.json
|
||||
│ └── Light/ # Default light theme
|
||||
│ └── light.theme.json
|
||||
├── main.js # Electron main process entry point
|
||||
├── preload.js # Electron preload script (IPC bridge)
|
||||
├── config.js # Global configuration loader
|
||||
├── package.json # Project metadata and dependencies
|
||||
├── CODE_OF_CONDUCT.md # Community guidelines
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
├── LICENSE # GPLv3 license
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
Freedom Loader uses a client-server architecture within a single Electron application:
|
||||
|
||||
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
|
||||
- **Backend**: Express.js server running locally for download management
|
||||
- **App modules**: Electron-specific logic (window, IPC, updates, RPC) isolated in `app/`
|
||||
- **IPC Bridge**: Secure communication via Electron's preload script
|
||||
- **Logging**: Winston-based structured logging with file rotation
|
||||
- **Updates**: Automatic checking and installation via electron-updater
|
||||
|
||||
## Theme Workshop
|
||||
|
||||
Freedom Loader includes a web-based theme creator available at [Freedom Loader Workshop](https://masteracnolo.github.io/Freedom-Loader-Workshop/).
|
||||
|
||||
- Create custom themes with a live preview of the actual UI
|
||||
- Browse and download community themes
|
||||
- Export themes as `.zip` files ready to drop into the `theme/` folder
|
||||
- Supports background images, custom colors for every UI element
|
||||
|
||||
### Installing a custom theme
|
||||
1. Download a `.zip` theme file
|
||||
2. Drop it in the `theme/` folder of your Freedom Loader installation
|
||||
3. Restart the app — your theme appears in the settings panel
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Core Technologies
|
||||
|
||||
[](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
|
||||
|
||||
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome.
|
||||
Contributions are welcome and appreciated. This project benefits from community involvement.
|
||||
|
||||
## Submitting a community theme
|
||||
|
||||
Want your theme featured in the Workshop? Please **do not open an issue on this repository** for theme submissions.
|
||||
Head over to the [Freedom Loader Workshop repository](https://github.com/MasterAcnolo/Freedom-Loader-Workshop) and open an issue there with your theme files, preview screenshots, and description.
|
||||
|
||||
### Bug Reports
|
||||
|
||||
Use the GitHub Issues system and include reproduction steps and logs when possible.
|
||||
Logs can be found here:
|
||||
`C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-20xx-xx-xx.log`
|
||||
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 a clear description and use cases.
|
||||
Open a feature request issue with:
|
||||
|
||||
- Clear description of the proposed feature
|
||||
- Use cases and benefits
|
||||
- Potential implementation approach (if applicable)
|
||||
|
||||
### Pull Requests
|
||||
|
||||
Fork the repository, make your changes, and submit a PR once everything is tested and clean.
|
||||
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 the Project
|
||||
## Support
|
||||
|
||||
If you'd like to support development:
|
||||
If you find this project useful and want to support its development:
|
||||
|
||||
<a href="https://paypal.me/axelnicolas25">
|
||||
<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white" />
|
||||
</a>
|
||||
[](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 the software under the terms of this license.
|
||||
|
||||
See the full license in the [LICENSE](./LICENSE) file.
|
||||
You are free to use, modify, and redistribute this software under the terms of the GPL-3.0 license. See the [LICENSE](LICENSE) file for complete details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
||||
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
||||
- [Deno](https://deno.com/) for helping me bypassing challenges
|
||||
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
||||
- The open-source community for continuous support and contributions
|
||||
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
||||
- Zakaria for the website icon
|
||||
- Jacques Chirac to love Apples
|
||||
- All users who test, report issues, and help improve the application
|
||||
|
||||
---
|
||||
|
||||
## Note
|
||||
<div align="center">
|
||||
|
||||
AI assistance was used occasionally to speed up development, but all code and decisions were reviewed and validated manually.
|
||||
**Freedom Loader** - Put freedom in your downloads
|
||||
|
||||
[Website](https://masteracnolo.github.io/Freedom-Loader-Site/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/Freedom-Loader-Site/wiki) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
|
||||
|
||||
</div>
|
||||
|
||||
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!**
|
||||
83
app/autoUpdater.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
const { dialog } = require("electron");
|
||||
const { logger } = require("../server/logger");
|
||||
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
|
||||
function initAutoUpdater(mainWindow) {
|
||||
autoUpdater.on("update-available", async (info) => {
|
||||
logger.info(`Update available: ${info.version}`);
|
||||
|
||||
const { response } = await dialog.showMessageBox(mainWindow, {
|
||||
type: "info",
|
||||
title: "Update Available",
|
||||
message: `Version ${info.version} is available.`,
|
||||
detail: "Would you like to download and install it now?",
|
||||
buttons: ["Install Update", "Maybe Later"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
if (response === 0) {
|
||||
autoUpdater.downloadUpdate();
|
||||
} else {
|
||||
mainWindow?.webContents.executeJavaScript(
|
||||
`window.showUpdateBadge && window.showUpdateBadge("${info.version}")`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on("download-progress", (progress) => {
|
||||
logger.info(`Download progress: ${Math.round(progress.percent)}%`);
|
||||
mainWindow?.webContents.send("update-progress", {
|
||||
percent: Math.round(progress.percent),
|
||||
speed: progress.bytesPerSecond,
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", async (info) => {
|
||||
logger.info(`Update downloaded: ${info.version}`);
|
||||
|
||||
const { response } = await dialog.showMessageBox(mainWindow, {
|
||||
type: "info",
|
||||
title: "Update Ready",
|
||||
message: `Version ${info.version} has been downloaded.`,
|
||||
detail: "The application will restart to apply the update.",
|
||||
buttons: ["Install Now", "Later"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
if (response === 0) autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (err) => {
|
||||
logger.error("Auto update error:", err.message);
|
||||
dialog.showErrorBox("Update Error", err.message);
|
||||
});
|
||||
|
||||
checkForUpdates();
|
||||
}
|
||||
|
||||
async function checkForUpdates() {
|
||||
try {
|
||||
await autoUpdater.checkForUpdates();
|
||||
} catch (err) {
|
||||
logger.error("Update check failed:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadUpdate() {
|
||||
try {
|
||||
await autoUpdater.downloadUpdate();
|
||||
} catch (err) {
|
||||
logger.error("Download failed:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function installUpdate() {
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
|
||||
module.exports = { initAutoUpdater, downloadUpdate, installUpdate };
|
||||
32
app/dependencyCheck.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const fs = require("fs");
|
||||
const { app, dialog } = require("electron");
|
||||
const { logger } = require("../server/logger");
|
||||
|
||||
function checkNativeDependencies() {
|
||||
const { binaryPaths } = require("../server/helpers/path.helpers");
|
||||
|
||||
const deps = [
|
||||
{ name: "yt-dlp.exe", path: binaryPaths.ytDlp },
|
||||
{ name: "ffmpeg.exe", path: binaryPaths.ffmpeg },
|
||||
{ name: "ffprobe.exe", path: binaryPaths.ffprobe },
|
||||
{ name: "deno.exe", path: binaryPaths.deno },
|
||||
];
|
||||
|
||||
const missing = deps.filter(d => !fs.existsSync(d.path));
|
||||
if (missing.length === 0) return true;
|
||||
|
||||
const list = missing.map(d => d.name).join(", ");
|
||||
logger.error(`Missing dependencies: ${list}`);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
dialog.showErrorBox(
|
||||
"Missing dependencies",
|
||||
`The following files are missing in the 'ressources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.`
|
||||
);
|
||||
app.quit();
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = { checkNativeDependencies };
|
||||
@@ -1,6 +1,6 @@
|
||||
const config = require('../config');
|
||||
const RPC = require("discord-rpc");
|
||||
const { logger } = require("./logger");
|
||||
const { logger } = require("../server/logger");
|
||||
|
||||
const clientId = `${config.DiscordRPCID}`;
|
||||
const rpc = new RPC.Client({ transport: "ipc" });
|
||||
@@ -17,36 +17,31 @@ function startRPC() {
|
||||
details: `Open Source Download Tools - ${config.version}`,
|
||||
state: "masteracnolo.github.io/FreedomLoader",
|
||||
};
|
||||
|
||||
rpc.clearActivity()
|
||||
rpc.setActivity(presence);
|
||||
|
||||
// Met à jour la présence toutes les 15s
|
||||
intervalId = setInterval(() => {
|
||||
rpc.setActivity(presence);
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
async function cleanExit() {
|
||||
try {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
|
||||
if (rpc && rpc.transport) {
|
||||
await rpc.destroy();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error while closing the RPC:", err);
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
process.on("exit", cleanExit);
|
||||
process.on("SIGINT", cleanExit);
|
||||
process.on("SIGTERM", cleanExit);
|
||||
|
||||
rpc.login({ clientId }).catch(err => {
|
||||
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};
|
||||
127
app/ipcHandlers.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const { ipcMain, dialog, shell } = require("electron");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { logger, logDir } = require("../server/logger");
|
||||
const { configFeatures, featuresPath } = require("../config");
|
||||
const { getThemes, reloadThemes } = require("./themeManager");
|
||||
const config = require("../config");
|
||||
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
|
||||
const { userThemesPath } = require("../server/helpers/path.helpers");
|
||||
|
||||
const FEATURE_WHITELIST = new Set([
|
||||
"autoUpdate",
|
||||
"discordRPC",
|
||||
"customTopBar",
|
||||
"autoCheckInfo",
|
||||
"addThumbnail",
|
||||
"addMetadata",
|
||||
"verboseLogs",
|
||||
"autoDownloadPlaylist",
|
||||
"customCodec",
|
||||
"theme",
|
||||
"createPlaylistFolders",
|
||||
"notifySystem"
|
||||
]);
|
||||
|
||||
const configFolderPath = featuresPath;
|
||||
|
||||
const themeFolderPath = userThemesPath;
|
||||
|
||||
function registerIpcHandlers(getMainWindow) {
|
||||
|
||||
// Infos générales
|
||||
ipcMain.handle("version", () => config.version);
|
||||
ipcMain.handle("features", () => configFeatures);
|
||||
|
||||
// Sélection et validation de dossier
|
||||
ipcMain.handle("select-download-folder", async () => {
|
||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||
if (result.canceled) {
|
||||
logger.info("Folder selection cancelled by user");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const validated = validateDownloadPath(result.filePaths[0]);
|
||||
logger.info(`Folder selected and validated: ${validated}`);
|
||||
return validated;
|
||||
} catch (err) {
|
||||
logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("validate-download-path", (_, userPath) => validateDownloadPath(userPath));
|
||||
ipcMain.handle("get-default-download-path", () => getDefaultDownloadPath());
|
||||
|
||||
// Progression dans la taskbar
|
||||
ipcMain.on("set-progress", (_, percent) => {
|
||||
getMainWindow()?.setProgressBar(percent / 100);
|
||||
});
|
||||
|
||||
// TOPBAR ACTION
|
||||
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
|
||||
ipcMain.on("window-maximize", () => {
|
||||
const win = getMainWindow();
|
||||
if (!win) return;
|
||||
win.isMaximized() ? win.unmaximize() : win.maximize();
|
||||
});
|
||||
ipcMain.on("window-close", () => getMainWindow()?.close());
|
||||
|
||||
|
||||
ipcMain.on("open-devtools", () =>
|
||||
getMainWindow()?.webContents.openDevTools({ mode: "detach" })
|
||||
);
|
||||
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
|
||||
ipcMain.on("open-website", () =>
|
||||
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/")
|
||||
);
|
||||
ipcMain.on("open-wiki", () =>
|
||||
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki")
|
||||
);
|
||||
ipcMain.on("open-workshop", () =>
|
||||
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Workshop")
|
||||
);
|
||||
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
|
||||
|
||||
|
||||
|
||||
// THEME
|
||||
ipcMain.handle("get-themes", () => getThemes());
|
||||
|
||||
ipcMain.on("open-theme", () => shell.openPath(themeFolderPath));
|
||||
|
||||
ipcMain.handle("reload-themes", async () => {
|
||||
return await reloadThemes();
|
||||
});
|
||||
|
||||
// Modification des features
|
||||
ipcMain.handle("set-feature", (event, { key, value }) => {
|
||||
try {
|
||||
if (!FEATURE_WHITELIST.has(key)) {
|
||||
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (configFeatures[key] === value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
configFeatures[key] = value;
|
||||
|
||||
fs.writeFileSync(
|
||||
configFolderPath,
|
||||
JSON.stringify(configFeatures, null, 2),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
logger.info(`Feature updated: ${key} = ${value}`);
|
||||
return true;
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`set-feature failed (${key}): ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { registerIpcHandlers };
|
||||
32
app/pathValidator.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { logger } = require("../server/logger");
|
||||
|
||||
let _defaultPath = null;
|
||||
|
||||
function getDefaultDownloadPath() {
|
||||
if (!_defaultPath) {
|
||||
const { defaultDownloadFolder } = require("../server/helpers/path.helpers");
|
||||
_defaultPath = defaultDownloadFolder;
|
||||
}
|
||||
return _defaultPath;
|
||||
}
|
||||
|
||||
function validateDownloadPath(userPath) {
|
||||
const { isSafePath } = require("../server/helpers/validation.helpers");
|
||||
|
||||
if (!userPath) return getDefaultDownloadPath();
|
||||
|
||||
try {
|
||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||
if (!isSafePath(resolved)) {
|
||||
throw new Error("Path not allowed: system folders are blocked!");
|
||||
}
|
||||
return resolved;
|
||||
} catch (err) {
|
||||
logger.error(`Invalid download path: ${userPath} — ${err.message}`);
|
||||
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { validateDownloadPath, getDefaultDownloadPath };
|
||||
58
app/splashManager.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const { BrowserWindow, app } = require("electron");
|
||||
const path = require("path");
|
||||
|
||||
let splashWindow = null;
|
||||
|
||||
function createSplashWindow() {
|
||||
|
||||
const splashOptions = {
|
||||
width: 400,
|
||||
height: 300,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
alwaysOnTop: true,
|
||||
resizable: false,
|
||||
skipTaskbar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
};
|
||||
|
||||
splashWindow = new BrowserWindow(splashOptions);
|
||||
|
||||
const splashPath = path.join(__dirname, "../public/splash.html");
|
||||
splashWindow.loadFile(splashPath);
|
||||
|
||||
// Inject banner path for both dev and packaged app
|
||||
splashWindow.webContents.on('did-finish-load', () => {
|
||||
let bannerPath;
|
||||
|
||||
// Check if app is packaged
|
||||
if (app.isPackaged) {
|
||||
bannerPath = path.join(process.resourcesPath, 'banner.png');
|
||||
} else {
|
||||
// In dev, use build folder
|
||||
bannerPath = path.join(__dirname, '../build/banner.png');
|
||||
}
|
||||
|
||||
splashWindow.webContents.executeJavaScript(`
|
||||
document.querySelector('img[alt="Freedom Loader"]').src = 'file:///${bannerPath.replace(/\\/g, '/')}';
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
function closeSplashWindow() {
|
||||
if (splashWindow) {
|
||||
splashWindow.close();
|
||||
splashWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setSplashProgress(step) {
|
||||
if (splashWindow) {
|
||||
splashWindow.webContents.executeJavaScript(`window.setProgress(${step})`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createSplashWindow, closeSplashWindow, setSplashProgress };
|
||||
188
app/themeManager.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { logger } = require("../server/logger");
|
||||
const JSZip = require("jszip");
|
||||
|
||||
const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
|
||||
const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"];
|
||||
const ALLOWED_IMAGE_NAMES = ["cover", "background"];
|
||||
|
||||
const THEME_ORDER = ["dark", "light"];
|
||||
|
||||
const REQUIRED_KEYS = [
|
||||
["meta", "name"],
|
||||
["meta", "author"],
|
||||
["meta", "version"],
|
||||
["meta", "formatVersion"],
|
||||
["style", "colors", "background"],
|
||||
["style", "colors", "text", "default"],
|
||||
["style", "form", "button", "background"],
|
||||
["style", "progressBar", "fill"],
|
||||
];
|
||||
|
||||
function getNestedValue(obj, keys) {
|
||||
return keys.reduce((acc, key) => acc?.[key], obj);
|
||||
}
|
||||
|
||||
function validateThemeJson(json) {
|
||||
for (const keyPath of REQUIRED_KEYS) {
|
||||
if (getNestedValue(json, keyPath) === undefined) {
|
||||
return { valid: false, reason: `Missing key: ${keyPath.join(".")}` };
|
||||
}
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function buildThemeObject(themeId, themeJson, imageData) {
|
||||
return {
|
||||
id: themeId.toLowerCase(),
|
||||
name: themeJson.meta.name || themeId,
|
||||
author: themeJson.meta.author || "Unknown",
|
||||
version: themeJson.meta.version,
|
||||
subtitle: themeJson.meta.subtitle || "",
|
||||
style: themeJson.style,
|
||||
image: imageData,
|
||||
};
|
||||
}
|
||||
|
||||
function extractImage(buffer, filename) {
|
||||
if (buffer.length > MAX_IMAGE_SIZE) {
|
||||
logger.warn(`Image too large, ignoring`);
|
||||
return null;
|
||||
}
|
||||
const ext = path.extname(filename).toLowerCase().replace(".", "");
|
||||
const mime = ext === "jpg" || ext === "jpeg" ? "jpeg" : ext;
|
||||
return `data:image/${mime};base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
|
||||
async function loadThemeFromZip(zipPath, themeId) {
|
||||
try {
|
||||
const zipBuffer = fs.readFileSync(zipPath);
|
||||
const zip = await JSZip.loadAsync(zipBuffer);
|
||||
|
||||
const jsonFile = Object.keys(zip.files).find(f => f.endsWith(".theme.json"));
|
||||
if (!jsonFile) { logger.warn(`Theme ${themeId}: no .theme.json, skipping`); return null; }
|
||||
|
||||
let themeJson;
|
||||
try { themeJson = JSON.parse(await zip.files[jsonFile].async("string")); }
|
||||
catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
|
||||
|
||||
const validation = validateThemeJson(themeJson);
|
||||
if (!validation.valid) { logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`); return null; }
|
||||
|
||||
let imageData = null;
|
||||
const imageFile = Object.keys(zip.files).find(f => {
|
||||
const ext = path.extname(f).toLowerCase();
|
||||
const name = path.basename(f, ext).toLowerCase();
|
||||
return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
|
||||
});
|
||||
|
||||
if (imageFile) {
|
||||
const buf = await zip.files[imageFile].async("nodebuffer");
|
||||
imageData = extractImage(buf, imageFile);
|
||||
}
|
||||
|
||||
logger.info(`Theme loaded (zip): ${themeId}`);
|
||||
return buildThemeObject(themeId, themeJson, imageData);
|
||||
|
||||
} catch (err) {
|
||||
logger.warn(`Theme ${themeId}: failed to load zip — ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadThemeFromFolder(folderPath, themeId) {
|
||||
try {
|
||||
const jsonFile = fs.readdirSync(folderPath).find(f => f.endsWith(".theme.json"));
|
||||
if (!jsonFile) { logger.warn(`Theme ${themeId}: no .theme.json, skipping`); return null; }
|
||||
|
||||
let themeJson;
|
||||
try { themeJson = JSON.parse(fs.readFileSync(path.join(folderPath, jsonFile), "utf-8")); }
|
||||
catch { logger.warn(`Theme ${themeId}: invalid JSON, skipping`); return null; }
|
||||
|
||||
const validation = validateThemeJson(themeJson);
|
||||
if (!validation.valid) { logger.warn(`Theme ${themeId}: ${validation.reason}, skipping`); return null; }
|
||||
|
||||
let imageData = null;
|
||||
const imageFile = fs.readdirSync(folderPath).find(f => {
|
||||
const ext = path.extname(f).toLowerCase();
|
||||
const name = path.basename(f, ext).toLowerCase();
|
||||
return ALLOWED_IMAGE_NAMES.includes(name) && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
|
||||
});
|
||||
|
||||
if (imageFile) {
|
||||
const buf = fs.readFileSync(path.join(folderPath, imageFile));
|
||||
imageData = extractImage(buf, imageFile);
|
||||
}
|
||||
|
||||
logger.info(`Theme loaded (folder): ${themeId}`);
|
||||
return buildThemeObject(themeId, themeJson, imageData);
|
||||
|
||||
} catch (err) {
|
||||
logger.warn(`Theme ${themeId}: failed to load folder — ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let cachedThemes = null;
|
||||
let themeFolderPath = null;
|
||||
|
||||
function setThemeFolderPath(folderPath) {
|
||||
themeFolderPath = folderPath;
|
||||
}
|
||||
|
||||
async function loadThemesFromFolder() {
|
||||
const themes = [];
|
||||
|
||||
if (!fs.existsSync(themeFolderPath)) {
|
||||
logger.warn(`Theme folder not found: ${themeFolderPath}`);
|
||||
return themes;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(themeFolderPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(themeFolderPath, file);
|
||||
const themeId = path.basename(file, path.extname(file));
|
||||
|
||||
if (file.endsWith(".zip")) {
|
||||
const theme = await loadThemeFromZip(filePath, themeId);
|
||||
if (theme) themes.push(theme);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
const theme = await loadThemeFromFolder(filePath, themeId);
|
||||
if (theme) themes.push(theme);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return themes;
|
||||
}
|
||||
|
||||
function getThemeOrder(id) {
|
||||
const index = THEME_ORDER.indexOf(id);
|
||||
return index === -1 ? Infinity : index;
|
||||
}
|
||||
|
||||
async function initThemes(folderPath) {
|
||||
setThemeFolderPath(folderPath);
|
||||
const themes = await loadThemesFromFolder();
|
||||
logger.info(`Themes before sort: ${themes.map(t => t.id).join(", ")}`);
|
||||
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
|
||||
logger.info(`Themes after sort: ${cachedThemes.map(t => t.id).join(", ")}`);
|
||||
}
|
||||
|
||||
function getThemes() {
|
||||
return cachedThemes ?? [];
|
||||
}
|
||||
|
||||
async function reloadThemes() {
|
||||
const themes = await loadThemesFromFolder();
|
||||
cachedThemes = themes.sort((a, b) => getThemeOrder(a.id) - getThemeOrder(b.id));
|
||||
logger.info(`Themes reloaded: ${cachedThemes.length} theme(s)`);
|
||||
return cachedThemes;
|
||||
}
|
||||
|
||||
module.exports = { initThemes, getThemes , reloadThemes };
|
||||
58
app/windowManager.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const { BrowserWindow, app } = require("electron");
|
||||
const path = require("path");
|
||||
const { logger } = require("../server/logger");
|
||||
const { configFeatures } = require("../config");
|
||||
const config = require("../config");
|
||||
|
||||
let mainWindow = null;
|
||||
|
||||
async function createMainWindow() {
|
||||
if (mainWindow) {
|
||||
logger.warn("Window already exists, no new creation!");
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
const iconPath = config.devMode
|
||||
? path.join(__dirname, "../build/app-icon.ico")
|
||||
: path.join(process.resourcesPath, "build/app-icon.ico");
|
||||
|
||||
const windowOptions = {
|
||||
title: `Freedom Loader ${config.version}`,
|
||||
icon: iconPath,
|
||||
width: 750,
|
||||
height: 800,
|
||||
minWidth: 750,
|
||||
minHeight: 800,
|
||||
frame: !configFeatures.customTopBar,
|
||||
devTools: !app.isPackaged,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, "../preload.js"),
|
||||
},
|
||||
};
|
||||
|
||||
mainWindow = new BrowserWindow(windowOptions);
|
||||
|
||||
try {
|
||||
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||
logger.info("Window loaded");
|
||||
} catch (err) {
|
||||
logger.error("Window loading error:", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
logger.info("Main window closed");
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
function getMainWindow() {
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
module.exports = { createMainWindow, getMainWindow };
|
||||
12
app/ytDlpUpdater.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const { execFile } = require("child_process");
|
||||
const { logger } = require("../server/logger");
|
||||
|
||||
function updateYtDlp(ytDlpPath) {
|
||||
logger.info("yt-dlp update check starting...");
|
||||
execFile(ytDlpPath, ["-U"], (err, stdout) => {
|
||||
if (err) logger.warn("yt-dlp update failed (continuing):", err.message);
|
||||
else logger.info(`yt-dlp update: ${stdout.trim()}`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { updateYtDlp };
|
||||
|
Before Width: | Height: | Size: 134 KiB |
BIN
build/banner.bmp
Normal file
|
After Width: | Height: | Size: 151 KiB |
39
config.js
@@ -1,10 +1,45 @@
|
||||
const packageJson = require("./package.json");
|
||||
const { app } = require("electron");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
|
||||
const devMode = !app.isPackaged;
|
||||
|
||||
|
||||
function resolveConfigPath() {
|
||||
if (devMode) {
|
||||
const devConfigPath = path.join(__dirname, "config", "config.dev.json");
|
||||
if (!fs.existsSync(devConfigPath)) {
|
||||
const defaultConfigPath = path.join(__dirname, "config", "config.default.json");
|
||||
fs.copyFileSync(defaultConfigPath, devConfigPath);
|
||||
}
|
||||
return devConfigPath;
|
||||
}
|
||||
|
||||
const userConfigPath = path.join(app.getPath("userData"), "config.json");
|
||||
if (!fs.existsSync(userConfigPath)) {
|
||||
const defaultConfigPath = path.join(process.resourcesPath, "config", "config.default.json");
|
||||
fs.copyFileSync(defaultConfigPath, userConfigPath);
|
||||
}
|
||||
|
||||
return userConfigPath;
|
||||
}
|
||||
|
||||
const featuresPath = resolveConfigPath();
|
||||
|
||||
function loadFeatures() {
|
||||
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
const configFeatures = loadFeatures();
|
||||
|
||||
module.exports = {
|
||||
version: packageJson.version,
|
||||
applicationPort: "8787",
|
||||
debugMode: true,
|
||||
localMode: !app.isPackaged,
|
||||
devMode,
|
||||
DiscordRPCID: "1410934537051181146",
|
||||
configFeatures,
|
||||
featuresPath
|
||||
}
|
||||
17
config/config.default.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"autoUpdate": true,
|
||||
"discordRPC": true,
|
||||
"customTopBar": true,
|
||||
"autoCheckInfo": true,
|
||||
"addThumbnail": true,
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"createPlaylistFolders": true,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
"outputTitleCheck": true,
|
||||
"downloadSystem": true,
|
||||
"notifySystem": true,
|
||||
"theme": "dark"
|
||||
}
|
||||
220
main.js
@@ -1,182 +1,90 @@
|
||||
const config = require("./config.js");
|
||||
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
||||
process.on("uncaughtException", (err) => {
|
||||
console.error("Uncaught exception:", err);
|
||||
app.quit();
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.error("Unhandled rejection:", reason);
|
||||
app.quit();
|
||||
});
|
||||
|
||||
const { app } = require("electron");
|
||||
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
|
||||
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");
|
||||
|
||||
let mainWindow;
|
||||
const logsFolderPath = logDir;
|
||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
||||
const { initAutoUpdater } = require("./app/autoUpdater");
|
||||
const { startRPC, stopRPC } = require("./app/discordRPC");
|
||||
|
||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
||||
const { configFeatures } = require("./config");
|
||||
const config = require("./config");
|
||||
const { initThemes } = require("./app/themeManager");
|
||||
|
||||
const { checkNativeDependencies } = require("./app/dependencyCheck");
|
||||
const { updateYtDlp } = require("./app/ytDlpUpdater");
|
||||
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
||||
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
||||
const { userThemesPath, initUserThemes } = require("./server/helpers/path.helpers");
|
||||
|
||||
app.setName("Freedom Loader");
|
||||
app.setAppUserModelId("com.masteracnolo.freedomloader");
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
|
||||
// Gestion single instance
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
|
||||
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
||||
function checkNativeDependencies() {
|
||||
const deps = [
|
||||
{ name: "yt-dlp.exe", path: path.join(process.resourcesPath, "yt-dlp.exe") },
|
||||
{ name: "ffmpeg.exe", path: path.join(process.resourcesPath, "ffmpeg.exe") },
|
||||
{ name: "ffprobe.exe", path: path.join(process.resourcesPath, "ffprobe.exe") },
|
||||
{ name: "deno.exe", path: path.join(process.resourcesPath, "deno.exe") },
|
||||
];
|
||||
const missing = deps.filter(dep => !fs.existsSync(dep.path));
|
||||
let errorMsg = "";
|
||||
if (missing.length > 0) {
|
||||
const missingList = missing.map(dep => dep.name).join(", ");
|
||||
logger.error(`Missing dependencies: ${missingList}`);
|
||||
errorMsg += `The following files are missing in the 'ressources' folder:\n${missingList}`;
|
||||
}
|
||||
if (errorMsg) {
|
||||
app.whenReady().then(() => {
|
||||
dialog.showErrorBox(
|
||||
"Missing dependencies",
|
||||
`${errorMsg}\n\nThe application will now exit. Try to reinstall`
|
||||
);
|
||||
app.quit();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!config.localMode){
|
||||
if (!gotLock) {
|
||||
// Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
|
||||
// Ici la nouvelle instance continue normalement
|
||||
} else {
|
||||
if (!checkNativeDependencies()) {
|
||||
// Arrêt déjà géré dans la fonction
|
||||
} else {
|
||||
if (!config.devMode) {
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (gotLock) {
|
||||
app.on("second-instance", () => {
|
||||
// La vieille instance se ferme
|
||||
if (mainWindow) {
|
||||
logger.info("New Instance Detected, closing the older...");
|
||||
mainWindow.destroy();
|
||||
mainWindow = null;
|
||||
}
|
||||
logger.info("New instance detected, closing older...");
|
||||
getMainWindow()?.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 ${config.version}`,
|
||||
width: 750,
|
||||
height: 800,
|
||||
minWidth: 750,
|
||||
minHeight: 800,
|
||||
frame:false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||
logger.info("Window Loaded");
|
||||
} catch (err) {
|
||||
logger.error("Window Loading Error:", err);
|
||||
}
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
logger.info("Main Window Closed");
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
// IPC
|
||||
ipcMain.handle("select-download-folder", async () => {
|
||||
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];
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
logger.error(`Error when creating Output Folder : ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
|
||||
|
||||
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();
|
||||
});
|
||||
ipcMain.on("window-maximize", () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
}
|
||||
});
|
||||
ipcMain.on("window-close", () => {
|
||||
if (mainWindow) mainWindow.close();
|
||||
});
|
||||
|
||||
// Topbar custom actions
|
||||
ipcMain.on("open-devtools", () => {
|
||||
if (mainWindow) mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
});
|
||||
ipcMain.on("open-logs", () => {
|
||||
if (logsFolderPath) shell.openPath(logsFolderPath);
|
||||
});
|
||||
ipcMain.on("open-website", () => {
|
||||
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/index.html");
|
||||
});
|
||||
ipcMain.on("open-wiki", () => {
|
||||
shell.openExternal("https://masteracnolo.github.io/FreedomLoader/pages/wiki.html");
|
||||
});
|
||||
|
||||
|
||||
// App ready
|
||||
app.whenReady().then(async () => {
|
||||
logSessionStart();
|
||||
logger.info("App Ready, Server Express starting...");
|
||||
|
||||
const serverPath = path.join(__dirname, "server", "server.js");
|
||||
const expressServer = require(serverPath);
|
||||
createSplashWindow();
|
||||
|
||||
if (!config.devMode && !checkNativeDependencies()) return;
|
||||
|
||||
const { userYtDlp } = require("./server/helpers/path.helpers");
|
||||
updateYtDlp(userYtDlp);
|
||||
|
||||
try {
|
||||
await expressServer.startServer();
|
||||
logger.info("Express Server Started");
|
||||
setSplashProgress(0); // Checking dependencies
|
||||
await require("./server/server").startServer();
|
||||
|
||||
const { startRPC } = require("./server/discordRPC");
|
||||
startRPC();
|
||||
setSplashProgress(1); // Starting server
|
||||
registerIpcHandlers(getMainWindow);
|
||||
|
||||
setSplashProgress(2); // Loading themes
|
||||
initUserThemes();
|
||||
await initThemes(userThemesPath);
|
||||
|
||||
setSplashProgress(3); // Almost ready
|
||||
await createMainWindow();
|
||||
AutoUpdater(mainWindow);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
closeSplashWindow();
|
||||
getMainWindow().show();
|
||||
if (configFeatures.discordRPC) startRPC();
|
||||
|
||||
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
||||
|
||||
} catch (err) {
|
||||
logger.error("Window or Server error :", err);
|
||||
logger.error("Boot error:", err);
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
logger.info("All Window Closed, shuting down app");
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
logger.info("Shutting down...");
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on("before-quit", () => logSessionEnd());
|
||||
app.on("before-quit", async () => {
|
||||
await stopRPC();
|
||||
logger.info("All services stopped. Have a nice day!");
|
||||
logSessionEnd();
|
||||
});
|
||||
1848
package-lock.json
generated
41
package.json
@@ -1,29 +1,30 @@
|
||||
{
|
||||
"name": "freedom-loader",
|
||||
"productName": "Freedom Loader",
|
||||
"version": "1.3.1",
|
||||
"version": "1.6.0",
|
||||
"author": "MasterAcnolo",
|
||||
"description": "Freedom Loader",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron --trace-warnings .",
|
||||
"dist": "electron-builder",
|
||||
"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",
|
||||
"jszip": "^3.10.1",
|
||||
"webidl-conversions": "^8.0.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^37.1.0",
|
||||
"electron-builder": "^26.0.12"
|
||||
"electron": "^41.2.0",
|
||||
"electron-builder": "^26.8.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.masteracnolo.freedomloader",
|
||||
@@ -38,15 +39,18 @@
|
||||
"files": [
|
||||
"**/*",
|
||||
"node_modules/**/*",
|
||||
"!logs/*.json",
|
||||
"server/**/*",
|
||||
"server/node_modules/**/*"
|
||||
"!config/config.dev.json",
|
||||
"!theme/**/*"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "build"
|
||||
},
|
||||
"asar": true,
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "theme",
|
||||
"to": "theme"
|
||||
},
|
||||
{
|
||||
"from": "build/confirm-icon.png",
|
||||
"to": "confirm-icon.png"
|
||||
@@ -55,29 +59,37 @@
|
||||
"from": "build/app-icon.ico",
|
||||
"to": "app-icon.ico"
|
||||
},
|
||||
{
|
||||
"from": "build/banner.bmp",
|
||||
"to": "banner.bmp"
|
||||
},
|
||||
{
|
||||
"from": "build/banner.png",
|
||||
"to": "banner.png"
|
||||
},
|
||||
{
|
||||
"from": "build/error.png",
|
||||
"to": "error.png"
|
||||
},
|
||||
{
|
||||
"from": "ressources/yt-dlp.exe",
|
||||
"to": "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": "deno.exe"
|
||||
"to": "binaries/deno.exe"
|
||||
},
|
||||
{
|
||||
"from": "server",
|
||||
"to": "server"
|
||||
"from": "config/config.default.json",
|
||||
"to": "config/config.default.json"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
@@ -90,6 +102,7 @@
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"installerIcon": "build/app-icon.ico",
|
||||
"uninstallerIcon": "build/uninstaller-icon.ico",
|
||||
"installerSidebar": "build/banner.bmp",
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "Freedom Loader"
|
||||
|
||||
13
preload.js
@@ -3,7 +3,13 @@ const { contextBridge, ipcRenderer } = require("electron");
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
||||
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
||||
setProgress: (percent) => ipcRenderer.send("set-progress", percent)
|
||||
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
|
||||
getFeatures: () => ipcRenderer.invoke("features"),
|
||||
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
|
||||
getVersion: () => ipcRenderer.invoke("version"),
|
||||
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path),
|
||||
getThemes: () => ipcRenderer.invoke("get-themes"),
|
||||
reloadThemes: () => ipcRenderer.invoke("reload-themes")
|
||||
});
|
||||
|
||||
// Contrôles de fenêtre et outils custom pour la topbar
|
||||
@@ -14,5 +20,8 @@ contextBridge.exposeInMainWorld("topbarAPI", {
|
||||
openDevTools: () => ipcRenderer.send("open-devtools"),
|
||||
openLogs: () => ipcRenderer.send("open-logs"),
|
||||
openWebsite: () => ipcRenderer.send("open-website"),
|
||||
openWiki: () => ipcRenderer.send("open-wiki")
|
||||
openTheme: () => ipcRenderer.send("open-theme"),
|
||||
openWiki: () => ipcRenderer.send("open-wiki"),
|
||||
openWorkshop: () => ipcRenderer.send("open-workshop"),
|
||||
openConfig: () => ipcRenderer.send("open-config")
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 967 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 5.3 MiB |
@@ -7,9 +7,8 @@
|
||||
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>
|
||||
|
||||
<link rel="stylesheet" href="styles/styles.css">
|
||||
<link rel="stylesheet" href="styles/layout/topbar.css">
|
||||
@@ -17,12 +16,15 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="topbar">
|
||||
<header>
|
||||
<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>
|
||||
<button id="workshop-btn" title="Workshop">Theme Workshop</button>
|
||||
<!-- <button id="theme-btn" title="Theme">Theme</button> -->
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
<button id="minimize-btn" title="Réduire"><img src="assets/icon/minimize.svg"></button>
|
||||
@@ -30,21 +32,204 @@
|
||||
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-switcher">
|
||||
<label for="themeSelect">Thème :</label>
|
||||
<select id="themeSelect" aria-label="Choisir le thème"></select>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<header>
|
||||
<h1 id="title"> Freedom Loader </h1>
|
||||
<p id="subtitle"> Because why not? </p>
|
||||
</header>
|
||||
|
||||
<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; align-items: center; gap: 10px;">
|
||||
<span class="setting-title">Thème</span>
|
||||
|
||||
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
||||
|
||||
</select>
|
||||
<button id="refresh-themes-btn" title="Refresh themes" class="refresh-themes-btn">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<small>Choose the theme of the application</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="discordRPC" id="discordRPC">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Discord RPC</span>
|
||||
<small>Enable/disable Discord Rich Presence for your app.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoDownloadPlaylist" id="autoDownloadPlaylist">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Download Playlist automatically?</span>
|
||||
<small>If enabled, playlists will download without confirmation.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="createPlaylistFolders" id="createPlaylistFolders">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Create Playlist Folders</span>
|
||||
<small>Create a dedicated folder for each playlist with its name.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="notifySystem" id="notifySystem">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Notification at the end</span>
|
||||
<small>Notify when a download finishes.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Auto Fetch Info</span>
|
||||
<small>Automatically fetch information for new downloads.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Advanced Settings -->
|
||||
<details class="settings-section">
|
||||
<summary>Advanced Settings</summary>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoUpdate" id="autoUpdate">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Auto Update</span>
|
||||
<small>Enable automatic app updates.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Output title (special chars)</span>
|
||||
<small>Check for unusual characters in output titles.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Add Metadata</span>
|
||||
<small>Include metadata in downloaded files.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="addThumbnail" id="addThumbnail">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Add Thumbnail</span>
|
||||
<small>Include thumbnails in downloaded files.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="customTopBar" id="customTopBar">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Custom Top bar</span>
|
||||
<small>Use this if the topbar is missing or you prefer a custom one.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item" style="margin-left: 32px; margin-top: 20px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Custom Codec</span>
|
||||
<select data-key="customCodec">
|
||||
<option value="h264">H264</option>
|
||||
<option value="av1">AV1</option>
|
||||
<option value="av01">AV01</option>
|
||||
<option value="vp9.2">VP9.2</option>
|
||||
<option value="vp9">VP9</option>
|
||||
<option value="h265">H265</option>
|
||||
<option value="theora">Theora</option>
|
||||
</select>
|
||||
<small>Choose your preferred video codec.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<!-- Developer Settings -->
|
||||
<details class="settings-section">
|
||||
<summary>Developer Settings</summary>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="downloadSystem" id="downloadSystem">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Download System</span>
|
||||
<small>Enable/disable the internal download system.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="logSystem" id="logSystem">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Log System</span>
|
||||
<small>Enable logging system.</small>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Verbose Logs</span>
|
||||
<small>Show verbose logs for debugging purposes.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<div class="settings-footer">
|
||||
<button id="open-theme">Open Theme Folder</button>
|
||||
<button id="open-json-btn">Open JSON Configuration</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="container" id="container">
|
||||
<h1 id="title"> Freedom Loader </h1>
|
||||
<p id="subtitle"> Because why not? </p>
|
||||
|
||||
<form action="/download" method="POST" id="downloadForm">
|
||||
<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">
|
||||
|
||||
@@ -65,6 +250,7 @@
|
||||
|
||||
<button type="submit" onclick="startProgress()">Download</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="download-path">
|
||||
@@ -72,16 +258,20 @@
|
||||
<button id="changePath">Edit</button>
|
||||
</div>
|
||||
|
||||
<div class="progress-container">
|
||||
<div id="downloadStage" style="display: none; font-size: 0.85rem; color: var(--default-text-color); margin-top: 8px;"></div>
|
||||
<div id="downloadProgressWrapper">
|
||||
<div id="downloadProgress"></div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px;">
|
||||
<div style="display: flex; gap: 20px; align-items: center;">
|
||||
<div id="downloadProgressText"></div>
|
||||
<div id="downloadSpeedText"></div>
|
||||
<div id="playlistInfoText" style="display: none; font-size: 0.9rem;"></div>
|
||||
<button type="button" id="stopBtn" onclick="stopDownload()" class="stop-btn" style="display:none;" title="Stop download">■</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="downloadStatus"></div>
|
||||
<div id="downloadStatus" style="display: none;"></div>
|
||||
|
||||
<div class="infos-container">
|
||||
|
||||
@@ -95,10 +285,23 @@
|
||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<span id="version-badge" class="version-badge">v1.3.1</span>
|
||||
<span id="version-badge" class="version-badge"></span>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<h2 id="reference"> Because Why Not ? </h2>
|
||||
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="script/toast.js"></script>
|
||||
<script src="script/clipboardPaste.js"></script>
|
||||
<script src="script/custompath.js"></script>
|
||||
<script src="script/settingsPanel.js"></script>
|
||||
<script src="script/appVersion.js"></script>
|
||||
<script src="script/downloadstatus.js"></script>
|
||||
<script src="script/fetchinfo.js"></script>
|
||||
<script src="script/progressBar.js"></script>
|
||||
|
||||
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();
|
||||
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,41 +1,60 @@
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
const savePathElem = document.getElementById("savePath");
|
||||
const form = document.getElementById("downloadForm");
|
||||
|
||||
// Essayer de charger depuis le localStorage
|
||||
let savedPath = localStorage.getItem("customDownloadPath");
|
||||
|
||||
// Sinon demander le chemin par défaut à l'API Electron
|
||||
if (!savedPath) {
|
||||
savedPath = await window.electronAPI.getDefaultDownloadPath();
|
||||
}
|
||||
|
||||
// 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;
|
||||
savePathElem.title = 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,63 +1,129 @@
|
||||
const themes = {
|
||||
default: { label: "Default", subtitle: "Because why not?" },
|
||||
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
|
||||
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
|
||||
neon: { label: "Néon", 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");
|
||||
let loadedThemes = [];
|
||||
|
||||
function populateThemeSelect() {
|
||||
for (const [themeKey, themeInfo] of Object.entries(themes)) {
|
||||
function applyTheme(theme) {
|
||||
const root = document.documentElement;
|
||||
const style = theme.style;
|
||||
|
||||
// Background
|
||||
if (theme.image) {
|
||||
document.body.style.backgroundImage = `url('${theme.image}')`;
|
||||
document.body.style.backgroundSize = style.background?.size || "cover";
|
||||
document.body.style.backgroundPosition = style.background?.position || "center";
|
||||
document.body.style.backgroundAttachment = style.background?.attachment || "fixed";
|
||||
} else {
|
||||
document.body.style.backgroundImage = "";
|
||||
}
|
||||
|
||||
// Colors
|
||||
root.style.setProperty("--background-color", style.colors?.background || "");
|
||||
root.style.setProperty("--default-text-color", style.colors?.text?.default || "");
|
||||
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "");
|
||||
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "");
|
||||
|
||||
// Form
|
||||
root.style.setProperty("--form-bg-color", style.form?.background || "");
|
||||
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "");
|
||||
root.style.setProperty("--form-input-border-color", style.form?.input?.border || "");
|
||||
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "");
|
||||
root.style.setProperty("--form-input-text-color", style.form?.input?.text || "");
|
||||
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "");
|
||||
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "");
|
||||
root.style.setProperty("--form-button-text-color", style.form?.button?.text || "");
|
||||
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "");
|
||||
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "");
|
||||
|
||||
// Checkbox
|
||||
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "");
|
||||
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "");
|
||||
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "");
|
||||
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "");
|
||||
|
||||
// Download
|
||||
root.style.setProperty("--download-status-color", style.download?.status || "");
|
||||
|
||||
// Progress bar
|
||||
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "");
|
||||
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "");
|
||||
|
||||
// Video info
|
||||
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "");
|
||||
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "");
|
||||
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "");
|
||||
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "");
|
||||
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "");
|
||||
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "");
|
||||
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "");
|
||||
|
||||
// Playlist
|
||||
root.style.setProperty("--playlist-background-color", style.playlist?.background || "");
|
||||
|
||||
// Settings
|
||||
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "");
|
||||
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "");
|
||||
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "");
|
||||
root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || "");
|
||||
root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || "");
|
||||
root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || "");
|
||||
root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || "");
|
||||
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "");
|
||||
root.style.setProperty("--settings-text-color", style.settings?.text || "");
|
||||
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "");
|
||||
|
||||
// Subtitle
|
||||
const subtitleEl = document.getElementById("subtitle");
|
||||
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
|
||||
}
|
||||
|
||||
function saveTheme(themeId) {
|
||||
window.electronAPI.setFeature("theme", themeId);
|
||||
}
|
||||
|
||||
function populateThemeSelect(themes) {
|
||||
themeSelect.innerHTML = "";
|
||||
for (const theme of themes) {
|
||||
const option = document.createElement("option");
|
||||
option.value = themeKey;
|
||||
option.textContent = themeInfo.label;
|
||||
option.value = theme.id;
|
||||
option.textContent = theme.name;
|
||||
themeSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(themeKey) {
|
||||
document.body.classList.remove(...Object.keys(themes));
|
||||
document.body.classList.add(themeKey);
|
||||
async function initThemes() {
|
||||
loadedThemes = await window.electronAPI.getThemes();
|
||||
|
||||
const subtitleElement = document.getElementById("subtitle");
|
||||
if (subtitleElement && themes[themeKey]) {
|
||||
subtitleElement.textContent = themes[themeKey].subtitle;
|
||||
}
|
||||
if (!loadedThemes.length) return;
|
||||
|
||||
populateThemeSelect(loadedThemes);
|
||||
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
const savedId = features.theme;
|
||||
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
|
||||
|
||||
themeSelect.value = theme.id;
|
||||
applyTheme(theme);
|
||||
}
|
||||
|
||||
function saveTheme(themeKey) {
|
||||
localStorage.setItem("selectedTheme", themeKey);
|
||||
}
|
||||
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem("selectedTheme");
|
||||
|
||||
if (savedTheme && themes[savedTheme]) {
|
||||
applyTheme(savedTheme);
|
||||
themeSelect.value = savedTheme;
|
||||
} else {
|
||||
applyTheme("dark"); // thème par défaut
|
||||
themeSelect.value = "dark";
|
||||
}
|
||||
}
|
||||
|
||||
// Quand l'utilisateur change le thème depuis le select
|
||||
themeSelect.addEventListener("change", (event) => {
|
||||
const selectedTheme = event.target.value;
|
||||
if (themes[selectedTheme]) {
|
||||
applyTheme(selectedTheme);
|
||||
saveTheme(selectedTheme);
|
||||
themeSelect.addEventListener("change", (e) => {
|
||||
const theme = loadedThemes.find(t => t.id === e.target.value);
|
||||
if (theme) {
|
||||
applyTheme(theme);
|
||||
saveTheme(theme.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialisation
|
||||
populateThemeSelect();
|
||||
loadTheme();
|
||||
async function refreshThemes() {
|
||||
loadedThemes = await window.electronAPI.reloadThemes();
|
||||
populateThemeSelect(loadedThemes);
|
||||
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
const savedId = features.theme;
|
||||
const theme = loadedThemes.find(t => t.id === savedId) || loadedThemes[0];
|
||||
|
||||
themeSelect.value = theme.id;
|
||||
applyTheme(theme);
|
||||
}
|
||||
|
||||
window.refreshThemes = refreshThemes;
|
||||
|
||||
initThemes();
|
||||
@@ -1,11 +1,29 @@
|
||||
const form = document.getElementById("downloadForm");
|
||||
const statusDiv = document.getElementById("downloadStatus");
|
||||
const button = form.querySelector("button");
|
||||
const button = form.querySelector("button[type=\"submit\"]");
|
||||
const stopBtn = document.getElementById("stopBtn");
|
||||
let isDownloading = false;
|
||||
let userStoppedDownload = false;
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
button.disabled = true; // Empêche les clics multiples
|
||||
statusDiv.textContent = "Download in progress...";
|
||||
|
||||
const urlInput = form.querySelector("input[name='url']");
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
// Basic URL validation
|
||||
if (!url) {
|
||||
window.showError("Please enter a URL");
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
statusDiv.style.display = "none";
|
||||
isDownloading = true;
|
||||
userStoppedDownload = false;
|
||||
|
||||
// Show progress UI immediately with initial message
|
||||
showInitialProgress();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const params = new URLSearchParams(formData);
|
||||
@@ -18,20 +36,92 @@ form.addEventListener("submit", async (e) => {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
statusDiv.textContent = res;
|
||||
if (!userStoppedDownload) {
|
||||
const errorText = await res.text();
|
||||
window.showError(errorText);
|
||||
resetProgressBar();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
statusDiv.textContent = text;
|
||||
// Download successful - check if notifications are enabled
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
if (features.notifySystem) {
|
||||
window.showSuccess("Download completed!");
|
||||
}
|
||||
|
||||
} catch {
|
||||
statusDiv.textContent = "❌ An Error has Occured.";
|
||||
} catch (err) {
|
||||
if (!isDownloading && !userStoppedDownload) {
|
||||
statusDiv.style.display = "none";
|
||||
}
|
||||
} finally {
|
||||
isDownloading = false;
|
||||
button.disabled = false;
|
||||
|
||||
setTimeout(() => {
|
||||
statusDiv.textContent = "";
|
||||
}, 5000);
|
||||
stopBtn.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
function showInitialProgress() {
|
||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||
const progressBarText = document.getElementById("downloadProgressText");
|
||||
const speedElement = document.getElementById("downloadSpeedText");
|
||||
const stageElement = document.getElementById("downloadStage");
|
||||
const stopBtn = document.getElementById("stopBtn");
|
||||
|
||||
if (progressWrapper) progressWrapper.style.display = "block";
|
||||
if (progressBarText) {
|
||||
progressBarText.style.display = "block";
|
||||
progressBarText.innerHTML = "0%";
|
||||
}
|
||||
if (speedElement) {
|
||||
speedElement.style.display = "block";
|
||||
speedElement.textContent = "0 Mib/s";
|
||||
}
|
||||
if (stageElement) {
|
||||
stageElement.style.display = "block";
|
||||
stageElement.textContent = "Retrieving data...";
|
||||
}
|
||||
if (stopBtn) stopBtn.style.display = "inline-flex";
|
||||
}
|
||||
|
||||
async function stopDownload() {
|
||||
if (!isDownloading) return;
|
||||
|
||||
userStoppedDownload = true;
|
||||
button.disabled = true;
|
||||
stopBtn.disabled = true;
|
||||
|
||||
try {
|
||||
await fetch("/download/cancel", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
});
|
||||
|
||||
window.showWarning("Download stopped");
|
||||
} catch (err) {
|
||||
console.error("Error stopping download:", err);
|
||||
} finally {
|
||||
isDownloading = false;
|
||||
button.disabled = false;
|
||||
stopBtn.disabled = false;
|
||||
stopBtn.style.display = "none";
|
||||
resetProgressBar();
|
||||
}
|
||||
}
|
||||
|
||||
function resetProgressBar() {
|
||||
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||
if (progressWrapper) progressWrapper.style.display = "none";
|
||||
|
||||
const progressBar = document.getElementById("downloadProgress");
|
||||
if (progressBar) progressBar.style.width = "0%";
|
||||
|
||||
const progressBarText = document.getElementById("downloadProgressText");
|
||||
if (progressBarText) progressBarText.style.display = "none";
|
||||
|
||||
const speedElement = document.getElementById("downloadSpeedText");
|
||||
if (speedElement) speedElement.style.display = "none";
|
||||
|
||||
const playlistInfoElement = document.getElementById("playlistInfoText");
|
||||
if (playlistInfoElement) playlistInfoElement.style.display = "none";
|
||||
}
|
||||
@@ -26,20 +26,39 @@ async function fetchVideoInfo(url) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
async function init() {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const urlInput = document.getElementById("UrlInput");
|
||||
const infoDiv = document.getElementById("videoInfo");
|
||||
const loaderBox = document.getElementById("loaderBox");
|
||||
const form = document.getElementById("downloadForm");
|
||||
|
||||
// Input caché pour stocker le titre de la playlist
|
||||
let playlistTitleInput = document.getElementById("playlistTitle");
|
||||
if (!playlistTitleInput) {
|
||||
playlistTitleInput = document.createElement("input");
|
||||
playlistTitleInput.type = "hidden";
|
||||
playlistTitleInput.name = "playlistTitle";
|
||||
playlistTitleInput.id = "playlistTitle";
|
||||
form.appendChild(playlistTitleInput);
|
||||
}
|
||||
|
||||
let lastFetchedUrl = "";
|
||||
|
||||
urlInput.addEventListener("input", async () => {
|
||||
|
||||
const configFeatures = await window.electronAPI.getFeatures();
|
||||
if (!configFeatures.autoCheckInfo) return;
|
||||
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
// Si champ vide -> reset total
|
||||
// IF field empty -> total reset
|
||||
if (!url || url.length < 2) {
|
||||
infoDiv.innerHTML = "";
|
||||
infoDiv.classList.remove("visible", "playlist-mode");
|
||||
playlistTitleInput.value = "";
|
||||
lastFetchedUrl = "";
|
||||
return;
|
||||
}
|
||||
@@ -51,7 +70,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const data = await fetchVideoInfo(url);
|
||||
loaderBox.style.display = "none";
|
||||
|
||||
// Gestion des erreurs
|
||||
|
||||
if (data.error) {
|
||||
infoDiv.innerHTML = `
|
||||
<div style="
|
||||
@@ -63,17 +82,37 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
`;
|
||||
infoDiv.classList.add("visible");
|
||||
infoDiv.classList.remove("playlist-mode");
|
||||
playlistTitleInput.value = "";
|
||||
loaderBox.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- PLAYLIST ----------
|
||||
if (data.type === "playlist") {
|
||||
playlistTitleInput.value = data.title;
|
||||
infoDiv.classList.add("playlist-mode");
|
||||
infoDiv.innerHTML = `
|
||||
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist Detected: ${data.title}</strong></h3>
|
||||
<h3 style="color:var(--video-info-heading-color);"><strong>Video Count: ${data.count}</strong></h3>
|
||||
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p>
|
||||
<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>
|
||||
`;
|
||||
|
||||
@@ -81,39 +120,58 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
data.videos.forEach(v => {
|
||||
const durationStr = v.duration
|
||||
? `${Math.floor(v.duration / 60)}m ${(v.duration % 60).toString().padStart(2,"0")}s` : "Inconnue";
|
||||
? `${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 style="margin-bottom:12px;">
|
||||
<img src="${v.thumbnail}" width="160" alt="Thumbnail">
|
||||
<p><strong>${v.title}</strong></p>
|
||||
<p>Duration : ${durationStr}</p>
|
||||
<p><a href="${videoUrl}" target="_blank">URL</a>
|
||||
<button class="copy-btn" data-url="${videoUrl}">📋</button>
|
||||
</p>
|
||||
<div 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>
|
||||
`;
|
||||
});
|
||||
|
||||
// Gestion du bouton copier
|
||||
// Copy Button
|
||||
listDiv.addEventListener("click", (event) => {
|
||||
if (event.target.classList.contains("copy-btn")) {
|
||||
const btn = event.target;
|
||||
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.textContent;
|
||||
const original = btn.innerHTML;
|
||||
|
||||
btn.style.opacity = 0;
|
||||
btn.style.transform = "scale(0.7)";
|
||||
|
||||
setTimeout(() => {
|
||||
btn.textContent = "✅";
|
||||
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)";
|
||||
|
||||
@@ -122,7 +180,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
btn.style.transform = "scale(0.7)";
|
||||
|
||||
setTimeout(() => {
|
||||
btn.textContent = original;
|
||||
btn.innerHTML = original;
|
||||
btn.style.opacity = 1;
|
||||
btn.style.transform = "scale(1)";
|
||||
btn.disabled = false;
|
||||
@@ -133,10 +191,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}, 300);
|
||||
})
|
||||
.catch(() => {
|
||||
const original = btn.textContent;
|
||||
btn.textContent = "❌";
|
||||
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.textContent = original;
|
||||
btn.innerHTML = original;
|
||||
btn.disabled = false;
|
||||
}, 1500);
|
||||
});
|
||||
@@ -148,6 +208,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
|
||||
infoDiv.classList.remove("playlist-mode");
|
||||
playlistTitleInput.value = "";
|
||||
|
||||
// ---------- VIDEO NORMALE ----------
|
||||
const durationStr = data.duration
|
||||
@@ -177,4 +238,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
infoDiv.classList.add("visible");
|
||||
});
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
init();
|
||||
@@ -3,19 +3,34 @@ const progressBar = document.getElementById("downloadProgress");
|
||||
const progressBarText = document.getElementById("downloadProgressText")
|
||||
|
||||
const speedElement = document.getElementById("downloadSpeedText");
|
||||
const stageElement = document.getElementById("downloadStage");
|
||||
const playlistInfoElement = document.getElementById("playlistInfoText");
|
||||
const speedEvt = new EventSource("/download/speed");
|
||||
const stageEvt = new EventSource("/download/stage");
|
||||
const playlistInfoEvt = new EventSource("/download/playlist-info");
|
||||
|
||||
function startProgress() {
|
||||
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 showProgress() {
|
||||
progressWrapper.style.display = "block";
|
||||
progressBarText.style.display = "block";
|
||||
speedElement.style.display = "block";
|
||||
|
||||
// Show stop button
|
||||
const stopBtn = document.getElementById("stopBtn");
|
||||
if (stopBtn) stopBtn.style.display = "inline-flex";
|
||||
}
|
||||
|
||||
function updateProgress(percent) {
|
||||
// Show progress div and stop button only when percent > 0
|
||||
if (percent > 0 && progressWrapper.style.display !== "block") {
|
||||
showProgress();
|
||||
}
|
||||
|
||||
progressBar.style.width = `${percent}%`;
|
||||
progressBarText.innerHTML = `${percent}%`;
|
||||
}
|
||||
@@ -28,9 +43,19 @@ function resetProgress() {
|
||||
|
||||
speedElement.textContent = "0 Mib/s";
|
||||
speedElement.style.display = "none";
|
||||
|
||||
stageElement.textContent = "";
|
||||
stageElement.style.display = "none";
|
||||
|
||||
playlistInfoElement.textContent = "";
|
||||
playlistInfoElement.style.display = "none";
|
||||
|
||||
// Hide stop button
|
||||
const stopBtn = document.getElementById("stopBtn");
|
||||
if (stopBtn) stopBtn.style.display = "none";
|
||||
}
|
||||
|
||||
// Connexion SSE
|
||||
// SSE Connexion
|
||||
const evtSource = new EventSource("/download/progress");
|
||||
evtSource.onmessage = e => {
|
||||
if (e.data === "reset") {
|
||||
@@ -40,8 +65,11 @@ evtSource.onmessage = e => {
|
||||
}
|
||||
|
||||
if (e.data === "done") {
|
||||
// Keep progress bar visible for better UX, let toast handle the feedback
|
||||
setTimeout(() => {
|
||||
resetProgress();
|
||||
window.electronAPI.setProgress(-1);
|
||||
}, 2000); // Wait 2s so user sees 100% progress
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,11 +77,8 @@ evtSource.onmessage = e => {
|
||||
|
||||
if (!isNaN(percent)) {
|
||||
updateProgress(percent);
|
||||
window.electronAPI.setProgress(percent); // update barre des tâches
|
||||
if (percent >= 100) setTimeout(() => {
|
||||
resetProgress();
|
||||
window.electronAPI.setProgress(-1); // retire la barre
|
||||
}, 500);
|
||||
window.electronAPI.setProgress(percent); // Update Task Bar
|
||||
// Don't hide at 100%, wait for "done" signal instead
|
||||
}
|
||||
};
|
||||
|
||||
@@ -61,3 +86,13 @@ speedEvt.onmessage = e => {
|
||||
speedElement.style.display = "block";
|
||||
speedElement.textContent = e.data; // ex: "5.2MiB/s"
|
||||
};
|
||||
|
||||
stageEvt.onmessage = e => {
|
||||
stageElement.style.display = "block";
|
||||
stageElement.textContent = e.data; // ex: "📥 Downloading..."
|
||||
};
|
||||
|
||||
playlistInfoEvt.onmessage = e => {
|
||||
playlistInfoElement.style.display = "block";
|
||||
playlistInfoElement.textContent = e.data; // ex: "Item 1 of 15"
|
||||
};
|
||||
66
public/script/settingsPanel.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const settingsPanel = document.getElementById("settings-panel");
|
||||
const settingsModal = document.querySelector(".settings-modal");
|
||||
|
||||
const settingsButton = document.getElementById("settings-button");
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
settingsButton.addEventListener("click", () => {
|
||||
const isOpen = settingsPanel.style.display === "block";
|
||||
settingsPanel.style.display = isOpen ? "none" : "block";
|
||||
});
|
||||
|
||||
function closePanel() {
|
||||
isOpen = false;
|
||||
settingsPanel.style.display = "none";
|
||||
}
|
||||
|
||||
|
||||
async function loadSettings() {
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
document.querySelectorAll(".settings-section input, .settings-section select").forEach(el => {
|
||||
const key = el.dataset.key;
|
||||
if (features[key] !== undefined) {
|
||||
if (el.type === "checkbox") el.checked = features[key];
|
||||
else if (el.tagName === "SELECT") el.value = features[key];
|
||||
}
|
||||
|
||||
el.addEventListener("change", () => {
|
||||
if (key === "theme") return;
|
||||
let value = el.type === "checkbox" ? el.checked : el.value;
|
||||
window.electronAPI.setFeature(key, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Open The JSON
|
||||
document.getElementById("open-json-btn").addEventListener("click", () => {
|
||||
window.topbarAPI.openConfig();
|
||||
});
|
||||
|
||||
// Open Theme Folder
|
||||
document.getElementById("open-theme").addEventListener("click", () => {
|
||||
window.topbarAPI.openTheme();
|
||||
});
|
||||
|
||||
// Refresh Themes
|
||||
document.getElementById("refresh-themes-btn").addEventListener("click", function() {
|
||||
this.classList.add("spinning");
|
||||
window.refreshThemes();
|
||||
|
||||
setTimeout(() => {
|
||||
this.classList.remove("spinning");
|
||||
}, 600);
|
||||
});
|
||||
|
||||
|
||||
/* clic sur l'overlay = fermer */
|
||||
settingsPanel.addEventListener("click", closePanel);
|
||||
|
||||
/* clic dans la modal = stop propagation */
|
||||
settingsModal.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
loadSettings();
|
||||
54
public/script/toast.js
Normal file
@@ -0,0 +1,54 @@
|
||||
window.showToast = function(message, type = 'info', duration = 4000) {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.classList.add('toast', type);
|
||||
|
||||
// Icons for different types
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
warning: '⚠',
|
||||
info: 'ℹ'
|
||||
};
|
||||
|
||||
const icon = icons[type] || icons.info;
|
||||
|
||||
toast.innerHTML = `
|
||||
<span class="toast-icon">${icon}</span>
|
||||
<span class="toast-message">${message}</span>
|
||||
<button class="toast-close" title="Close">×</button>
|
||||
`;
|
||||
|
||||
// Add close functionality
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
closeBtn.addEventListener('click', () => removeToast(toast));
|
||||
|
||||
// Add to container
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto remove after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => removeToast(toast), duration);
|
||||
}
|
||||
|
||||
return toast;
|
||||
};
|
||||
|
||||
function removeToast(toast) {
|
||||
if (!toast.parentElement) return;
|
||||
|
||||
toast.classList.add('removing');
|
||||
setTimeout(() => {
|
||||
if (toast.parentElement) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
window.showSuccess = (message, duration = 4000) => window.showToast(message, 'success', duration);
|
||||
window.showError = (message, duration = 5000) => window.showToast(message, 'error', duration);
|
||||
window.showWarning = (message, duration = 4000) => window.showToast(message, 'warning', duration);
|
||||
window.showInfo = (message, duration = 4000) => window.showToast(message, 'info', duration);
|
||||
@@ -1,8 +1,8 @@
|
||||
// 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');
|
||||
@@ -10,6 +10,7 @@ function setupTopbarListeners() {
|
||||
const logsBtn = document.getElementById('logs-btn');
|
||||
const websiteBtn = document.getElementById('website-btn');
|
||||
const wikiBtn = document.getElementById('wiki-btn');
|
||||
const workshopBtn = document.getElementById('workshop-btn');
|
||||
|
||||
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
||||
@@ -18,7 +19,15 @@ function setupTopbarListeners() {
|
||||
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
||||
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
||||
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
||||
});
|
||||
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
||||
}
|
||||
|
||||
setupTopbarListeners();
|
||||
document.addEventListener('DOMContentLoaded', setupTopbarListeners);
|
||||
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
if(!features.customTopBar){
|
||||
document.getElementById("topbar").style.display = "none";
|
||||
document.getElementById("container").style.marginTop = "0";
|
||||
document.getElementById("theme-switcher").style.top = "30px";
|
||||
}
|
||||
94
public/splash.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
.splash-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #D9D9D9;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 28px;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}
|
||||
.progress-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.progress-track {
|
||||
width: 180px;
|
||||
height: 3px;
|
||||
background: #d0d0d0;
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: #1a1a1a;
|
||||
border-radius: 99px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.status {
|
||||
font-size: 14px;
|
||||
color: #6d6d6d;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="splash-container">
|
||||
<img src="./banner.png" alt="Freedom Loader" style="width: 90%;">
|
||||
<div class="progress-wrapper">
|
||||
<div class="progress-track">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<span class="status" id="status">Starting...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const steps = [
|
||||
"Checking dependencies...",
|
||||
"Starting server...",
|
||||
"Loading themes...",
|
||||
"Almost ready...",
|
||||
];
|
||||
let current = 0;
|
||||
const bar = document.getElementById("progress-bar");
|
||||
const status = document.getElementById("status");
|
||||
|
||||
function advance(label, percent) {
|
||||
bar.style.width = percent + "%";
|
||||
status.textContent = label;
|
||||
}
|
||||
|
||||
window.setProgress = (step) => {
|
||||
if (step >= steps.length) {
|
||||
advance("", 100);
|
||||
return;
|
||||
}
|
||||
const percent = Math.round(((step + 1) / steps.length) * 100);
|
||||
advance(steps[step], percent);
|
||||
};
|
||||
|
||||
window.setProgress(0);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -15,13 +15,13 @@
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
--clr: var(--checkbox-bg-checked);
|
||||
--clr: var(--checkbox-checked-bg-color);
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1.3em;
|
||||
width: 1.3em;
|
||||
background-color: var(--checkbox-bg-default);
|
||||
background-color: var(--checkbox-unchecked-bg-color);
|
||||
border-radius: .5rem;
|
||||
transition: background-color 300ms, border-radius 300ms;
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
border: 1px dashed var(--form-input-border-color);
|
||||
word-break: break-all;
|
||||
display: inline-block;
|
||||
max-width: 80%;
|
||||
display: inline-flex;
|
||||
max-width: clamp(200px, 60vw, 600px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
background: var(--form-bg-color);
|
||||
border: 2px solid var(--mp3-text-color);
|
||||
border: 2px solid var(--audio-only-label-color);
|
||||
border-radius: 8px;
|
||||
color: var(--mp3-text-color);
|
||||
color: var(--audio-only-label-color);
|
||||
font-weight: bold;
|
||||
animation: pulse 1.2s infinite;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
/* Container pour isoler la progress bar */
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Wrapper de la barre de téléchargement */
|
||||
#downloadProgressWrapper {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 12px;
|
||||
background-color: #444;
|
||||
border-radius: 6px;
|
||||
background-color: var(--progress-bar-bg-color, #cccccc);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 10px auto;
|
||||
margin: 0;
|
||||
display: none;
|
||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Barre de progression */
|
||||
#downloadProgress {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background-color: var(--video-info-link-color, #4caf50);
|
||||
border-radius: 6px 0 0 6px;
|
||||
background-color: var(--progress-bar-fill-color, #007bff);
|
||||
border-radius: 10px;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -25,7 +37,7 @@
|
||||
font-size: 0.8rem;
|
||||
color: #fff;
|
||||
content: "";
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive pour mobile */
|
||||
@@ -39,3 +51,39 @@
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stop Button */
|
||||
.stop-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
min-width: 40px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: var(--form-button-bg-color);
|
||||
color: var(--form-button-text-color);
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stop-btn:hover:not(:disabled) {
|
||||
background-color: var(--form-button-bg-hover-color);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.stop-btn:active:not(:disabled) {
|
||||
transform: scale(0.96);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stop-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
.theme-switcher {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
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: 1050;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
136
public/styles/components/toast.css
Normal file
@@ -0,0 +1,136 @@
|
||||
/* Toast Container */
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
top: 140px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Toast */
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background-color: rgba(50, 50, 50, 0.9);
|
||||
border-radius: 6px;
|
||||
color: #ffffff;
|
||||
font-size: 0.9rem;
|
||||
max-width: 220px;
|
||||
min-width: 100px;
|
||||
animation: slideIn 0.25s ease-out;
|
||||
pointer-events: auto;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background-color: rgba(40, 167, 69, 0.15);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background-color: rgba(220, 53, 69, 0.15);
|
||||
}
|
||||
|
||||
.toast.warning {
|
||||
background-color: rgba(255, 193, 7, 0.15);
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
background-color: rgba(23, 162, 184, 0.15);
|
||||
}
|
||||
|
||||
/* Toast Icon */
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast.success .toast-icon {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.toast.error .toast-icon {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.toast.warning .toast-icon {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.toast.info .toast-icon {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
/* Toast Message */
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Toast Close Button */
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(350px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(350px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.removing {
|
||||
animation: slideOut 0.25s ease-out;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
#toast-container {
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
max-width: none;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1em;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -50,7 +50,7 @@ form#downloadForm {
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
gap: 0.5em;
|
||||
color: var(--form-input-text-color);
|
||||
color: var(--audio-only-label-color);
|
||||
}
|
||||
|
||||
#UrlInput {
|
||||
@@ -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,40 @@ 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 {
|
||||
color: var(--paste-button-icon-color);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 2vw;
|
||||
}
|
||||
|
||||
|
||||
header h1{
|
||||
margin-bottom: 0.2vw;
|
||||
#title {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
277
public/styles/layout/settingsPanel.css
Normal file
@@ -0,0 +1,277 @@
|
||||
/* SETTINGS BUTTON */
|
||||
.settings-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
opacity: 0.8;
|
||||
z-index: 15;
|
||||
background-color: var(--settings-button-bg-color);
|
||||
border-radius: 14px;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-button svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
stroke: var(--settings-button-text-color);
|
||||
}
|
||||
|
||||
.settings-button:hover {
|
||||
transform: scale(1.1);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.settings-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
|
||||
/* SETTINGS PANEL */
|
||||
.settings-panel {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 40px;
|
||||
width: 40vw;
|
||||
max-width: 320px;
|
||||
max-height: 80vh;
|
||||
background-color: var(--settings-modal-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.2s ease-out;
|
||||
color: var(--settings-text-color);
|
||||
}
|
||||
|
||||
.settings-panel::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.settings-modal {
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
max-height: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: none;
|
||||
|
||||
}
|
||||
|
||||
/* TITLE */
|
||||
.settings-title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings-title h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.settings-title p {
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* SECTION */
|
||||
.settings-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px 15px;
|
||||
background: var(--settings-section-bg-color);
|
||||
border-radius: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* SETTING ITEM */
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.setting-item input[type="checkbox"] {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.setting-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.setting-info small {
|
||||
font-size: 12px;
|
||||
color: var(--settings-subtitle-color);
|
||||
margin-top: 2px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#theme .settings-title{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.setting-item select {
|
||||
width: 70%;
|
||||
background: #fff;
|
||||
border: 1px solid #242424;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.setting-item select:hover,
|
||||
.setting-item select:focus {
|
||||
background: #474747;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* SELECT */
|
||||
.settings-section select {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* DETAILS / DROPDOWN */
|
||||
details {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px 15px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
details[open] {
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* FOOTER */
|
||||
.settings-footer {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#open-json-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
background-color: var(--open-json-button-bg-color);
|
||||
color: var(--open-json-button-text-color);
|
||||
}
|
||||
|
||||
#open-theme {
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
background-color: var(--open-theme-button-bg-color);
|
||||
color: var(--open-theme-button-text-color);
|
||||
}
|
||||
|
||||
#experimental-args {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
border: 1px solid #333;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* CLOSE BTN */
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* REFRESH THEMES BUTTON */
|
||||
.refresh-themes-btn {
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--form-input-border-color);
|
||||
background-color: var(--form-input-bg-color);
|
||||
color: var(--form-input-text-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-themes-btn:hover {
|
||||
background-color: var(--form-button-bg-hover-color);
|
||||
}
|
||||
|
||||
.refresh-themes-btn.spinning svg {
|
||||
animation: spin 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.refresh-themes-btn svg {
|
||||
color: currentColor;
|
||||
transition: transform 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
/* ANIMATION */
|
||||
@keyframes popIn {
|
||||
from { transform: scale(0.95); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -2,20 +2,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 1100;
|
||||
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
|
||||
height: 100%;
|
||||
background: #1f1f1f98;
|
||||
color: #eaeaea;
|
||||
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
|
||||
}
|
||||
|
||||
/* LEFT - custom buttons */
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 1.5em 2em;
|
||||
background-color: var(--infos-box-color);
|
||||
background-color: var(--video-info-bg-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||
color: var(--video-info-text-color);
|
||||
@@ -69,80 +69,293 @@
|
||||
|
||||
/* --- Playlist Mode --- */
|
||||
#videoInfo.playlist-mode {
|
||||
max-width: 900px;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
/* Style du bloc "Playlist détectée" */
|
||||
#videoInfo.playlist-mode > p {
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
/* 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(220px, 1fr));
|
||||
gap: 18px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Un item de playlist */
|
||||
#playlistVideos > div {
|
||||
/* Carte vidéo playlist */
|
||||
.playlist-video-card {
|
||||
background: var(--playlist-background-color);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
#playlistVideos > div p {
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#videoInfo.playlist-mode {
|
||||
max-width: 95%;
|
||||
.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;
|
||||
}
|
||||
|
||||
#playlistVideos > div:hover {
|
||||
.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);
|
||||
}
|
||||
|
||||
#playlistVideos img {
|
||||
width: 100%;
|
||||
.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;
|
||||
margin-bottom: 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 {
|
||||
all: unset;
|
||||
background: transparent;
|
||||
border: none;
|
||||
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: 4px;
|
||||
vertical-align: middle;
|
||||
transition: transform 0.1s ease, color 0.1s ease, opacity 0.15s ease;
|
||||
font-size: 1em;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#playlistVideos > div p a{
|
||||
text-decoration: none;
|
||||
color: var(--video-info-link-color);
|
||||
.copy-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--video-info-text-color);
|
||||
}
|
||||
|
||||
#playlistVideos > div p a:hover{
|
||||
text-decoration: underline;
|
||||
.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;
|
||||
@@ -152,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,29 +1,33 @@
|
||||
/* Layout */
|
||||
@import url("layout/topbar.css");
|
||||
@import url("layout/header.css");
|
||||
@import url("layout/form.css");
|
||||
@import url("layout/container.css");
|
||||
@import url("layout/header.css");
|
||||
@import url("layout/settingsPanel.css");
|
||||
@import url("layout/videoinfo.css");
|
||||
|
||||
/* Components */
|
||||
@import url("components/checkbox.css");
|
||||
@import url("components/themebutton.css");
|
||||
@import url("components/loader.css");
|
||||
|
||||
@import url("theme/themeimport.css");
|
||||
@import url("components/editpathbutton.css");
|
||||
@import url("components/progressBar.css");
|
||||
@import url("components/toast.css");
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||
|
||||
#app {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
#app.visible {
|
||||
opacity: 1;
|
||||
|
||||
/* Because why not */
|
||||
#reference{
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
bottom:0vh;
|
||||
left: 10px;
|
||||
font-size: 1em;
|
||||
color: #b0b0b02f;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
|
||||
html,body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -34,33 +38,45 @@ html,body{
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
body{
|
||||
background-color: var(--background-color);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--default-text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
/* Scrollbar globale pour Chromium/Electron */
|
||||
body::-webkit-scrollbar {
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1100;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
height: calc(100vh - 48px);
|
||||
margin-top: 48px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Scrollbar pour main */
|
||||
main::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track {
|
||||
main::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb {
|
||||
main::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 5px;
|
||||
border: 2px solid transparent;
|
||||
@@ -68,36 +84,32 @@ body::-webkit-scrollbar-thumb {
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb:hover {
|
||||
main::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
transform: scaleX(1.05);
|
||||
}
|
||||
|
||||
/* Thème clair */
|
||||
body.light::-webkit-scrollbar-track {
|
||||
body.light main::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body.light::-webkit-scrollbar-thumb {
|
||||
body.light main::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
body.light::-webkit-scrollbar-thumb:hover {
|
||||
body.light main::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.container{
|
||||
margin-top: 50px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
header p{
|
||||
#subtitle{
|
||||
color: var(--subtitle-color);
|
||||
}
|
||||
|
||||
.checkbox-inline{
|
||||
color: var(--mp3-text-color);
|
||||
}
|
||||
|
||||
body,
|
||||
.container,
|
||||
input,
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
body.chirac {
|
||||
|
||||
background-image: url('../../assets/images/Chirac.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
|
||||
|
||||
--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);
|
||||
--form-input-bg-color: #f5f5f5;
|
||||
--form-input-border-color: #002395;
|
||||
--form-input-border-focus-color: #EF4135;
|
||||
--form-input-text-color: #002395;
|
||||
--form-input-placeholder-color: #999999;
|
||||
--form-button-bg-color: #002395;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #EF4135;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #002395;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(0, 35, 149, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #002395;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: rgba(255, 255, 255, 0.9);
|
||||
--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;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #d1d1d1ca;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Je serai le président de tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Mes chers compatriotes
|
||||
Je serai le président de tous les Français, de tous les Français
|
||||
Je mesure la difficulté de la tâche qui nous attend
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Je veux un État, je veux les Français
|
||||
Je veux un État, je veux
|
||||
Je veux un État-tat-tat-tat vigoureux
|
||||
Un État-tat-tat-tat impartial
|
||||
Un État-tat-tat-tat vigoureux
|
||||
Un État-tat-tat-tat exigeant
|
||||
Un État-tat-tat-tat
|
||||
Je serai le président de tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Tous les Français
|
||||
Un État qui n'isole pas, tous, pas, pas
|
||||
Tous, pas, tous, pas, pas
|
||||
Tous, pas, tous, pas, pas (les Français)
|
||||
Tous, tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, tous, pas, tous, pas, pas (les Français)
|
||||
Tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, pas, tous, tous, pas, pas
|
||||
Tous, tous, tous-tous, tous-tous, pas, pas
|
||||
Tous, tous-tous, tous-tous, pas, pas
|
||||
Un État-tat-tat-tat vigoureux
|
||||
Un État-tat-tat-tat impartial
|
||||
Un État-tat-tat-tat exigeant
|
||||
Un État-tat-tat-tat
|
||||
Je serai le président de tous les Français
|
||||
|
||||
*/
|
||||
@@ -1,39 +0,0 @@
|
||||
body.dark {
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212; /* noir/gris très foncé */
|
||||
--default-text-color: #e0e0e0; /* texte clair */
|
||||
--subtitle-color: #2196f3; /* bleu accent */
|
||||
--mp3-text-color: #f5f5f5; /* texte checkbox */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1e1e1e; /* fond formulaire */
|
||||
--form-input-bg-color: #2a2a2a; /* fond inputs */
|
||||
--form-input-border-color: #444444; /* bordure */
|
||||
--form-input-border-focus-color: #2196f3; /* focus bleu */
|
||||
--form-input-text-color: #f5f5f5; /* texte clair */
|
||||
--form-input-placeholder-color: #888888; /* placeholder gris */
|
||||
--form-button-bg-color: #2196f3; /* bouton bleu */
|
||||
--form-button-text-color: #ffffff; /* texte bouton */
|
||||
--form-button-bg-hover-color: #1976d2; /* hover bleu plus foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555; /* gris moyen */
|
||||
--checkbox-bg-checked: #2196f3; /* bleu accent */
|
||||
--checkbox-checkmark-border-color: #ffffff; /* blanc */
|
||||
--checkbox-pulse-color: rgba(33, 150, 243, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #2196f3;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1a1a1a;
|
||||
--video-info-text-color: #e0e0e0;
|
||||
--video-info-heading-color: #2196f3;
|
||||
--video-info-list-color: #cccccc;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #2196f3;
|
||||
--video-info-link-hover-color: #1976d2;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #303030;
|
||||
}
|
||||
@@ -1,43 +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: #0c0140;
|
||||
--video-info-text-color: #ffffff;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #070823a3;
|
||||
}
|
||||
|
||||
body.default .download-path #savePath {
|
||||
color: #ffffff;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
body.drift {
|
||||
|
||||
background-image: url('../../assets/images/Drift.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Palette principale */
|
||||
--background-color: #0f1218;
|
||||
--default-text-color: #3d4b61;
|
||||
--subtitle-color: #1a1a1a;
|
||||
--mp3-text-color: #c0d4ff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1e222a;
|
||||
--form-input-bg-color: #13171d;
|
||||
--form-input-border-color: #3b4b6d;
|
||||
--form-input-border-focus-color: #6ea8ff;
|
||||
--form-input-text-color: #f0f4ff;
|
||||
--form-input-placeholder-color: #7a8fae;
|
||||
--form-button-bg-color: #2a2979;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #353393;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #2a2e38;
|
||||
--checkbox-bg-checked: #2a2979;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(76,108,255,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ff8e8e;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1e222ad0;
|
||||
--video-info-text-color: #e8e8f0;
|
||||
--video-info-heading-color: #9ab6ff;
|
||||
--video-info-list-color: #c0d4ff;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #4c6cff;
|
||||
--video-info-link-hover-color: #6c8dff;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #424b5cc2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body.drift .download-path{
|
||||
color: #ededed;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
body.fanatic{
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212;
|
||||
--default-text-color: #eee;
|
||||
--subtitle-color: #FF5900;
|
||||
--mp3-text-color: #ffffff;
|
||||
|
||||
/* Formulaire */
|
||||
--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;
|
||||
--checkbox-bg-checked: #ff6600;
|
||||
--checkbox-checkmark-border-color: #eee;
|
||||
--checkbox-pulse-color: rgba(255, 102, 0, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #FF5900;
|
||||
|
||||
/* Video Info Box */
|
||||
--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;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
body.light {
|
||||
/* Couleurs générales */
|
||||
--background-color: #dadada;
|
||||
--default-text-color: #222222;
|
||||
--subtitle-color: #007bff;
|
||||
--mp3-text-color: #000000;
|
||||
|
||||
/* Formulaire */
|
||||
--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;
|
||||
--checkbox-bg-checked: #007bff;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(0, 123, 255, 0.4);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #ffffff;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #c5c5c5b2;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
body.neon {
|
||||
|
||||
background: (url("../../assets/images/drift.jpg"));
|
||||
background-image: linear-gradient(135deg, #0f0c29, #302b63, #24243e); /* dégradé sombre violet/bleu */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #1b1b2f;
|
||||
--default-text-color: #e0e0ff;
|
||||
--subtitle-color: #a3a3ff;
|
||||
--mp3-text-color: #c0c0ff;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #2c2c4a;
|
||||
--form-input-bg-color: #1f1f3a;
|
||||
--form-input-border-color: #5858f0;
|
||||
--form-input-border-focus-color: #a3a3ff;
|
||||
--form-input-text-color: #ffffff;
|
||||
--form-input-placeholder-color: #8888ff;
|
||||
--form-button-bg-color: #5858f0;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #7a7aff;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #302b63;
|
||||
--checkbox-bg-checked: #5858f0;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(88,88,240,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #a3a3ff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #2c2c4a;
|
||||
--video-info-text-color: #e0e0ff;
|
||||
--video-info-heading-color: #a3a3ff;
|
||||
--video-info-list-color: #c0c0ff;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #5858f0;
|
||||
--video-info-link-hover-color: #7a7aff;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #3e3e64;
|
||||
}
|
||||
|
||||
body.neon .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
body.nf {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
body.nf .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
body.songbird {
|
||||
|
||||
background-image: url('../../assets/images/Songbird.webp');
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
background-attachment: fixed;
|
||||
|
||||
/* Couleurs générales */
|
||||
--background-color: #0a0a0a; /* noir profond */
|
||||
--default-text-color: #ff2a2a; /* rouge néon */
|
||||
--subtitle-color: #f2f2f2; /* rouge sombre */
|
||||
--mp3-text-color: #ff4d4d; /* rouge un peu plus clair */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: rgba(20,0,0,0.85); /* noir semi-transparent avec rouge */
|
||||
--form-input-bg-color: #1a0a0a;
|
||||
--form-input-border-color: #ff2a2a;
|
||||
--form-input-border-focus-color: #b30000;
|
||||
--form-input-text-color: #ffffff;
|
||||
--form-input-placeholder-color: #950404;
|
||||
--form-button-bg-color: #b30000;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #ff2a2a;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #630000;
|
||||
--checkbox-bg-checked: #ff2a2a;
|
||||
--checkbox-checkmark-border-color: #ffffff;
|
||||
--checkbox-pulse-color: rgba(255,42,42,0.3);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ff2a2a;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: rgba(30,0,0,0.9);
|
||||
--video-info-text-color: #ff4d4d;
|
||||
--video-info-heading-color: #ff2a2a;
|
||||
--video-info-list-color: #b30000;
|
||||
--video-info-list-strong-color: #ffffff;
|
||||
--video-info-link-color: #ff2a2a;
|
||||
--video-info-link-hover-color: #b30000;
|
||||
|
||||
/* Playlist Color */
|
||||
--playlist-background-color: #3f0000;
|
||||
}
|
||||
|
||||
body.cyberpunk .download-path {
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
body.spicy {
|
||||
/* Couleurs générales */
|
||||
--background-color: #121212; /* fond noir profond */
|
||||
--default-text-color: #ffffff; /* texte clair */
|
||||
--subtitle-color: #f87918; /* accent rouge piment */
|
||||
--mp3-text-color: #ffffff; /* texte checkbox inline */
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #1a1a1a; /* fond formulaire gris très foncé */
|
||||
--form-input-bg-color: #2b2b2b; /* fond inputs gris foncé */
|
||||
--form-input-border-color: #444444; /* bordure gris */
|
||||
--form-input-border-focus-color: #ff1a1a; /* focus rouge piment */
|
||||
--form-input-text-color: #eeeeee; /* texte inputs */
|
||||
--form-input-placeholder-color: #888888; /* placeholder gris moyen */
|
||||
--form-button-bg-color: #ff1a1a; /* bouton rouge vif */
|
||||
--form-button-text-color: #ffffff; /* texte bouton blanc */
|
||||
--form-button-bg-hover-color: #cc0000; /* hover rouge foncé */
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #555555; /* gris moyen */
|
||||
--checkbox-bg-checked: #ff1a1a; /* rouge piment */
|
||||
--checkbox-checkmark-border-color: #f5f5f5; /* blanc cassé */
|
||||
--checkbox-pulse-color: rgba(255, 26, 26, 0.5); /* pulse rouge translucide */
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #ffffff; /* rouge vif */
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #1a1a1a; /* fond infos gris foncé */
|
||||
--video-info-text-color: #f5f5f5; /* texte clair */
|
||||
--video-info-heading-color: #ff1a1a; /* titres rouges */
|
||||
--video-info-list-color: #cccccc; /* texte liste gris clair */
|
||||
--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;
|
||||
}
|
||||
|
||||
/* Optionnel: background piments */
|
||||
body.spicy {
|
||||
background-image: url('../../assets/images/Spicy.png');
|
||||
background-size:cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
10
public/styles/theme/themeimport.css
vendored
@@ -1,10 +0,0 @@
|
||||
@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("nf-theme.css");
|
||||
@import url("drift-theme.css");
|
||||
@import url("neon-theme.css");
|
||||
@import url("songbird-theme.css");
|
||||
@import url("spicy-theme.css")
|
||||
@@ -3,7 +3,7 @@
|
||||
--background-color: #001224;
|
||||
--default-text-color: #eee;
|
||||
--subtitle-color: #007bff;
|
||||
--mp3-text-color: #000000;
|
||||
--audio-only-label-color: #000000;
|
||||
|
||||
/* Formulaire */
|
||||
--form-bg-color: #ffffff;
|
||||
@@ -15,22 +15,40 @@
|
||||
--form-button-bg-color: #007bff;
|
||||
--form-button-text-color: #ffffff;
|
||||
--form-button-bg-hover-color: #0056b3;
|
||||
--paste-button-icon-color: #ffffff;
|
||||
|
||||
/* Checkbox */
|
||||
--checkbox-bg-default: #cccccc;
|
||||
--checkbox-bg-checked: #3B82F6;
|
||||
--checkbox-unchecked-bg-color: #cccccc;
|
||||
--checkbox-checked-bg-color: #3B82F6;
|
||||
--checkbox-checkmark-border-color: #E0E0E2;
|
||||
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||
|
||||
/* Download status */
|
||||
--download-status-color: #007bff;
|
||||
|
||||
/* Progress bar */
|
||||
--progress-bar-bg-color: #cccccc;
|
||||
--progress-bar-fill-color: #007bff;
|
||||
|
||||
/* Video Info Box */
|
||||
--infos-box-color: #f0f0f0;
|
||||
--video-info-bg-color: #f0f0f0;
|
||||
--video-info-text-color: #222222;
|
||||
--video-info-heading-color: #007bff;
|
||||
--video-info-list-color: #444444;
|
||||
--video-info-list-strong-color: #000000;
|
||||
--video-info-link-color: #007bff;
|
||||
--video-info-link-hover-color: #0056b3;
|
||||
|
||||
/* Playlist */
|
||||
--playlist-background-color: #4f4f4f;
|
||||
|
||||
/* Settings */
|
||||
--settings-button-bg-color: var(--form-bg-color);
|
||||
--settings-button-text-color: var(--default-text-color);
|
||||
|
||||
--settings-modal-bg-color: var(--form-bg-color);
|
||||
--settings-section-bg-color: var(--form-bg-color);
|
||||
--settings-text-color: var(--default-text-color);
|
||||
--settings-subtitle-color: #aaa;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const { fetchDownload } = require("../services/download.services");
|
||||
const { fetchDownload, cancelDownload } = require("../services/download.services");
|
||||
const { logger } = require("../logger");
|
||||
const { isValidUrl, isSafePath } = require("../helpers/validation");
|
||||
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
||||
const { notifyDownloadFinished } = require("../helpers/notify");
|
||||
const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
|
||||
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
|
||||
const { configFeatures } = require("../../config");
|
||||
|
||||
const listeners = [];
|
||||
const speedListeners = [];
|
||||
const stageListeners = [];
|
||||
const playlistInfoListeners = [];
|
||||
|
||||
async function downloadController(req, res) {
|
||||
try {
|
||||
@@ -14,18 +16,23 @@ async function downloadController(req, res) {
|
||||
audioOnly: req.body.audioOnly === "1",
|
||||
quality: req.body.quality || "best",
|
||||
outputFolder: req.body.savePath || undefined,
|
||||
playlistTitle: req.body.playlistTitle || undefined,
|
||||
};
|
||||
|
||||
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !");
|
||||
if (options.outputFolder && !isSafePath(options.outputFolder)) return res.status(400).send("❌ Save Path Not Allowed.");
|
||||
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.");
|
||||
}
|
||||
|
||||
const filePath = await fetchDownload(options, listeners, speedListeners);
|
||||
notifyDownloadFinished(filePath);
|
||||
res.send("✅ Download Done !");
|
||||
// Get output folder when the download is finished
|
||||
const outputFolder = await fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners);
|
||||
notifyDownloadFinished(outputFolder, configFeatures.notifySystem);
|
||||
res.send("Download Done !");
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`Server Error in /download : ${err.message}`);
|
||||
res.status(500).send(`❌ ${err.message}`);
|
||||
res.status(500).send(`Server Error`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,5 +68,47 @@ function speedController(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
function stageController(req, res) {
|
||||
res.set({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
});
|
||||
|
||||
module.exports = { downloadController, progressController, speedController};
|
||||
const sendStage = stage => res.write(`data: ${stage}\n\n`);
|
||||
stageListeners.push(sendStage);
|
||||
|
||||
req.on('close', () => {
|
||||
stageListeners.splice(stageListeners.indexOf(sendStage), 1);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
function cancelDownloadController(req, res) {
|
||||
const cancelled = cancelDownload();
|
||||
if (cancelled) {
|
||||
logger.info("Download and queue cancelled by user");
|
||||
res.send("Download stopped! Queue cleared.");
|
||||
} else {
|
||||
logger.warn("No download to cancel");
|
||||
res.status(400).send("No download in progress !");
|
||||
}
|
||||
}
|
||||
|
||||
function playlistInfoController(req, res) {
|
||||
res.set({
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
});
|
||||
|
||||
const sendPlaylistInfo = info => res.write(`data: ${info}\n\n`);
|
||||
playlistInfoListeners.push(sendPlaylistInfo);
|
||||
|
||||
req.on('close', () => {
|
||||
playlistInfoListeners.splice(playlistInfoListeners.indexOf(sendPlaylistInfo), 1);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { downloadController, progressController, speedController, stageController, cancelDownloadController, playlistInfoController};
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const { fetchInfo } = require("../services/info.services");
|
||||
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo");
|
||||
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
|
||||
const { logger } = require("../logger");
|
||||
const { isValidUrl } = require("../helpers/validation");
|
||||
const { isValidUrl } = require("../helpers/validation.helpers");
|
||||
|
||||
async function infoController(req, res){
|
||||
|
||||
const url = req.body.url || req.query.url; // Gérer POST et GET
|
||||
const url = req.body.url || req.query.url; // POST et GET
|
||||
|
||||
|
||||
/* Si pas d'url ou url invalide*/
|
||||
if (!url || !isValidUrl(url)) return res.status(400).send("❌ Invalid URL Or Missing");
|
||||
// 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")) {
|
||||
if (url.includes("&list") || url.includes("?list")) {
|
||||
logger.info("Estimated Data Type: Playlist")
|
||||
|
||||
} else{
|
||||
@@ -24,7 +22,32 @@ async function infoController(req, res){
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetchInfo(url);
|
||||
let data;
|
||||
let fetchError = null;
|
||||
|
||||
try {
|
||||
data = await fetchInfo(url);
|
||||
} catch (err) {
|
||||
fetchError = err;
|
||||
|
||||
// If it's a playlist URL and fetch failed, try fetching just the single video
|
||||
if ((url.includes("&list") || url.includes("?list"))) {
|
||||
logger.info(`Playlist fetch failed: ${err.message}. Retrying with single video...`);
|
||||
|
||||
// Remove the &list parameter
|
||||
const singleVideoUrl = url.split("&list")[0].split("?list")[0];
|
||||
|
||||
try {
|
||||
data = await fetchInfo(singleVideoUrl);
|
||||
logger.info(`Single video fetch succeeded after playlist failure`);
|
||||
} catch (retryErr) {
|
||||
logger.error(`Single video fetch also failed: ${retryErr.message}`);
|
||||
throw err; // Throw the original error
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (data._type === "playlist") {
|
||||
|
||||
@@ -40,7 +63,8 @@ async function infoController(req, res){
|
||||
|
||||
|
||||
} catch (err) {
|
||||
return res.status(500).send(`❌ ${err.message}`);
|
||||
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
|
||||
return res.status(500).send(`Unable to fetch info`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
85
server/helpers/buildArgs.helpers.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const path = require("path");
|
||||
|
||||
const getUserBrowser = require("./getBrowser.helpers.js");
|
||||
const { ffmpegPath, denoPath} = require("./path.helpers.js");
|
||||
const { configFeatures } = require("../../config.js");
|
||||
const { logger } = require("../logger.js");
|
||||
|
||||
function validateCodec(codec){
|
||||
|
||||
const validCodec = [
|
||||
"av01","vp9.2", "vp9" , "h265" , "h264" , "vp8" , "h263", "theora"
|
||||
]
|
||||
|
||||
if(validCodec.includes(codec)){
|
||||
logger.info(`Codec Valid: ${codec}`)
|
||||
return codec
|
||||
} else{
|
||||
logger.error(`Codec not valid: ${codec}. Using default codec`)
|
||||
return "h264"
|
||||
}
|
||||
}
|
||||
|
||||
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
|
||||
logger.info("--- CONFIGURATION ---");
|
||||
|
||||
logger.info(`CONFIG autoUpdate: ${configFeatures.autoUpdate}`);
|
||||
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
||||
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
||||
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
||||
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
||||
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
||||
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
||||
logger.info(`CONFIG autoDownloadPlaylist: ${configFeatures.autoDownloadPlaylist}`);
|
||||
logger.info(`CONFIG customCodec: ${configFeatures.customCodec}`);
|
||||
|
||||
// ACTIVATE THEM WHEN THE FEATURE IS IMPLEMENTED
|
||||
// logger.info(`CONFIG logSystem: ${configFeatures.logSystem}`);
|
||||
// logger.info(`CONFIG outputTitleCheck: ${configFeatures.outputTitleCheck}`);
|
||||
// logger.info(`CONFIG downloadSystem: ${configFeatures.downloadSystem}`);
|
||||
// logger.info(`CONFIG notifySystem: ${configFeatures.notifySystem}`);
|
||||
|
||||
logger.info("---------------------");
|
||||
|
||||
const args = [
|
||||
configFeatures.verboseLogs ? "--verbose" : null, // Verbose Logs
|
||||
"--cookies-from-browser", `${getUserBrowser()}`,
|
||||
"--no-continue",
|
||||
"--no-overwrites",
|
||||
"--newline", // YT-DLP Logs Format
|
||||
configFeatures.addThumbnail ? "--embed-thumbnail" : null,
|
||||
configFeatures.addMetadata ? "--add-metadata" : null,
|
||||
"--concurrent-fragments", "8",
|
||||
"--retries", "10",
|
||||
"--fragment-retries", "10",
|
||||
"--ffmpeg-location", ffmpegPath,
|
||||
"--extractor-args","youtube:player_client=default",
|
||||
"--js-runtimes", `deno:${denoPath}`,
|
||||
"-S",
|
||||
`vcodec:${validateCodec(configFeatures.customCodec) || "h264"}`+
|
||||
`,acodec:aac`,
|
||||
configFeatures.autoDownloadPlaylist ? "--yes-playlist" : "--no-playlist"
|
||||
];
|
||||
|
||||
if (audioOnly) args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3");
|
||||
else args.push("--merge-output", "mp4");
|
||||
|
||||
const qualityMap = {
|
||||
best: "bestvideo+bestaudio/best/mp4",
|
||||
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
worst: "worstvideo+worstaudio/worst/mp4",
|
||||
1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]/mp4",
|
||||
720: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
480: "bestvideo[height<=480]+bestaudio/best[height<=480]/mp4",
|
||||
};
|
||||
|
||||
args.push("-f", qualityMap[quality] || "best");
|
||||
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
|
||||
args.push(url);
|
||||
|
||||
return args.filter(Boolean);
|
||||
}
|
||||
|
||||
module.exports = { buildYtDlpArgs };
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const getUserBrowser = require("./getBrowser");
|
||||
const { ffmpegPath, denoPath} = require("./path");
|
||||
|
||||
|
||||
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
|
||||
const args = [
|
||||
"--cookies-from-browser", `${getUserBrowser()}`,
|
||||
"--no-continue",
|
||||
"--no-overwrites",
|
||||
"--embed-thumbnail",
|
||||
"--add-metadata",
|
||||
"--concurrent-fragments", "8",
|
||||
"--retries", "10",
|
||||
"--fragment-retries", "10",
|
||||
"--ffmpeg-location", ffmpegPath,
|
||||
"--extractor-args","youtube:player_client=default",
|
||||
"--js-runtimes", `deno:${denoPath}`
|
||||
];
|
||||
|
||||
if (audioOnly) args.push("-f", "bestaudio", "--extract-audio", "--audio-format", "mp3");
|
||||
else args.push("--merge-output", "mp4");
|
||||
|
||||
const qualityMap = {
|
||||
best: "bestvideo+bestaudio/best/mp4",
|
||||
medium: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
worst: "worstvideo+worstaudio/worst/mp4",
|
||||
1080: "bestvideo[height<=1080]+bestaudio/best[height<=1080]/mp4",
|
||||
720: "bestvideo[height<=720]+bestaudio/best[height<=720]/mp4",
|
||||
480: "bestvideo[height<=480]+bestaudio/best[height<=480]/mp4",
|
||||
};
|
||||
|
||||
args.push("-f", qualityMap[quality] || "best");
|
||||
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
|
||||
args.push(url);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
module.exports = { buildYtDlpArgs };
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const notify = require("./notify")
|
||||
const notify = require("./notify.helpers")
|
||||
const { logger } = require("../logger");
|
||||
|
||||
function getUserBrowser() {
|
||||
const userProfile = os.homedir();
|
||||
|
||||
// Liste des navigateurs supportés par yt-dlp (Actuellement je peux que garantir Firefox, Désolé)
|
||||
// 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") },
|
||||
@@ -18,15 +19,21 @@ function getUserBrowser() {
|
||||
// { 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;
|
||||
} else{
|
||||
}
|
||||
}
|
||||
|
||||
// No browser found - notify user
|
||||
logger.warn("No supported browser found on the system");
|
||||
notify.notifyFirefoxBrowserMissing();
|
||||
}
|
||||
}
|
||||
notify.notifyCookiesBrowserError()
|
||||
return ;
|
||||
|
||||
// Fallback: return "Firefox" to let YT-DLP manage error
|
||||
// this avoid app crash
|
||||
return "firefox";
|
||||
}
|
||||
|
||||
module.exports = getUserBrowser
|
||||
@@ -1,12 +1,13 @@
|
||||
const { Notification, shell } = require("electron");
|
||||
const path = require("path");
|
||||
const { iconPaths } = require("./path.helpers");
|
||||
|
||||
function notifyDownloadFinished(folder, notifyEnabled = true) {
|
||||
if (!notifyEnabled) return;
|
||||
|
||||
function notifyDownloadFinished(folder) {
|
||||
const iconPath = path.join(process.resourcesPath, "confirm-icon.png");
|
||||
const notif = new Notification({
|
||||
title: "Freedom Loader",
|
||||
body: "Your download is complete, click here to open it.",
|
||||
icon: iconPath,
|
||||
icon: iconPaths.confirm,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openPath(folder));
|
||||
@@ -14,11 +15,10 @@ function notifyDownloadFinished(folder) {
|
||||
}
|
||||
|
||||
function notifyCookiesBrowserError(){
|
||||
const iconPath = path.join(process.resourcesPath, "error.png");
|
||||
const notif = new Notification({
|
||||
title: "Cookies Error",
|
||||
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
||||
icon: iconPath,
|
||||
icon: iconPaths.error,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
|
||||
@@ -26,11 +26,10 @@ function notifyCookiesBrowserError(){
|
||||
}
|
||||
|
||||
function notifyFirefoxBrowserMissing(){
|
||||
const iconPath = path.join(process.ressourcesPath, "error.png");
|
||||
const notif = new Notification({
|
||||
title: "Firefox Missing",
|
||||
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
||||
icon: iconPath,
|
||||
icon: iconPaths.error,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))
|
||||
@@ -2,12 +2,6 @@ function parseVideo(data) {
|
||||
|
||||
return {
|
||||
type: "video",
|
||||
// id: data.id,
|
||||
// title: data.title,
|
||||
// url: data.webpage_url,
|
||||
// duration: data.duration,
|
||||
// thumbnail: data.thumbnail,
|
||||
// uploader: data.uploader
|
||||
...data
|
||||
};
|
||||
}
|
||||
123
server/helpers/path.helpers.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const { app } = require("electron");
|
||||
const config = require("../../config.js");
|
||||
|
||||
const { logger } = require("../logger.js");
|
||||
|
||||
// Centralized resource paths
|
||||
const resourcesPath = config.devMode
|
||||
? path.join(__dirname, "../../ressources")
|
||||
: process.resourcesPath;
|
||||
|
||||
// Default download folder (centralized)
|
||||
const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
||||
|
||||
// Binary paths
|
||||
let userYtDlp;
|
||||
let ffmpegPath;
|
||||
let denoPath;
|
||||
|
||||
const sourceYtDlp = path.join(resourcesPath, "binaries","yt-dlp.exe");
|
||||
|
||||
if (config.devMode) {
|
||||
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
|
||||
ffmpegPath = path.join(__dirname, "../../ressources/"); // <- has ffmpeg.exe and ffprobe.exe
|
||||
denoPath = path.join(__dirname, "../../ressources/deno.exe");
|
||||
} else {
|
||||
userYtDlp = path.join(app.getPath("userData"),"yt-dlp.exe");
|
||||
ffmpegPath = path.join(resourcesPath, "binaries","ffmpeg.exe");
|
||||
denoPath = path.join(resourcesPath, "binaries","deno.exe");
|
||||
|
||||
if (!fs.existsSync(userYtDlp)) {
|
||||
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||
}
|
||||
}
|
||||
|
||||
// Notification icon paths
|
||||
const iconPaths = {
|
||||
confirm: path.join(resourcesPath, "confirm-icon.png"),
|
||||
error: path.join(resourcesPath, "error.png"),
|
||||
app: path.join(resourcesPath, "app-icon.ico")
|
||||
};
|
||||
|
||||
// Binary paths for verification
|
||||
const binaryPaths = {
|
||||
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
|
||||
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
|
||||
ffprobe: path.join(resourcesPath, "binaries", "ffprobe.exe"),
|
||||
deno: path.join(resourcesPath, "binaries", "deno.exe")
|
||||
};
|
||||
|
||||
// Theme paths - themes stored in AppData to survive updates
|
||||
const userThemesPath = path.join(app.getPath("userData"), "themes");
|
||||
const defaultThemesSourcePath = path.join(resourcesPath, "theme");
|
||||
|
||||
function initUserThemes() {
|
||||
try {
|
||||
if (!fs.existsSync(userThemesPath)) {
|
||||
fs.mkdirSync(userThemesPath, { recursive: true });
|
||||
// Ensure folder is writable
|
||||
fs.chmodSync(userThemesPath, 0o777);
|
||||
logger.info("Created user themes directory");
|
||||
}
|
||||
|
||||
// Copy default themes from resources to user directory if they don't exist
|
||||
if (fs.existsSync(defaultThemesSourcePath)) {
|
||||
const defaultThemes = fs.readdirSync(defaultThemesSourcePath);
|
||||
for (const theme of defaultThemes) {
|
||||
const srcPath = path.join(defaultThemesSourcePath, theme);
|
||||
const destPath = path.join(userThemesPath, theme);
|
||||
|
||||
if (!fs.existsSync(destPath)) {
|
||||
try {
|
||||
copyDirectory(srcPath, destPath);
|
||||
fs.chmodSync(destPath, 0o777);
|
||||
logger.info(`Copied default theme: ${theme}`);
|
||||
} catch (copyErr) {
|
||||
logger.warn(`Failed to copy theme ${theme}: ${copyErr.message}. Theme folder will be created, add theme files manually.`);
|
||||
try {
|
||||
fs.mkdirSync(destPath, { recursive: true });
|
||||
fs.chmodSync(destPath, 0o777);
|
||||
} catch (mkdirErr) {
|
||||
logger.warn(`Could not create theme directory ${theme}: ${mkdirErr.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Default themes source path not found: ${defaultThemesSourcePath}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed to initialize user themes: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function copyDirectory(src, dest) {
|
||||
try {
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.mkdirSync(dest, { recursive: true, mode: 0o777 });
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(src);
|
||||
for (const file of files) {
|
||||
const srcFile = path.join(src, file);
|
||||
const destFile = path.join(dest, file);
|
||||
|
||||
if (fs.statSync(srcFile).isDirectory()) {
|
||||
copyDirectory(srcFile, destFile);
|
||||
} else {
|
||||
fs.copyFileSync(srcFile, destFile);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Copy error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!userYtDlp){ logger.error("Missing YT-DLP")}
|
||||
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
|
||||
if (!denoPath){ logger.error("Missing DENO")}
|
||||
|
||||
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes };
|
||||
@@ -1,26 +0,0 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { app } = require("electron");
|
||||
const config = require("../../config");
|
||||
|
||||
let userYtDlp;
|
||||
let ffmpegPath;
|
||||
let denoPath;
|
||||
|
||||
const sourceYtDlp = path.join(process.resourcesPath, "yt-dlp.exe");
|
||||
|
||||
if (config.localMode) {
|
||||
userYtDlp = path.join(__dirname, "../../ressources/yt-dlp.exe");
|
||||
ffmpegPath = path.join(__dirname, "../../ressources"); // <- contient ffmpeg.exe et ffprobe.exe
|
||||
denoPath = path.join(__dirname, "../../ressources/deno.exe");
|
||||
} else {
|
||||
userYtDlp = path.join(app.getPath("userData"), "yt-dlp.exe");
|
||||
ffmpegPath = path.join(process.resourcesPath, "ffmpeg.exe");
|
||||
denoPath = path.join(process.resourcesPath, "deno.exe");
|
||||
|
||||
if (!fs.existsSync(userYtDlp)) {
|
||||
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { userYtDlp, ffmpegPath, denoPath };
|
||||
14
server/helpers/rateLimit.helpers.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
|
||||
}
|
||||
@@ -13,7 +13,7 @@ function isValidUrl(url) {
|
||||
|
||||
function isSafePath(folder) {
|
||||
if (!folder || folder.length < 3) return false;
|
||||
const unsafe = ["System32", "/etc", "\\Windows"];
|
||||
const unsafe = ["System32", "\\Windows"];
|
||||
const resolved = path.resolve(folder);
|
||||
return !unsafe.some(u => resolved.includes(u));
|
||||
}
|
||||
@@ -3,20 +3,24 @@ const DailyRotateFile = require("winston-daily-rotate-file");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const config = require("../config")
|
||||
const config = require("../config");
|
||||
|
||||
// Dossier de logs Windows
|
||||
// Logs folder in Windows
|
||||
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||
|
||||
// Création du dossier "logs" si nécessaire
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
// Create "Logs" folder if needed
|
||||
try {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error(`Failed to create log directory: ${error.message}`);
|
||||
}
|
||||
|
||||
const logFormat = format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||
);
|
||||
|
||||
// Configuration du logger
|
||||
// Logger configuration
|
||||
const logger = createLogger({
|
||||
level: "info",
|
||||
format: logFormat,
|
||||
@@ -36,21 +40,13 @@ const logger = createLogger({
|
||||
],
|
||||
});
|
||||
|
||||
function getSessionStartLine() {
|
||||
return `--- Starting session: ${new Date().toISOString()} ---`;
|
||||
}
|
||||
|
||||
function getSessionEndLine() {
|
||||
return `--- Ending session: ${new Date().toISOString()} ---`;
|
||||
}
|
||||
|
||||
function logSessionStart() {
|
||||
logger.info(getSessionStartLine());
|
||||
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
|
||||
logger.info(`Application Version: ${config.version}`)
|
||||
}
|
||||
|
||||
function logSessionEnd() {
|
||||
logger.info(getSessionEndLine());
|
||||
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { downloadController, progressController, speedController } = require("../controller/download.controller");
|
||||
const { downloadController, progressController, speedController, stageController, cancelDownloadController, playlistInfoController } = require("../controller/download.controller");
|
||||
|
||||
router.post("/", downloadController);
|
||||
router.post("/cancel", cancelDownloadController);
|
||||
|
||||
// SSE pour la progression du téléchargement
|
||||
// SSE for download progress
|
||||
router.get("/progress", progressController);
|
||||
router.get("/speed", speedController);
|
||||
router.get("/stage", stageController);
|
||||
router.get("/playlist-info", playlistInfoController);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,47 +1,23 @@
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { logger, logSessionStart, logSessionEnd } = require("./logger");
|
||||
const { logger, logSessionEnd } = require("./logger");
|
||||
const config = require("../config");
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp } = require("./helpers/path");
|
||||
|
||||
const { rateLimite } = require("./helpers/rateLimit.helpers");
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Dossier de téléchargement
|
||||
const outputFolder = path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||
|
||||
// Création du dossier si nécessaire
|
||||
try {
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
logger.info(`Freedom Loader folder ready in ${outputFolder}`);
|
||||
} catch (err) {
|
||||
logger.error("Unable to create folder:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Mise à jour yt-dlp au
|
||||
execFile(userYtDlp, ["-U"], (err, stdout, stderr) => {
|
||||
if (err) logger.warn("yt-dlp update error:", err);
|
||||
else logger.info(`Update yt-dlp : ${stdout}`);
|
||||
});
|
||||
|
||||
// Middlewares
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static(path.join(__dirname, "../public")));
|
||||
|
||||
// Routes
|
||||
app.use("/download", require("./routes/download.route"));
|
||||
app.use("/info", require("./routes/info.route"));
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
app.get("/", rateLimite, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||
});
|
||||
|
||||
// Fonction pour démarrer le serveur
|
||||
async function startServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = app.listen(config.applicationPort, () => {
|
||||
@@ -50,14 +26,14 @@ async function startServer() {
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
logger.error("Express Server Error :", err);
|
||||
logger.error("Express server error:", err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
const gracefulExit = () => {
|
||||
logSessionEnd();
|
||||
server.close(() => {
|
||||
logger.info("Express Server close cleanly.");
|
||||
logger.info("Express server closed cleanly.");
|
||||
process.exit();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,27 +1,132 @@
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp } = require("../helpers/path");
|
||||
const path = require("path");
|
||||
const { userYtDlp, defaultDownloadFolder } = require("../helpers/path.helpers");
|
||||
const fs = require("fs");
|
||||
const { logger } = require("../logger");
|
||||
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
||||
const notify = require("../helpers/notify")
|
||||
const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
|
||||
const notify = require("../helpers/notify.helpers");
|
||||
const path = require("path");
|
||||
const { isSafePath } = require("../helpers/validation.helpers");
|
||||
const { configFeatures } = require("../../config.js");
|
||||
|
||||
function fetchDownload(options, listeners, speedListeners) {
|
||||
let currentDownloadProcess = null;
|
||||
|
||||
// Détecte si une URL est une playlist
|
||||
function isPlaylistUrl(url) {
|
||||
return url.includes("?list=") || url.includes("&list=");
|
||||
}
|
||||
|
||||
function createPlaylistFolder(basePath, playlistTitle) {
|
||||
const sanitizedTitle = playlistTitle
|
||||
.replace(/[<>:"|?*]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.substring(0, 200);
|
||||
|
||||
const playlistPath = path.join(basePath, sanitizedTitle);
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(playlistPath)) {
|
||||
fs.mkdirSync(playlistPath, { recursive: true });
|
||||
logger.info(`Playlist folder created: ${playlistPath}`);
|
||||
return playlistPath;
|
||||
}
|
||||
|
||||
let counter = 1;
|
||||
let newPath;
|
||||
|
||||
while (counter <= 1000) {
|
||||
newPath = path.join(basePath, `${sanitizedTitle} ${counter}`);
|
||||
if (!fs.existsSync(newPath)) {
|
||||
fs.mkdirSync(newPath, { recursive: true });
|
||||
logger.info(`Playlist folder created with increment: ${newPath}`);
|
||||
return newPath;
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
|
||||
logger.error(`Could not find available playlist folder after 1000 attempts`);
|
||||
throw new Error("Unable to create playlist folder");
|
||||
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
|
||||
|
||||
// Fallback: create folder "Untitled Playlist X"
|
||||
let counter = 1;
|
||||
let fallbackPath;
|
||||
|
||||
while (counter <= 1000) {
|
||||
fallbackPath = path.join(basePath, `Untitled Playlist ${counter}`);
|
||||
if (!fs.existsSync(fallbackPath)) {
|
||||
try {
|
||||
fs.mkdirSync(fallbackPath, { recursive: true });
|
||||
logger.info(`Fallback playlist folder created: ${fallbackPath}`);
|
||||
return fallbackPath;
|
||||
} catch (mkErr) {
|
||||
counter++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
|
||||
logger.error(`Could not create playlist folder after 1000 attempts`);
|
||||
throw new Error("Unable to create playlist folder");
|
||||
}
|
||||
}
|
||||
|
||||
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
||||
fs.mkdirSync(outputFolder, { recursive: true });
|
||||
logger.info(`CONFIG createPlaylistFolders: ${configFeatures.createPlaylistFolders}`);
|
||||
|
||||
const args = buildYtDlpArgs({ ...options, outputFolder });
|
||||
let outputFolder = options.outputFolder || defaultDownloadFolder;
|
||||
|
||||
// Normalize path and validate it's safe (within Users folder)
|
||||
let safeOutputFolder = path.resolve(outputFolder);
|
||||
|
||||
if (!isSafePath(safeOutputFolder)) {
|
||||
logger.warn(`Path not allowed, using default instead: ${safeOutputFolder}`);
|
||||
safeOutputFolder = path.resolve(defaultDownloadFolder);
|
||||
}
|
||||
|
||||
// Create download folder if it doesn't exist
|
||||
try {
|
||||
fs.mkdirSync(safeOutputFolder, { recursive: true });
|
||||
logger.info(`Output folder ready: ${safeOutputFolder}`);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to create output folder: ${err.message}`);
|
||||
return reject(new Error(`Unable to create download folder: ${err.message}`));
|
||||
}
|
||||
|
||||
// Détecte si c'est une playlist et crée un dossier approprié
|
||||
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url);
|
||||
|
||||
if (isPlaylist && configFeatures.createPlaylistFolders) {
|
||||
try {
|
||||
const playlistName = options.playlistTitle || "Untitled Playlist";
|
||||
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
|
||||
logger.info(`Playlist detected, using folder: ${safeOutputFolder}`);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to create playlist folder: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
} else if (isPlaylist && !configFeatures.createPlaylistFolders) {
|
||||
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
|
||||
}
|
||||
|
||||
const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder });
|
||||
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||
|
||||
const child = execFile(userYtDlp, args);
|
||||
currentDownloadProcess = child;
|
||||
|
||||
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
|
||||
|
||||
child.on("close", code => {
|
||||
currentDownloadProcess = null;
|
||||
listeners.forEach(fn => fn("done"));
|
||||
if (code === 0) resolve(outputFolder);
|
||||
if (code === 0) resolve(safeOutputFolder);
|
||||
else if (code === null) reject(new Error(`Download cancelled by user`));
|
||||
else reject(new Error(`YT-DLP failed with code : ${code}`));
|
||||
});
|
||||
|
||||
@@ -31,8 +136,11 @@ function fetchDownload(options, listeners, speedListeners) {
|
||||
if (!line.trim()) return;
|
||||
logger.info(`[yt-dlp] ${line}`);
|
||||
|
||||
/* Barre de Chargement*/
|
||||
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
|
||||
// Progress Bar
|
||||
if (line.startsWith("[download] Destination:")) {
|
||||
listeners.forEach(fn => fn("reset"));
|
||||
stageListeners.forEach(fn => fn("Downloading..."));
|
||||
}
|
||||
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
||||
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
|
||||
|
||||
@@ -44,6 +152,30 @@ function fetchDownload(options, listeners, speedListeners) {
|
||||
}
|
||||
}
|
||||
|
||||
// Playlist item tracking
|
||||
const itemMatch = line.match(/Downloading\s+(?:item|video)?\s*(\d+)\s+of\s+(\d+)/i);
|
||||
if (itemMatch) {
|
||||
const itemInfo = `Item ${itemMatch[1]} of ${itemMatch[2]}`;
|
||||
playlistInfoListeners.forEach(fn => fn(itemInfo));
|
||||
}
|
||||
|
||||
// Stage messages
|
||||
if (line.includes("[Merger]")) {
|
||||
stageListeners.forEach(fn => fn("Merging formats..."));
|
||||
}
|
||||
if (line.includes("[ExtractAudio]")) {
|
||||
stageListeners.forEach(fn => fn("Extracting audio..."));
|
||||
}
|
||||
if (line.includes("[Metadata]")) {
|
||||
stageListeners.forEach(fn => fn("Adding metadata..."));
|
||||
}
|
||||
if (line.includes("[ThumbnailsConvertor]")) {
|
||||
stageListeners.forEach(fn => fn("Converting thumbnail..."));
|
||||
}
|
||||
if (line.includes("[EmbedThumbnail]")) {
|
||||
stageListeners.forEach(fn => fn("Embedding thumbnail..."));
|
||||
}
|
||||
|
||||
if (line.match(/ERROR: Could not copy .* cookie database/i)) {
|
||||
notify.notifyCookiesBrowserError();
|
||||
}
|
||||
@@ -59,4 +191,31 @@ function fetchDownload(options, listeners, speedListeners) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { fetchDownload };
|
||||
function cancelDownload() {
|
||||
if (currentDownloadProcess) {
|
||||
logger.info("Cancelling download and killing all child processes...");
|
||||
|
||||
// Force kill the process and all its children with SIGKILL
|
||||
try {
|
||||
// Try to kill with SIGKILL on Windows (process group) or Unix
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows, kill the process tree
|
||||
require('child_process').execSync(`taskkill /PID ${currentDownloadProcess.pid} /T /F`, { stdio: 'ignore' });
|
||||
} else {
|
||||
// On Unix, send SIGKILL to process group
|
||||
process.kill(-currentDownloadProcess.pid, 'SIGKILL');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(`Error killing process: ${err.message}`);
|
||||
// Fallback to regular kill
|
||||
currentDownloadProcess.kill('SIGKILL');
|
||||
}
|
||||
|
||||
currentDownloadProcess = null;
|
||||
logger.info("Download cancelled successfully");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = { fetchDownload, cancelDownload };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp, denoPath } = require("../helpers/path");
|
||||
const getUserBrowser = require("../helpers/getBrowser");
|
||||
const { userYtDlp, denoPath } = require("../helpers/path.helpers");
|
||||
const getUserBrowser = require("../helpers/getBrowser.helpers");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
function fetchInfo(url) {
|
||||
@@ -17,8 +17,8 @@ function fetchInfo(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
execFile(userYtDlp,[...args, url],
|
||||
{ timeout: 30_000, // 30s, si jamais plus, abandon de la requête
|
||||
maxBuffer: 10 * 1024 * 1024 }, // 10MO de réponse max (Par défaut: 200ko)
|
||||
{ timeout: 30_000, // 30s, if more timeout
|
||||
maxBuffer: 10 * 1024 * 1024 }, // 10MO max response (Default: 200ko)
|
||||
|
||||
(error, stdout, stderr) => {
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
const { app, Notification } = require("electron");
|
||||
const { logger } = require("./logger");
|
||||
|
||||
function AutoUpdater() {
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
logger.info(`New Version Available : ${info.version}`);
|
||||
new Notification({
|
||||
title: "Freedom Loader",
|
||||
body: `New Version Available : ${info.version}. Application will restart`
|
||||
}).show();
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
logger.info(`Update Downloaded : ${info.version}`);
|
||||
new Notification({
|
||||
title: "Freedom Loader",
|
||||
body: `Update ${info.version} downloaded.`
|
||||
}).on("click", () =>
|
||||
shell.openExternal("https://github.com/MasterAcnolo/Freedom-Loader/releases/latest")
|
||||
).show();
|
||||
|
||||
setTimeout(() => {
|
||||
autoUpdater.quitAndInstall();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (err) => {
|
||||
logger.error("Auto Update Error :", err.message);
|
||||
new Notification({
|
||||
title: "Freedom Loader - Update Error",
|
||||
body: err.message
|
||||
}).show();
|
||||
});
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
try {
|
||||
await autoUpdater.checkForUpdates();
|
||||
logger.info("Update check completed");
|
||||
} catch (err) {
|
||||
logger.error("Error during update check :", err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { AutoUpdater };
|
||||
96
theme/template.theme.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "",
|
||||
"author": "",
|
||||
"version": "1.0.0",
|
||||
"formatVersion": "1.0"
|
||||
},
|
||||
|
||||
"style": {
|
||||
"background": {
|
||||
"image": "",
|
||||
"size": "cover",
|
||||
"position": "center",
|
||||
"attachment": "fixed"
|
||||
},
|
||||
|
||||
"colors": {
|
||||
"background": "",
|
||||
"text": {
|
||||
"default": "",
|
||||
"subtitle": "",
|
||||
"audioOnly": ""
|
||||
}
|
||||
},
|
||||
|
||||
"form": {
|
||||
"background": "",
|
||||
"input": {
|
||||
"background": "",
|
||||
"border": "",
|
||||
"borderFocus": "",
|
||||
"text": "",
|
||||
"placeholder": ""
|
||||
},
|
||||
"button": {
|
||||
"background": "",
|
||||
"text": "",
|
||||
"hover": ""
|
||||
},
|
||||
"pasteButtonIcon": ""
|
||||
},
|
||||
|
||||
"checkbox": {
|
||||
"background": {
|
||||
"unchecked": "",
|
||||
"checked": ""
|
||||
},
|
||||
"checkmarkBorder": "",
|
||||
"pulse": ""
|
||||
},
|
||||
|
||||
"download": {
|
||||
"status": ""
|
||||
},
|
||||
|
||||
"progressBar": {
|
||||
"background": "",
|
||||
"fill": ""
|
||||
},
|
||||
|
||||
"videoInfo": {
|
||||
"background": "",
|
||||
"text": "",
|
||||
"heading": "",
|
||||
"list": "",
|
||||
"strong": "",
|
||||
"link": "",
|
||||
"linkHover": ""
|
||||
},
|
||||
|
||||
"playlist": {
|
||||
"background": ""
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"button": {
|
||||
"background": "",
|
||||
"text": ""
|
||||
},
|
||||
"openThemeButton": {
|
||||
"background": "",
|
||||
"text": ""
|
||||
},
|
||||
"openJsonButton": {
|
||||
"background": "",
|
||||
"text": ""
|
||||
},
|
||||
"background": {
|
||||
"modal": "",
|
||||
"section": ""
|
||||
},
|
||||
"text": "",
|
||||
"subtitle": ""
|
||||
}
|
||||
}
|
||||
}
|
||||