40 Commits

Author SHA1 Message Date
Axel Nicolas
69de9eb9fc Merge pull request #58 from MasterAcnolo/1.6.4
1.6.4 Version
2026-10-07 20:35:34 +02:00
MasterAcnolo
cfa85649b3 docs: update changelog to reflect recent changes and improvements 2026-10-07 19:25:51 +02:00
MasterAcnolo
773ed9550d refactor: remove autoUpdate feature from configuration and UI 2026-10-07 19:24:47 +02:00
MasterAcnolo
ea3a475525 docs: add option to auto-display changelog after updates in configuration 2026-10-07 18:19:58 +02:00
MasterAcnolo
738d5ec62f fix: update README to clarify custom top bar feature and add cache directory information 2026-10-07 18:04:58 +02:00
MasterAcnolo
aff7637358 fix: remove experimental label from system tray option description 2026-10-07 17:22:44 +02:00
MasterAcnolo
7ee6d6fbe7 refactor: remove custom top bar feature from configuration and UI 2026-10-07 17:21:10 +02:00
MasterAcnolo
6aa3a39819 feat: options to not show changelog on app init 2026-10-07 10:25:29 +02:00
MasterAcnolo
f0971ebccc fix: adjust size for custom controls in topbar 2026-10-06 21:43:27 +02:00
MasterAcnolo
18c8621095 fix: update release script to reference new RELEASE-CHANGELOG.md and correct deb package name 2026-10-06 21:43:11 +02:00
MasterAcnolo
b2eb3b3b40 feat: implement changelog feature with UI modal and caching 2026-10-06 21:42:57 +02:00
MasterAcnolo
7af6898301 fix: moved CHANGELOD.md to root 2026-10-05 10:54:59 +02:00
MasterAcnolo
dc8d0a62d7 fix: enhance clean:dist and build:rpm npm scripts 2026-10-04 23:09:15 +02:00
MasterAcnolo
cf65069496 fix: extract .desktop inside a dedicated file and add npm run test:metainfo and test:desktop to check file.
also clean:dist
2026-10-02 14:52:30 +02:00
MasterAcnolo
a308352ea0 fix: update website links to new domain 2026-10-02 09:26:46 +02:00
MasterAcnolo
3712813833 fix: update desktop entry and metainfo filename 2026-10-02 09:15:19 +02:00
MasterAcnolo
9398329959 fix(release): correct AppImage filename format in release script 2026-09-29 22:14:28 +02:00
MasterAcnolo
8614b0e32e chore: remove dotenv dependency and update version to 1.6.4 2026-09-29 22:00:40 +02:00
MasterAcnolo
c212302550 chore: remove dotenv dependency and .env.example file 2026-09-29 21:59:51 +02:00
MasterAcnolo
0835e13840 refactor: reduce build bundle size by 50% 2026-09-29 21:57:54 +02:00
MasterAcnolo
1c0dd32732 feat(bugReport): add service status check and update UI if sendReport server is down 2026-09-29 21:30:20 +02:00
MasterAcnolo
1e099f6bb6 fix(sendReport): remove API key requirement for bug report service configuration 2026-09-29 17:27:16 +02:00
MasterAcnolo
1937738ab8 fix(autoUpdate): improve error handling and UI reset during update process 2026-09-29 17:20:42 +02:00
MasterAcnolo
ac1d74ff23 fix(autoUpdate): better error handling in frontEnd
#50
2026-09-27 21:19:48 +02:00
copilot-swe-agent[bot]
7f86d46ae7 fix: validate backend bug report configuration before request
Co-authored-by: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com>
2026-09-27 18:31:59 +00:00
MasterAcnolo
187e3af559 fix: log transport names files local date 2026-09-27 20:03:25 +02:00
MasterAcnolo
e8c499fe23 fix: sendReport wrong IPC instance 2026-09-27 20:02:56 +02:00
MasterAcnolo
99b7164ff8 fix(update): don't check for update if the app is installed with SNAP or Flatpak 2026-09-27 16:23:28 +02: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
38 changed files with 2899 additions and 298 deletions

View File

@@ -9,8 +9,8 @@ assignees: 'MasterAcnolo'
## Pre-submission Checklist ## Pre-submission Checklist
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/#/faq) - [ ] I have checked the [FAQ](https://freedomloader.acnolo.fr/#/faq)
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/#/wiki) - [ ] I have reviewed the [Wiki](https://freedomloader.acnolo.fr/#/wiki)
- [ ] I have searched existing issues - [ ] I have searched existing issues
## Question ## Question

13
.gitignore vendored
View File

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

View File

