Compare commits
No commits in common. "d9f95c6864c21f9bb6c252f6040da3114f058740" and "488d2304be632bb325598e270e7feb5c2a10e3a9" have entirely different histories.
d9f95c6864
...
488d2304be
|
|
@ -413,9 +413,7 @@ export default function DeviceDetailOverlay({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(deviceData.cache !== undefined ||
|
{deviceData.cache !== undefined && (
|
||||||
(deviceData.base_url ?? '').includes('://') ||
|
|
||||||
(deviceData.url ?? '').includes('://')) && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm text-neutral-500 block mb-2">Cache</label>
|
<label className="text-sm text-neutral-500 block mb-2">Cache</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
|
||||||
|
|
@ -164,62 +164,34 @@ export async function readSettings(): Promise<ReadSettingsResult | null> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SavedJson {
|
|
||||||
configJson: string;
|
|
||||||
devicesJson: string;
|
|
||||||
/** True if at least one file was actually written. */
|
|
||||||
wrote: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Serialize config into the two canonical JSON strings without writing anything. */
|
|
||||||
export function serializeConfig(config: SettingsConfig): SavedJson {
|
|
||||||
const { devices, ...mainConfig } = config;
|
|
||||||
return {
|
|
||||||
configJson: JSON.stringify(mainConfig, null, 2) + '\n',
|
|
||||||
devicesJson: JSON.stringify({ devices }, null, 2) + '\n',
|
|
||||||
wrote: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist the settings object to the WebDAV server, creating the
|
* Persist the settings object to the WebDAV server, creating the
|
||||||
* containing `/.sys/` directory if needed. Pretty-prints with 2-space
|
* containing `/.sys/` directory if needed. Pretty-prints with 2-space
|
||||||
* indentation so the file is diff-friendly.
|
* indentation so the file is diff-friendly.
|
||||||
*
|
|
||||||
* Pass `lastSaved` to skip writing files whose content hasn't changed.
|
|
||||||
* Returns the canonical JSON strings that were written (or would have been).
|
|
||||||
*/
|
*/
|
||||||
export async function writeSettings(
|
export async function writeSettings(config: SettingsConfig): Promise<void> {
|
||||||
config: SettingsConfig,
|
|
||||||
lastSaved?: SavedJson | null,
|
|
||||||
): Promise<SavedJson> {
|
|
||||||
const next = serializeConfig(config);
|
|
||||||
|
|
||||||
const writeConfig = !lastSaved || next.configJson !== lastSaved.configJson;
|
|
||||||
const writeDevices = !lastSaved || next.devicesJson !== lastSaved.devicesJson;
|
|
||||||
|
|
||||||
if (!writeConfig && !writeDevices) return { ...next, wrote: false };
|
|
||||||
|
|
||||||
try { await createFolder(SETTINGS_DIR, true); } catch { /* exists */ }
|
try { await createFolder(SETTINGS_DIR, true); } catch { /* exists */ }
|
||||||
|
|
||||||
const writes: Promise<void>[] = [];
|
const { devices, ...mainConfig } = config;
|
||||||
if (writeConfig) writes.push(putFileContents(SETTINGS_PATH, next.configJson));
|
const configJson = JSON.stringify(mainConfig, null, 2) + '\n';
|
||||||
if (writeDevices) writes.push(putFileContents(DEVICES_PATH, next.devicesJson));
|
const devicesJson = JSON.stringify({ devices }, null, 2) + '\n';
|
||||||
await Promise.all(writes);
|
|
||||||
|
await Promise.all([
|
||||||
|
putFileContents(SETTINGS_PATH, configJson),
|
||||||
|
putFileContents(DEVICES_PATH, devicesJson),
|
||||||
|
]);
|
||||||
|
|
||||||
// Mirror to /sd/.sys/ if the SD card is mounted.
|
// Mirror to /sd/.sys/ if the SD card is mounted.
|
||||||
try {
|
try {
|
||||||
const sdStat = await stat('/sd');
|
const sdStat = await stat('/sd');
|
||||||
if (sdStat) {
|
if (sdStat) {
|
||||||
try { await createFolder(SD_SYS_DIR, true); } catch { /* exists */ }
|
try { await createFolder(SD_SYS_DIR, true); } catch { /* exists */ }
|
||||||
const sdWrites: Promise<void>[] = [];
|
await Promise.all([
|
||||||
if (writeConfig) sdWrites.push(putFileContents(SD_SYS_DIR + '/config.json', next.configJson));
|
putFileContents(SD_SYS_DIR + '/config.json', configJson),
|
||||||
if (writeDevices) sdWrites.push(putFileContents(SD_SYS_DIR + '/devices.json', next.devicesJson));
|
putFileContents(SD_SYS_DIR + '/devices.json', devicesJson),
|
||||||
await Promise.all(sdWrites);
|
]);
|
||||||
}
|
}
|
||||||
} catch { /* /sd unreachable — skip mirror */ }
|
} catch { /* /sd unreachable — skip mirror */ }
|
||||||
|
|
||||||
return { ...next, wrote: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SaveStatus =
|
export type SaveStatus =
|
||||||
|
|
@ -274,7 +246,6 @@ export function useSettings(): UseSettingsResult {
|
||||||
const dirtyRef = useRef<boolean>(false);
|
const dirtyRef = useRef<boolean>(false);
|
||||||
const pendingRef = useRef<number>(0);
|
const pendingRef = useRef<number>(0);
|
||||||
const savedTimerRef = useRef<number | null>(null);
|
const savedTimerRef = useRef<number | null>(null);
|
||||||
const lastSavedRef = useRef<SavedJson | null>(null);
|
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setSaveStatus('loading');
|
setSaveStatus('loading');
|
||||||
|
|
@ -286,7 +257,7 @@ export function useSettings(): UseSettingsResult {
|
||||||
// Persist the migrated config immediately so the device is updated.
|
// Persist the migrated config immediately so the device is updated.
|
||||||
setSaveStatus('saving');
|
setSaveStatus('saving');
|
||||||
try {
|
try {
|
||||||
lastSavedRef.current = await writeSettings(result.config);
|
await writeSettings(result.config);
|
||||||
setSaveStatus('saved');
|
setSaveStatus('saved');
|
||||||
savedTimerRef.current = window.setTimeout(() => {
|
savedTimerRef.current = window.setTimeout(() => {
|
||||||
setSaveStatus((s) => (s === 'saved' ? 'idle' : s));
|
setSaveStatus((s) => (s === 'saved' ? 'idle' : s));
|
||||||
|
|
@ -302,8 +273,6 @@ export function useSettings(): UseSettingsResult {
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Record what the server holds so flushNow can skip unchanged files.
|
|
||||||
lastSavedRef.current = serializeConfig(result.config);
|
|
||||||
}
|
}
|
||||||
loadedRef.current = true;
|
loadedRef.current = true;
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
|
|
@ -330,16 +299,12 @@ export function useSettings(): UseSettingsResult {
|
||||||
setPendingCount(0);
|
setPendingCount(0);
|
||||||
setSaveStatus('saving');
|
setSaveStatus('saving');
|
||||||
try {
|
try {
|
||||||
const result = await writeSettings(configRef.current, lastSavedRef.current);
|
await writeSettings(configRef.current);
|
||||||
lastSavedRef.current = result;
|
|
||||||
if (result.wrote) {
|
|
||||||
setSaveStatus('saved');
|
setSaveStatus('saved');
|
||||||
|
// Drop the "saved" badge after a short delay so it doesn't linger.
|
||||||
savedTimerRef.current = window.setTimeout(() => {
|
savedTimerRef.current = window.setTimeout(() => {
|
||||||
setSaveStatus((s) => (s === 'saved' ? 'idle' : s));
|
setSaveStatus((s) => (s === 'saved' ? 'idle' : s));
|
||||||
}, SAVED_INDICATOR_MS);
|
}, SAVED_INDICATOR_MS);
|
||||||
} else {
|
|
||||||
setSaveStatus('idle');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Roll back: the change is still pending.
|
// Roll back: the change is still pending.
|
||||||
dirtyRef.current = true;
|
dirtyRef.current = true;
|
||||||
|
|
@ -359,20 +324,19 @@ export function useSettings(): UseSettingsResult {
|
||||||
// Modern browsers: `fetch` with `keepalive: true` continues even
|
// Modern browsers: `fetch` with `keepalive: true` continues even
|
||||||
// after the page is being unloaded. We don't await it.
|
// after the page is being unloaded. We don't await it.
|
||||||
try {
|
try {
|
||||||
const next = serializeConfig(configRef.current);
|
const { devices, ...mainConfig } = configRef.current;
|
||||||
const last = lastSavedRef.current;
|
|
||||||
if (!last || next.configJson !== last.configJson) {
|
|
||||||
void fetch(absoluteUrl(SETTINGS_PATH), {
|
void fetch(absoluteUrl(SETTINGS_PATH), {
|
||||||
method: 'PUT', body: next.configJson,
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' }, keepalive: true,
|
body: JSON.stringify(mainConfig, null, 2) + '\n',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
keepalive: true,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
if (!last || next.devicesJson !== last.devicesJson) {
|
|
||||||
void fetch(absoluteUrl(DEVICES_PATH), {
|
void fetch(absoluteUrl(DEVICES_PATH), {
|
||||||
method: 'PUT', body: next.devicesJson,
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' }, keepalive: true,
|
body: JSON.stringify({ devices }, null, 2) + '\n',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
keepalive: true,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user