14 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
21 changed files with 830 additions and 88 deletions

View File

@@ -56,7 +56,7 @@ The primary goal is to make media downloading accessible to users who want offli
### 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
- **Theme Workshop** - web-based theme creator and browser
- **Toast notifications** - in-app feedback system
@@ -151,6 +151,7 @@ freedom-loader
|---------------|---------------------------------------------------------|--------------------------------------|
| **Downloads** | `C:\Users\[USERNAME]\Downloads\Freedom Loader` | `~/Downloads/Freedom Loader` |
| **Logs** | `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\` | `~/.local/share/FreedomLoader/logs/` |
| **Cache** | ``
> [!NOTE]
> 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
```json
{
"autoUpdate": true,
"systemTray": true,
"discordRPC": true,
"customTopBar": true,
"autoCheckInfo": true,
"addThumbnail": true,
"addMetadata": true,
@@ -241,6 +240,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
"downloadSystem": true,
"notifySystem": true,
"enableHardwareAcceleration": true,
"showChanglogsAfterUpdate": true,
"theme": "dark"
}
```
@@ -249,10 +249,8 @@ Freedom Loader can be configured either through the settings panel in the UI or
| Option | Type | Default | Description |
|------------------------------|---------|----------|------------------------------------------------------------|
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
| `systemTray` | boolean | `false` | Allow app to minimize on close |
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
| `customTopBar` | boolean | `true` | Use custom application top bar |
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
| `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files |
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
@@ -266,6 +264,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
| `downloadSystem` | boolean | `true` | Enable download system |
| `notifySystem` | boolean | `true` | Enable system notifications |
| `enableHardwareAcceleration` | boolean | `true` | Enable Hardware Acceleration |
| `showChanglogsAfterUpdate` | boolean | `true` | Auto-display changelog popup after and update |
| `theme` | string | `dark` | Current Theme Name |
### Supported Codecs

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

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