@@ -112,8 +112,8 @@ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
## Questions? ## Questions?
- Check the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/faq) - Check the [FAQ](https://freedomloader.acnolo.fr/faq)
- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki) - Review the [Wiki](https://freedomloader.acnolo.fr/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)
--- ---

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<a href="https://masteracnolo.github.io/Freedom-Loader-Site/"><img <a href="https://freedomloader.acnolo.fr/"><img
src="./build/banner.png" src="./build/banner.png"
alt="Banner" alt="Banner"
style="width: 50%;"/></a> style="width: 50%;"/></a>
@@ -9,7 +9,7 @@
[![Release](https://img.shields.io/github/v/release/MasterAcnolo/Freedom-Loader?style=for-the-badge&color=blue)](https://github.com/MasterAcnolo/Freedom-Loader/releases) [![Release](https://img.shields.io/github/v/release/MasterAcnolo/Freedom-Loader?style=for-the-badge&color=blue)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0)
[![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Site/) [![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://freedomloader.acnolo.fr/)
[![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Workshop/) [![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](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>
@@ -56,7 +56,7 @@ 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
- **Custom themes** - ZIP-based theme system with live preview - **Custom themes** - ZIP-based theme system with live preview
- **Theme Workshop** - web-based theme creator and browser - **Theme Workshop** - web-based theme creator and browser
- **Toast notifications** - in-app feedback system - **Toast notifications** - in-app feedback system
@@ -151,6 +151,7 @@ freedom-loader
|---------------|---------------------------------------------------------|--------------------------------------| |---------------|---------------------------------------------------------|--------------------------------------|
| **Downloads** | `C:\Users\[USERNAME]\Downloads\Freedom Loader` | `~/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/` | | **Logs** | `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\` | `~/.local/share/FreedomLoader/logs/` |
| **Cache** | ``
> [!NOTE] > [!NOTE]
> The download folder can be changed anytime via the "Edit" button in the UI, on both platforms. > The download folder can be changed anytime via the "Edit" button in the UI, on both platforms.
@@ -224,10 +225,8 @@ Freedom Loader can be configured either through the settings panel in the UI or
### Available Options ### Available Options
```json ```json
{ {
"autoUpdate": true,
"systemTray": true, "systemTray": true,
"discordRPC": true, "discordRPC": true,
"customTopBar": true,
"autoCheckInfo": true, "autoCheckInfo": true,
"addThumbnail": true, "addThumbnail": true,
"addMetadata": true, "addMetadata": true,
@@ -241,6 +240,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
"downloadSystem": true, "downloadSystem": true,
"notifySystem": true, "notifySystem": true,
"enableHardwareAcceleration": true, "enableHardwareAcceleration": true,
"showChanglogsAfterUpdate": true,
"theme": "dark" "theme": "dark"
} }
``` ```
@@ -249,10 +249,8 @@ Freedom Loader can be configured either through the settings panel in the UI or
| Option | Type | Default | Description | | Option | Type | Default | Description |
|------------------------------|---------|----------|------------------------------------------------------------| |------------------------------|---------|----------|------------------------------------------------------------|
| `autoUpdate` | boolean | `true` | Enable automatic application updates | | `systemTray` | boolean | `false` | Allow app to minimize on close |
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration | | `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 | | `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files | | `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files | | `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
@@ -266,6 +264,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
| `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 | | `enableHardwareAcceleration` | boolean | `true` | Enable Hardware Acceleration |
| `showChanglogsAfterUpdate` | boolean | `true` | Auto-display changelog popup after and update |
| `theme` | string | `dark` | Current Theme Name | | `theme` | string | `dark` | Current Theme Name |
### Supported Codecs ### Supported Codecs
@@ -354,7 +353,7 @@ See [BINARIES.md](./BINARIES.md) for how to setup the binaries.
## Roadmap ## Roadmap
See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap). See the most recent Roadmap [Here](https://freedomloader.acnolo.fr/Roadmap).
## Contributing ## Contributing
@@ -367,13 +366,29 @@ Head over to the [Freedom Loader Workshop repository](https://github.com/MasterA
### Bug Reports ### 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 - 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\` or `~/.local/share/FreedomLoader/logs/` - Relevant logs from `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs/`
- Screenshots if applicable - Screenshots or recordings if applicable
### Feature Requests ### Feature Requests
@@ -418,6 +433,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/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) [Website](https://freedomloader.acnolo.fr/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://freedomloader.acnolo.fr/wiki) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
</div> </div>

View File

@@ -9,7 +9,6 @@
*/ */
const { autoUpdater } = require("electron-updater"); const { autoUpdater } = require("electron-updater");
const { dialog } = require("electron");
const { logger } = require("../server/logger"); const { logger } = require("../server/logger");
/** /**
@@ -21,6 +20,26 @@ const { logger } = require("../server/logger");
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = 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. * Initializes application auto-update lifecycle.
* *
@@ -32,7 +51,6 @@ autoUpdater.autoInstallOnAppQuit = false;
* @param {BrowserWindow} mainWindow - main Electron window instance * @param {BrowserWindow} mainWindow - main Electron window instance
*/ */
function initAutoUpdater(mainWindow) { function initAutoUpdater(mainWindow) {
/** /**
* Triggered when a new version is detected. * Triggered when a new version is detected.
* Prompts user to install or defer update. * Prompts user to install or defer update.
@@ -40,23 +58,12 @@ function initAutoUpdater(mainWindow) {
autoUpdater.on("update-available", async (info) => { autoUpdater.on("update-available", async (info) => {
logger.info(`Update available: ${info.version}`); logger.info(`Update available: ${info.version}`);
const { response } = await dialog.showMessageBox(mainWindow, { mainWindow?.webContents.send("update-available", {
type: "info", version: info.version,
title: "Update Available", environment: getUpdateEnvironment(),
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,
}); });
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) => { autoUpdater.on("download-progress", (progress) => {
logger.info(`Download progress: ${Math.round(progress.percent)}%`); logger.info(`Download progress: ${Math.round(progress.percent)}%`);
mainWindow?.webContents.send("update-progress", { mainWindow?.webContents.send("update-progress", {
percent: Math.round(progress.percent), percent: progress.percent,
speed: progress.bytesPerSecond, bytesPerSecond: progress.bytesPerSecond,
transferred: progress.transferred,
total: progress.total,
}); });
}); });
@@ -78,17 +87,9 @@ function initAutoUpdater(mainWindow) {
autoUpdater.on("update-downloaded", async (info) => { autoUpdater.on("update-downloaded", async (info) => {
logger.info(`Update downloaded: ${info.version}`); logger.info(`Update downloaded: ${info.version}`);
const { response } = await dialog.showMessageBox(mainWindow, { mainWindow?.webContents.send("update-downloaded", info);
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();
}); });
/** /**
@@ -102,14 +103,16 @@ function initAutoUpdater(mainWindow) {
* If no update is available, I put this because there is no Linux version before 1.6.0 * If no update is available, I put this because there is no Linux version before 1.6.0
* @type {boolean} * @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) { if (isNoUpdateAvailable) {
logger.warn("Auto update: no update metadata found (probably no previous release), ignoring", msg); logger.warn("Auto update: no update metadata found (probably no previous release), ignoring", msg);
return; return;
} }
logger.error("Auto update error:", msg); logger.error("Auto update error:", err);
mainWindow?.webContents.send("update-error", msg);
}); });
checkForUpdates(); checkForUpdates();
@@ -121,18 +124,9 @@ function initAutoUpdater(mainWindow) {
*/ */
async function checkForUpdates() { async function checkForUpdates() {
/** // If app is was installed from SNAP or Flatpak.
* Prevents electron-updater from running inside sandboxed environments. if (process.env.SNAP || process.env.FLATPAK_ID) return;
* Snap and Flatpak lock the file system in read-only mode, causing the updater to crash. if (!require("electron").app.isPackaged) return;
* 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 ;
try { try {
await autoUpdater.checkForUpdates(); await autoUpdater.checkForUpdates();
@@ -150,6 +144,7 @@ async function downloadUpdate() {
await autoUpdater.downloadUpdate(); await autoUpdater.downloadUpdate();
} catch (err) { } catch (err) {
logger.error("Download failed:", err.message); logger.error("Download failed:", err.message);
throw err;
} }
} }
@@ -160,4 +155,13 @@ function installUpdate() {
autoUpdater.quitAndInstall(); autoUpdater.quitAndInstall();
} }
module.exports = { initAutoUpdater, downloadUpdate, installUpdate }; function isUpdateAvailable() {
return updateAvailable;
}
module.exports = {
initAutoUpdater,
downloadUpdate,
installUpdate,
isUpdateAvailable,
};

177
app/changelogManager.js Normal file
View File

@@ -0,0 +1,177 @@
const { app } = require("electron");
const fs = require("fs");
const path = require("path");
const https = require("https");
const config = require("../config");
const { logger } = require("../server/logger");
/** Maximum age of the cached release list before it must be refreshed. */
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
/** Filename used to persist changelog data in the Electron user data folder. */
const CACHE_FILE_NAME = "changelog-cache.json";
/** GitHub API endpoint containing the application's published releases. */
const RELEASES_URL = "https://api.github.com/repos/MasterAcnolo/Freedom-Loader/releases";
/** Returns the path of the local changelog cache file. */
function getCachePath() {
return path.join(app.getPath("userData"), CACHE_FILE_NAME);
}
/**
* Extracts the numeric major, minor, and patch parts from a version string.
*
* @param {string} version - Version string or release tag
* @returns {number[]|null} Parsed version parts, or null when invalid
*/
function normalizeVersion(version) {
const match = String(version || "").match(/(\d+)\.(\d+)\.(\d+)/);
return match ? match.slice(1).map(Number) : null;
}
/**
* Compares two semantic version strings.
*
* @param {string} left - First version to compare
* @param {string} right - Second version to compare
* @returns {number} Negative, zero, or positive comparison result
*/
function compareVersions(left, right) {
const leftParts = normalizeVersion(left) || [0, 0, 0];
const rightParts = normalizeVersion(right) || [0, 0, 0];
for (let index = 0; index < 3; index += 1) {
if (leftParts[index] !== rightParts[index]) {
return leftParts[index] - rightParts[index];
}
}
return 0;
}
/** Reads and parses the cached changelog, if one is available. */
function readCache() {
try {
return JSON.parse(fs.readFileSync(getCachePath(), "utf8"));
} catch {
return null;
}
}
/** Persists the fetched releases together with their cache timestamp. */
function writeCache(releases) {
try {
fs.writeFileSync(
getCachePath(),
JSON.stringify({ cachedAt: Date.now(), releases }, null, 2),
"utf8",
);
} catch (error) {
logger.warn(`Unable to cache changelog: ${error.message}`);
}
}
/**
* Fetches published releases from GitHub and maps them to the UI data shape.
*
* @returns {Promise<object[]>} Published releases
*/
function fetchReleases() {
return new Promise((resolve, reject) => {
const request = https.get(
RELEASES_URL,
{
headers: {
Accept: "application/vnd.github+json",
"User-Agent": "Freedom-Loader",
},
},
(response) => {
let body = "";
response.setEncoding("utf8");
response.on("data", (chunk) => {
body += chunk;
});
response.on("end", () => {
// Do not attempt to parse an error response as a release list.
if (response.statusCode !== 200) {
reject(new Error(`GitHub returned HTTP ${response.statusCode}`));
return;
}
try {
// Draft releases are not visible to users and should not appear in the changelog.
const releases = JSON.parse(body)
.filter((release) => !release.draft)
.map((release) => ({
version: release.tag_name,
name: release.name || release.tag_name,
date: release.published_at || release.created_at,
body: release.body || "No changelog available.",
url: release.html_url,
}));
resolve(releases);
} catch (error) {
reject(error);
}
});
},
);
request.setTimeout(10000, () => {
request.destroy(new Error("Changelog request timed out"));
});
request.on("error", reject);
});
}
/**
* Returns cached or freshly fetched releases and identifies unread versions.
*
* @returns {Promise<object>} Changelog data for the renderer
*/
async function getChangelog() {
const cache = readCache();
let releases = cache?.releases || [];
const cacheIsFresh = cache && cache.releases?.length > 0 && Date.now() - cache.cachedAt < CACHE_TTL_MS;
// Refresh stale or missing data, while keeping the last cache as a fallback.
if (!cacheIsFresh) {
try {
releases = await fetchReleases();
writeCache(releases);
} catch (error) {
logger.warn(`Unable to retrieve changelog: ${error.message}`);
}
}
const lastSeenVersion = cache?.lastSeenVersion || null;
// Without a previous version, all available releases are considered unread.
const unreadReleases = lastSeenVersion
? releases.filter((release) => compareVersions(release.version, lastSeenVersion) > 0)
: releases;
return {
// An empty release list means that the fetch failed or returned no releases.
error: releases.length === 0,
currentVersion: config.version,
lastSeenVersion,
hasUnread: unreadReleases.length > 0,
releases,
unreadReleases,
};
}
/** Marks the current application version as seen in the local cache. */
function markChangelogSeen() {
const cache = readCache() || { releases: [] };
cache.lastSeenVersion = config.version;
cache.cachedAt = cache.cachedAt || 0 ;
fs.mkdirSync(path.dirname(getCachePath()), { recursive: true });
fs.writeFileSync(getCachePath(), JSON.stringify(cache, null, 2), "utf8");
return true;
}
module.exports = { getChangelog, markChangelogSeen };

View File

@@ -95,9 +95,9 @@ function startRPC() {
smallImageText: "By MasterAcnolo", smallImageText: "By MasterAcnolo",
startTimestamp: new Date(), startTimestamp: new Date(),
details: `Open Source Download Tools - ${config.version}`, details: `Open Source Download Tools - ${config.version}`,
state: "masteracnolo.github.io/FreedomLoader", state: "freedomloader.acnolo.fr",
}; };
rpc.clearActivity() rpc.clearActivity();
rpc.setActivity(presence); rpc.setActivity(presence);
}); });

73
app/ipcHandlers.js Normal file → Executable file
View File

@@ -13,12 +13,15 @@
const { ipcMain, dialog, shell } = require("electron"); const { ipcMain, dialog, shell } = require("electron");
const fs = require("fs"); const fs = require("fs");
const { logger, logDir } = require("../server/logger"); const { logger, logDir } = require("../server/logger");
const {configFeatures, featuresPath, devMode} = require("../config"); const { configFeatures, featuresPath, devMode } = require("../config");
const { getThemes, reloadThemes } = require("./themeManager"); const { getThemes, reloadThemes } = require("./themeManager");
const config = require("../config"); const config = require("../config");
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator"); const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
const { userThemesPath } = require("../server/helpers/path.helpers"); const { userThemesPath } = require("../server/helpers/path.helpers");
const { createSystemTray, destroyTray } = require("./tray"); const { createSystemTray, destroyTray } = require("./tray");
const {sendReport, checkReportService, getReportServiceStatus} = require("./sendReport");
const { isUpdateAvailable, downloadUpdate, installUpdate } = require("./autoUpdater");
const { getChangelog, markChangelogSeen } = require("./changelogManager");
/** /**
* Security whitelist for feature flags that can be modified at runtime. * Security whitelist for feature flags that can be modified at runtime.
@@ -27,10 +30,8 @@ const { createSystemTray, destroyTray } = require("./tray");
* Acts as a basic validation layer for IPC "set-feature". * Acts as a basic validation layer for IPC "set-feature".
*/ */
const FEATURE_WHITELIST = new Set([ const FEATURE_WHITELIST = new Set([
"autoUpdate",
"systemTray", "systemTray",
"discordRPC", "discordRPC",
"customTopBar",
"autoCheckInfo", "autoCheckInfo",
"addThumbnail", "addThumbnail",
"addMetadata", "addMetadata",
@@ -41,7 +42,8 @@ const FEATURE_WHITELIST = new Set([
"theme", "theme",
"createPlaylistFolders", "createPlaylistFolders",
"notifySystem", "notifySystem",
"enableHardwareAcceleration" "enableHardwareAcceleration",
"showChanglogsAfterUpdate"
]); ]);
/** /**
@@ -64,12 +66,21 @@ const themeFolderPath = userThemesPath;
* @param {Function} getMainWindow - Function returning current BrowserWindow instance * @param {Function} getMainWindow - Function returning current BrowserWindow instance
*/ */
function registerIpcHandlers(getMainWindow) { function registerIpcHandlers(getMainWindow) {
/** /**
* Returns application version from config. * Returns application version from config.
*/ */
ipcMain.handle("version", () => config.version); ipcMain.handle("version", () => config.version);
/**
* Retrieves the changelog from cache or GitHub API.
*/
ipcMain.handle("get-changelog", () => getChangelog());
/**
* Marks all changelog entries as seen and updates cache.
*/
ipcMain.handle("mark-changelog-seen", () => markChangelogSeen());
/** /**
* Returns runtime feature configuration object. * Returns runtime feature configuration object.
*/ */
@@ -117,15 +128,14 @@ function registerIpcHandlers(getMainWindow) {
ipcMain.on("window-minimize", () => { ipcMain.on("window-minimize", () => {
// Minimize to tray // Minimize to tray
if (configFeatures.systemTray) { if (configFeatures.systemTray) {
getMainWindow()?.hide() getMainWindow()?.hide();
logger.info("Window Minimized in SystemTray (Using Minimize Button)"); logger.info("Window Minimized in SystemTray (Using Minimize Button)");
} else { } else {
// Native minimize // Native minimize
getMainWindow()?.minimize() getMainWindow()?.minimize();
logger.info("Window minimized normally"); logger.info("Window minimized normally");
} }
} });
);
/** /**
* Toggles maximize/unmaximize state of main window. * Toggles maximize/unmaximize state of main window.
@@ -141,7 +151,6 @@ function registerIpcHandlers(getMainWindow) {
*/ */
ipcMain.on("window-close", () => getMainWindow()?.close()); ipcMain.on("window-close", () => getMainWindow()?.close());
/** /**
* Opens Chromium DevTools in detached mode. * Opens Chromium DevTools in detached mode.
*/ */
@@ -158,14 +167,14 @@ function registerIpcHandlers(getMainWindow) {
* Opens external website in default browser. * Opens external website in default browser.
*/ */
ipcMain.on("open-website", () => ipcMain.on("open-website", () =>
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/") shell.openExternal("https://freedomloader.acnolo.fr/")
); );
/** /**
* Opens official wiki page in external browser. * Opens official wiki page in external browser.
*/ */
ipcMain.on("open-wiki", () => ipcMain.on("open-wiki", () =>
shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki") shell.openExternal("https://freedomloader.acnolo.fr/wiki")
); );
/** /**
@@ -180,7 +189,12 @@ function registerIpcHandlers(getMainWindow) {
*/ */
ipcMain.on("open-config", () => shell.openPath(configFolderPath)); 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. * Retrieves available themes from filesystem.
@@ -199,6 +213,36 @@ function registerIpcHandlers(getMainWindow) {
return await reloadThemes(); return await reloadThemes();
}); });
/**
* Send bug report
*/
ipcMain.handle("send-report", async (_, params) => {
return await sendReport(params);
});
ipcMain.handle("get-report-service-status", async () => {
await checkReportService();
return getReportServiceStatus();
});
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. * Updates a runtime feature flag and persists it to disk.
* *
@@ -246,12 +290,11 @@ function registerIpcHandlers(getMainWindow) {
fs.writeFileSync( fs.writeFileSync(
configFolderPath, configFolderPath,
JSON.stringify(configFeatures, null, 2), JSON.stringify(configFeatures, null, 2),
"utf-8" "utf-8",
); );
logger.info(`Feature updated: ${key} = ${value}`); logger.info(`Feature updated: ${key} = ${value}`);
return true; return true;
} catch (err) { } catch (err) {
logger.error(`set-feature failed (${key}): ${err.message}`); logger.error(`set-feature failed (${key}): ${err.message}`);
return false; return false;

108
app/sendReport.js Normal file
View File

@@ -0,0 +1,108 @@
const fs = require("fs");
const path = require("path");
const {logger, logDir} = require("../server/logger");
const REPORT_URL = "https://freedom-loader-report.acnolo.fr";
const REPORT_SERVICE_TIMEOUT_MS = 5000;
let reportServiceStatus = {
status: "checking",
message: "Checking bug report service..."
};
let reportServiceCheck = null;
async function checkReportService() {
if (reportServiceCheck) {
return reportServiceCheck;
}
reportServiceCheck = (async () => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), REPORT_SERVICE_TIMEOUT_MS);
try {
const response = await fetch(REPORT_URL, {
method: "GET",
signal: controller.signal
});
if (response.status >= 500) {
throw new Error(`Server responded with status ${response.status}`);
}
reportServiceStatus = {
status: "online",
message: "Bug report service is available."
};
logger.info(`Bug report service is reachable (status ${response.status}).`);
} catch (err) {
reportServiceStatus = {
status: "offline",
message: "Bug report service is unavailable. Reports may fail to send."
};
logger.warn(`Bug report service is unreachable: ${err.message}`);
} finally {
clearTimeout(timeout);
}
return reportServiceStatus;
})();
return reportServiceCheck;
}
function getReportServiceStatus() {
return reportServiceStatus;
}
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 now = new Date();
const today = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, "0")
].join("-");
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(REPORT_URL, {
method: "POST",
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, checkReportService, getReportServiceStatus};

View File

@@ -95,7 +95,7 @@ function createSystemTray(devMode) {
} else { } else {
window.show(); window.show();
window.focus(); window.focus();
logger.info("Window Restored from SystemTray (Double Click on Tray Icon)") logger.info("Window Restored from SystemTray (Double Click on Tray Icon)");
} }
}); });

View File

@@ -57,7 +57,7 @@ async function createMainWindow() {
height: 800, height: 800,
minWidth: 750, minWidth: 750,
minHeight: 800, minHeight: 800,
frame: !configFeatures.customTopBar, frame: false, // Force custom topbar for consistent UI across platforms, and less maintenance headaches with native titlebar actions
devTools: !app.isPackaged, devTools: !app.isPackaged,
show: false, show: false,
webPreferences: { webPreferences: {

View File

@@ -1,8 +1,6 @@
{ {
"autoUpdate": true,
"systemTray": false, "systemTray": false,
"discordRPC": true, "discordRPC": true,
"customTopBar": true,
"autoCheckInfo": true, "autoCheckInfo": true,
"addThumbnail": true, "addThumbnail": true,
"addMetadata": true, "addMetadata": true,
@@ -16,5 +14,6 @@
"downloadSystem": true, "downloadSystem": true,
"notifySystem": true, "notifySystem": true,
"enableHardwareAcceleration": true, "enableHardwareAcceleration": true,
"showChanglogsAfterUpdate": true,
"theme": "dark" "theme": "dark"
} }

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}
}
}
];

15
main.js
View File

@@ -13,6 +13,9 @@ process.on("unhandledRejection", (reason) => {
app.quit(); app.quit();
}); });
if (process.platform === 'linux') {
app.setDesktopName('com.masteracnolo.freedomloader.desktop');
}
/** /**
* True if this is the primary instance (lock acquired successfully) * True if this is the primary instance (lock acquired successfully)
@@ -48,9 +51,9 @@ const { configFeatures, devMode} = require("./config");
*/ */
if (!configFeatures.enableHardwareAcceleration){ if (!configFeatures.enableHardwareAcceleration){
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
logger.info("Disabled Hardware Acceleration") logger.info("Disabled Hardware Acceleration");
} else { } else {
logger.info("Enable Hardware Acceleration") logger.info("Enable Hardware Acceleration");
} }
if(devMode){ if(devMode){
@@ -59,7 +62,7 @@ if(devMode){
*/ */
const { devtron } = require('@electron/devtron'); const { devtron } = require('@electron/devtron');
devtron.install(); devtron.install();
logger.info("Loaded DevTron Extension") logger.info("Loaded DevTron Extension");
} }
/** /**
@@ -77,6 +80,7 @@ const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./
const { userThemesPath, initUserThemes, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers"); const { userThemesPath, initUserThemes, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
const {createSystemTray, destroyTray} = require("./app/tray"); const {createSystemTray, destroyTray} = require("./app/tray");
const { stopServer } = require("./server/server"); const { stopServer } = require("./server/server");
const {checkReportService} = require("./app/sendReport");
/** /**
* Global flag indicating if the application is intentionally shutting down. * Global flag indicating if the application is intentionally shutting down.
@@ -132,6 +136,9 @@ app.on("second-instance", () => {
app.whenReady().then(async () => { app.whenReady().then(async () => {
logSessionStart(logDir, defaultDownloadFolder); logSessionStart(logDir, defaultDownloadFolder);
checkReportService().catch((err) => {
logger.warn(`Bug report service startup check failed: ${err.message}`);
});
createSplashWindow(); createSplashWindow();
@@ -166,7 +173,7 @@ app.whenReady().then(async () => {
if (configFeatures.discordRPC) startRPC(); if (configFeatures.discordRPC) startRPC();
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow()); initAutoUpdater(getMainWindow());
} catch (err) { } catch (err) {
logger.error("Boot error:", err); logger.error("Boot error:", err);

1083
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,10 @@
"name": "freedom-loader", "name": "freedom-loader",
"desktopName": "com.masteracnolo.freedomloader", "desktopName": "com.masteracnolo.freedomloader",
"productName": "Freedom Loader", "productName": "Freedom Loader",
"version": "1.6.3", "version": "1.6.4",
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>", "author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
"description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms", "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/", "homepage": "https://freedomloader.acnolo.fr/",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/MasterAcnolo/Freedom-Loader.git" "url": "https://github.com/MasterAcnolo/Freedom-Loader.git"
@@ -16,6 +16,7 @@
}, },
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"clean:dist": "rm -rf dist/ && rm -rf srpm-out/",
"start": "electron --no-warnings .", "start": "electron --no-warnings .",
"start:warn": "electron --trace-warnings .", "start:warn": "electron --trace-warnings .",
"start:debug": "WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .", "start:debug": "WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .",
@@ -25,13 +26,17 @@
"build": "electron-builder", "build": "electron-builder",
"build:win": "electron-builder --win", "build:win": "electron-builder --win",
"build:linux": "electron-builder --linux", "build:linux": "electron-builder --linux",
"build:rpm": "bash scripts/create-copr-package.sh", "build:rpm": "npm run clean:dist && npm run build:linux && bash scripts/create-copr-package.sh",
"build:all": "electron-builder -wl", "build:all": "electron-builder -wl",
"release": "bash scripts/release.sh", "release": "bash scripts/release.sh",
"release:publish": "bash scripts/release.sh --publish", "release:publish": "bash scripts/release.sh --publish",
"release:dry-run": "bash scripts/release.sh --dry-run", "release:dry-run": "bash scripts/release.sh --dry-run",
"test": "jest", "test": "jest",
"test:unit": "jest test/unit", "test:unit": "jest test/unit",
"test:metainfo": "appstreamcli validate package/com.masteracnolo.freedomloader.metainfo.xml",
"test:desktop": "desktop-file-validate package/com.masteracnolo.freedomloader.desktop",
"lint": "eslint . --format pretty",
"lint:fix": "eslint . --fix --format pretty",
"update": "npm update" "update": "npm update"
}, },
"dependencies": { "dependencies": {
@@ -48,16 +53,21 @@
}, },
"devDependencies": { "devDependencies": {
"@electron/devtron": "^2.1.1", "@electron/devtron": "^2.1.1",
"@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1", "@jest/globals": "^30.4.1",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
"electron": "^44.3.0", "electron": "^44.3.0",
"electron-builder": "^26.15.3", "electron-builder": "^26.15.3",
"eslint": "^10.10.0",
"eslint-formatter-pretty": "^7.1.0",
"globals": "^17.12.0",
"jest": "^30.4.2", "jest": "^30.4.2",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"playwright": "^1.62.1" "playwright": "^1.62.1"
}, },
"build": { "build": {
"appId": "com.masteracnolo.freedomloader", "appId": "com.masteracnolo.freedomloader",
"artifactName": "Freedom-Loader-${version}.${ext}",
"productName": "Freedom Loader", "productName": "Freedom Loader",
"publish": [ "publish": [
{ {
@@ -68,10 +78,15 @@
} }
], ],
"files": [ "files": [
"**/*", "app/**/*",
"node_modules/**/*", "public/**/*",
"!config/config.dev.json", "server/**/*",
"!theme/**/*" "build/**/*",
"config/config.default.json",
"config.js",
"main.js",
"preload.js",
"package.json"
], ],
"directories": { "directories": {
"buildResources": "build" "buildResources": "build"

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Name=Freedom Loader
Exec=/opt/freedom-loader/freedom-loader %U
Icon=freedom-loader
Type=Application
Categories=AudioVideo;
Comment=Free and open-source GUI for yt-dlp
StartupWMClass=com.masteracnolo.freedomloader

View File

@@ -45,7 +45,7 @@
</p> </p>
</description> </description>
<launchable type="desktop-id">freedom-loader.desktop</launchable> <launchable type="desktop-id">com.masteracnolo.freedomloader.desktop</launchable>
<url type="homepage">https://github.com/MasterAcnolo/Freedom-Loader</url> <url type="homepage">https://github.com/MasterAcnolo/Freedom-Loader</url>
<url type="bugtracker">https://github.com/MasterAcnolo/Freedom-Loader/issues</url> <url type="bugtracker">https://github.com/MasterAcnolo/Freedom-Loader/issues</url>

View File

@@ -8,6 +8,26 @@ const { contextBridge, ipcRenderer } = require("electron");
* All calls are explicitly whitelisted. * All calls are explicitly whitelisted.
*/ */
contextBridge.exposeInMainWorld("electronAPI", { 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 * Return process.platform to renderer
@@ -42,14 +62,23 @@ contextBridge.exposeInMainWorld("electronAPI", {
* @param {string} key - Feature name * @param {string} key - Feature name
* @param {any} value - Feature value * @param {any} value - Feature value
*/ */
setFeature: (key, value) => setFeature: (key, value) => ipcRenderer.invoke("set-feature", { key, value }),
ipcRenderer.invoke("set-feature", { key, value }),
/** /**
* Returns the current application version. * Returns the current application version.
*/ */
getVersion: () => ipcRenderer.invoke("version"), getVersion: () => ipcRenderer.invoke("version"),
/**
* Returns the changelog data
*/
getChangelog: () => ipcRenderer.invoke("get-changelog"),
/**
* Marks the current changelog version as seen by the user.
*/
markChangelogSeen: () => ipcRenderer.invoke("mark-changelog-seen"),
/** /**
* Validates a download path before using it for file operations. * Validates a download path before using it for file operations.
* *
@@ -67,8 +96,70 @@ contextBridge.exposeInMainWorld("electronAPI", {
* Forces a reload of theme files (useful after modification/import). * Forces a reload of theme files (useful after modification/import).
*/ */
reloadThemes: () => ipcRenderer.invoke("reload-themes"), 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 update error event.
*
* @param {Function} callback - Function executed with error information when an update error occurs.
* @returns {Electron.IpcRenderer}
*/
onUpdateError: (callback) => ipcRenderer.on("update-error", (_, err) => callback(err)),
/**
* Subscribes to the download progress event.
*
* @param {Function} callback - Function executed with progress metrics (percent, bytesPerSecond, transferred, total).
* @returns {Electron.IpcRenderer}
*/
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. * Exposes window control and developer utilities for the custom topbar UI.
@@ -76,7 +167,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
* These methods forward commands to the Electron main process via IPC. * These methods forward commands to the Electron main process via IPC.
*/ */
contextBridge.exposeInMainWorld("topbarAPI", { contextBridge.exposeInMainWorld("topbarAPI", {
/** /**
* Minimizes the application window. * Minimizes the application window.
*/ */
@@ -126,4 +216,17 @@ contextBridge.exposeInMainWorld("topbarAPI", {
* Opens configuration/settings panel. * Opens configuration/settings panel.
*/ */
openConfig: () => ipcRenderer.send("open-config"), openConfig: () => ipcRenderer.send("open-config"),
/**
* Send Bug Report
*
* @param params
* @returns {Promise<any>}
*/
sendReport: (params) => ipcRenderer.invoke("send-report", params),
/**
* Returns the startup health status of the bug report service.
*/
getReportServiceStatus: () => ipcRenderer.invoke("get-report-service-status"),
}); });

View File

@@ -13,6 +13,8 @@
<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">
<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> </head>
<body> <body>
@@ -21,9 +23,11 @@
<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="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="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="workshop-btn" title="Workshop">Theme Workshop</button>
<button id="changelog-btn" title="Changelog">Changelog</button>
<!-- <button id="theme-btn" title="Theme">Theme</button> --> <!-- <button id="theme-btn" title="Theme">Theme</button> -->
</div> </div>
<div class="window-controls"> <div class="window-controls">
@@ -120,10 +124,10 @@
<summary>Advanced Settings</summary> <summary>Advanced Settings</summary>
<div class="setting-item"> <div class="setting-item">
<input type="checkbox" data-key="autoUpdate" id="autoUpdate"> <input type="checkbox" data-key="showChanglogsAfterUpdate" id="showChanglogsAfterUpdate">
<div class="setting-info"> <div class="setting-info">
<span class="setting-title">Auto Update</span> <span class="setting-title">Show Changelogs After Update</span>
<small>Enable automatic app updates.</small> <small>Display changelogs after each update.</small>
</div> </div>
</div> </div>
@@ -131,7 +135,7 @@
<input data-key="systemTray" id="systemTray" type="checkbox"> <input data-key="systemTray" id="systemTray" type="checkbox">
<div class="setting-info"> <div class="setting-info">
<span class="setting-title">System Tray</span> <span class="setting-title">System Tray</span>
<small>Minimize app on close, in the System Tray. [EXPERIMENTAL]</small> <small>Minimize app on close, in the System Tray.</small>
</div> </div>
</div> </div>
@@ -167,13 +171,13 @@
</div> </div>
</div> </div>
<div class="setting-item"> <!-- <div class="setting-item">
<input type="checkbox" data-key="customTopBar" id="customTopBar"> <input type="checkbox" data-key="customTopBar" id="customTopBar">
<div class="setting-info"> <div class="setting-info">
<span class="setting-title">Custom Top bar</span> <span class="setting-title">Custom Top bar</span>
<small>Use this if the topbar is missing or you prefer a custom one.</small> <small>Use this if the topbar is missing or you prefer a custom one.</small>
</div> </div>
</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">
@@ -295,14 +299,53 @@
<span id="version-badge" class="version-badge"></span> <span id="version-badge" class="version-badge"></span>
</div>
</main> </main>
<!-- Toast Container --> <!-- Toast Container -->
<div id="toast-container"></div> <div id="toast-container"></div>
<div class="changelog-overlay" id="changelog-overlay" hidden>
<section class="changelog-modal" role="dialog" aria-modal="true" aria-labelledby="changelog-title">
<div class="changelog-header">
<div>
<span class="changelog-eyebrow">Freedom Loader</span>
<h2 id="changelog-title">What's new</h2>
</div>
<button class="changelog-close" id="changelog-close" type="button" title="Close changelog" aria-label="Close changelog">&times;</button>
</div>
<div class="changelog-list" id="changelog-list">
<p class="changelog-loading">Loading changelog...</p>
</div>
</section>
</div>
<h2 id="reference"> Because Why Not ? </h2> <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 --> <!-- Scripts -->
<script src="script/toast.js"></script> <script src="script/toast.js"></script>
@@ -314,6 +357,9 @@
<script src="script/fetchinfo.js"></script> <script src="script/fetchinfo.js"></script>
<script src="script/progressBar.js"></script> <script src="script/progressBar.js"></script>
<script src="script/customthemes.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/changelog.js"></script>
<script type="module" src="script/topbar.js"></script> <script type="module" src="script/topbar.js"></script>
</body> </body>

109
public/script/autoUpdate.js Normal file
View File

@@ -0,0 +1,109 @@
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;
const resetUI = () => {
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.onUpdateError((err) => {
console.error("Update failed asynchronously:", err);
resetUI();
});
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 {
resetUI();
}
};
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();

282
public/script/changelog.js Executable file
View File

@@ -0,0 +1,282 @@
/** DOM elements used by the changelog modal. */
const UI = {
overlay: document.getElementById("changelog-overlay"),
list: document.getElementById("changelog-list"),
closeBtn: document.getElementById("changelog-close"),
openBtn: document.getElementById("changelog-btn")
};
/** Changelog payload loaded from the Electron main process. */
let changelogData = null;
/**
* Formats an ISO release date for the current locale.
*
* @param {string} dateString - Release date returned by GitHub
* @returns {string} Localized date or an empty string
*/
function formatDate(dateString) {
if (!dateString) return "";
return new Intl.DateTimeFormat(undefined, {
year: "numeric",
month: "short",
day: "numeric",
}).format(new Date(dateString));
}
/**
* Parses inline Markdown and appends safe DOM nodes to a parent element.
*
* @param {HTMLElement} parent - Element receiving the parsed content
* @param {string} value - Inline Markdown text
*/
function parseInline(parent, value) {
const tokenPattern = /(\*\*|__)(.+?)\1|(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_)|`([^`]+)`|~~([^~]+)~~|\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
let lastIndex = 0;
let match;
while ((match = tokenPattern.exec(value)) !== null) {
if (match.index > lastIndex) {
parent.appendChild(document.createTextNode(value.slice(lastIndex, match.index)));
}
const element = match[7]
? document.createElement("a")
: document.createElement(match[1] ? "strong" : match[3] || match[4] ? "em" : match[5] ? "code" : "del");
element.textContent = match[7] || match[2] || match[3] || match[4] || match[5] || match[6];
if (match[7]) {
element.href = match[8];
element.target = "_blank";
element.rel = "noopener noreferrer";
}
parent.appendChild(element);
lastIndex = tokenPattern.lastIndex;
}
if (lastIndex < value.length) {
parent.appendChild(document.createTextNode(value.slice(lastIndex)));
}
}
/**
* Parses block-level Markdown such as headings, lists, quotes and code blocks.
*
* @param {HTMLElement} parent - Element receiving the parsed content
* @param {string} markdown - Release body in Markdown format
*/
function parseBlock(parent, markdown) {
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
let paragraph = [];
let list = null;
let codeBlock = null;
const flushParagraph = () => {
if (!paragraph.length) return;
const element = document.createElement("p");
paragraph.forEach((line, index) => {
if (index) element.appendChild(document.createElement("br"));
parseInline(element, line);
});
parent.appendChild(element);
paragraph = [];
};
const closeList = () => {
if (list) {
parent.appendChild(list);
list = null;
}
};
lines.forEach((line) => {
if (line.trim().startsWith("```")) {
flushParagraph();
closeList();
if (codeBlock) {
const code = document.createElement("code");
code.textContent = codeBlock.join("\n");
const pre = document.createElement("pre");
pre.appendChild(code);
parent.appendChild(pre);
codeBlock = null;
} else {
codeBlock = [];
}
return;
}
if (codeBlock) {
codeBlock.push(line);
return;
}
const heading = line.match(/^(#{1,6})\s+(.+)$/);
const unorderedItem = line.match(/^\s*[-*+]\s+(.+)$/);
const orderedItem = line.match(/^\s*\d+[.]\s+(.+)$/);
const quote = line.match(/^\s*>\s?(.*)$/);
if (!line.trim()) {
flushParagraph();
closeList();
} else if (heading) {
flushParagraph();
closeList();
const element = document.createElement(`h${heading[1].length}`);
parseInline(element, heading[2]);
parent.appendChild(element);
} else if (unorderedItem || orderedItem) {
flushParagraph();
const isOrdered = Boolean(orderedItem);
if (!list || list.tagName.toLowerCase() !== (isOrdered ? "ol" : "ul")) {
closeList();
list = document.createElement(isOrdered ? "ol" : "ul");
}
const item = document.createElement("li");
parseInline(item, (unorderedItem || orderedItem)[1]);
list.appendChild(item);
} else if (quote) {
flushParagraph();
closeList();
const element = document.createElement("blockquote");
parseInline(element, quote[1]);
parent.appendChild(element);
} else if (/^\s*([-*_])\s*\1\s*\1\s*$/.test(line)) {
flushParagraph();
closeList();
parent.appendChild(document.createElement("hr"));
} else {
paragraph.push(line);
}
});
flushParagraph();
closeList();
if (codeBlock) {
const code = document.createElement("code");
code.textContent = codeBlock.join("\n");
const pre = document.createElement("pre");
pre.appendChild(code);
parent.appendChild(pre);
}
}
/** Renders the empty state when no releases are available. */
function renderEmpty() {
const emptyMessage = document.createElement("p");
emptyMessage.className = "changelog-empty";
emptyMessage.textContent = "No changelog available.";
UI.list.appendChild(emptyMessage);
}
/**
* Renders one release entry in the changelog list.
*
* @param {object} release - Release data from the backend
*/
function renderEntry(release) {
const article = document.createElement("article");
article.className = "changelog-entry";
const heading = document.createElement("div");
heading.className = "changelog-entry-heading";
const title = document.createElement("h3");
title.textContent = release.name;
const metadata = document.createElement("span");
metadata.textContent = [release.version, formatDate(release.date)]
.filter(Boolean)
.join(" · ");
heading.append(title, metadata);
const body = document.createElement("div");
body.className = "changelog-body";
parseBlock(body, release.body);
article.append(heading, body);
UI.list.appendChild(article);
}
/** Renders all releases, replacing the current modal contents. */
function renderReleases(releases) {
UI.list.replaceChildren();
if (!releases.length) {
renderEmpty();
return;
}
releases.forEach((release) => renderEntry(release));
}
/** Opens the changelog modal and moves focus to its close button. */
function openChangelog(releases) {
renderReleases(releases);
UI.overlay.hidden = false;
UI.closeBtn?.focus();
}
/** Closes the modal and records the current version as seen. */
async function closeChangelog() {
UI.overlay.hidden = true;
await window.electronAPI.markChangelogSeen();
}
/** Loads changelog data and opens the modal when unread releases exist. */
async function loadChangelog() {
const features = await window.electronAPI.getFeatures();
try {
changelogData = await window.electronAPI.getChangelog();
if (!changelogData.error && changelogData.hasUnread && features.showChanglogsAfterUpdate === true) {
openChangelog(changelogData.unreadReleases);
window.electronAPI.logInfo("Showing Changelog");
}
} catch (error) {
console.error("Unable to load changelog:", error);
}
}
/** Handles a click on the button that opens the changelog. */
async function handleOpenClick() {
if (!changelogData || changelogData.error) {
changelogData = await window.electronAPI.getChangelog();
}
if (changelogData.error) {
window.showError("Unable to load changelog. Please check your network connection.");
return;
}
openChangelog(changelogData.releases);
}
/** Closes the modal when the user clicks on the overlay background. */
function handleOverlayClick(event) {
if (event.target === UI.overlay) closeChangelog();
}
/** Closes the modal when Escape is pressed. */
function handleKeydown(event) {
if (event.key === "Escape" && !UI.overlay.hidden) closeChangelog();
}
/** Registers all changelog modal event listeners. */
function bindEvents() {
UI.openBtn?.addEventListener("click", handleOpenClick);
UI.closeBtn?.addEventListener("click", closeChangelog);
UI.overlay?.addEventListener("click", handleOverlayClick);
document.addEventListener("keydown", handleKeydown);
}
/** Initializes the changelog data flow and modal interactions. */
function init() {
bindEvents();
loadChangelog();
}
init();

View File

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

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

@@ -0,0 +1,165 @@
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 id="bugReportServiceStatus" role="status" aria-live="polite" style="display:none !important; align-items:center !important; gap:8px !important; color:#aaaaaa !important; font-size:0.8rem !important;">
<span id="bugReportServiceStatusDot" style="width:8px !important; height:8px !important; border-radius:50% !important; background:#f59e0b !important; flex-shrink:0 !important;"></span>
<span id="bugReportServiceStatusText">Checking bug report service...</span>
</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" id="bugSendBtn" disabled style="background:#4f46e5 !important; border:none !important; color:#ffffff !important; padding:8px 16px !important; border-radius:6px !important; cursor:not-allowed !important; opacity:0.5 !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");
const sendButton = this.modal.querySelector("#bugSendBtn");
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();
if (sendButton.disabled) {
return;
}
const data = {
title: titleInput.value,
description: descInput.value,
includeLogs: this.modal.querySelector("#bugIncludeLogs").value
};
window.electronAPI.logInfo("Submitting:", data);
try {
const success = await window.topbarAPI.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";
this.updateServiceStatus();
}
close() {
this.modal.style.display = "none";
}
async updateServiceStatus() {
const statusText = this.modal.querySelector("#bugReportServiceStatusText");
const statusDot = this.modal.querySelector("#bugReportServiceStatusDot");
const sendButton = this.modal.querySelector("#bugSendBtn");
sendButton.disabled = true;
sendButton.style.setProperty("cursor", "not-allowed", "important");
sendButton.style.setProperty("opacity", "0.5", "important");
try {
const serviceStatus = await window.topbarAPI.getReportServiceStatus();
const serviceAvailable = serviceStatus.status === "online";
const statusVisible = !serviceAvailable;
statusText.textContent = serviceStatus.message;
statusDot.style.background = "#ef4444";
statusText.style.color = "#fca5a5";
this.modal.querySelector("#bugReportServiceStatus").style.setProperty(
"display",
statusVisible ? "flex" : "none",
"important"
);
sendButton.disabled = !serviceAvailable;
sendButton.style.setProperty("cursor", serviceAvailable ? "pointer" : "not-allowed", "important");
sendButton.style.setProperty("opacity", serviceAvailable ? "1" : "0.5", "important");
} catch (err) {
statusText.textContent = "Unable to check bug report service.";
statusDot.style.background = "#ef4444";
statusText.style.color = "#fca5a5";
this.modal.querySelector("#bugReportServiceStatus").style.setProperty("display", "flex", "important");
sendButton.disabled = true;
window.electronAPI.logError("Failed to retrieve bug report service status:", err.message);
}
}
}
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. * Toggles visibility of the settings panel when clicking the settings button.
*/ */
settingsButton.addEventListener("click", () => { settingsButton.addEventListener("click", () => {
const currentlyOpen = settingsPanel.style.display === "block"; settingsPanel.classList.toggle("open");
settingsPanel.style.display = currentlyOpen ? "none" : "block";
}); });
/** /**
@@ -25,7 +24,7 @@ settingsButton.addEventListener("click", () => {
*/ */
function closePanel() { function closePanel() {
isOpen = false; 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); toast.classList.add("toast", type);
const icons = { const icons = {
success: "✓", success: "check_circle",
error: "✕", error: "error",
warning: "⚠", warning: "warning",
info: "ℹ" info: "info"
}; };
const icon = icons[type] || icons.info; const icon = icons[type] || icons.info;
toast.style.setProperty("--toast-duration", `${duration}ms`);
toast.innerHTML = ` toast.innerHTML = `
<span class="toast-icon">${icon}</span> <span class="toast-icon material-icons-round">${icon}</span>
<span class="toast-message">${message}</span> <span class="toast-message">${message}</span>
<button class="toast-close" title="Close">×</button> <button class="toast-close" title="Close">×</button>
<div class="toast-progress"></div>
`; `;
const closeBtn = toast.querySelector(".toast-close"); const closeBtn = toast.querySelector(".toast-close");

View File

@@ -35,25 +35,26 @@ export function initTopBar() {
} }
async function attachListeners(){ //* Disabled because i disabled native topbar for now, but keeping the code in case i want to re-enable it later
try{ // async function attachListeners(){
const featuresList = await window.electronAPI.getFeatures(); // try{
// const featuresList = await window.electronAPI.getFeatures();
if (!featuresList.customTopBar) { // if (!featuresList.customTopBar) {
const topbar = document.getElementById("topbar"); // const topbar = document.getElementById("topbar");
const container = document.getElementById("container"); // const container = document.getElementById("container");
const themeSwitcher = document.getElementById("theme-switcher"); // const themeSwitcher = document.getElementById("theme-switcher");
if (topbar) topbar.style.display = "none"; // if (topbar) topbar.style.display = "none";
if (container) container.style.marginTop = "0"; // if (container) container.style.marginTop = "0";
if (themeSwitcher) themeSwitcher.style.top = "30px"; // if (themeSwitcher) themeSwitcher.style.top = "30px";
} // }
} catch (error) { // } catch (error) {
console.error("Failed to load layout features for topbar:", error); // console.error("Failed to load layout features for topbar:", error);
} // }
} // }
initTopBar() initTopBar()
document.addEventListener("DOMContentLoaded", attachListeners) //document.addEventListener("DOMContentLoaded", attachListeners)

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

@@ -0,0 +1,229 @@
.changelog-overlay {
position: fixed;
inset: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(5, 10, 16, 0.72);
backdrop-filter: blur(8px);
}
.changelog-overlay[hidden] {
display: none;
}
.changelog-modal {
display: flex;
flex-direction: column;
width: min(680px, 100%);
max-height: min(720px, 88vh);
overflow: hidden;
color: var(--settings-text-color, #f8fafc);
background: var(--settings-modal-bg-color, #18212b);
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 14px;
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
}
.changelog-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
padding: 24px 28px 18px;
border-bottom: 1px solid rgba(148, 163, 184, 0.18);
}
.changelog-eyebrow {
color: var(--fill-accent, #0ea5e9);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.changelog-header h2 {
margin: 5px 0 0;
font-size: 26px;
}
.changelog-close {
width: 34px;
height: 34px;
padding: 0;
color: inherit;
font-size: 24px;
line-height: 1;
background: transparent;
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 8px;
cursor: pointer;
}
.changelog-close:hover {
background: rgba(148, 163, 184, 0.14);
}
.changelog-list {
overflow-y: auto;
padding: 20px 28px 28px;
scrollbar-color: var(--fill-accent, #0ea5e9) transparent;
scrollbar-width: thin;
}
.changelog-list::-webkit-scrollbar,
.changelog-body pre::-webkit-scrollbar {
width: 7px;
height: 7px;
}
.changelog-list::-webkit-scrollbar-track,
.changelog-body pre::-webkit-scrollbar-track {
background: transparent;
}
.changelog-list::-webkit-scrollbar-thumb,
.changelog-body pre::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--fill-accent, #0ea5e9) 70%, transparent);
border: 2px solid transparent;
border-radius: 999px;
background-clip: padding-box;
}
.changelog-list::-webkit-scrollbar-thumb:hover,
.changelog-body pre::-webkit-scrollbar-thumb:hover {
background: var(--fill-accent, #0ea5e9);
background-clip: padding-box;
}
.changelog-entry {
padding: 18px 0;
border-bottom: 1px solid rgba(148, 163, 184, 0.16);
}
.changelog-entry:first-child {
padding-top: 0;
}
.changelog-entry:last-child {
border-bottom: 0;
}
.changelog-entry-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
}
.changelog-entry-heading h3 {
margin: 0;
font-size: 17px;
}
.changelog-entry-heading span {
flex-shrink: 0;
color: var(--settings-subtitle-color, #94a3b8);
font-size: 12px;
}
.changelog-body {
margin: 12px 0 0;
color: var(--settings-subtitle-color, #cbd5e1);
font-size: 13px;
line-height: 1.55;
overflow-wrap: anywhere;
}
.changelog-body p {
margin: 0 0 10px;
}
.changelog-body p:last-child {
margin-bottom: 0;
}
.changelog-body h1,
.changelog-body h2,
.changelog-body h3,
.changelog-body h4,
.changelog-body h5,
.changelog-body h6 {
margin: 14px 0 8px;
color: var(--settings-text-color, #f8fafc);
}
.changelog-body h1 { font-size: 20px; }
.changelog-body h2 { font-size: 18px; }
.changelog-body h3 { font-size: 16px; }
.changelog-body h4,
.changelog-body h5,
.changelog-body h6 { font-size: 14px; }
.changelog-body ul,
.changelog-body ol {
margin: 8px 0 12px;
padding-left: 22px;
}
.changelog-body li + li {
margin-top: 4px;
}
.changelog-body code {
padding: 2px 5px;
color: var(--settings-text-color, #f8fafc);
background: rgba(148, 163, 184, 0.16);
border-radius: 4px;
font-size: 12px;
}
.changelog-body pre {
padding: 10px 12px;
overflow-x: auto;
background: rgba(15, 23, 42, 0.45);
border-radius: 6px;
}
.changelog-body pre code {
padding: 0;
background: transparent;
white-space: pre;
}
.changelog-body blockquote {
margin: 10px 0;
padding-left: 12px;
color: var(--settings-subtitle-color, #cbd5e1);
border-left: 3px solid var(--fill-accent, #0ea5e9);
}
.changelog-body a {
color: var(--fill-accent, #0ea5e9);
}
.changelog-loading,
.changelog-empty {
margin: 0;
color: var(--settings-subtitle-color, #cbd5e1);
}
@media (max-width: 780px) {
.changelog-overlay {
padding: 12px;
}
.changelog-header,
.changelog-list {
padding-left: 18px;
padding-right: 18px;
}
.changelog-entry-heading {
align-items: flex-start;
flex-direction: column;
gap: 5px;
}
}

View File

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

View File

@@ -45,11 +45,22 @@
border-radius: 12px; border-radius: 12px;
padding: 20px; padding: 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4); box-shadow: 0 8px 24px rgba(0,0,0,0.4);
display: none;
overflow-y: auto; overflow-y: auto;
z-index: 9999; z-index: 9999;
animation: slideIn 0.2s ease-out; /*animation: slideIn 0.2s ease-out; */
color: var(--settings-text-color); 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 { .settings-panel::-webkit-scrollbar {

View File

@@ -14,8 +14,8 @@
.custom-controls { .custom-controls {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 4px;
padding-left: 12px; padding-left: 10px;
} }
/* RIGHT - window buttons */ /* RIGHT - window buttons */
@@ -43,11 +43,11 @@
/* Custom buttons (Logs / Wiki / etc.) */ /* Custom buttons (Logs / Wiki / etc.) */
.custom-controls button { .custom-controls button {
height: 32px; height: 28px;
padding: 0 14px; padding: 0 10px;
font-size: 15px; font-size: 14px;
border-radius: 8px; border-radius: 7px;
} }
.custom-controls button:hover { .custom-controls button:hover {

View File

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

View File

@@ -0,0 +1,24 @@
### Features
- **In-App Feedback**: Added a built-in feedback form allowing users to send feedback, bug reports, and daily logs directly to the development team.
- **Community Contributions**: GitHub issues are now better maintained on the repository. If you want to contribute to the project, feel free to drop by and check out the `good first issue` tags!
- **Changelog**: The changelog is now automatically displayed after an update (can be disabled if preferred).
### Improvements
- **Bundle Size**: Optimized assets and dependencies, reducing the overall application bundle size by approximately 30% for specific versions.
- **Updater UI**: Reworked the update notification for better clarity. Windows and AppImage users can download updates seamlessly in-app, while unsupported Linux package users are gracefully redirected to the manual download page. Flatpak and Snap users are not prompted, as updates are handled natively by their respective package managers.
- **Toast Notifications**: Refactored the style and overall UX of toast alerts.
- **Code Quality**: Implemented a linter to enforce coding standards.
### Fixes
- **Auto-Updater**: Fixed a major issue causing `404 Not Found` errors when attempting to download updates for `.AppImage` releases.
- **Linux Updates**: Fixed update crashes on `.rpm` and `.snap` packages. In-app update flows are now disabled for these formats to let the OS handle the process natively.
- **Windows Logs**: Fixed a file naming issue with the log transport system on Windows.
- **Desktop Entry Name**: Fixed the desktop entry name for Linux users, ensuring it displays correctly in application menus.
- **Custom Top Bar**: Removed the `customTopBar` settings.
- **Auto Update**: Removed the `autoAppUpdate` settings.
- **UI (System Tray)**: Removed the ***[EXPERIMENTAL]*** label from the config panel.
### Chores
- **Dependencies**: Removed `dotenv` from the project.
- **Website**: A brand new website URL is now available: [freedomloader.acnolo.fr](https://freedomloader.acnolo.fr)
- **Dev**: Added a new npm script to check Linux file quality.

View File

@@ -54,20 +54,6 @@ exec /opt/freedom-loader/freedom-loader "$@"
EOF EOF
chmod +x "$TMP_WRAPPER/freedom-loader" chmod +x "$TMP_WRAPPER/freedom-loader"
# Desktop entry file
TMP_DESKTOP="$ROOT_DIR/.rpm-pkg-tmp-desktop"
mkdir -p "$TMP_DESKTOP"
cat > "$TMP_DESKTOP/freedom-loader.desktop" << 'EOF'
[Desktop Entry]
Name=Freedom Loader
Exec=/opt/freedom-loader/freedom-loader %U
Icon=freedom-loader
Type=Application
Categories=AudioVideo;Utility;Network;
Comment=Free and open-source GUI for yt-dlp
StartupWMClass=Freedom Loader
EOF
# ------------------------------------------------------------ # ------------------------------------------------------------
# Step 1 - Build .rpm from linux-unpacked via fpm # Step 1 - Build .rpm from linux-unpacked via fpm
# ------------------------------------------------------------ # ------------------------------------------------------------
@@ -78,7 +64,7 @@ echo "Building RPM..."
--version "$VERSION" \ --version "$VERSION" \
--architecture x86_64 \ --architecture x86_64 \
--description "Free and open-source GUI for yt-dlp" \ --description "Free and open-source GUI for yt-dlp" \
--url "https://masteracnolo.github.io/Freedom-Loader-Site/" \ --url "https://freedomloader.acnolo.fr/" \
--maintainer "MasterAcnolo <MasterAcnolo@users.noreply.github.com>" \ --maintainer "MasterAcnolo <MasterAcnolo@users.noreply.github.com>" \
--license "GPL-3.0-only" \ --license "GPL-3.0-only" \
--depends gtk3 \ --depends gtk3 \
@@ -89,11 +75,10 @@ echo "Building RPM..."
--package "$RPM_OUT" \ --package "$RPM_OUT" \
"$ROOT_DIR/dist/linux-unpacked/=/opt/freedom-loader" \ "$ROOT_DIR/dist/linux-unpacked/=/opt/freedom-loader" \
"$TMP_WRAPPER/freedom-loader=/usr/bin/freedom-loader" \ "$TMP_WRAPPER/freedom-loader=/usr/bin/freedom-loader" \
"$TMP_DESKTOP/freedom-loader.desktop=/usr/share/applications/freedom-loader.desktop" \
"$ROOT_DIR/build/app-icon.png=/usr/share/icons/hicolor/512x512/apps/freedom-loader.png" \ "$ROOT_DIR/build/app-icon.png=/usr/share/icons/hicolor/512x512/apps/freedom-loader.png" \
"$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml" "$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml" \
"$ROOT_DIR/package/com.masteracnolo.freedomloader.desktop=/usr/share/applications/com.masteracnolo.freedomloader.desktop"
rm -rf "$TMP_WRAPPER" "$TMP_DESKTOP" rm -rf "$TMP_WRAPPER"
echo "RPM built: $RPM_OUT" echo "RPM built: $RPM_OUT"
# ------------------------------------------------------------ # ------------------------------------------------------------

View File

@@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
VERSION=$(node -p "require('$ROOT_DIR/package.json').version") 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 "Error: The current version ($VERSION) is a 'preview"
echo "Unable to launch release pipeline. Please update the package.json with a stable version.." echo "Unable to launch release pipeline. Please update the package.json with a stable version.."
exit 1 exit 1
@@ -64,6 +64,16 @@ echo "[0/5] Cleaning workspace..."
rm -rf "$ROOT_DIR/dist" "$ROOT_DIR/srpm-out" rm -rf "$ROOT_DIR/dist" "$ROOT_DIR/srpm-out"
echo "Workspace cleaned (dist/ and srpm-out/ removed)" echo "Workspace cleaned (dist/ and srpm-out/ removed)"
# ------------------------------------------------------------
# Step 0.3 - Retrieve Changelog
# ------------------------------------------------------------
if [ ! -f "$ROOT_DIR/scripts/RELEASE-CHANGELOG.md" ]; then
echo "Warning: RELEASE-CHANGELOG.md not found, release notes will be empty."
CHANGELOG=""
else
CHANGELOG="$(cat "$ROOT_DIR/scripts/RELEASE-CHANGELOG.md")"
echo "Changelog retrieved successfully"
fi
# ------------------------------------------------------------ # ------------------------------------------------------------
# Step 1 - Build Linux packages (AppImage, deb, snap) # Step 1 - Build Linux packages (AppImage, deb, snap)
# ------------------------------------------------------------ # ------------------------------------------------------------
@@ -105,6 +115,10 @@ PREV_TAG=$(git rev-list --tags --skip=1 --max-count=1 | xargs git describe --tag
RELEASE_NOTES="# Freedom Loader - $VERSION RELEASE_NOTES="# Freedom Loader - $VERSION
## Changelog
$CHANGELOG
## Found a bug or issue? ## Found a bug or issue?
Please report it in the [GitHub Issues](https://github.com/MasterAcnolo/Freedom-Loader/issues) section. Please report it in the [GitHub Issues](https://github.com/MasterAcnolo/Freedom-Loader/issues) section.
@@ -118,8 +132,8 @@ if [ "$DRY_RUN" = false ]; then
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe" \ "$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe" \
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe.blockmap" \ "$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe.blockmap" \
"$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm" \ "$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm" \
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.deb" \ "$ROOT_DIR/dist/Freedom-Loader_${VERSION}_amd64.deb" \
"$ROOT_DIR/dist/Freedom Loader-${VERSION}.AppImage" \ "$ROOT_DIR/dist/Freedom-Loader-${VERSION}.AppImage" \
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \ "$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \
"$ROOT_DIR/dist/latest.yml" \ "$ROOT_DIR/dist/latest.yml" \
"$ROOT_DIR/dist/latest-linux.yml" "$ROOT_DIR/dist/latest-linux.yml"

View File

@@ -67,9 +67,7 @@ function buildYtDlpArgs({url, audioOnly, quality, outputFolder, isPlaylist}) {
logger.info("--- CONFIGURATION ---"); logger.info("--- CONFIGURATION ---");
logger.info(`CONFIG autoUpdate: ${configFeatures.autoUpdate}`);
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`); logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`); logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
logger.info(`CONFIG keepPlaylistOrder: ${configFeatures.keepPlaylistOrder}`); logger.info(`CONFIG keepPlaylistOrder: ${configFeatures.keepPlaylistOrder}`);
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`); logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);

View File

@@ -11,7 +11,6 @@ jest.mock("../../server/helpers/path.helpers.js", () => ({
jest.mock("../../config.js", () => ({ jest.mock("../../config.js", () => ({
configFeatures: { configFeatures: {
autoUpdate: false,
discordRPC: false, discordRPC: false,
customTopBar: false, customTopBar: false,
autoCheckInfo: false, autoCheckInfo: false,