| author | |
| committer | |
| log | 1e9bac7f6e783fe0498ddeb8392c5f4b50aad19d |
| tree | 40c9a2bbf74e92c9eb1480953117e0a52c0727d8 |
| parent | 1c290767f22f9d8c7d3fa378d9450c8f48184679 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 55 insertions(+), 2 deletions(-)
apps/desktop/src/main.ts+35-2| ... | ... | @@ -4,10 +4,24 @@ import * as FS from "node:fs"; |
| 4 | 4 | import * as OS from "node:os"; |
| 5 | 5 | import * as Path from "node:path"; |
| 6 | 6 | |
| 7 | import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, protocol, shell } from "electron"; | |
| 7 | import { | |
| 8 | app, | |
| 9 | BrowserWindow, | |
| 10 | dialog, | |
| 11 | ipcMain, | |
| 12 | Menu, | |
| 13 | nativeImage, | |
| 14 | nativeTheme, | |
| 15 | protocol, | |
| 16 | shell, | |
| 17 | } from "electron"; | |
| 8 | 18 | import type { MenuItemConstructorOptions } from "electron"; |
| 9 | 19 | import * as Effect from "effect/Effect"; |
| 10 | import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts"; | |
| 20 | import type { | |
| 21 | DesktopTheme, | |
| 22 | DesktopUpdateActionResult, | |
| 23 | DesktopUpdateState, | |
| 24 | } from "@t3tools/contracts"; | |
| 11 | 25 | import { autoUpdater } from "electron-updater"; |
| 12 | 26 | |
| 13 | 27 | import type { ContextMenuItem } from "@t3tools/contracts"; |
| ... | ... | @@ -34,6 +48,7 @@ fixPath(); |
| 34 | 48 | |
| 35 | 49 | const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; |
| 36 | 50 | const CONFIRM_CHANNEL = "desktop:confirm"; |
| 51 | const SET_THEME_CHANNEL = "desktop:set-theme"; | |
| 37 | 52 | const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; |
| 38 | 53 | const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; |
| 39 | 54 | const MENU_ACTION_CHANNEL = "desktop:menu-action"; |
| ... | ... | @@ -137,6 +152,14 @@ function getSafeExternalUrl(rawUrl: unknown): string | null { |
| 137 | 152 | return parsedUrl.toString(); |
| 138 | 153 | } |
| 139 | 154 | |
| 155 | function getSafeTheme(rawTheme: unknown): DesktopTheme | null { | |
| 156 | if (rawTheme === "light" || rawTheme === "dark" || rawTheme === "system") { | |
| 157 | return rawTheme; | |
| 158 | } | |
| 159 | ||
| 160 | return null; | |
| 161 | } | |
| 162 | ||
| 140 | 163 | function writeDesktopStreamChunk( |
| 141 | 164 | streamName: "stdout" | "stderr", |
| 142 | 165 | chunk: unknown, |
| ... | ... | @@ -1037,6 +1060,16 @@ function registerIpcHandlers(): void { |
| 1037 | 1060 | return showDesktopConfirmDialog(message, owner); |
| 1038 | 1061 | }); |
| 1039 | 1062 | |
| 1063 | ipcMain.removeHandler(SET_THEME_CHANNEL); | |
| 1064 | ipcMain.handle(SET_THEME_CHANNEL, async (_event, rawTheme: unknown) => { | |
| 1065 | const theme = getSafeTheme(rawTheme); | |
| 1066 | if (!theme) { | |
| 1067 | return; | |
| 1068 | } | |
| 1069 | ||
| 1070 | nativeTheme.themeSource = theme; | |
| 1071 | }); | |
| 1072 | ||
| 1040 | 1073 | ipcMain.removeHandler(CONTEXT_MENU_CHANNEL); |
| 1041 | 1074 | ipcMain.handle( |
| 1042 | 1075 | CONTEXT_MENU_CHANNEL, |
apps/desktop/src/preload.ts+2| ... | ... | @@ -3,6 +3,7 @@ import type { DesktopBridge } from "@t3tools/contracts"; |
| 3 | 3 | |
| 4 | 4 | const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; |
| 5 | 5 | const CONFIRM_CHANNEL = "desktop:confirm"; |
| 6 | const SET_THEME_CHANNEL = "desktop:set-theme"; | |
| 6 | 7 | const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; |
| 7 | 8 | const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; |
| 8 | 9 | const MENU_ACTION_CHANNEL = "desktop:menu-action"; |
| ... | ... | @@ -16,6 +17,7 @@ contextBridge.exposeInMainWorld("desktopBridge", { |
| 16 | 17 | getWsUrl: () => wsUrl, |
| 17 | 18 | pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL), |
| 18 | 19 | confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message), |
| 20 | setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme), | |
| 19 | 21 | showContextMenu: (items, position) => ipcRenderer.invoke(CONTEXT_MENU_CHANNEL, items, position), |
| 20 | 22 | openExternal: (url: string) => ipcRenderer.invoke(OPEN_EXTERNAL_CHANNEL, url), |
| 21 | 23 | onMenuAction: (listener) => { |
apps/web/src/hooks/useTheme.ts+16| ... | ... | @@ -11,6 +11,7 @@ const MEDIA_QUERY = "(prefers-color-scheme: dark)"; |
| 11 | 11 | |
| 12 | 12 | let listeners: Array<() => void> = []; |
| 13 | 13 | let lastSnapshot: ThemeSnapshot | null = null; |
| 14 | let lastDesktopTheme: Theme | null = null; | |
| 14 | 15 | function emitChange() { |
| 15 | 16 | for (const listener of listeners) listener(); |
| 16 | 17 | } |
| ... | ... | @@ -31,6 +32,7 @@ function applyTheme(theme: Theme, suppressTransitions = false) { |
| 31 | 32 | } |
| 32 | 33 | const isDark = theme === "dark" || (theme === "system" && getSystemDark()); |
| 33 | 34 | document.documentElement.classList.toggle("dark", isDark); |
| 35 | syncDesktopTheme(theme); | |
| 34 | 36 | if (suppressTransitions) { |
| 35 | 37 | // Force a reflow so the no-transitions class takes effect before removal |
| 36 | 38 | // oxlint-disable-next-line no-unused-expressions |
| ... | ... | @@ -41,6 +43,20 @@ function applyTheme(theme: Theme, suppressTransitions = false) { |
| 41 | 43 | } |
| 42 | 44 | } |
| 43 | 45 | |
| 46 | function syncDesktopTheme(theme: Theme) { | |
| 47 | const bridge = window.desktopBridge; | |
| 48 | if (!bridge || lastDesktopTheme === theme) { | |
| 49 | return; | |
| 50 | } | |
| 51 | ||
| 52 | lastDesktopTheme = theme; | |
| 53 | void bridge.setTheme(theme).catch(() => { | |
| 54 | if (lastDesktopTheme === theme) { | |
| 55 | lastDesktopTheme = null; | |
| 56 | } | |
| 57 | }); | |
| 58 | } | |
| 59 | ||
| 44 | 60 | // Apply immediately on module load to prevent flash |
| 45 | 61 | applyTheme(getStored()); |
| 46 | 62 |
packages/contracts/src/ipc.ts+2| ... | ... | @@ -64,6 +64,7 @@ export type DesktopUpdateStatus = |
| 64 | 64 | | "error"; |
| 65 | 65 | |
| 66 | 66 | export type DesktopRuntimeArch = "arm64" | "x64" | "other"; |
| 67 | export type DesktopTheme = "light" | "dark" | "system"; | |
| 67 | 68 | |
| 68 | 69 | export interface DesktopRuntimeInfo { |
| 69 | 70 | hostArch: DesktopRuntimeArch; |
| ... | ... | @@ -97,6 +98,7 @@ export interface DesktopBridge { |
| 97 | 98 | getWsUrl: () => string | null; |
| 98 | 99 | pickFolder: () => Promise<string | null>; |
| 99 | 100 | confirm: (message: string) => Promise<boolean>; |
| 101 | setTheme: (theme: DesktopTheme) => Promise<void>; | |
| 100 | 102 | showContextMenu: <T extends string>( |
| 101 | 103 | items: readonly ContextMenuItem<T>[], |
| 102 | 104 | position?: { x: number; y: number }, |