7 Commits

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

#50
2026-09-25 21:08:18 +02:00
MasterAcnolo
b319a1e445 fix(ipc): send report IPC API 2026-09-16 22:16:59 +02:00
MasterAcnolo
4a64a40226 fix(front): settings menu transition 2026-09-16 20:17:42 +02:00
12 changed files with 512 additions and 123 deletions

10
.gitignore vendored
View File

@@ -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

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

View File

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

View File

@@ -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/",

View File

@@ -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),
}); });

View File

@@ -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>

View File

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

View File

@@ -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

@@ -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

@@ -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

@@ -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');

View File

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