92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
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;
|
|
}
|
|
}
|