mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-07-29 18:25:47 +02:00
Compare commits
101 Commits
1.4.3
...
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 |
4
.github/ISSUE_TEMPLATE/question.md
vendored
4
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -9,8 +9,8 @@ assignees: 'MasterAcnolo'
|
|||||||
|
|
||||||
## Pre-submission Checklist
|
## Pre-submission Checklist
|
||||||
|
|
||||||
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/FreedomLoader/pages/faq.html)
|
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/pages/faq)
|
||||||
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/pages/wiki)
|
||||||
- [ ] I have searched existing issues
|
- [ ] I have searched existing issues
|
||||||
|
|
||||||
## Question
|
## Question
|
||||||
|
|||||||
2
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
2
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
@@ -7,4 +7,4 @@
|
|||||||
## Found a bug or issue?
|
## Found a bug or issue?
|
||||||
- Please report it in the GitHub Issues section
|
- Please report it in the GitHub Issues section
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.4.0...1.4.1
|
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/1.W.X...1.Y.Z
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,5 +7,9 @@
|
|||||||
/ressources/ffprobe.exe
|
/ressources/ffprobe.exe
|
||||||
/ressources/deno.exe
|
/ressources/deno.exe
|
||||||
|
|
||||||
|
/theme/**
|
||||||
|
|
||||||
|
config/config.dev.json
|
||||||
|
|
||||||
logs/*.json
|
logs/*.json
|
||||||
logs/*.log
|
logs/*.log
|
||||||
@@ -148,8 +148,8 @@ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
|
|||||||
|
|
||||||
## Questions?
|
## Questions?
|
||||||
|
|
||||||
- Check the [FAQ](https://masteracnolo.github.io/FreedomLoader/pages/faq.html)
|
- Check the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/faq)
|
||||||
- Review the [Wiki](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html)
|
- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki)
|
||||||
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
- Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
174
README.md
174
README.md
@@ -1,16 +1,17 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<a href="https://masteracnolo.github.io/FreedomLoader/"><img
|
<a href="https://masteracnolo.github.io/Freedom-Loader-Site/"><img
|
||||||
src="./build/banner.png"
|
src="./build/banner.png"
|
||||||
alt="Banner"
|
alt="Banner"
|
||||||
style="width: 50%;"/></a>
|
style="width: 50%;"/></a>
|
||||||
|
|
||||||
### **A clean, open-source multimedia downloader for Windows**
|
### **A clean, open-source multimedia downloader for Windows**
|
||||||
|
|
||||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://masteracnolo.github.io/FreedomLoader/)
|
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
||||||
<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>
|
[](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>
|
||||||
|
|
||||||
@@ -22,6 +23,11 @@ Freedom Loader is a desktop application built with Electron that provides a stra
|
|||||||
|
|
||||||
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.
|
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
|
## Table of Contents
|
||||||
|
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
@@ -30,10 +36,12 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
- [Preview](#preview)
|
- [Preview](#preview)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Project Structure](#project-structure)
|
- [Project Structure](#project-structure)
|
||||||
|
- [Theme Workshop](#Theme-Workshop)
|
||||||
- [Technology Stack](#technology-stack)
|
- [Technology Stack](#technology-stack)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Roadmap](#roadmap)
|
- [Roadmap](#roadmap)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
|
- [Submitting a community theme](#Submitting-a-community-theme)
|
||||||
- [Support](#support)
|
- [Support](#support)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
@@ -53,7 +61,9 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
### User Interface
|
### User Interface
|
||||||
|
|
||||||
- **Custom window controls** - Frameless window with custom top bar (optional)
|
- **Custom window controls** - Frameless window with custom top bar (optional)
|
||||||
- **Theme system** - Multiple pre-built themes with dynamic switching
|
- **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
|
- **Real-time progress tracking** - Live download progress bar with network speed indicator
|
||||||
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
||||||
- **Settings panel** - In-app configuration interface with live updates
|
- **Settings panel** - In-app configuration interface with live updates
|
||||||
@@ -70,6 +80,9 @@ The primary goal is to make media downloading accessible to users who want offli
|
|||||||
- **Rate limiting** - Express-based request throttling to prevent server overload
|
- **Rate limiting** - Express-based request throttling to prevent server overload
|
||||||
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
||||||
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
||||||
|
- **Splash screen** — animated loading screen on startup
|
||||||
|
- **Stop download** — ability to cancel in-progress downloads
|
||||||
|
- **Playlist folders** — dedicated folder creation per playlist
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -152,7 +165,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
|||||||
|
|
||||||
### Configuration File Location
|
### Configuration File Location
|
||||||
```
|
```
|
||||||
C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
%APPDATA%\Freedom Loader\config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Options
|
### Available Options
|
||||||
@@ -166,11 +179,13 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
|||||||
"addMetadata": true,
|
"addMetadata": true,
|
||||||
"verboseLogs": false,
|
"verboseLogs": false,
|
||||||
"autoDownloadPlaylist": true,
|
"autoDownloadPlaylist": true,
|
||||||
|
"createPlaylistFolders": true,
|
||||||
"customCodec": "h264",
|
"customCodec": "h264",
|
||||||
"logSystem": true,
|
"logSystem": true,
|
||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
"downloadSystem": true,
|
"downloadSystem": true,
|
||||||
"notifySystem": true
|
"notifySystem": true,
|
||||||
|
"theme": "dark"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -186,11 +201,13 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
|||||||
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||||
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||||
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||||
|
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
||||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||||
| `logSystem` | boolean | `true` | Enable application logging |
|
| `logSystem` | boolean | `true` | Enable application logging |
|
||||||
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
||||||
| `downloadSystem` | boolean | `true` | Enable download system |
|
| `downloadSystem` | boolean | `true` | Enable download system |
|
||||||
| `notifySystem` | boolean | `true` | Enable system notifications |
|
| `notifySystem` | boolean | `true` | Enable system notifications |
|
||||||
|
| `theme` | string | `dark` | Current Theme Name |
|
||||||
|
|
||||||
### Supported Codecs
|
### Supported Codecs
|
||||||
|
|
||||||
@@ -209,19 +226,88 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
|||||||
|
|
||||||
```
|
```
|
||||||
Freedom-Loader/
|
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
|
├── build/ # Build resources and assets
|
||||||
├── config/ # Configuration files
|
├── config/ # Configuration files
|
||||||
├── public/ # Frontend assets (HTML, CSS, JavaScript)
|
│ ├── config.default.json
|
||||||
├── ressources/ # Internal resources (icons, binaries)
|
│ └── config.dev.json
|
||||||
├── server/ # Express server code
|
├── public/ # Frontend assets and UI
|
||||||
│ ├── routes/ # API route handlers
|
│ ├── index.html
|
||||||
│ ├── services/ # Business logic
|
│ ├── splash.html
|
||||||
│ └── utils/ # Server utilities
|
│ ├── assets/
|
||||||
├── .github/ # GitHub configuration and workflows
|
│ │ ├── icon/ # Application icons
|
||||||
├── main.js # Electron main process
|
│ │ └── logo/ # Logo assets
|
||||||
├── preload.js # Electron preload script
|
│ ├── script/ # Frontend JavaScript modules
|
||||||
├── config.js # Global application configuration
|
│ │ ├── appVersion.js
|
||||||
└── package.json # Dependencies and npm scripts
|
│ │ ├── 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
|
### Architecture Overview
|
||||||
@@ -230,10 +316,25 @@ Freedom Loader uses a client-server architecture within a single Electron applic
|
|||||||
|
|
||||||
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
|
- **Frontend**: HTML/CSS/JavaScript served via Electron's renderer process
|
||||||
- **Backend**: Express.js server running locally for download management
|
- **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
|
- **IPC Bridge**: Secure communication via Electron's preload script
|
||||||
- **Logging**: Winston-based structured logging with file rotation
|
- **Logging**: Winston-based structured logging with file rotation
|
||||||
- **Updates**: Automatic checking and installation via electron-updater
|
- **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
|
## Technology Stack
|
||||||
|
|
||||||
### Core Technologies
|
### Core Technologies
|
||||||
@@ -324,39 +425,17 @@ ressources/
|
|||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### Completed
|
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap).
|
||||||
|
|
||||||
- [x] ~~Website and documentation~~
|
|
||||||
- [x] ~~Automatic YT-DLP updates~~
|
|
||||||
- [x] ~~Automatic application updates~~
|
|
||||||
- [x] ~~Enhanced download status display~~
|
|
||||||
- [x] ~~Video chapter extraction~~
|
|
||||||
- [x] ~~Custom output path selection~~
|
|
||||||
- [x] ~~Custom codec selection~~
|
|
||||||
- [x] ~~Settings Panel with toggle switch for features.~~
|
|
||||||
|
|
||||||
### In Progress
|
|
||||||
|
|
||||||
- [ ] Additional format support (WebM, OGG, etc.)
|
|
||||||
- [ ] Refactoring and cleaning the code base.
|
|
||||||
- [ ] Chrome/Edge cookie support
|
|
||||||
- [ ] Improved multi-site support
|
|
||||||
|
|
||||||
### Planned
|
|
||||||
|
|
||||||
- [ ] Subtitle download and embedding
|
|
||||||
- [ ] Multiple language support
|
|
||||||
- [ ] Partial video downloads
|
|
||||||
- [ ] Custom file naming patterns
|
|
||||||
- [ ] Parallel downloads
|
|
||||||
- [ ] Automatic sponsor detection (SponsorBlock integration)
|
|
||||||
- [ ] Download history
|
|
||||||
- [ ] Linux version
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome and appreciated. This project benefits from community involvement.
|
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
|
### Bug Reports
|
||||||
|
|
||||||
Use the GitHub Issues system and include:
|
Use the GitHub Issues system and include:
|
||||||
@@ -408,6 +487,7 @@ You are free to use, modify, and redistribute this software under the terms of t
|
|||||||
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
||||||
- The open-source community for continuous support and contributions
|
- The open-source community for continuous support and contributions
|
||||||
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
||||||
|
- Zakaria for the website icon
|
||||||
- Jacques Chirac to love Apples
|
- Jacques Chirac to love Apples
|
||||||
- All users who test, report issues, and help improve the application
|
- All users who test, report issues, and help improve the application
|
||||||
|
|
||||||
@@ -417,6 +497,6 @@ You are free to use, modify, and redistribute this software under the terms of t
|
|||||||
|
|
||||||
**Freedom Loader** - Put freedom in your downloads
|
**Freedom Loader** - Put freedom in your downloads
|
||||||
|
|
||||||
[Website](https://masteracnolo.github.io/FreedomLoader/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/FreedomLoader/pages/wiki.html) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
|
[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>
|
</div>
|
||||||
|
|||||||
83
app/autoUpdater.js
Normal file
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
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 config = require('../config');
|
||||||
const RPC = require("discord-rpc");
|
const RPC = require("discord-rpc");
|
||||||
const { logger } = require("./logger");
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
const clientId = `${config.DiscordRPCID}`;
|
const clientId = `${config.DiscordRPCID}`;
|
||||||
const rpc = new RPC.Client({ transport: "ipc" });
|
const rpc = new RPC.Client({ transport: "ipc" });
|
||||||
@@ -20,10 +20,6 @@ function startRPC() {
|
|||||||
rpc.clearActivity()
|
rpc.clearActivity()
|
||||||
rpc.setActivity(presence);
|
rpc.setActivity(presence);
|
||||||
|
|
||||||
// Met à jour la présence toutes les 15s
|
|
||||||
intervalId = setInterval(() => {
|
|
||||||
rpc.setActivity(presence);
|
|
||||||
}, 15000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.login({ clientId }).catch(err => {
|
rpc.login({ clientId }).catch(err => {
|
||||||
127
app/ipcHandlers.js
Normal file
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
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
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
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
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
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 };
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 134 KiB |
41
config.js
41
config.js
@@ -3,24 +3,43 @@ const { app } = require("electron");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const localMode = !app.isPackaged;
|
|
||||||
|
|
||||||
const featuresPath = localMode ? path.join(__dirname, "./config/config.json") : path.join(path.join(process.resourcesPath,"config/" ,"config.json"));
|
const devMode = !app.isPackaged;
|
||||||
|
|
||||||
let features = {};
|
|
||||||
|
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() {
|
function loadFeatures() {
|
||||||
const raw = fs.readFileSync(featuresPath, "utf-8");
|
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||||
features = JSON.parse(raw);
|
return JSON.parse(raw);
|
||||||
return features;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const configFeatures = loadFeatures();
|
const configFeatures = loadFeatures();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
applicationPort: "8787",
|
applicationPort: "8787",
|
||||||
localMode,
|
devMode,
|
||||||
DiscordRPCID: "1410934537051181146",
|
DiscordRPCID: "1410934537051181146",
|
||||||
configFeatures
|
configFeatures,
|
||||||
|
featuresPath
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,11 @@
|
|||||||
"addMetadata": true,
|
"addMetadata": true,
|
||||||
"verboseLogs": false,
|
"verboseLogs": false,
|
||||||
"autoDownloadPlaylist": true,
|
"autoDownloadPlaylist": true,
|
||||||
|
"createPlaylistFolders": true,
|
||||||
"customCodec": "h264",
|
"customCodec": "h264",
|
||||||
"logSystem": true,
|
"logSystem": true,
|
||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
"downloadSystem": true,
|
"downloadSystem": true,
|
||||||
"notifySystem": true
|
"notifySystem": true,
|
||||||
|
"theme": "dark"
|
||||||
}
|
}
|
||||||
298
main.js
298
main.js
@@ -1,270 +1,90 @@
|
|||||||
const config = require("./config.js");
|
process.on("uncaughtException", (err) => {
|
||||||
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
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 path = require("path");
|
||||||
const os = require("os");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
||||||
const { AutoUpdater } = require("./server/update.js");
|
const { initAutoUpdater } = require("./app/autoUpdater");
|
||||||
const { configFeatures } = require("./config.js");
|
const { startRPC, stopRPC } = require("./app/discordRPC");
|
||||||
const { startRPC, stopRPC} = require("./server/discordRPC");
|
|
||||||
|
|
||||||
let mainWindow;
|
const { configFeatures } = require("./config");
|
||||||
const logsFolderPath = logDir;
|
const config = require("./config");
|
||||||
|
const { initThemes } = require("./app/themeManager");
|
||||||
|
|
||||||
const basePath = config.localMode
|
const { checkNativeDependencies } = require("./app/dependencyCheck");
|
||||||
? path.join(__dirname )
|
const { updateYtDlp } = require("./app/ytDlpUpdater");
|
||||||
: path.join(path.dirname(process.execPath), "resources");
|
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
||||||
|
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
||||||
|
const { userThemesPath, initUserThemes } = require("./server/helpers/path.helpers");
|
||||||
|
|
||||||
const configFolderPath = path.join(basePath, "config" ,"config.json");
|
app.setName("Freedom Loader");
|
||||||
|
app.setAppUserModelId("com.masteracnolo.freedomloader");
|
||||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
|
||||||
|
|
||||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
|
||||||
app.disableHardwareAcceleration();
|
app.disableHardwareAcceleration();
|
||||||
|
|
||||||
ipcMain.handle("version", () => config.version);
|
if (!config.devMode) {
|
||||||
|
const gotLock = app.requestSingleInstanceLock();
|
||||||
// Gestion single instance
|
if (gotLock) {
|
||||||
const gotLock = app.requestSingleInstanceLock();
|
app.on("second-instance", () => {
|
||||||
|
logger.info("New instance detected, closing older...");
|
||||||
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
getMainWindow()?.destroy();
|
||||||
function checkNativeDependencies() {
|
|
||||||
const deps = [
|
|
||||||
{ name: "yt-dlp.exe", path: path.join(process.resourcesPath, "binaries","yt-dlp.exe") },
|
|
||||||
{ name: "ffmpeg.exe", path: path.join(process.resourcesPath, "binaries", "ffmpeg.exe") },
|
|
||||||
{ name: "ffprobe.exe", path: path.join(process.resourcesPath, "binaries", "ffprobe.exe") },
|
|
||||||
{ name: "deno.exe", path: path.join(process.resourcesPath, "binaries", "deno.exe") },
|
|
||||||
];
|
|
||||||
const missing = deps.filter(dep => !fs.existsSync(dep.path));
|
|
||||||
let errorMsg = "";
|
|
||||||
if (missing.length > 0) {
|
|
||||||
const missingList = missing.map(dep => dep.name).join(", ");
|
|
||||||
logger.error(`Missing dependencies: ${missingList}`);
|
|
||||||
errorMsg += `The following files are missing in the 'ressources' folder:\n${missingList}`;
|
|
||||||
}
|
|
||||||
if (errorMsg) {
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
dialog.showErrorBox(
|
|
||||||
"Missing dependencies",
|
|
||||||
`${errorMsg}\n\nThe application will now exit. Try to reinstall`
|
|
||||||
);
|
|
||||||
app.quit();
|
|
||||||
});
|
});
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!config.localMode){
|
|
||||||
if (!gotLock) {
|
|
||||||
// Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
|
|
||||||
// Ici la nouvelle instance continue normalement
|
|
||||||
} else {
|
|
||||||
if (!checkNativeDependencies()) {
|
|
||||||
// Arrêt déjà géré dans la fonction
|
|
||||||
} else {
|
|
||||||
app.on("second-instance", () => {
|
|
||||||
// La vieille instance se ferme
|
|
||||||
if (mainWindow) {
|
|
||||||
logger.info("New Instance Detected, closing the older...");
|
|
||||||
mainWindow.destroy();
|
|
||||||
mainWindow = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Création fenêtre principale
|
|
||||||
async function createMainWindow() {
|
|
||||||
if (mainWindow) {
|
|
||||||
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: !configFeatures.customTopBar,
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateDownloadPath(userPath) {
|
|
||||||
const userHome = os.homedir(); // C:\Users\<User>
|
|
||||||
|
|
||||||
if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader");
|
|
||||||
|
|
||||||
// Résolution canonique et suivi des symlinks
|
|
||||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
|
||||||
const normalizedHome = path.resolve(userHome) + path.sep;
|
|
||||||
|
|
||||||
if (!resolved.startsWith(normalizedHome)) {
|
|
||||||
throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !");
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// IPC
|
|
||||||
ipcMain.handle("select-download-folder", async () => {
|
|
||||||
try {
|
|
||||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
|
||||||
const validatedPath = validateDownloadPath(result.filePaths[0]);
|
|
||||||
logger.info(`Folder Checked and Valid : ${validatedPath}`);
|
|
||||||
return validatedPath;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`An Error Occured when validating folder : ${err.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("validate-download-path", (event, userPath) => {
|
|
||||||
return validateDownloadPath(userPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
ipcMain.on("open-config", () => {
|
|
||||||
if (configFolderPath) shell.openPath(configFolderPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// App ready
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
logSessionStart();
|
logSessionStart();
|
||||||
logger.info("App Ready, Server Express starting...");
|
|
||||||
|
|
||||||
const serverPath = path.join(__dirname, "server", "server.js")
|
createSplashWindow();
|
||||||
|
|
||||||
const expressServer = require(serverPath);
|
if (!config.devMode && !checkNativeDependencies()) return;
|
||||||
|
|
||||||
|
const { userYtDlp } = require("./server/helpers/path.helpers");
|
||||||
|
updateYtDlp(userYtDlp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await expressServer.startServer();
|
setSplashProgress(0); // Checking dependencies
|
||||||
logger.info("Express Server Started");
|
await require("./server/server").startServer();
|
||||||
|
|
||||||
ipcMain.handle("features", () => {
|
setSplashProgress(1); // Starting server
|
||||||
return configFeatures;
|
registerIpcHandlers(getMainWindow);
|
||||||
});
|
|
||||||
|
|
||||||
const featureWhitelist = new Set([
|
setSplashProgress(2); // Loading themes
|
||||||
"autoUpdate",
|
initUserThemes();
|
||||||
"discordRPC",
|
await initThemes(userThemesPath);
|
||||||
"customTopBar",
|
|
||||||
"autoCheckInfo",
|
|
||||||
"addThumbnail",
|
|
||||||
"addMetadata",
|
|
||||||
"verboseLogs",
|
|
||||||
"autoDownloadPlaylist",
|
|
||||||
"customCodec"
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle("set-feature", (event, { key, value }) => {
|
|
||||||
try {
|
|
||||||
if (!featureWhitelist.has(key)) {
|
|
||||||
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// optionnel mais propre
|
|
||||||
if (configFeatures[key] === value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
configFeatures[key] = value;
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
configFolderPath,
|
|
||||||
JSON.stringify(configFeatures, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(`Feature updated: ${key} = ${value}`);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`set-feature failed (${key}): ${err.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
configFeatures.discordRPC ? startRPC() : "";
|
|
||||||
|
|
||||||
|
setSplashProgress(3); // Almost ready
|
||||||
await createMainWindow();
|
await createMainWindow();
|
||||||
configFeatures.autoUpdate ? AutoUpdater(mainWindow) : ""; // Auto Update
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
closeSplashWindow();
|
||||||
|
getMainWindow().show();
|
||||||
|
if (configFeatures.discordRPC) startRPC();
|
||||||
|
|
||||||
|
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Window or Server error :", err);
|
logger.error("Boot error:", err);
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
logger.info("All Window Closed, shuting down app");
|
logger.info("Shutting down...");
|
||||||
if (process.platform !== "darwin") app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", async () => {
|
||||||
logSessionEnd()
|
await stopRPC();
|
||||||
stopRPC()
|
logger.info("All services stopped. Have a nice day!");
|
||||||
|
logSessionEnd();
|
||||||
});
|
});
|
||||||
551
package-lock.json
generated
551
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"version": "1.4.1",
|
"version": "1.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"version": "1.4.1",
|
"version": "1.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
@@ -14,13 +14,14 @@
|
|||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"webidl-conversions": "^8.0.0",
|
"webidl-conversions": "^8.0.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0"
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^37.1.0",
|
"electron": "^41.2.0",
|
||||||
"electron-builder": "^26.4.0"
|
"electron-builder": "^26.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@colors/colors": {
|
"node_modules/@colors/colors": {
|
||||||
@@ -79,10 +80,28 @@
|
|||||||
"node": ">=10.12.0"
|
"node": ">=10.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/asar/node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@electron/asar/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||||
|
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/asar/node_modules/minimatch": {
|
"node_modules/@electron/asar/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -355,10 +374,17 @@
|
|||||||
"node": ">=16.4"
|
"node": ">=16.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/universal/node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@electron/universal/node_modules/brace-expansion": {
|
"node_modules/@electron/universal/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -366,9 +392,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/universal/node_modules/fs-extra": {
|
"node_modules/@electron/universal/node_modules/fs-extra": {
|
||||||
"version": "11.3.3",
|
"version": "11.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
|
||||||
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
|
"integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -394,13 +420,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/universal/node_modules/minimatch": {
|
"node_modules/@electron/universal/node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
@@ -442,9 +468,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
|
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
|
||||||
"version": "11.3.3",
|
"version": "11.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
|
||||||
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
|
"integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -485,29 +511,6 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/balanced-match": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "20 || >=22"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@isaacs/brace-expansion": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@isaacs/balanced-match": "^4.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "20 || >=22"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -578,13 +581,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
|
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
|
||||||
"version": "7.1.2",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^6.0.1"
|
"ansi-regex": "^6.2.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -813,9 +816,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||||
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
|
"integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -833,9 +836,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/http-cache-semantics": {
|
"node_modules/@types/http-cache-semantics": {
|
||||||
"version": "4.0.4",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
|
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -857,13 +860,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.7",
|
"version": "24.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
|
||||||
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/plist": {
|
"node_modules/@types/plist": {
|
||||||
@@ -914,9 +917,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xmldom/xmldom": {
|
"node_modules/@xmldom/xmldom": {
|
||||||
"version": "0.8.11",
|
"version": "0.8.13",
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||||
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
|
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -964,9 +967,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1023,9 +1026,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/app-builder-lib": {
|
"node_modules/app-builder-lib": {
|
||||||
"version": "26.7.0",
|
"version": "26.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz",
|
||||||
"integrity": "sha512-/UgCD8VrO79Wv8aBNpjMfsS1pIUfIPURoRn0Ik6tMe5avdZF+vQgl/juJgipcMmH3YS0BD573lCdCHyoi84USg==",
|
"integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1040,7 +1043,7 @@
|
|||||||
"@malept/flatpak-bundler": "^0.4.0",
|
"@malept/flatpak-bundler": "^0.4.0",
|
||||||
"@types/fs-extra": "9.0.13",
|
"@types/fs-extra": "9.0.13",
|
||||||
"async-exit-hook": "^2.0.1",
|
"async-exit-hook": "^2.0.1",
|
||||||
"builder-util": "26.4.1",
|
"builder-util": "26.8.1",
|
||||||
"builder-util-runtime": "9.5.1",
|
"builder-util-runtime": "9.5.1",
|
||||||
"chromium-pickle-js": "^0.2.0",
|
"chromium-pickle-js": "^0.2.0",
|
||||||
"ci-info": "4.3.1",
|
"ci-info": "4.3.1",
|
||||||
@@ -1048,7 +1051,7 @@
|
|||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"dotenv-expand": "^11.0.6",
|
"dotenv-expand": "^11.0.6",
|
||||||
"ejs": "^3.1.8",
|
"ejs": "^3.1.8",
|
||||||
"electron-publish": "26.6.0",
|
"electron-publish": "26.8.1",
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"hosted-git-info": "^4.1.0",
|
"hosted-git-info": "^4.1.0",
|
||||||
"isbinaryfile": "^5.0.0",
|
"isbinaryfile": "^5.0.0",
|
||||||
@@ -1070,8 +1073,8 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"dmg-builder": "26.7.0",
|
"dmg-builder": "26.8.1",
|
||||||
"electron-builder-squirrel-windows": "26.7.0"
|
"electron-builder-squirrel-windows": "26.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/app-builder-lib/node_modules/@electron/get": {
|
"node_modules/app-builder-lib/node_modules/@electron/get": {
|
||||||
@@ -1250,11 +1253,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
@@ -1349,14 +1355,16 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^4.0.2"
|
||||||
"concat-map": "0.0.1"
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
@@ -1402,9 +1410,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/builder-util": {
|
"node_modules/builder-util": {
|
||||||
"version": "26.4.1",
|
"version": "26.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz",
|
||||||
"integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==",
|
"integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1510,10 +1518,17 @@
|
|||||||
"node": "^18.17.0 || >=20.5.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cacache/node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cacache/node_modules/brace-expansion": {
|
"node_modules/cacache/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1550,13 +1565,13 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/cacache/node_modules/minimatch": {
|
"node_modules/cacache/node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
@@ -1868,9 +1883,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||||
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -1911,9 +1926,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||||
"dev": true,
|
"license": "MIT"
|
||||||
"license": "MIT",
|
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/crc": {
|
"node_modules/crc": {
|
||||||
"version": "3.8.0",
|
"version": "3.8.0",
|
||||||
@@ -2128,10 +2141,28 @@
|
|||||||
"p-limit": "^3.1.0 "
|
"p-limit": "^3.1.0 "
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dir-compare/node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/dir-compare/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||||
|
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dir-compare/node_modules/minimatch": {
|
"node_modules/dir-compare/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2155,14 +2186,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dmg-builder": {
|
"node_modules/dmg-builder": {
|
||||||
"version": "26.7.0",
|
"version": "26.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz",
|
||||||
"integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==",
|
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "26.7.0",
|
"app-builder-lib": "26.8.1",
|
||||||
"builder-util": "26.4.1",
|
"builder-util": "26.8.1",
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"iconv-lite": "^0.6.2",
|
"iconv-lite": "^0.6.2",
|
||||||
"js-yaml": "^4.1.0"
|
"js-yaml": "^4.1.0"
|
||||||
@@ -2309,15 +2340,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron": {
|
"node_modules/electron": {
|
||||||
"version": "37.10.3",
|
"version": "41.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron/-/electron-37.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/electron/-/electron-41.2.0.tgz",
|
||||||
"integrity": "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g==",
|
"integrity": "sha512-0OKLiymqfV0WK68RBXqAm3Myad2TpI5wwxLCBEUcH5Nugo3YfSk7p1Js/AL9266qTz5xZioUnxt9hG8FFwax0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/get": "^2.0.0",
|
"@electron/get": "^2.0.0",
|
||||||
"@types/node": "^22.7.7",
|
"@types/node": "^24.9.0",
|
||||||
"extract-zip": "^2.0.1"
|
"extract-zip": "^2.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2328,18 +2359,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-builder": {
|
"node_modules/electron-builder": {
|
||||||
"version": "26.7.0",
|
"version": "26.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz",
|
||||||
"integrity": "sha512-LoXbCvSFxLesPneQ/fM7FB4OheIDA2tjqCdUkKlObV5ZKGhYgi5VHPHO/6UUOUodAlg7SrkPx7BZJPby+Vrtbg==",
|
"integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "26.7.0",
|
"app-builder-lib": "26.8.1",
|
||||||
"builder-util": "26.4.1",
|
"builder-util": "26.8.1",
|
||||||
"builder-util-runtime": "9.5.1",
|
"builder-util-runtime": "9.5.1",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"ci-info": "^4.2.0",
|
"ci-info": "^4.2.0",
|
||||||
"dmg-builder": "26.7.0",
|
"dmg-builder": "26.8.1",
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"lazy-val": "^1.0.5",
|
"lazy-val": "^1.0.5",
|
||||||
"simple-update-notifier": "2.0.0",
|
"simple-update-notifier": "2.0.0",
|
||||||
@@ -2354,15 +2385,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-builder-squirrel-windows": {
|
"node_modules/electron-builder-squirrel-windows": {
|
||||||
"version": "26.7.0",
|
"version": "26.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz",
|
||||||
"integrity": "sha512-3EqkQK+q0kGshdPSKEPb2p5F75TENMKu6Fe5aTdeaPfdzFK4Yjp5L0d6S7K8iyvqIsGQ/ei4bnpyX9wt+kVCKQ==",
|
"integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "26.7.0",
|
"app-builder-lib": "26.8.1",
|
||||||
"builder-util": "26.4.1",
|
"builder-util": "26.8.1",
|
||||||
"electron-winstaller": "5.4.0"
|
"electron-winstaller": "5.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2405,14 +2436,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-publish": {
|
"node_modules/electron-publish": {
|
||||||
"version": "26.6.0",
|
"version": "26.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz",
|
||||||
"integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==",
|
"integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/fs-extra": "^9.0.11",
|
"@types/fs-extra": "^9.0.11",
|
||||||
"builder-util": "26.4.1",
|
"builder-util": "26.8.1",
|
||||||
"builder-util-runtime": "9.5.1",
|
"builder-util-runtime": "9.5.1",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
@@ -2460,9 +2491,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-updater": {
|
"node_modules/electron-updater": {
|
||||||
"version": "6.7.3",
|
"version": "6.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz",
|
||||||
"integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==",
|
"integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"builder-util-runtime": "9.5.1",
|
"builder-util-runtime": "9.5.1",
|
||||||
@@ -2502,9 +2533,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-updater/node_modules/semver": {
|
"node_modules/electron-updater/node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -2763,12 +2794,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-rate-limit": {
|
"node_modules/express-rate-limit": {
|
||||||
"version": "8.2.1",
|
"version": "8.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz",
|
||||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
"integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ip-address": "10.0.1"
|
"ip-address": "^10.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
@@ -2877,19 +2908,26 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
||||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
"integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimatch": "^5.0.1"
|
"minimatch": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/filelist/node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/filelist/node_modules/brace-expansion": {
|
"node_modules/filelist/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2897,9 +2935,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/filelist/node_modules/minimatch": {
|
"node_modules/filelist/node_modules/minimatch": {
|
||||||
"version": "5.1.6",
|
"version": "5.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3153,10 +3191,28 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob/node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||||
|
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob/node_modules/minimatch": {
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3186,9 +3242,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/global-agent/node_modules/semver": {
|
"node_modules/global-agent/node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -3458,6 +3514,12 @@
|
|||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/imurmurhash": {
|
"node_modules/imurmurhash": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||||
@@ -3487,9 +3549,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
"version": "10.0.1",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
@@ -3555,6 +3617,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isbinaryfile": {
|
"node_modules/isbinaryfile": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz",
|
||||||
@@ -3569,13 +3637,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
|
||||||
"integrity": "sha512-mIcis6w+JiQf3P7t7mg/35GKB4T1FQsBOtMIvuKw4YErj5RjtbhcTd5/I30fmkmGMwvI0WlzSNN+27K0QCMkAw==",
|
"integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jackspeak": {
|
"node_modules/jackspeak": {
|
||||||
@@ -3679,6 +3747,48 @@
|
|||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jszip": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
|
"dependencies": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"setimmediate": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jszip/node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jszip/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jszip/node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -3701,10 +3811,19 @@
|
|||||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.23",
|
"version": "4.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -3904,16 +4023,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "10.1.2",
|
"version": "10.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
"integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==",
|
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/brace-expansion": "^5.0.1"
|
"brace-expansion": "^5.0.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
@@ -3930,11 +4049,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
@@ -3971,11 +4090,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minipass-flush": {
|
"node_modules/minipass-flush": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz",
|
||||||
"integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
|
"integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minipass": "^3.0.0"
|
"minipass": "^3.0.0"
|
||||||
},
|
},
|
||||||
@@ -4100,9 +4219,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-abi": {
|
"node_modules/node-abi": {
|
||||||
"version": "4.26.0",
|
"version": "4.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz",
|
||||||
"integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==",
|
"integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4390,6 +4509,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0"
|
"license": "BlueOak-1.0.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -4444,9 +4569,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "8.3.0",
|
"version": "8.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -4483,9 +4608,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4550,6 +4675,12 @@
|
|||||||
"node": "^18.17.0 || >=20.5.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/progress": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
@@ -4600,9 +4731,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4621,9 +4752,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.1",
|
"version": "6.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"side-channel": "^1.1.0"
|
||||||
@@ -4884,9 +5015,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sanitize-filename": {
|
"node_modules/sanitize-filename": {
|
||||||
"version": "1.6.3",
|
"version": "1.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz",
|
||||||
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
|
"integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "WTFPL OR ISC",
|
"license": "WTFPL OR ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4894,9 +5025,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sax": {
|
"node_modules/sax": {
|
||||||
"version": "1.4.4",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||||||
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=11.0.0"
|
"node": ">=11.0.0"
|
||||||
@@ -4982,6 +5113,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/setimmediate": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
@@ -5104,9 +5241,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-update-notifier/node_modules/semver": {
|
"node_modules/simple-update-notifier/node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -5336,9 +5473,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.5.7",
|
"version": "7.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
|
||||||
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
"integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5459,14 +5596,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.16",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.3"
|
"picomatch": "^4.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -5476,9 +5613,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5558,9 +5695,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.4.2",
|
"version": "1.6.0",
|
||||||
"author": "MasterAcnolo",
|
"author": "MasterAcnolo",
|
||||||
"description": "Freedom Loader",
|
"description": "Freedom Loader",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@@ -17,13 +17,14 @@
|
|||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"webidl-conversions": "^8.0.0",
|
"webidl-conversions": "^8.0.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0"
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^37.1.0",
|
"electron": "^41.2.0",
|
||||||
"electron-builder": "^26.4.0"
|
"electron-builder": "^26.8.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.masteracnolo.freedomloader",
|
"appId": "com.masteracnolo.freedomloader",
|
||||||
@@ -38,13 +39,18 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"**/*",
|
"**/*",
|
||||||
"node_modules/**/*",
|
"node_modules/**/*",
|
||||||
"!logs/*.json"
|
"!config/config.dev.json",
|
||||||
|
"!theme/**/*"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "build"
|
"buildResources": "build"
|
||||||
},
|
},
|
||||||
"asar": true,
|
"asar": true,
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "theme",
|
||||||
|
"to": "theme"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"from": "build/confirm-icon.png",
|
"from": "build/confirm-icon.png",
|
||||||
"to": "confirm-icon.png"
|
"to": "confirm-icon.png"
|
||||||
@@ -57,6 +63,10 @@
|
|||||||
"from": "build/banner.bmp",
|
"from": "build/banner.bmp",
|
||||||
"to": "banner.bmp"
|
"to": "banner.bmp"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"from": "build/banner.png",
|
||||||
|
"to": "banner.png"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"from": "build/error.png",
|
"from": "build/error.png",
|
||||||
"to": "error.png"
|
"to": "error.png"
|
||||||
@@ -78,8 +88,8 @@
|
|||||||
"to": "binaries/deno.exe"
|
"to": "binaries/deno.exe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "config/config.json",
|
"from": "config/config.default.json",
|
||||||
"to": "config/config.json"
|
"to": "config/config.default.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
getFeatures: () => ipcRenderer.invoke("features"),
|
getFeatures: () => ipcRenderer.invoke("features"),
|
||||||
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
|
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
|
||||||
getVersion: () => ipcRenderer.invoke("version"),
|
getVersion: () => ipcRenderer.invoke("version"),
|
||||||
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path)
|
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
|
// Contrôles de fenêtre et outils custom pour la topbar
|
||||||
@@ -18,6 +20,8 @@ contextBridge.exposeInMainWorld("topbarAPI", {
|
|||||||
openDevTools: () => ipcRenderer.send("open-devtools"),
|
openDevTools: () => ipcRenderer.send("open-devtools"),
|
||||||
openLogs: () => ipcRenderer.send("open-logs"),
|
openLogs: () => ipcRenderer.send("open-logs"),
|
||||||
openWebsite: () => ipcRenderer.send("open-website"),
|
openWebsite: () => ipcRenderer.send("open-website"),
|
||||||
|
openTheme: () => ipcRenderer.send("open-theme"),
|
||||||
openWiki: () => ipcRenderer.send("open-wiki"),
|
openWiki: () => ipcRenderer.send("open-wiki"),
|
||||||
|
openWorkshop: () => ipcRenderer.send("open-workshop"),
|
||||||
openConfig: () => ipcRenderer.send("open-config")
|
openConfig: () => ipcRenderer.send("open-config")
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 967 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 620 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 MiB |
@@ -7,9 +7,8 @@
|
|||||||
default-src 'self' 'unsafe-inline' data:;
|
default-src 'self' 'unsafe-inline' data:;
|
||||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||||
font-src https://fonts.gstatic.com;
|
font-src https://fonts.gstatic.com;
|
||||||
img-src 'self' data: https://i.ytimg.com;
|
img-src 'self' data: https:;
|
||||||
">
|
">
|
||||||
<title></title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles/styles.css">
|
<link rel="stylesheet" href="styles/styles.css">
|
||||||
<link rel="stylesheet" href="styles/layout/topbar.css">
|
<link rel="stylesheet" href="styles/layout/topbar.css">
|
||||||
@@ -17,19 +16,23 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="topbar" id="topbar">
|
<header>
|
||||||
<div class="custom-controls">
|
<div class="topbar" id="topbar">
|
||||||
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
<div class="custom-controls">
|
||||||
<button id="logs-btn" title="Logs">Logs</button>
|
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
||||||
<button id="website-btn" title="Website">Website</button>
|
<button id="logs-btn" title="Logs">Logs</button>
|
||||||
<button id="wiki-btn" title="Wiki">Wiki</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>
|
||||||
|
<button id="maximize-btn" title="Maximiser"><img src="assets/icon/maximize.svg"></button>
|
||||||
|
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-controls">
|
</header>
|
||||||
<button id="minimize-btn" title="Réduire"><img src="assets/icon/minimize.svg"></button>
|
|
||||||
<button id="maximize-btn" title="Maximiser"><img src="assets/icon/maximize.svg"></button>
|
|
||||||
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="settings-button" id="settings-button">
|
<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>
|
<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>
|
||||||
@@ -52,12 +55,18 @@
|
|||||||
|
|
||||||
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<div style="display: flex;">
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
<span class="setting-title">Thème</span>
|
<span class="setting-title">Thème</span>
|
||||||
|
|
||||||
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
|
<button id="refresh-themes-btn" title="Refresh themes" class="refresh-themes-btn">
|
||||||
|
<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>
|
</div>
|
||||||
<small>Choose the theme of the application</small>
|
<small>Choose the theme of the application</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,20 +89,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
<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">
|
<input type="checkbox" data-key="notifySystem" id="notifySystem">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Notification at the end</span>
|
<span class="setting-title">Notification at the end</span>
|
||||||
<small>Notify when a download finishes.</small>
|
<small>Notify when a download finishes.</small>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
@@ -142,6 +151,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</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-item" style="margin-left: 32px; margin-top: 20px;">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Custom Codec</span>
|
<span class="setting-title">Custom Codec</span>
|
||||||
@@ -191,6 +208,7 @@
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div class="settings-footer">
|
<div class="settings-footer">
|
||||||
|
<button id="open-theme">Open Theme Folder</button>
|
||||||
<button id="open-json-btn">Open JSON Configuration</button>
|
<button id="open-json-btn">Open JSON Configuration</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -198,71 +216,89 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container" id="container">
|
<main>
|
||||||
|
<div class="container" id="container">
|
||||||
|
<h1 id="title"> Freedom Loader </h1>
|
||||||
|
<p id="subtitle"> Because why not? </p>
|
||||||
|
|
||||||
<header>
|
<form action="/download" method="POST" id="downloadForm">
|
||||||
<h1 id="title"> Freedom Loader </h1>
|
<div class="url-input-container">
|
||||||
<p id="subtitle"> Because why not? </p>
|
<input type="text" name="url" placeholder="URL" id="UrlInput">
|
||||||
</header>
|
<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>
|
||||||
|
|
||||||
<form action="/download" method="POST" id="downloadForm">
|
<label class="checkbox-inline">
|
||||||
<input name="url" placeholder="URL" id="UrlInput">
|
|
||||||
|
|
||||||
<label class="checkbox-inline">
|
<label class="checkbox-mp3">
|
||||||
|
<input type="checkbox" name="audioOnly" value="1">
|
||||||
|
<div class="checkmark"></div>
|
||||||
|
</label>
|
||||||
|
Audio Only (MP3)
|
||||||
|
</label>
|
||||||
|
<select name="quality">
|
||||||
|
<option value="best">Best Quality</option>
|
||||||
|
<option value="medium">Medium Quality</option>
|
||||||
|
<option value="worst">Lowest Quality</option>
|
||||||
|
<option value="1080">1080p</option>
|
||||||
|
<option value="720">720p</option>
|
||||||
|
<option value="480">480p</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<label class="checkbox-mp3">
|
<button type="submit" onclick="startProgress()">Download</button>
|
||||||
<input type="checkbox" name="audioOnly" value="1">
|
</form>
|
||||||
<div class="checkmark"></div>
|
|
||||||
</label>
|
|
||||||
Audio Only (MP3)
|
|
||||||
</label>
|
|
||||||
<select name="quality">
|
|
||||||
<option value="best">Best Quality</option>
|
|
||||||
<option value="medium">Medium Quality</option>
|
|
||||||
<option value="worst">Lowest Quality</option>
|
|
||||||
<option value="1080">1080p</option>
|
|
||||||
<option value="720">720p</option>
|
|
||||||
<option value="480">480p</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button type="submit" onclick="startProgress()">Download</button>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="download-path">
|
<div class="download-path">
|
||||||
Will be save on <span id="savePath"></span>
|
Will be save on <span id="savePath"></span>
|
||||||
<button id="changePath">Edit</button>
|
<button id="changePath">Edit</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="downloadProgressWrapper">
|
<div class="progress-container">
|
||||||
<div id="downloadProgress"></div>
|
<div id="downloadStage" style="display: none; font-size: 0.85rem; color: var(--default-text-color); margin-top: 8px;"></div>
|
||||||
</div>
|
<div id="downloadProgressWrapper">
|
||||||
|
<div id="downloadProgress"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 20px; align-items: center;">
|
||||||
|
<div id="downloadProgressText"></div>
|
||||||
|
<div id="downloadSpeedText"></div>
|
||||||
|
<div id="playlistInfoText" style="display: none; font-size: 0.9rem;"></div>
|
||||||
|
<button type="button" id="stopBtn" onclick="stopDownload()" class="stop-btn" style="display:none;" title="Stop download">■</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 20px;">
|
<div id="downloadStatus" style="display: none;"></div>
|
||||||
<div id="downloadProgressText"></div>
|
|
||||||
<div id="downloadSpeedText"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="downloadStatus"></div>
|
<div class="infos-container">
|
||||||
|
|
||||||
<div class="infos-container">
|
<div id="loaderBox" class="loader-box" style="display:none;">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<span>Fetching info...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="videoInfo" style="margin-top: 20px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span id="version-badge" class="version-badge"></span>
|
||||||
|
|
||||||
<div id="loaderBox" class="loader-box" style="display:none;">
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<span>Fetching info...</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<div id="videoInfo" style="margin-top: 20px;"></div>
|
<!-- Toast Container -->
|
||||||
</div>
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
<span id="version-badge" class="version-badge"></span>
|
|
||||||
|
|
||||||
<h2 id="reference"> Because Why Not ? </h2>
|
<h2 id="reference"> Because Why Not ? </h2>
|
||||||
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
<script src="script/toast.js"></script>
|
||||||
|
<script src="script/clipboardPaste.js"></script>
|
||||||
<script src="script/custompath.js"></script>
|
<script src="script/custompath.js"></script>
|
||||||
<script src="script/settingsPanel.js"></script>
|
<script src="script/settingsPanel.js"></script>
|
||||||
<script src="script/appVersion.js"></script>
|
<script src="script/appVersion.js"></script>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
async function versionLabel(){
|
async function versionLabel(){
|
||||||
const appVersion = await window.electronAPI.getVersion();
|
const appVersion = await window.electronAPI.getVersion();
|
||||||
|
|
||||||
|
// Write in front the app version for debugging (bottom right)
|
||||||
document.getElementById("version-badge").textContent = `v${appVersion}`;
|
document.getElementById("version-badge").textContent = `v${appVersion}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
35
public/script/clipboardPaste.js
Normal file
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.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
async function applyPathFromBack(path) {
|
async function applyPathFromBack(path) {
|
||||||
savePathElem.textContent = path;
|
savePathElem.textContent = path;
|
||||||
|
savePathElem.title = path;
|
||||||
hidden.value = path;
|
hidden.value = path;
|
||||||
localStorage.setItem("customDownloadPath", path); // UX only
|
localStorage.setItem("customDownloadPath", path); // UX only
|
||||||
}
|
}
|
||||||
@@ -45,18 +46,15 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
.getElementById("changePath")
|
.getElementById("changePath")
|
||||||
.addEventListener("click", async () => {
|
.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const selectedPath =
|
// selectDownloadFolder already returns a validated path
|
||||||
|
const validatedPath =
|
||||||
await window.electronAPI.selectDownloadFolder();
|
await window.electronAPI.selectDownloadFolder();
|
||||||
|
|
||||||
if (!selectedPath) return; // annulé
|
if (!validatedPath) return; // cancelled
|
||||||
|
|
||||||
// Validation back obligatoire
|
|
||||||
const validatedPath =
|
|
||||||
await window.electronAPI.getValidatedDownloadPath(selectedPath);
|
|
||||||
|
|
||||||
await applyPathFromBack(validatedPath);
|
await applyPathFromBack(validatedPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Dossier non autorisé.");
|
alert("This folder is not allowed. Only specific folders (Users, Downloads, Documents) are authorized for downloads.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,62 +1,129 @@
|
|||||||
const themes = {
|
|
||||||
dark: { label: "Sombre", subtitle: "Darkness is my ally" },
|
|
||||||
light: { label: "Clair", subtitle: "Qui aime ce thème ?" },
|
|
||||||
neon: { label: "Purple", subtitle: "How was your day ?"},
|
|
||||||
nf: { label: "NF", subtitle: "You call it music, i call it my Therapist" },
|
|
||||||
songbird: { label: "Songbird", subtitle: "From Her to Eternity" },
|
|
||||||
drift: { label: "Drift", subtitle: "Si la route t'appelle, contre appel" },
|
|
||||||
fanatic: { label: "Fanatic", subtitle: "Always Fnatic !" },
|
|
||||||
chirac: { label: "Chirac", subtitle: "J'aime les pommes" },
|
|
||||||
spicy: { label: "Spicy", subtitle: "The Spiciest One" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const themeSelect = document.getElementById("themeSelect");
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
let loadedThemes = [];
|
||||||
|
|
||||||
function populateThemeSelect() {
|
function applyTheme(theme) {
|
||||||
for (const [themeKey, themeInfo] of Object.entries(themes)) {
|
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");
|
const option = document.createElement("option");
|
||||||
option.value = themeKey;
|
option.value = theme.id;
|
||||||
option.textContent = themeInfo.label;
|
option.textContent = theme.name;
|
||||||
themeSelect.appendChild(option);
|
themeSelect.appendChild(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTheme(themeKey) {
|
async function initThemes() {
|
||||||
document.body.classList.remove(...Object.keys(themes));
|
loadedThemes = await window.electronAPI.getThemes();
|
||||||
document.body.classList.add(themeKey);
|
|
||||||
|
|
||||||
const subtitleElement = document.getElementById("subtitle");
|
if (!loadedThemes.length) return;
|
||||||
if (subtitleElement && themes[themeKey]) {
|
|
||||||
subtitleElement.textContent = themes[themeKey].subtitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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) {
|
themeSelect.addEventListener("change", (e) => {
|
||||||
localStorage.setItem("selectedTheme", themeKey);
|
const theme = loadedThemes.find(t => t.id === e.target.value);
|
||||||
}
|
if (theme) {
|
||||||
|
applyTheme(theme);
|
||||||
function loadTheme() {
|
saveTheme(theme.id);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialisation
|
async function refreshThemes() {
|
||||||
populateThemeSelect();
|
loadedThemes = await window.electronAPI.reloadThemes();
|
||||||
loadTheme();
|
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 form = document.getElementById("downloadForm");
|
||||||
const statusDiv = document.getElementById("downloadStatus");
|
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) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
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 formData = new FormData(form);
|
||||||
const params = new URLSearchParams(formData);
|
const params = new URLSearchParams(formData);
|
||||||
@@ -18,20 +36,92 @@ form.addEventListener("submit", async (e) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
statusDiv.textContent = res;
|
if (!userStoppedDownload) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
window.showError(errorText);
|
||||||
|
resetProgressBar();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await res.text();
|
// Download successful - check if notifications are enabled
|
||||||
statusDiv.textContent = text;
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
if (features.notifySystem) {
|
||||||
|
window.showSuccess("Download completed!");
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch (err) {
|
||||||
statusDiv.textContent = "❌ An Error has Occured.";
|
if (!isDownloading && !userStoppedDownload) {
|
||||||
|
statusDiv.style.display = "none";
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
isDownloading = false;
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
|
stopBtn.style.display = "none";
|
||||||
setTimeout(() => {
|
|
||||||
statusDiv.textContent = "";
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function showInitialProgress() {
|
||||||
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
|
const progressBarText = document.getElementById("downloadProgressText");
|
||||||
|
const speedElement = document.getElementById("downloadSpeedText");
|
||||||
|
const stageElement = document.getElementById("downloadStage");
|
||||||
|
const stopBtn = document.getElementById("stopBtn");
|
||||||
|
|
||||||
|
if (progressWrapper) progressWrapper.style.display = "block";
|
||||||
|
if (progressBarText) {
|
||||||
|
progressBarText.style.display = "block";
|
||||||
|
progressBarText.innerHTML = "0%";
|
||||||
|
}
|
||||||
|
if (speedElement) {
|
||||||
|
speedElement.style.display = "block";
|
||||||
|
speedElement.textContent = "0 Mib/s";
|
||||||
|
}
|
||||||
|
if (stageElement) {
|
||||||
|
stageElement.style.display = "block";
|
||||||
|
stageElement.textContent = "Retrieving data...";
|
||||||
|
}
|
||||||
|
if (stopBtn) stopBtn.style.display = "inline-flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopDownload() {
|
||||||
|
if (!isDownloading) return;
|
||||||
|
|
||||||
|
userStoppedDownload = true;
|
||||||
|
button.disabled = true;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch("/download/cancel", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
});
|
||||||
|
|
||||||
|
window.showWarning("Download stopped");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error stopping download:", err);
|
||||||
|
} finally {
|
||||||
|
isDownloading = false;
|
||||||
|
button.disabled = false;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
stopBtn.style.display = "none";
|
||||||
|
resetProgressBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetProgressBar() {
|
||||||
|
const progressWrapper = document.getElementById("downloadProgressWrapper");
|
||||||
|
if (progressWrapper) progressWrapper.style.display = "none";
|
||||||
|
|
||||||
|
const progressBar = document.getElementById("downloadProgress");
|
||||||
|
if (progressBar) progressBar.style.width = "0%";
|
||||||
|
|
||||||
|
const progressBarText = document.getElementById("downloadProgressText");
|
||||||
|
if (progressBarText) progressBarText.style.display = "none";
|
||||||
|
|
||||||
|
const speedElement = document.getElementById("downloadSpeedText");
|
||||||
|
if (speedElement) speedElement.style.display = "none";
|
||||||
|
|
||||||
|
const playlistInfoElement = document.getElementById("playlistInfoText");
|
||||||
|
if (playlistInfoElement) playlistInfoElement.style.display = "none";
|
||||||
|
}
|
||||||
@@ -33,6 +33,17 @@ async function init() {
|
|||||||
const urlInput = document.getElementById("UrlInput");
|
const urlInput = document.getElementById("UrlInput");
|
||||||
const infoDiv = document.getElementById("videoInfo");
|
const infoDiv = document.getElementById("videoInfo");
|
||||||
const loaderBox = document.getElementById("loaderBox");
|
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 = "";
|
let lastFetchedUrl = "";
|
||||||
|
|
||||||
@@ -43,10 +54,11 @@ async function init() {
|
|||||||
|
|
||||||
const url = urlInput.value.trim();
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
// Si champ vide -> reset total
|
// IF field empty -> total reset
|
||||||
if (!url || url.length < 2) {
|
if (!url || url.length < 2) {
|
||||||
infoDiv.innerHTML = "";
|
infoDiv.innerHTML = "";
|
||||||
infoDiv.classList.remove("visible", "playlist-mode");
|
infoDiv.classList.remove("visible", "playlist-mode");
|
||||||
|
playlistTitleInput.value = "";
|
||||||
lastFetchedUrl = "";
|
lastFetchedUrl = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,7 +70,7 @@ async function init() {
|
|||||||
const data = await fetchVideoInfo(url);
|
const data = await fetchVideoInfo(url);
|
||||||
loaderBox.style.display = "none";
|
loaderBox.style.display = "none";
|
||||||
|
|
||||||
// Gestion des erreurs
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
infoDiv.innerHTML = `
|
infoDiv.innerHTML = `
|
||||||
<div style="
|
<div style="
|
||||||
@@ -70,17 +82,37 @@ async function init() {
|
|||||||
`;
|
`;
|
||||||
infoDiv.classList.add("visible");
|
infoDiv.classList.add("visible");
|
||||||
infoDiv.classList.remove("playlist-mode");
|
infoDiv.classList.remove("playlist-mode");
|
||||||
|
playlistTitleInput.value = "";
|
||||||
loaderBox.style.display = "none";
|
loaderBox.style.display = "none";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- PLAYLIST ----------
|
// ---------- PLAYLIST ----------
|
||||||
if (data.type === "playlist") {
|
if (data.type === "playlist") {
|
||||||
|
playlistTitleInput.value = data.title;
|
||||||
infoDiv.classList.add("playlist-mode");
|
infoDiv.classList.add("playlist-mode");
|
||||||
infoDiv.innerHTML = `
|
infoDiv.innerHTML = `
|
||||||
<h3 style="color:var(--video-info-heading-color);"><strong>Playlist Detected: ${data.title}</strong></h3>
|
<div class="playlist-header">
|
||||||
<h3 style="color:var(--video-info-heading-color);"><strong>Video Count: ${data.count}</strong></h3>
|
<div class="playlist-info">
|
||||||
<p><strong>Channel :</strong> ${data.channel || "Unknown"}</p>
|
<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>
|
<div id="playlistVideos"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -88,39 +120,58 @@ async function init() {
|
|||||||
|
|
||||||
data.videos.forEach(v => {
|
data.videos.forEach(v => {
|
||||||
const durationStr = v.duration
|
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;
|
const videoUrl = v.id ? `https://www.youtube.com/watch?v=${v.id}` : v.url;
|
||||||
|
|
||||||
listDiv.innerHTML += `
|
listDiv.innerHTML += `
|
||||||
<div style="margin-bottom:12px;">
|
<div class="playlist-video-card">
|
||||||
<img src="${v.thumbnail}" width="160" alt="Thumbnail">
|
<div class="video-thumbnail-wrapper">
|
||||||
<p><strong>${v.title}</strong></p>
|
<img src="${v.thumbnail}" alt="${v.title}" class="video-thumbnail">
|
||||||
<p>Duration : ${durationStr}</p>
|
<span class="video-duration">${durationStr}</span>
|
||||||
<p><a href="${videoUrl}" target="_blank">URL</a>
|
</div>
|
||||||
<button class="copy-btn" data-url="${videoUrl}">📋</button>
|
<div class="video-content">
|
||||||
</p>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestion du bouton copier
|
// Copy Button
|
||||||
listDiv.addEventListener("click", (event) => {
|
listDiv.addEventListener("click", (event) => {
|
||||||
if (event.target.classList.contains("copy-btn")) {
|
if (event.target.classList.contains("copy-btn") || event.target.closest(".copy-btn")) {
|
||||||
const btn = event.target;
|
const btn = event.target.closest(".copy-btn") || event.target;
|
||||||
if (btn.disabled) return;
|
if (btn.disabled) return;
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const url = btn.dataset.url;
|
const url = btn.dataset.url;
|
||||||
navigator.clipboard.writeText(url)
|
navigator.clipboard.writeText(url)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const original = btn.textContent;
|
const original = btn.innerHTML;
|
||||||
|
|
||||||
btn.style.opacity = 0;
|
btn.style.opacity = 0;
|
||||||
btn.style.transform = "scale(0.7)";
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
setTimeout(() => {
|
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.opacity = 1;
|
||||||
btn.style.transform = "scale(1)";
|
btn.style.transform = "scale(1)";
|
||||||
|
|
||||||
@@ -129,7 +180,7 @@ async function init() {
|
|||||||
btn.style.transform = "scale(0.7)";
|
btn.style.transform = "scale(0.7)";
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.textContent = original;
|
btn.innerHTML = original;
|
||||||
btn.style.opacity = 1;
|
btn.style.opacity = 1;
|
||||||
btn.style.transform = "scale(1)";
|
btn.style.transform = "scale(1)";
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -140,10 +191,12 @@ async function init() {
|
|||||||
}, 300);
|
}, 300);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
const original = btn.textContent;
|
const original = btn.innerHTML;
|
||||||
btn.textContent = "❌";
|
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(() => {
|
setTimeout(() => {
|
||||||
btn.textContent = original;
|
btn.innerHTML = original;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
@@ -155,6 +208,7 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
infoDiv.classList.remove("playlist-mode");
|
infoDiv.classList.remove("playlist-mode");
|
||||||
|
playlistTitleInput.value = "";
|
||||||
|
|
||||||
// ---------- VIDEO NORMALE ----------
|
// ---------- VIDEO NORMALE ----------
|
||||||
const durationStr = data.duration
|
const durationStr = data.duration
|
||||||
|
|||||||
@@ -3,19 +3,34 @@ const progressBar = document.getElementById("downloadProgress");
|
|||||||
const progressBarText = document.getElementById("downloadProgressText")
|
const progressBarText = document.getElementById("downloadProgressText")
|
||||||
|
|
||||||
const speedElement = document.getElementById("downloadSpeedText");
|
const speedElement = document.getElementById("downloadSpeedText");
|
||||||
|
const stageElement = document.getElementById("downloadStage");
|
||||||
|
const playlistInfoElement = document.getElementById("playlistInfoText");
|
||||||
const speedEvt = new EventSource("/download/speed");
|
const speedEvt = new EventSource("/download/speed");
|
||||||
|
const stageEvt = new EventSource("/download/stage");
|
||||||
|
const playlistInfoEvt = new EventSource("/download/playlist-info");
|
||||||
|
|
||||||
function startProgress() {
|
function startProgress() {
|
||||||
progressWrapper.style.display = "block";
|
|
||||||
progressBar.style.width = "0%";
|
progressBar.style.width = "0%";
|
||||||
progressBarText.style.display = "block";
|
|
||||||
progressBarText.innerHTML = "0%";
|
progressBarText.innerHTML = "0%";
|
||||||
|
|
||||||
speedElement.style.display = "block";
|
|
||||||
speedElement.innerHTML = "0 Mib/s";
|
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) {
|
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}%`;
|
progressBar.style.width = `${percent}%`;
|
||||||
progressBarText.innerHTML = `${percent}%`;
|
progressBarText.innerHTML = `${percent}%`;
|
||||||
}
|
}
|
||||||
@@ -28,9 +43,19 @@ function resetProgress() {
|
|||||||
|
|
||||||
speedElement.textContent = "0 Mib/s";
|
speedElement.textContent = "0 Mib/s";
|
||||||
speedElement.style.display = "none";
|
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");
|
const evtSource = new EventSource("/download/progress");
|
||||||
evtSource.onmessage = e => {
|
evtSource.onmessage = e => {
|
||||||
if (e.data === "reset") {
|
if (e.data === "reset") {
|
||||||
@@ -40,8 +65,11 @@ evtSource.onmessage = e => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e.data === "done") {
|
if (e.data === "done") {
|
||||||
resetProgress();
|
// Keep progress bar visible for better UX, let toast handle the feedback
|
||||||
window.electronAPI.setProgress(-1);
|
setTimeout(() => {
|
||||||
|
resetProgress();
|
||||||
|
window.electronAPI.setProgress(-1);
|
||||||
|
}, 2000); // Wait 2s so user sees 100% progress
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,11 +77,8 @@ evtSource.onmessage = e => {
|
|||||||
|
|
||||||
if (!isNaN(percent)) {
|
if (!isNaN(percent)) {
|
||||||
updateProgress(percent);
|
updateProgress(percent);
|
||||||
window.electronAPI.setProgress(percent); // update barre des tâches
|
window.electronAPI.setProgress(percent); // Update Task Bar
|
||||||
if (percent >= 100) setTimeout(() => {
|
// Don't hide at 100%, wait for "done" signal instead
|
||||||
resetProgress();
|
|
||||||
window.electronAPI.setProgress(-1); // retire la barre
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,3 +86,13 @@ speedEvt.onmessage = e => {
|
|||||||
speedElement.style.display = "block";
|
speedElement.style.display = "block";
|
||||||
speedElement.textContent = e.data; // ex: "5.2MiB/s"
|
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"
|
||||||
|
};
|
||||||
@@ -15,7 +15,7 @@ function closePanel() {
|
|||||||
settingsPanel.style.display = "none";
|
settingsPanel.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// charge les features
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
const features = await window.electronAPI.getFeatures();
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
@@ -27,15 +27,31 @@ async function loadSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
el.addEventListener("change", () => {
|
el.addEventListener("change", () => {
|
||||||
|
if (key === "theme") return;
|
||||||
let value = el.type === "checkbox" ? el.checked : el.value;
|
let value = el.type === "checkbox" ? el.checked : el.value;
|
||||||
window.electronAPI.setFeature(key, value);
|
window.electronAPI.setFeature(key, value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ouvrir le JSON
|
// Open The JSON
|
||||||
document.getElementById("open-json-btn").addEventListener("click", () => {
|
document.getElementById("open-json-btn").addEventListener("click", () => {
|
||||||
window.topbarAPI.openConfig(); // ton IPC existant
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
54
public/script/toast.js
Normal file
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,29 +1,28 @@
|
|||||||
// Ajout des listeners pour la topbar
|
// Ajout des listeners pour la topbar
|
||||||
function setupTopbarListeners() {
|
function setupTopbarListeners() {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
const { topbarAPI } = window;
|
||||||
const { topbarAPI } = window;
|
if (!topbarAPI) return;
|
||||||
if (!topbarAPI) return;
|
|
||||||
const minBtn = document.getElementById('minimize-btn');
|
|
||||||
const maxBtn = document.getElementById('maximize-btn');
|
|
||||||
const closeBtn = document.getElementById('close-btn');
|
|
||||||
const devtoolsBtn = document.getElementById('devtools-btn');
|
|
||||||
const logsBtn = document.getElementById('logs-btn');
|
|
||||||
const websiteBtn = document.getElementById('website-btn');
|
|
||||||
const wikiBtn = document.getElementById('wiki-btn');
|
|
||||||
const configBtn = document.getElementById('config-btn');
|
|
||||||
|
|
||||||
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
const minBtn = document.getElementById('minimize-btn');
|
||||||
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
const maxBtn = document.getElementById('maximize-btn');
|
||||||
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
const closeBtn = document.getElementById('close-btn');
|
||||||
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
const devtoolsBtn = document.getElementById('devtools-btn');
|
||||||
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
const logsBtn = document.getElementById('logs-btn');
|
||||||
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
const websiteBtn = document.getElementById('website-btn');
|
||||||
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
const wikiBtn = document.getElementById('wiki-btn');
|
||||||
if (configBtn) configBtn.onclick = () => topbarAPI.openConfig();
|
const workshopBtn = document.getElementById('workshop-btn');
|
||||||
});
|
|
||||||
|
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||||
|
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
||||||
|
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
||||||
|
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
||||||
|
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
||||||
|
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
||||||
|
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
||||||
|
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTopbarListeners(); // IF it put it the if check. It don't work. Why ?
|
document.addEventListener('DOMContentLoaded', setupTopbarListeners);
|
||||||
|
|
||||||
const features = await window.electronAPI.getFeatures();
|
const features = await window.electronAPI.getFeatures();
|
||||||
|
|
||||||
|
|||||||
94
public/splash.html
Normal file
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 {
|
.checkmark {
|
||||||
--clr: var(--checkbox-bg-checked);
|
--clr: var(--checkbox-checked-bg-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 1.3em;
|
height: 1.3em;
|
||||||
width: 1.3em;
|
width: 1.3em;
|
||||||
background-color: var(--checkbox-bg-default);
|
background-color: var(--checkbox-unchecked-bg-color);
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
transition: background-color 300ms, border-radius 300ms;
|
transition: background-color 300ms, border-radius 300ms;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,11 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
border: 1px dashed var(--form-input-border-color);
|
border: 1px dashed var(--form-input-border-color);
|
||||||
word-break: break-all;
|
display: inline-flex;
|
||||||
display: inline-block;
|
max-width: clamp(200px, 60vw, 600px);
|
||||||
max-width: 80%;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: var(--form-bg-color);
|
background: var(--form-bg-color);
|
||||||
border: 2px solid var(--mp3-text-color);
|
border: 2px solid var(--audio-only-label-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--mp3-text-color);
|
color: var(--audio-only-label-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
animation: pulse 1.2s infinite;
|
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 */
|
/* Wrapper de la barre de téléchargement */
|
||||||
#downloadProgressWrapper {
|
#downloadProgressWrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background-color: #444;
|
background-color: var(--progress-bar-bg-color, #cccccc);
|
||||||
border-radius: 6px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 10px auto;
|
margin: 0;
|
||||||
display: none;
|
display: none;
|
||||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Barre de progression */
|
/* Barre de progression */
|
||||||
#downloadProgress {
|
#downloadProgress {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0%;
|
width: 0%;
|
||||||
background-color: var(--video-info-link-color, #4caf50);
|
background-color: var(--progress-bar-fill-color, #007bff);
|
||||||
border-radius: 6px 0 0 6px;
|
border-radius: 10px;
|
||||||
transition: width 0.2s ease;
|
transition: width 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +37,7 @@
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
content: "";
|
content: "";
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive pour mobile */
|
/* Responsive pour mobile */
|
||||||
@@ -39,3 +51,39 @@
|
|||||||
font-size: 0.7rem;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
136
public/styles/components/toast.css
Normal file
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;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ form#downloadForm {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
color: var(--form-input-text-color);
|
color: var(--audio-only-label-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#UrlInput {
|
#UrlInput {
|
||||||
@@ -62,6 +62,7 @@ form#downloadForm {
|
|||||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
background-color: var(--form-input-bg-color);
|
background-color: var(--form-input-bg-color);
|
||||||
color: var(--form-input-text-color);
|
color: var(--form-input-text-color);
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#UrlInput:focus {
|
#UrlInput:focus {
|
||||||
@@ -74,3 +75,40 @@ form#downloadForm {
|
|||||||
#UrlInput::placeholder {
|
#UrlInput::placeholder {
|
||||||
color: var(--form-input-placeholder-color);
|
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 {
|
header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
z-index: 1100;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 2vw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
header h1{
|
color: var(--default-text-color);
|
||||||
margin-bottom: 0.2vw;
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
top: 60px;
|
top: 60px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
z-index: 15;
|
||||||
background-color: var(--settings-button-bg-color);
|
background-color: var(--settings-button-bg-color);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
width: 40vw;
|
width: 40vw;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
background-color: var(--settings-bg-primary-color);
|
background-color: var(--settings-modal-bg-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||||
@@ -89,7 +90,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
background: var(--settings-bg-secondary-color);
|
background: var(--settings-section-bg-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
@@ -190,8 +191,18 @@ details summary {
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: var(--form-button-bg-color);
|
background-color: var(--open-json-button-bg-color);
|
||||||
color: #fff;
|
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 {
|
#experimental-args {
|
||||||
@@ -222,6 +233,33 @@ details summary {
|
|||||||
opacity: 1;
|
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 */
|
/* ANIMATION */
|
||||||
@keyframes popIn {
|
@keyframes popIn {
|
||||||
from { transform: scale(0.95); opacity: 0; }
|
from { transform: scale(0.95); opacity: 0; }
|
||||||
@@ -232,3 +270,8 @@ details summary {
|
|||||||
from { transform: translateX(20px); opacity: 0; }
|
from { transform: translateX(20px); opacity: 0; }
|
||||||
to { transform: translateX(0); opacity: 1; }
|
to { transform: translateX(0); opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
@@ -2,20 +2,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1100;
|
|
||||||
|
|
||||||
height: 48px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
background: #1f1f1f98;
|
background: #1f1f1f98;
|
||||||
color: #eaeaea;
|
color: #eaeaea;
|
||||||
|
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* LEFT - custom buttons */
|
/* LEFT - custom buttons */
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding: 1.5em 2em;
|
padding: 1.5em 2em;
|
||||||
background-color: var(--infos-box-color);
|
background-color: var(--video-info-bg-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||||
color: var(--video-info-text-color);
|
color: var(--video-info-text-color);
|
||||||
@@ -69,80 +69,293 @@
|
|||||||
|
|
||||||
/* --- Playlist Mode --- */
|
/* --- Playlist Mode --- */
|
||||||
#videoInfo.playlist-mode {
|
#videoInfo.playlist-mode {
|
||||||
max-width: 900px;
|
max-width: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style du bloc "Playlist détectée" */
|
/* Playlist Header */
|
||||||
#videoInfo.playlist-mode > p {
|
.playlist-header {
|
||||||
text-align: center;
|
display: flex;
|
||||||
font-size: 1.1rem;
|
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;
|
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 */
|
/* Layout liste des vidéos */
|
||||||
#playlistVideos {
|
#playlistVideos {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 18px;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Un item de playlist */
|
/* Carte vidéo playlist */
|
||||||
#playlistVideos > div {
|
.playlist-video-card {
|
||||||
background: var(--playlist-background-color);
|
background: var(--playlist-background-color);
|
||||||
border-radius: 10px;
|
border-radius: 14px;
|
||||||
padding: 10px;
|
overflow: hidden;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
|
||||||
text-align: center;
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#playlistVideos > div p {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: center;
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
flex-wrap: wrap;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#videoInfo.playlist-mode {
|
.playlist-video-card::before {
|
||||||
max-width: 95%;
|
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);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlistVideos img {
|
.video-duration {
|
||||||
width: 100%;
|
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;
|
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 {
|
.copy-btn {
|
||||||
all: unset;
|
background: rgba(255, 255, 255, 0.06);
|
||||||
background: transparent;
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 4px;
|
padding: 0.5rem;
|
||||||
vertical-align: middle;
|
border-radius: 8px;
|
||||||
transition: transform 0.1s ease, color 0.1s ease, opacity 0.15s ease;
|
transition: all 0.2s ease;
|
||||||
font-size: 1em;
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlistVideos > div p a{
|
.copy-btn svg {
|
||||||
text-decoration: none;
|
width: 18px;
|
||||||
color: var(--video-info-link-color);
|
height: 18px;
|
||||||
|
color: var(--video-info-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlistVideos > div p a:hover{
|
.copy-btn:hover {
|
||||||
text-decoration: underline;
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#playlistVideos {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-meta {
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
#videoInfo {
|
#videoInfo {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
@@ -152,4 +365,21 @@
|
|||||||
#videoInfo h3 {
|
#videoInfo h3 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#playlistVideos {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-header {
|
||||||
|
padding: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
/* Layout */
|
/* Layout */
|
||||||
@import url("layout/form.css");
|
@import url("layout/topbar.css");
|
||||||
@import url("layout/settingsPanel.css");
|
|
||||||
@import url("layout/container.css");
|
|
||||||
@import url("layout/header.css");
|
@import url("layout/header.css");
|
||||||
|
@import url("layout/form.css");
|
||||||
|
@import url("layout/container.css");
|
||||||
|
@import url("layout/settingsPanel.css");
|
||||||
@import url("layout/videoinfo.css");
|
@import url("layout/videoinfo.css");
|
||||||
|
|
||||||
/* Components */
|
/* Components */
|
||||||
@import url("components/checkbox.css");
|
@import url("components/checkbox.css");
|
||||||
@import url("components/loader.css");
|
@import url("components/loader.css");
|
||||||
|
|
||||||
@import url("theme/themeimport.css");
|
|
||||||
@import url("components/editpathbutton.css");
|
@import url("components/editpathbutton.css");
|
||||||
@import url("components/progressBar.css");
|
@import url("components/progressBar.css");
|
||||||
|
@import url("components/toast.css");
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
|
|
||||||
#app {
|
|
||||||
display: block;
|
/* Because why not */
|
||||||
opacity: 0;
|
#reference{
|
||||||
transition: opacity 0.6s ease;
|
position: fixed;
|
||||||
}
|
pointer-events: none;
|
||||||
#app.visible {
|
bottom:0vh;
|
||||||
opacity: 1;
|
left: 10px;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #b0b0b02f;
|
||||||
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
html,body{
|
html,body{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -34,43 +38,45 @@ html,body{
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body{
|
||||||
margin: 0;
|
background-color: var(--background-color);
|
||||||
background-color: var(--background-color);
|
color: var(--default-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
z-index: 1100;
|
||||||
color: var(--default-text-color);
|
height: 48px;
|
||||||
display: flex;
|
}
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
main {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 48px);
|
||||||
|
margin-top: 48px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Because why not */
|
/* Scrollbar pour main */
|
||||||
#reference{
|
main::-webkit-scrollbar {
|
||||||
position: fixed;
|
|
||||||
pointer-events: none;
|
|
||||||
bottom:0vh;
|
|
||||||
left: 10px;
|
|
||||||
font-size: 1em;
|
|
||||||
color: #b0b0b02f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar globale pour Chromium/Electron */
|
|
||||||
body::-webkit-scrollbar {
|
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::-webkit-scrollbar-track {
|
main::-webkit-scrollbar-track {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::-webkit-scrollbar-thumb {
|
main::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
@@ -78,36 +84,32 @@ body::-webkit-scrollbar-thumb {
|
|||||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
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);
|
background-color: rgba(255, 255, 255, 0.35);
|
||||||
transform: scaleX(1.05);
|
transform: scaleX(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Thème clair */
|
/* Thème clair */
|
||||||
body.light::-webkit-scrollbar-track {
|
body.light main::-webkit-scrollbar-track {
|
||||||
background: rgba(0, 0, 0, 0.05);
|
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);
|
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);
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container{
|
.container{
|
||||||
margin-top: 50px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header p{
|
#subtitle{
|
||||||
color: var(--subtitle-color);
|
color: var(--subtitle-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-inline{
|
|
||||||
color: var(--mp3-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
body,
|
||||||
.container,
|
.container,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -1,103 +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;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #00196a;
|
|
||||||
--settings-button-text-color: #dcdcdc;
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #0b0528;
|
|
||||||
--settings-bg-secondary-color: #1c1055;
|
|
||||||
|
|
||||||
--settings-text-color: #cecece;
|
|
||||||
--settings-subtitle-color: #adadad;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
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,49 +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;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #1e1e1e;
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #1e1e1e;
|
|
||||||
--settings-bg-secondary-color: #2a2a2a;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #aaa;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,61 +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;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #1e222ace;
|
|
||||||
--settings-button-text-color: #b3b3b4;
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #1e222a;
|
|
||||||
--settings-bg-secondary-color: #272d38;
|
|
||||||
|
|
||||||
--settings-text-color: #bababa;
|
|
||||||
--settings-subtitle-color: #cacaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
body.drift .download-path{
|
|
||||||
color: #ededed;
|
|
||||||
}
|
|
||||||
@@ -1,49 +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;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #1e1e1e;
|
|
||||||
--settings-button-text-color: #dcdcdc;
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #1e1e1e;
|
|
||||||
--settings-bg-secondary-color: #2d2d2d;
|
|
||||||
|
|
||||||
--settings-text-color: #cecece;
|
|
||||||
--settings-subtitle-color: #adadad;
|
|
||||||
}
|
|
||||||
@@ -1,48 +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;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #e4e4e4;
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #cbcbcb;
|
|
||||||
--settings-bg-secondary-color: #cbcbcb;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #656565;
|
|
||||||
}
|
|
||||||
@@ -1,59 +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;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #202038;
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #242454;
|
|
||||||
--settings-bg-secondary-color: #2f2f6f;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #d2d2d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.neon .download-path {
|
|
||||||
color: var(--default-text-color);
|
|
||||||
}
|
|
||||||
@@ -1,58 +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;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #2a2a2a;
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #2a2a2a;
|
|
||||||
--settings-bg-secondary-color: #313131;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #7a7979;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.nf .download-path {
|
|
||||||
color: var(--default-text-color);
|
|
||||||
}
|
|
||||||
@@ -1,58 +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;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: rgba(29, 0, 0, 0.784);
|
|
||||||
--settings-button-text-color: var(--default-text-color);
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #121212;
|
|
||||||
--settings-bg-secondary-color: #1a1a1a;
|
|
||||||
--settings-text-color: var(--default-text-color);
|
|
||||||
--settings-subtitle-color: #7a7979;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.cyberpunk .download-path {
|
|
||||||
color: var(--default-text-color);
|
|
||||||
}
|
|
||||||
@@ -1,59 +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;
|
|
||||||
|
|
||||||
/* Settings */
|
|
||||||
--settings-button-bg-color: #1a1a1a;
|
|
||||||
--settings-button-text-color: #dcdcdc;
|
|
||||||
|
|
||||||
--settings-bg-primary-color: #1a1a1a;
|
|
||||||
--settings-bg-secondary-color: #292929;
|
|
||||||
|
|
||||||
--settings-text-color: #cecece;
|
|
||||||
--settings-subtitle-color: #adadad;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optionnel: background piments */
|
|
||||||
body.spicy {
|
|
||||||
background-image: url('../../assets/images/Spicy.png');
|
|
||||||
background-size:cover;
|
|
||||||
background-position: center;
|
|
||||||
background-attachment: fixed;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
9
public/styles/theme/themeimport.css
vendored
9
public/styles/theme/themeimport.css
vendored
@@ -1,9 +0,0 @@
|
|||||||
@import url("chirac-theme.css");
|
|
||||||
@import url("dark-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;
|
--background-color: #001224;
|
||||||
--default-text-color: #eee;
|
--default-text-color: #eee;
|
||||||
--subtitle-color: #007bff;
|
--subtitle-color: #007bff;
|
||||||
--mp3-text-color: #000000;
|
--audio-only-label-color: #000000;
|
||||||
|
|
||||||
/* Formulaire */
|
/* Formulaire */
|
||||||
--form-bg-color: #ffffff;
|
--form-bg-color: #ffffff;
|
||||||
@@ -15,18 +15,23 @@
|
|||||||
--form-button-bg-color: #007bff;
|
--form-button-bg-color: #007bff;
|
||||||
--form-button-text-color: #ffffff;
|
--form-button-text-color: #ffffff;
|
||||||
--form-button-bg-hover-color: #0056b3;
|
--form-button-bg-hover-color: #0056b3;
|
||||||
|
--paste-button-icon-color: #ffffff;
|
||||||
|
|
||||||
/* Checkbox */
|
/* Checkbox */
|
||||||
--checkbox-bg-default: #cccccc;
|
--checkbox-unchecked-bg-color: #cccccc;
|
||||||
--checkbox-bg-checked: #3B82F6;
|
--checkbox-checked-bg-color: #3B82F6;
|
||||||
--checkbox-checkmark-border-color: #E0E0E2;
|
--checkbox-checkmark-border-color: #E0E0E2;
|
||||||
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
--checkbox-pulse-color: rgba(59, 130, 246, 0.5);
|
||||||
|
|
||||||
/* Download status */
|
/* Download status */
|
||||||
--download-status-color: #007bff;
|
--download-status-color: #007bff;
|
||||||
|
|
||||||
|
/* Progress bar */
|
||||||
|
--progress-bar-bg-color: #cccccc;
|
||||||
|
--progress-bar-fill-color: #007bff;
|
||||||
|
|
||||||
/* Video Info Box */
|
/* Video Info Box */
|
||||||
--infos-box-color: #f0f0f0;
|
--video-info-bg-color: #f0f0f0;
|
||||||
--video-info-text-color: #222222;
|
--video-info-text-color: #222222;
|
||||||
--video-info-heading-color: #007bff;
|
--video-info-heading-color: #007bff;
|
||||||
--video-info-list-color: #444444;
|
--video-info-list-color: #444444;
|
||||||
@@ -34,12 +39,15 @@
|
|||||||
--video-info-link-color: #007bff;
|
--video-info-link-color: #007bff;
|
||||||
--video-info-link-hover-color: #0056b3;
|
--video-info-link-hover-color: #0056b3;
|
||||||
|
|
||||||
|
/* Playlist */
|
||||||
|
--playlist-background-color: #4f4f4f;
|
||||||
|
|
||||||
/* Settings */
|
/* Settings */
|
||||||
--settings-button-bg-color: var(--form-bg-color);
|
--settings-button-bg-color: var(--form-bg-color);
|
||||||
--settings-button-text-color: var(--default-text-color);
|
--settings-button-text-color: var(--default-text-color);
|
||||||
|
|
||||||
--settings-bg-primary-color: var(--form-bg-color);
|
--settings-modal-bg-color: var(--form-bg-color);
|
||||||
--settings-bg-secondary-color: var(--form-bg-color);
|
--settings-section-bg-color: var(--form-bg-color);
|
||||||
--settings-text-color: var(--default-text-color);
|
--settings-text-color: var(--default-text-color);
|
||||||
--settings-subtitle-color: #aaa;
|
--settings-subtitle-color: #aaa;
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,10 +1,13 @@
|
|||||||
const { fetchDownload } = require("../services/download.services");
|
const { fetchDownload, cancelDownload } = require("../services/download.services");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
const { isValidUrl, isSafePath } = require("../helpers/validation");
|
const { isValidUrl, isSafePath } = require("../helpers/validation.helpers");
|
||||||
const { notifyDownloadFinished } = require("../helpers/notify");
|
const { notifyDownloadFinished } = require("../helpers/notify.helpers");
|
||||||
|
const { configFeatures } = require("../../config");
|
||||||
|
|
||||||
const listeners = [];
|
const listeners = [];
|
||||||
const speedListeners = [];
|
const speedListeners = [];
|
||||||
|
const stageListeners = [];
|
||||||
|
const playlistInfoListeners = [];
|
||||||
|
|
||||||
async function downloadController(req, res) {
|
async function downloadController(req, res) {
|
||||||
try {
|
try {
|
||||||
@@ -13,18 +16,23 @@ async function downloadController(req, res) {
|
|||||||
audioOnly: req.body.audioOnly === "1",
|
audioOnly: req.body.audioOnly === "1",
|
||||||
quality: req.body.quality || "best",
|
quality: req.body.quality || "best",
|
||||||
outputFolder: req.body.savePath || undefined,
|
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.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.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);
|
// Get output folder when the download is finished
|
||||||
notifyDownloadFinished(filePath);
|
const outputFolder = await fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners);
|
||||||
res.send("✅ Download Done !");
|
notifyDownloadFinished(outputFolder, configFeatures.notifySystem);
|
||||||
|
res.send("Download Done !");
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Server Error in /download : ${err.message}`);
|
logger.error(`Server Error in /download : ${err.message}`);
|
||||||
res.status(500).send(`❌ Server Error`);
|
res.status(500).send(`Server Error`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,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,19 +1,20 @@
|
|||||||
const { fetchInfo } = require("../services/info.services");
|
const { fetchInfo } = require("../services/info.services");
|
||||||
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo");
|
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
const { isValidUrl } = require("../helpers/validation");
|
const { isValidUrl } = require("../helpers/validation.helpers");
|
||||||
|
|
||||||
async function infoController(req, res){
|
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, url non-string ou url invalide*/
|
// 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");
|
|
||||||
|
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}`);
|
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")
|
logger.info("Estimated Data Type: Playlist")
|
||||||
|
|
||||||
} else{
|
} else{
|
||||||
@@ -21,7 +22,32 @@ async function infoController(req, res){
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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") {
|
if (data._type === "playlist") {
|
||||||
|
|
||||||
@@ -38,7 +64,7 @@ async function infoController(req, res){
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
|
logger.error(`Info controller error: ${err && err.message ? err.message : err}`);
|
||||||
return res.status(500).send(`❌ Unable to fetch info`);
|
return res.status(500).send(`Unable to fetch info`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const getUserBrowser = require("./getBrowser");
|
const getUserBrowser = require("./getBrowser.helpers.js");
|
||||||
const { ffmpegPath, denoPath} = require("./path");
|
const { ffmpegPath, denoPath} = require("./path.helpers.js");
|
||||||
const { configFeatures } = require("../../config.js");
|
const { configFeatures } = require("../../config.js");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger.js");
|
||||||
|
|
||||||
function validateCodec(codec){
|
function validateCodec(codec){
|
||||||
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const notify = require("./notify")
|
const notify = require("./notify.helpers")
|
||||||
|
const { logger } = require("../logger");
|
||||||
|
|
||||||
function getUserBrowser() {
|
function getUserBrowser() {
|
||||||
const userProfile = os.homedir();
|
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 = [
|
const browsers = [
|
||||||
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
{ name: "firefox", path: path.join(userProfile, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles") },
|
||||||
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
// { name: "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") }
|
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Search for an available browser
|
||||||
for (const browser of browsers) {
|
for (const browser of browsers) {
|
||||||
if (fs.existsSync(browser.path)) {
|
if (fs.existsSync(browser.path)) {
|
||||||
|
logger.info(`Browser found: ${browser.name}`);
|
||||||
return browser.name;
|
return browser.name;
|
||||||
} else{
|
|
||||||
notify.notifyFirefoxBrowserMissing();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notify.notifyCookiesBrowserError()
|
|
||||||
return ;
|
// No browser found - notify user
|
||||||
|
logger.warn("No supported browser found on the system");
|
||||||
|
notify.notifyFirefoxBrowserMissing();
|
||||||
|
|
||||||
|
// Fallback: return "Firefox" to let YT-DLP manage error
|
||||||
|
// this avoid app crash
|
||||||
|
return "firefox";
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = getUserBrowser
|
module.exports = getUserBrowser
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
const { Notification, shell } = require("electron");
|
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({
|
const notif = new Notification({
|
||||||
title: "Freedom Loader",
|
title: "Freedom Loader",
|
||||||
body: "Your download is complete, click here to open it.",
|
body: "Your download is complete, click here to open it.",
|
||||||
icon: iconPath,
|
icon: iconPaths.confirm,
|
||||||
});
|
});
|
||||||
|
|
||||||
notif.on("click", () => shell.openPath(folder));
|
notif.on("click", () => shell.openPath(folder));
|
||||||
@@ -14,11 +15,10 @@ function notifyDownloadFinished(folder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function notifyCookiesBrowserError(){
|
function notifyCookiesBrowserError(){
|
||||||
const iconPath = path.join(process.resourcesPath, "error.png");
|
|
||||||
const notif = new Notification({
|
const notif = new Notification({
|
||||||
title: "Cookies Error",
|
title: "Cookies Error",
|
||||||
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
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"));
|
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"));
|
||||||
@@ -26,11 +26,10 @@ function notifyCookiesBrowserError(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function notifyFirefoxBrowserMissing(){
|
function notifyFirefoxBrowserMissing(){
|
||||||
const iconPath = path.join(process.ressourcesPath, "error.png");
|
|
||||||
const notif = new Notification({
|
const notif = new Notification({
|
||||||
title: "Firefox Missing",
|
title: "Firefox Missing",
|
||||||
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
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"))
|
notif.on("click", () => shell.openExternal("https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"))
|
||||||
@@ -2,12 +2,6 @@ function parseVideo(data) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
type: "video",
|
type: "video",
|
||||||
// id: data.id,
|
|
||||||
// title: data.title,
|
|
||||||
// url: data.webpage_url,
|
|
||||||
// duration: data.duration,
|
|
||||||
// thumbnail: data.thumbnail,
|
|
||||||
// uploader: data.uploader
|
|
||||||
...data
|
...data
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
123
server/helpers/path.helpers.js
Normal file
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,33 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
const { app } = require("electron");
|
|
||||||
const config = require("../../config");
|
|
||||||
|
|
||||||
const { logger } = require("../logger.js");
|
|
||||||
|
|
||||||
let userYtDlp;
|
|
||||||
let ffmpegPath;
|
|
||||||
let denoPath;
|
|
||||||
|
|
||||||
const sourceYtDlp = path.join(process.resourcesPath, "binaries","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, "binaries","ffmpeg.exe");
|
|
||||||
denoPath = path.join(process.resourcesPath, "binaries","deno.exe");
|
|
||||||
|
|
||||||
if (!fs.existsSync(userYtDlp)) {
|
|
||||||
fs.copyFileSync(sourceYtDlp, userYtDlp);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userYtDlp){ logger.error("Missing YT-DLP")}
|
|
||||||
if (!ffmpegPath){ logger.error("Missing FFMPEG")}
|
|
||||||
if (!denoPath){ logger.error("Missing DENO")}
|
|
||||||
|
|
||||||
module.exports = { userYtDlp, ffmpegPath, denoPath };
|
|
||||||
@@ -3,7 +3,7 @@ const RateLimit = require("express-rate-limit");
|
|||||||
|
|
||||||
/* Rate Limite */
|
/* Rate Limite */
|
||||||
const rateLimite = RateLimit({
|
const rateLimite = RateLimit({
|
||||||
windowMs: 15 * 60, // 15 minutes
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
max: 5, // limit each IP to 100 requests per windowMs on the root path
|
max: 5, // limit each IP to 100 requests per windowMs on the root path
|
||||||
message: "Too many requests, please try again later.",
|
message: "Too many requests, please try again later.",
|
||||||
statusCode: 429, // HTTP status code for "Too Many Requests"
|
statusCode: 429, // HTTP status code for "Too Many Requests"
|
||||||
@@ -13,7 +13,7 @@ function isValidUrl(url) {
|
|||||||
|
|
||||||
function isSafePath(folder) {
|
function isSafePath(folder) {
|
||||||
if (!folder || folder.length < 3) return false;
|
if (!folder || folder.length < 3) return false;
|
||||||
const unsafe = ["System32", "/etc", "\\Windows"];
|
const unsafe = ["System32", "\\Windows"];
|
||||||
const resolved = path.resolve(folder);
|
const resolved = path.resolve(folder);
|
||||||
return !unsafe.some(u => resolved.includes(u));
|
return !unsafe.some(u => resolved.includes(u));
|
||||||
}
|
}
|
||||||
@@ -3,20 +3,24 @@ const DailyRotateFile = require("winston-daily-rotate-file");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const os = require("os");
|
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");
|
const logDir = path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs");
|
||||||
|
|
||||||
// Création du dossier "logs" si nécessaire
|
// Create "Logs" folder if needed
|
||||||
fs.mkdirSync(logDir, { recursive: true });
|
try {
|
||||||
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to create log directory: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
const logFormat = format.combine(
|
const logFormat = format.combine(
|
||||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Configuration du logger
|
// Logger configuration
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
level: "info",
|
level: "info",
|
||||||
format: logFormat,
|
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() {
|
function logSessionStart() {
|
||||||
logger.info(getSessionStartLine());
|
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
|
||||||
logger.info(`Application Version: ${config.version}`)
|
logger.info(`Application Version: ${config.version}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function logSessionEnd() {
|
function logSessionEnd() {
|
||||||
logger.info(getSessionEndLine());
|
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
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("/", downloadController);
|
||||||
|
router.post("/cancel", cancelDownloadController);
|
||||||
|
|
||||||
// SSE pour la progression du téléchargement
|
// SSE for download progress
|
||||||
router.get("/progress", progressController);
|
router.get("/progress", progressController);
|
||||||
router.get("/speed", speedController);
|
router.get("/speed", speedController);
|
||||||
|
router.get("/stage", stageController);
|
||||||
|
router.get("/playlist-info", playlistInfoController);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -1,47 +1,23 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { logger, logSessionEnd } = require("./logger");
|
const { logger, logSessionEnd } = require("./logger");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
const { execFile } = require("child_process");
|
const { rateLimite } = require("./helpers/rateLimit.helpers");
|
||||||
const { userYtDlp } = require("./helpers/path");
|
|
||||||
const { rateLimite } = require("./helpers/rateLimit")
|
|
||||||
|
|
||||||
const app = express();
|
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
|
// Middlewares
|
||||||
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(express.static(path.join(__dirname, "../public")));
|
app.use(express.static(path.join(__dirname, "../public")));
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use("/download", require("./routes/download.route"));
|
app.use("/download", require("./routes/download.route"));
|
||||||
app.use("/info", require("./routes/info.route"));
|
app.use("/info", require("./routes/info.route"));
|
||||||
|
app.get("/", rateLimite, (req, res) => {
|
||||||
app.get("/", rateLimite ,(req, res) => {
|
|
||||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fonction pour démarrer le serveur
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = app.listen(config.applicationPort, () => {
|
const server = app.listen(config.applicationPort, () => {
|
||||||
@@ -50,19 +26,19 @@ async function startServer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.on("error", (err) => {
|
server.on("error", (err) => {
|
||||||
logger.error("Express Server Error :", err);
|
logger.error("Express server error:", err);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
const gracefulExit = () => {
|
const gracefulExit = () => {
|
||||||
logSessionEnd();
|
logSessionEnd();
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
logger.info("Express Server close cleanly.");
|
logger.info("Express server closed cleanly.");
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("SIGINT", gracefulExit);
|
process.on("SIGINT", gracefulExit);
|
||||||
process.on("SIGTERM", gracefulExit);
|
process.on("SIGTERM", gracefulExit);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,132 @@
|
|||||||
const { execFile } = require("child_process");
|
const { execFile } = require("child_process");
|
||||||
const { userYtDlp } = require("../helpers/path");
|
const { userYtDlp, defaultDownloadFolder } = require("../helpers/path.helpers");
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
const { buildYtDlpArgs } = require("../helpers/buildArgs");
|
const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
|
||||||
const notify = require("../helpers/notify")
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader");
|
logger.info(`CONFIG createPlaylistFolders: ${configFeatures.createPlaylistFolders}`);
|
||||||
fs.mkdirSync(outputFolder, { recursive: true });
|
|
||||||
|
|
||||||
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(" ")}`);
|
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||||
|
|
||||||
const child = execFile(userYtDlp, args);
|
const child = execFile(userYtDlp, args);
|
||||||
|
currentDownloadProcess = child;
|
||||||
|
|
||||||
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
|
child.on("error", err => reject(new Error(`YT-DLP Error : ${err.message}`)));
|
||||||
|
|
||||||
child.on("close", code => {
|
child.on("close", code => {
|
||||||
|
currentDownloadProcess = null;
|
||||||
listeners.forEach(fn => fn("done"));
|
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}`));
|
else reject(new Error(`YT-DLP failed with code : ${code}`));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,8 +136,11 @@ function fetchDownload(options, listeners, speedListeners) {
|
|||||||
if (!line.trim()) return;
|
if (!line.trim()) return;
|
||||||
logger.info(`[yt-dlp] ${line}`);
|
logger.info(`[yt-dlp] ${line}`);
|
||||||
|
|
||||||
/* Barre de Chargement*/
|
// Progress Bar
|
||||||
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
|
if (line.startsWith("[download] Destination:")) {
|
||||||
|
listeners.forEach(fn => fn("reset"));
|
||||||
|
stageListeners.forEach(fn => fn("Downloading..."));
|
||||||
|
}
|
||||||
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
||||||
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
|
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)) {
|
if (line.match(/ERROR: Could not copy .* cookie database/i)) {
|
||||||
notify.notifyCookiesBrowserError();
|
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 { execFile } = require("child_process");
|
||||||
const { userYtDlp, denoPath } = require("../helpers/path");
|
const { userYtDlp, denoPath } = require("../helpers/path.helpers");
|
||||||
const getUserBrowser = require("../helpers/getBrowser");
|
const getUserBrowser = require("../helpers/getBrowser.helpers");
|
||||||
const { logger } = require("../logger");
|
const { logger } = require("../logger");
|
||||||
|
|
||||||
function fetchInfo(url) {
|
function fetchInfo(url) {
|
||||||
@@ -17,8 +17,8 @@ function fetchInfo(url) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
execFile(userYtDlp,[...args, url],
|
execFile(userYtDlp,[...args, url],
|
||||||
{ timeout: 30_000, // 30s, si jamais plus, abandon de la requête
|
{ timeout: 30_000, // 30s, if more timeout
|
||||||
maxBuffer: 10 * 1024 * 1024 }, // 10MO de réponse max (Par défaut: 200ko)
|
maxBuffer: 10 * 1024 * 1024 }, // 10MO max response (Default: 200ko)
|
||||||
|
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
const { autoUpdater } = require("electron-updater");
|
|
||||||
const { app, Notification, shell} = require("electron");
|
|
||||||
const { logger } = require("./logger");
|
|
||||||
|
|
||||||
function AutoUpdater() {
|
|
||||||
|
|
||||||
autoUpdater.on("update-available", (info) => {
|
|
||||||
logger.info(`New Version Available : ${info.version}`);
|
|
||||||
new Notification({
|
|
||||||
title: "Freedom Loader",
|
|
||||||
body: `New Version Available : ${info.version}. Application will restart`
|
|
||||||
}).show();
|
|
||||||
});
|
|
||||||
|
|
||||||
autoUpdater.on("update-downloaded", (info) => {
|
|
||||||
logger.info(`Update Downloaded : ${info.version}`);
|
|
||||||
new Notification({
|
|
||||||
title: "Freedom Loader",
|
|
||||||
body: `Update ${info.version} downloaded.`
|
|
||||||
}).on("click", () =>
|
|
||||||
shell.openExternal("https://github.com/MasterAcnolo/Freedom-Loader/releases/latest")
|
|
||||||
).show();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
autoUpdater.quitAndInstall();
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
autoUpdater.on("error", (err) => {
|
|
||||||
logger.error("Auto Update Error :", err.message);
|
|
||||||
new Notification({
|
|
||||||
title: "Freedom Loader - Update Error",
|
|
||||||
body: err.message
|
|
||||||
}).show();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
|
||||||
try {
|
|
||||||
await autoUpdater.checkForUpdates();
|
|
||||||
logger.info("Update check completed");
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Error during update check :", err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { AutoUpdater };
|
|
||||||
96
theme/template.theme.json
Normal file
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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user