Compare commits
151 Commits
1.4.2
...
9b2c6c9552
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b2c6c9552 | ||
|
|
871ebe2498 | ||
|
|
730069a3c6 | ||
|
|
7a0cbd7456 | ||
|
|
5d676b45e7 | ||
|
|
a7f3d84a7d | ||
|
|
fbce5e5b7b | ||
|
|
cc7cc38392 | ||
|
|
6a9e5a063b | ||
|
|
cf8c95711c | ||
|
|
3683e4245f | ||
|
|
01affd8ddf | ||
|
|
faba3f516b | ||
|
|
c1d9448a2b | ||
|
|
1bff8376b4 | ||
|
|
3f44c01bd7 | ||
|
|
d0bdd887e3 | ||
|
|
609ac2eadb | ||
|
|
c484d322bf | ||
|
|
b321a1d164 | ||
|
|
89e6fcf9bc | ||
|
|
ba2f317551 | ||
|
|
553e383c94 | ||
|
|
89cc0b85a3 | ||
|
|
abd0bf9006 | ||
|
|
653f73805e | ||
|
|
4f03c5876f | ||
|
|
a5f05f32af | ||
|
|
0d1b1e4919 | ||
|
|
0e77a94ec7 | ||
|
|
7f481d20b2 | ||
|
|
1ff32ca021 | ||
|
|
90d8a9f65d | ||
|
|
d5965c2ce0 | ||
|
|
15f59a6093 | ||
|
|
8b31436e25 | ||
|
|
7d5203001b | ||
|
|
60709c4fad | ||
|
|
2fb6509edc | ||
|
|
eebce83976 | ||
|
|
a56a070bc8 | ||
|
|
e1ef7032bb | ||
|
|
e7333b7b6b | ||
|
|
e2b51d32d1 | ||
|
|
81515c6ab8 | ||
|
|
c88189b44f | ||
|
|
b745ebd29f | ||
|
|
13ee4c170b | ||
|
|
0218664c5a | ||
|
|
6afc5e06d6 | ||
|
|
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
@@ -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
@@ -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
|
||||||
25
.gitignore
vendored
@@ -1,11 +1,24 @@
|
|||||||
|
# Dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/downloads
|
|
||||||
|
# Build Output Folder
|
||||||
/dist
|
/dist
|
||||||
/out
|
/out
|
||||||
|
/srpm-out
|
||||||
|
|
||||||
/ressources/ffmpeg.exe
|
# Config JSON File. Create it and fill it if you want to have dev only settings
|
||||||
/ressources/ffprobe.exe
|
config/config.dev.json
|
||||||
/ressources/deno.exe
|
|
||||||
|
|
||||||
logs/*.json
|
# IDE
|
||||||
logs/*.log
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
|
||||||
|
# Linux executable
|
||||||
|
ffmpeg
|
||||||
|
ffprobe
|
||||||
|
deno
|
||||||
|
|
||||||
|
# Windows Executable
|
||||||
|
ffmpeg.exe
|
||||||
|
ffprobe.exe
|
||||||
|
deno.exe
|
||||||
96
BINARIES.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# External binaries
|
||||||
|
|
||||||
|
This project depends on three external tools that are **not bundled in the repository**.
|
||||||
|
You must download them manually and place each binary in the correct folder before building or running the app locally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required tools
|
||||||
|
|
||||||
|
| Tool | Role |
|
||||||
|
|---------|---------------------------------------------|
|
||||||
|
| yt-dlp | Video and audio download engine |
|
||||||
|
| ffmpeg | Media encoding, transcoding, muxing |
|
||||||
|
| ffprobe | Media file analysis and metadata extraction |
|
||||||
|
| deno | JavaScript / TypeScript runtime |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Folder structure
|
||||||
|
|
||||||
|
Place the binaries exactly as shown below.
|
||||||
|
Do not rename them — the application resolves them by these exact names.
|
||||||
|
|
||||||
|
```
|
||||||
|
resources/
|
||||||
|
└── binaries/
|
||||||
|
├── linux/
|
||||||
|
│ ├── yt-dlp
|
||||||
|
│ ├── ffmpeg
|
||||||
|
│ ├── ffprobe
|
||||||
|
│ └── deno
|
||||||
|
└── win-32/
|
||||||
|
├── yt-dlp.exe
|
||||||
|
├── ffmpeg.exe
|
||||||
|
├── ffprobe.exe
|
||||||
|
└── deno.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Download links
|
||||||
|
|
||||||
|
### yt-dlp
|
||||||
|
|
||||||
|
| Platform | URL |
|
||||||
|
|----------|----------------------------------------------------------------------|
|
||||||
|
| Windows | https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe |
|
||||||
|
| Linux | https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp |
|
||||||
|
|
||||||
|
Rename the downloaded file to `yt-dlp` (or `yt-dlp.exe` on Windows).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ffmpeg and ffprobe
|
||||||
|
|
||||||
|
**Windows and Linux** — both binaries are included in the same archive.
|
||||||
|
|
||||||
|
| Platform | URL |
|
||||||
|
|-------------|-----------------------------------------------------------------------------------------------------------|
|
||||||
|
| Windows x64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip |
|
||||||
|
| Linux x64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz |
|
||||||
|
| Linux arm64 | https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz |
|
||||||
|
|
||||||
|
Extract the archive and copy `ffmpeg` and `ffprobe` from the `bin/` subfolder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### deno
|
||||||
|
|
||||||
|
| Platform | URL |
|
||||||
|
|-------------|----------------------------------------------------------------------------------------------|
|
||||||
|
| Windows x64 | https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip |
|
||||||
|
| Linux x64 | https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip |
|
||||||
|
| Linux arm64 | https://github.com/denoland/deno/releases/latest/download/deno-aarch64-unknown-linux-gnu.zip |
|
||||||
|
|
||||||
|
Each archive contains a single binary. Rename it to `deno` (or `deno.exe` on Windows).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permissions (Linux )
|
||||||
|
|
||||||
|
After placing the binaries, make them executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x resources/binaries/linux/yt-dlp
|
||||||
|
chmod +x resources/binaries/linux/ffmpeg
|
||||||
|
chmod +x resources/binaries/linux/ffprobe
|
||||||
|
chmod +x resources/binaries/linux/deno
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- These binaries are excluded from version control via `.gitignore`.
|
||||||
|
- Always use the latest stable release of each tool.
|
||||||
@@ -105,7 +105,7 @@ Freedom-Loader/
|
|||||||
│ ├── controller/ # Business logic
|
│ ├── controller/ # Business logic
|
||||||
│ └── helpers/ # Utility functions
|
│ └── helpers/ # Utility functions
|
||||||
├── public/ # Frontend (HTML, CSS, JS)
|
├── public/ # Frontend (HTML, CSS, JS)
|
||||||
└── ressources/ # Binaries (yt-dlp, ffmpeg, etc.)
|
└── resources/ # Binaries (yt-dlp, ffmpeg, etc.)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
284
README.md
@@ -1,15 +1,16 @@
|
|||||||
<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 and Linux**
|
||||||
|
|
||||||
[](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/)
|
||||||
|
[](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>
|
<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,38 +80,88 @@ 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
|
||||||
|
|
||||||
|
[](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
|
||||||
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
|
[](https://snapcraft.io/freedom-loader)
|
||||||
|
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- **Operating System**: Windows 10 or later
|
- **Windows**: Windows 10 or later
|
||||||
- **Browser**: Mozilla Firefox (required for cookie extraction)
|
- **Linux**: any modern x86_64 distribution
|
||||||
- [Download Firefox](https://www.firefox.com)
|
- **Browser**: Mozilla Firefox ([download](https://www.firefox.com)) required for cookie extraction, on both platforms
|
||||||
|
|
||||||
### Standard Installation
|
### Windows
|
||||||
|
|
||||||
1. Download the latest release from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
1. Download the latest `.exe` from the [Releases page](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
2. Run the downloaded `.exe` installer
|
2. Run the installer (click "More info" → "Run anyway" if Windows Defender warns you)
|
||||||
3. If Windows Defender shows a warning, click "More info" then "Run anyway"
|
3. Follow the installation wizard
|
||||||
4. Follow the installation wizard
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
#### Fedora (via Copr — recommended)
|
||||||
|
|
||||||
|
Installing via Copr lets `dnf` handle future updates automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf copr enable masteracnolo/freedom-loader
|
||||||
|
sudo dnf install freedom-loader
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Debian / Ubuntu (.deb)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download the latest .deb from the Releases page, then:
|
||||||
|
sudo apt install ./freedom-loader_1.x.x_amd64.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Snap (any distro with snapd)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo snap install freedom-loader
|
||||||
|
```
|
||||||
|
|
||||||
|
#### RPM (Fedora/RHEL/openSUSE, without Copr)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download the latest .rpm from the Releases page, then:
|
||||||
|
sudo dnf install ./freedom-loader-1.x.y.x86_64.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### AppImage (any distro, no installation needed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download the latest .AppImage from the Releases page, then:
|
||||||
|
chmod +x Freedom-Loader-1.x.y.AppImage
|
||||||
|
./Freedom-Loader-1.x.y.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
Once installed via any method, launch Freedom Loader from your application menu, or from a terminal with:
|
||||||
|
```bash
|
||||||
|
freedom-loader
|
||||||
|
```
|
||||||
|
|
||||||
### File Locations
|
### File Locations
|
||||||
|
|
||||||
Downloaded files are stored in:
|
| | Windows | Linux |
|
||||||
```
|
|---------------|---------------------------------------------------------|--------------------------------------|
|
||||||
C:\Users\[USERNAME]\Downloads\Freedom Loader
|
| **Downloads** | `C:\Users\[USERNAME]\Downloads\Freedom Loader` | `~/Downloads/Freedom Loader` |
|
||||||
```
|
| **Logs** | `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\` | `~/.local/share/FreedomLoader/logs/` |
|
||||||
> [!NOTE]
|
|
||||||
> *You can change it whenever you want. Just click the "Edit" button in the UI.*
|
|
||||||
|
|
||||||
Application logs can be found at:
|
> [!NOTE]
|
||||||
```
|
> The download folder can be changed anytime via the "Edit" button in the UI, on both platforms.
|
||||||
C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> On Linux, you can start Freedom Loader using `freedom-loader` command
|
||||||
|
|
||||||
### Basic Download
|
### Basic Download
|
||||||
|
|
||||||
1. Launch Freedom Loader
|
1. Launch Freedom Loader
|
||||||
@@ -125,7 +185,7 @@ The application supports various configuration options:
|
|||||||
If you encounter issues:
|
If you encounter issues:
|
||||||
|
|
||||||
1. Restart the application
|
1. Restart the application
|
||||||
2. Check the logs in `AppData\Local\FreedomLoader\logs\`
|
2. Check the logs in `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs`
|
||||||
3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools"
|
3. Open the developer console: `CTRL + SHIFT + I` → "Toggle Developer Tools"
|
||||||
4. Create an issue on GitHub with logs and reproduction steps
|
4. Create an issue on GitHub with logs and reproduction steps
|
||||||
|
|
||||||
@@ -151,8 +211,15 @@ If you encounter issues:
|
|||||||
Freedom Loader can be configured either through the settings panel in the UI or by directly editing the `config.json` file located in the application data directory.
|
Freedom Loader can be configured either through the settings panel in the UI or by directly editing the `config.json` file located in the application data directory.
|
||||||
|
|
||||||
### Configuration File Location
|
### Configuration File Location
|
||||||
|
|
||||||
|
#### Windows
|
||||||
```
|
```
|
||||||
C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
%APPDATA%\Freedom Loader\config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux
|
||||||
|
```
|
||||||
|
~/.config/Freedom\ Loader/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Options
|
### Available Options
|
||||||
@@ -166,18 +233,22 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
|||||||
"addMetadata": true,
|
"addMetadata": true,
|
||||||
"verboseLogs": false,
|
"verboseLogs": false,
|
||||||
"autoDownloadPlaylist": true,
|
"autoDownloadPlaylist": true,
|
||||||
|
"keepPlaylistOrder": false,
|
||||||
|
"createPlaylistFolders": true,
|
||||||
"customCodec": "h264",
|
"customCodec": "h264",
|
||||||
"logSystem": true,
|
"logSystem": true,
|
||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
"downloadSystem": true,
|
"downloadSystem": true,
|
||||||
"notifySystem": true
|
"notifySystem": true,
|
||||||
|
"enableHardwareAcceleration": true,
|
||||||
|
"theme": "dark"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration Options
|
### Configuration Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
|--------|------|---------|-------------|
|
|------------------------------|---------|----------|------------------------------------------------------------|
|
||||||
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
||||||
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
||||||
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
||||||
@@ -186,11 +257,15 @@ 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 |
|
||||||
|
| `keepPlaylistOrder` | boolean | `false` | Add index before title, usefull for album |
|
||||||
|
| `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 |
|
||||||
|
| `enableHardwareAcceleration` | boolean | `true` | Enable Hardware Acceleration |
|
||||||
|
| `theme` | string | `dark` | Current Theme Name |
|
||||||
|
|
||||||
### Supported Codecs
|
### Supported Codecs
|
||||||
|
|
||||||
@@ -203,25 +278,94 @@ C:\Users\[USERNAME]\AppData\Roaming\FreedomLoader\config.json
|
|||||||
- `theora` - Theora (legacy open codec)
|
- `theora` - Theora (legacy open codec)
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Configuration changes may require an application restart to take effect.
|
> Some configuration changes may require an application restart to take effect.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
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
|
||||||
|
├── resources/ # 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 +374,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
|
||||||
@@ -283,7 +442,7 @@ npm run build
|
|||||||
This project relies on several external binaries.
|
This project relies on several external binaries.
|
||||||
Some of them are **not included** in the public repository and must be added manually.
|
Some of them are **not included** in the public repository and must be added manually.
|
||||||
|
|
||||||
You must download the required binaries and place them in the `ressources` folder, using the exact filenames listed below.
|
You must download the required binaries and place them in the `resources` folder, using the exact filenames listed below.
|
||||||
|
|
||||||
#### Required binaries
|
#### Required binaries
|
||||||
|
|
||||||
@@ -304,7 +463,7 @@ You must download the required binaries and place them in the `ressources` folde
|
|||||||
Final folder structure:
|
Final folder structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
ressources/
|
resources/
|
||||||
├── deno.exe
|
├── deno.exe
|
||||||
├── ffmpeg.exe
|
├── ffmpeg.exe
|
||||||
├── ffprobe.exe
|
├── ffprobe.exe
|
||||||
@@ -320,43 +479,21 @@ ressources/
|
|||||||
- Write clear commit messages
|
- Write clear commit messages
|
||||||
- Test thoroughly before submitting changes
|
- Test thoroughly before submitting changes
|
||||||
- Update documentation when adding features
|
- Update documentation when adding features
|
||||||
- Maintain compatibility with Windows 10+
|
- Maintain compatibility with Windows 10+ and Linux
|
||||||
|
|
||||||
## 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:
|
||||||
@@ -364,7 +501,7 @@ Use the GitHub Issues system and include:
|
|||||||
- Clear description of the issue
|
- Clear description of the issue
|
||||||
- Steps to reproduce
|
- Steps to reproduce
|
||||||
- Expected vs actual behavior
|
- Expected vs actual behavior
|
||||||
- Relevant logs from `AppData\Local\FreedomLoader\logs\`
|
- Relevant logs from `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs/`
|
||||||
- Screenshots if applicable
|
- Screenshots if applicable
|
||||||
|
|
||||||
### Feature Requests
|
### Feature Requests
|
||||||
@@ -404,10 +541,11 @@ You are free to use, modify, and redistribute this software under the terms of t
|
|||||||
|
|
||||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
||||||
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
||||||
- [Deno](https://deno.com/) for helping me bypassing challenges
|
- [Deno](https://deno.com/) for helping me to bypass challenges
|
||||||
- [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 +555,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>
|
||||||
|
|||||||
69
SECURITY.md
@@ -2,15 +2,14 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
We actively maintain and provide security updates for the following versions of Freedom Loader:
|
Freedom Loader follows a **latest-only** support model. Security fixes are applied to the current release only — no backports to older versions.
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------------- | ------------------ |
|
||||||
| 1.4.x | :white_check_mark: |
|
| Latest stable | :white_check_mark: |
|
||||||
| 1.3.x | :white_check_mark: |
|
| Older versions | :x: |
|
||||||
| < 1.3.0 | :x: |
|
|
||||||
|
|
||||||
**Note**: We recommend always using the latest version to benefit from the most recent security patches and features.
|
I recommend always running the latest version. The built-in auto-updater will notify you when a new release is available. Older versions will not receive security patches regardless of severity.
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
@@ -32,37 +31,40 @@ Instead, please report security issues privately by:
|
|||||||
|
|
||||||
### What to Expect
|
### What to Expect
|
||||||
|
|
||||||
- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours
|
- **Acknowledgment**: I will acknowledge receipt of your report within 48 hours
|
||||||
- **Assessment**: We will assess the vulnerability and determine its severity
|
- **Assessment**: I will assess the vulnerability and determine its severity
|
||||||
- **Updates**: We will keep you informed of our progress
|
- **Updates**: I will keep you informed of our progress
|
||||||
- **Resolution**: We aim to release a fix within 7-14 days for critical vulnerabilities
|
- **Resolution**: I aim to release a fix within 7-14 days for critical vulnerabilities
|
||||||
- **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous)
|
- **Credit**: I will credit you in the release notes (unless you prefer to remain anonymous)
|
||||||
|
|
||||||
## Security Best Practices for Users
|
## Security Best Practices for Users
|
||||||
|
|
||||||
When using Freedom Loader, we recommend:
|
When using Freedom Loader, I recommend:
|
||||||
|
|
||||||
### General Security
|
### General Security
|
||||||
|
|
||||||
- **Keep Updated**: Always use the latest version of Freedom Loader
|
- **Keep Updated**: Always use the latest version of Freedom Loader — older versions receive no security patches
|
||||||
- **Official Sources**: Download only from official releases on [GitHub](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
- **Official Sources**: Download only from official sources:
|
||||||
- **Verify Downloads**: Check that installers are properly signed (Windows SmartScreen may show warnings for new releases)
|
- [GitHub Releases](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||||
|
- [Fedora COPR](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
|
||||||
|
- [Snap Store](https://snapcraft.io/freedom-loader)
|
||||||
|
- **Verify Downloads**: Check that installers are from one of the above sources
|
||||||
- **Antivirus**: Keep your antivirus software up to date
|
- **Antivirus**: Keep your antivirus software up to date
|
||||||
|
|
||||||
### Configuration Security
|
### Configuration Security
|
||||||
|
|
||||||
- **Download Path**: Only set download paths within your user directory (`C:\Users\[USERNAME]\...`)
|
- **Download Path**: Only set download paths within your user directory
|
||||||
- **Cookies**: Be aware that Firefox cookies are used for authentication — keep Firefox secure
|
- **Cookies**: Be aware that Firefox cookies are used for authentication — keep Firefox secure
|
||||||
- **Logs**: Logs may contain sensitive information—avoid sharing them publicly without review
|
- **Logs**: Logs may contain sensitive information such as URLs or local paths — avoid sharing them publicly without review
|
||||||
|
|
||||||
### Privacy Considerations
|
### Privacy Considerations
|
||||||
|
|
||||||
Freedom Loader respects your privacy:
|
Freedom Loader respects your privacy:
|
||||||
|
|
||||||
- **No Data Collection**: We don't collect, store, or transmit your personal data
|
- **No Data Collection**: I don't collect, store, or transmit your personal data
|
||||||
- **No Telemetry**: No usage tracking or analytics
|
- **No Telemetry**: No usage tracking or analytics
|
||||||
- **Local Operation**: All downloads are processed locally on your machine
|
- **Local Operation**: All downloads are processed locally on your machine
|
||||||
- **Optional Features**: Discord RPC is optional and can be disabled
|
- **Optional Features**: Discord RPC is optional and can be disabled in settings
|
||||||
|
|
||||||
## Known Security Considerations
|
## Known Security Considerations
|
||||||
|
|
||||||
@@ -71,16 +73,16 @@ Freedom Loader respects your privacy:
|
|||||||
Freedom Loader accesses Firefox cookies to download protected content. This is:
|
Freedom Loader accesses Firefox cookies to download protected content. This is:
|
||||||
|
|
||||||
- **By Design**: Required for age-restricted or member-only content
|
- **By Design**: Required for age-restricted or member-only content
|
||||||
- **Local Only**: Cookies are read locally and never transmitted
|
- **Local Only**: Cookies are read locally and never transmitted outside your machine
|
||||||
- **User Controlled**: You can control what content you download
|
- **User Controlled**: You control what content you download
|
||||||
|
|
||||||
### Native Dependencies
|
### Native Dependencies
|
||||||
|
|
||||||
Freedom Loader bundles native binaries:
|
Freedom Loader bundles the following official binaries:
|
||||||
|
|
||||||
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp) — auto-updated on each launch
|
||||||
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
|
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
|
||||||
- **Deno**: Official builds from [Deno.land](https://deno.land/)
|
- **Deno**: Official builds from [Deno.com](https://deno.com/)
|
||||||
|
|
||||||
These dependencies are verified and updated regularly.
|
These dependencies are verified and updated regularly.
|
||||||
|
|
||||||
@@ -91,13 +93,13 @@ New releases may trigger Windows Defender warnings because:
|
|||||||
- The application is not yet widely distributed
|
- The application is not yet widely distributed
|
||||||
- Code signing certificates are expensive for open-source projects
|
- Code signing certificates are expensive for open-source projects
|
||||||
|
|
||||||
This is expected behavior for new releases. The warning will decrease as more users download the software.
|
This is expected behavior for new releases and will diminish as more users download the software.
|
||||||
|
|
||||||
## Vulnerability Disclosure Policy
|
## Vulnerability Disclosure Policy
|
||||||
|
|
||||||
### Our Commitment
|
### Our Commitment
|
||||||
|
|
||||||
We are committed to:
|
I am committed to:
|
||||||
|
|
||||||
- Responding promptly to security reports
|
- Responding promptly to security reports
|
||||||
- Working with security researchers to verify and address issues
|
- Working with security researchers to verify and address issues
|
||||||
@@ -107,9 +109,9 @@ We are committed to:
|
|||||||
### Disclosure Timeline
|
### Disclosure Timeline
|
||||||
|
|
||||||
1. **Day 0**: Vulnerability reported privately
|
1. **Day 0**: Vulnerability reported privately
|
||||||
2. **Day 1-2**: Acknowledgment sent to reporter
|
2. **Day 1–2**: Acknowledgment sent to reporter
|
||||||
3. **Day 3-7**: Vulnerability assessed and fix developed
|
3. **Day 3–7**: Vulnerability assessed and fix developed
|
||||||
4. **Day 7-14**: Fix released (critical vulnerabilities prioritized)
|
4. **Day 7–14**: Fix released (critical vulnerabilities prioritized)
|
||||||
5. **Day 14+**: Public disclosure (coordinated with reporter)
|
5. **Day 14+**: Public disclosure (coordinated with reporter)
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
@@ -129,16 +131,16 @@ The following are generally not considered security vulnerabilities:
|
|||||||
- Issues requiring physical access to the user's machine
|
- Issues requiring physical access to the user's machine
|
||||||
- Social engineering attacks
|
- Social engineering attacks
|
||||||
- Denial of service against third-party services
|
- Denial of service against third-party services
|
||||||
- Issues in third-party dependencies (report to upstream projects)
|
- Issues in third-party dependencies (report to upstream projects directly)
|
||||||
- Missing security headers on local HTTP server (no remote access)
|
- Missing security headers on the local HTTP server (not exposed remotely)
|
||||||
|
|
||||||
## Security Updates
|
## Security Updates
|
||||||
|
|
||||||
Security updates are released as:
|
Security updates are released as:
|
||||||
|
|
||||||
- **Patch Releases**: For critical security fixes (e.g., 1.4.1 → 1.4.2)
|
- **Patch Releases**: For critical security fixes (e.g., 1.6.0 → 1.6.1)
|
||||||
- **Automatic Updates**: Users are notified via the built-in update system
|
- **Automatic Updates**: Users are notified via the built-in update system
|
||||||
- **Release Notes**: Security fixes are clearly marked in changelog
|
- **Release Notes**: Security fixes are clearly marked in the changelog
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
@@ -150,6 +152,7 @@ Security updates are released as:
|
|||||||
|
|
||||||
For security-related questions or concerns:
|
For security-related questions or concerns:
|
||||||
|
|
||||||
|
- **Email**: masteracnolo25@gmail.com
|
||||||
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
|
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
157
app/autoUpdater.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Electron auto-update system using electron-updater.
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - update detection
|
||||||
|
* - user confirmation dialogs
|
||||||
|
* - download progress streaming to renderer
|
||||||
|
* - application restart and installation
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { autoUpdater } = require("electron-updater");
|
||||||
|
const { dialog } = require("electron");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures autoUpdater behavior:
|
||||||
|
* - disables automatic download
|
||||||
|
* - disables automatic installation on quit
|
||||||
|
* (manual user-controlled update flow)
|
||||||
|
*/
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes application auto-update lifecycle.
|
||||||
|
*
|
||||||
|
* Registers update event listeners and binds them to:
|
||||||
|
* - UI dialogs (Electron dialog)
|
||||||
|
* - renderer communication (webContents)
|
||||||
|
* - download/install flow control
|
||||||
|
*
|
||||||
|
* @param {BrowserWindow} mainWindow - main Electron window instance
|
||||||
|
*/
|
||||||
|
function initAutoUpdater(mainWindow) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when a new version is detected.
|
||||||
|
* Prompts user to install or defer update.
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
await autoUpdater.downloadUpdate();
|
||||||
|
} else {
|
||||||
|
mainWindow?.webContents.executeJavaScript(
|
||||||
|
`window.showUpdateBadge && window.showUpdateBadge("${info.version}")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams update download progress to renderer process.
|
||||||
|
* Used to update UI progress bar and status indicators.
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when update has been fully downloaded.
|
||||||
|
* Prompts user to restart application and install update.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles update system errors.
|
||||||
|
* Logs failure and displays an error dialog to user.
|
||||||
|
*/
|
||||||
|
autoUpdater.on("error", (err) => {
|
||||||
|
const msg = err?.message || "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If no update is available, I put this because there is no Linux version before 1.6.0
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
const isNoUpdateAvailable = /404/.test(msg) || /Cannot find latest.*\.yml/i.test(msg);
|
||||||
|
|
||||||
|
if (isNoUpdateAvailable) {
|
||||||
|
logger.warn("Auto update: no update metadata found (probably no previous release), ignoring", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Auto update error:", msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkForUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually triggers update check on startup.
|
||||||
|
* Separated from init for reusability and testability.
|
||||||
|
*/
|
||||||
|
async function checkForUpdates() {
|
||||||
|
|
||||||
|
if (process.env.SNAP) {
|
||||||
|
logger.info("Running as Snap, update managed by snapd, skipping electron-updater");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!require("electron").app.isPackaged) return ;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await autoUpdater.checkForUpdates();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Update check failed:", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually triggers update download.
|
||||||
|
* Used when user accepts update prompt.
|
||||||
|
*/
|
||||||
|
async function downloadUpdate() {
|
||||||
|
try {
|
||||||
|
await autoUpdater.downloadUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Download failed:", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately quits application and installs downloaded update.
|
||||||
|
*/
|
||||||
|
function installUpdate() {
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initAutoUpdater, downloadUpdate, installUpdate };
|
||||||
67
app/dependencyCheck.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { app, dialog } = require("electron");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that all required native binaries exist before the app starts.
|
||||||
|
*
|
||||||
|
* This includes:
|
||||||
|
* - yt-dlp (download engine)
|
||||||
|
* - ffmpeg (media processing)
|
||||||
|
* - ffprobe (media inspection)
|
||||||
|
* - deno (JS runtime used by yt-dlp hooks)
|
||||||
|
*
|
||||||
|
* If any dependency is missing:
|
||||||
|
* - logs the error
|
||||||
|
* - shows a blocking Electron error dialog
|
||||||
|
* - exits the application
|
||||||
|
*
|
||||||
|
* @returns {boolean} true if all dependencies are present, false otherwise
|
||||||
|
*/
|
||||||
|
function checkNativeDependencies() {
|
||||||
|
/**
|
||||||
|
* Native dependency paths are resolved at runtime from the server helpers.
|
||||||
|
* This ensures correct resolution in both dev and packaged builds.
|
||||||
|
*/
|
||||||
|
const { binaryPaths } = require("../server/helpers/path.helpers");
|
||||||
|
|
||||||
|
const isWindows = process.platform === "win32";
|
||||||
|
|
||||||
|
const addExe = isWindows ? ".exe" : "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of required external executables used by the application runtime.
|
||||||
|
* Each entry defines a binary name and its resolved absolute path.
|
||||||
|
*/
|
||||||
|
const deps = [
|
||||||
|
{ name: `yt-dlp${addExe}`, path: binaryPaths.ytDlp },
|
||||||
|
{ name: `ffmpeg${addExe}`, path: binaryPaths.ffmpeg },
|
||||||
|
{ name: `ffprobe${addExe}`, path: binaryPaths.ffprobe },
|
||||||
|
{ name: `deno${addExe}`, path: binaryPaths.deno },
|
||||||
|
];
|
||||||
|
|
||||||
|
const missing = deps.filter(d => !fs.existsSync(d.path));
|
||||||
|
if (missing.length === 0) return true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-readable list of missing binaries for logging and user dialog display.
|
||||||
|
*/
|
||||||
|
const list = missing.map(d => d.name).join(", ");
|
||||||
|
logger.error(`Missing dependencies: ${list}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defers error dialog until Electron app is fully initialized.
|
||||||
|
* Required to safely display modal dialogs before quit.
|
||||||
|
*/
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
dialog.showErrorBox(
|
||||||
|
"Missing dependencies",
|
||||||
|
`The following files are missing in the 'resources' folder:\n${list}\n\nThe application will now exit. Try to reinstall.`
|
||||||
|
);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { checkNativeDependencies };
|
||||||
87
app/discordRPC.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const config = require('../config');
|
||||||
|
const RPC = require("discord-rpc");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord Application Client ID used to authenticate the RPC connection.
|
||||||
|
* Comes from the application configuration.
|
||||||
|
*/
|
||||||
|
const clientId = `${config.DiscordRPCID}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord RPC client instance using IPC transport.
|
||||||
|
* Maintains a persistent connection with Discord desktop client.
|
||||||
|
*/
|
||||||
|
const rpc = new RPC.Client({ transport: "ipc" });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional interval reference used for future RPC refresh logic.
|
||||||
|
* Currently reserved for potential periodic activity updates.
|
||||||
|
*/
|
||||||
|
let intervalId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes Discord Rich Presence (RPC) connection.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Connect to Discord via IPC transport
|
||||||
|
* - Set application presence (title, state, timestamps, assets)
|
||||||
|
* - Handle connection errors gracefully via logger
|
||||||
|
*
|
||||||
|
* Triggered once during application startup.
|
||||||
|
*/
|
||||||
|
function startRPC() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rich Presence payload describing current application state
|
||||||
|
* shown in Discord user profile.
|
||||||
|
*/
|
||||||
|
rpc.on("ready", () => {
|
||||||
|
const presence = {
|
||||||
|
largeImageKey: "icon",
|
||||||
|
smallImageKey: "acnolo_pfp",
|
||||||
|
smallImageText: "By MasterAcnolo",
|
||||||
|
startTimestamp: new Date(),
|
||||||
|
details: `Open Source Download Tools - ${config.version}`,
|
||||||
|
state: "masteracnolo.github.io/FreedomLoader",
|
||||||
|
};
|
||||||
|
rpc.clearActivity()
|
||||||
|
rpc.setActivity(presence);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.login({ clientId }).catch(err => {
|
||||||
|
logger.error("Unable to connect to the RPC:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully stops Discord Rich Presence connection.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Clear any running intervals
|
||||||
|
* - Remove current activity from Discord
|
||||||
|
* - Destroy RPC transport connection
|
||||||
|
* - Handle cleanup errors safely
|
||||||
|
*/
|
||||||
|
async function stopRPC(){
|
||||||
|
try {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures RPC connection exists before attempting cleanup.
|
||||||
|
*/
|
||||||
|
if (rpc && rpc.transport) {
|
||||||
|
await rpc.clearActivity()
|
||||||
|
await rpc.destroy();
|
||||||
|
} else{
|
||||||
|
logger.error("Not Able to close RPC")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Error while closing the RPC:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { startRPC, stopRPC};
|
||||||
234
app/ipcHandlers.js
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* Electron IPC main process registry.
|
||||||
|
*
|
||||||
|
* Centralizes all IPC channels exposed to renderer process:
|
||||||
|
* - Application info (version, features)
|
||||||
|
* - Window controls (topbar actions)
|
||||||
|
* - File system interactions (theme, config, download paths)
|
||||||
|
* - UI state synchronization (progress bar)
|
||||||
|
*
|
||||||
|
* Acts as the bridge between renderer and privileged Node/Electron APIs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { ipcMain, dialog, shell } = require("electron");
|
||||||
|
const fs = require("fs");
|
||||||
|
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");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security whitelist for feature flags that can be modified at runtime.
|
||||||
|
*
|
||||||
|
* Prevents unauthorized or unexpected configuration keys from being persisted.
|
||||||
|
* Acts as a basic validation layer for IPC "set-feature".
|
||||||
|
*/
|
||||||
|
const FEATURE_WHITELIST = new Set([
|
||||||
|
"autoUpdate",
|
||||||
|
"discordRPC",
|
||||||
|
"customTopBar",
|
||||||
|
"autoCheckInfo",
|
||||||
|
"addThumbnail",
|
||||||
|
"addMetadata",
|
||||||
|
"verboseLogs",
|
||||||
|
"autoDownloadPlaylist",
|
||||||
|
"keepPlaylistOrder",
|
||||||
|
"customCodec",
|
||||||
|
"theme",
|
||||||
|
"createPlaylistFolders",
|
||||||
|
"notifySystem",
|
||||||
|
"enableHardwareAcceleration"
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolute path to the configuration file storing feature flags.
|
||||||
|
*/
|
||||||
|
const configFolderPath = featuresPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory containing user-installed themes.
|
||||||
|
*/
|
||||||
|
const themeFolderPath = userThemesPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers all IPC handlers and event listeners for Electron main process.
|
||||||
|
*
|
||||||
|
* This function wires renderer → main communication channels:
|
||||||
|
* - synchronous queries (handle)
|
||||||
|
* - fire-and-forget events (on)
|
||||||
|
*
|
||||||
|
* @param {Function} getMainWindow - Function returning current BrowserWindow instance
|
||||||
|
*/
|
||||||
|
function registerIpcHandlers(getMainWindow) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns application version from config.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("version", () => config.version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns runtime feature configuration object.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("features", () => configFeatures);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens native folder selection dialog and validates selected path.
|
||||||
|
*
|
||||||
|
* Ensures:
|
||||||
|
* - user cancellation is handled safely
|
||||||
|
* - selected path is validated against security rules
|
||||||
|
* - unsafe directories are rejected
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates Windows/macOS taskbar progress indicator.
|
||||||
|
*
|
||||||
|
* @param {number} percent - Progress value (0–100)
|
||||||
|
*/
|
||||||
|
ipcMain.on("set-progress", (_, percent) => {
|
||||||
|
getMainWindow()?.setProgressBar(percent / 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window minimize request from renderer.
|
||||||
|
*/
|
||||||
|
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles maximize/unmaximize state of main window.
|
||||||
|
*/
|
||||||
|
ipcMain.on("window-maximize", () => {
|
||||||
|
const win = getMainWindow();
|
||||||
|
if (!win) return;
|
||||||
|
win.isMaximized() ? win.unmaximize() : win.maximize();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the main application window.
|
||||||
|
*/
|
||||||
|
ipcMain.on("window-close", () => getMainWindow()?.close());
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Chromium DevTools in detached mode.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-devtools", () =>
|
||||||
|
getMainWindow()?.webContents.openDevTools({ mode: "detach" })
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens application logs directory in system file explorer.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens external website in default browser.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-website", () =>
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/")
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens official wiki page in external browser.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-wiki", () =>
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki")
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens workshop page in external browser.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-workshop", () =>
|
||||||
|
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Workshop")
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens configuration folder in system file explorer.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves available themes from filesystem.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("get-themes", () => getThemes());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens theme directory in file explorer.
|
||||||
|
*/
|
||||||
|
ipcMain.on("open-theme", () => shell.openPath(themeFolderPath));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads themes from disk dynamically.
|
||||||
|
*/
|
||||||
|
ipcMain.handle("reload-themes", async () => {
|
||||||
|
return await reloadThemes();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a runtime feature flag and persists it to disk.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* - Validates key against whitelist
|
||||||
|
* - Prevents unnecessary writes if value is unchanged
|
||||||
|
* - Updates in-memory config object
|
||||||
|
* - Persists to configuration file
|
||||||
|
*
|
||||||
|
* Security note:
|
||||||
|
* Only whitelisted keys can be modified via IPC to prevent config injection.
|
||||||
|
*
|
||||||
|
* @param {Electron.IpcMainEvent} event
|
||||||
|
* @param {{key: string, value: any}} payload
|
||||||
|
* @returns {boolean} success state
|
||||||
|
*/
|
||||||
|
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 };
|
||||||
90
app/pathValidator.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached resolved default download path.
|
||||||
|
* Initialized lazily on first access to avoid unnecessary imports.
|
||||||
|
*/
|
||||||
|
let _defaultPath = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the application's default download directory.
|
||||||
|
*
|
||||||
|
* This value is lazily loaded and cached after first call.
|
||||||
|
* The path originates from server configuration helpers.
|
||||||
|
*
|
||||||
|
* @returns {string} Absolute default download path
|
||||||
|
*/
|
||||||
|
function getDefaultDownloadPath() {
|
||||||
|
|
||||||
|
if (!_defaultPath) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-loaded dependency providing the configured default download folder.
|
||||||
|
* Required only when function is executed to reduce startup cost.
|
||||||
|
*/
|
||||||
|
const { defaultDownloadFolder } = require("../server/helpers/path.helpers");
|
||||||
|
|
||||||
|
_defaultPath = defaultDownloadFolder;
|
||||||
|
|
||||||
|
}
|
||||||
|
return _defaultPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and sanitizes a user-provided download path.
|
||||||
|
*
|
||||||
|
* Steps performed:
|
||||||
|
* - Resolves absolute real filesystem path
|
||||||
|
* - Normalizes symbolic links via realpathSync
|
||||||
|
* - Checks path safety rules (blocks system-critical directories)
|
||||||
|
* - Falls back to default path if input is empty
|
||||||
|
*
|
||||||
|
* @param {string} userPath - User-provided directory path
|
||||||
|
* @returns {string} Sanitized absolute path
|
||||||
|
* @throws Error If path is invalid, unsafe, or inaccessible
|
||||||
|
*/
|
||||||
|
function validateDownloadPath(userPath) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-loaded helper used to enforce safe directory constraints.
|
||||||
|
* Prevents writes to protected system folders.
|
||||||
|
*/
|
||||||
|
const { isSafePath } = require("../server/helpers/validation.helpers");
|
||||||
|
|
||||||
|
if (!userPath) return getDefaultDownloadPath();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves filesystem symlinks to ensure canonical absolute path.
|
||||||
|
* Required to prevent path traversal or alias bypass.
|
||||||
|
*/
|
||||||
|
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||||
|
|
||||||
|
if (!fs.existsSync(resolved)) {
|
||||||
|
fs.mkdirSync(resolved, { recursive: true });
|
||||||
|
logger.info(`Download folder created: ${resolved}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const real = fs.realpathSync(resolved);
|
||||||
|
|
||||||
|
if (!isSafePath(resolved)) {
|
||||||
|
throw new Error("Path not allowed: system folders are blocked!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return real;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles invalid filesystem paths, permissions issues, or resolution failures.
|
||||||
|
* Logs diagnostic information before propagating a sanitized error.
|
||||||
|
*/
|
||||||
|
logger.error(`Invalid download path: ${userPath} — ${err.message}`);
|
||||||
|
throw new Error(`Invalid or inaccessible path: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { validateDownloadPath, getDefaultDownloadPath };
|
||||||
107
app/splashManager.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const { BrowserWindow, app } = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the splash screen BrowserWindow instance.
|
||||||
|
* Used during application startup before main window is ready.
|
||||||
|
*/
|
||||||
|
let splashWindow = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and displays the splash screen window.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Initializes a frameless, always-on-top splash window
|
||||||
|
* - Loads splash HTML UI
|
||||||
|
* - Dynamically injects banner image depending on dev/production mode
|
||||||
|
* - Acts as startup visual feedback before main window loads
|
||||||
|
*/
|
||||||
|
function createSplashWindow() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for splash screen BrowserWindow.
|
||||||
|
* Optimized for minimal UI and fast startup display.
|
||||||
|
*/
|
||||||
|
const splashOptions = {
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
frame: false,
|
||||||
|
transparent: true,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
resizable: false,
|
||||||
|
skipTaskbar: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the splash screen window.
|
||||||
|
*/
|
||||||
|
splashWindow = new BrowserWindow(splashOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolute path to splash HTML file used as initial UI.
|
||||||
|
* Loaded locally from application bundle or project folder.
|
||||||
|
*/
|
||||||
|
const splashPath = path.join(__dirname, "../public/splash.html");
|
||||||
|
splashWindow.loadFile(splashPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes once splash HTML is fully loaded.
|
||||||
|
* Used here to inject dynamic assets (banner image path).
|
||||||
|
*/
|
||||||
|
splashWindow.webContents.on('did-finish-load', () => {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves splash banner image path depending on runtime mode:
|
||||||
|
* - Production: bundled resources directory
|
||||||
|
* - Development: local build directory
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects runtime image source into splash DOM.
|
||||||
|
* Uses file:// protocol to load local image safely in Electron context.
|
||||||
|
*/
|
||||||
|
splashWindow.webContents.executeJavaScript(`
|
||||||
|
document.querySelector('img[alt="Freedom Loader"]').src = 'file:///${bannerPath.replace(/\\/g, '/')}';
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes and cleans up the splash screen window.
|
||||||
|
*
|
||||||
|
* Should be called once main window is ready to display.
|
||||||
|
*/
|
||||||
|
function closeSplashWindow() {
|
||||||
|
if (splashWindow) {
|
||||||
|
splashWindow.close();
|
||||||
|
splashWindow = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates splash screen progress indicator from main process.
|
||||||
|
*
|
||||||
|
* Sends a step value to renderer via injected JS function.
|
||||||
|
*
|
||||||
|
* @param {number} step - Current initialization step or progress value
|
||||||
|
*/
|
||||||
|
function setSplashProgress(step) {
|
||||||
|
if (splashWindow) {
|
||||||
|
splashWindow.webContents.executeJavaScript(`window.setProgress(${step})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createSplashWindow, closeSplashWindow, setSplashProgress };
|
||||||
337
app/themeManager.js
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
/**
|
||||||
|
* Theme loading and parsing system.
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - Folder-based themes
|
||||||
|
* - ZIP-based themes
|
||||||
|
* - Theme validation against required schema
|
||||||
|
* - Image embedding as base64 data URIs
|
||||||
|
* - Cached theme registry with sorted priority order
|
||||||
|
*
|
||||||
|
* Acts as the core theme ingestion pipeline for the application UI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
const JSZip = require("jszip");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum allowed size (in bytes) for theme images.
|
||||||
|
* Prevents memory abuse from large embedded assets.
|
||||||
|
*/
|
||||||
|
const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of supported image file extensions for theme assets.
|
||||||
|
*/
|
||||||
|
const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed base filenames for theme background/cover images.
|
||||||
|
* Restricts theme asset resolution to known conventions.
|
||||||
|
*/
|
||||||
|
const ALLOWED_IMAGE_NAMES = ["cover", "background"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priority order used to sort themes in UI.
|
||||||
|
* Themes not listed are placed at the end.
|
||||||
|
*/
|
||||||
|
const THEME_ORDER = ["dark", "light"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required JSON schema paths for a valid theme definition.
|
||||||
|
* Each entry represents a nested property path that must exist.
|
||||||
|
*/
|
||||||
|
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"],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely retrieves a deeply nested value from an object using a key path array.
|
||||||
|
*
|
||||||
|
* @param {object} obj - Target object
|
||||||
|
* @param {string[]} keys - Property path segments
|
||||||
|
* @returns {*} value or undefined if path is invalid
|
||||||
|
*/
|
||||||
|
function getNestedValue(obj, keys) {
|
||||||
|
return keys.reduce((acc, key) => acc?.[key], obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates theme JSON structure against required schema.
|
||||||
|
*
|
||||||
|
* Ensures all mandatory configuration keys exist before theme is accepted.
|
||||||
|
*
|
||||||
|
* @param {object} json - Parsed theme JSON
|
||||||
|
* @returns {{valid: boolean, reason?: string}}
|
||||||
|
*/
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes raw theme JSON into application-ready theme object.
|
||||||
|
*
|
||||||
|
* @param {string} themeId
|
||||||
|
* @param {object} themeJson
|
||||||
|
* @param {string|null} imageData - Base64 encoded theme image
|
||||||
|
* @returns {object} normalized theme object
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and encodes an image from theme archive into base64 data URI.
|
||||||
|
*
|
||||||
|
* Applies safety checks:
|
||||||
|
* - Rejects images exceeding MAX_IMAGE_SIZE
|
||||||
|
* - Converts file extension into valid MIME type
|
||||||
|
*
|
||||||
|
* @param {Buffer} buffer
|
||||||
|
* @param {string} filename
|
||||||
|
* @returns {string|null} data URI or null if invalid
|
||||||
|
*/
|
||||||
|
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")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a theme from a ZIP archive.
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* - Extract ZIP content
|
||||||
|
* - Locate .theme.json file
|
||||||
|
* - Validate JSON structure
|
||||||
|
* - Extract optional image asset
|
||||||
|
* - Build normalized theme object
|
||||||
|
*
|
||||||
|
* @param {string} zipPath
|
||||||
|
* @param {string} themeId
|
||||||
|
* @returns {Promise<object|null>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a theme from a directory structure.
|
||||||
|
*
|
||||||
|
* Equivalent to ZIP loader but works on filesystem directly.
|
||||||
|
*
|
||||||
|
* @param {string} folderPath
|
||||||
|
* @param {string} themeId
|
||||||
|
* @returns {Promise<object|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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory cache of loaded themes.
|
||||||
|
* Avoids re-parsing filesystem on every request.
|
||||||
|
*/
|
||||||
|
let cachedThemes = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active theme directory path used as source of truth.
|
||||||
|
*/
|
||||||
|
let themeFolderPath = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active theme directory path.
|
||||||
|
*
|
||||||
|
* @param {string} folderPath
|
||||||
|
*/
|
||||||
|
function setThemeFolderPath(folderPath) {
|
||||||
|
themeFolderPath = folderPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all themes from the configured theme directory.
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - ZIP themes (.zip)
|
||||||
|
* - Folder themes (directory-based)
|
||||||
|
*
|
||||||
|
* Iterates through filesystem entries and delegates parsing
|
||||||
|
* to the appropriate loader (ZIP or folder).
|
||||||
|
*
|
||||||
|
* @returns {Promise<object[]>} List of valid theme objects
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return themes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns sorting priority for a theme ID.
|
||||||
|
*
|
||||||
|
* Lower index = higher priority.
|
||||||
|
* Themes not in THEME_ORDER are placed at the end.
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function getThemeOrder(id) {
|
||||||
|
const index = THEME_ORDER.indexOf(id);
|
||||||
|
return index === -1 ? Infinity : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the theme system.
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* - Sets theme folder path
|
||||||
|
* - Loads all themes from disk
|
||||||
|
* - Sorts themes by priority order
|
||||||
|
* - Caches result in memory
|
||||||
|
*
|
||||||
|
* @param {string} folderPath
|
||||||
|
*/
|
||||||
|
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(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns cached themes.
|
||||||
|
*
|
||||||
|
* If themes are not loaded yet, returns empty array.
|
||||||
|
*
|
||||||
|
* @returns {object[]}
|
||||||
|
*/
|
||||||
|
function getThemes() {
|
||||||
|
return cachedThemes ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads themes from disk and refreshes cache.
|
||||||
|
*
|
||||||
|
* Used when themes are modified at runtime without restart.
|
||||||
|
*
|
||||||
|
* @returns {Promise<object[]>}
|
||||||
|
*/
|
||||||
|
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 };
|
||||||
108
app/windowManager.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Electron main window module dependencies.
|
||||||
|
* Handles BrowserWindow lifecycle and application UI shell.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { BrowserWindow, app } = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
const { configFeatures } = require("../config");
|
||||||
|
const config = require("../config");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton reference to the main Electron BrowserWindow instance.
|
||||||
|
* Ensures only one application window exists at runtime.
|
||||||
|
*/
|
||||||
|
let mainWindow = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and initializes the main Electron application window.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Ensures singleton window instance (prevents duplicates)
|
||||||
|
* - Configures window options (size, icon, preload, security settings)
|
||||||
|
* - Loads frontend application from local dev server
|
||||||
|
* - Handles lifecycle events (close, errors)
|
||||||
|
*
|
||||||
|
* @returns {Promise<BrowserWindow>} The created or existing window instance
|
||||||
|
*/
|
||||||
|
async function createMainWindow() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents multiple instances of the main window.
|
||||||
|
* Returns existing instance if already created.
|
||||||
|
*/
|
||||||
|
if (mainWindow) {
|
||||||
|
logger.warn("Window already exists, no new creation!");
|
||||||
|
return mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves application icon path depending on runtime mode:
|
||||||
|
* - Development: local build folder
|
||||||
|
* - Production: packaged Electron resources
|
||||||
|
*/
|
||||||
|
const iconPath = config.devMode
|
||||||
|
? path.join(__dirname, "../build/app-icon.ico")
|
||||||
|
: path.join(process.resourcesPath, "build/app-icon.ico");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron BrowserWindow configuration object.
|
||||||
|
* Defines UI behavior, security settings, and preload bridge.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the frontend application served by the local dev server.
|
||||||
|
* In production, this could be replaced with a file:// build.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
await mainWindow.loadURL(`http://localhost:${config.applicationPort}`);
|
||||||
|
logger.info("Window loaded");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Window loading error:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up window reference when the main window is closed.
|
||||||
|
* Prevents memory leaks and allows recreation if needed.
|
||||||
|
*/
|
||||||
|
mainWindow.on("closed", () => {
|
||||||
|
logger.info("Main window closed");
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current main window instance.
|
||||||
|
*
|
||||||
|
* Useful for IPC handlers and background services
|
||||||
|
* needing access to the renderer process.
|
||||||
|
*
|
||||||
|
* @returns {BrowserWindow | null}
|
||||||
|
*/
|
||||||
|
function getMainWindow() {
|
||||||
|
return mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createMainWindow, getMainWindow };
|
||||||
35
app/ytDlpUpdater.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const { execFile } = require("child_process");
|
||||||
|
const { logger } = require("../server/logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to update the local yt-dlp binary using its built-in update command.
|
||||||
|
*
|
||||||
|
* This function:
|
||||||
|
* - Executes yt-dlp with the `-U` flag
|
||||||
|
* - Logs update output or warnings if update fails
|
||||||
|
* - Never throws: update failure is non-blocking by design
|
||||||
|
*
|
||||||
|
* @param {string} ytDlpPath - Absolute path to the yt-dlp executable
|
||||||
|
*/
|
||||||
|
function updateYtDlp(ytDlpPath) {
|
||||||
|
|
||||||
|
logger.info("yt-dlp update check starting...");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes yt-dlp CLI process in a non-interactive mode.
|
||||||
|
* Used here specifically for self-update via `-U` flag.
|
||||||
|
*/
|
||||||
|
execFile(ytDlpPath, ["-U"], (err, stdout) => {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles yt-dlp update result:
|
||||||
|
* - Logs warning if update fails (network, binary issues, permissions)
|
||||||
|
* - Logs stdout output when update succeeds
|
||||||
|
*/
|
||||||
|
if (err) logger.warn("yt-dlp update failed (continuing):", err.message);
|
||||||
|
|
||||||
|
else logger.info(`yt-dlp update: ${stdout.trim()}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { updateYtDlp };
|
||||||
|
Before Width: | Height: | Size: 134 KiB |
BIN
build/app-icon.png
Executable file
|
After Width: | Height: | Size: 107 KiB |
125
config.js
@@ -3,24 +3,129 @@ const { app } = require("electron");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const localMode = !app.isPackaged;
|
/**
|
||||||
|
* Indicates whether the application is running in development mode.
|
||||||
|
* Determined by Electron's packaging state.
|
||||||
|
*/
|
||||||
|
const devMode = process.env.NODE_ENV === "test" || !app?.isPackaged;
|
||||||
|
|
||||||
const featuresPath = localMode ? path.join(__dirname, "./config/config.json") : path.join(path.join(process.resourcesPath,"config/" ,"config.json"));
|
/**
|
||||||
|
* Resolves the configuration file path depending on runtime environment.
|
||||||
|
*
|
||||||
|
* In development:
|
||||||
|
* - Uses local config/config.dev.json
|
||||||
|
* - Creates it from default if missing
|
||||||
|
*
|
||||||
|
* In production:
|
||||||
|
* - Uses app userData/config.json
|
||||||
|
* - Creates it from bundled default config if missing
|
||||||
|
*
|
||||||
|
* @returns {string} Absolute path to the active configuration file
|
||||||
|
*/
|
||||||
|
function resolveConfigPath() {
|
||||||
|
if (devMode) {
|
||||||
|
const devConfigPath = path.join(__dirname, "config", "config.dev.json");
|
||||||
|
|
||||||
let features = {};
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the active configuration file used at runtime.
|
||||||
|
*/
|
||||||
|
const featuresPath = resolveConfigPath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and parses the feature configuration file.
|
||||||
|
*
|
||||||
|
* @returns {Object} Parsed feature flags configuration
|
||||||
|
* @throws {Error} If the configuration file is missing or invalid
|
||||||
|
*/
|
||||||
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();
|
/**
|
||||||
|
* Reloads feature flags from disk.
|
||||||
|
* Use this instead of the cached configFeatures snapshot
|
||||||
|
* when you need the latest user settings at call time.
|
||||||
|
*
|
||||||
|
* @returns {Object} Fresh feature flags from config file
|
||||||
|
*/
|
||||||
|
function reloadFeatures() {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback to cached snapshot if file read fails
|
||||||
|
return configFeatures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory snapshot of application feature flags.
|
||||||
|
*
|
||||||
|
* Note: Changes to the config file are not automatically reflected.
|
||||||
|
* A restart or reload is required.
|
||||||
|
*/
|
||||||
|
let configFeatures = loadFeatures();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
version: packageJson.version,
|
/**
|
||||||
|
* Application version from package.json
|
||||||
|
*/
|
||||||
|
version: devMode ? `dev-${packageJson.version}` : `v${packageJson.version}` ,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend Express / internal server port
|
||||||
|
*/
|
||||||
applicationPort: "8787",
|
applicationPort: "8787",
|
||||||
localMode,
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the app is running in development mode
|
||||||
|
*/
|
||||||
|
devMode,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord RPC application identifier
|
||||||
|
*/
|
||||||
DiscordRPCID: "1410934537051181146",
|
DiscordRPCID: "1410934537051181146",
|
||||||
configFeatures
|
|
||||||
}
|
/**
|
||||||
|
* Runtime feature flags loaded from configuration file
|
||||||
|
*/
|
||||||
|
configFeatures,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to reload the config
|
||||||
|
*/
|
||||||
|
reloadFeatures,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the active configuration file
|
||||||
|
*/
|
||||||
|
featuresPath,
|
||||||
|
};
|
||||||
@@ -7,9 +7,13 @@
|
|||||||
"addMetadata": true,
|
"addMetadata": true,
|
||||||
"verboseLogs": false,
|
"verboseLogs": false,
|
||||||
"autoDownloadPlaylist": true,
|
"autoDownloadPlaylist": true,
|
||||||
|
"keepPlaylistOrder": false,
|
||||||
|
"createPlaylistFolders": true,
|
||||||
"customCodec": "h264",
|
"customCodec": "h264",
|
||||||
"logSystem": true,
|
"logSystem": true,
|
||||||
"outputTitleCheck": true,
|
"outputTitleCheck": true,
|
||||||
"downloadSystem": true,
|
"downloadSystem": true,
|
||||||
"notifySystem": true
|
"notifySystem": true,
|
||||||
|
"enableHardwareAcceleration": true,
|
||||||
|
"theme": "dark"
|
||||||
}
|
}
|
||||||
21
jest.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
testEnvironment: "node",
|
||||||
|
testMatch: ["**/test/unit/**/*.test.js"],
|
||||||
|
collectCoverageFrom: [
|
||||||
|
"server/**/*.js",
|
||||||
|
"app/**/*.js",
|
||||||
|
"!app/autoUpdater.js",
|
||||||
|
],
|
||||||
|
|
||||||
|
modulePathIgnorePatterns: [
|
||||||
|
"<rootDir>/dist",
|
||||||
|
"<rootDir>/release",
|
||||||
|
"<rootDir>/build",
|
||||||
|
],
|
||||||
|
|
||||||
|
watchPathIgnorePatterns: [
|
||||||
|
"<rootDir>/dist",
|
||||||
|
"<rootDir>/release",
|
||||||
|
"<rootDir>/build",
|
||||||
|
],
|
||||||
|
};
|
||||||
357
main.js
@@ -1,270 +1,149 @@
|
|||||||
const config = require("./config.js");
|
/**
|
||||||
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require("electron");
|
* Handle crash application and avoid ghost instance
|
||||||
const path = require("path");
|
*/
|
||||||
const os = require("os");
|
const { app } = require("electron");
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
process.on("uncaughtException", (err) => {
|
||||||
const { AutoUpdater } = require("./server/update.js");
|
console.error("Uncaught exception:", err);
|
||||||
const { configFeatures } = require("./config.js");
|
|
||||||
const { startRPC, stopRPC} = require("./server/discordRPC");
|
|
||||||
|
|
||||||
let mainWindow;
|
|
||||||
const logsFolderPath = logDir;
|
|
||||||
|
|
||||||
const basePath = config.localMode
|
|
||||||
? path.join(__dirname )
|
|
||||||
: path.join(path.dirname(process.execPath), "resources");
|
|
||||||
|
|
||||||
const configFolderPath = path.join(basePath, "config" ,"config.json");
|
|
||||||
|
|
||||||
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader");
|
|
||||||
|
|
||||||
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
|
|
||||||
app.disableHardwareAcceleration();
|
|
||||||
|
|
||||||
ipcMain.handle("version", () => config.version);
|
|
||||||
|
|
||||||
// Gestion single instance
|
|
||||||
const gotLock = app.requestSingleInstanceLock();
|
|
||||||
|
|
||||||
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
|
|
||||||
function checkNativeDependencies() {
|
|
||||||
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();
|
app.quit();
|
||||||
});
|
});
|
||||||
return false;
|
|
||||||
}
|
process.on("unhandledRejection", (reason) => {
|
||||||
return true;
|
console.error("Unhandled rejection:", reason);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if this is the primary instance (lock acquired successfully)
|
||||||
|
*/
|
||||||
|
const isPrimaryInstance = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we are on a second instance
|
||||||
|
*/
|
||||||
|
if (!isPrimaryInstance) {
|
||||||
|
app.quit();
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!config.localMode){
|
/**
|
||||||
if (!gotLock) {
|
* Set App Name and AppID
|
||||||
// Une instance existe déjà -> fermer l'ancienne et continuer la nouvelle
|
*/
|
||||||
// Ici la nouvelle instance continue normalement
|
app.setName("Freedom Loader");
|
||||||
} else {
|
app.setAppUserModelId("com.masteracnolo.freedomloader");
|
||||||
if (!checkNativeDependencies()) {
|
|
||||||
// Arrêt déjà géré dans la fonction
|
|
||||||
|
/**
|
||||||
|
* Load the app dependencies for hardware choice
|
||||||
|
*/
|
||||||
|
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||||
|
const { configFeatures } = require("./config");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On start, enable or disable hardWareAcceleration
|
||||||
|
*/
|
||||||
|
if (!configFeatures.enableHardwareAcceleration){
|
||||||
|
app.disableHardwareAcceleration();
|
||||||
|
logger.info("Disabled Hardware Acceleration")
|
||||||
} else {
|
} else {
|
||||||
|
logger.info("Enable Hardware Acceleration")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory snapshot of application feature flags.
|
||||||
|
*
|
||||||
|
* Note: Changes to the config file are not automatically reflected.
|
||||||
|
* A restart could be required.
|
||||||
|
*/
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
|
if(config.devMode){
|
||||||
|
/**
|
||||||
|
* Start devTron extensions - @see https://github.com/electron/devtron
|
||||||
|
*/
|
||||||
|
const { devtron } = require('@electron/devtron');
|
||||||
|
devtron.install();
|
||||||
|
logger.info("Loaded DevTron Extension")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the app dependencies
|
||||||
|
*/
|
||||||
|
const { initThemes } = require("./app/themeManager");
|
||||||
|
|
||||||
|
const { initAutoUpdater } = require("./app/autoUpdater");
|
||||||
|
const { startRPC, stopRPC } = require("./app/discordRPC");
|
||||||
|
const { checkNativeDependencies } = require("./app/dependencyCheck");
|
||||||
|
const { updateYtDlp } = require("./app/ytDlpUpdater");
|
||||||
|
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
||||||
|
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
||||||
|
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
|
||||||
|
const { userThemesPath, initUserThemes, isWindows, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If another instance want to run
|
||||||
|
*/
|
||||||
app.on("second-instance", () => {
|
app.on("second-instance", () => {
|
||||||
// La vieille instance se ferme
|
logger.info("Second instance detected");
|
||||||
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({
|
const mainWindow = require("./app/windowManager").getMainWindow();
|
||||||
title: `Freedom Loader ${config.version}`,
|
|
||||||
width: 750,
|
if (!mainWindow) return;
|
||||||
height: 800,
|
|
||||||
minWidth: 750,
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
minHeight: 800,
|
|
||||||
frame: !configFeatures.customTopBar,
|
mainWindow.show();
|
||||||
webPreferences: {
|
mainWindow.focus();
|
||||||
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(logDir, defaultDownloadFolder);
|
||||||
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",
|
|
||||||
"discordRPC",
|
|
||||||
"customTopBar",
|
|
||||||
"autoCheckInfo",
|
|
||||||
"addThumbnail",
|
|
||||||
"addMetadata",
|
|
||||||
"verboseLogs",
|
|
||||||
"autoDownloadPlaylist",
|
|
||||||
"customCodec"
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
validateBinaries();
|
||||||
|
|
||||||
ipcMain.handle("set-feature", (event, { key, value }) => {
|
initUserThemes();
|
||||||
try {
|
await initThemes(userThemesPath);
|
||||||
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
|
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
6341
package-lock.json
generated
141
package.json
@@ -1,13 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.4.2",
|
"version": "1.6.1",
|
||||||
"author": "MasterAcnolo",
|
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
|
||||||
"description": "Freedom Loader",
|
"description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms",
|
||||||
|
"homepage": "https://masteracnolo.github.io/Freedom-Loader-Site/",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MasterAcnolo/Freedom-Loader.git"
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/MasterAcnolo/Freedom-Loader/issues"
|
||||||
|
},
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron --trace-warnings .",
|
"start": "electron --trace-warnings .",
|
||||||
|
|
||||||
"build": "electron-builder",
|
"build": "electron-builder",
|
||||||
|
"build:win": "electron-builder --win",
|
||||||
|
"build:linux": "electron-builder --linux",
|
||||||
|
"build:rpm": "bash scripts/create-copr-package.sh",
|
||||||
|
"build:all": "electron-builder -wl",
|
||||||
|
"release": "bash scripts/release.sh",
|
||||||
|
"release:publish": "bash scripts/release.sh --publish",
|
||||||
|
"release:dry-run": "bash scripts/release.sh --dry-run",
|
||||||
|
|
||||||
|
"test": "jest",
|
||||||
|
"test:unit": "jest test/unit",
|
||||||
|
|
||||||
"update": "npm update"
|
"update": "npm update"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -17,13 +38,19 @@
|
|||||||
"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/devtron": "^2.1.1",
|
||||||
"electron-builder": "^26.4.0"
|
"@jest/globals": "^30.4.1",
|
||||||
|
"@playwright/test": "^1.62.1",
|
||||||
|
"electron": "^43.3.0",
|
||||||
|
"electron-builder": "^26.15.3",
|
||||||
|
"jest": "^30.4.2",
|
||||||
|
"playwright": "^1.62.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.masteracnolo.freedomloader",
|
"appId": "com.masteracnolo.freedomloader",
|
||||||
@@ -32,13 +59,15 @@
|
|||||||
{
|
{
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
"owner": "MasterAcnolo",
|
"owner": "MasterAcnolo",
|
||||||
"repo": "Freedom-Loader"
|
"repo": "Freedom-Loader",
|
||||||
|
"releaseType": "draft"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"**/*",
|
"**/*",
|
||||||
"node_modules/**/*",
|
"node_modules/**/*",
|
||||||
"!logs/*.json"
|
"!config/config.dev.json",
|
||||||
|
"!theme/**/*"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "build"
|
"buildResources": "build"
|
||||||
@@ -46,45 +75,52 @@
|
|||||||
"asar": true,
|
"asar": true,
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "build/confirm-icon.png",
|
"from": "theme",
|
||||||
"to": "confirm-icon.png"
|
"to": "theme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "build/app-icon.ico",
|
"from": "build/confirm-icon.png",
|
||||||
"to": "app-icon.ico"
|
"to": "confirm-icon.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "ressources/yt-dlp.exe",
|
"from": "config/config.default.json",
|
||||||
"to": "binaries/yt-dlp.exe"
|
"to": "config/config.default.json"
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "ressources/ffmpeg.exe",
|
|
||||||
"to": "binaries/ffmpeg.exe"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "ressources/ffprobe.exe",
|
|
||||||
"to": "binaries/ffprobe.exe"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "ressources/deno.exe",
|
|
||||||
"to": "binaries/deno.exe"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "config/config.json",
|
|
||||||
"to": "config/config.json"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"icon": "build/app-icon.ico"
|
"icon": "build/app-icon.ico",
|
||||||
|
"artifactName": "Freedom-Loader-Setup-${version}.${ext}",
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/win-32/yt-dlp.exe",
|
||||||
|
"to": "binaries/yt-dlp.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/win-32/ffmpeg.exe",
|
||||||
|
"to": "binaries/ffmpeg.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/win-32/ffprobe.exe",
|
||||||
|
"to": "binaries/ffprobe.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/win-32/deno.exe",
|
||||||
|
"to": "binaries/deno.exe"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
@@ -96,6 +132,51 @@
|
|||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": true,
|
"createStartMenuShortcut": true,
|
||||||
"shortcutName": "Freedom Loader"
|
"shortcutName": "Freedom Loader"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage",
|
||||||
|
"deb",
|
||||||
|
"snap"
|
||||||
|
],
|
||||||
|
"category": "Utility",
|
||||||
|
"icon": "build/app-icon.png",
|
||||||
|
"executableName": "freedom-loader",
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/linux/yt-dlp",
|
||||||
|
"to": "binaries/yt-dlp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/linux/ffmpeg",
|
||||||
|
"to": "binaries/ffmpeg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/linux/ffprobe",
|
||||||
|
"to": "binaries/ffprobe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources/binaries/linux/deno",
|
||||||
|
"to": "binaries/deno"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deb": {
|
||||||
|
"fpm": [
|
||||||
|
"./package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rpm": {
|
||||||
|
"fpm": [
|
||||||
|
"./package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"snap": {
|
||||||
|
"confinement": "strict",
|
||||||
|
"plugs": [
|
||||||
|
"default",
|
||||||
|
"removable-media"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
package/com.masteracnolo.freedomloader.metainfo.xml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>com.masteracnolo.freedomloader</id>
|
||||||
|
|
||||||
|
<name>Freedom Loader</name>
|
||||||
|
|
||||||
|
<summary>GUI for yt-dlp to download video and audio with metadata</summary>
|
||||||
|
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>GPL-3.0-only</project_license>
|
||||||
|
|
||||||
|
<developer id="com.masteracnolo">
|
||||||
|
<name>MasterAcnolo</name>
|
||||||
|
</developer>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
Freedom Loader is a free and open-source graphical interface built
|
||||||
|
around yt-dlp, the powerful command-line media downloader. It lets
|
||||||
|
you download video and audio from hundreds of supported websites
|
||||||
|
without ever touching a terminal, with no ads, no tracking, and
|
||||||
|
no questionable third-party services.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Under the hood, Freedom Loader bundles yt-dlp and ffmpeg directly,
|
||||||
|
so there is nothing extra to install: paste a link, choose a
|
||||||
|
format and quality, and the app handles fetching, converting
|
||||||
|
and tagging the file for you.
|
||||||
|
</p>
|
||||||
|
<p>Features include:</p>
|
||||||
|
<ul>
|
||||||
|
<li>MP4 video or MP3 audio downloads with embedded metadata and thumbnails</li>
|
||||||
|
<li>Quality selection from best to specific resolutions (1080p, 720p, 480p)</li>
|
||||||
|
<li>Codec choice: H.264, H.265, AV1, VP9 and Theora</li>
|
||||||
|
<li>Playlist downloads with automatic per-playlist folders</li>
|
||||||
|
<li>Cookie-based access to age-restricted and member-only content</li>
|
||||||
|
<li>Custom theme system with a web-based theme creator</li>
|
||||||
|
<li>Real-time download progress with network speed indicator</li>
|
||||||
|
<li>Optional Discord Rich Presence integration</li>
|
||||||
|
<li>Automatic application and download-engine updates</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
DRM-protected content is not supported and cannot be downloaded.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<launchable type="desktop-id">freedom-loader.desktop</launchable>
|
||||||
|
|
||||||
|
<url type="homepage">https://github.com/MasterAcnolo/Freedom-Loader</url>
|
||||||
|
<url type="bugtracker">https://github.com/MasterAcnolo/Freedom-Loader/issues</url>
|
||||||
|
<url type="vcs-browser">https://github.com/MasterAcnolo/Freedom-Loader</url>
|
||||||
|
|
||||||
|
<categories>
|
||||||
|
<category>AudioVideo</category>
|
||||||
|
<category>Utility</category>
|
||||||
|
<category>Network</category>
|
||||||
|
</categories>
|
||||||
|
|
||||||
|
<releases>
|
||||||
|
<release version="1.6.0" date="2026-06-23">
|
||||||
|
<description>
|
||||||
|
<p>Latest stable release.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
|
||||||
|
<content_rating type="oars-1.1"/>
|
||||||
|
|
||||||
|
<provides>
|
||||||
|
<binary>freedom-loader</binary>
|
||||||
|
</provides>
|
||||||
|
</component>
|
||||||
9
package/copr-build.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh -xe
|
||||||
|
git clone --depth 1 https://github.com/MasterAcnolo/Freedom-Loader.git src
|
||||||
|
cd src
|
||||||
|
npm ci
|
||||||
|
npx electron-builder --linux dir
|
||||||
|
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
tar czf "$resultdir/freedom-loader-$VERSION.tar.gz" -C dist linux-unpacked
|
||||||
|
sed "s/__VERSION__/$VERSION/" package/freedom-loader.spec > "$resultdir/freedom-loader.spec"
|
||||||
25
package/freedom-loader.spec
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
Name: freedom-loader
|
||||||
|
Version: __VERSION__
|
||||||
|
Release: 1%{?dist}
|
||||||
|
Summary: GUI for yt-dlp media downloads
|
||||||
|
License: GPL-3.0-only
|
||||||
|
Source0: freedom-loader-__VERSION__.x86_64.rpm
|
||||||
|
BuildArch: x86_64
|
||||||
|
Requires: glibc, gtk3, nss, libXScrnSaver
|
||||||
|
|
||||||
|
%description
|
||||||
|
GUI for yt-dlp media downloads.
|
||||||
|
|
||||||
|
%install
|
||||||
|
rpm2cpio %{SOURCE0} | (cd %{buildroot} && cpio -idmv)
|
||||||
|
mkdir -p %{buildroot}/usr/bin
|
||||||
|
ln -sf "/opt/Freedom Loader/freedom-loader" %{buildroot}/usr/bin/freedom-loader
|
||||||
|
|
||||||
|
%files
|
||||||
|
/opt/*
|
||||||
|
/usr/bin/freedom-loader
|
||||||
|
/usr/share/applications/*
|
||||||
|
/usr/share/metainfo/*
|
||||||
|
/usr/share/icons/hicolor
|
||||||
|
|
||||||
|
%changelog
|
||||||
114
preload.js
@@ -1,23 +1,129 @@
|
|||||||
const { contextBridge, ipcRenderer } = require("electron");
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the main application API to the renderer process.
|
||||||
|
*
|
||||||
|
* This API acts as a secure bridge between the renderer and the
|
||||||
|
* Electron main process using IPC (Inter-Process Communication).
|
||||||
|
* All calls are explicitly whitelisted.
|
||||||
|
*/
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return process.platform to renderer
|
||||||
|
*/
|
||||||
|
getProcessPlatform: () => process.platform,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default system download directory.
|
||||||
|
*/
|
||||||
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a native dialog allowing the user to select a download folder.
|
||||||
|
*/
|
||||||
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends download progress value to the main process (used for taskbar / UI sync).
|
||||||
|
*
|
||||||
|
* @param {number} percent - Download progress percentage (0–100)
|
||||||
|
*/
|
||||||
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
|
setProgress: (percent) => ipcRenderer.send("set-progress", percent),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves application feature flags (runtime configuration).
|
||||||
|
*/
|
||||||
getFeatures: () => ipcRenderer.invoke("features"),
|
getFeatures: () => ipcRenderer.invoke("features"),
|
||||||
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
|
|
||||||
|
/**
|
||||||
|
* Updates a feature flag dynamically at runtime.
|
||||||
|
*
|
||||||
|
* @param {string} key - Feature name
|
||||||
|
* @param {any} value - Feature value
|
||||||
|
*/
|
||||||
|
setFeature: (key, value) =>
|
||||||
|
ipcRenderer.invoke("set-feature", { key, value }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current application version.
|
||||||
|
*/
|
||||||
getVersion: () => ipcRenderer.invoke("version"),
|
getVersion: () => ipcRenderer.invoke("version"),
|
||||||
getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path)
|
|
||||||
|
/**
|
||||||
|
* Validates a download path before using it for file operations.
|
||||||
|
*
|
||||||
|
* @param {string} path - Path to validate
|
||||||
|
*/
|
||||||
|
getValidatedDownloadPath: (path) =>
|
||||||
|
ipcRenderer.invoke("validate-download-path", path),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves available themes from the filesystem or config layer.
|
||||||
|
*/
|
||||||
|
getThemes: () => ipcRenderer.invoke("get-themes"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces a reload of theme files (useful after modification/import).
|
||||||
|
*/
|
||||||
|
reloadThemes: () => ipcRenderer.invoke("reload-themes"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Contrôles de fenêtre et outils custom pour la topbar
|
|
||||||
|
/**
|
||||||
|
* Exposes window control and developer utilities for the custom topbar UI.
|
||||||
|
*
|
||||||
|
* These methods forward commands to the Electron main process via IPC.
|
||||||
|
*/
|
||||||
contextBridge.exposeInMainWorld("topbarAPI", {
|
contextBridge.exposeInMainWorld("topbarAPI", {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimizes the application window.
|
||||||
|
*/
|
||||||
minimize: () => ipcRenderer.send("window-minimize"),
|
minimize: () => ipcRenderer.send("window-minimize"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles maximize/restore state of the application window.
|
||||||
|
*/
|
||||||
maximize: () => ipcRenderer.send("window-maximize"),
|
maximize: () => ipcRenderer.send("window-maximize"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the application window.
|
||||||
|
*/
|
||||||
close: () => ipcRenderer.send("window-close"),
|
close: () => ipcRenderer.send("window-close"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Chromium DevTools for debugging.
|
||||||
|
*/
|
||||||
openDevTools: () => ipcRenderer.send("open-devtools"),
|
openDevTools: () => ipcRenderer.send("open-devtools"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens application logs directory or log viewer.
|
||||||
|
*/
|
||||||
openLogs: () => ipcRenderer.send("open-logs"),
|
openLogs: () => ipcRenderer.send("open-logs"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the official website or project homepage.
|
||||||
|
*/
|
||||||
openWebsite: () => ipcRenderer.send("open-website"),
|
openWebsite: () => ipcRenderer.send("open-website"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the theme directory or theme editor interface.
|
||||||
|
*/
|
||||||
|
openTheme: () => ipcRenderer.send("open-theme"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens project wiki/documentation.
|
||||||
|
*/
|
||||||
openWiki: () => ipcRenderer.send("open-wiki"),
|
openWiki: () => ipcRenderer.send("open-wiki"),
|
||||||
openConfig: () => ipcRenderer.send("open-config")
|
|
||||||
|
/**
|
||||||
|
* Opens the workshop or external content library.
|
||||||
|
*/
|
||||||
|
openWorkshop: () => ipcRenderer.send("open-workshop"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens configuration/settings panel.
|
||||||
|
*/
|
||||||
|
openConfig: () => ipcRenderer.send("open-config"),
|
||||||
});
|
});
|
||||||
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 967 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 5.3 MiB |
@@ -7,9 +7,8 @@
|
|||||||
default-src 'self' 'unsafe-inline' data:;
|
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,12 +16,15 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
<div class="topbar" id="topbar">
|
<div class="topbar" id="topbar">
|
||||||
<div class="custom-controls">
|
<div class="custom-controls">
|
||||||
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
<button id="devtools-btn" title="Dev Tools">Tools</button>
|
||||||
<button id="logs-btn" title="Logs">Logs</button>
|
<button id="logs-btn" title="Logs">Logs</button>
|
||||||
<button id="website-btn" title="Website">Website</button>
|
<button id="website-btn" title="Website">Website</button>
|
||||||
<button id="wiki-btn" title="Wiki">Wiki</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>
|
||||||
<div class="window-controls">
|
<div class="window-controls">
|
||||||
<button id="minimize-btn" title="Réduire"><img src="assets/icon/minimize.svg"></button>
|
<button id="minimize-btn" title="Réduire"><img src="assets/icon/minimize.svg"></button>
|
||||||
@@ -30,6 +32,7 @@
|
|||||||
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
|
<button id="close-btn" title="Fermer"><img src="assets/icon/close.svg"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<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,27 +89,27 @@
|
|||||||
</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">
|
||||||
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
|
<input data-key="keepPlaylistOrder" id="keepPlaylistOrder" type="checkbox">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
<span class="setting-title">Auto Fetch Info</span>
|
<span class="setting-title">Keep Playlist Order</span>
|
||||||
<small>Automatically fetch information for new downloads.</small>
|
<small>Add index before output file, to keep the order.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,6 +135,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input data-key="autoCheckInfo" id="autoCheckInfo" type="checkbox">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Auto Fetch Info</span>
|
||||||
|
<small>Automatically fetch information for new downloads.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
@@ -142,6 +159,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>
|
||||||
@@ -158,28 +183,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<input type="checkbox" data-key="enableHardwareAcceleration" id="enableHardwareAcceleration">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="setting-title">Enable Hardware Acceleration</span>
|
||||||
|
<small>Uses GPU acceleration for rendering when available. <br> (Be effective on next launch)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- Developer Settings -->
|
<!-- Developer Settings -->
|
||||||
<details class="settings-section">
|
<details class="settings-section">
|
||||||
<summary>Developer Settings</summary>
|
<summary>Developer Settings</summary>
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
|
||||||
<input type="checkbox" data-key="downloadSystem" id="downloadSystem">
|
|
||||||
<div class="setting-info">
|
|
||||||
<span class="setting-title">Download System</span>
|
|
||||||
<small>Enable/disable the internal download system.</small>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<!-- <div class="setting-item">
|
|
||||||
<input type="checkbox" data-key="logSystem" id="logSystem">
|
|
||||||
<div class="setting-info">
|
|
||||||
<span class="setting-title">Log System</span>
|
|
||||||
<small>Enable logging system.</small>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
<input type="checkbox" data-key="verboseLogs" id="verboseLogs">
|
||||||
<div class="setting-info">
|
<div class="setting-info">
|
||||||
@@ -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,15 +216,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<main>
|
||||||
<div class="container" id="container">
|
<div class="container" id="container">
|
||||||
|
|
||||||
<header>
|
|
||||||
<h1 id="title"> Freedom Loader </h1>
|
<h1 id="title"> Freedom Loader </h1>
|
||||||
<p id="subtitle"> Because why not? </p>
|
<p id="subtitle"> Because why not? </p>
|
||||||
</header>
|
|
||||||
|
|
||||||
<form action="/download" method="POST" id="downloadForm">
|
<form action="/download" method="POST" id="downloadForm">
|
||||||
<input name="url" placeholder="URL" id="UrlInput">
|
<div class="url-input-container">
|
||||||
|
<input type="text" name="url" placeholder="URL" id="UrlInput">
|
||||||
|
<button type="button" id="pasteBtn" class="paste-btn" title="Paste from clipboard">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9 5H7C5.89543 5 5 5.89543 5 7V19C5 20.1046 5.89543 21 7 21H17C18.1046 21 19 20.1046 19 19V7C19 5.89543 18.1046 5 17 5H15M9 5C9 6.10457 9.89543 7 11 7H13C14.1046 7 15 6.10457 15 5M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5M12 12H15M12 16H15M9 12H9.01M9 16H9.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="checkbox-inline">
|
<label class="checkbox-inline">
|
||||||
|
|
||||||
@@ -227,6 +250,7 @@
|
|||||||
|
|
||||||
<button type="submit" onclick="startProgress()">Download</button>
|
<button type="submit" onclick="startProgress()">Download</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="download-path">
|
<div class="download-path">
|
||||||
@@ -234,16 +258,20 @@
|
|||||||
<button id="changePath">Edit</button>
|
<button id="changePath">Edit</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-container">
|
||||||
|
<div id="downloadStage" style="display: none; font-size: 0.85rem; color: var(--default-text-color); margin-top: 8px;"></div>
|
||||||
<div id="downloadProgressWrapper">
|
<div id="downloadProgressWrapper">
|
||||||
<div id="downloadProgress"></div>
|
<div id="downloadProgress"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: flex; gap: 20px; align-items: center;">
|
||||||
<div style="display: flex; gap: 20px;">
|
|
||||||
<div id="downloadProgressText"></div>
|
<div id="downloadProgressText"></div>
|
||||||
<div id="downloadSpeedText"></div>
|
<div id="downloadSpeedText"></div>
|
||||||
|
<div id="playlistInfoText" style="display: none; font-size: 0.9rem;"></div>
|
||||||
|
<button type="button" id="stopBtn" onclick="stopDownload()" class="stop-btn" style="display:none;" title="Stop download">â– </button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="downloadStatus"></div>
|
<div id="downloadStatus" style="display: none;"></div>
|
||||||
|
|
||||||
<div class="infos-container">
|
<div class="infos-container">
|
||||||
|
|
||||||
@@ -259,10 +287,19 @@
|
|||||||
|
|
||||||
<span id="version-badge" class="version-badge"></span>
|
<span id="version-badge" class="version-badge"></span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
<h2 id="reference"> Because Why Not ? </h2>
|
<h2 id="reference"> Because Why Not ? </h2>
|
||||||
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
<script src="script/toast.js"></script>
|
||||||
|
<script src="script/customConfiguration.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,6 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Retrieves the application version from the Electron main process
|
||||||
|
* and displays it in the UI version badge (bottom-right corner).
|
||||||
|
*
|
||||||
|
* This is mainly used for debugging and build identification.
|
||||||
|
*/
|
||||||
async function versionLabel() {
|
async function versionLabel() {
|
||||||
const appVersion = await window.electronAPI.getVersion();
|
const appVersion = await window.electronAPI.getVersion();
|
||||||
document.getElementById("version-badge").textContent = `v${appVersion}`;
|
|
||||||
};
|
/**
|
||||||
|
* UI element displaying the current application version.
|
||||||
|
* Updated at runtime after IPC call resolves.
|
||||||
|
*/
|
||||||
|
const versionBadge = document.getElementById("version-badge");
|
||||||
|
|
||||||
|
if (!versionBadge) return;
|
||||||
|
|
||||||
|
versionBadge.textContent = `${appVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
versionLabel();
|
versionLabel();
|
||||||
40
public/script/clipboardPaste.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Initializes clipboard paste functionality for the URL input field.
|
||||||
|
*
|
||||||
|
* When the user clicks the paste button, the system attempts to read
|
||||||
|
* the clipboard content and inject it into the URL input field.
|
||||||
|
* It also triggers an input event to notify any bound listeners.
|
||||||
|
*/
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const pasteBtn = document.getElementById("pasteBtn");
|
||||||
|
const urlInput = document.getElementById("UrlInput");
|
||||||
|
|
||||||
|
if (!pasteBtn || !urlInput) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles clipboard paste action triggered by the paste button.
|
||||||
|
* Reads clipboard text and inserts it into the URL input field.
|
||||||
|
*/
|
||||||
|
pasteBtn.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
urlInput.value = await navigator.clipboard.readText();
|
||||||
|
|
||||||
|
// Notify frameworks / listeners bound to input changes
|
||||||
|
urlInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
|
||||||
|
urlInput.focus();
|
||||||
|
|
||||||
|
// Temporary UI feedback on paste action
|
||||||
|
pasteBtn.style.backgroundColor = "var(--form-button-bg-hover-color)";
|
||||||
|
setTimeout(() => {
|
||||||
|
pasteBtn.style.backgroundColor = "";
|
||||||
|
}, 200);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Clipboard access failed:", err);
|
||||||
|
|
||||||
|
alert(
|
||||||
|
"Unable to access clipboard. Please paste manually using Ctrl+V."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
27
public/script/customConfiguration.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
async function initPlatformRestrictions() {
|
||||||
|
const platform = await window.electronAPI.getProcessPlatform();
|
||||||
|
|
||||||
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
|
const refreshBtn = document.getElementById("refresh-themes-btn");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the OS windows
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
const isWindows = platform === "win32";
|
||||||
|
|
||||||
|
console.log(isWindows);
|
||||||
|
|
||||||
|
// If it's windows, the button is not disabled.
|
||||||
|
themeSelect.disabled = !isWindows;
|
||||||
|
refreshBtn.disabled = !isWindows;
|
||||||
|
|
||||||
|
themeSelect.classList.toggle("disabled", !isWindows);
|
||||||
|
refreshBtn.classList.toggle("disabled", !isWindows);
|
||||||
|
|
||||||
|
if (!isWindows) {
|
||||||
|
themeSelect.title = "Themes are only available on Windows";
|
||||||
|
refreshBtn.title = "Themes are only available on Windows";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Initializes the download path system once the UI is ready.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Restore last known user-selected download path (localStorage)
|
||||||
|
* - Validate it against backend rules
|
||||||
|
* - Synchronize with backend authoritative path
|
||||||
|
* - Setup folder selection UI interaction
|
||||||
|
*/
|
||||||
window.addEventListener("DOMContentLoaded", async () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
const savePathElem = document.getElementById("savePath");
|
const savePathElem = document.getElementById("savePath");
|
||||||
const form = document.getElementById("downloadForm");
|
const form = document.getElementById("downloadForm");
|
||||||
|
|
||||||
// input caché = DERNIER chemin validé par le back
|
/**
|
||||||
|
* Hidden form field storing the validated download path
|
||||||
|
* used during download submission.
|
||||||
|
*
|
||||||
|
* Created dynamically if not already present in DOM.
|
||||||
|
*/
|
||||||
let hidden = document.getElementById("savePathInput");
|
let hidden = document.getElementById("savePathInput");
|
||||||
|
|
||||||
if (!hidden) {
|
if (!hidden) {
|
||||||
hidden = document.createElement("input");
|
hidden = document.createElement("input");
|
||||||
hidden.type = "hidden";
|
hidden.type = "hidden";
|
||||||
@@ -12,12 +27,32 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
form.appendChild(hidden);
|
form.appendChild(hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a validated download path received from backend.
|
||||||
|
*
|
||||||
|
* Updates:
|
||||||
|
* - UI display (visible path)
|
||||||
|
* - tooltip (full path)
|
||||||
|
* - hidden form input (used for submission)
|
||||||
|
* - localStorage cache (UX persistence only, not authoritative)
|
||||||
|
*
|
||||||
|
* @param {string} path - validated absolute download path
|
||||||
|
*/
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the initial download path on application startup.
|
||||||
|
*
|
||||||
|
* Priority order:
|
||||||
|
* 1. cached localStorage value (UX only, not trusted)
|
||||||
|
* 2. backend validated version of cached path
|
||||||
|
* 3. backend default fallback path if validation fails
|
||||||
|
*/
|
||||||
async function loadInitialPath() {
|
async function loadInitialPath() {
|
||||||
// On affiche ce que le user a vu la dernière fois (UX)
|
// On affiche ce que le user a vu la dernière fois (UX)
|
||||||
const cached = localStorage.getItem("customDownloadPath");
|
const cached = localStorage.getItem("customDownloadPath");
|
||||||
@@ -41,22 +76,22 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
await loadInitialPath();
|
await loadInitialPath();
|
||||||
|
|
||||||
document
|
/**
|
||||||
.getElementById("changePath")
|
* Opens system folder selector and updates download path.
|
||||||
.addEventListener("click", async () => {
|
*
|
||||||
|
* The selected folder is validated by Electron backend
|
||||||
|
* before being applied to the UI and persisted.
|
||||||
|
*/
|
||||||
|
document.getElementById("changePath").addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const selectedPath =
|
// selectDownloadFolder already returns a validated path
|
||||||
await window.electronAPI.selectDownloadFolder();
|
const validatedPath = 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,256 @@
|
|||||||
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");
|
||||||
|
/**
|
||||||
|
* Currently selected themes loaded from backend.
|
||||||
|
* Used as cache for UI theme switching.
|
||||||
|
*/
|
||||||
|
let loadedThemes = [];
|
||||||
|
|
||||||
function populateThemeSelect() {
|
/**
|
||||||
for (const [themeKey, themeInfo] of Object.entries(themes)) {
|
* Applies a theme object to the application UI by mapping
|
||||||
|
* theme configuration values to CSS variables and DOM properties.
|
||||||
|
*
|
||||||
|
* This function mutates:
|
||||||
|
* - document.body (background styling)
|
||||||
|
* - :root CSS variables (design system tokens)
|
||||||
|
* - subtitle DOM element (theme metadata display)
|
||||||
|
*
|
||||||
|
* @param {Object} theme - Theme definition object
|
||||||
|
* @param {Object} theme.style - Style configuration
|
||||||
|
*/
|
||||||
|
function applyTheme(theme) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const style = theme.style;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
if (theme.image) {
|
||||||
|
document.body.style.backgroundImage = `url('${theme.image}')`;
|
||||||
|
document.body.style.backgroundSize = style.background?.size || "cover";
|
||||||
|
document.body.style.backgroundPosition = style.background?.position || "center";
|
||||||
|
document.body.style.backgroundAttachment = style.background?.attachment || "fixed";
|
||||||
|
} else {
|
||||||
|
document.body.style.backgroundImage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS variables (design tokens)
|
||||||
|
root.style.setProperty("--background-color", style.colors?.background || "#121212");
|
||||||
|
root.style.setProperty("--default-text-color", style.colors?.text?.default || "#e0e0e0");
|
||||||
|
root.style.setProperty("--subtitle-color", style.colors?.text?.subtitle || "#2196f3");
|
||||||
|
root.style.setProperty("--audio-only-label-color", style.colors?.text?.audioOnly || "#f5f5f5");
|
||||||
|
|
||||||
|
root.style.setProperty("--form-bg-color", style.form?.background || "#1e1e1e");
|
||||||
|
root.style.setProperty("--form-input-bg-color", style.form?.input?.background || "#2a2a2a");
|
||||||
|
root.style.setProperty("--form-input-border-color", style.form?.input?.border || "#444444");
|
||||||
|
root.style.setProperty("--form-input-border-focus-color", style.form?.input?.borderFocus || "#2196f3");
|
||||||
|
root.style.setProperty("--form-input-text-color", style.form?.input?.text || "#f5f5f5");
|
||||||
|
root.style.setProperty("--form-input-placeholder-color", style.form?.input?.placeholder || "#888888");
|
||||||
|
root.style.setProperty("--form-button-bg-color", style.form?.button?.background || "#2196f3");
|
||||||
|
root.style.setProperty("--form-button-text-color", style.form?.button?.text || "#ffffff");
|
||||||
|
root.style.setProperty("--form-button-bg-hover-color", style.form?.button?.hover || "#1976d2");
|
||||||
|
root.style.setProperty("--paste-button-icon-color", style.form?.pasteButtonIcon || "#ffffff");
|
||||||
|
|
||||||
|
root.style.setProperty("--checkbox-unchecked-bg-color", style.checkbox?.background?.unchecked || "#555555");
|
||||||
|
root.style.setProperty("--checkbox-checked-bg-color", style.checkbox?.background?.checked || "#2196f3");
|
||||||
|
root.style.setProperty("--checkbox-checkmark-border-color", style.checkbox?.checkmarkBorder || "#ffffff");
|
||||||
|
root.style.setProperty("--checkbox-pulse-color", style.checkbox?.pulse || "rgba(33, 150, 243, 0.5)");
|
||||||
|
|
||||||
|
root.style.setProperty("--download-status-color", style.download?.status || "#2196f3");
|
||||||
|
|
||||||
|
root.style.setProperty("--progress-bar-bg-color", style.progressBar?.background || "#444444");
|
||||||
|
root.style.setProperty("--progress-bar-fill-color", style.progressBar?.fill || "#2196f3");
|
||||||
|
|
||||||
|
root.style.setProperty("--video-info-bg-color", style.videoInfo?.background || "#1a1a1a");
|
||||||
|
root.style.setProperty("--video-info-text-color", style.videoInfo?.text || "#e0e0e0");
|
||||||
|
root.style.setProperty("--video-info-heading-color", style.videoInfo?.heading || "#2196f3");
|
||||||
|
root.style.setProperty("--video-info-list-color", style.videoInfo?.list || "#cccccc");
|
||||||
|
root.style.setProperty("--video-info-list-strong-color", style.videoInfo?.strong || "#ffffff");
|
||||||
|
root.style.setProperty("--video-info-link-color", style.videoInfo?.link || "#2196f3");
|
||||||
|
root.style.setProperty("--video-info-link-hover-color", style.videoInfo?.linkHover || "#1976d2");
|
||||||
|
|
||||||
|
root.style.setProperty("--playlist-background-color", style.playlist?.background || "#303030");
|
||||||
|
|
||||||
|
root.style.setProperty("--settings-button-bg-color", style.settings?.button?.background || "#1e1e1e");
|
||||||
|
root.style.setProperty("--settings-button-text-color", style.settings?.button?.text || "#e0e0e0");
|
||||||
|
root.style.setProperty("--open-theme-button-bg-color", style.settings?.openThemeButton?.background || "#2196f3");
|
||||||
|
root.style.setProperty("--open-theme-button-text-color", style.settings?.openThemeButton?.text || "#ffffff");
|
||||||
|
root.style.setProperty("--open-json-button-bg-color", style.settings?.openJsonButton?.background || style.settings?.button?.background || "#1e1e1e");
|
||||||
|
root.style.setProperty("--open-json-button-text-color", style.settings?.openJsonButton?.text || style.settings?.button?.text || "#e0e0e0");
|
||||||
|
root.style.setProperty("--settings-modal-bg-color", style.settings?.background?.modal || "#1e1e1e");
|
||||||
|
root.style.setProperty("--settings-section-bg-color", style.settings?.background?.section || "#2a2a2a");
|
||||||
|
root.style.setProperty("--settings-text-color", style.settings?.text || "#e0e0e0");
|
||||||
|
root.style.setProperty("--settings-subtitle-color", style.settings?.subtitle || "#aaaaaa");
|
||||||
|
|
||||||
|
const subtitleEl = document.getElementById("subtitle");
|
||||||
|
if (subtitleEl) subtitleEl.textContent = theme.subtitle || theme.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists selected theme ID into Electron backend features storage.
|
||||||
|
*
|
||||||
|
* @param {string} themeId
|
||||||
|
*/
|
||||||
|
function saveTheme(themeId) {
|
||||||
|
window.electronAPI.setFeature("theme", themeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates theme dropdown selector with available themes.
|
||||||
|
*
|
||||||
|
* @param {Array} themes
|
||||||
|
*/
|
||||||
|
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) {
|
/**
|
||||||
document.body.classList.remove(...Object.keys(themes));
|
* Initializes theme system:
|
||||||
document.body.classList.add(themeKey);
|
* - loads themes from backend
|
||||||
|
* - restores saved theme
|
||||||
|
* - applies selected theme
|
||||||
|
*/
|
||||||
|
async function initThemes() {
|
||||||
|
loadedThemes = await window.electronAPI.getThemes();
|
||||||
|
|
||||||
const subtitleElement = document.getElementById("subtitle");
|
if (!loadedThemes.length) {
|
||||||
if (subtitleElement && themes[themeKey]) {
|
const defaultTheme = {
|
||||||
subtitleElement.textContent = themes[themeKey].subtitle;
|
id: "dark",
|
||||||
|
name: "Dark",
|
||||||
|
subtitle: "Darkness is my ally",
|
||||||
|
style: {
|
||||||
|
background: {
|
||||||
|
size: "cover",
|
||||||
|
position: "center",
|
||||||
|
attachment: "fixed"
|
||||||
|
},
|
||||||
|
|
||||||
|
colors: {
|
||||||
|
background: "#121212",
|
||||||
|
text: {
|
||||||
|
default: "#e0e0e0",
|
||||||
|
subtitle: "#2196f3",
|
||||||
|
audioOnly: "#f5f5f5"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
}
|
form: {
|
||||||
|
background: "#1e1e1e",
|
||||||
|
input: {
|
||||||
|
background: "#2a2a2a",
|
||||||
|
border: "#444444",
|
||||||
|
borderFocus: "#2196f3",
|
||||||
|
text: "#f5f5f5",
|
||||||
|
placeholder: "#888888"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
background: "#2196f3",
|
||||||
|
text: "#ffffff",
|
||||||
|
hover: "#1976d2"
|
||||||
|
},
|
||||||
|
pasteButtonIcon: "#ffffff"
|
||||||
|
},
|
||||||
|
|
||||||
function saveTheme(themeKey) {
|
checkbox: {
|
||||||
localStorage.setItem("selectedTheme", themeKey);
|
background: {
|
||||||
}
|
unchecked: "#555555",
|
||||||
|
checked: "#2196f3"
|
||||||
|
},
|
||||||
|
checkmarkBorder: "#ffffff",
|
||||||
|
pulse: "rgba(33, 150, 243, 0.5)"
|
||||||
|
},
|
||||||
|
|||||||