diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index ecfa48e..5c74cfe 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -9,8 +9,8 @@ assignees: 'MasterAcnolo' ## Pre-submission Checklist -- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/#/faq) -- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/#/wiki) +- [ ] I have checked the [FAQ](https://freedomloader.acnolo.fr/#/faq) +- [ ] I have reviewed the [Wiki](https://freedomloader.acnolo.fr/#/wiki) - [ ] I have searched existing issues ## Question diff --git a/.gitignore b/.gitignore index 81ae903..3e4ba25 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c2f407..ab56aaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,8 +112,8 @@ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details. ## Questions? -- Check the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/faq) -- Review the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/wiki) +- Check the [FAQ](https://freedomloader.acnolo.fr/faq) +- Review the [Wiki](https://freedomloader.acnolo.fr/wiki) - Open a [Question issue](https://github.com/MasterAcnolo/Freedom-Loader/issues/new/choose) --- diff --git a/README.md b/README.md index 6944a2e..2b88d75 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- Banner @@ -9,7 +9,7 @@ [![Release](https://img.shields.io/github/v/release/MasterAcnolo/Freedom-Loader?style=for-the-badge&color=blue)](https://github.com/MasterAcnolo/Freedom-Loader/releases) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0) -[![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Site/) +[![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://freedomloader.acnolo.fr/) [![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Workshop/) @@ -56,7 +56,7 @@ The primary goal is to make media downloading accessible to users who want offli ### User Interface -- **Custom window controls** - Frameless window with custom top bar (optional) +- **Custom window controls** - Frameless window with custom top bar - **Custom themes** - ZIP-based theme system with live preview - **Theme Workshop** - web-based theme creator and browser - **Toast notifications** - in-app feedback system @@ -151,6 +151,7 @@ freedom-loader |---------------|---------------------------------------------------------|--------------------------------------| | **Downloads** | `C:\Users\[USERNAME]\Downloads\Freedom Loader` | `~/Downloads/Freedom Loader` | | **Logs** | `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\` | `~/.local/share/FreedomLoader/logs/` | +| **Cache** | `` > [!NOTE] > The download folder can be changed anytime via the "Edit" button in the UI, on both platforms. @@ -224,10 +225,8 @@ Freedom Loader can be configured either through the settings panel in the UI or ### Available Options ```json { - "autoUpdate": true, "systemTray": true, "discordRPC": true, - "customTopBar": true, "autoCheckInfo": true, "addThumbnail": true, "addMetadata": true, @@ -241,6 +240,7 @@ Freedom Loader can be configured either through the settings panel in the UI or "downloadSystem": true, "notifySystem": true, "enableHardwareAcceleration": true, + "showChanglogsAfterUpdate": true, "theme": "dark" } ``` @@ -249,10 +249,8 @@ Freedom Loader can be configured either through the settings panel in the UI or | Option | Type | Default | Description | |------------------------------|---------|----------|------------------------------------------------------------| -| `autoUpdate` | boolean | `true` | Enable automatic application updates | -| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] | +| `systemTray` | boolean | `false` | Allow app to minimize on close | | `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration | -| `customTopBar` | boolean | `true` | Use custom application top bar | | `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste | | `addThumbnail` | boolean | `true` | Embed thumbnail in downloaded audio files | | `addMetadata` | boolean | `true` | Add metadata tags to downloaded files | @@ -266,6 +264,7 @@ Freedom Loader can be configured either through the settings panel in the UI or | `downloadSystem` | boolean | `true` | Enable download system | | `notifySystem` | boolean | `true` | Enable system notifications | | `enableHardwareAcceleration` | boolean | `true` | Enable Hardware Acceleration | +| `showChanglogsAfterUpdate` | boolean | `true` | Auto-display changelog popup after and update | | `theme` | string | `dark` | Current Theme Name | ### Supported Codecs @@ -354,7 +353,7 @@ See [BINARIES.md](./BINARIES.md) for how to setup the binaries. ## Roadmap -See the most recent Roadmap [Here](https://masteracnolo.github.io/Freedom-Loader-Site/Roadmap). +See the most recent Roadmap [Here](https://freedomloader.acnolo.fr/Roadmap). ## Contributing @@ -367,13 +366,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 @@ -418,6 +433,6 @@ You are free to use, modify, and redistribute this software under the terms of t **Freedom Loader** - Put freedom in your downloads -[Website](https://masteracnolo.github.io/Freedom-Loader-Site/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://masteracnolo.github.io/Freedom-Loader-Site/wiki) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues) +[Website](https://freedomloader.acnolo.fr/) • [Download](https://github.com/MasterAcnolo/Freedom-Loader/releases) • [Documentation](https://freedomloader.acnolo.fr/wiki) • [Report Bug](https://github.com/MasterAcnolo/Freedom-Loader/issues)
diff --git a/app/autoUpdater.js b/app/autoUpdater.js index bbb800a..d3abab8 100644 --- a/app/autoUpdater.js +++ b/app/autoUpdater.js @@ -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,16 @@ 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); + mainWindow?.webContents.send("update-error", msg); }); checkForUpdates(); @@ -121,18 +124,9 @@ function initAutoUpdater(mainWindow) { */ 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 app is was installed from SNAP or Flatpak. + if (process.env.SNAP || process.env.FLATPAK_ID) return; + if (!require("electron").app.isPackaged) return; try { await autoUpdater.checkForUpdates(); @@ -150,6 +144,7 @@ async function downloadUpdate() { await autoUpdater.downloadUpdate(); } catch (err) { logger.error("Download failed:", err.message); + throw err; } } @@ -160,4 +155,13 @@ function installUpdate() { autoUpdater.quitAndInstall(); } -module.exports = { initAutoUpdater, downloadUpdate, installUpdate }; \ No newline at end of file +function isUpdateAvailable() { + return updateAvailable; +} + +module.exports = { + initAutoUpdater, + downloadUpdate, + installUpdate, + isUpdateAvailable, +}; diff --git a/app/changelogManager.js b/app/changelogManager.js new file mode 100644 index 0000000..449223b --- /dev/null +++ b/app/changelogManager.js @@ -0,0 +1,177 @@ +const { app } = require("electron"); +const fs = require("fs"); +const path = require("path"); +const https = require("https"); +const config = require("../config"); +const { logger } = require("../server/logger"); + +/** Maximum age of the cached release list before it must be refreshed. */ +const CACHE_TTL_MS = 24 * 60 * 60 * 1000; + +/** Filename used to persist changelog data in the Electron user data folder. */ +const CACHE_FILE_NAME = "changelog-cache.json"; + +/** GitHub API endpoint containing the application's published releases. */ +const RELEASES_URL = "https://api.github.com/repos/MasterAcnolo/Freedom-Loader/releases"; + +/** Returns the path of the local changelog cache file. */ +function getCachePath() { + return path.join(app.getPath("userData"), CACHE_FILE_NAME); +} + +/** + * Extracts the numeric major, minor, and patch parts from a version string. + * + * @param {string} version - Version string or release tag + * @returns {number[]|null} Parsed version parts, or null when invalid + */ +function normalizeVersion(version) { + const match = String(version || "").match(/(\d+)\.(\d+)\.(\d+)/); + return match ? match.slice(1).map(Number) : null; +} + +/** + * Compares two semantic version strings. + * + * @param {string} left - First version to compare + * @param {string} right - Second version to compare + * @returns {number} Negative, zero, or positive comparison result + */ +function compareVersions(left, right) { + const leftParts = normalizeVersion(left) || [0, 0, 0]; + const rightParts = normalizeVersion(right) || [0, 0, 0]; + + for (let index = 0; index < 3; index += 1) { + if (leftParts[index] !== rightParts[index]) { + return leftParts[index] - rightParts[index]; + } + } + + return 0; +} + +/** Reads and parses the cached changelog, if one is available. */ +function readCache() { + try { + return JSON.parse(fs.readFileSync(getCachePath(), "utf8")); + } catch { + return null; + } +} + +/** Persists the fetched releases together with their cache timestamp. */ +function writeCache(releases) { + try { + fs.writeFileSync( + getCachePath(), + JSON.stringify({ cachedAt: Date.now(), releases }, null, 2), + "utf8", + ); + } catch (error) { + logger.warn(`Unable to cache changelog: ${error.message}`); + } +} + +/** + * Fetches published releases from GitHub and maps them to the UI data shape. + * + * @returns {Promise} Published releases + */ +function fetchReleases() { + return new Promise((resolve, reject) => { + const request = https.get( + RELEASES_URL, + { + headers: { + Accept: "application/vnd.github+json", + "User-Agent": "Freedom-Loader", + }, + }, + (response) => { + let body = ""; + response.setEncoding("utf8"); + response.on("data", (chunk) => { + body += chunk; + }); + response.on("end", () => { + // Do not attempt to parse an error response as a release list. + if (response.statusCode !== 200) { + reject(new Error(`GitHub returned HTTP ${response.statusCode}`)); + return; + } + + try { + // Draft releases are not visible to users and should not appear in the changelog. + const releases = JSON.parse(body) + .filter((release) => !release.draft) + .map((release) => ({ + version: release.tag_name, + name: release.name || release.tag_name, + date: release.published_at || release.created_at, + body: release.body || "No changelog available.", + url: release.html_url, + })); + resolve(releases); + } catch (error) { + reject(error); + } + }); + }, + ); + + request.setTimeout(10000, () => { + request.destroy(new Error("Changelog request timed out")); + }); + request.on("error", reject); + }); +} + +/** + * Returns cached or freshly fetched releases and identifies unread versions. + * + * @returns {Promise} Changelog data for the renderer + */ +async function getChangelog() { + const cache = readCache(); + let releases = cache?.releases || []; + const cacheIsFresh = cache && cache.releases?.length > 0 && Date.now() - cache.cachedAt < CACHE_TTL_MS; + + // Refresh stale or missing data, while keeping the last cache as a fallback. + if (!cacheIsFresh) { + try { + releases = await fetchReleases(); + writeCache(releases); + } catch (error) { + logger.warn(`Unable to retrieve changelog: ${error.message}`); + } + } + + const lastSeenVersion = cache?.lastSeenVersion || null; + + // Without a previous version, all available releases are considered unread. + const unreadReleases = lastSeenVersion + ? releases.filter((release) => compareVersions(release.version, lastSeenVersion) > 0) + : releases; + + return { + // An empty release list means that the fetch failed or returned no releases. + error: releases.length === 0, + currentVersion: config.version, + lastSeenVersion, + hasUnread: unreadReleases.length > 0, + releases, + unreadReleases, + }; +} + +/** Marks the current application version as seen in the local cache. */ +function markChangelogSeen() { + const cache = readCache() || { releases: [] }; + cache.lastSeenVersion = config.version; + cache.cachedAt = cache.cachedAt || 0 ; + fs.mkdirSync(path.dirname(getCachePath()), { recursive: true }); + fs.writeFileSync(getCachePath(), JSON.stringify(cache, null, 2), "utf8"); + return true; +} + +module.exports = { getChangelog, markChangelogSeen }; diff --git a/app/discordRPC.js b/app/discordRPC.js index 101b8ff..4a3c8bc 100644 --- a/app/discordRPC.js +++ b/app/discordRPC.js @@ -95,9 +95,9 @@ function startRPC() { smallImageText: "By MasterAcnolo", startTimestamp: new Date(), details: `Open Source Download Tools - ${config.version}`, - state: "masteracnolo.github.io/FreedomLoader", + state: "freedomloader.acnolo.fr", }; - rpc.clearActivity() + rpc.clearActivity(); rpc.setActivity(presence); }); diff --git a/app/ipcHandlers.js b/app/ipcHandlers.js old mode 100644 new mode 100755 index 1f3510a..9a104ad --- a/app/ipcHandlers.js +++ b/app/ipcHandlers.js @@ -13,12 +13,15 @@ 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, checkReportService, getReportServiceStatus} = require("./sendReport"); +const { isUpdateAvailable, downloadUpdate, installUpdate } = require("./autoUpdater"); +const { getChangelog, markChangelogSeen } = require("./changelogManager"); /** * Security whitelist for feature flags that can be modified at runtime. @@ -27,10 +30,8 @@ const { createSystemTray, destroyTray } = require("./tray"); * Acts as a basic validation layer for IPC "set-feature". */ const FEATURE_WHITELIST = new Set([ - "autoUpdate", "systemTray", "discordRPC", - "customTopBar", "autoCheckInfo", "addThumbnail", "addMetadata", @@ -41,7 +42,8 @@ const FEATURE_WHITELIST = new Set([ "theme", "createPlaylistFolders", "notifySystem", - "enableHardwareAcceleration" + "enableHardwareAcceleration", + "showChanglogsAfterUpdate" ]); /** @@ -64,12 +66,21 @@ const themeFolderPath = userThemesPath; * @param {Function} getMainWindow - Function returning current BrowserWindow instance */ function registerIpcHandlers(getMainWindow) { - /** * Returns application version from config. */ ipcMain.handle("version", () => config.version); + /** + * Retrieves the changelog from cache or GitHub API. + */ + ipcMain.handle("get-changelog", () => getChangelog()); + + /** + * Marks all changelog entries as seen and updates cache. + */ + ipcMain.handle("mark-changelog-seen", () => markChangelogSeen()); + /** * Returns runtime feature configuration object. */ @@ -115,17 +126,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 +151,6 @@ function registerIpcHandlers(getMainWindow) { */ ipcMain.on("window-close", () => getMainWindow()?.close()); - /** * Opens Chromium DevTools in detached mode. */ @@ -152,20 +161,20 @@ 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. */ ipcMain.on("open-website", () => - shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/") + shell.openExternal("https://freedomloader.acnolo.fr/") ); /** * Opens official wiki page in external browser. */ ipcMain.on("open-wiki", () => - shell.openExternal("https://masteracnolo.github.io/Freedom-Loader-Site/wiki") + shell.openExternal("https://freedomloader.acnolo.fr/wiki") ); /** @@ -180,8 +189,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 +213,36 @@ function registerIpcHandlers(getMainWindow) { return await reloadThemes(); }); + /** + * Send bug report + */ + ipcMain.handle("send-report", async (_, params) => { + return await sendReport(params); + }); + + ipcMain.handle("get-report-service-status", async () => { + await checkReportService(); + return getReportServiceStatus(); + }); + + ipcMain.handle("is-update-available", () => { + return isUpdateAvailable(); + }); + + ipcMain.handle("download-update", async () => { + return await downloadUpdate(); + }); + + ipcMain.handle("install-update", async () => { + return installUpdate(); + }); + + ipcMain.on("open-release-page", () => { + shell.openExternal( + "https://github.com/MasterAcnolo/Freedom-Loader/releases/latest", + ); + }); + /** * Updates a runtime feature flag and persists it to disk. * @@ -216,47 +260,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 }; \ No newline at end of file +module.exports = { registerIpcHandlers }; diff --git a/app/sendReport.js b/app/sendReport.js new file mode 100644 index 0000000..e37266b --- /dev/null +++ b/app/sendReport.js @@ -0,0 +1,108 @@ +const fs = require("fs"); +const path = require("path"); +const {logger, logDir} = require("../server/logger"); + +const REPORT_URL = "https://freedom-loader-report.acnolo.fr"; +const REPORT_SERVICE_TIMEOUT_MS = 5000; +let reportServiceStatus = { + status: "checking", + message: "Checking bug report service..." +}; +let reportServiceCheck = null; + +async function checkReportService() { + if (reportServiceCheck) { + return reportServiceCheck; + } + + reportServiceCheck = (async () => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), REPORT_SERVICE_TIMEOUT_MS); + + try { + const response = await fetch(REPORT_URL, { + method: "GET", + signal: controller.signal + }); + + if (response.status >= 500) { + throw new Error(`Server responded with status ${response.status}`); + } + + reportServiceStatus = { + status: "online", + message: "Bug report service is available." + }; + logger.info(`Bug report service is reachable (status ${response.status}).`); + } catch (err) { + reportServiceStatus = { + status: "offline", + message: "Bug report service is unavailable. Reports may fail to send." + }; + logger.warn(`Bug report service is unreachable: ${err.message}`); + } finally { + clearTimeout(timeout); + } + + return reportServiceStatus; + })(); + + return reportServiceCheck; +} + +function getReportServiceStatus() { + return reportServiceStatus; +} + +async function sendReport(params) { + try { + const {title, description, includeLogs} = params; + + if (!title || !description) { + throw new Error("Title and description are required."); + } + + const formData = new FormData(); + formData.append("title", title); + formData.append("context", description); + + if (includeLogs === "yes" && logDir) { + // YYYY-MM-DD + const now = new Date(); + const today = [ + now.getFullYear(), + String(now.getMonth() + 1).padStart(2, "0"), + String(now.getDate()).padStart(2, "0") + ].join("-"); + + const logFilePath = path.join(logDir, `LOGS-${today}.log`); + + if (fs.existsSync(logFilePath)) { + const logFileContent = fs.readFileSync(logFilePath); + const blob = new Blob([logFileContent], {type: "text/plain"}); + formData.append("file", blob, `LOGS-${today}.log`); + logger.info("Logs attached to bug report."); + } else { + logger.warn(`Log file not found for today: ${logFilePath}`); + } + } + + const response = await fetch(REPORT_URL, { + method: "POST", + body: formData + }); + + if (!response.ok) { + logger.error(`Server returned an error: ${response.status}`); + throw new Error(`Server responded with status ${response.status}`); + } + + logger.info("Bug report successfully sent."); + return true; + } catch (err) { + logger.error(`Failed to send bug report: ${err.message}`); + throw err; + } +} + +module.exports = {sendReport, checkReportService, getReportServiceStatus}; \ No newline at end of file diff --git a/app/tray.js b/app/tray.js index 5ab9b57..19dca34 100644 --- a/app/tray.js +++ b/app/tray.js @@ -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)"); } }); diff --git a/app/windowManager.js b/app/windowManager.js index a21ed42..ee30b8f 100644 --- a/app/windowManager.js +++ b/app/windowManager.js @@ -57,7 +57,7 @@ async function createMainWindow() { height: 800, minWidth: 750, minHeight: 800, - frame: !configFeatures.customTopBar, + frame: false, // Force custom topbar for consistent UI across platforms, and less maintenance headaches with native titlebar actions devTools: !app.isPackaged, show: false, webPreferences: { diff --git a/config/config.default.json b/config/config.default.json index c620f50..b8306fa 100644 --- a/config/config.default.json +++ b/config/config.default.json @@ -1,8 +1,6 @@ { - "autoUpdate": true, "systemTray": false, "discordRPC": true, - "customTopBar": true, "autoCheckInfo": true, "addThumbnail": true, "addMetadata": true, @@ -16,5 +14,6 @@ "downloadSystem": true, "notifySystem": true, "enableHardwareAcceleration": true, + "showChanglogsAfterUpdate": true, "theme": "dark" } \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..0bc67c9 --- /dev/null +++ b/eslint.config.js @@ -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} + } + } +]; \ No newline at end of file diff --git a/main.js b/main.js index 46f56b4..474a03e 100644 --- a/main.js +++ b/main.js @@ -13,6 +13,9 @@ process.on("unhandledRejection", (reason) => { app.quit(); }); +if (process.platform === 'linux') { + app.setDesktopName('com.masteracnolo.freedomloader.desktop'); +} /** * True if this is the primary instance (lock acquired successfully) @@ -48,9 +51,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 +61,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"); } /** @@ -77,6 +80,7 @@ const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./ const { userThemesPath, initUserThemes, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers"); const {createSystemTray, destroyTray} = require("./app/tray"); const { stopServer } = require("./server/server"); +const {checkReportService} = require("./app/sendReport"); /** * Global flag indicating if the application is intentionally shutting down. @@ -132,6 +136,9 @@ app.on("second-instance", () => { app.whenReady().then(async () => { logSessionStart(logDir, defaultDownloadFolder); + checkReportService().catch((err) => { + logger.warn(`Bug report service startup check failed: ${err.message}`); + }); createSplashWindow(); @@ -166,7 +173,7 @@ app.whenReady().then(async () => { if (configFeatures.discordRPC) startRPC(); - if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow()); + initAutoUpdater(getMainWindow()); } catch (err) { logger.error("Boot error:", err); diff --git a/package-lock.json b/package-lock.json index ec010cf..f6b1d1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "freedom-loader", - "version": "1.6.3-preview", + "version": "1.6.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "freedom-loader", - "version": "1.6.3-preview", + "version": "1.6.4", "license": "GPL-3.0-only", "dependencies": { "chalk": "^4.1.2", @@ -22,10 +22,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" @@ -574,6 +578,67 @@ "dev": true, "license": "MIT" }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -953,6 +1018,134 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@fontsource-variable/roboto": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@fontsource-variable/roboto/-/roboto-5.3.0.tgz", @@ -973,6 +1166,72 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1586,6 +1845,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", @@ -1975,6 +2241,31 @@ "@types/ms": "*" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -2019,6 +2310,13 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -2446,6 +2744,29 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ag-charts-types": { "version": "12.3.1", "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-12.3.1.tgz", @@ -2693,6 +3014,19 @@ "node": ">=8" } }, + "node_modules/app-builder-lib/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/app-builder-lib/node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3151,6 +3485,20 @@ "node": ">=6.0.0" } }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -3196,6 +3544,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -3654,6 +4012,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3827,19 +4192,6 @@ "js-yaml": "^4.1.0" } }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dotenv-expand": { "version": "11.0.7", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", @@ -3856,6 +4208,19 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4155,6 +4520,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -4252,6 +4630,319 @@ "node": ">=8" } }, + "node_modules/eslint": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-formatter-pretty": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-7.1.0.tgz", + "integrity": "sha512-iyPrgpwKC3MMM75Wxn0VouD89HolpG+BL95NOxgwOWO0R+Omapo7gFX2xcGVsUDS7KiXLtDuynLbNbOARSE7YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "^9.6.1", + "ansi-escapes": "^7.1.0", + "chalk": "^5.6.2", + "eslint-rule-docs": "^1.1.235", + "log-symbols": "^7.0.1", + "plur": "^5.1.0", + "string-width": "^8.1.0", + "supports-hyperlinks": "^4.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-formatter-pretty/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/eslint-rule-docs": { + "version": "1.1.235", + "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", + "integrity": "sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -4266,6 +4957,52 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4410,6 +5147,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", @@ -4461,6 +5205,16 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, "node_modules/file-stream-rotator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", @@ -4565,6 +5319,25 @@ "node": ">=8" } }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -4724,6 +5497,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4869,6 +5655,19 @@ "node": ">=10.0" } }, + "node_modules/globals": { + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -4982,6 +5781,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -4994,6 +5806,13 @@ "node": ">= 0.4" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -5109,6 +5928,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -5188,6 +6017,16 @@ "node": ">= 0.10" } }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5279,6 +6118,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -6187,6 +7039,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -6264,6 +7123,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -6320,6 +7193,23 @@ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -6976,6 +7866,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -7297,6 +8205,22 @@ "node": ">=10.4.0" } }, + "node_modules/plur": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz", + "integrity": "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "irregular-plurals": "^3.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/postject": { "version": "1.0.0-alpha.6", "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", @@ -7327,6 +8251,16 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-format": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", @@ -7451,6 +8385,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -7488,6 +8432,26 @@ "node": ">=16.0.0" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -8349,6 +9313,49 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", + "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^5.0.1", + "supports-color": "^10.2.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", + "integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/synckit": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", @@ -8604,6 +9611,19 @@ "dev": true, "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -8799,6 +9819,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-composed-ref": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", @@ -9031,6 +10061,16 @@ "node": ">= 6" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9190,6 +10230,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 56b35e4..e452533 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "freedom-loader", "desktopName": "com.masteracnolo.freedomloader", "productName": "Freedom Loader", - "version": "1.6.3", + "version": "1.6.4", "author": "MasterAcnolo ", "description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms", - "homepage": "https://masteracnolo.github.io/Freedom-Loader-Site/", + "homepage": "https://freedomloader.acnolo.fr/", "repository": { "type": "git", "url": "https://github.com/MasterAcnolo/Freedom-Loader.git" @@ -16,6 +16,7 @@ }, "main": "main.js", "scripts": { + "clean:dist": "rm -rf dist/ && rm -rf srpm-out/", "start": "electron --no-warnings .", "start:warn": "electron --trace-warnings .", "start:debug": "WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .", @@ -25,13 +26,17 @@ "build": "electron-builder", "build:win": "electron-builder --win", "build:linux": "electron-builder --linux", - "build:rpm": "bash scripts/create-copr-package.sh", + "build:rpm": "npm run clean:dist && npm run build:linux && bash scripts/create-copr-package.sh", "build:all": "electron-builder -wl", "release": "bash scripts/release.sh", "release:publish": "bash scripts/release.sh --publish", "release:dry-run": "bash scripts/release.sh --dry-run", "test": "jest", "test:unit": "jest test/unit", + "test:metainfo": "appstreamcli validate package/com.masteracnolo.freedomloader.metainfo.xml", + "test:desktop": "desktop-file-validate package/com.masteracnolo.freedomloader.desktop", + "lint": "eslint . --format pretty", + "lint:fix": "eslint . --fix --format pretty", "update": "npm update" }, "dependencies": { @@ -48,16 +53,21 @@ }, "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" }, "build": { "appId": "com.masteracnolo.freedomloader", + "artifactName": "Freedom-Loader-${version}.${ext}", "productName": "Freedom Loader", "publish": [ { @@ -68,10 +78,15 @@ } ], "files": [ - "**/*", - "node_modules/**/*", - "!config/config.dev.json", - "!theme/**/*" + "app/**/*", + "public/**/*", + "server/**/*", + "build/**/*", + "config/config.default.json", + "config.js", + "main.js", + "preload.js", + "package.json" ], "directories": { "buildResources": "build" diff --git a/package/com.masteracnolo.freedomloader.desktop b/package/com.masteracnolo.freedomloader.desktop new file mode 100644 index 0000000..5b4ea55 --- /dev/null +++ b/package/com.masteracnolo.freedomloader.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Freedom Loader +Exec=/opt/freedom-loader/freedom-loader %U +Icon=freedom-loader +Type=Application +Categories=AudioVideo; +Comment=Free and open-source GUI for yt-dlp +StartupWMClass=com.masteracnolo.freedomloader \ No newline at end of file diff --git a/package/com.masteracnolo.freedomloader.metainfo.xml b/package/com.masteracnolo.freedomloader.metainfo.xml index 4efdddf..2feae69 100644 --- a/package/com.masteracnolo.freedomloader.metainfo.xml +++ b/package/com.masteracnolo.freedomloader.metainfo.xml @@ -45,7 +45,7 @@

- freedom-loader.desktop + com.masteracnolo.freedomloader.desktop https://github.com/MasterAcnolo/Freedom-Loader https://github.com/MasterAcnolo/Freedom-Loader/issues diff --git a/preload.js b/preload.js index aee2ac8..a8233a1 100644 --- a/preload.js +++ b/preload.js @@ -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,14 +62,23 @@ 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"), + /** + * Returns the changelog data + */ + getChangelog: () => ipcRenderer.invoke("get-changelog"), + + /** + * Marks the current changelog version as seen by the user. + */ + markChangelogSeen: () => ipcRenderer.invoke("mark-changelog-seen"), + /** * Validates a download path before using it for file operations. * @@ -67,8 +96,70 @@ 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} 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} + */ + downloadUpdate: () => ipcRenderer.invoke("download-update"), + + /** + * Instructs the main process to quit the application and install the downloaded update. + * + * @returns {Promise} + */ + installUpdate: () => ipcRenderer.invoke("install-update"), + + /** + * Subscribes to the update error event. + * + * @param {Function} callback - Function executed with error information when an update error occurs. + * @returns {Electron.IpcRenderer} + */ + onUpdateError: (callback) => ipcRenderer.on("update-error", (_, err) => callback(err)), + + /** + * Subscribes to the download progress event. + * + * @param {Function} callback - Function executed with progress metrics (percent, bytesPerSecond, transferred, total). + * @returns {Electron.IpcRenderer} + */ + onDownloadProgress: (callback) => + ipcRenderer.on("update-progress", (_, progress) => callback(progress)), + + /** + * Subscribes to the update downloaded event. + * Triggered when the update file is fully downloaded and ready to install. + * + * @param {Function} callback - Function executed with update info. + */ + onUpdateDownloaded: (callback) => + ipcRenderer.on("update-downloaded", (_, info) => callback(info)), + + openReleasePage: () => ipcRenderer.send("open-release-page"), +}); /** * Exposes window control and developer utilities for the custom topbar UI. @@ -76,7 +167,6 @@ contextBridge.exposeInMainWorld("electronAPI", { * These methods forward commands to the Electron main process via IPC. */ contextBridge.exposeInMainWorld("topbarAPI", { - /** * Minimizes the application window. */ @@ -126,4 +216,17 @@ contextBridge.exposeInMainWorld("topbarAPI", { * Opens configuration/settings panel. */ openConfig: () => ipcRenderer.send("open-config"), + + /** + * Send Bug Report + * + * @param params + * @returns {Promise} + */ + sendReport: (params) => ipcRenderer.invoke("send-report", params), + + /** + * Returns the startup health status of the bug report service. + */ + getReportServiceStatus: () => ipcRenderer.invoke("get-report-service-status"), }); \ No newline at end of file diff --git a/public/index.html b/public/index.html index 8502f85..7f7688e 100644 --- a/public/index.html +++ b/public/index.html @@ -12,7 +12,9 @@ - + + + @@ -21,9 +23,11 @@
+ +
@@ -120,38 +124,38 @@ Advanced Settings
- +
- Auto Update - Enable automatic app updates. + Show Changelogs After Update + Display changelogs after each update.
-
- -
- System Tray - Minimize app on close, in the System Tray. [EXPERIMENTAL] -
+
+ +
+ System Tray + Minimize app on close, in the System Tray.
+
- - -
- -
- Auto Fetch Info - Automatically fetch information for new downloads. -
+ -
+
+ +
+ Auto Fetch Info + Automatically fetch information for new downloads. +
+
+ +
Add Metadata @@ -167,13 +171,13 @@
-
+
@@ -295,14 +299,53 @@ -
+ +

Because Why Not ?

+ @@ -314,6 +357,9 @@ + + + diff --git a/public/script/autoUpdate.js b/public/script/autoUpdate.js new file mode 100644 index 0000000..d11271a --- /dev/null +++ b/public/script/autoUpdate.js @@ -0,0 +1,109 @@ +async function initUpdateUI() { + const container = document.getElementById("updateAvailable"); + + const hasUpdate = await window.electronAPI.isUpdateAvailable(); + if (hasUpdate) { + setupAndShowUpdateUI(); + } + + window.electronAPI.onUpdateAvailable((info) => { + setupAndShowUpdateUI(info); + }); + + function setupAndShowUpdateUI(info) { + container.style.display = "block"; + + const btn = document.getElementById("btn-update"); + const icon = document.getElementById("update-icon"); + const title = document.getElementById("update-title"); + const sub = document.getElementById("update-sub"); + const progressWrap = document.getElementById("progress-wrap"); + const progressFill = document.getElementById("progress-fill"); + const progressPct = document.getElementById("progress-pct"); + const progressEta = document.getElementById("progress-eta"); + + if (info && info.version) { + sub.textContent = `A new version (${info.version}) is ready`; + } + + if (info && info.environment && info.environment !== "auto") { + icon.textContent = "open_in_new"; + + if (info.environment === "snap") { + sub.textContent = `Version ${info.version} is out. Use the Snap Store or run "snap refresh".`; + btn.textContent = "View Release"; + } else { + sub.textContent = `Version ${info.version} is out. Update via your package manager or download it manually.`; + btn.textContent = "Download manually"; + } + + btn.onclick = () => { + window.electronAPI.openReleasePage(); + container.style.display = "none"; + }; + + return; + } + + const initialButtonText = btn.textContent; + const initialIconText = icon.textContent; + const initialTitleText = title.textContent; + const initialSubText = sub.textContent; + + const resetUI = () => { + btn.disabled = false; + btn.textContent = initialButtonText; + icon.textContent = initialIconText; + title.textContent = initialTitleText; + sub.textContent = initialSubText; + progressWrap.style.display = "none"; + progressFill.style.width = "0%"; + progressPct.textContent = "0%"; + progressEta.textContent = ""; + }; + + window.electronAPI.onUpdateError((err) => { + console.error("Update failed asynchronously:", err); + resetUI(); + }); + + btn.onclick = async () => { + + btn.disabled = true; + btn.textContent = "Downloading..."; + icon.textContent = "download"; + title.textContent = "Downloading update"; + progressWrap.style.display = "flex"; + + try { + await window.electronAPI.downloadUpdate(); + } catch { + resetUI(); + } + }; + + window.electronAPI.onDownloadProgress((progress) => { + const pct = Math.round(progress.percent); + progressFill.style.width = pct + "%"; + progressPct.textContent = pct + "%"; + + const remaining = (progress.total - progress.transferred) / progress.bytesPerSecond; + progressEta.textContent = + remaining > 0 ? `~${Math.ceil(remaining)}s left` : "almost done"; + }); + + window.electronAPI.onUpdateDownloaded(() => { + progressFill.style.width = "100%"; + progressPct.textContent = "100%"; + progressEta.textContent = "Done"; + icon.textContent = "check"; + title.textContent = "Ready to install"; + sub.textContent = "Restart to apply the update"; + btn.textContent = "Restart"; + btn.disabled = false; + btn.onclick = () => window.electronAPI.installUpdate(); + }); + } +} + +initUpdateUI(); diff --git a/public/script/changelog.js b/public/script/changelog.js new file mode 100755 index 0000000..fd88401 --- /dev/null +++ b/public/script/changelog.js @@ -0,0 +1,282 @@ +/** DOM elements used by the changelog modal. */ +const UI = { + overlay: document.getElementById("changelog-overlay"), + list: document.getElementById("changelog-list"), + closeBtn: document.getElementById("changelog-close"), + openBtn: document.getElementById("changelog-btn") +}; + +/** Changelog payload loaded from the Electron main process. */ +let changelogData = null; + +/** + * Formats an ISO release date for the current locale. + * + * @param {string} dateString - Release date returned by GitHub + * @returns {string} Localized date or an empty string + */ +function formatDate(dateString) { + if (!dateString) return ""; + return new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }).format(new Date(dateString)); +} + +/** + * Parses inline Markdown and appends safe DOM nodes to a parent element. + * + * @param {HTMLElement} parent - Element receiving the parsed content + * @param {string} value - Inline Markdown text + */ +function parseInline(parent, value) { + const tokenPattern = /(\*\*|__)(.+?)\1|(? lastIndex) { + parent.appendChild(document.createTextNode(value.slice(lastIndex, match.index))); + } + + const element = match[7] + ? document.createElement("a") + : document.createElement(match[1] ? "strong" : match[3] || match[4] ? "em" : match[5] ? "code" : "del"); + + element.textContent = match[7] || match[2] || match[3] || match[4] || match[5] || match[6]; + + if (match[7]) { + element.href = match[8]; + element.target = "_blank"; + element.rel = "noopener noreferrer"; + } + + parent.appendChild(element); + lastIndex = tokenPattern.lastIndex; + } + + if (lastIndex < value.length) { + parent.appendChild(document.createTextNode(value.slice(lastIndex))); + } +} + +/** + * Parses block-level Markdown such as headings, lists, quotes and code blocks. + * + * @param {HTMLElement} parent - Element receiving the parsed content + * @param {string} markdown - Release body in Markdown format + */ +function parseBlock(parent, markdown) { + const lines = markdown.replace(/\r\n?/g, "\n").split("\n"); + let paragraph = []; + let list = null; + let codeBlock = null; + + const flushParagraph = () => { + if (!paragraph.length) return; + const element = document.createElement("p"); + paragraph.forEach((line, index) => { + if (index) element.appendChild(document.createElement("br")); + parseInline(element, line); + }); + parent.appendChild(element); + paragraph = []; + }; + + const closeList = () => { + if (list) { + parent.appendChild(list); + list = null; + } + }; + + lines.forEach((line) => { + if (line.trim().startsWith("```")) { + flushParagraph(); + closeList(); + if (codeBlock) { + const code = document.createElement("code"); + code.textContent = codeBlock.join("\n"); + const pre = document.createElement("pre"); + pre.appendChild(code); + parent.appendChild(pre); + codeBlock = null; + } else { + codeBlock = []; + } + return; + } + + if (codeBlock) { + codeBlock.push(line); + return; + } + + const heading = line.match(/^(#{1,6})\s+(.+)$/); + const unorderedItem = line.match(/^\s*[-*+]\s+(.+)$/); + const orderedItem = line.match(/^\s*\d+[.]\s+(.+)$/); + const quote = line.match(/^\s*>\s?(.*)$/); + + if (!line.trim()) { + flushParagraph(); + closeList(); + } else if (heading) { + flushParagraph(); + closeList(); + const element = document.createElement(`h${heading[1].length}`); + parseInline(element, heading[2]); + parent.appendChild(element); + } else if (unorderedItem || orderedItem) { + flushParagraph(); + const isOrdered = Boolean(orderedItem); + if (!list || list.tagName.toLowerCase() !== (isOrdered ? "ol" : "ul")) { + closeList(); + list = document.createElement(isOrdered ? "ol" : "ul"); + } + const item = document.createElement("li"); + parseInline(item, (unorderedItem || orderedItem)[1]); + list.appendChild(item); + } else if (quote) { + flushParagraph(); + closeList(); + const element = document.createElement("blockquote"); + parseInline(element, quote[1]); + parent.appendChild(element); + } else if (/^\s*([-*_])\s*\1\s*\1\s*$/.test(line)) { + flushParagraph(); + closeList(); + parent.appendChild(document.createElement("hr")); + } else { + paragraph.push(line); + } + }); + + flushParagraph(); + closeList(); + if (codeBlock) { + const code = document.createElement("code"); + code.textContent = codeBlock.join("\n"); + const pre = document.createElement("pre"); + pre.appendChild(code); + parent.appendChild(pre); + } +} + +/** Renders the empty state when no releases are available. */ +function renderEmpty() { + const emptyMessage = document.createElement("p"); + emptyMessage.className = "changelog-empty"; + emptyMessage.textContent = "No changelog available."; + UI.list.appendChild(emptyMessage); +} + +/** + * Renders one release entry in the changelog list. + * + * @param {object} release - Release data from the backend + */ +function renderEntry(release) { + const article = document.createElement("article"); + article.className = "changelog-entry"; + + const heading = document.createElement("div"); + heading.className = "changelog-entry-heading"; + + const title = document.createElement("h3"); + title.textContent = release.name; + + const metadata = document.createElement("span"); + metadata.textContent = [release.version, formatDate(release.date)] + .filter(Boolean) + .join(" · "); + + heading.append(title, metadata); + + const body = document.createElement("div"); + body.className = "changelog-body"; + parseBlock(body, release.body); + + article.append(heading, body); + UI.list.appendChild(article); +} + +/** Renders all releases, replacing the current modal contents. */ +function renderReleases(releases) { + UI.list.replaceChildren(); + if (!releases.length) { + renderEmpty(); + return; + } + releases.forEach((release) => renderEntry(release)); +} + +/** Opens the changelog modal and moves focus to its close button. */ +function openChangelog(releases) { + renderReleases(releases); + UI.overlay.hidden = false; + UI.closeBtn?.focus(); +} + +/** Closes the modal and records the current version as seen. */ +async function closeChangelog() { + UI.overlay.hidden = true; + await window.electronAPI.markChangelogSeen(); +} + +/** Loads changelog data and opens the modal when unread releases exist. */ +async function loadChangelog() { + + const features = await window.electronAPI.getFeatures(); + + try { + changelogData = await window.electronAPI.getChangelog(); + + if (!changelogData.error && changelogData.hasUnread && features.showChanglogsAfterUpdate === true) { + openChangelog(changelogData.unreadReleases); + window.electronAPI.logInfo("Showing Changelog"); + } + } catch (error) { + console.error("Unable to load changelog:", error); + } +} + +/** Handles a click on the button that opens the changelog. */ +async function handleOpenClick() { + if (!changelogData || changelogData.error) { + changelogData = await window.electronAPI.getChangelog(); + } + + if (changelogData.error) { + window.showError("Unable to load changelog. Please check your network connection."); + return; + } + + openChangelog(changelogData.releases); +} + +/** Closes the modal when the user clicks on the overlay background. */ +function handleOverlayClick(event) { + if (event.target === UI.overlay) closeChangelog(); +} + +/** Closes the modal when Escape is pressed. */ +function handleKeydown(event) { + if (event.key === "Escape" && !UI.overlay.hidden) closeChangelog(); +} + +/** Registers all changelog modal event listeners. */ +function bindEvents() { + UI.openBtn?.addEventListener("click", handleOpenClick); + UI.closeBtn?.addEventListener("click", closeChangelog); + UI.overlay?.addEventListener("click", handleOverlayClick); + document.addEventListener("keydown", handleKeydown); +} + +/** Initializes the changelog data flow and modal interactions. */ +function init() { + bindEvents(); + loadChangelog(); +} + +init(); \ No newline at end of file diff --git a/public/script/fetchinfo.js b/public/script/fetchinfo.js index 2d79d67..6893ea1 100644 --- a/public/script/fetchinfo.js +++ b/public/script/fetchinfo.js @@ -269,7 +269,7 @@ async function init() { infoDiv.classList.add("visible"); }); - }) + }); } diff --git a/public/script/reportBug.js b/public/script/reportBug.js new file mode 100644 index 0000000..650174c --- /dev/null +++ b/public/script/reportBug.js @@ -0,0 +1,165 @@ +class BugReportModal { + constructor() { + this.modal = null; + this.form = null; + this.init(); + } + + init() { + this.modal = document.createElement("div"); + this.modal.className = "bug-modal-overlay"; + this.modal.style.cssText = "display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); justify-content:center; align-items:center; z-index:99999; font-family:sans-serif;"; + + this.modal.innerHTML = ` +
+

Report a Bug

+
+
+ + +
+
+ + +
+
+ + +
+
+ + Checking bug report service... +
+
+ + +
+
+
+ `; + + document.body.appendChild(this.modal); + this.form = this.modal.querySelector("#bugReportForm"); + + this.bindEvents(); + } + + bindEvents() { + const titleInput = this.modal.querySelector("#bugTitle"); + const descInput = this.modal.querySelector("#bugDescription"); + const sendButton = this.modal.querySelector("#bugSendBtn"); + + titleInput.addEventListener("input", () => { + localStorage.setItem("draft_bug_title", titleInput.value); + }); + + descInput.addEventListener("input", () => { + localStorage.setItem("draft_bug_desc", descInput.value); + }); + + this.modal.querySelector("#bugCancelBtn").addEventListener("click", () => { + this.close(); + }); + + this.modal.addEventListener("click", (e) => { + if (e.target === this.modal) { + this.close(); + } + }); + + this.form.addEventListener("submit", async (e) => { + e.preventDefault(); + if (sendButton.disabled) { + return; + } + + const data = { + title: titleInput.value, + description: descInput.value, + includeLogs: this.modal.querySelector("#bugIncludeLogs").value + }; + + window.electronAPI.logInfo("Submitting:", data); + + try { + const success = await window.topbarAPI.sendReport(data); + + if (success) { + localStorage.removeItem("draft_bug_title"); + localStorage.removeItem("draft_bug_desc"); + this.form.reset(); + this.close(); + window.showSuccess("Bug report sent successfully!"); + } + } catch (err) { + window.showError("Failed to send bug report."); + window.electronAPI.logError("Failed to send bug report:", err.message); + } + }); + } + + loadState() { + const savedTitle = localStorage.getItem("draft_bug_title"); + const savedDesc = localStorage.getItem("draft_bug_desc"); + + if (savedTitle) { + this.modal.querySelector("#bugTitle").value = savedTitle; + } + if (savedDesc) { + this.modal.querySelector("#bugDescription").value = savedDesc; + } + } + + open() { + this.loadState(); + this.modal.style.display = "flex"; + this.updateServiceStatus(); + } + + close() { + this.modal.style.display = "none"; + } + + async updateServiceStatus() { + const statusText = this.modal.querySelector("#bugReportServiceStatusText"); + const statusDot = this.modal.querySelector("#bugReportServiceStatusDot"); + const sendButton = this.modal.querySelector("#bugSendBtn"); + + sendButton.disabled = true; + sendButton.style.setProperty("cursor", "not-allowed", "important"); + sendButton.style.setProperty("opacity", "0.5", "important"); + + try { + const serviceStatus = await window.topbarAPI.getReportServiceStatus(); + const serviceAvailable = serviceStatus.status === "online"; + const statusVisible = !serviceAvailable; + statusText.textContent = serviceStatus.message; + statusDot.style.background = "#ef4444"; + statusText.style.color = "#fca5a5"; + this.modal.querySelector("#bugReportServiceStatus").style.setProperty( + "display", + statusVisible ? "flex" : "none", + "important" + ); + sendButton.disabled = !serviceAvailable; + sendButton.style.setProperty("cursor", serviceAvailable ? "pointer" : "not-allowed", "important"); + sendButton.style.setProperty("opacity", serviceAvailable ? "1" : "0.5", "important"); + } catch (err) { + statusText.textContent = "Unable to check bug report service."; + statusDot.style.background = "#ef4444"; + statusText.style.color = "#fca5a5"; + this.modal.querySelector("#bugReportServiceStatus").style.setProperty("display", "flex", "important"); + sendButton.disabled = true; + window.electronAPI.logError("Failed to retrieve bug report service status:", err.message); + } + } +} + +window.bugModal = new BugReportModal(); + +document.getElementById("report-bug-btn").addEventListener("click", () => { + window.bugModal.open(); +}); \ No newline at end of file diff --git a/public/script/settingsPanel.js b/public/script/settingsPanel.js index 528679d..37c435e 100644 --- a/public/script/settingsPanel.js +++ b/public/script/settingsPanel.js @@ -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"); } /** diff --git a/public/script/toast.js b/public/script/toast.js index 56afa60..d4b6406 100644 --- a/public/script/toast.js +++ b/public/script/toast.js @@ -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 = ` - ${icon} + ${icon} ${message} +
`; const closeBtn = toast.querySelector(".toast-close"); diff --git a/public/script/topbar.js b/public/script/topbar.js index c8ad591..e2a601d 100644 --- a/public/script/topbar.js +++ b/public/script/topbar.js @@ -35,25 +35,26 @@ export function initTopBar() { } -async function attachListeners(){ - try{ - const featuresList = await window.electronAPI.getFeatures(); +//* Disabled because i disabled native topbar for now, but keeping the code in case i want to re-enable it later +// async function attachListeners(){ +// try{ +// const featuresList = await window.electronAPI.getFeatures(); - if (!featuresList.customTopBar) { - const topbar = document.getElementById("topbar"); - const container = document.getElementById("container"); - const themeSwitcher = document.getElementById("theme-switcher"); +// if (!featuresList.customTopBar) { +// const topbar = document.getElementById("topbar"); +// const container = document.getElementById("container"); +// const themeSwitcher = document.getElementById("theme-switcher"); - if (topbar) topbar.style.display = "none"; - if (container) container.style.marginTop = "0"; - if (themeSwitcher) themeSwitcher.style.top = "30px"; - } +// if (topbar) topbar.style.display = "none"; +// if (container) container.style.marginTop = "0"; +// if (themeSwitcher) themeSwitcher.style.top = "30px"; +// } - } catch (error) { - console.error("Failed to load layout features for topbar:", error); - } +// } catch (error) { +// console.error("Failed to load layout features for topbar:", error); +// } -} +// } initTopBar() -document.addEventListener("DOMContentLoaded", attachListeners) \ No newline at end of file +//document.addEventListener("DOMContentLoaded", attachListeners) \ No newline at end of file diff --git a/public/styles/components/autoUpdate.css b/public/styles/components/autoUpdate.css new file mode 100644 index 0000000..b19fcac --- /dev/null +++ b/public/styles/components/autoUpdate.css @@ -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; +} diff --git a/public/styles/components/changelog.css b/public/styles/components/changelog.css new file mode 100644 index 0000000..d6b3b28 --- /dev/null +++ b/public/styles/components/changelog.css @@ -0,0 +1,229 @@ +.changelog-overlay { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(5, 10, 16, 0.72); + backdrop-filter: blur(8px); +} + +.changelog-overlay[hidden] { + display: none; +} + +.changelog-modal { + display: flex; + flex-direction: column; + width: min(680px, 100%); + max-height: min(720px, 88vh); + overflow: hidden; + color: var(--settings-text-color, #f8fafc); + background: var(--settings-modal-bg-color, #18212b); + border: 1px solid rgba(148, 163, 184, 0.28); + border-radius: 14px; + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45); +} + +.changelog-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; + padding: 24px 28px 18px; + border-bottom: 1px solid rgba(148, 163, 184, 0.18); +} + +.changelog-eyebrow { + color: var(--fill-accent, #0ea5e9); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.changelog-header h2 { + margin: 5px 0 0; + font-size: 26px; +} + +.changelog-close { + width: 34px; + height: 34px; + padding: 0; + color: inherit; + font-size: 24px; + line-height: 1; + background: transparent; + border: 1px solid rgba(148, 163, 184, 0.28); + border-radius: 8px; + cursor: pointer; +} + +.changelog-close:hover { + background: rgba(148, 163, 184, 0.14); +} + +.changelog-list { + overflow-y: auto; + padding: 20px 28px 28px; + scrollbar-color: var(--fill-accent, #0ea5e9) transparent; + scrollbar-width: thin; +} + +.changelog-list::-webkit-scrollbar, +.changelog-body pre::-webkit-scrollbar { + width: 7px; + height: 7px; +} + +.changelog-list::-webkit-scrollbar-track, +.changelog-body pre::-webkit-scrollbar-track { + background: transparent; +} + +.changelog-list::-webkit-scrollbar-thumb, +.changelog-body pre::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--fill-accent, #0ea5e9) 70%, transparent); + border: 2px solid transparent; + border-radius: 999px; + background-clip: padding-box; +} + +.changelog-list::-webkit-scrollbar-thumb:hover, +.changelog-body pre::-webkit-scrollbar-thumb:hover { + background: var(--fill-accent, #0ea5e9); + background-clip: padding-box; +} + +.changelog-entry { + padding: 18px 0; + border-bottom: 1px solid rgba(148, 163, 184, 0.16); +} + +.changelog-entry:first-child { + padding-top: 0; +} + +.changelog-entry:last-child { + border-bottom: 0; +} + +.changelog-entry-heading { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; +} + +.changelog-entry-heading h3 { + margin: 0; + font-size: 17px; +} + +.changelog-entry-heading span { + flex-shrink: 0; + color: var(--settings-subtitle-color, #94a3b8); + font-size: 12px; +} + +.changelog-body { + margin: 12px 0 0; + color: var(--settings-subtitle-color, #cbd5e1); + font-size: 13px; + line-height: 1.55; + overflow-wrap: anywhere; +} + +.changelog-body p { + margin: 0 0 10px; +} + +.changelog-body p:last-child { + margin-bottom: 0; +} + +.changelog-body h1, +.changelog-body h2, +.changelog-body h3, +.changelog-body h4, +.changelog-body h5, +.changelog-body h6 { + margin: 14px 0 8px; + color: var(--settings-text-color, #f8fafc); +} + +.changelog-body h1 { font-size: 20px; } +.changelog-body h2 { font-size: 18px; } +.changelog-body h3 { font-size: 16px; } +.changelog-body h4, +.changelog-body h5, +.changelog-body h6 { font-size: 14px; } + +.changelog-body ul, +.changelog-body ol { + margin: 8px 0 12px; + padding-left: 22px; +} + +.changelog-body li + li { + margin-top: 4px; +} + +.changelog-body code { + padding: 2px 5px; + color: var(--settings-text-color, #f8fafc); + background: rgba(148, 163, 184, 0.16); + border-radius: 4px; + font-size: 12px; +} + +.changelog-body pre { + padding: 10px 12px; + overflow-x: auto; + background: rgba(15, 23, 42, 0.45); + border-radius: 6px; +} + +.changelog-body pre code { + padding: 0; + background: transparent; + white-space: pre; +} + +.changelog-body blockquote { + margin: 10px 0; + padding-left: 12px; + color: var(--settings-subtitle-color, #cbd5e1); + border-left: 3px solid var(--fill-accent, #0ea5e9); +} + +.changelog-body a { + color: var(--fill-accent, #0ea5e9); +} + +.changelog-loading, +.changelog-empty { + margin: 0; + color: var(--settings-subtitle-color, #cbd5e1); +} + +@media (max-width: 780px) { + .changelog-overlay { + padding: 12px; + } + + .changelog-header, + .changelog-list { + padding-left: 18px; + padding-right: 18px; + } + + .changelog-entry-heading { + align-items: flex-start; + flex-direction: column; + gap: 5px; + } +} diff --git a/public/styles/components/toast.css b/public/styles/components/toast.css index 5972c0b..efde624 100644 --- a/public/styles/components/toast.css +++ b/public/styles/components/toast.css @@ -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; } -} +} \ No newline at end of file diff --git a/public/styles/layout/settingsPanel.css b/public/styles/layout/settingsPanel.css index 32b81e0..1c26f09 100644 --- a/public/styles/layout/settingsPanel.css +++ b/public/styles/layout/settingsPanel.css @@ -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 { diff --git a/public/styles/layout/topbar.css b/public/styles/layout/topbar.css index 158d8d2..1b06239 100644 --- a/public/styles/layout/topbar.css +++ b/public/styles/layout/topbar.css @@ -14,8 +14,8 @@ .custom-controls { display: flex; align-items: center; - gap: 6px; - padding-left: 12px; + gap: 4px; + padding-left: 10px; } /* RIGHT - window buttons */ @@ -43,11 +43,11 @@ /* Custom buttons (Logs / Wiki / etc.) */ .custom-controls button { - height: 32px; - padding: 0 14px; + height: 28px; + padding: 0 10px; - font-size: 15px; - border-radius: 8px; + font-size: 14px; + border-radius: 7px; } .custom-controls button:hover { diff --git a/public/styles/styles.css b/public/styles/styles.css index 948263a..29067ee 100644 --- a/public/styles/styles.css +++ b/public/styles/styles.css @@ -12,6 +12,8 @@ @import url("components/editpathbutton.css"); @import url("components/progressBar.css"); @import url("components/toast.css"); +@import url("components/autoUpdate.css"); +@import url("components/changelog.css"); @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); diff --git a/scripts/CHANGELOG.md b/scripts/CHANGELOG.md deleted file mode 100644 index f5c3c0d..0000000 --- a/scripts/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -[//]: # (This file is used for the release CHANGELOG. Please use Markdown to describe changes.) \ No newline at end of file diff --git a/scripts/RELEASE-CHANGELOG.md b/scripts/RELEASE-CHANGELOG.md new file mode 100644 index 0000000..085c067 --- /dev/null +++ b/scripts/RELEASE-CHANGELOG.md @@ -0,0 +1,24 @@ +### Features +- **In-App Feedback**: Added a built-in feedback form allowing users to send feedback, bug reports, and daily logs directly to the development team. +- **Community Contributions**: GitHub issues are now better maintained on the repository. If you want to contribute to the project, feel free to drop by and check out the `good first issue` tags! +- **Changelog**: The changelog is now automatically displayed after an update (can be disabled if preferred). + +### Improvements +- **Bundle Size**: Optimized assets and dependencies, reducing the overall application bundle size by approximately 30% for specific versions. +- **Updater UI**: Reworked the update notification for better clarity. Windows and AppImage users can download updates seamlessly in-app, while unsupported Linux package users are gracefully redirected to the manual download page. Flatpak and Snap users are not prompted, as updates are handled natively by their respective package managers. +- **Toast Notifications**: Refactored the style and overall UX of toast alerts. +- **Code Quality**: Implemented a linter to enforce coding standards. + +### Fixes +- **Auto-Updater**: Fixed a major issue causing `404 Not Found` errors when attempting to download updates for `.AppImage` releases. +- **Linux Updates**: Fixed update crashes on `.rpm` and `.snap` packages. In-app update flows are now disabled for these formats to let the OS handle the process natively. +- **Windows Logs**: Fixed a file naming issue with the log transport system on Windows. +- **Desktop Entry Name**: Fixed the desktop entry name for Linux users, ensuring it displays correctly in application menus. +- **Custom Top Bar**: Removed the `customTopBar` settings. +- **Auto Update**: Removed the `autoAppUpdate` settings. +- **UI (System Tray)**: Removed the ***[EXPERIMENTAL]*** label from the config panel. + +### Chores +- **Dependencies**: Removed `dotenv` from the project. +- **Website**: A brand new website URL is now available: [freedomloader.acnolo.fr](https://freedomloader.acnolo.fr) +- **Dev**: Added a new npm script to check Linux file quality. \ No newline at end of file diff --git a/scripts/create-copr-package.sh b/scripts/create-copr-package.sh index 5bb8161..e1303c9 100755 --- a/scripts/create-copr-package.sh +++ b/scripts/create-copr-package.sh @@ -54,20 +54,6 @@ exec /opt/freedom-loader/freedom-loader "$@" EOF chmod +x "$TMP_WRAPPER/freedom-loader" -# Desktop entry file -TMP_DESKTOP="$ROOT_DIR/.rpm-pkg-tmp-desktop" -mkdir -p "$TMP_DESKTOP" -cat > "$TMP_DESKTOP/freedom-loader.desktop" << 'EOF' -[Desktop Entry] -Name=Freedom Loader -Exec=/opt/freedom-loader/freedom-loader %U -Icon=freedom-loader -Type=Application -Categories=AudioVideo;Utility;Network; -Comment=Free and open-source GUI for yt-dlp -StartupWMClass=Freedom Loader -EOF - # ------------------------------------------------------------ # Step 1 - Build .rpm from linux-unpacked via fpm # ------------------------------------------------------------ @@ -78,7 +64,7 @@ echo "Building RPM..." --version "$VERSION" \ --architecture x86_64 \ --description "Free and open-source GUI for yt-dlp" \ - --url "https://masteracnolo.github.io/Freedom-Loader-Site/" \ + --url "https://freedomloader.acnolo.fr/" \ --maintainer "MasterAcnolo " \ --license "GPL-3.0-only" \ --depends gtk3 \ @@ -89,11 +75,10 @@ echo "Building RPM..." --package "$RPM_OUT" \ "$ROOT_DIR/dist/linux-unpacked/=/opt/freedom-loader" \ "$TMP_WRAPPER/freedom-loader=/usr/bin/freedom-loader" \ - "$TMP_DESKTOP/freedom-loader.desktop=/usr/share/applications/freedom-loader.desktop" \ "$ROOT_DIR/build/app-icon.png=/usr/share/icons/hicolor/512x512/apps/freedom-loader.png" \ - "$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml" - -rm -rf "$TMP_WRAPPER" "$TMP_DESKTOP" + "$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml" \ + "$ROOT_DIR/package/com.masteracnolo.freedomloader.desktop=/usr/share/applications/com.masteracnolo.freedomloader.desktop" +rm -rf "$TMP_WRAPPER" echo "RPM built: $RPM_OUT" # ------------------------------------------------------------ diff --git a/scripts/release.sh b/scripts/release.sh index 0816e80..4c4758a 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -67,11 +67,11 @@ echo "Workspace cleaned (dist/ and srpm-out/ removed)" # ------------------------------------------------------------ # Step 0.3 - Retrieve Changelog # ------------------------------------------------------------ -if [ ! -f "$ROOT_DIR/CHANGELOG.md" ]; then - echo "Warning: CHANGELOG.md not found, release notes will be empty." +if [ ! -f "$ROOT_DIR/scripts/RELEASE-CHANGELOG.md" ]; then + echo "Warning: RELEASE-CHANGELOG.md not found, release notes will be empty." CHANGELOG="" else - CHANGELOG="$(cat "$ROOT_DIR/CHANGELOG.md")" + CHANGELOG="$(cat "$ROOT_DIR/scripts/RELEASE-CHANGELOG.md")" echo "Changelog retrieved successfully" fi # ------------------------------------------------------------ @@ -132,8 +132,8 @@ if [ "$DRY_RUN" = false ]; then "$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe" \ "$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe.blockmap" \ "$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm" \ - "$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.deb" \ - "$ROOT_DIR/dist/Freedom Loader-${VERSION}.AppImage" \ + "$ROOT_DIR/dist/Freedom-Loader_${VERSION}_amd64.deb" \ + "$ROOT_DIR/dist/Freedom-Loader-${VERSION}.AppImage" \ "$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \ "$ROOT_DIR/dist/latest.yml" \ "$ROOT_DIR/dist/latest-linux.yml" diff --git a/server/helpers/buildArgs.helpers.js b/server/helpers/buildArgs.helpers.js index 75ad30f..0f1991d 100644 --- a/server/helpers/buildArgs.helpers.js +++ b/server/helpers/buildArgs.helpers.js @@ -67,9 +67,7 @@ function buildYtDlpArgs({url, audioOnly, quality, outputFolder, isPlaylist}) { logger.info("--- CONFIGURATION ---"); - logger.info(`CONFIG autoUpdate: ${configFeatures.autoUpdate}`); logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`); - logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`); logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`); logger.info(`CONFIG keepPlaylistOrder: ${configFeatures.keepPlaylistOrder}`); logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`); diff --git a/test/unit/buildArgs.test.js b/test/unit/buildArgs.test.js index 4742a57..920de22 100644 --- a/test/unit/buildArgs.test.js +++ b/test/unit/buildArgs.test.js @@ -11,7 +11,6 @@ jest.mock("../../server/helpers/path.helpers.js", () => ({ jest.mock("../../config.js", () => ({ configFeatures: { - autoUpdate: false, discordRPC: false, customTopBar: false, autoCheckInfo: false,