+
{title}
-
+
{settings.map(Setting => {
if (typeof Setting === "function") return ;
@@ -156,19 +162,18 @@ function SettingsSections() {
return (
(Settings[key as any] = v)}
- note={description}
disabled={disabled?.()}
key={key}
- >
- {title}
-
+ />
);
})}
- {i < arr.length - 1 &&
}
+ {i < arr.length - 1 &&
}
));
@@ -178,14 +183,13 @@ function SettingsSections() {
export default ErrorBoundary.wrap(
function SettingsUI() {
return (
-
- {/* FIXME: Outdated type */}
- {/* @ts-expect-error Outdated type */}
-
+
+
);
},
{
diff --git a/src/renderer/components/settings/Updater.tsx b/src/renderer/components/settings/Updater.tsx
new file mode 100644
index 000000000..8869f11e0
--- /dev/null
+++ b/src/renderer/components/settings/Updater.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { useAwaiter } from "@vencord/types/utils";
+import { Button, Text } from "@vencord/types/webpack/common";
+
+import { cl } from "./Settings";
+
+export function Updater() {
+ const [isOutdated] = useAwaiter(VesktopNative.app.isOutdated);
+
+ if (!isOutdated) return null;
+
+ return (
+
+ Your Vesktop is outdated!
+ Staying up to date is important for security and stability.
+
+
+
+ );
+}
diff --git a/src/renderer/components/settings/UserAssets.css b/src/renderer/components/settings/UserAssets.css
new file mode 100644
index 000000000..622107a22
--- /dev/null
+++ b/src/renderer/components/settings/UserAssets.css
@@ -0,0 +1,31 @@
+.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: grid;
+ width: 100%;
+ gap: 0.5em;
+ margin-bottom: auto;
+}
+
+.vcd-user-assets-buttons {
+ display: flex;
+ 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 000000000..08c30c73f
--- /dev/null
+++ b/src/renderer/components/settings/UserAssets.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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 { FormSwitch } from "@vencord/types/components";
+import {
+ Margins,
+ 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 { useSettings } from "renderer/settings";
+
+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 settings = useSettings();
+
+ const isSplash = asset === "splash";
+ const imageRendering = isSplash && settings.splashPixelated ? "pixelated" : "auto";
+
+ const onChooseAsset = (value?: null) => async () => {
+ const res = await VesktopNative.fileManager.chooseUserAsset(asset, value);
+ if (res === "ok") {
+ setVersion(Date.now());
+ if (isSplash && value === null) {
+ settings.splashPixelated = false;
+ }
+ } else if (res === "failed") {
+ showToast("Something went wrong. Please try again");
+ }
+ };
+
+ return (
+
+
+ {wordsToTitle(wordsFromCamel(asset))}
+
+
+

+
+
+
+
+
+ {isSplash && (
+
(settings.splashPixelated = val)}
+ className={Margins.top16}
+ hideBorder
+ />
+ )}
+
+
+
+ );
+}
diff --git a/src/renderer/components/settings/VesktopSettingsSwitch.tsx b/src/renderer/components/settings/VesktopSettingsSwitch.tsx
index bd1c2fb07..98f9b716a 100644
--- a/src/renderer/components/settings/VesktopSettingsSwitch.tsx
+++ b/src/renderer/components/settings/VesktopSettingsSwitch.tsx
@@ -4,13 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import { Switch } from "@vencord/types/webpack/common";
+import { FormSwitch } from "@vencord/types/components";
import { ComponentProps } from "react";
-export function VesktopSettingsSwitch(props: ComponentProps
) {
- return (
-
- {props.children}
-
- );
+import { cl } from "./Settings";
+
+export function VesktopSettingsSwitch(props: ComponentProps) {
+ return ;
}
diff --git a/src/renderer/components/settings/WindowsTransparencyControls.tsx b/src/renderer/components/settings/WindowsTransparencyControls.tsx
index 3e864a7eb..9147ded75 100644
--- a/src/renderer/components/settings/WindowsTransparencyControls.tsx
+++ b/src/renderer/components/settings/WindowsTransparencyControls.tsx
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
+import { ErrorBoundary } from "@vencord/types/components";
import { Margins } from "@vencord/types/utils";
import { Forms, Select } from "@vencord/types/webpack/common";
@@ -13,35 +14,37 @@ export const WindowsTransparencyControls: SettingsComponent = ({ settings }) =>
if (!VesktopNative.app.supportsWindowsTransparency()) return null;
return (
-
-
Transparency Options
-
- Requires a full restart. You will need a theme that supports transparency for this to work.
-
+
+
+ Transparency Options
+
+ Requires a full restart. You will need a theme that supports transparency for this to work.
+
-
+
+
);
};
diff --git a/src/renderer/components/settings/settings.css b/src/renderer/components/settings/settings.css
index 5ae9887b1..36e136ca2 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 e72775cad..041fdeadd 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 df083e266..65fa86776 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",
@@ -61,7 +60,17 @@ export const enum IpcEvents {
KEYBIND_SET_KEYBINDS = "VCD_KEYBIND_SET_KEYBINDS",
KEYBIND_GET_CURRENT_SHORTCUT = "VCD_KEYBIND_GET_CURRENT_SHORTCUT",
- KEYBIND_NEEDS_XDP = "VCD_KEYBIND_NEEDS_XDP"
+ KEYBIND_NEEDS_XDP = "VCD_KEYBIND_NEEDS_XDP",
+ 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 {
diff --git a/src/shared/paths.ts b/src/shared/paths.ts
index a935d825a..db5033a17 100644
--- a/src/shared/paths.ts
+++ b/src/shared/paths.ts
@@ -7,6 +7,4 @@
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/src/shared/settings.d.ts b/src/shared/settings.d.ts
index 01c26e593..4b4a7e5cf 100644
--- a/src/shared/settings.d.ts
+++ b/src/shared/settings.d.ts
@@ -11,6 +11,7 @@ export interface Settings {
transparencyOption?: "none" | "mica" | "tabbed" | "acrylic";
tray?: boolean;
minimizeToTray?: boolean;
+ autoStartMinimized?: boolean;
openLinksWithElectron?: boolean;
staticTitle?: boolean;
enableMenu?: boolean;
@@ -27,6 +28,7 @@ export interface Settings {
splashTheming?: boolean;
splashColor?: string;
splashBackground?: string;
+ splashPixelated?: boolean;
spellCheckLanguages?: string[];
@@ -49,11 +51,16 @@ export interface State {
maximized?: boolean;
minimized?: boolean;
windowBounds?: Rectangle;
- displayId: int;
firstLaunch?: boolean;
steamOSLayoutVersion?: number;
+ 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 000000000..056776858
--- /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/icon.ico b/static/icon.ico
deleted file mode 100644
index 1dc9894da..000000000
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 027ab0b6b..000000000
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 fc564900a..000000000
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 000000000..c693eb595
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 000000000..df7877513
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 000000000..f4b80d625
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 000000000..6a34cf669
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 000000000..4af928ce6
Binary files /dev/null and b/static/tray/trayUnread.png differ
diff --git a/static/views/about.html b/static/views/about.html
index 38ea463af..02688df58 100644
--- a/static/views/about.html
+++ b/static/views/about.html
@@ -1,7 +1,10 @@
+
+
+
About Vesktop
-
+
-

+
Loading Vesktop...
@@ -61,4 +66,4 @@
messageElement.textContent = message;
})
});
-
+
\ No newline at end of file
diff --git a/static/views/style.css b/static/views/style.css
deleted file mode 100644
index 872cee750..000000000
--- a/static/views/style.css
+++ /dev/null
@@ -1,37 +0,0 @@
-:root {
- --bg: white;
- --fg: black;
- --fg-secondary: #313338;
- --fg-semi-trans: rgb(0 0 0 / 0.2);
- --link: #006ce7;
- --link-hover: #005bb5;
-}
-
-@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;
- }
-}
-
-body {
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
- "Open Sans", "Helvetica Neue", sans-serif;
- margin: 0;
- padding: 0;
- background: var(--bg);
- color: var(--fg);
-}
-
-a {
- color: var(--link);
- transition: color 0.2s linear;
-}
-
-a:hover {
- color: var(--link-hover);
-}
\ No newline at end of file
diff --git a/static/views/updater.html b/static/views/updater.html
deleted file mode 100644
index 7eaba43d2..000000000
--- a/static/views/updater.html
+++ /dev/null
@@ -1,123 +0,0 @@
-
-
-
-
-
-
-
-
-
- Update Available
- There's a new update for Vesktop! Update now to get new fixes and features!
-
- Current:
-
- Latest:
-
-
- Changelog
- Loading...
-
-
-
-
-
-
-
-
-
diff --git a/static/views/updater/index.html b/static/views/updater/index.html
new file mode 100644
index 000000000..e9111001a
--- /dev/null
+++ b/static/views/updater/index.html
@@ -0,0 +1,64 @@
+
+
+
+
+ Vesktop Updater
+
+
+
+
+
+
+
+
+ An update is available!
+
+
+
+
+ Current version:
+ New version:
+
+ Release Notes
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static/views/updater/script.js b/static/views/updater/script.js
new file mode 100644
index 000000000..2af79bce4
--- /dev/null
+++ b/static/views/updater/script.js
@@ -0,0 +1,69 @@
+const { update, version: currentVersion } = await VesktopUpdaterNative.getData();
+
+document.getElementById("current-version").textContent = currentVersion;
+document.getElementById("new-version").textContent = update.version;
+document.getElementById("release-notes").innerHTML = update.releaseNotes
+ .map(
+ ({ version, note: html }) => `
+
+ Version ${version}
+ ${html.replace(/<\/?h([1-3])/g, (m, level) => m.replace(level, Number(level) + 3))}
+
+ `
+ )
+ .join("\n");
+
+document.querySelectorAll("a").forEach(a => {
+ a.target = "_blank";
+});
+
+// remove useless headings
+document.querySelectorAll("h3, h4, h5, h6").forEach(h => {
+ if (h.textContent.trim().toLowerCase() === "what's changed") {
+ h.remove();
+ }
+});
+
+/** @type {HTMLDialogElement} */
+const updateDialog = document.getElementById("update-dialog");
+/** @type {HTMLDialogElement} */
+const installingDialog = document.getElementById("installing-dialog");
+/** @type {HTMLProgressElement} */
+const downloadProgress = document.getElementById("download-progress");
+/** @type {HTMLElement} */
+const errorText = document.getElementById("error");
+
+document.getElementById("update-button").addEventListener("click", () => {
+ downloadProgress.value = 0;
+ errorText.textContent = "";
+
+ if (navigator.platform.startsWith("Linux")) {
+ document.getElementById("linux-note").classList.remove("hidden");
+ }
+
+ updateDialog.showModal();
+
+ VesktopUpdaterNative.installUpdate().then(() => {
+ downloadProgress.value = 100;
+ updateDialog.closedBy = "any";
+
+ installingDialog.showModal();
+ updateDialog.classList.add("hidden");
+ });
+});
+
+document.getElementById("later-button").addEventListener("click", () => VesktopUpdaterNative.snoozeUpdate());
+document.getElementById("ignore-button").addEventListener("click", () => {
+ const confirmed = confirm(
+ "Are you sure you want to ignore this update? You will not be notified about this update again. Updates are important for security and stability."
+ );
+ if (confirmed) VesktopUpdaterNative.ignoreUpdate();
+});
+
+VesktopUpdaterNative.onProgress(percent => (downloadProgress.value = percent));
+VesktopUpdaterNative.onError(message => {
+ updateDialog.closedBy = "any";
+ errorText.textContent = `An error occurred while downloading the update: ${message}`;
+ installingDialog.close();
+ updateDialog.classList.remove("hidden");
+});
diff --git a/static/views/updater/style.css b/static/views/updater/style.css
new file mode 100644
index 000000000..97db89f96
--- /dev/null
+++ b/static/views/updater/style.css
@@ -0,0 +1,147 @@
+html,
+body {
+ height: 100%;
+ margin: 0;
+ box-sizing: border-box;
+}
+
+body {
+ padding: 2em;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+
+ header,
+ footer {
+ flex: 0 0 auto;
+ }
+
+ main {
+ flex: 1 1 0%;
+ min-height: 0;
+ overflow: auto;
+ }
+
+ footer {
+ margin-top: 1em;
+ }
+}
+
+
+#versions {
+ display: inline-grid;
+ grid-template-columns: auto 1fr;
+ column-gap: 0.5em;
+
+ .label {
+ white-space: nowrap;
+ }
+
+ .value {
+ text-align: left;
+ }
+
+ #current-version {
+ color: #fc8f8a;
+ }
+
+ #new-version {
+ color: #71c07f;
+ }
+}
+
+#release-notes {
+ display: grid;
+ gap: 1em;
+}
+
+#buttons {
+ display: flex;
+ gap: 0.5em;
+ justify-content: end;
+}
+
+button {
+ cursor: pointer;
+ padding: 0.5rem 1rem;
+ font-size: 1.2em;
+ color: var(--fg);
+ border: none;
+ border-radius: 3px;
+ font-weight: bold;
+ transition: filter 0.2 ease-in-out;
+}
+
+button:hover,
+button:active {
+ filter: brightness(0.9);
+}
+
+.green {
+ background-color: #248046;
+}
+
+.grey {
+ background-color: rgba(151, 151, 159, 0.12);
+}
+
+.hidden {
+ display: none;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 0 0 0.5em 0;
+}
+
+h1 {
+ text-align: center;
+}
+
+h2 {
+ margin-bottom: 1em;
+}
+
+dialog {
+ width: 80%;
+ padding: 2em;
+}
+
+progress {
+ width: 100%;
+ height: 1.5em;
+ margin-top: 1em;
+}
+
+#error {
+ color: red;
+ font-weight: bold;
+}
+
+.spinner-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 2em;
+}
+
+.spinner {
+ width: 48px;
+ height: 48px;
+ border: 5px solid var(--fg);
+ border-bottom-color: transparent;
+ border-radius: 50%;
+ display: inline-block;
+ box-sizing: border-box;
+ animation: rotation 1s linear infinite;
+}
+
+@keyframes rotation {
+ to {
+ transform: rotate(360deg);
+ }
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 4e23ba29a..22551bd2a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,7 +10,7 @@
"target": "ESNEXT",
"jsx": "preserve",
- // we have duplicate electron types but it's w/e
+ // @vencord/types has some errors for now
"skipLibCheck": true,
"baseUrl": "./src/",