feat: 新增主题|多语言配置

This commit is contained in:
duanshuwen
2025-10-12 14:38:18 +08:00
parent ebac04b786
commit 98b3651a1b
17 changed files with 245 additions and 5 deletions

View File

@@ -0,0 +1,7 @@
import { exposeThemeContext } from "./theme/theme-context";
import { exposeWindowContext } from "./window/window-context";
export default function exposeContexts() {
exposeWindowContext();
exposeThemeContext();
}

View File

@@ -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();
}

View File

@@ -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";

View File

@@ -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),
});
}

View File

@@ -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;
});
}

View File

@@ -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";

View File

@@ -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),
});
}

View File

@@ -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();
});
}

View File

@@ -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;
}

View File

@@ -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<ThemePreferences> {
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");
}
}

View File

@@ -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();
}

View File

@@ -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.

25
src/types.d.ts vendored Normal file
View File

@@ -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<boolean>;
dark: () => Promise<void>;
light: () => Promise<void>;
system: () => Promise<boolean>;
current: () => Promise<"dark" | "light" | "system">;
}
interface ElectronWindow {
minimize: () => Promise<void>;
maximize: () => Promise<void>;
close: () => Promise<void>;
}
declare interface Window {
themeMode: ThemeModeContext;
electronWindow: ElectronWindow;
}

1
src/types/theme-mode.ts Normal file
View File

@@ -0,0 +1 @@
export type ThemeMode = "dark" | "light" | "system";

View File

@@ -18,7 +18,8 @@
"@/*": ["./src/*"],
"@stores/*": ["./src/stores/*"],
"@utils/*": ["./src/utils/*"],
"@api/*": ["./src/api/*"]
"@api/*": ["./src/api/*"],
"@types/*": ["./src/types/*"]
},
"outDir": "dist",
"moduleResolution": "bundler",

View File

@@ -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"),
},
},
});

View File

@@ -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"),
},
},
});