@@ -21,6 +21,7 @@ const { userThemesPath } = require("../server/helpers/path.helpers");
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.
@@ -29,10 +30,8 @@ const { isUpdateAvailable, downloadUpdate, installUpdate } = require("./autoUpda
* Acts as a basic validation layer for IPC "set-feature".
*/
const FEATURE_WHITELIST = new Set([
"autoUpdate",
"systemTray",
"discordRPC",
"customTopBar",
"autoCheckInfo",
"addThumbnail",
"addMetadata",
@@ -43,7 +42,8 @@ const FEATURE_WHITELIST = new Set([
"theme",
"createPlaylistFolders",
"notifySystem",
"enableHardwareAcceleration"
"enableHardwareAcceleration",
"showChanglogsAfterUpdate"
]);
/**
@@ -71,6 +71,16 @@ function registerIpcHandlers(getMainWindow) {
*/
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.
*/

View File

@@ -57,7 +57,7 @@ async function createMainWindow() {
height: 800,
minWidth: 750,
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,
show: false,
webPreferences: {

View File

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

View File

@@ -173,7 +173,7 @@ app.whenReady().then(async () => {
if (configFeatures.discordRPC) startRPC();
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
initAutoUpdater(getMainWindow());
} catch (err) {
logger.error("Boot error:", err);

View File

@@ -16,6 +16,7 @@
},
"main": "main.js",
"scripts": {
"clean:dist": "rm -rf dist/ && rm -rf srpm-out/",
"start": "electron --no-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 .",
@@ -25,13 +26,15 @@
"build": "electron-builder",
"build:win": "electron-builder --win",
"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",
"release": "bash scripts/release.sh",
"release:publish": "bash scripts/release.sh --publish",
"release:dry-run": "bash scripts/release.sh --dry-run",
"test": "jest",
"test:unit": "jest test/unit",
"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"
@@ -64,6 +67,7 @@
},
"build": {
"appId": "com.masteracnolo.freedomloader",
"artifactName": "Freedom-Loader-${version}.${ext}",
"productName": "Freedom Loader",
"publish": [
{

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

@@ -69,6 +69,16 @@ contextBridge.exposeInMainWorld("electronAPI", {
*/
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.
*

View File

@@ -27,6 +27,7 @@
<button id="website-btn" title="Website">Website</button>
<button id="wiki-btn" title="Wiki">Wiki</button>
<button id="workshop-btn" title="Workshop">Theme Workshop</button>
<button id="changelog-btn" title="Changelog">Changelog</button>
<!-- <button id="theme-btn" title="Theme">Theme</button> -->
</div>
<div class="window-controls">
@@ -123,38 +124,38 @@
<summary>Advanced Settings</summary>
<div class="setting-item">
<input type="checkbox" data-key="autoUpdate" id="autoUpdate">
<input type="checkbox" data-key="showChanglogsAfterUpdate" id="showChanglogsAfterUpdate">
<div class="setting-info">
<span class="setting-title">Auto Update</span>
<small>Enable automatic app updates.</small>
<span class="setting-title">Show Changelogs After Update</span>
<small>Display changelogs after each update.</small>
</div>
</div>
<div class="setting-item">
<input data-key="systemTray" id="systemTray" type="checkbox">
<div class="setting-info">
<span class="setting-title">System Tray</span>
<small>Minimize app on close, in the System Tray. [EXPERIMENTAL]</small>
</div>
<div class="setting-item">
<input data-key="systemTray" id="systemTray" type="checkbox">
<div class="setting-info">
<span class="setting-title">System Tray</span>
<small>Minimize app on close, in the System Tray.</small>
</div>
</div>
<!-- <div class="setting-item">
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
<div class="setting-info">
<span class="setting-title">Output title (special chars)</span>
<small>Check for unusual characters in output titles.</small>
</div>
</div> -->
<div class="setting-item">
<input data-key="autoCheckInfo" id="autoCheckInfo" type="checkbox">
<div class="setting-info">
<span class="setting-title">Auto Fetch Info</span>
<small>Automatically fetch information for new downloads.</small>
</div>
<!-- <div class="setting-item">
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
<div class="setting-info">
<span class="setting-title">Output title (special chars)</span>
<small>Check for unusual characters in output titles.</small>
</div>
</div> -->
<div class="setting-item">
<div class="setting-item">
<input data-key="autoCheckInfo" id="autoCheckInfo" type="checkbox">
<div class="setting-info">
<span class="setting-title">Auto Fetch Info</span>
<small>Automatically fetch information for new downloads.</small>
</div>
</div>
<div class="setting-item">
<input type="checkbox" data-key="addMetadata" id="addMetadata">
<div class="setting-info">
<span class="setting-title">Add Metadata</span>
@@ -170,13 +171,13 @@
</div>
</div>
<div class="setting-item">
<!-- <div class="setting-item">
<input type="checkbox" data-key="customTopBar" id="customTopBar">
<div class="setting-info">
<span class="setting-title">Custom Top bar</span>
<small>Use this if the topbar is missing or you prefer a custom one.</small>
</div>
</div>
</div> -->
<div class="setting-item" style="margin-left: 32px; margin-top: 20px;">
<div class="setting-info">
@@ -303,6 +304,21 @@
<!-- Toast Container -->
<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>
<div id="updateAvailable" style="display:none">
@@ -343,6 +359,7 @@
<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>
</body>

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

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

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

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

View File

@@ -13,6 +13,7 @@
@import url("components/progressBar.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');

View File

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

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
chmod +x "$TMP_WRAPPER/freedom-loader"
# Desktop entry file
TMP_DESKTOP="$ROOT_DIR/.rpm-pkg-tmp-desktop"
mkdir -p "$TMP_DESKTOP"
cat > "$TMP_DESKTOP/com.masteracnolo.freedomloader.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=com.masteracnolo.freedomloader
EOF
# ------------------------------------------------------------
# Step 1 - Build .rpm from linux-unpacked via fpm
# ------------------------------------------------------------
@@ -89,11 +75,10 @@ echo "Building RPM..."
--package "$RPM_OUT" \
"$ROOT_DIR/dist/linux-unpacked/=/opt/freedom-loader" \
"$TMP_WRAPPER/freedom-loader=/usr/bin/freedom-loader" \
"$TMP_DESKTOP/com.masteracnolo.freedomloader.desktop=/usr/share/applications/freedom-loader.desktop" \
"$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"
rm -rf "$TMP_WRAPPER" "$TMP_DESKTOP"
"$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"
echo "RPM built: $RPM_OUT"
# ------------------------------------------------------------

View File

@@ -67,11 +67,11 @@ echo "Workspace cleaned (dist/ and srpm-out/ removed)"
# ------------------------------------------------------------
# Step 0.3 - Retrieve Changelog
# ------------------------------------------------------------
if [ ! -f "$ROOT_DIR/CHANGELOG.md" ]; then
echo "Warning: CHANGELOG.md not found, release notes will be empty."
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/CHANGELOG.md")"
CHANGELOG="$(cat "$ROOT_DIR/scripts/RELEASE-CHANGELOG.md")"
echo "Changelog retrieved successfully"
fi
# ------------------------------------------------------------
@@ -132,7 +132,7 @@ if [ "$DRY_RUN" = false ]; then
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe" \
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe.blockmap" \
"$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}_amd64.snap" \
"$ROOT_DIR/dist/latest.yml" \

View File

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

View File

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