diff --git a/build/Assets.car b/build/Assets.car new file mode 100644 index 0000000..60c0fe0 Binary files /dev/null and b/build/Assets.car differ diff --git a/build/background.tiff b/build/background.tiff index a0e3f7f..1306d07 100644 Binary files a/build/background.tiff and b/build/background.tiff differ diff --git a/build/icon.icns b/build/icon.icns index 34d8371..6b3e94f 100644 Binary files a/build/icon.icns and b/build/icon.icns differ diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000..9e06b43 Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png deleted file mode 100644 index 027ab0b..0000000 Binary files a/build/icon.png and /dev/null differ diff --git a/build/icon.svg b/build/icon.svg new file mode 100644 index 0000000..3bcbd11 --- /dev/null +++ b/build/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package.json b/package.json index 2a7bb8a..a41998c 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "build": { "appId": "xyz.defautluser0.notnextop", "productName": "Not-Nextop", + "executableName": "not-nextop", "files": [ "!*", "!node_modules", @@ -79,9 +80,10 @@ "discord" ] }, - "beforePack": "scripts/build/sandboxFix.js", + "beforePack": "scripts/build/beforePack.mjs", + "afterPack": "scripts/build/afterPack.mjs", "linux": { - "icon": "build/icon.icns", + "icon": "build/icon.svg", "category": "Network", "maintainer": "jaegerwald.dev+nexop@gmail.com", "target": [ @@ -141,7 +143,8 @@ "NSMicrophoneUsageDescription": "This app needs access to the microphone", "NSCameraUsageDescription": "This app needs access to the camera", "com.apple.security.device.audio-input": true, - "com.apple.security.device.camera": true + "com.apple.security.device.camera": true, + "CFBundleIconName": "Icon" }, "notarize": true }, @@ -171,6 +174,7 @@ "oneClick": false }, "win": { + "icon": "build/icon.ico", "target": [ { "target": "nsis", diff --git a/scripts/build/addAssetsCar.mjs b/scripts/build/addAssetsCar.mjs new file mode 100644 index 0000000..123b61b --- /dev/null +++ b/scripts/build/addAssetsCar.mjs @@ -0,0 +1,24 @@ +import { copyFile, readdir } from "fs/promises"; + +/** + * @param {{ + * readonly appOutDir: string; + * readonly arch: Arch; + * readonly electronPlatformName: string; + * readonly outDir: string; + * readonly packager: PlatformPackager; + * readonly targets: Target[]; + * }} context + */ +export async function addAssetsCar({ appOutDir }) { + if (process.platform !== "darwin") return; + + const appName = (await readdir(appOutDir)).find(item => item.endsWith(".app")); + + if (!appName) { + console.warn(`Could not find .app directory in ${appOutDir}. Skipping adding assets.car`); + return; + } + + await copyFile("build/Assets.car", `${appOutDir}/${appName}/Contents/Resources/Assets.car`); +} diff --git a/scripts/build/afterPack.mjs b/scripts/build/afterPack.mjs new file mode 100644 index 0000000..6a446d6 --- /dev/null +++ b/scripts/build/afterPack.mjs @@ -0,0 +1,5 @@ +import { addAssetsCar } from "./addAssetsCar.mjs"; + +export default async function afterPack(context) { + await addAssetsCar(context); +} diff --git a/scripts/build/beforePack.mjs b/scripts/build/beforePack.mjs new file mode 100644 index 0000000..5b1fa38 --- /dev/null +++ b/scripts/build/beforePack.mjs @@ -0,0 +1,5 @@ +import { applyAppImageSandboxFix } from "./sandboxFix.mjs"; + +export default async function beforePack() { + await applyAppImageSandboxFix(); +} diff --git a/scripts/build/sandboxFix.js b/scripts/build/sandboxFix.mjs similarity index 91% rename from scripts/build/sandboxFix.js rename to scripts/build/sandboxFix.mjs index ff04732..03f92e4 100644 --- a/scripts/build/sandboxFix.js +++ b/scripts/build/sandboxFix.mjs @@ -6,18 +6,21 @@ // Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js -const fs = require("fs/promises"); -const path = require("path"); +import fs from "fs/promises"; +import path from "path"; +import AppImageTarget from "app-builder-lib/out/targets/AppImageTarget.js"; + let isApplied = false; -const hook = async () => { - if (isApplied) return; - isApplied = true; +export async function applyAppImageSandboxFix() { if (process.platform !== "linux") { // this fix is only required on linux return; } - const AppImageTarget = require("app-builder-lib/out/targets/AppImageTarget"); + + if (isApplied) return; + isApplied = true; + const oldBuildMethod = AppImageTarget.default.prototype.build; AppImageTarget.default.prototype.build = async function (...args) { console.log("Running AppImage builder hook", args); @@ -69,6 +72,4 @@ exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ] return ret; }; -}; - -module.exports = hook; +} diff --git a/src/main/about.ts b/src/main/about.ts index bed2f34..3e3e5f0 100644 --- a/src/main/about.ts +++ b/src/main/about.ts @@ -6,7 +6,7 @@ import { app, BrowserWindow } from "electron"; import { join } from "path"; -import { ICON_PATH, VIEW_DIR } from "shared/paths"; +import { VIEW_DIR } from "shared/paths"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; @@ -17,7 +17,6 @@ export async function createAboutWindow() { const about = new BrowserWindow({ center: true, autoHideMenuBar: true, - icon: ICON_PATH, height, width }); diff --git a/src/main/appBadge.ts b/src/main/appBadge.ts index 66ad70b..969caf3 100644 --- a/src/main/appBadge.ts +++ b/src/main/appBadge.ts @@ -8,6 +8,8 @@ import { app, NativeImage, nativeImage } from "electron"; import { join } from "path"; import { BADGE_DIR } from "shared/paths"; +import { AppEvents } from "./events"; + const imgCache = new Map(); function loadBadge(index: number) { const cached = imgCache.get(index); @@ -21,7 +23,13 @@ function loadBadge(index: number) { let lastIndex: null | number = -1; +/** + * -1 = show unread indicator + * 0 = clear + */ export function setBadgeCount(count: number) { + AppEvents.emit("setTrayVariant", count !== 0 ? "trayUnread" : "tray"); + switch (process.platform) { case "linux": if (count === -1) count = 0; diff --git a/src/main/events.ts b/src/main/events.ts new file mode 100644 index 0000000..f331a53 --- /dev/null +++ b/src/main/events.ts @@ -0,0 +1,15 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { EventEmitter } from "events"; + +import { UserAssetType } from "./userAssets"; + +export const AppEvents = new EventEmitter<{ + appLoaded: []; + userAssetChanged: [UserAssetType]; + setTrayVariant: ["tray" | "trayUnread"]; +}>(); diff --git a/src/main/firstLaunch.ts b/src/main/firstLaunch.ts index 69e01a0..4ba335a 100644 --- a/src/main/firstLaunch.ts +++ b/src/main/firstLaunch.ts @@ -9,7 +9,7 @@ import { BrowserWindow } from "electron/main"; import { copyFileSync, mkdirSync, readdirSync } from "fs"; import { join } from "path"; import { SplashProps } from "shared/browserWinProperties"; -import { ICON_PATH, VIEW_DIR } from "shared/paths"; +import { VIEW_DIR } from "shared/paths"; import { autoStart } from "./autoStart"; import { DATA_DIR } from "./constants"; @@ -32,8 +32,7 @@ export function createFirstLaunchTour() { frame: true, autoHideMenuBar: true, height: 470, - width: 550, - icon: ICON_PATH + width: 550 }); makeLinksOpenExternally(win); diff --git a/src/main/index.ts b/src/main/index.ts index 3dda0cc..4344e0c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,6 +5,7 @@ */ import "./ipc"; +import "./userAssets"; import { app, BrowserWindow, nativeTheme } from "electron"; import { autoUpdater } from "electron-updater"; @@ -15,6 +16,7 @@ import { createWindows, mainWin } from "./mainWindow"; import { registerMediaPermissionsHandler } from "./mediaPermissions"; import { registerScreenShareHandler } from "./screenShare"; import { Settings, State } from "./settings"; +import { setAsDefaultProtocolClient } from "./utils/setAsDefaultProtocolClient"; import { isDeckGameMode } from "./utils/steamOS"; if (!IS_DEV) { @@ -31,7 +33,7 @@ const isLinux = process.platform === "linux"; export let enableHardwareAcceleration = true; function init() { - app.setAsDefaultProtocolClient("discord"); + setAsDefaultProtocolClient("discord"); const { disableSmoothScroll, hardwareAcceleration, hardwareVideoAcceleration } = Settings.store; diff --git a/src/main/mainWindow.ts b/src/main/mainWindow.ts index 0d5457a..9269811 100644 --- a/src/main/mainWindow.ts +++ b/src/main/mainWindow.ts @@ -1,52 +1,44 @@ /* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience - * Copyright (c) 2023 Vendicated and Vencord contributors + + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ import { app, BrowserWindow, BrowserWindowConstructorOptions, - dialog, Menu, MenuItemConstructorOptions, nativeTheme, screen, - session, - Tray + session } from "electron"; -import { EventEmitter } from "events"; -import { rm } from "fs/promises"; import { join } from "path"; import { IpcCommands, IpcEvents } from "shared/IpcEvents"; import { isTruthy } from "shared/utils/guards"; import { once } from "shared/utils/once"; import type { SettingsStore } from "shared/utils/SettingsStore"; -import { ICON_PATH } from "../shared/paths"; import { createAboutWindow } from "./about"; import { initArRPC } from "./arrpc"; -import { - BrowserUserAgent, - DATA_DIR, - DEFAULT_HEIGHT, - DEFAULT_WIDTH, - MessageBoxChoice, - MIN_HEIGHT, - MIN_WIDTH, - VENCORD_FILES_DIR -} from "./constants"; +import { BrowserUserAgent, DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH, VENCORD_FILES_DIR } from "./constants"; +import { AppEvents } from "./events"; import { darwinURL } from "./index"; import { sendRendererCommand } from "./ipcCommands"; import { Settings, State, VencordSettings } from "./settings"; import { createSplashWindow, updateSplashMessage } from "./splash"; +import { destroyTray, initTray } from "./tray"; +import { clearData } from "./utils/clearData"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS"; import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader"; let isQuitting = false; -let tray: Tray; applyDeckKeyboardFix(); @@ -77,84 +69,6 @@ function makeSettingsListenerHelpers(o: SettingsStore) { const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpers(Settings); const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings); -function initTray(win: BrowserWindow) { - const onTrayClick = () => { - if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide(); - else win.show(); - }; - const trayMenu = Menu.buildFromTemplate([ - { - label: "Open", - click() { - win.show(); - } - }, - { - label: "About", - click: createAboutWindow - }, - { - label: "Repair Not-Nexulien", - async click() { - await downloadVencordFiles(); - app.relaunch(); - app.quit(); - } - }, - { - label: "Reset Not-Nextop", - async click() { - await clearData(win); - } - }, - { - type: "separator" - }, - { - label: "Restart", - click() { - app.relaunch(); - app.quit(); - } - }, - { - label: "Quit", - click() { - isQuitting = true; - app.quit(); - } - } - ]); - - tray = new Tray(ICON_PATH); - tray.setToolTip("Not-Nextop"); - tray.setContextMenu(trayMenu); - tray.on("click", onTrayClick); -} - -async function clearData(win: BrowserWindow) { - const { response } = await dialog.showMessageBox(win, { - message: "Are you sure you want to reset Not-Nextop?", - detail: "This will log you out, clear caches and reset all your settings!\n\nNot-Nextop will automatically restart after this operation.", - buttons: ["Yes", "No"], - cancelId: MessageBoxChoice.Cancel, - defaultId: MessageBoxChoice.Default, - type: "warning" - }); - - if (response === MessageBoxChoice.Cancel) return; - - win.close(); - - await win.webContents.session.clearStorageData(); - await win.webContents.session.clearCache(); - await win.webContents.session.clearCodeCaches({}); - await rm(DATA_DIR, { force: true, recursive: true }); - - app.relaunch(); - app.quit(); -} - type MenuItemList = Array; function initMenuBar(win: BrowserWindow) { @@ -333,8 +247,8 @@ function initWindowBoundsListeners(win: BrowserWindow) { function initSettingsListeners(win: BrowserWindow) { addSettingsListener("tray", enable => { - if (enable) initTray(win); - else tray?.destroy(); + if (enable) initTray(win, q => (isQuitting = q)); + else destroyTray(); }); addSettingsListener("disableMinSize", disable => { @@ -439,7 +353,6 @@ function createMainWindow() { // disable renderer backgrounding to prevent the app from unloading when in the background backgroundThrottling: false }, - icon: ICON_PATH, frame: !noFrame, ...(transparent && { transparent: true, @@ -477,7 +390,9 @@ function createMainWindow() { }); initWindowBoundsListeners(win); - if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win); + if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") + initTray(win, q => (isQuitting = q)); + initMenuBar(win); makeLinksOpenExternally(win); initSettingsListeners(win); @@ -497,8 +412,6 @@ function createMainWindow() { const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js"))); -const loadEvents = new EventEmitter(); - export function loadUrl(uri: string | undefined) { const branch = Settings.store.discordBranch; const subdomain = branch === "canary" || branch === "ptb" ? `${branch}.` : ""; @@ -506,7 +419,7 @@ export function loadUrl(uri: string | undefined) { // we do not rely on 'did-finish-load' because it fires even if loadURL fails which triggers early detruction of the splash mainWin .loadURL(`https://${subdomain}discord.com/${uri ? new URL(uri).pathname.slice(1) || "app" : "app"}`) - .then(() => loadEvents.emit("app-loaded")) + .then(() => AppEvents.emit("appLoaded")) .catch(error => retryUrl(error.url, error.code)); } @@ -533,7 +446,7 @@ export async function createWindows() { mainWin = createMainWindow(); - loadEvents.on("app-loaded", () => { + AppEvents.on("appLoaded", () => { splash?.destroy(); if (!startMinimized) { diff --git a/src/main/splash.ts b/src/main/splash.ts index 79add0f..58f4785 100644 --- a/src/main/splash.ts +++ b/src/main/splash.ts @@ -7,7 +7,7 @@ import { BrowserWindow } from "electron"; import { join } from "path"; import { SplashProps } from "shared/browserWinProperties"; -import { ICON_PATH, VIEW_DIR } from "shared/paths"; +import { VIEW_DIR } from "shared/paths"; import { Settings } from "./settings"; @@ -16,7 +16,6 @@ let splash: BrowserWindow | undefined; export function createSplashWindow(startMinimized = false) { splash = new BrowserWindow({ ...SplashProps, - icon: ICON_PATH, show: !startMinimized, webPreferences: { preload: join(__dirname, "splashPreload.js") diff --git a/src/main/tray.ts b/src/main/tray.ts new file mode 100644 index 0000000..f58a719 --- /dev/null +++ b/src/main/tray.ts @@ -0,0 +1,92 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { app, BrowserWindow, Menu, Tray } from "electron"; + +import { createAboutWindow } from "./about"; +import { AppEvents } from "./events"; +import { Settings } from "./settings"; +import { resolveAssetPath } from "./userAssets"; +import { clearData } from "./utils/clearData"; +import { downloadVencordFiles } from "./utils/vencordLoader"; + +let tray: Tray; +let trayVariant: "tray" | "trayUnread" = "tray"; + +AppEvents.on("userAssetChanged", async asset => { + if (tray && (asset === "tray" || asset === "trayUnread")) { + tray.setImage(await resolveAssetPath(trayVariant)); + } +}); + +AppEvents.on("setTrayVariant", async variant => { + if (trayVariant === variant) return; + + trayVariant = variant; + if (!tray) return; + + tray.setImage(await resolveAssetPath(trayVariant)); +}); + +export function destroyTray() { + tray?.destroy(); +} + +export async function initTray(win: BrowserWindow, setIsQuitting: (val: boolean) => void) { + const onTrayClick = () => { + if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide(); + else win.show(); + }; + + const trayMenu = Menu.buildFromTemplate([ + { + label: "Open", + click() { + win.show(); + } + }, + { + label: "About", + click: createAboutWindow + }, + { + label: "Repair Vencord", + async click() { + await downloadVencordFiles(); + app.relaunch(); + app.quit(); + } + }, + { + label: "Reset Vesktop", + async click() { + await clearData(win); + } + }, + { + type: "separator" + }, + { + label: "Restart", + click() { + app.relaunch(); + app.quit(); + } + }, + { + label: "Quit", + click() { + setIsQuitting(true); + app.quit(); + } + } + ]); + + tray = new Tray(await resolveAssetPath(trayVariant)); + tray.setToolTip("Vesktop"); + tray.setContextMenu(trayMenu); + tray.on("click", onTrayClick); +} diff --git a/src/main/userAssets.ts b/src/main/userAssets.ts new file mode 100644 index 0000000..51219ce --- /dev/null +++ b/src/main/userAssets.ts @@ -0,0 +1,110 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { app, dialog, net, protocol } from "electron"; +import { copyFile, mkdir, rm } from "fs/promises"; +import { join } from "path"; +import { IpcEvents } from "shared/IpcEvents"; +import { STATIC_DIR } from "shared/paths"; +import { pathToFileURL } from "url"; + +import { DATA_DIR } from "./constants"; +import { AppEvents } from "./events"; +import { mainWin } from "./mainWindow"; +import { fileExistsAsync } from "./utils/fileExists"; +import { handle } from "./utils/ipcWrappers"; + +const CUSTOMIZABLE_ASSETS = ["splash", "tray", "trayUnread"] as const; +export type UserAssetType = (typeof CUSTOMIZABLE_ASSETS)[number]; + +const DEFAULT_ASSETS: Record = { + splash: "splash.webp", + tray: `tray/${process.platform === "darwin" ? "trayTemplate" : "tray"}.png`, + trayUnread: "tray/trayUnread.png" +}; + +const UserAssetFolder = join(DATA_DIR, "userAssets"); + +export async function resolveAssetPath(asset: UserAssetType) { + if (!CUSTOMIZABLE_ASSETS.includes(asset)) { + throw new Error(`Invalid asset: ${asset}`); + } + + const assetPath = join(UserAssetFolder, asset); + if (await fileExistsAsync(assetPath)) { + return assetPath; + } + + return join(STATIC_DIR, DEFAULT_ASSETS[asset]); +} + +app.whenReady().then(() => { + protocol.handle("vesktop", async req => { + if (!req.url.startsWith("vesktop://assets/")) { + return new Response(null, { status: 404 }); + } + + const asset = decodeURI(req.url) + .slice("vesktop://assets/".length) + .replace(/\?v=\d+$/, "") + .replace(/\/+$/, ""); + + // @ts-expect-error dumb types + if (!CUSTOMIZABLE_ASSETS.includes(asset)) { + return new Response(null, { status: 404 }); + } + + try { + const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href); + if (res.ok) return res; + } catch {} + + return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href); + }); +}); + +handle(IpcEvents.CHOOSE_USER_ASSET, async (_event, asset: UserAssetType, value?: null) => { + if (!CUSTOMIZABLE_ASSETS.includes(asset)) { + throw `Invalid asset: ${asset}`; + } + + const assetPath = join(UserAssetFolder, asset); + + if (value === null) { + try { + await rm(assetPath, { force: true }); + AppEvents.emit("userAssetChanged", asset); + return "ok"; + } catch (e) { + console.error(`Failed to remove user asset ${asset}:`, e); + return "failed"; + } + } + + const res = await dialog.showOpenDialog(mainWin, { + properties: ["openFile"], + title: `Select an image to use as ${asset}`, + defaultPath: app.getPath("pictures"), + filters: [ + { + name: "Images", + extensions: ["png", "jpg", "jpeg", "webp", "gif", "avif", "svg"] + } + ] + }); + + if (res.canceled || !res.filePaths.length) return "cancelled"; + + try { + await mkdir(UserAssetFolder, { recursive: true }); + await copyFile(res.filePaths[0], assetPath); + AppEvents.emit("userAssetChanged", asset); + return "ok"; + } catch (e) { + console.error(`Failed to copy user asset ${asset}:`, e); + return "failed"; + } +}); diff --git a/src/main/utils/clearData.ts b/src/main/utils/clearData.ts new file mode 100644 index 0000000..6ce5c89 --- /dev/null +++ b/src/main/utils/clearData.ts @@ -0,0 +1,32 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { app, BrowserWindow, dialog } from "electron"; +import { rm } from "fs/promises"; +import { DATA_DIR, MessageBoxChoice } from "main/constants"; + +export async function clearData(win: BrowserWindow) { + const { response } = await dialog.showMessageBox(win, { + message: "Are you sure you want to reset Vesktop?", + detail: "This will log you out, clear caches and reset all your settings!\n\nVesktop will automatically restart after this operation.", + buttons: ["Yes", "No"], + cancelId: MessageBoxChoice.Cancel, + defaultId: MessageBoxChoice.Default, + type: "warning" + }); + + if (response === MessageBoxChoice.Cancel) return; + + win.close(); + + await win.webContents.session.clearStorageData(); + await win.webContents.session.clearCache(); + await win.webContents.session.clearCodeCaches({}); + await rm(DATA_DIR, { force: true, recursive: true }); + + app.relaunch(); + app.quit(); +} diff --git a/src/main/utils/fileExists.ts b/src/main/utils/fileExists.ts new file mode 100644 index 0000000..ce73efb --- /dev/null +++ b/src/main/utils/fileExists.ts @@ -0,0 +1,13 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { access, constants } from "fs/promises"; + +export async function fileExistsAsync(path: string) { + return await access(path, constants.F_OK) + .then(() => true) + .catch(() => false); +} diff --git a/src/main/utils/setAsDefaultProtocolClient.ts b/src/main/utils/setAsDefaultProtocolClient.ts new file mode 100644 index 0000000..c4b3580 --- /dev/null +++ b/src/main/utils/setAsDefaultProtocolClient.ts @@ -0,0 +1,29 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { execFile } from "child_process"; +import { app } from "electron"; + +export async function setAsDefaultProtocolClient(protocol: string) { + if (process.platform !== "linux") { + return app.setAsDefaultProtocolClient(protocol); + } + + // electron setAsDefaultProtocolClient uses xdg-settings instead of xdg-mime. + // xdg-settings had a bug where it would also register the app as a handler for text/html, + // aka become your browser. This bug was fixed years ago (xdg-utils 1.2.0) but Ubuntu ships + // 7 (YES, SEVEN) years out of date xdg-utils which STILL has the bug. + // FIXME: remove this workaround when Ubuntu updates their xdg-utils or electron switches to xdg-mime. + + const { CHROME_DESKTOP } = process.env; + if (!CHROME_DESKTOP) return false; + + return new Promise(resolve => { + execFile("xdg-mime", ["default", CHROME_DESKTOP, `x-scheme-handler/${protocol}`], err => { + resolve(err == null); + }); + }); +} diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts index 1525965..5847f65 100644 --- a/src/preload/VesktopNative.ts +++ b/src/preload/VesktopNative.ts @@ -42,7 +42,9 @@ export const VesktopNative = { fileManager: { showItemInFolder: (path: string) => invoke(IpcEvents.SHOW_ITEM_IN_FOLDER, path), getVencordDir: () => sendSync(IpcEvents.GET_VENCORD_DIR), - selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value) + selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value), + chooseUserAsset: (asset: string, value?: null) => + invoke<"cancelled" | "invalid" | "ok" | "failed">(IpcEvents.CHOOSE_USER_ASSET, asset, value) }, settings: { get: () => sendSync(IpcEvents.GET_SETTINGS), diff --git a/src/renderer/components/settings/Settings.tsx b/src/renderer/components/settings/Settings.tsx index a85ee60..cee57e0 100644 --- a/src/renderer/components/settings/Settings.tsx +++ b/src/renderer/components/settings/Settings.tsx @@ -16,6 +16,7 @@ import { AutoStartToggle } from "./AutoStartToggle"; import { DeveloperOptionsButton } from "./DeveloperOptions"; import { DiscordBranchPicker } from "./DiscordBranchPicker"; import { NotificationBadgeToggle } from "./NotificationBadgeToggle"; +import { UserAssetsButton } from "./UserAssets"; import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch"; import { WindowsTransparencyControls } from "./WindowsTransparencyControls"; @@ -82,7 +83,8 @@ const SettingsOptions: Record> description: "Adapt the splash window colors to your custom theme", defaultValue: true }, - WindowsTransparencyControls + WindowsTransparencyControls, + UserAssetsButton ], Behaviour: [ { diff --git a/src/renderer/components/settings/UserAssets.css b/src/renderer/components/settings/UserAssets.css new file mode 100644 index 0000000..5ad8cb0 --- /dev/null +++ b/src/renderer/components/settings/UserAssets.css @@ -0,0 +1,25 @@ +.vcd-user-assets { + display: flex; + margin-block: 1em 2em; + flex-direction: column; + gap: 1em; +} + +.vcd-user-assets-asset { + display: flex; + margin-top: 0.5em; + align-items: center; + gap: 1em; +} + +.vcd-user-assets-actions { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +.vcd-user-assets-image { + height: 7.5em; + width: 7.5em; + object-fit: contain; +} \ No newline at end of file diff --git a/src/renderer/components/settings/UserAssets.tsx b/src/renderer/components/settings/UserAssets.tsx new file mode 100644 index 0000000..89b4cae --- /dev/null +++ b/src/renderer/components/settings/UserAssets.tsx @@ -0,0 +1,80 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vencord contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./UserAssets.css"; + +import { + ModalCloseButton, + ModalContent, + ModalHeader, + ModalRoot, + ModalSize, + openModal, + wordsFromCamel, + wordsToTitle +} from "@vencord/types/utils"; +import { Button, showToast, Text, useState } from "@vencord/types/webpack/common"; +import { UserAssetType } from "main/userAssets"; + +import { SettingsComponent } from "./Settings"; + +const CUSTOMIZABLE_ASSETS: UserAssetType[] = ["splash", "tray", "trayUnread"]; + +export const UserAssetsButton: SettingsComponent = () => { + return ; +}; + +function openAssetsModal() { + openModal(props => ( + + + + User Assets + + + + + +
+ {CUSTOMIZABLE_ASSETS.map(asset => ( + + ))} +
+
+
+ )); +} + +function Asset({ asset }: { asset: UserAssetType }) { + // cache busting + const [version, setVersion] = useState(Date.now()); + + const onChooseAsset = (value?: null) => async () => { + const res = await VesktopNative.fileManager.chooseUserAsset(asset, value); + if (res === "ok") { + setVersion(Date.now()); + } else if (res === "failed") { + showToast("Something went wrong. Please try again"); + } + }; + + return ( +
+ + {wordsToTitle(wordsFromCamel(asset))} + +
+ +
+ + +
+
+
+ ); +} diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 03126d2..2eaf2af 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -57,7 +57,9 @@ export const enum IpcEvents { IPC_COMMAND = "VCD_IPC_COMMAND", DEVTOOLS_OPENED = "VCD_DEVTOOLS_OPENED", - DEVTOOLS_CLOSED = "VCD_DEVTOOLS_CLOSED" + DEVTOOLS_CLOSED = "VCD_DEVTOOLS_CLOSED", + + CHOOSE_USER_ASSET = "VCD_CHOOSE_USER_ASSET" } export const enum IpcCommands { diff --git a/src/shared/paths.ts b/src/shared/paths.ts index a935d82..30e0132 100644 --- a/src/shared/paths.ts +++ b/src/shared/paths.ts @@ -9,4 +9,3 @@ import { join } from "path"; export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static"); export const VIEW_DIR = /* @__PURE__ */ join(STATIC_DIR, "views"); export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges"); -export const ICON_PATH = /* @__PURE__ */ join(STATIC_DIR, "icon.png"); diff --git a/static/icon.ico b/static/icon.ico deleted file mode 100644 index 1dc9894..0000000 Binary files a/static/icon.ico and /dev/null differ diff --git a/static/icon.png b/static/icon.png deleted file mode 100644 index afc960c..0000000 Binary files a/static/icon.png and /dev/null differ diff --git a/static/shiggy.gif b/static/shiggy.gif deleted file mode 100644 index fc56490..0000000 Binary files a/static/shiggy.gif and /dev/null differ diff --git a/static/splash.webp b/static/splash.webp new file mode 100644 index 0000000..a247191 Binary files /dev/null and b/static/splash.webp differ diff --git a/static/tray.png b/static/tray.png new file mode 100644 index 0000000..df78775 Binary files /dev/null and b/static/tray.png differ diff --git a/static/tray/tray.png b/static/tray/tray.png new file mode 100644 index 0000000..f4b80d6 Binary files /dev/null and b/static/tray/tray.png differ diff --git a/static/tray/trayTemplate.png b/static/tray/trayTemplate.png new file mode 100644 index 0000000..6a34cf6 Binary files /dev/null and b/static/tray/trayTemplate.png differ diff --git a/static/tray/trayUnread.png b/static/tray/trayUnread.png new file mode 100644 index 0000000..4af928c Binary files /dev/null and b/static/tray/trayUnread.png differ diff --git a/static/views/splash.html b/static/views/splash.html index e8e944a..ffb3548 100644 --- a/static/views/splash.html +++ b/static/views/splash.html @@ -36,19 +36,14 @@ img { width: 128px; height: 128px; - image-rendering: pixelated; + object-fit: contain; }
- shiggy +

Loading Vesktop...

@@ -61,4 +56,4 @@ messageElement.textContent = message; }) }); - + \ No newline at end of file