feat: 新增主题|多语言配置
This commit is contained in:
7
src/helpers/ipc/context-exposer.ts
Normal file
7
src/helpers/ipc/context-exposer.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { exposeThemeContext } from "./theme/theme-context";
|
||||||
|
import { exposeWindowContext } from "./window/window-context";
|
||||||
|
|
||||||
|
export default function exposeContexts() {
|
||||||
|
exposeWindowContext();
|
||||||
|
exposeThemeContext();
|
||||||
|
}
|
||||||
8
src/helpers/ipc/listeners-register.ts
Normal file
8
src/helpers/ipc/listeners-register.ts
Normal 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();
|
||||||
|
}
|
||||||
5
src/helpers/ipc/theme/theme-channels.ts
Normal file
5
src/helpers/ipc/theme/theme-channels.ts
Normal 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";
|
||||||
19
src/helpers/ipc/theme/theme-context.ts
Normal file
19
src/helpers/ipc/theme/theme-context.ts
Normal 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
37
src/helpers/ipc/theme/window-channels.ts
Normal file
37
src/helpers/ipc/theme/window-channels.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
3
src/helpers/ipc/window/window-channels.ts
Normal file
3
src/helpers/ipc/window/window-channels.ts
Normal 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";
|
||||||
15
src/helpers/ipc/window/window-context.ts
Normal file
15
src/helpers/ipc/window/window-context.ts
Normal 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
22
src/helpers/ipc/window/window-listeners.ts
Normal file
22
src/helpers/ipc/window/window-listeners.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
19
src/helpers/language_helper.ts
Normal file
19
src/helpers/language_helper.ts
Normal 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;
|
||||||
|
}
|
||||||
64
src/helpers/theme_helpers.ts
Normal file
64
src/helpers/theme_helpers.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/helpers/window_helpers.ts
Normal file
11
src/helpers/window_helpers.ts
Normal 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();
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { app, BrowserWindow } from "electron";
|
import { app, BrowserWindow } from "electron";
|
||||||
|
import registerListeners from "./helpers/ipc/listeners-register";
|
||||||
import path from "node:path";
|
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.
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
if (started) {
|
// if (started) {
|
||||||
app.quit();
|
// app.quit();
|
||||||
}
|
// }
|
||||||
|
|
||||||
const createWindow = () => {
|
const createWindow = () => {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
|
|||||||
25
src/types.d.ts
vendored
Normal file
25
src/types.d.ts
vendored
Normal 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
1
src/types/theme-mode.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type ThemeMode = "dark" | "light" | "system";
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"],
|
||||||
"@stores/*": ["./src/stores/*"],
|
"@stores/*": ["./src/stores/*"],
|
||||||
"@utils/*": ["./src/utils/*"],
|
"@utils/*": ["./src/utils/*"],
|
||||||
"@api/*": ["./src/api/*"]
|
"@api/*": ["./src/api/*"],
|
||||||
|
"@types/*": ["./src/types/*"]
|
||||||
},
|
},
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export default defineConfig({
|
|||||||
"@stores": resolve(__dirname, "./src/stores"),
|
"@stores": resolve(__dirname, "./src/stores"),
|
||||||
"@utils": resolve(__dirname, "./src/utils"),
|
"@utils": resolve(__dirname, "./src/utils"),
|
||||||
"@api": resolve(__dirname, "./src/api"),
|
"@api": resolve(__dirname, "./src/api"),
|
||||||
|
"@types": resolve(__dirname, "./src/types"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default defineConfig({
|
|||||||
"@stores": resolve(__dirname, "./src/stores"),
|
"@stores": resolve(__dirname, "./src/stores"),
|
||||||
"@utils": resolve(__dirname, "./src/utils"),
|
"@utils": resolve(__dirname, "./src/utils"),
|
||||||
"@api": resolve(__dirname, "./src/api"),
|
"@api": resolve(__dirname, "./src/api"),
|
||||||
|
"@types": resolve(__dirname, "./src/types"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user