94 lines
3.0 KiB
TypeScript
94 lines
3.0 KiB
TypeScript
|
|
import { DEFAULT_CUSTOM_ACCENT, isValidHslToken } from './settingsStateDefaults';
|
||
|
|
|
||
|
|
export type CustomAccentMutationSource = 'local' | 'incoming';
|
||
|
|
|
||
|
|
export type CustomAccentRecord = {
|
||
|
|
color: string;
|
||
|
|
version: number;
|
||
|
|
};
|
||
|
|
|
||
|
|
const FALLBACK_RECORD: CustomAccentRecord = {
|
||
|
|
color: DEFAULT_CUSTOM_ACCENT,
|
||
|
|
version: 0,
|
||
|
|
};
|
||
|
|
|
||
|
|
function normalizeAccentColor(raw: unknown): string | null {
|
||
|
|
if (typeof raw !== 'string') return null;
|
||
|
|
const trimmed = raw.trim();
|
||
|
|
return isValidHslToken(trimmed) ? trimmed : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parse persisted / IPC custom-accent payloads.
|
||
|
|
* Accepts legacy plain HSL tokens ("221.2 83.2% 53.3%") and versioned records.
|
||
|
|
*/
|
||
|
|
export function parseCustomAccentRecord(raw: unknown): CustomAccentRecord {
|
||
|
|
if (typeof raw === 'string') {
|
||
|
|
const trimmed = raw.trim();
|
||
|
|
if (trimmed.startsWith('{')) {
|
||
|
|
try {
|
||
|
|
return parseCustomAccentRecord(JSON.parse(trimmed));
|
||
|
|
} catch {
|
||
|
|
// fall through to plain HSL
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const color = normalizeAccentColor(trimmed);
|
||
|
|
if (color) return { color, version: 0 };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (raw && typeof raw === 'object') {
|
||
|
|
const record = raw as { color?: unknown; version?: unknown };
|
||
|
|
const color = normalizeAccentColor(record.color) ?? FALLBACK_RECORD.color;
|
||
|
|
const version = Number(record.version);
|
||
|
|
return {
|
||
|
|
color,
|
||
|
|
version: Number.isFinite(version) && version > 0 ? Math.floor(version) : 0,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return { ...FALLBACK_RECORD };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function serializeCustomAccentRecord(record: CustomAccentRecord): string {
|
||
|
|
const color = normalizeAccentColor(record.color) ?? FALLBACK_RECORD.color;
|
||
|
|
return JSON.stringify({
|
||
|
|
color,
|
||
|
|
version: Math.max(0, Math.floor(record.version) || 0),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Incoming peer updates must not clobber a newer local/drag revision.
|
||
|
|
* Equal versions are treated as already-applied (no state thrash).
|
||
|
|
*/
|
||
|
|
export function shouldApplyCustomAccentRecord(
|
||
|
|
current: CustomAccentRecord,
|
||
|
|
incoming: CustomAccentRecord,
|
||
|
|
): boolean {
|
||
|
|
if (incoming.version > current.version) return true;
|
||
|
|
if (incoming.version < current.version) return false;
|
||
|
|
// Same version: only apply when the color itself differs and both are
|
||
|
|
// legacy/unversioned (version 0), so first-load plain strings still sync.
|
||
|
|
if (incoming.version === 0 && current.version === 0) {
|
||
|
|
return incoming.color !== current.color;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Decide whether a custom-accent state change should be rebroadcast to peer
|
||
|
|
* windows. Incoming IPC/storage updates must not notify again - otherwise a
|
||
|
|
* fast native color-picker drag in the settings window ping-pongs with the
|
||
|
|
* main window and the accent CSS variables oscillate (see #2743, same class
|
||
|
|
* as window-opacity #2018).
|
||
|
|
*/
|
||
|
|
export function shouldBroadcastCustomAccentChange(
|
||
|
|
mutationSource: CustomAccentMutationSource,
|
||
|
|
persistMounted: boolean,
|
||
|
|
): { shouldBroadcast: boolean; nextSource: CustomAccentMutationSource } {
|
||
|
|
if (mutationSource === 'incoming') {
|
||
|
|
return { shouldBroadcast: false, nextSource: 'local' };
|
||
|
|
}
|
||
|
|
return { shouldBroadcast: persistMounted, nextSource: 'local' };
|
||
|
|
}
|