The device settings screen shipped with three defects: - Sliders (audio sync, playback speed, subtitle sync, next-up prompt, sleep timer) were rendered controlled with only onValueCommit, so the thumb never moved and no gesture could change the value. A shared SettingSlider now keeps a draft during the drag and commits once on release; the admin RegistrySettingControl had the same bug and uses it too. - "Change how they look" called an onOpenPanel the page never passed. The screen now opens the shared SubtitleAppearancePanelView scoped to the selected device and profile, with a reset that clears the row. - Every setting was shown for every device, so a browser offered "Screen orientation" and native-only toggles. settingsgen now emits the manifest's advisory platforms field into the TypeScript contract, and the screen hides settings whose platforms exclude the target device — unless the device stores a value, which must stay clearable. audio_sync_ms, dolby_vision_enabled and dv_profile7_hdr10_fallback gain native-only tags (no web consumer exists); platforms is advisory UI metadata, so no revision bump. Kotlin/Swift generators deliberately unchanged: the native clients hardcode their own applicability today, and their vendored bindings are regenerated from their own repos. Part of #215 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
import { useState } from "react";
|
|
|
|
import { Slider } from "@/components/ui/slider";
|
|
|
|
interface SettingSliderProps {
|
|
/** The persisted value. The thumb returns here if a save is rejected. */
|
|
value: number;
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
unit?: string;
|
|
disabled?: boolean;
|
|
"aria-label"?: string;
|
|
/** Fired once per gesture, when the thumb is released or a key is pressed. */
|
|
onCommit: (value: number) => void;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* A slider bound to a saved setting.
|
|
*
|
|
* Radix's Slider is controlled here, so the thumb only moves if something
|
|
* re-renders it with a new value. Persisting on every pointer move would flood
|
|
* the API, but rendering only the saved value makes the control inert — the
|
|
* thumb snaps back under the pointer and onValueCommit never sees a change,
|
|
* which is exactly the bug this component exists to prevent. While a drag is
|
|
* in flight the draft value drives the thumb and the readout; on release the
|
|
* draft is committed and cleared, and the saved value takes over again.
|
|
*/
|
|
export function SettingSlider({
|
|
value,
|
|
min,
|
|
max,
|
|
step,
|
|
unit,
|
|
disabled = false,
|
|
"aria-label": ariaLabel,
|
|
onCommit,
|
|
className,
|
|
}: SettingSliderProps) {
|
|
const [draft, setDraft] = useState<number | null>(null);
|
|
const shown = draft ?? value;
|
|
|
|
return (
|
|
<div className={className ?? "flex w-full items-center gap-3 sm:max-w-[260px]"}>
|
|
<Slider
|
|
value={[shown]}
|
|
min={min}
|
|
max={max}
|
|
step={step}
|
|
disabled={disabled}
|
|
aria-label={ariaLabel}
|
|
thumbLabels={ariaLabel ? [ariaLabel] : undefined}
|
|
onValueChange={(values) => setDraft(values[0] ?? shown)}
|
|
onValueCommit={(values) => {
|
|
setDraft(null);
|
|
const next = values[0];
|
|
if (next !== undefined && next !== value) onCommit(next);
|
|
}}
|
|
/>
|
|
<span className="text-muted-foreground min-w-16 text-right text-xs font-medium tabular-nums">
|
|
{shown}
|
|
{unit ? ` ${unit}` : ""}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|