diff --git a/scripts/build/build.mts b/scripts/build/build.mts index 0f62ac6..79002c7 100644 --- a/scripts/build/build.mts +++ b/scripts/build/build.mts @@ -80,24 +80,31 @@ await Promise.all([ ...NodeCommonOpts, entryPoints: ["src/main/index.ts"], outfile: "dist/js/main.js", - footer: { js: "//# sourceURL=VCDMain" } + footer: { js: "//# sourceURL=VesktopMain" } }), createContext({ ...NodeCommonOpts, entryPoints: ["src/main/arrpc/worker.ts"], outfile: "dist/js/arRpcWorker.js", - footer: { js: "//# sourceURL=VCDArRpcWorker" } + footer: { js: "//# sourceURL=VesktopArRpcWorker" } }), createContext({ ...NodeCommonOpts, entryPoints: ["src/preload/index.ts"], outfile: "dist/js/preload.js", - footer: { js: "//# sourceURL=VCDPreload" } + footer: { js: "//# sourceURL=VesktopPreload" } }), createContext({ ...NodeCommonOpts, entryPoints: ["src/preload/splash.ts"], - outfile: "dist/js/splashPreload.js" + outfile: "dist/js/splashPreload.js", + footer: { js: "//# sourceURL=VesktopSplashPreload" } + }), + createContext({ + ...NodeCommonOpts, + entryPoints: ["src/preload/updater.ts"], + outfile: "dist/js/updaterPreload.js", + footer: { js: "//# sourceURL=VesktopUpdaterPreload" } }), createContext({ ...CommonOpts, @@ -110,7 +117,7 @@ await Promise.all([ jsxFragment: "VencordFragment", external: ["@vencord/types/*"], plugins: [vencordDep, includeDirPlugin("patches", "src/renderer/patches")], - footer: { js: "//# sourceURL=VCDRenderer" } + footer: { js: "//# sourceURL=VesktopRenderer" } }) ]); diff --git a/src/main/index.ts b/src/main/index.ts index ee59367..dbc8fdd 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,11 +4,11 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import "./updater"; import "./ipc"; import "./userAssets"; import { app, BrowserWindow, nativeTheme } from "electron"; -import { autoUpdater } from "electron-updater"; import { checkCommandLineForHelpOrVersion } from "./cli"; import { DATA_DIR } from "./constants"; @@ -22,10 +22,6 @@ import { isDeckGameMode } from "./utils/steamOS"; checkCommandLineForHelpOrVersion(); -if (!IS_DEV) { - autoUpdater.checkForUpdatesAndNotify(); -} - console.log("Vesktop v" + app.getVersion()); // Make the Vencord files use our DATA_DIR diff --git a/src/main/updater.ts b/src/main/updater.ts new file mode 100644 index 0000000..e8bec2b --- /dev/null +++ b/src/main/updater.ts @@ -0,0 +1,81 @@ +/* + * 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, ipcMain } from "electron"; +import { autoUpdater, UpdateInfo } from "electron-updater"; +import { join } from "path"; +import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents"; +import { VIEW_DIR } from "shared/paths"; +import { Millis } from "shared/utils/millis"; + +import { State } from "./settings"; +import { handle } from "./utils/ipcWrappers"; +import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; + +let updaterWindow: BrowserWindow | null = null; + +autoUpdater.on("update-available", update => { + if (State.store.updater?.ignoredVersion === update.version) return; + if ((State.store.updater?.snoozeUntil ?? 0) > Date.now()) return; + + openUpdater(update); +}); + +autoUpdater.on("update-downloaded", () => setTimeout(() => autoUpdater.quitAndInstall(), 100)); +autoUpdater.on("download-progress", p => + updaterWindow?.webContents.send(UpdaterIpcEvents.DOWNLOAD_PROGRESS, p.percent) +); +autoUpdater.on("error", err => updaterWindow?.webContents.send(UpdaterIpcEvents.ERROR, err.message)); + +autoUpdater.autoDownload = false; +autoUpdater.autoInstallOnAppQuit = false; +autoUpdater.fullChangelog = true; + +const isOutdated = autoUpdater.checkForUpdates().then(res => Boolean(res?.isUpdateAvailable)); + +handle(IpcEvents.UPDATER_IS_OUTDATED, () => isOutdated); +handle(IpcEvents.UPDATER_OPEN, async () => { + const res = await autoUpdater.checkForUpdates(); + if (res?.isUpdateAvailable && res.updateInfo) openUpdater(res.updateInfo); +}); + +function openUpdater(update: UpdateInfo) { + updaterWindow = new BrowserWindow({ + title: "Vesktop Updater", + autoHideMenuBar: true, + webPreferences: { + preload: join(__dirname, "updaterPreload.js") + }, + minHeight: 400, + minWidth: 750 + }); + makeLinksOpenExternally(updaterWindow); + + handle(UpdaterIpcEvents.GET_DATA, () => ({ update, version: app.getVersion() })); + handle(UpdaterIpcEvents.INSTALL, async () => { + await autoUpdater.downloadUpdate(); + }); + handle(UpdaterIpcEvents.SNOOZE_UPDATE, () => { + State.store.updater ??= {}; + State.store.updater.snoozeUntil = Date.now() + 1 * Millis.DAY; + updaterWindow?.close(); + }); + handle(UpdaterIpcEvents.IGNORE_UPDATE, () => { + State.store.updater ??= {}; + State.store.updater.ignoredVersion = update.version; + updaterWindow?.close(); + }); + + updaterWindow.on("closed", () => { + ipcMain.removeHandler(UpdaterIpcEvents.GET_DATA); + ipcMain.removeHandler(UpdaterIpcEvents.INSTALL); + ipcMain.removeHandler(UpdaterIpcEvents.SNOOZE_UPDATE); + ipcMain.removeHandler(UpdaterIpcEvents.IGNORE_UPDATE); + updaterWindow = null; + }); + + updaterWindow.loadFile(join(VIEW_DIR, "updater.html")); +} diff --git a/src/main/utils/ipcWrappers.ts b/src/main/utils/ipcWrappers.ts index 27a35fd..a8d0f34 100644 --- a/src/main/utils/ipcWrappers.ts +++ b/src/main/utils/ipcWrappers.ts @@ -6,7 +6,7 @@ import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron"; import { DISCORD_HOSTNAMES } from "main/constants"; -import { IpcEvents } from "shared/IpcEvents"; +import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents"; export function validateSender(frame: WebFrameMain | null, event: string) { if (!frame) throw new Error(`ipc[${event}]: No sender frame`); @@ -25,14 +25,14 @@ export function validateSender(frame: WebFrameMain | null, event: string) { } } -export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) { +export function handleSync(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) { ipcMain.on(event, (e, ...args) => { validateSender(e.senderFrame, event); e.returnValue = cb(e, ...args); }); } -export function handle(event: IpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) { +export function handle(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) { ipcMain.handle(event, (e, ...args) => { validateSender(e.senderFrame, event); return cb(e, ...args); diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts index 5847f65..51149c9 100644 --- a/src/preload/VesktopNative.ts +++ b/src/preload/VesktopNative.ts @@ -32,7 +32,9 @@ export const VesktopNative = { getVersion: () => sendSync(IpcEvents.GET_VERSION), setBadgeCount: (count: number) => invoke(IpcEvents.SET_BADGE_COUNT, count), supportsWindowsTransparency: () => sendSync(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY), - getEnableHardwareAcceleration: () => sendSync(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION) + getEnableHardwareAcceleration: () => sendSync(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION), + isOutdated: () => invoke(IpcEvents.UPDATER_IS_OUTDATED), + openUpdater: () => invoke(IpcEvents.UPDATER_OPEN) }, autostart: { isEnabled: () => sendSync(IpcEvents.AUTOSTART_ENABLED), diff --git a/src/preload/typedIpc.ts b/src/preload/typedIpc.ts index 91129e0..ebb8975 100644 --- a/src/preload/typedIpc.ts +++ b/src/preload/typedIpc.ts @@ -5,12 +5,12 @@ */ import { ipcRenderer } from "electron"; -import { IpcEvents } from "shared/IpcEvents"; +import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents"; -export function invoke(event: IpcEvents, ...args: any[]) { +export function invoke(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) { return ipcRenderer.invoke(event, ...args) as Promise; } -export function sendSync(event: IpcEvents, ...args: any[]) { +export function sendSync(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) { return ipcRenderer.sendSync(event, ...args) as T; } diff --git a/src/preload/updater.ts b/src/preload/updater.ts new file mode 100644 index 0000000..1881f65 --- /dev/null +++ b/src/preload/updater.ts @@ -0,0 +1,24 @@ +/* + * 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 { contextBridge, ipcRenderer } from "electron"; +import type { UpdateInfo } from "electron-updater"; +import { UpdaterIpcEvents } from "shared/IpcEvents"; + +import { invoke } from "./typedIpc"; + +contextBridge.exposeInMainWorld("VesktopUpdaterNative", { + getData: () => invoke(UpdaterIpcEvents.GET_DATA), + installUpdate: () => invoke(UpdaterIpcEvents.INSTALL), + onProgress: (cb: (percent: number) => void) => { + ipcRenderer.on(UpdaterIpcEvents.DOWNLOAD_PROGRESS, (_, percent: number) => cb(percent)); + }, + onError: (cb: (message: string) => void) => { + ipcRenderer.on(UpdaterIpcEvents.ERROR, (_, message: string) => cb(message)); + }, + snoozeUpdate: () => invoke(UpdaterIpcEvents.SNOOZE_UPDATE), + ignoreUpdate: () => invoke(UpdaterIpcEvents.IGNORE_UPDATE) +}); diff --git a/src/renderer/components/ScreenSharePicker.tsx b/src/renderer/components/ScreenSharePicker.tsx index 9f3bf8f..ea9a4a3 100644 --- a/src/renderer/components/ScreenSharePicker.tsx +++ b/src/renderer/components/ScreenSharePicker.tsx @@ -6,6 +6,7 @@ import "./screenSharePicker.css"; +import { classNameFactory } from "@vencord/types/api/Styles"; import { FormSwitch } from "@vencord/types/components"; import { closeModal, Logger, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils"; import { onceReady } from "@vencord/types/webpack"; @@ -15,7 +16,7 @@ import type { Dispatch, SetStateAction } from "react"; import { MediaEngineStore } from "renderer/common"; import { addPatch } from "renderer/patches/shared"; import { State, useSettings, useVesktopState } from "renderer/settings"; -import { classNameFactory, isLinux, isWindows } from "renderer/utils"; +import { isLinux, isWindows } from "renderer/utils"; const StreamResolutions = ["480", "720", "1080", "1440", "2160"] as const; const StreamFps = ["15", "30", "60"] as const; diff --git a/src/renderer/components/settings/DeveloperOptions.tsx b/src/renderer/components/settings/DeveloperOptions.tsx index 3d75b69..5ce71b0 100644 --- a/src/renderer/components/settings/DeveloperOptions.tsx +++ b/src/renderer/components/settings/DeveloperOptions.tsx @@ -17,7 +17,7 @@ import { import { Button, Forms, Text, Toasts } from "@vencord/types/webpack/common"; import { Settings } from "shared/settings"; -import { SettingsComponent } from "./Settings"; +import { cl, SettingsComponent } from "./Settings"; export const DeveloperOptionsButton: SettingsComponent = ({ settings }) => { return ; @@ -41,7 +41,7 @@ function openDeveloperOptionsModal(settings: Settings) { Debugging -
+
+
+ ); +} diff --git a/src/renderer/components/settings/VesktopSettingsSwitch.tsx b/src/renderer/components/settings/VesktopSettingsSwitch.tsx index e456885..98f9b71 100644 --- a/src/renderer/components/settings/VesktopSettingsSwitch.tsx +++ b/src/renderer/components/settings/VesktopSettingsSwitch.tsx @@ -7,6 +7,8 @@ import { FormSwitch } from "@vencord/types/components"; import { ComponentProps } from "react"; +import { cl } from "./Settings"; + export function VesktopSettingsSwitch(props: ComponentProps) { - return ; + return ; } diff --git a/src/renderer/components/settings/settings.css b/src/renderer/components/settings/settings.css index 5ae9887..36e136c 100644 --- a/src/renderer/components/settings/settings.css +++ b/src/renderer/components/settings/settings.css @@ -31,4 +31,17 @@ .vcd-settings-switch { margin-bottom: 0; +} + +.vcd-settings-updater-card { + padding: 1em; + margin-bottom: 1em; + display: grid; + gap: 0.5em; + + border-radius: 8px; + background-color: var(--bg-secondary); + background: var(--background-feedback-warning); + border: 1px solid var(--info-warning-foreground); + color: var(--text-feedback-warning); } \ No newline at end of file diff --git a/src/renderer/utils.ts b/src/renderer/utils.ts index e72775c..041fdea 100644 --- a/src/renderer/utils.ts +++ b/src/renderer/utils.ts @@ -19,26 +19,3 @@ const { platform } = navigator; export const isWindows = platform.startsWith("Win"); export const isMac = platform.startsWith("Mac"); export const isLinux = platform.startsWith("Linux"); - -type ClassNameFactoryArg = string | string[] | Record | false | null | undefined | 0 | ""; -/** - * @param prefix The prefix to add to each class, defaults to `""` - * @returns A classname generator function - * @example - * const cl = classNameFactory("plugin-"); - * - * cl("base", ["item", "editable"], { selected: null, disabled: true }) - * // => "plugin-base plugin-item plugin-editable plugin-disabled" - */ -export const classNameFactory = - (prefix: string = "") => - (...args: ClassNameFactoryArg[]) => { - const classNames = new Set(); - for (const arg of args) { - if (arg && typeof arg === "string") classNames.add(arg); - else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name)); - else if (arg && typeof arg === "object") - Object.entries(arg).forEach(([name, value]) => value && classNames.add(name)); - } - return Array.from(classNames, name => prefix + name).join(" "); - }; diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 2eaf2af..9052fbe 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -27,9 +27,8 @@ export const enum IpcEvents { GET_VENCORD_DIR = "VCD_GET_VENCORD_DIR", SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR", - UPDATER_GET_DATA = "VCD_UPDATER_GET_DATA", - UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD", - UPDATE_IGNORE = "VCD_UPDATE_IGNORE", + UPDATER_IS_OUTDATED = "VCD_UPDATER_IS_OUTDATED", + UPDATER_OPEN = "VCD_UPDATER_OPEN", SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES", SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT", @@ -62,6 +61,15 @@ export const enum IpcEvents { CHOOSE_USER_ASSET = "VCD_CHOOSE_USER_ASSET" } +export const enum UpdaterIpcEvents { + GET_DATA = "VCD_UPDATER_GET_DATA", + INSTALL = "VCD_UPDATER_INSTALL", + DOWNLOAD_PROGRESS = "VCD_UPDATER_DOWNLOAD_PROGRESS", + ERROR = "VCD_UPDATER_ERROR", + SNOOZE_UPDATE = "VCD_UPDATER_SNOOZE_UPDATE", + IGNORE_UPDATE = "VCD_UPDATER_IGNORE_UPDATE" +} + export const enum IpcCommands { RPC_ACTIVITY = "rpc:activity", RPC_INVITE = "rpc:invite", diff --git a/src/shared/settings.d.ts b/src/shared/settings.d.ts index 5d3263c..37bf48d 100644 --- a/src/shared/settings.d.ts +++ b/src/shared/settings.d.ts @@ -58,4 +58,9 @@ export interface State { linuxAutoStartEnabled?: boolean; vencordDir?: string; + + updater?: { + ignoredVersion?: string; + snoozeUntil?: number; + }; } diff --git a/src/shared/utils/millis.ts b/src/shared/utils/millis.ts new file mode 100644 index 0000000..0567768 --- /dev/null +++ b/src/shared/utils/millis.ts @@ -0,0 +1,12 @@ +/* + * 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 + */ + +export const enum Millis { + SECOND = 1000, + MINUTE = 60 * SECOND, + HOUR = 60 * MINUTE, + DAY = 24 * HOUR +} diff --git a/static/views/style.css b/static/views/style.css index 872cee7..622fb6e 100644 --- a/static/views/style.css +++ b/static/views/style.css @@ -1,21 +1,12 @@ :root { - --bg: white; - --fg: black; - --fg-secondary: #313338; - --fg-semi-trans: rgb(0 0 0 / 0.2); - --link: #006ce7; - --link-hover: #005bb5; -} + color-scheme: light dark; -@media (prefers-color-scheme: dark) { - :root { - --bg: hsl(223 6.7% 20.6%); - --fg: white; - --fg-secondary: #b5bac1; - --fg-semi-trans: rgb(255 255 255 / 0.2); - --link: #00a8fc; - --link-hover: #0086c3; - } + --bg: light-dark(white, hsl(223 6.7% 20.6%)); + --fg: light-dark(black, white); + --fg-secondary: light-dark(#313338, #b5bac1); + --fg-semi-trans: light-dark(rgb(0 0 0 / 0.2), rgb(255 255 255 / 0.2)); + --link: light-dark(#006ce7, #00a8fc); + --link-hover: light-dark(#005bb5, #0086c3); } body { diff --git a/static/views/updater.html b/static/views/updater.html new file mode 100644 index 0000000..ed0c3f7 --- /dev/null +++ b/static/views/updater.html @@ -0,0 +1,268 @@ + + Vesktop Updater + + + + + + + +
+

An update is available!

+
+ +
+

+ Current version: + New version: +

+

Release Notes

+ +
+
+ +
+
+ + + +
+
+ + +

Downloading Update

+

+ Please wait while the update is being downloaded. Once the update finished downloading, it will + automatically install and Vesktop will restart. +

+ + + + + +

+
+ + +

Installing Update

+

Please wait while the update is being installed. Vesktop will restart shortly.

+ +
+
+
+
+ + + \ No newline at end of file