111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
import { useState } from "react";
|
|
import { parseSubtitleAppearance, type SubtitleAppearance } from "@/lib/subtitleAppearance";
|
|
import { SubtitleAppearancePanelView } from "@/components/settings/SubtitleAppearancePanelView";
|
|
import type { AdminDeviceSetting } from "@/hooks/queries/admin/users";
|
|
|
|
interface AdminSubtitleAppearanceDialogProps {
|
|
/**
|
|
* The setting being edited. When null the dialog is hidden. The setting
|
|
* may be a real override or a synthesized default-row from the manifest;
|
|
* `isOverride` decides whether the reset action is offered.
|
|
*/
|
|
setting: AdminDeviceSetting | null;
|
|
/**
|
|
* Whether the underlying setting is a real device override (vs. a
|
|
* synthesized default that hasn't been written yet). Drives the footer
|
|
* "Reset override" affordance.
|
|
*/
|
|
isOverride: boolean;
|
|
/** Close affordance — clear the parent's `setting` state. */
|
|
onClose: () => void;
|
|
/**
|
|
* Persist a new value as a JSON string. The parent fires the actual
|
|
* mutation (so the dialog stays decoupled from the network layer).
|
|
*/
|
|
onSave: (setting: AdminDeviceSetting, value: string) => void;
|
|
/** Remove the override on the device. Only invoked when `isOverride`. */
|
|
onReset: (setting: AdminDeviceSetting) => void;
|
|
/** Pending mutation flag — surfaces in the footer status. */
|
|
saving?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Admin override editor for the `subtitle_appearance` device setting.
|
|
* Reuses the same {@link SubtitleAppearancePanelView} the player presents
|
|
* to end users — same preview, same controls — so admin overrides land in
|
|
* exactly the same shape clients consume.
|
|
*
|
|
* The local appearance state is kept optimistic so dragging a slider feels
|
|
* instantaneous; whenever the parent's `setting.value` flips (because the
|
|
* admin device-detail cache invalidated after a save), we resync.
|
|
*/
|
|
export function AdminSubtitleAppearanceDialog({
|
|
setting,
|
|
isOverride,
|
|
onClose,
|
|
onSave,
|
|
onReset,
|
|
saving = false,
|
|
}: AdminSubtitleAppearanceDialogProps) {
|
|
const settingKey = setting?.key;
|
|
const settingValue = setting?.value ?? null;
|
|
|
|
const [appearance, setAppearance] = useState<SubtitleAppearance>(() =>
|
|
parseSubtitleAppearance(settingValue),
|
|
);
|
|
// Use the React "adjust state during render" pattern to resync optimistic
|
|
// state when the underlying setting changes — either because the dialog
|
|
// was opened on a different setting, or because a save round-tripped and
|
|
// produced a fresh `setting.value` from the cache. Avoids the cascading-
|
|
// render warning that a useEffect would trigger.
|
|
const [prev, setPrev] = useState<{ key: string | undefined; value: string | null }>({
|
|
key: settingKey,
|
|
value: settingValue,
|
|
});
|
|
if (prev.key !== settingKey || prev.value !== settingValue) {
|
|
setPrev({ key: settingKey, value: settingValue });
|
|
setAppearance(parseSubtitleAppearance(settingValue));
|
|
}
|
|
|
|
if (!setting) return null;
|
|
|
|
const handleChange = (patch: Partial<SubtitleAppearance>) => {
|
|
const next = { ...appearance, ...patch };
|
|
setAppearance(next);
|
|
onSave(setting, JSON.stringify(next));
|
|
};
|
|
|
|
const handleReset = () => {
|
|
onReset(setting);
|
|
// The cache will repopulate `setting.value` to the empty/default string;
|
|
// the effect above takes care of resyncing local state.
|
|
onClose();
|
|
};
|
|
|
|
// Eyebrow gives admin context that the player panel lacks: which user +
|
|
// device + profile the override is being scoped to.
|
|
const profileLabel = setting.profile_name?.trim() || setting.profile_id || "default";
|
|
const deviceLabel = setting.device_name?.trim() || "device";
|
|
const eyebrow = `${profileLabel} · ${deviceLabel}`;
|
|
|
|
return (
|
|
<SubtitleAppearancePanelView
|
|
open
|
|
value={appearance}
|
|
onChange={handleChange}
|
|
onClose={onClose}
|
|
canReset={isOverride}
|
|
onReset={handleReset}
|
|
resetLabel="Reset override"
|
|
eyebrow={eyebrow}
|
|
status={
|
|
saving
|
|
? "Saving…"
|
|
: isOverride
|
|
? "Changes saved automatically"
|
|
: "Changing a value creates an override"
|
|
}
|
|
/>
|
|
);
|
|
}
|