29 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
9d09cf1b5d Handle updater download failures for retry UI
Co-authored-by: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
2026-09-27 16:09:55 +00:00
copilot-swe-agent[bot]
c80d65483a Initial plan 2026-09-27 16:07:06 +00:00
MasterAcnolo
d11c0f9839 add: .zed and OS Generated files to .gitignore, and better exclusion of IDE folder 2026-09-27 11:30:57 +02:00
MasterAcnolo
1e0b2affee update: bump app version to 1.6.4 since it's no longer a preview 2026-09-27 11:28:05 +02:00
MasterAcnolo
9975b2b583 refactor(update): add an UI indicator in frontend
disable OS native pop up menu and fix autoupdate crash on rpm and snap

#50
2026-09-25 21:08:18 +02:00
MasterAcnolo
b319a1e445 fix(ipc): send report IPC API 2026-09-16 22:16:59 +02:00
MasterAcnolo
4a64a40226 fix(front): settings menu transition 2026-09-16 20:17:42 +02:00
MasterAcnolo
2c0f5b315b refactor: toast notification style 2026-09-13 15:53:21 +02:00
MasterAcnolo
58b6637eeb feat: implement linter with config file 2026-09-13 12:06:33 +02:00
MasterAcnolo
8af5a4a383 update: bump app version to 1.6.4-preview 2026-09-13 11:56:03 +02:00
Axel Nicolas
23f04bafc3 Merge pull request #56 from MasterAcnolo/feature/send-logs
Feature/send logs
2026-09-13 11:54:43 +02:00
MasterAcnolo
35ffd7077e fix: onClick attribute on Report a Bug
(#53)
2026-09-13 11:44:37 +02:00
MasterAcnolo
408df0e2f6 feat: send logs feature that send logs to a distant service. Full integration in ui
+ expose logger method to frontEnd (#53)
2026-09-13 11:41:09 +02:00
MasterAcnolo
7dee94da6f refactor: extract CHANGELOG into a separated file. Should avoid characters issue 2026-09-11 11:01:12 +02:00
MasterAcnolo
cd43ae10fc fix: bump app version to 1.6.3 since it's no longer a preview 2026-09-10 21:36:33 +02:00
Axel Nicolas
6978277b4b Update codeql.yml 2026-09-10 15:13:02 +02:00
MasterAcnolo
b91c20d450 fix: release.sh release message ending 2026-09-10 15:09:54 +02:00
Axel Nicolas
014b1ad52a Update codeql.yml 2026-09-10 15:09:19 +02:00
Axel Nicolas
ec93d20a39 Create codeql.yml 2026-09-10 15:06:52 +02:00
MasterAcnolo
09c511fcb8 fix: docs in path.helpers 2026-09-10 15:01:50 +02:00
MasterAcnolo
f343fbd148 fix(discord-rpc): remove unusued code 2026-09-10 15:00:00 +02:00
Axel Nicolas
6d99d5f91c Update README.md 2026-09-10 13:55:09 +02:00
MasterAcnolo
0f0bb874bf feat: add logs when minimize and maximize (tray and not) 2026-09-10 12:01:07 +02:00
MasterAcnolo
b9267ec408 fix: enhance package.json with metadata used by electron-builder on Linux 2026-09-10 12:00:20 +02:00
MasterAcnolo
311bff6df5 refactor: systemTray window logic. Window reference is now dynamic and guarded against destroyed states. Minimize can now put window in the systemTray if option is active 2026-09-10 10:27:11 +02:00
MasterAcnolo
e4243a01ae fix: devMode logic 2026-09-10 10:25:03 +02:00
MasterAcnolo
52be9c6b4f update: electron from 43.3.0 to 44.3.0 2026-09-10 10:23:29 +02:00
MasterAcnolo
e541eb442a fix:
- replace unusued throw new Error by logger.error
- remove unusued variables isWindows, err
- remove unusued logFormat
- remove unusued imports logSessionEnd
2026-09-09 09:21:11 +02:00
MasterAcnolo
e2c2dde4a2 fix: README.md 2026-09-09 08:59:55 +02:00
35 changed files with 2064 additions and 232 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
BUG_REPORT_URL=
BUG_REPORT_API_KEY=

46
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: "CodeQL PR"
on:
pull_request:
branches: [ "main" ]
schedule:
- cron: '00 00 * * 1'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'Manual build required'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

13
.gitignore vendored
View File

@@ -6,12 +6,21 @@
/out
/srpm-out
# .env
*.env
# Config JSON File. Create it and fill it if you want to have dev only settings
config/config.dev.json
# IDE
/.idea
/.vscode
.idea/
.vscode/
.zed/
# OS generated files
.DS_Store
.DS_Store?
Thumbs.db
# Linux executable
ffmpeg

View File

@@ -15,8 +15,6 @@
</div>
---
## Overview
Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services.
@@ -252,7 +250,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
| Option | Type | Default | Description |
|------------------------------|---------|----------|------------------------------------------------------------|
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
| `customTopBar` | boolean | `true` | Use custom application top bar |
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
@@ -369,13 +367,29 @@ Head over to the [Freedom Loader Workshop repository](https://github.com/MasterA
### Bug Reports
Use the GitHub Issues system and include:
You can report bugs either using the built-in application system or via the GitHub Issues system.
#### Using the Built-in Report Tool
1. Open Freedom Loader.
2. Trigger the bug report interface.
3. Fill in the title and description of the issue.
4. Choose whether to include today's logs automatically.
5. Submit the report.
> [!NOTE]
> If you need to attach extra context like screenshots or screen recordings, it is recommended to use the GitHub Issues
system instead.
#### Using GitHub Issues
Open a new issue on the GitHub repository and include:
- Clear description of the issue
- Steps to reproduce
- Expected vs actual behavior
- Relevant logs from `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs/`
- Screenshots if applicable
- Screenshots or recordings if applicable
### Feature Requests
@@ -412,7 +426,6 @@ You are free to use, modify, and redistribute this software under the terms of t
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
- [@AureCX](https://github.com/AureCX) for being the Official Windows Tester!
- Zakaria for the website icon
- Jacques Chirac to love Apples
- All users who test, report issues, and help improve the application
---

View File

@@ -9,7 +9,6 @@
*/
const { autoUpdater } = require("electron-updater");
const { dialog } = require("electron");
const { logger } = require("../server/logger");
/**
@@ -21,6 +20,26 @@ const { logger } = require("../server/logger");
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
let updateAvailable = false;
/**
* Determines how the update should be handled based on the platform
* @returns {"auto" | "snap" | "flatpak" | "linux-native"}
*/
function getUpdateEnvironment() {
if (
process.env.APPIMAGE ||
process.platform === "win32" ||
process.platform === "darwin"
) {
return "auto";
}
if (process.env.SNAP) return "snap";
if (process.env.FLATPAK_ID) return "flatpak";
return "linux-native";
}
/**
* Initializes application auto-update lifecycle.
*
@@ -32,7 +51,6 @@ autoUpdater.autoInstallOnAppQuit = false;
* @param {BrowserWindow} mainWindow - main Electron window instance
*/
function initAutoUpdater(mainWindow) {
/**
* Triggered when a new version is detected.
* Prompts user to install or defer update.
@@ -40,23 +58,12 @@ function initAutoUpdater(mainWindow) {
autoUpdater.on("update-available", async (info) => {
logger.info(`Update available: ${info.version}`);
const { response } = await dialog.showMessageBox(mainWindow, {
type: "info",
title: "Update Available",
message: `Version ${info.version} is available.`,
detail: "Would you like to download and install it now?",
buttons: ["Install Update", "Maybe Later"],
defaultId: 0,
cancelId: 1,
mainWindow?.webContents.send("update-available", {
version: info.version,
environment: getUpdateEnvironment(),
});
updateAvailable = true;
if (response === 0) {
await autoUpdater.downloadUpdate();
} else {
mainWindow?.webContents.executeJavaScript(
`window.showUpdateBadge && window.showUpdateBadge("${info.version}")`
);
}
});
/**
@@ -66,8 +73,10 @@ function initAutoUpdater(mainWindow) {
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,
percent: progress.percent,
bytesPerSecond: progress.bytesPerSecond,
transferred: progress.transferred,
total: progress.total,
});
});
@@ -78,17 +87,9 @@ function initAutoUpdater(mainWindow) {
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,
});
mainWindow?.webContents.send("update-downloaded", info);
if (response === 0) autoUpdater.quitAndInstall();
});
/**
@@ -102,14 +103,15 @@ function initAutoUpdater(mainWindow) {
* 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);
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);
logger.error("Auto update error:", err);
});
checkForUpdates();
@@ -120,19 +122,7 @@ function initAutoUpdater(mainWindow) {
* Separated from init for reusability and testability.
*/
async function checkForUpdates() {
/**
* Prevents electron-updater from running inside sandboxed environments.
* Snap and Flatpak lock the file system in read-only mode, causing the updater to crash.
* In these environments, updates are safely handled by snapd or the Flatpak runtime.
* Standalone Linux packages (.deb, .rpm, AppImage) will bypass this and update normally.
*/
if (process.env.SNAP || process.env.FLATPAK_ID) {
logger.info("Running as Snap or Flatpak. Updates are managed by the OS store. Skipping electron-updater.");
return;
}
if (!require("electron").app.isPackaged) return ;
if (!require("electron").app.isPackaged) return;
try {
await autoUpdater.checkForUpdates();
@@ -150,6 +140,7 @@ async function downloadUpdate() {
await autoUpdater.downloadUpdate();
} catch (err) {
logger.error("Download failed:", err.message);
throw err;
}
}
@@ -160,4 +151,13 @@ function installUpdate() {
autoUpdater.quitAndInstall();
}
module.exports = { initAutoUpdater, downloadUpdate, installUpdate };
function isUpdateAvailable() {
return updateAvailable;
}
module.exports = {
initAutoUpdater,
downloadUpdate,
installUpdate,
isUpdateAvailable,
};

View File

@@ -97,12 +97,12 @@ function startRPC() {
details: `Open Source Download Tools - ${config.version}`,
state: "masteracnolo.github.io/FreedomLoader",
};
rpc.clearActivity()
rpc.clearActivity();
rpc.setActivity(presence);
});
rpc.login({ clientId }).catch(err => {
rpc.login({ clientId }).catch(() => {
// Since 1.6.2, it no longer print the error. This is because if discord is not opened, it will send an error.
// logger.error("Unable to connect to the RPC:", err);
logger.info("Unable to connect to the Discord RPC. Discord is maybe not launched");
@@ -123,14 +123,6 @@ async function stopRPC() {
try {
if (intervalId) clearInterval(intervalId);
/**
* Ensures RPC connection AND the underlying socket exist
* before attempting to clear activity to prevent crash.
*/
if (rpc.transport && rpc.transport.socket) {
await rpc.clearActivity();
}
await rpc.destroy();
} catch (err) {

View File

@@ -13,12 +13,14 @@
const { ipcMain, dialog, shell } = require("electron");
const fs = require("fs");
const { logger, logDir } = require("../server/logger");
const { configFeatures, featuresPath } = require("../config");
const { configFeatures, featuresPath, devMode } = require("../config");
const { getThemes, reloadThemes } = require("./themeManager");
const config = require("../config");
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
const { userThemesPath } = require("../server/helpers/path.helpers");
const { createSystemTray, destroyTray } = require("./tray");
const { sendReport } = require("./sendReport");
const { isUpdateAvailable, downloadUpdate, installUpdate } = require("./autoUpdater");
/**
* Security whitelist for feature flags that can be modified at runtime.
@@ -64,7 +66,6 @@ const themeFolderPath = userThemesPath;
* @param {Function} getMainWindow - Function returning current BrowserWindow instance
*/
function registerIpcHandlers(getMainWindow) {
/**
* Returns application version from config.
*/
@@ -114,7 +115,17 @@ function registerIpcHandlers(getMainWindow) {
/**
* Window minimize request from renderer.
*/
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
ipcMain.on("window-minimize", () => {
// Minimize to tray
if (configFeatures.systemTray) {
getMainWindow()?.hide();
logger.info("Window Minimized in SystemTray (Using Minimize Button)");
} else {
// Native minimize
getMainWindow()?.minimize();
logger.info("Window minimized normally");
}
});
/**
* Toggles maximize/unmaximize state of main window.
@@ -130,7 +141,6 @@ function registerIpcHandlers(getMainWindow) {
*/
ipcMain.on("window-close", () => getMainWindow()?.close());
/**
* Opens Chromium DevTools in detached mode.
*/
@@ -141,7 +151,7 @@ function registerIpcHandlers(getMainWindow) {
/**
* Opens application logs directory in system file explorer.
*/
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
/**
* Opens external website in default browser.
@@ -169,7 +179,12 @@ function registerIpcHandlers(getMainWindow) {
*/
ipcMain.on("open-config", () => shell.openPath(configFolderPath));
/**
* Front end logger
*/
ipcMain.on("log-error", (_, message) => logger.error(`[Frontend] ${message}`));
ipcMain.on("log-info", (_, message) => logger.info(`[Frontend] ${message}`));
ipcMain.on("log-warn", (_, message) => logger.warn(`[Frontend] ${message}`));
/**
* Retrieves available themes from filesystem.
@@ -188,6 +203,31 @@ function registerIpcHandlers(getMainWindow) {
return await reloadThemes();
});
/**
* Send bug report
*/
ipcMain.handle("send-report", async (_, params) => {
return await sendReport(params);
});
ipcMain.handle("is-update-available", () => {
return isUpdateAvailable();
});
ipcMain.handle("download-update", async () => {
return await downloadUpdate();
});
ipcMain.handle("install-update", async () => {
return installUpdate();
});
ipcMain.on("open-release-page", () => {
shell.openExternal(
"https://github.com/MasterAcnolo/Freedom-Loader/releases/latest",
);
});
/**
* Updates a runtime feature flag and persists it to disk.
*
@@ -205,47 +245,46 @@ function registerIpcHandlers(getMainWindow) {
* @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;
}
try {
if (!FEATURE_WHITELIST.has(key)) {
logger.warn(`Rejected feature (not whitelisted): ${key}`);
return false;
}
if (configFeatures[key] === value) {
return true;
}
if (configFeatures[key] === value) {
return true;
}
/**
* Dynamically intercepts and applies System Tray state changes at runtime.
* Instantiates or destroys the tray icon immediately to prevent window
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
*/
if (key === "systemTray") {
if (value === true) {
logger.info("System Tray enabled dynamically.");
createSystemTray(getMainWindow(), config.devMode);
} else {
logger.info("System Tray disabled dynamically.");
destroyTray();
}
}
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;
/**
* Dynamically intercepts and applies System Tray state changes at runtime.
* Instantiates or destroys the tray icon immediately to prevent window
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
*/
if (key === "systemTray") {
if (value === true) {
logger.info("System Tray enabled dynamically.");
createSystemTray(devMode);
} else {
logger.info("System Tray disabled dynamically.");
destroyTray();
}
});
}
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 };

View File

@@ -66,7 +66,7 @@ function validateDownloadPath(userPath) {
const real = fs.realpathSync(absolutePath);
if (!isSafePath(real)) {
throw new Error("Path not allowed: system folders are blocked!");
logger.error("Path not allowed: system folders are blocked!");
}
return real;

54
app/sendReport.js Normal file
View File

@@ -0,0 +1,54 @@
const fs = require("fs");
const path = require("path");
const {logger, logDir} = require("../server/logger");
async function sendReport(params) {
try {
const {title, description, includeLogs} = params;
if (!title || !description) {
throw new Error("Title and description are required.");
}
const formData = new FormData();
formData.append("title", title);
formData.append("context", description);
if (includeLogs === "yes" && logDir) {
// YYYY-MM-DD
const today = new Date().toISOString().split("T")[0];
const logFilePath = path.join(logDir, `LOGS-${today}.log`);
if (fs.existsSync(logFilePath)) {
const logFileContent = fs.readFileSync(logFilePath);
const blob = new Blob([logFileContent], {type: "text/plain"});
formData.append("file", blob, `LOGS-${today}.log`);
logger.info("Logs attached to bug report.");
} else {
logger.warn(`Log file not found for today: ${logFilePath}`);
}
}
const response = await fetch(process.env.BUG_REPORT_URL, {
method: "POST",
headers: {
"X-Api-Key": process.env.BUG_REPORT_API_KEY
},
body: formData
});
if (!response.ok) {
logger.error(`Server returned an error: ${response.status}`);
throw new Error(`Server responded with status ${response.status}`);
}
logger.info("Bug report successfully sent.");
return true;
} catch (err) {
logger.error(`Failed to send bug report: ${err.message}`);
throw err;
}
}
module.exports = {sendReport};

View File

@@ -2,6 +2,7 @@ const {app, Menu, Tray, nativeImage} = require("electron");
const path = require("path");
const {logger} = require("../server/logger"); // Ajuste le chemin si besoin
const fs = require("fs");
const {getMainWindow} = require("./windowManager");
/**
* Global reference to the Tray instance to prevent garbage collection.
@@ -17,11 +18,10 @@ let tray = null;
* - Builds the right-click context menu.
* - Handles left-click behavior to toggle main window visibility.
*
* @param {import('electron').BrowserWindow} mainWindow - The main application window.
* @param {boolean} devMode - Indicates whether the application is running in development mode.
* @returns {Tray} The created Tray instance.
*/
function createSystemTray(mainWindow, devMode) {
function createSystemTray(devMode) {
// Prevent creating multiple tray instances
if (tray) return tray;
@@ -58,7 +58,12 @@ function createSystemTray(mainWindow, devMode) {
{
label: "Show Freedom Loader",
click: () => {
mainWindow.show();
const window = getMainWindow();
if (window && !window.isDestroyed()) {
window.show();
window.focus();
logger.info("Window Restored from SystemTray (Using Show Action)");
}
},
},
{ type: "separator" },
@@ -82,11 +87,15 @@ function createSystemTray(mainWindow, devMode) {
* but it remains standard practice for Windows and Linux environments.
*/
tray.on("click", () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
const window = getMainWindow();
if (!window || window.isDestroyed()) return;
if (window.isVisible()) {
window.hide();
} else {
mainWindow.show();
mainWindow.focus();
window.show();
window.focus();
logger.info("Window Restored from SystemTray (Double Click on Tray Icon)");
}
});

View File

@@ -87,9 +87,11 @@ async function createMainWindow() {
* cancels the destruction and hides the window instead.
*/
mainWindow.on('close', (event) => {
if (!app.isQuitting && config.systemTray ) {
if (!app.isQuitting && config.configFeatures.systemTray) {
event.preventDefault();
mainWindow.hide();
logger.info("Window Minimized in SystemTray (Using Close Button)");
return false;
}
});

View File

@@ -7,7 +7,7 @@ const path = require("path");
* 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 devMode = !app.isPackaged || process.env.NODE_ENV === "test";
/**
* Resolves the configuration file path depending on runtime environment.

40
eslint.config.js Normal file
View File

@@ -0,0 +1,40 @@
const js = require("@eslint/js");
const globals = require("globals");
module.exports = [
js.configs.recommended,
{ignores: ["node_modules/**", "srpm-out/**", "dist/**", "test/**"]},
{
languageOptions: {
ecmaVersion: "latest",
sourceType: "commonjs",
},
rules: {
"no-unused-vars": ["error", {argsIgnorePattern: "^_"}],
"no-console": "off",
"semi": ["error", "always"]
},
},
{
files: ["app/**", "server/**", "main.js"],
languageOptions: {
globals: {...globals.node}
}
},
{
files: ["public/**"],
languageOptions: {
globals: {...globals.browser}
}
},
{
files: ["preload.js"],
languageOptions: {
globals: {...globals.node, ...globals.browser}
}
}
];

21
main.js
View File

@@ -38,7 +38,7 @@ app.setAppUserModelId("com.masteracnolo.freedomloader");
* Load the app dependencies for hardware choice
*/
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
const { configFeatures } = require("./config");
const { configFeatures, devMode} = require("./config");
/**
* In-memory snapshot of application feature flags.
@@ -48,18 +48,18 @@ const { configFeatures } = require("./config");
*/
if (!configFeatures.enableHardwareAcceleration){
app.disableHardwareAcceleration();
logger.info("Disabled Hardware Acceleration")
logger.info("Disabled Hardware Acceleration");
} else {
logger.info("Enable Hardware Acceleration")
logger.info("Enable Hardware Acceleration");
}
if(configFeatures.devMode){
if(devMode){
/**
* Start devTron extensions - @see https://github.com/electron/devtron
*/
const { devtron } = require('@electron/devtron');
devtron.install();
logger.info("Loaded DevTron Extension")
logger.info("Loaded DevTron Extension");
}
/**
@@ -74,10 +74,15 @@ 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");
const { userThemesPath, initUserThemes, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
const {createSystemTray, destroyTray} = require("./app/tray");
const { stopServer } = require("./server/server");
/**
* Expose .env in process.env
*/
require("dotenv").config();
/**
* Global flag indicating if the application is intentionally shutting down.
* Used across the app to bypass the "minimize to tray on close" behavior.
@@ -135,7 +140,7 @@ app.whenReady().then(async () => {
createSplashWindow();
if (!configFeatures.devMode && !checkNativeDependencies()) return;
if (!devMode && !checkNativeDependencies()) return;
const { userYtDlp } = require("./server/helpers/path.helpers");
updateYtDlp(userYtDlp);
@@ -161,7 +166,7 @@ app.whenReady().then(async () => {
getMainWindow().show();
if (configFeatures.systemTray) {
createSystemTray(getMainWindow(), configFeatures.devMode);
createSystemTray(devMode);
}
if (configFeatures.discordRPC) startRPC();

1086
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
{
"name": "freedom-loader",
"desktopName": "com.masteracnolo.freedomloader",
"productName": "Freedom Loader",
"version": "1.6.3-preview",
"version": "1.6.4",
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
"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/",
@@ -21,7 +22,6 @@
"dev": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --no-warnings .\"",
"dev:warn": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --trace-warnings .\"",
"dev:debug": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .\"",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:linux": "electron-builder --linux",
@@ -30,16 +30,17 @@
"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",
"lint": "eslint . --format pretty",
"lint:fix": "eslint . --fix --format pretty",
"update": "npm update"
},
"dependencies": {
"chalk": "^4.1.2",
"debug": "^4.4.1",
"discord-rpc": "^4.0.1",
"dotenv": "^17.4.2",
"electron-updater": "^6.6.2",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
@@ -50,10 +51,14 @@
},
"devDependencies": {
"@electron/devtron": "^2.1.1",
"@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1",
"@playwright/test": "^1.62.1",
"electron": "^43.3.0",
"electron": "^44.3.0",
"electron-builder": "^26.15.3",
"eslint": "^10.10.0",
"eslint-formatter-pretty": "^7.1.0",
"globals": "^17.12.0",
"jest": "^30.4.2",
"nodemon": "^3.1.14",
"playwright": "^1.62.1"
@@ -136,6 +141,8 @@
"snap"
],
"category": "Utility",
"synopsis": "GUI for yt-dlp to download video and audio with metadata",
"maintainer": "MasterAcnolo",
"icon": "build/app-icon.png",
"executableName": "freedom-loader",
"extraResources": [

View File

@@ -8,6 +8,26 @@ const { contextBridge, ipcRenderer } = require("electron");
* All calls are explicitly whitelisted.
*/
contextBridge.exposeInMainWorld("electronAPI", {
/**
* Sends error log message.
*
* @param {...any} args - Values to log
*/
logError: (...args) => ipcRenderer.send("log-error", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
/**
* Sends info log message.
*
* @param {...any} args - Values to log
*/
logInfo: (...args) => ipcRenderer.send("log-info", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
/**
* Sends warning log message.
*
* @param {...any} args - Values to log
*/
logWarn: (...args) => ipcRenderer.send("log-warn", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
/**
* Return process.platform to renderer
@@ -42,12 +62,11 @@ contextBridge.exposeInMainWorld("electronAPI", {
* @param {string} key - Feature name
* @param {any} value - Feature value
*/
setFeature: (key, value) =>
ipcRenderer.invoke("set-feature", { key, value }),
setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
/**
* Returns the current application version.
*/
*/
getVersion: () => ipcRenderer.invoke("version"),
/**
@@ -67,8 +86,61 @@ contextBridge.exposeInMainWorld("electronAPI", {
* Forces a reload of theme files (useful after modification/import).
*/
reloadThemes: () => ipcRenderer.invoke("reload-themes"),
});
/*
* ==========================================
* Auto Updater IPC
* ==========================================
*/
/**
* Checks if an update has already been detected by the main process.
*
* @returns {Promise<boolean>} Resolves to true if an update is available.
*/
isUpdateAvailable: () => ipcRenderer.invoke("is-update-available"),
/**
* Subscribes to the update available event pushed by the main process.
*
* @param {Function} callback - Function executed with update info when an update is found.
*/
onUpdateAvailable: (callback) =>
ipcRenderer.on("update-available", (_, info) => callback(info)),
/**
* Instructs the main process to start downloading the available update.
*
* @returns {Promise<void>}
*/
downloadUpdate: () => ipcRenderer.invoke("download-update"),
/**
* Instructs the main process to quit the application and install the downloaded update.
*
* @returns {Promise<void>}
*/
installUpdate: () => ipcRenderer.invoke("install-update"),
/**
* Subscribes to the download progress event.
*
* @param {Function} callback - Function executed with progress metrics (percent, bytesPerSecond, transferred, total).
*/
onDownloadProgress: (callback) =>
ipcRenderer.on("update-progress", (_, progress) => callback(progress)),
/**
* Subscribes to the update downloaded event.
* Triggered when the update file is fully downloaded and ready to install.
*
* @param {Function} callback - Function executed with update info.
*/
onUpdateDownloaded: (callback) =>
ipcRenderer.on("update-downloaded", (_, info) => callback(info)),
openReleasePage: () => ipcRenderer.send("open-release-page"),
});
/**
* Exposes window control and developer utilities for the custom topbar UI.
@@ -76,7 +148,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
* These methods forward commands to the Electron main process via IPC.
*/
contextBridge.exposeInMainWorld("topbarAPI", {
/**
* Minimizes the application window.
*/
@@ -126,4 +197,12 @@ contextBridge.exposeInMainWorld("topbarAPI", {
* Opens configuration/settings panel.
*/
openConfig: () => ipcRenderer.send("open-config"),
/**
* Send Bug Report
*
* @param params
* @returns {Promise<any>}
*/
sendReport: (params) => ipcRenderer.invoke("send-report", params),
});

View File

@@ -13,6 +13,8 @@
<link rel="stylesheet" href="styles/styles.css">
<link rel="stylesheet" href="styles/layout/topbar.css">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
</head>
<body>
@@ -21,6 +23,7 @@
<div class="custom-controls">
<button id="devtools-btn" title="Dev Tools">Tools</button>
<button id="logs-btn" title="Logs">Logs</button>
<button id="report-bug-btn" title="Report a bug">Report a Bug</button> <!-- TODO: Find a better place -->
<button id="website-btn" title="Website">Website</button>
<button id="wiki-btn" title="Wiki">Wiki</button>
<button id="workshop-btn" title="Workshop">Theme Workshop</button>
@@ -295,7 +298,6 @@
<span id="version-badge" class="version-badge"></span>
</div>
</main>
<!-- Toast Container -->
@@ -303,6 +305,31 @@
<h2 id="reference"> Because Why Not ? </h2>
<div id="updateAvailable" style="display:none">
<div class="update-banner">
<div class="banner-row">
<div class="banner-left">
<div class="icon-wrap">
<span class="material-symbols-outlined" id="update-icon">download</span>
</div>
<div class="text-block">
<span class="update-title" id="update-title">Update available</span>
<span class="update-sub" id="update-sub">A new version is ready</span>
</div>
</div>
<button class="btn-update" id="btn-update">Download</button>
</div>
<div class="progress-wrap" id="progress-wrap" style="display:none">
<div class="progress-track">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-meta">
<span id="progress-pct">0%</span>
<span id="progress-eta"></span>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="script/toast.js"></script>
@@ -314,6 +341,8 @@
<script src="script/fetchinfo.js"></script>
<script src="script/progressBar.js"></script>
<script src="script/customthemes.js"></script>
<script src="script/reportBug.js"></script>
<script src="script/autoUpdate.js"></script>
<script type="module" src="script/topbar.js"></script>
</body>

View File

@@ -0,0 +1,99 @@
async function initUpdateUI() {
const container = document.getElementById("updateAvailable");
const hasUpdate = await window.electronAPI.isUpdateAvailable();
if (hasUpdate) {
setupAndShowUpdateUI();
}
window.electronAPI.onUpdateAvailable((info) => {
setupAndShowUpdateUI(info);
});
function setupAndShowUpdateUI(info) {
container.style.display = "block";
const btn = document.getElementById("btn-update");
const icon = document.getElementById("update-icon");
const title = document.getElementById("update-title");
const sub = document.getElementById("update-sub");
const progressWrap = document.getElementById("progress-wrap");
const progressFill = document.getElementById("progress-fill");
const progressPct = document.getElementById("progress-pct");
const progressEta = document.getElementById("progress-eta");
if (info && info.version) {
sub.textContent = `A new version (${info.version}) is ready`;
}
if (info && info.environment && info.environment !== "auto") {
icon.textContent = "open_in_new";
if (info.environment === "snap") {
sub.textContent = `Version ${info.version} is out. Use the Snap Store or run "snap refresh".`;
btn.textContent = "View Release";
} else {
sub.textContent = `Version ${info.version} is out. Update via your package manager or download it manually.`;
btn.textContent = "Download manually";
}
btn.onclick = () => {
window.electronAPI.openReleasePage();
container.style.display = "none";
};
return;
}
const initialButtonText = btn.textContent;
const initialIconText = icon.textContent;
const initialTitleText = title.textContent;
const initialSubText = sub.textContent;
btn.onclick = async () => {
btn.disabled = true;
btn.textContent = "Downloading...";
icon.textContent = "download";
title.textContent = "Downloading update";
progressWrap.style.display = "flex";
try {
await window.electronAPI.downloadUpdate();
} catch {
btn.disabled = false;
btn.textContent = initialButtonText;
icon.textContent = initialIconText;
title.textContent = initialTitleText;
sub.textContent = initialSubText;
progressWrap.style.display = "none";
progressFill.style.width = "0%";
progressPct.textContent = "0%";
progressEta.textContent = "";
}
};
window.electronAPI.onDownloadProgress((progress) => {
const pct = Math.round(progress.percent);
progressFill.style.width = pct + "%";
progressPct.textContent = pct + "%";
const remaining = (progress.total - progress.transferred) / progress.bytesPerSecond;
progressEta.textContent =
remaining > 0 ? `~${Math.ceil(remaining)}s left` : "almost done";
});
window.electronAPI.onUpdateDownloaded(() => {
progressFill.style.width = "100%";
progressPct.textContent = "100%";
progressEta.textContent = "Done";
icon.textContent = "check";
title.textContent = "Ready to install";
sub.textContent = "Restart to apply the update";
btn.textContent = "Restart";
btn.disabled = false;
btn.onclick = () => window.electronAPI.installUpdate();
});
}
}
initUpdateUI();

View File

@@ -269,7 +269,7 @@ async function init() {
infoDiv.classList.add("visible");
});
})
});
}

121
public/script/reportBug.js Normal file
View File

@@ -0,0 +1,121 @@
class BugReportModal {
constructor() {
this.modal = null;
this.form = null;
this.init();
}
init() {
this.modal = document.createElement("div");
this.modal.className = "bug-modal-overlay";
this.modal.style.cssText = "display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); justify-content:center; align-items:center; z-index:99999; font-family:sans-serif;";
this.modal.innerHTML = `
<div class="bug-modal-content" style="background:#14141491 !important; color:#ffffff !important; padding:24px !important; border-radius:10px !important; width:420px !important; box-shadow: 0 10px 25px rgba(0,0,0,0.8) !important; display:flex !important; flex-direction:column !important; gap:16px !important; border: 1px solid #333 !important; box-sizing: border-box !important;">
<h3 style="margin:0 !important; font-size:1.2rem !important; font-weight:600 !important; color:#ffffff !important;">Report a Bug</h3>
<form id="bugReportForm" style="display:flex !important; flex-direction:column !important; gap:12px !important; width:unset !important; margin:0 !important; background:transparent !important; box-shadow: unset !important;">
<div style="display:flex !important; flex-direction:column !important; gap:4px !important;">
<label style="font-size:0.85rem !important; color:#aaaaaa !important;">Title <span style="color: red; font-weight: bold" >*</span> </label>
<input type="text" id="bugTitle" required style="background:#2a2a2a !important; border:1px solid #444 !important; color:#ffffff !important; border-radius:6px !important; padding:8px 10px !important; outline:none !important; width:100% !important; box-sizing: border-box !important;" />
</div>
<div style="display:flex !important; flex-direction:column !important; gap:4px !important;">
<label style="font-size:0.85rem !important; color:#aaaaaa !important;">Description <span style="color: red; font-weight: bold" >*</span></label>
<textarea id="bugDescription" placeholder="Provide some details about the bug" required rows="4" style="background:#2a2a2a !important; border:1px solid #444 !important; color:#ffffff !important; border-radius:6px !important; padding:8px 10px !important; outline:none !important; resize:vertical !important; width:100% !important; box-sizing: border-box !important;"></textarea>
</div>
<div style="display:flex !important; flex-direction:column !important; gap:4px !important;">
<label style="font-size:0.85rem !important; color:#aaaaaa !important;">Include Logs From Today</label>
<select id="bugIncludeLogs" style="background:#2a2a2a !important; border:1px solid #444 !important; color:#ffffff !important; border-radius:6px !important; padding:8px 10px !important; outline:none !important; width:100% !important; box-sizing: border-box !important;">
<option value="yes" style="background:#2a2a2a !important; color:#ffffff !important;">Yes</option>
<option value="no" style="background:#2a2a2a !important; color:#ffffff !important;">No</option>
</select>
</div>
<div style="display:flex !important; justify-content:flex-end !important; gap:8px !important; margin-top:8px !important;">
<button type="button" id="bugCancelBtn" style="background:#333333 !important; border:none !important; color:#cccccc !important; padding:8px 14px !important; border-radius:6px !important; cursor:pointer !important;">Cancel</button>
<button type="submit" style="background:#4f46e5 !important; border:none !important; color:#ffffff !important; padding:8px 16px !important; border-radius:6px !important; cursor:pointer !important; font-weight:500 !important;">Send</button>
</div>
</form>
</div>
`;
document.body.appendChild(this.modal);
this.form = this.modal.querySelector("#bugReportForm");
this.bindEvents();
}
bindEvents() {
const titleInput = this.modal.querySelector("#bugTitle");
const descInput = this.modal.querySelector("#bugDescription");
titleInput.addEventListener("input", () => {
localStorage.setItem("draft_bug_title", titleInput.value);
});
descInput.addEventListener("input", () => {
localStorage.setItem("draft_bug_desc", descInput.value);
});
this.modal.querySelector("#bugCancelBtn").addEventListener("click", () => {
this.close();
});
this.modal.addEventListener("click", (e) => {
if (e.target === this.modal) {
this.close();
}
});
this.form.addEventListener("submit", async (e) => {
e.preventDefault();
const data = {
title: titleInput.value,
description: descInput.value,
includeLogs: this.modal.querySelector("#bugIncludeLogs").value
};
window.electronAPI.logInfo("Submitting:", data);
try {
const success = await window.electronAPI.sendReport(data);
if (success) {
localStorage.removeItem("draft_bug_title");
localStorage.removeItem("draft_bug_desc");
this.form.reset();
this.close();
window.showSuccess("Bug report sent successfully!");
}
} catch (err) {
window.showError("Failed to send bug report.");
window.electronAPI.logError("Failed to send bug report:", err.message);
}
});
}
loadState() {
const savedTitle = localStorage.getItem("draft_bug_title");
const savedDesc = localStorage.getItem("draft_bug_desc");
if (savedTitle) {
this.modal.querySelector("#bugTitle").value = savedTitle;
}
if (savedDesc) {
this.modal.querySelector("#bugDescription").value = savedDesc;
}
}
open() {
this.loadState();
this.modal.style.display = "flex";
}
close() {
this.modal.style.display = "none";
}
}
window.bugModal = new BugReportModal();
document.getElementById("report-bug-btn").addEventListener("click", () => {
window.bugModal.open();
});

View File

@@ -16,8 +16,7 @@ let isOpen = false;
* Toggles visibility of the settings panel when clicking the settings button.
*/
settingsButton.addEventListener("click", () => {
const currentlyOpen = settingsPanel.style.display === "block";
settingsPanel.style.display = currentlyOpen ? "none" : "block";
settingsPanel.classList.toggle("open");
});
/**
@@ -25,7 +24,7 @@ settingsButton.addEventListener("click", () => {
*/
function closePanel() {
isOpen = false;
settingsPanel.style.display = "none";
settingsPanel.classList.remove("open");
}
/**

View File

@@ -17,18 +17,20 @@ window.showToast = function (message, type = "info", duration = 4000) {
toast.classList.add("toast", type);
const icons = {
success: "✓",
error: "✕",
warning: "⚠",
info: "ℹ"
success: "check_circle",
error: "error",
warning: "warning",
info: "info"
};
const icon = icons[type] || icons.info;
toast.style.setProperty("--toast-duration", `${duration}ms`);
toast.innerHTML = `
<span class="toast-icon">${icon}</span>
<span class="toast-icon material-icons-round">${icon}</span>
<span class="toast-message">${message}</span>
<button class="toast-close" title="Close">×</button>
<div class="toast-progress"></div>
`;
const closeBtn = toast.querySelector(".toast-close");

View File

@@ -0,0 +1,128 @@
:root {
--border-accent: #0ea5e966;
--bg-accent: #0ea5e915;
--fill-accent: #0ea5e9;
--text-accent: #0369a1;
--text-secondary: #64748b;
}
#updateAvailable {
display: none;
width: fit-content;
max-width: 420px;
position: absolute;
right: 18px;
bottom: 5%;
z-index: 50;
}
.update-banner {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px 16px;
border-radius: 10px;
border: 0.5px solid var(--border-accent);
background: var(--bg-accent);
backdrop-filter: blur(8px);
}
.banner-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.banner-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.icon-wrap {
width: 32px;
height: 32px;
border-radius: 8px;
background: var(--fill-accent);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.icon-wrap i {
font-size: 16px;
color: #fff;
}
.text-block {
display: flex;
flex-direction: column;
gap: 4px;
}
.update-title {
font-size: 13px;
font-weight: 600;
color: var(--text-accent);
}
.update-sub {
font-size: 11px;
color: var(--text-secondary);
line-height: 1.4;
}
.btn-update {
font-size: 12px;
font-weight: 500;
padding: 6px 14px;
border-radius: 8px;
background: var(--fill-accent);
color: #fff;
border: none;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
transition: opacity 0.2s;
}
.btn-update:hover:not(:disabled) {
opacity: 0.9;
}
.btn-update:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.progress-wrap {
display: flex;
flex-direction: column;
gap: 6px;
}
.progress-track {
height: 4px;
border-radius: 99px;
background: var(--border-accent);
overflow: hidden;
width: 100%;
}
.progress-fill {
height: 100%;
border-radius: 99px;
background: var(--fill-accent);
width: 0%;
transition: width 0.3s ease;
}
.progress-meta {
font-size: 11px;
color: var(--text-secondary);
display: flex;
justify-content: space-between;
}

View File

@@ -5,73 +5,70 @@
right: 20px;
display: flex;
flex-direction: column;
gap: 8px;
gap: 12px;
z-index: 9999;
pointer-events: none;
}
/* Toast */
.toast {
position: relative;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background-color: rgba(50, 50, 50, 0.9);
border-radius: 6px;
color: #ffffff;
font-size: 0.9rem;
max-width: 220px;
min-width: 100px;
animation: slideIn 0.25s ease-out;
gap: 12px;
padding: 14px 16px;
background-color: rgba(30, 30, 30, 0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
color: #f3f4f6;
font-family: inherit;
font-size: 0.875rem;
max-width: 320px;
min-width: 220px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.2);
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
pointer-events: auto;
backdrop-filter: blur(8px);
}
.toast.success {
background-color: rgba(40, 167, 69, 0.15);
}
.toast.error {
background-color: rgba(220, 53, 69, 0.15);
}
.toast.warning {
background-color: rgba(255, 193, 7, 0.15);
}
.toast.info {
background-color: rgba(23, 162, 184, 0.15);
overflow: hidden;
}
/* Toast Icon */
.toast-icon {
flex-shrink: 0;
font-size: 18px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.toast-icon svg {
width: 100%;
height: 100%;
}
.toast.success .toast-icon {
color: #28a745;
color: #22c55e;
}
.toast.error .toast-icon {
color: #dc3545;
color: #ef4444;
}
.toast.warning .toast-icon {
color: #ffc107;
color: #f59e0b;
}
.toast.info .toast-icon {
color: #17a2b8;
color: #0ea5e9;
}
/* Toast Message */
.toast-message {
flex: 1;
font-weight: 500;
font-weight: 400;
line-height: 1.4;
word-break: break-word;
}
/* Toast Close Button */
@@ -79,54 +76,83 @@
flex-shrink: 0;
background: none;
border: none;
color: currentColor;
color: #9ca3af;
cursor: pointer;
font-size: 18px;
padding: 0;
font-size: 16px;
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.5;
transition: opacity 0.2s ease;
transition: background-color 0.15s ease, color 0.15s ease;
}
.toast-close:hover {
opacity: 1;
background-color: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.toast-progress {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
width: 100%;
background-color: rgba(255, 255, 255, 0.1);
animation: progress var(--toast-duration, 4000ms) linear forwards;
}
.toast.success .toast-progress {
background-color: #22c55e;
}
.toast.error .toast-progress {
background-color: #ef4444;
}
/* Animations */
@keyframes slideIn {
from {
transform: translateX(350px);
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateX(0);
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
transform: translateY(0);
opacity: 1;
}
to {
transform: translateX(350px);
transform: translateY(-20px);
opacity: 0;
}
}
.toast.removing {
animation: slideOut 0.25s ease-out;
@keyframes progress {
from {
width: 100%;
}
to {
width: 0%;
}
}
.toast.removing {
animation: slideOut 0.25s ease-in forwards;
}
/* Responsive */
@media (max-width: 480px) {
#toast-container {
bottom: 12px;
right: 12px;
left: 12px;
top: auto;
bottom: 16px;
right: 16px;
left: 16px;
}
.toast {

View File

@@ -45,11 +45,22 @@
border-radius: 12px;
padding: 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
display: none;
overflow-y: auto;
z-index: 9999;
animation: slideIn 0.2s ease-out;
/*animation: slideIn 0.2s ease-out; */
color: var(--settings-text-color);
display: block;
opacity: 0;
pointer-events: none;
transform: translateX(20px);
transition: opacity 0.2s ease-out, transform 0.2s ease-out;
}
.settings-panel.open {
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
.settings-panel::-webkit-scrollbar {

View File

@@ -12,6 +12,7 @@
@import url("components/editpathbutton.css");
@import url("components/progressBar.css");
@import url("components/toast.css");
@import url("components/autoUpdate.css");
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');

1
scripts/CHANGELOG.md Normal file
View File

@@ -0,0 +1 @@
[//]: # (This file is used for the release CHANGELOG. Please use Markdown to describe changes.)

View File

@@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
f [[ "$VERSION" == *"-preview"* ]]; then
if [[ "$VERSION" == *"-preview"* ]]; then
echo "Error: The current version ($VERSION) is a 'preview"
echo "Unable to launch release pipeline. Please update the package.json with a stable version.."
exit 1
@@ -64,6 +64,16 @@ echo "[0/5] Cleaning workspace..."
rm -rf "$ROOT_DIR/dist" "$ROOT_DIR/srpm-out"
echo "Workspace cleaned (dist/ and srpm-out/ removed)"
# ------------------------------------------------------------
# Step 0.3 - Retrieve Changelog
# ------------------------------------------------------------
if [ ! -f "$ROOT_DIR/CHANGELOG.md" ]; then
echo "Warning: CHANGELOG.md not found, release notes will be empty."
CHANGELOG=""
else
CHANGELOG="$(cat "$ROOT_DIR/CHANGELOG.md")"
echo "Changelog retrieved successfully"
fi
# ------------------------------------------------------------
# Step 1 - Build Linux packages (AppImage, deb, snap)
# ------------------------------------------------------------
@@ -105,19 +115,13 @@ PREV_TAG=$(git rev-list --tags --skip=1 --max-count=1 | xargs git describe --tag
RELEASE_NOTES="# Freedom Loader - $VERSION
## Changelog
$CHANGELOG
## Found a bug or issue?
Please report it in the [GitHub Issues](https://github.com/MasterAcnolo/Freedom-Loader/issues) section.
## Next Release (non-exhaustive roadmap)
- More format options
- Subtitle support
- Improved UI / UX
- Language selection
- Download specific parts of videos
- File renaming options
- Parallel downloads
- Skip sponsored parts automatically
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/${PREV_TAG}...${TAG}"
if [ "$DRY_RUN" = false ]; then

View File

@@ -77,11 +77,11 @@ async function infoController(req, res) {
);
// Preserve original error context
throw err;
logger.error(err);
}
} else {
throw err;
logger.error(err);
}
}

View File

@@ -260,7 +260,7 @@ function validateBinaries() {
/**
* Helper to lazy load logger, avoid circular import
* @returns {winston.Logger}
* @returns {import("winston").Logger}
*/
function getLogger() {
return require("../logger.js").logger;

View File

@@ -17,11 +17,6 @@ try {
console.error(`Failed to create log directory: ${error.message}`);
}
const logFormat = format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
);
/**
* Format used by the saved file
* @type {Format}

View File

@@ -2,7 +2,7 @@ const express = require("express");
const path = require("path");
const config = require("../config");
const { logger, logSessionEnd } = require("./logger");
const {logger} = require("./logger");
const { rateLimit } = require("./helpers/rateLimit.helpers");
const app = express();

View File

@@ -65,7 +65,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
}
logger.error(`Could not find available playlist folder after 1000 attempts`);
throw new Error("Unable to create playlist folder");
logger.error("Unable to create playlist folder");
} catch (err) {
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);

View File

@@ -0,0 +1,53 @@
const mockDownloadUpdate = jest.fn();
const mockCheckForUpdates = jest.fn();
const mockOn = jest.fn();
const mockQuitAndInstall = jest.fn();
jest.mock("electron-updater", () => ({
autoUpdater: {
autoDownload: true,
autoInstallOnAppQuit: true,
on: mockOn,
checkForUpdates: mockCheckForUpdates,
downloadUpdate: mockDownloadUpdate,
quitAndInstall: mockQuitAndInstall,
},
}));
jest.mock("electron", () => ({
app: {
isPackaged: true,
},
}));
const mockLoggerError = jest.fn();
jest.mock("../../server/logger", () => ({
logger: {
info: jest.fn(),
warn: jest.fn(),
error: mockLoggerError,
},
}));
const { downloadUpdate } = require("../../app/autoUpdater");
describe("autoUpdater.downloadUpdate", () => {
beforeEach(() => {
jest.clearAllMocks();
});
test("resolves when electron-updater download succeeds", async () => {
mockDownloadUpdate.mockResolvedValue(undefined);
await expect(downloadUpdate()).resolves.toBeUndefined();
expect(mockDownloadUpdate).toHaveBeenCalledTimes(1);
});
test("rejects when electron-updater download fails", async () => {
const error = new Error("network down");
mockDownloadUpdate.mockRejectedValue(error);
await expect(downloadUpdate()).rejects.toThrow("network down");
expect(mockLoggerError).toHaveBeenCalledWith("Download failed:", "network down");
});
});