mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(desktop): live-reload linked css theme files (#3099)
This commit is contained in:
1 parent
21cb7ba69c
commit
b375abc20a
51 files changed
+4353
-1510
No files matched your search
@@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Buffer} from 'node:buffer';
|
||||
import fs from 'node:fs';
|
||||
import type {ThemeLinkedFileChange} from '@electron/common/Types';
|
||||
|
||||
const THEME_CSS_MAX_BYTES = 1024 * 1024;
|
||||
|
||||
interface ThemeLinkedFileWatchEntry {
|
||||
listener: () => void;
|
||||
generation: number;
|
||||
}
|
||||
|
||||
export async function readThemeCssFile(filePath: string): Promise<ThemeLinkedFileChange> {
|
||||
let handle: fs.promises.FileHandle | null = null;
|
||||
try {
|
||||
const linkStats = await fs.promises.lstat(filePath);
|
||||
if (!linkStats.isFile()) {
|
||||
return {path: filePath, error: 'not_file'};
|
||||
}
|
||||
handle = await fs.promises.open(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
||||
const stats = await handle.stat();
|
||||
if (!stats.isFile()) {
|
||||
return {path: filePath, error: 'not_file'};
|
||||
}
|
||||
if (stats.size > THEME_CSS_MAX_BYTES) {
|
||||
return {path: filePath, error: 'too_large'};
|
||||
}
|
||||
const buffer = Buffer.alloc(THEME_CSS_MAX_BYTES + 1);
|
||||
let bytesRead = 0;
|
||||
while (bytesRead < buffer.length) {
|
||||
const result = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
|
||||
if (result.bytesRead === 0) break;
|
||||
bytesRead += result.bytesRead;
|
||||
}
|
||||
if (bytesRead > THEME_CSS_MAX_BYTES) {
|
||||
return {path: filePath, error: 'too_large'};
|
||||
}
|
||||
return {path: filePath, css: buffer.toString('utf8', 0, bytesRead).replace(/^\uFEFF/, '')};
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException | null)?.code;
|
||||
if (code === 'ENOENT') {
|
||||
return {path: filePath, error: 'missing'};
|
||||
}
|
||||
if (code === 'ELOOP') {
|
||||
return {path: filePath, error: 'not_file'};
|
||||
}
|
||||
return {path: filePath, error: 'read_failed'};
|
||||
} finally {
|
||||
await handle?.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
export function createThemeLinkedFileWatchSet(
|
||||
emit: (change: ThemeLinkedFileChange) => void,
|
||||
intervalMs = 250,
|
||||
): {replace: (paths: ReadonlyArray<string>) => void; dispose: () => void} {
|
||||
const entries = new Map<string, ThemeLinkedFileWatchEntry>();
|
||||
const read = (filePath: string): void => {
|
||||
const entry = entries.get(filePath);
|
||||
if (!entry) return;
|
||||
entry.generation += 1;
|
||||
const generation = entry.generation;
|
||||
void readThemeCssFile(filePath).then((change) => {
|
||||
const current = entries.get(filePath);
|
||||
if (current !== entry || current.generation !== generation) return;
|
||||
emit(change);
|
||||
});
|
||||
};
|
||||
const unwatch = (filePath: string): void => {
|
||||
const entry = entries.get(filePath);
|
||||
if (!entry) return;
|
||||
fs.unwatchFile(filePath, entry.listener);
|
||||
entries.delete(filePath);
|
||||
};
|
||||
return {
|
||||
replace(paths) {
|
||||
const next = new Set(paths);
|
||||
for (const filePath of [...entries.keys()]) {
|
||||
if (!next.has(filePath)) unwatch(filePath);
|
||||
}
|
||||
for (const filePath of next) {
|
||||
if (entries.has(filePath)) continue;
|
||||
const entry: ThemeLinkedFileWatchEntry = {listener: () => read(filePath), generation: 0};
|
||||
entries.set(filePath, entry);
|
||||
fs.watchFile(filePath, {interval: intervalMs, persistent: false}, entry.listener);
|
||||
read(filePath);
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
for (const filePath of [...entries.keys()]) unwatch(filePath);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user