feat: Theme engine

big changes
This commit is contained in:
2026-04-02 00:08:38 +02:00
parent 65b9419869
commit bbb6deb0a2
48 changed files with 6150 additions and 235 deletions

View File

@@ -39,6 +39,13 @@ import {
getWindowIconPath,
updateCloseToTraySetting
} from '../window/create-window';
import {
deleteSavedTheme,
getSavedThemesPath,
listSavedThemes,
readSavedTheme,
writeSavedTheme
} from '../theme-library';
const DEFAULT_MIME_TYPE = 'application/octet-stream';
const FILE_CLIPBOARD_FORMATS = [
@@ -325,6 +332,15 @@ export function setupSystemHandlers(): void {
});
ipcMain.handle('get-app-data-path', () => app.getPath('userData'));
ipcMain.handle('get-saved-themes-path', async () => await getSavedThemesPath());
ipcMain.handle('list-saved-themes', async () => await listSavedThemes());
ipcMain.handle('read-saved-theme', async (_event, fileName: string) => await readSavedTheme(fileName));
ipcMain.handle('write-saved-theme', async (_event, fileName: string, text: string) => {
return await writeSavedTheme(fileName, text);
});
ipcMain.handle('delete-saved-theme', async (_event, fileName: string) => {
return await deleteSavedTheme(fileName);
});
ipcMain.handle('get-desktop-settings', () => getDesktopSettingsSnapshot());

View File

@@ -102,6 +102,12 @@ export interface WindowStateSnapshot {
isMinimized: boolean;
}
export interface SavedThemeFileDescriptor {
fileName: string;
modifiedAt: number;
path: string;
}
function readLinuxDisplayServer(): string {
if (process.platform !== 'linux') {
return 'N/A';
@@ -134,6 +140,11 @@ export interface ElectronAPI {
onLinuxScreenShareMonitorAudioChunk: (listener: (payload: LinuxScreenShareMonitorAudioChunkPayload) => void) => () => void;
onLinuxScreenShareMonitorAudioEnded: (listener: (payload: LinuxScreenShareMonitorAudioEndedPayload) => void) => () => void;
getAppDataPath: () => Promise<string>;
getSavedThemesPath: () => Promise<string>;
listSavedThemes: () => Promise<SavedThemeFileDescriptor[]>;
readSavedTheme: (fileName: string) => Promise<string>;
writeSavedTheme: (fileName: string, text: string) => Promise<boolean>;
deleteSavedTheme: (fileName: string) => Promise<boolean>;
consumePendingDeepLink: () => Promise<string | null>;
getDesktopSettings: () => Promise<{
autoUpdateMode: 'auto' | 'off' | 'version';
@@ -230,6 +241,11 @@ const electronAPI: ElectronAPI = {
};
},
getAppDataPath: () => ipcRenderer.invoke('get-app-data-path'),
getSavedThemesPath: () => ipcRenderer.invoke('get-saved-themes-path'),
listSavedThemes: () => ipcRenderer.invoke('list-saved-themes'),
readSavedTheme: (fileName) => ipcRenderer.invoke('read-saved-theme', fileName),
writeSavedTheme: (fileName, text) => ipcRenderer.invoke('write-saved-theme', fileName, text),
deleteSavedTheme: (fileName) => ipcRenderer.invoke('delete-saved-theme', fileName),
consumePendingDeepLink: () => ipcRenderer.invoke('consume-pending-deep-link'),
getDesktopSettings: () => ipcRenderer.invoke('get-desktop-settings'),
showDesktopNotification: (payload) => ipcRenderer.invoke('show-desktop-notification', payload),

92
electron/theme-library.ts Normal file
View File

@@ -0,0 +1,92 @@
import { app } from 'electron';
import * as fsp from 'fs/promises';
import * as path from 'path';
export interface SavedThemeFileDescriptor {
fileName: string;
modifiedAt: number;
path: string;
}
const SAVED_THEME_FILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\.json$/;
function resolveSavedThemesPath(): string {
return path.join(app.getPath('userData'), 'themes');
}
async function ensureSavedThemesPath(): Promise<string> {
const themesPath = resolveSavedThemesPath();
await fsp.mkdir(themesPath, { recursive: true });
return themesPath;
}
function assertSavedThemeFileName(fileName: string): string {
const normalized = typeof fileName === 'string'
? fileName.trim()
: '';
if (!SAVED_THEME_FILE_NAME_PATTERN.test(normalized) || normalized.includes('..')) {
throw new Error('Invalid saved theme file name.');
}
return normalized;
}
async function resolveSavedThemeFilePath(fileName: string): Promise<string> {
const themesPath = await ensureSavedThemesPath();
return path.join(themesPath, assertSavedThemeFileName(fileName));
}
export async function getSavedThemesPath(): Promise<string> {
return await ensureSavedThemesPath();
}
export async function listSavedThemes(): Promise<SavedThemeFileDescriptor[]> {
const themesPath = await ensureSavedThemesPath();
const entries = await fsp.readdir(themesPath, { withFileTypes: true });
const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.json'));
const descriptors = await Promise.all(files.map(async (entry) => {
const filePath = path.join(themesPath, entry.name);
const stats = await fsp.stat(filePath);
return {
fileName: entry.name,
modifiedAt: Math.round(stats.mtimeMs),
path: filePath
} satisfies SavedThemeFileDescriptor;
}));
return descriptors.sort((left, right) => right.modifiedAt - left.modifiedAt || left.fileName.localeCompare(right.fileName));
}
export async function readSavedTheme(fileName: string): Promise<string> {
const filePath = await resolveSavedThemeFilePath(fileName);
return await fsp.readFile(filePath, 'utf8');
}
export async function writeSavedTheme(fileName: string, text: string): Promise<boolean> {
const filePath = await resolveSavedThemeFilePath(fileName);
await fsp.writeFile(filePath, text, 'utf8');
return true;
}
export async function deleteSavedTheme(fileName: string): Promise<boolean> {
const filePath = await resolveSavedThemeFilePath(fileName);
try {
await fsp.unlink(filePath);
return true;
} catch (error) {
if ((error as { code?: string }).code === 'ENOENT') {
return true;
}
throw error;
}
}