From 98b3651a1b7bfccfdfdc2e884a272ed8878a2d71 Mon Sep 17 00:00:00 2001 From: duanshuwen Date: Sun, 12 Oct 2025 14:38:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=B8=BB=E9=A2=98|?= =?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/helpers/ipc/context-exposer.ts | 7 +++ src/helpers/ipc/listeners-register.ts | 8 +++ src/helpers/ipc/theme/theme-channels.ts | 5 ++ src/helpers/ipc/theme/theme-context.ts | 19 +++++++ src/helpers/ipc/theme/window-channels.ts | 37 +++++++++++++ src/helpers/ipc/window/window-channels.ts | 3 + src/helpers/ipc/window/window-context.ts | 15 +++++ src/helpers/ipc/window/window-listeners.ts | 22 ++++++++ src/helpers/language_helper.ts | 19 +++++++ src/helpers/theme_helpers.ts | 64 ++++++++++++++++++++++ src/helpers/window_helpers.ts | 11 ++++ src/main.ts | 9 +-- src/types.d.ts | 25 +++++++++ src/types/theme-mode.ts | 1 + tsconfig.json | 3 +- vite.main.config.ts | 1 + vite.renderer.config.ts | 1 + 17 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 src/helpers/ipc/context-exposer.ts create mode 100644 src/helpers/ipc/listeners-register.ts create mode 100644 src/helpers/ipc/theme/theme-channels.ts create mode 100644 src/helpers/ipc/theme/theme-context.ts create mode 100644 src/helpers/ipc/theme/window-channels.ts create mode 100644 src/helpers/ipc/window/window-channels.ts create mode 100644 src/helpers/ipc/window/window-context.ts create mode 100644 src/helpers/ipc/window/window-listeners.ts create mode 100644 src/helpers/language_helper.ts create mode 100644 src/helpers/theme_helpers.ts create mode 100644 src/helpers/window_helpers.ts create mode 100644 src/types.d.ts create mode 100644 src/types/theme-mode.ts diff --git a/src/helpers/ipc/context-exposer.ts b/src/helpers/ipc/context-exposer.ts new file mode 100644 index 0000000..14aecd9 --- /dev/null +++ b/src/helpers/ipc/context-exposer.ts @@ -0,0 +1,7 @@ +import { exposeThemeContext } from "./theme/theme-context"; +import { exposeWindowContext } from "./window/window-context"; + +export default function exposeContexts() { + exposeWindowContext(); + exposeThemeContext(); +} diff --git a/src/helpers/ipc/listeners-register.ts b/src/helpers/ipc/listeners-register.ts new file mode 100644 index 0000000..8c86fe4 --- /dev/null +++ b/src/helpers/ipc/listeners-register.ts @@ -0,0 +1,8 @@ +import { BrowserWindow } from "electron"; +import { addThemeEventListeners } from "./theme/theme-listeners"; +import { addWindowEventListeners } from "./window/window-listeners"; + +export default function registerListeners(mainWindow: BrowserWindow) { + addWindowEventListeners(mainWindow); + addThemeEventListeners(); +} diff --git a/src/helpers/ipc/theme/theme-channels.ts b/src/helpers/ipc/theme/theme-channels.ts new file mode 100644 index 0000000..a41ad8a --- /dev/null +++ b/src/helpers/ipc/theme/theme-channels.ts @@ -0,0 +1,5 @@ +export const THEME_MODE_CURRENT_CHANNEL = "theme-mode:current"; +export const THEME_MODE_TOGGLE_CHANNEL = "theme-mode:toggle"; +export const THEME_MODE_DARK_CHANNEL = "theme-mode:dark"; +export const THEME_MODE_LIGHT_CHANNEL = "theme-mode:light"; +export const THEME_MODE_SYSTEM_CHANNEL = "theme-mode:system"; diff --git a/src/helpers/ipc/theme/theme-context.ts b/src/helpers/ipc/theme/theme-context.ts new file mode 100644 index 0000000..1d493bd --- /dev/null +++ b/src/helpers/ipc/theme/theme-context.ts @@ -0,0 +1,19 @@ +import { + THEME_MODE_CURRENT_CHANNEL, + THEME_MODE_DARK_CHANNEL, + THEME_MODE_LIGHT_CHANNEL, + THEME_MODE_SYSTEM_CHANNEL, + THEME_MODE_TOGGLE_CHANNEL, +} from "./theme-channels"; + +export function exposeThemeContext() { + const { contextBridge, ipcRenderer } = window.require("electron"); + + contextBridge.exposeInMainWorld("themeMode", { + current: () => ipcRenderer.invoke(THEME_MODE_CURRENT_CHANNEL), + toggle: () => ipcRenderer.invoke(THEME_MODE_TOGGLE_CHANNEL), + dark: () => ipcRenderer.invoke(THEME_MODE_DARK_CHANNEL), + light: () => ipcRenderer.invoke(THEME_MODE_LIGHT_CHANNEL), + system: () => ipcRenderer.invoke(THEME_MODE_SYSTEM_CHANNEL), + }); +} diff --git a/src/helpers/ipc/theme/window-channels.ts b/src/helpers/ipc/theme/window-channels.ts new file mode 100644 index 0000000..4a2591b --- /dev/null +++ b/src/helpers/ipc/theme/window-channels.ts @@ -0,0 +1,37 @@ +import { nativeTheme } from "electron"; +import { ipcMain } from "electron"; +import { + THEME_MODE_CURRENT_CHANNEL, + THEME_MODE_DARK_CHANNEL, + THEME_MODE_LIGHT_CHANNEL, + THEME_MODE_SYSTEM_CHANNEL, + THEME_MODE_TOGGLE_CHANNEL, +} from "./theme-channels"; + +export function addThemeEventListeners() { + ipcMain.handle(THEME_MODE_CURRENT_CHANNEL, () => nativeTheme.themeSource); + + ipcMain.handle(THEME_MODE_TOGGLE_CHANNEL, () => { + if (nativeTheme.shouldUseDarkColors) { + nativeTheme.themeSource = "light"; + } else { + nativeTheme.themeSource = "dark"; + } + return nativeTheme.shouldUseDarkColors; + }); + + ipcMain.handle( + THEME_MODE_DARK_CHANNEL, + () => (nativeTheme.themeSource = "dark") + ); + + ipcMain.handle( + THEME_MODE_LIGHT_CHANNEL, + () => (nativeTheme.themeSource = "light") + ); + + ipcMain.handle(THEME_MODE_SYSTEM_CHANNEL, () => { + nativeTheme.themeSource = "system"; + return nativeTheme.shouldUseDarkColors; + }); +} diff --git a/src/helpers/ipc/window/window-channels.ts b/src/helpers/ipc/window/window-channels.ts new file mode 100644 index 0000000..3760008 --- /dev/null +++ b/src/helpers/ipc/window/window-channels.ts @@ -0,0 +1,3 @@ +export const WIN_MINIMIZE_CHANNEL = "window:minimize"; +export const WIN_MAXIMIZE_CHANNEL = "window:maximize"; +export const WIN_CLOSE_CHANNEL = "window:close"; diff --git a/src/helpers/ipc/window/window-context.ts b/src/helpers/ipc/window/window-context.ts new file mode 100644 index 0000000..f066884 --- /dev/null +++ b/src/helpers/ipc/window/window-context.ts @@ -0,0 +1,15 @@ +import { + WIN_MINIMIZE_CHANNEL, + WIN_MAXIMIZE_CHANNEL, + WIN_CLOSE_CHANNEL, +} from "./window-channels"; + +export function exposeWindowContext() { + const { contextBridge, ipcRenderer } = window.require("electron"); + + contextBridge.exposeInMainWorld("electronWindow", { + minimize: () => ipcRenderer.invoke(WIN_MINIMIZE_CHANNEL), + maximize: () => ipcRenderer.invoke(WIN_MAXIMIZE_CHANNEL), + close: () => ipcRenderer.invoke(WIN_CLOSE_CHANNEL), + }); +} diff --git a/src/helpers/ipc/window/window-listeners.ts b/src/helpers/ipc/window/window-listeners.ts new file mode 100644 index 0000000..5964f89 --- /dev/null +++ b/src/helpers/ipc/window/window-listeners.ts @@ -0,0 +1,22 @@ +import { BrowserWindow, ipcMain } from "electron"; +import { + WIN_CLOSE_CHANNEL, + WIN_MAXIMIZE_CHANNEL, + WIN_MINIMIZE_CHANNEL, +} from "./window-channels"; + +export function addWindowEventListeners(mainWindow: BrowserWindow) { + ipcMain.handle(WIN_MINIMIZE_CHANNEL, () => { + mainWindow.minimize(); + }); + ipcMain.handle(WIN_MAXIMIZE_CHANNEL, () => { + if (mainWindow.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow.maximize(); + } + }); + ipcMain.handle(WIN_CLOSE_CHANNEL, () => { + mainWindow.close(); + }); +} diff --git a/src/helpers/language_helper.ts b/src/helpers/language_helper.ts new file mode 100644 index 0000000..2d47a5e --- /dev/null +++ b/src/helpers/language_helper.ts @@ -0,0 +1,19 @@ +import type { i18n } from "i18next"; + +const languageLocalStorageKey = "lang"; + +export function setAppLanguage(lang: string, i18n: i18n) { + localStorage.setItem(languageLocalStorageKey, lang); + i18n.changeLanguage(lang); + document.documentElement.lang = lang; +} + +export function updateAppLanguage(i18n: i18n) { + const localLang = localStorage.getItem(languageLocalStorageKey); + if (!localLang) { + return; + } + + i18n.changeLanguage(localLang); + document.documentElement.lang = localLang; +} diff --git a/src/helpers/theme_helpers.ts b/src/helpers/theme_helpers.ts new file mode 100644 index 0000000..7af078c --- /dev/null +++ b/src/helpers/theme_helpers.ts @@ -0,0 +1,64 @@ +import { ThemeMode } from "@types/theme-mode"; + +const THEME_KEY = "theme"; + +export interface ThemePreferences { + system: ThemeMode; + local: ThemeMode | null; +} + +export async function getCurrentTheme(): Promise { + const currentTheme = await window.themeMode.current(); + const localTheme = localStorage.getItem(THEME_KEY) as ThemeMode | null; + + return { + system: currentTheme, + local: localTheme, + }; +} + +export async function setTheme(newTheme: ThemeMode) { + switch (newTheme) { + case "dark": + await window.themeMode.dark(); + updateDocumentTheme(true); + break; + case "light": + await window.themeMode.light(); + updateDocumentTheme(false); + break; + case "system": { + const isDarkMode = await window.themeMode.system(); + updateDocumentTheme(isDarkMode); + break; + } + } + + localStorage.setItem(THEME_KEY, newTheme); +} + +export async function toggleTheme() { + const isDarkMode = await window.themeMode.toggle(); + const newTheme = isDarkMode ? "dark" : "light"; + + updateDocumentTheme(isDarkMode); + localStorage.setItem(THEME_KEY, newTheme); +} + +export async function syncThemeWithLocal() { + const { local } = await getCurrentTheme(); + if (!local) { + setTheme("system"); + return; + } + + await setTheme(local); +} + +function updateDocumentTheme(isDarkMode: boolean) { + if (!isDarkMode) { + document.documentElement.classList.remove("dark"); + } else { + document.documentElement.classList.add("dark"); + } +} diff --git a/src/helpers/window_helpers.ts b/src/helpers/window_helpers.ts new file mode 100644 index 0000000..1b25c18 --- /dev/null +++ b/src/helpers/window_helpers.ts @@ -0,0 +1,11 @@ +export async function minimizeWindow() { + await window.electronWindow.minimize(); +} + +export async function maximizeWindow() { + await window.electronWindow.maximize(); +} + +export async function closeWindow() { + await window.electronWindow.close(); +} diff --git a/src/main.ts b/src/main.ts index 351c240..d0a7e7d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,12 @@ import { app, BrowserWindow } from "electron"; +import registerListeners from "./helpers/ipc/listeners-register"; import path from "node:path"; -import started from "electron-squirrel-startup"; +// import started from "electron-squirrel-startup"; // Handle creating/removing shortcuts on Windows when installing/uninstalling. -if (started) { - app.quit(); -} +// if (started) { +// app.quit(); +// } const createWindow = () => { // Create the browser window. diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..391d991 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,25 @@ +// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite +// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on +// whether you're running in development or production). +declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; +declare const MAIN_WINDOW_VITE_NAME: string; + +// Preload types +interface ThemeModeContext { + toggle: () => Promise; + dark: () => Promise; + light: () => Promise; + system: () => Promise; + current: () => Promise<"dark" | "light" | "system">; +} + +interface ElectronWindow { + minimize: () => Promise; + maximize: () => Promise; + close: () => Promise; +} + +declare interface Window { + themeMode: ThemeModeContext; + electronWindow: ElectronWindow; +} diff --git a/src/types/theme-mode.ts b/src/types/theme-mode.ts new file mode 100644 index 0000000..e73a3a6 --- /dev/null +++ b/src/types/theme-mode.ts @@ -0,0 +1 @@ +export type ThemeMode = "dark" | "light" | "system"; diff --git a/tsconfig.json b/tsconfig.json index 864e473..1048075 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "@/*": ["./src/*"], "@stores/*": ["./src/stores/*"], "@utils/*": ["./src/utils/*"], - "@api/*": ["./src/api/*"] + "@api/*": ["./src/api/*"], + "@types/*": ["./src/types/*"] }, "outDir": "dist", "moduleResolution": "bundler", diff --git a/vite.main.config.ts b/vite.main.config.ts index 6e8c0e4..d222cdf 100644 --- a/vite.main.config.ts +++ b/vite.main.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ "@stores": resolve(__dirname, "./src/stores"), "@utils": resolve(__dirname, "./src/utils"), "@api": resolve(__dirname, "./src/api"), + "@types": resolve(__dirname, "./src/types"), }, }, }); diff --git a/vite.renderer.config.ts b/vite.renderer.config.ts index 14c3a70..bbf54ed 100644 --- a/vite.renderer.config.ts +++ b/vite.renderer.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ "@stores": resolve(__dirname, "./src/stores"), "@utils": resolve(__dirname, "./src/utils"), "@api": resolve(__dirname, "./src/api"), + "@types": resolve(__dirname, "./src/types"), }, }, });