mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-09-27 20:11:15 +02:00
Compare commits
7 Commits
2c0f5b315b
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d09cf1b5d | ||
|
|
c80d65483a | ||
|
|
d11c0f9839 | ||
|
|
1e0b2affee | ||
|
|
9975b2b583 | ||
|
|
b319a1e445 | ||
|
|
4a64a40226 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -13,8 +13,14 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -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,15 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
@@ -120,19 +122,7 @@ function initAutoUpdater(mainWindow) {
|
|||||||
* Separated from init for reusability and testability.
|
* Separated from init for reusability and testability.
|
||||||
*/
|
*/
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
|
if (!require("electron").app.isPackaged) return;
|
||||||
/**
|
|
||||||
* Prevents electron-updater from running inside sandboxed environments.
|
|
||||||
* Snap and Flatpak lock the file system in read-only mode, causing the updater to crash.
|
|
||||||
* In these environments, updates are safely handled by snapd or the Flatpak runtime.
|
|
||||||
* Standalone Linux packages (.deb, .rpm, AppImage) will bypass this and update normally.
|
|
||||||
*/
|
|
||||||
if (process.env.SNAP || process.env.FLATPAK_ID) {
|
|
||||||
logger.info("Running as Snap or Flatpak. Updates are managed by the OS store. Skipping electron-updater.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!require("electron").app.isPackaged) return ;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await autoUpdater.checkForUpdates();
|
await autoUpdater.checkForUpdates();
|
||||||
@@ -150,6 +140,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 +151,13 @@ function installUpdate() {
|
|||||||
autoUpdater.quitAndInstall();
|
autoUpdater.quitAndInstall();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { initAutoUpdater, downloadUpdate, installUpdate };
|
function isUpdateAvailable() {
|
||||||
|
return updateAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initAutoUpdater,
|
||||||
|
downloadUpdate,
|
||||||
|
installUpdate,
|
||||||
|
isUpdateAvailable,
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,13 +13,14 @@
|
|||||||
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} = require("./sendReport");
|
const { sendReport } = require("./sendReport");
|
||||||
|
const { isUpdateAvailable, downloadUpdate, installUpdate } = require("./autoUpdater");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Security whitelist for feature flags that can be modified at runtime.
|
* Security whitelist for feature flags that can be modified at runtime.
|
||||||
@@ -65,7 +66,6 @@ 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.
|
||||||
*/
|
*/
|
||||||
@@ -116,17 +116,16 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
* Window minimize request from renderer.
|
* Window minimize request from renderer.
|
||||||
*/
|
*/
|
||||||
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.
|
||||||
@@ -142,7 +141,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.
|
||||||
*/
|
*/
|
||||||
@@ -153,7 +151,7 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
/**
|
/**
|
||||||
* Opens application logs directory in system file explorer.
|
* Opens application logs directory in system file explorer.
|
||||||
*/
|
*/
|
||||||
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
|
ipcMain.on("open-logs", () => logDir && shell.openPath(logDir));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens external website in default browser.
|
* Opens external website in default browser.
|
||||||
@@ -212,6 +210,24 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
return await sendReport(params);
|
return await sendReport(params);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("is-update-available", () => {
|
||||||
|
return isUpdateAvailable();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("download-update", async () => {
|
||||||
|
return await downloadUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("install-update", async () => {
|
||||||
|
return installUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on("open-release-page", () => {
|
||||||
|
shell.openExternal(
|
||||||
|
"https://github.com/MasterAcnolo/Freedom-Loader/releases/latest",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a runtime feature flag and persists it to disk.
|
* Updates a runtime feature flag and persists it to disk.
|
||||||
*
|
*
|
||||||
@@ -229,47 +245,46 @@ function registerIpcHandlers(getMainWindow) {
|
|||||||
* @returns {boolean} success state
|
* @returns {boolean} success state
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("set-feature", (event, { key, value }) => {
|
ipcMain.handle("set-feature", (event, { key, value }) => {
|
||||||
try {
|
try {
|
||||||
if (!FEATURE_WHITELIST.has(key)) {
|
if (!FEATURE_WHITELIST.has(key)) {
|
||||||
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
logger.warn(`Rejected feature (not whitelisted): ${key}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configFeatures[key] === value) {
|
if (configFeatures[key] === value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically intercepts and applies System Tray state changes at runtime.
|
* Dynamically intercepts and applies System Tray state changes at runtime.
|
||||||
* Instantiates or destroys the tray icon immediately to prevent window
|
* Instantiates or destroys the tray icon immediately to prevent window
|
||||||
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
|
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
|
||||||
*/
|
*/
|
||||||
if (key === "systemTray") {
|
if (key === "systemTray") {
|
||||||
if (value === true) {
|
if (value === true) {
|
||||||
logger.info("System Tray enabled dynamically.");
|
logger.info("System Tray enabled dynamically.");
|
||||||
createSystemTray(devMode);
|
createSystemTray(devMode);
|
||||||
} else {
|
} else {
|
||||||
logger.info("System Tray disabled dynamically.");
|
logger.info("System Tray disabled dynamically.");
|
||||||
destroyTray();
|
destroyTray();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configFeatures[key] = value;
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
configFolderPath,
|
|
||||||
JSON.stringify(configFeatures, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(`Feature updated: ${key} = ${value}`);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`set-feature failed (${key}): ${err.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
configFeatures[key] = value;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
configFolderPath,
|
||||||
|
JSON.stringify(configFeatures, null, 2),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Feature updated: ${key} = ${value}`);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`set-feature failed (${key}): ${err.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { registerIpcHandlers };
|
module.exports = { registerIpcHandlers };
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "freedom-loader",
|
"name": "freedom-loader",
|
||||||
"desktopName": "com.masteracnolo.freedomloader",
|
"desktopName": "com.masteracnolo.freedomloader",
|
||||||
"productName": "Freedom Loader",
|
"productName": "Freedom Loader",
|
||||||
"version": "1.6.4-preview",
|
"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://masteracnolo.github.io/Freedom-Loader-Site/",
|
||||||
|
|||||||
78
preload.js
78
preload.js
@@ -8,7 +8,6 @@ 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.
|
* Sends error log message.
|
||||||
*
|
*
|
||||||
@@ -30,14 +29,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
*/
|
*/
|
||||||
logWarn: (...args) => ipcRenderer.send("log-warn", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
|
logWarn: (...args) => ipcRenderer.send("log-warn", args.map(arg => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ")),
|
||||||
|
|
||||||
/**
|
|
||||||
* Send Bug Report
|
|
||||||
*
|
|
||||||
* @param params
|
|
||||||
* @returns {Promise<any>}
|
|
||||||
*/
|
|
||||||
sendReport: (params) => ipcRenderer.invoke('send-report', params),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return process.platform to renderer
|
* Return process.platform to renderer
|
||||||
*/
|
*/
|
||||||
@@ -71,12 +62,11 @@ 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"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,8 +86,61 @@ 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 download progress event.
|
||||||
|
*
|
||||||
|
* @param {Function} callback - Function executed with progress metrics (percent, bytesPerSecond, transferred, total).
|
||||||
|
*/
|
||||||
|
onDownloadProgress: (callback) =>
|
||||||
|
ipcRenderer.on("update-progress", (_, progress) => callback(progress)),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to the update downloaded event.
|
||||||
|
* Triggered when the update file is fully downloaded and ready to install.
|
||||||
|
*
|
||||||
|
* @param {Function} callback - Function executed with update info.
|
||||||
|
*/
|
||||||
|
onUpdateDownloaded: (callback) =>
|
||||||
|
ipcRenderer.on("update-downloaded", (_, info) => callback(info)),
|
||||||
|
|
||||||
|
openReleasePage: () => ipcRenderer.send("open-release-page"),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exposes window control and developer utilities for the custom topbar UI.
|
* Exposes window control and developer utilities for the custom topbar UI.
|
||||||
@@ -105,7 +148,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.
|
||||||
*/
|
*/
|
||||||
@@ -155,4 +197,12 @@ 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),
|
||||||
});
|
});
|
||||||
@@ -12,7 +12,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">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -304,6 +305,31 @@
|
|||||||
|
|
||||||
<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>
|
||||||
@@ -316,6 +342,7 @@
|
|||||||
<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/reportBug.js"></script>
|
||||||
|
<script src="script/autoUpdate.js"></script>
|
||||||
<script type="module" src="script/topbar.js"></script>
|
<script type="module" src="script/topbar.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
99
public/script/autoUpdate.js
Normal file
99
public/script/autoUpdate.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
async function initUpdateUI() {
|
||||||
|
const container = document.getElementById("updateAvailable");
|
||||||
|
|
||||||
|
const hasUpdate = await window.electronAPI.isUpdateAvailable();
|
||||||
|
if (hasUpdate) {
|
||||||
|
setupAndShowUpdateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.electronAPI.onUpdateAvailable((info) => {
|
||||||
|
setupAndShowUpdateUI(info);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupAndShowUpdateUI(info) {
|
||||||
|
container.style.display = "block";
|
||||||
|
|
||||||
|
const btn = document.getElementById("btn-update");
|
||||||
|
const icon = document.getElementById("update-icon");
|
||||||
|
const title = document.getElementById("update-title");
|
||||||
|
const sub = document.getElementById("update-sub");
|
||||||
|
const progressWrap = document.getElementById("progress-wrap");
|
||||||
|
const progressFill = document.getElementById("progress-fill");
|
||||||
|
const progressPct = document.getElementById("progress-pct");
|
||||||
|
const progressEta = document.getElementById("progress-eta");
|
||||||
|
|
||||||
|
if (info && info.version) {
|
||||||
|
sub.textContent = `A new version (${info.version}) is ready`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info && info.environment && info.environment !== "auto") {
|
||||||
|
icon.textContent = "open_in_new";
|
||||||
|
|
||||||
|
if (info.environment === "snap") {
|
||||||
|
sub.textContent = `Version ${info.version} is out. Use the Snap Store or run "snap refresh".`;
|
||||||
|
btn.textContent = "View Release";
|
||||||
|
} else {
|
||||||
|
sub.textContent = `Version ${info.version} is out. Update via your package manager or download it manually.`;
|
||||||
|
btn.textContent = "Download manually";
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.onclick = () => {
|
||||||
|
window.electronAPI.openReleasePage();
|
||||||
|
container.style.display = "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialButtonText = btn.textContent;
|
||||||
|
const initialIconText = icon.textContent;
|
||||||
|
const initialTitleText = title.textContent;
|
||||||
|
const initialSubText = sub.textContent;
|
||||||
|
|
||||||
|
btn.onclick = async () => {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = "Downloading...";
|
||||||
|
icon.textContent = "download";
|
||||||
|
title.textContent = "Downloading update";
|
||||||
|
progressWrap.style.display = "flex";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.electronAPI.downloadUpdate();
|
||||||
|
} catch {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = initialButtonText;
|
||||||
|
icon.textContent = initialIconText;
|
||||||
|
title.textContent = initialTitleText;
|
||||||
|
sub.textContent = initialSubText;
|
||||||
|
progressWrap.style.display = "none";
|
||||||
|
progressFill.style.width = "0%";
|
||||||
|
progressPct.textContent = "0%";
|
||||||
|
progressEta.textContent = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.electronAPI.onDownloadProgress((progress) => {
|
||||||
|
const pct = Math.round(progress.percent);
|
||||||
|
progressFill.style.width = pct + "%";
|
||||||
|
progressPct.textContent = pct + "%";
|
||||||
|
|
||||||
|
const remaining = (progress.total - progress.transferred) / progress.bytesPerSecond;
|
||||||
|
progressEta.textContent =
|
||||||
|
remaining > 0 ? `~${Math.ceil(remaining)}s left` : "almost done";
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onUpdateDownloaded(() => {
|
||||||
|
progressFill.style.width = "100%";
|
||||||
|
progressPct.textContent = "100%";
|
||||||
|
progressEta.textContent = "Done";
|
||||||
|
icon.textContent = "check";
|
||||||
|
title.textContent = "Ready to install";
|
||||||
|
sub.textContent = "Restart to apply the update";
|
||||||
|
btn.textContent = "Restart";
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.onclick = () => window.electronAPI.installUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initUpdateUI();
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
128
public/styles/components/autoUpdate.css
Normal file
128
public/styles/components/autoUpdate.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
@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('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');
|
||||||
|
|
||||||
|
|||||||
53
test/unit/autoUpdater.test.js
Normal file
53
test/unit/autoUpdater.test.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const mockDownloadUpdate = jest.fn();
|
||||||
|
const mockCheckForUpdates = jest.fn();
|
||||||
|
const mockOn = jest.fn();
|
||||||
|
const mockQuitAndInstall = jest.fn();
|
||||||
|
|
||||||
|
jest.mock("electron-updater", () => ({
|
||||||
|
autoUpdater: {
|
||||||
|
autoDownload: true,
|
||||||
|
autoInstallOnAppQuit: true,
|
||||||
|
on: mockOn,
|
||||||
|
checkForUpdates: mockCheckForUpdates,
|
||||||
|
downloadUpdate: mockDownloadUpdate,
|
||||||
|
quitAndInstall: mockQuitAndInstall,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
isPackaged: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockLoggerError = jest.fn();
|
||||||
|
jest.mock("../../server/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: mockLoggerError,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { downloadUpdate } = require("../../app/autoUpdater");
|
||||||
|
|
||||||
|
describe("autoUpdater.downloadUpdate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolves when electron-updater download succeeds", async () => {
|
||||||
|
mockDownloadUpdate.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await expect(downloadUpdate()).resolves.toBeUndefined();
|
||||||
|
expect(mockDownloadUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects when electron-updater download fails", async () => {
|
||||||
|
const error = new Error("network down");
|
||||||
|
mockDownloadUpdate.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(downloadUpdate()).rejects.toThrow("network down");
|
||||||
|
expect(mockLoggerError).toHaveBeenCalledWith("Download failed:", "network down");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user