fix(web): explicit keep-alive Off/Keep toggle + roomier custom display form
Two UX fixes on the Virtual displays Configuration card: - Keep-alive is no longer implicitly "on" by typing in the seconds field. It's an explicit two-button toggle — **Off** (tear down at disconnect) vs. **Keep for** [N] seconds — and the seconds input only appears when "Keep for" is selected. The duration is remembered across toggles, and the help text explains both modes. - Opened up the cramped custom form: the fields container is `space-y-6` with more padding (`p-5`, rounded-lg), each option group is `space-y-2.5`, and the Save button sits below a divider — so it reads as sections with room instead of a pressed stack. web tsc + vite build + biome-lint green; deployed on .21. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@
|
|||||||
"display_preset_workstation": "Workstation",
|
"display_preset_workstation": "Workstation",
|
||||||
"display_keep_alive": "Nach Trennung aktiv halten",
|
"display_keep_alive": "Nach Trennung aktiv halten",
|
||||||
"display_keep_alive_off": "Aus",
|
"display_keep_alive_off": "Aus",
|
||||||
|
"display_keep_alive_keep": "Behalten für",
|
||||||
"display_keep_alive_seconds": "Sekunden",
|
"display_keep_alive_seconds": "Sekunden",
|
||||||
"display_topology": "Topologie",
|
"display_topology": "Topologie",
|
||||||
"display_topology_auto": "Automatisch",
|
"display_topology_auto": "Automatisch",
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
"display_custom_desc": "Jede Option selbst festlegen.",
|
"display_custom_desc": "Jede Option selbst festlegen.",
|
||||||
"display_preset_current": "Aktiv",
|
"display_preset_current": "Aktiv",
|
||||||
"display_preset_soon": "in Kürze",
|
"display_preset_soon": "in Kürze",
|
||||||
"display_keep_alive_help": "Wie lange eine Anzeige (und bei gamescope ihr Spiel) nach dem Trennen bestehen bleibt. 0 = sofort abbauen.",
|
"display_keep_alive_help": "„Aus“ baut die Anzeige sofort beim Trennen ab. Halte sie (und bei gamescope ihr Spiel) am Leben, damit ein schnelles Wiederverbinden sofort fortsetzt, statt neu aufzubauen.",
|
||||||
"display_topology_help": "Was mit den physischen Monitoren des Hosts während des Streamings geschieht.",
|
"display_topology_help": "Was mit den physischen Monitoren des Hosts während des Streamings geschieht.",
|
||||||
"display_conflict": "Wenn ein weiterer Client verbindet",
|
"display_conflict": "Wenn ein weiterer Client verbindet",
|
||||||
"display_conflict_help": "Was passiert, wenn ein zweiter Client verbindet, während bereits gestreamt wird, und eine andere Auflösung anfragt.",
|
"display_conflict_help": "Was passiert, wenn ein zweiter Client verbindet, während bereits gestreamt wird, und eine andere Auflösung anfragt.",
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
"display_preset_workstation": "Workstation",
|
"display_preset_workstation": "Workstation",
|
||||||
"display_keep_alive": "Keep alive after disconnect",
|
"display_keep_alive": "Keep alive after disconnect",
|
||||||
"display_keep_alive_off": "Off",
|
"display_keep_alive_off": "Off",
|
||||||
|
"display_keep_alive_keep": "Keep for",
|
||||||
"display_keep_alive_seconds": "seconds",
|
"display_keep_alive_seconds": "seconds",
|
||||||
"display_topology": "Topology",
|
"display_topology": "Topology",
|
||||||
"display_topology_auto": "Automatic",
|
"display_topology_auto": "Automatic",
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
"display_custom_desc": "Set every option yourself.",
|
"display_custom_desc": "Set every option yourself.",
|
||||||
"display_preset_current": "Active",
|
"display_preset_current": "Active",
|
||||||
"display_preset_soon": "coming soon",
|
"display_preset_soon": "coming soon",
|
||||||
"display_keep_alive_help": "How long a display (and, on gamescope, its game) survives after the client disconnects. 0 = tear down immediately.",
|
"display_keep_alive_help": "Off tears the display down as soon as the client disconnects. Keep it alive (and, on gamescope, its game) so a quick reconnect resumes instantly instead of rebuilding.",
|
||||||
"display_topology_help": "What happens to the host's physical monitors while streaming.",
|
"display_topology_help": "What happens to the host's physical monitors while streaming.",
|
||||||
"display_conflict": "When another client connects",
|
"display_conflict": "When another client connects",
|
||||||
"display_conflict_help": "What happens if a second client connects while one is already streaming and asks for a different resolution.",
|
"display_conflict_help": "What happens if a second client connects while one is already streaming and asks for a different resolution.",
|
||||||
|
|||||||
@@ -147,10 +147,11 @@ const DisplayForm: FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ka = customFields.keep_alive;
|
const ka = customFields.keep_alive;
|
||||||
const secondsValue = ka.mode === "duration" ? ka.seconds : 300;
|
// The duration value, remembered across the Off/Keep toggle so switching back restores it.
|
||||||
|
const [keepSecs, setKeepSecs] = useState(ka.mode === "duration" ? ka.seconds : 300);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
{/* One-click presets — a 2-up grid so each has room to breathe */}
|
{/* One-click presets — a 2-up grid so each has room to breathe */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="text-base font-semibold">{m.display_preset()}</Label>
|
<Label className="text-base font-semibold">{m.display_preset()}</Label>
|
||||||
@@ -208,10 +209,10 @@ const DisplayForm: FC<{
|
|||||||
|
|
||||||
{/* Custom: every option by hand */}
|
{/* Custom: every option by hand */}
|
||||||
{isCustom && (
|
{isCustom && (
|
||||||
<div className="space-y-4 rounded-md border p-4">
|
<div className="space-y-6 rounded-lg border p-5">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2.5">
|
||||||
<Label>{m.display_keep_alive()}</Label>
|
<Label>{m.display_keep_alive()}</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={ka.mode === "off" ? "default" : "outline"}
|
variant={ka.mode === "off" ? "default" : "outline"}
|
||||||
@@ -220,25 +221,35 @@ const DisplayForm: FC<{
|
|||||||
>
|
>
|
||||||
{m.display_keep_alive_off()}
|
{m.display_keep_alive_off()}
|
||||||
</Button>
|
</Button>
|
||||||
<Input
|
<Button
|
||||||
type="number"
|
size="sm"
|
||||||
min={0}
|
variant={ka.mode === "duration" ? "default" : "outline"}
|
||||||
className="w-24"
|
|
||||||
value={secondsValue}
|
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onChange={(e) =>
|
onClick={() =>
|
||||||
setDraft({
|
setDraft({ ...draft, keep_alive: { mode: "duration", seconds: keepSecs } })
|
||||||
...draft,
|
|
||||||
keep_alive: {
|
|
||||||
mode: "duration",
|
|
||||||
seconds: Math.max(0, Number(e.target.value) || 0),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
<span className="text-sm text-muted-foreground">
|
{m.display_keep_alive_keep()}
|
||||||
{m.display_keep_alive_seconds()}
|
</Button>
|
||||||
</span>
|
{ka.mode === "duration" && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
className="w-24"
|
||||||
|
value={ka.seconds}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => {
|
||||||
|
const n = Math.max(0, Number(e.target.value) || 0);
|
||||||
|
setKeepSecs(n);
|
||||||
|
setDraft({ ...draft, keep_alive: { mode: "duration", seconds: n } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{m.display_keep_alive_seconds()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">{m.display_keep_alive_help()}</p>
|
<p className="text-xs text-muted-foreground">{m.display_keep_alive_help()}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,7 +296,7 @@ const DisplayForm: FC<{
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2.5">
|
||||||
<Label htmlFor="disp-max">{m.display_max()}</Label>
|
<Label htmlFor="disp-max">{m.display_max()}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="disp-max"
|
id="disp-max"
|
||||||
@@ -304,9 +315,11 @@ const DisplayForm: FC<{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={() => apply(draft)} disabled={busy}>
|
<div className="border-t pt-4">
|
||||||
{m.display_save()}
|
<Button onClick={() => apply(draft)} disabled={busy}>
|
||||||
</Button>
|
{m.display_save()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -337,7 +350,7 @@ const Choice: FC<{
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onPick: (v: string) => void;
|
onPick: (v: string) => void;
|
||||||
}> = ({ label, help, value, options, labels, disabled, onPick }) => (
|
}> = ({ label, help, value, options, labels, disabled, onPick }) => (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2.5">
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{options.map((o) => (
|
{options.map((o) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user