11 Commits

Author SHA1 Message Date
MasterAcnolo
d11c0f9839 add: .zed and OS Generated files to .gitignore, and better exclusion of IDE folder 2026-09-27 11:30:57 +02:00
MasterAcnolo
1e0b2affee update: bump app version to 1.6.4 since it's no longer a preview 2026-09-27 11:28:05 +02:00
MasterAcnolo
9975b2b583 refactor(update): add an UI indicator in frontend
disable OS native pop up menu and fix autoupdate crash on rpm and snap

#50
2026-09-25 21:08:18 +02:00
MasterAcnolo
b319a1e445 fix(ipc): send report IPC API 2026-09-16 22:16:59 +02:00
MasterAcnolo
4a64a40226 fix(front): settings menu transition 2026-09-16 20:17:42 +02:00
MasterAcnolo
2c0f5b315b refactor: toast notification style 2026-09-13 15:53:21 +02:00
MasterAcnolo
58b6637eeb feat: implement linter with config file 2026-09-13 12:06:33 +02:00
MasterAcnolo
8af5a4a383 update: bump app version to 1.6.4-preview 2026-09-13 11:56:03 +02:00
Axel Nicolas
23f04bafc3 Merge pull request #56 from MasterAcnolo/feature/send-logs
Feature/send logs
2026-09-13 11:54:43 +02:00
MasterAcnolo
35ffd7077e fix: onClick attribute on Report a Bug
(#53)
2026-09-13 11:44:37 +02:00
MasterAcnolo
408df0e2f6 feat: send logs feature that send logs to a distant service. Full integration in ui
+ expose logger method to frontEnd (#53)
2026-09-13 11:41:09 +02:00
23 changed files with 1890 additions and 187 deletions

2
.env.example Normal file
View File

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

13
.gitignore vendored
View File

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

View File

@@ -250,7 +250,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
| Option | Type | Default | Description |
|------------------------------|---------|----------|------------------------------------------------------------|
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
| `customTopBar` | boolean | `true` | Use custom application top bar |
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
@@ -367,13 +367,29 @@ Head over to the [Freedom Loader Workshop repository](https://github.com/MasterA
### Bug Reports
Use the GitHub Issues system and include:
You can report bugs either using the built-in application system or via the GitHub Issues system.
#### Using the Built-in Report Tool
1. Open Freedom Loader.
2. Trigger the bug report interface.
3. Fill in the title and description of the issue.
4. Choose whether to include today's logs automatically.
5. Submit the report.
> [!NOTE]
> If you need to attach extra context like screenshots or screen recordings, it is recommended to use the GitHub Issues
system instead.
#### Using GitHub Issues
Open a new issue on the GitHub repository and include:
- Clear description of the issue
- Steps to reproduce
- Expected vs actual behavior
- Relevant logs from `AppData\Local\FreedomLoader\logs\` or `~/.local/share/FreedomLoader/logs/`
- Screenshots if applicable
- Screenshots or recordings if applicable
### Feature Requests

View File

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

View File

@@ -97,7 +97,7 @@ function startRPC() {
details: `Open Source Download Tools - ${config.version}`,
state: "masteracnolo.github.io/FreedomLoader",
};
rpc.clearActivity()
rpc.clearActivity();
rpc.setActivity(presence);
});

View File

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

54
app/sendReport.js Normal file
View File

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

View File

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

40
eslint.config.js Normal file
View File

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

13
main.js
View File

@@ -48,9 +48,9 @@ const { configFeatures, devMode} = require("./config");
*/
if (!configFeatures.enableHardwareAcceleration){
app.disableHardwareAcceleration();
logger.info("Disabled Hardware Acceleration")
logger.info("Disabled Hardware Acceleration");
} else {
logger.info("Enable Hardware Acceleration")
logger.info("Enable Hardware Acceleration");
}
if(devMode){
@@ -58,8 +58,8 @@ if(devMode){
* Start devTron extensions - @see https://github.com/electron/devtron
*/
const { devtron } = require('@electron/devtron');
devtron.install();
logger.info("Loaded DevTron Extension")
devtron.install();
logger.info("Loaded DevTron Extension");
}
/**
@@ -78,6 +78,11 @@ const { userThemesPath, initUserThemes, validateBinaries, defaultDownloadFolder
const {createSystemTray, destroyTray} = require("./app/tray");
const { stopServer } = require("./server/server");
/**
* Expose .env in process.env
*/
require("dotenv").config();
/**
* Global flag indicating if the application is intentionally shutting down.
* Used across the app to bypass the "minimize to tray on close" behavior.

1078
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "freedom-loader",
"desktopName": "com.masteracnolo.freedomloader",
"productName": "Freedom Loader",
"version": "1.6.3",
"version": "1.6.4",
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
"description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms",
"homepage": "https://masteracnolo.github.io/Freedom-Loader-Site/",
@@ -32,12 +32,15 @@
"release:dry-run": "bash scripts/release.sh --dry-run",
"test": "jest",
"test:unit": "jest test/unit",
"lint": "eslint . --format pretty",
"lint:fix": "eslint . --fix --format pretty",
"update": "npm update"
},
"dependencies": {
"chalk": "^4.1.2",
"debug": "^4.4.1",
"discord-rpc": "^4.0.1",
"dotenv": "^17.4.2",
"electron-updater": "^6.6.2",
"express": "^5.1.0",
"express-rate-limit": "^8.2.1",
@@ -48,10 +51,14 @@
},
"devDependencies": {
"@electron/devtron": "^2.1.1",
"@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1",
"@playwright/test": "^1.62.1",
"electron": "^44.3.0",
"electron-builder": "^26.15.3",
"eslint": "^10.10.0",
"eslint-formatter-pretty": "^7.1.0",
"globals": "^17.12.0",
"jest": "^30.4.2",
"nodemon": "^3.1.14",
"playwright": "^1.62.1"

View File

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

View File

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

View File

@@ -0,0 +1,81 @@
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;
}
btn.onclick = () => {
window.electronAPI.downloadUpdate();
btn.disabled = true;
btn.textContent = "Downloading...";
icon.textContent = "download";
title.textContent = "Downloading update";
progressWrap.style.display = "flex";
};
window.electronAPI.onDownloadProgress((progress) => {
const pct = Math.round(progress.percent);
progressFill.style.width = pct + "%";
progressPct.textContent = pct + "%";
const remaining = (progress.total - progress.transferred) / progress.bytesPerSecond;
progressEta.textContent =
remaining > 0 ? `~${Math.ceil(remaining)}s left` : "almost done";
});
window.electronAPI.onUpdateDownloaded(() => {
progressFill.style.width = "100%";
progressPct.textContent = "100%";
progressEta.textContent = "Done";
icon.textContent = "check";
title.textContent = "Ready to install";
sub.textContent = "Restart to apply the update";
btn.textContent = "Restart";
btn.disabled = false;
btn.onclick = () => window.electronAPI.installUpdate();
});
}
}
initUpdateUI();

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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