diff --git a/src/options/pages/WebUIsPage.tsx b/src/options/pages/WebUIsPage.tsx index bd97a13..9160ff7 100644 --- a/src/options/pages/WebUIsPage.tsx +++ b/src/options/pages/WebUIsPage.tsx @@ -1,19 +1,35 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useSettings } from "../SettingsContext"; import ChipList from "../components/ChipList"; import AutoLabelDirSettingsEditor from "../components/AutoLabelDirSettingsEditor"; import Select from "../components/Select"; -import { Client, ClientDisplayName, WebUIFactory } from "../../models/clients"; +import { Client, ClientClassByClient, ClientDisplayName, WebUIFactory } from "../../models/clients"; import type { WebUISettings } from "../../models/webui"; import Toggle from "../components/Toggle"; import { generateId } from "../../util/utils"; const clientOptions = Object.values(Client).map(c => ({ value: c, label: ClientDisplayName[c] })); +function isClientSelected(client: Client | ""): client is Client { + return !!client && client in ClientClassByClient; +} + +const fieldInputStyle: React.CSSProperties = { + fontSize: 15, + borderRadius: 8, + padding: "6px 12px", + border: "1px solid var(--rta-border, #b7c9a7)", + background: "var(--rta-input-bg, #fff)", + color: "var(--rta-text, #1b241d)", +}; + function getDefaultWebUISettings(): WebUISettings { return { id: generateId(), - client: Client.QBittorrentWebUI, + // No client is chosen yet: the user picks one in the detail panel, which is + // what reveals the rest of the configuration. Until then this entry is a + // draft and is filtered out by consumers that resolve a concrete client. + client: "" as Client, name: "", host: "", port: 80, @@ -33,23 +49,106 @@ function getDefaultWebUISettings(): WebUISettings { }; } -interface WebUIEditorProps { +interface WebUIListItemProps { + webui: WebUISettings; + selected: boolean; + isPrimary: boolean; + onSelect: () => void; + onNameChange: (name: string) => void; +} + +function WebUIListItem({ webui, selected, isPrimary, onSelect, onNameChange }: WebUIListItemProps) { + const subtitle = isClientSelected(webui.client) ? ClientDisplayName[webui.client] : "No client selected"; + + return ( +
+
+ {selected ? ( + e.stopPropagation()} + onChange={e => onNameChange(e.target.value)} + placeholder="Unnamed WebUI" + style={{ + ...fieldInputStyle, + flex: 1, + minWidth: 0, + padding: "4px 8px", + fontWeight: 600, + }} + /> + ) : ( + + {webui.name || "Unnamed WebUI"} + + )} + {isPrimary && ( + + Primary + + )} +
+ + {subtitle} + +
+ ); +} + +interface WebUIDetailProps { webui: WebUISettings; onChange: (w: WebUISettings) => void; onRemove: () => void; onPromote: () => void; - isPrimary?: boolean; + isPrimary: boolean; } -function WebUIEditor({ webui, onChange, onRemove, onPromote, isPrimary }: WebUIEditorProps) { +function WebUIDetail({ webui, onChange, onRemove, onPromote, isPrimary }: WebUIDetailProps) { const [confirmRemove, setConfirmRemove] = useState(false); - const webUiInstance = WebUIFactory.createWebUI(webui); + const clientChosen = isClientSelected(webui.client); + const webUiInstance = clientChosen ? WebUIFactory.createWebUI(webui) : null; return ( -
-
- {webui.name || "Unnamed WebUI"} - {typeof onPromote === 'function' && !isPrimary && ( +
+
+ {webui.name || "Unnamed WebUI"} + {!isPrimary && ( )} @@ -79,7 +177,7 @@ function WebUIEditor({ webui, onChange, onRemove, onPromote, isPrimary }: WebUIE fontWeight: 700, cursor: "pointer", boxShadow: confirmRemove ? "0 0 8px 2px var(--rta-danger-dark, #8B0000)" : undefined, - transition: "all 0.15s" + transition: "all 0.15s", }} > {confirmRemove ? "For real?" : "Remove"} @@ -95,196 +193,228 @@ function WebUIEditor({ webui, onChange, onRemove, onPromote, isPrimary }: WebUIE padding: "6px 12px", fontWeight: 500, cursor: "pointer", - marginLeft: 8 }} >Cancel )}
- {/* Client + Name */} -
+ + {/* Client type: choosable while still a draft, locked once chosen. */} +
onChange({ ...webui, name: e.target.value })} - style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 180 }} - /> -
-
- {/* Host + Port + Secure + Relative Path */} -
-
- - onChange({ ...webui, host: e.target.value })} style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 120 }} /> -
-
- - onChange({ ...webui, port: Number(e.target.value) })} style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 80 }} /> -
- onChange({ ...webui, secure: v })} label="Secure (HTTPS)" /> -
- - onChange({ ...webui, relativePath: e.target.value })} style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 120 }} /> -
-
Base URL for API calls: {webUiInstance?.createBaseUrl()}
- {/* Username + Password */} -
-
- - onChange({ ...webui, username: e.target.value })} style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 120 }} /> -
-
- - onChange({ ...webui, password: e.target.value })} style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 120 }} /> -
-
- {/* Only show these fields if supported by the WebUI instance */} - {webUiInstance?.isAddPausedSupported && ( -
- {/* Torrents Paused Toggle */} - onChange({ ...webui, addPaused: v })} label="Add torrents paused" /> -
- )} - {webUiInstance?.isLabelDirChooserSupported && ( -
- {/* Per-torrent config selector toggle */} - onChange({ ...webui, showPerTorrentConfigSelector: v })} label="Show per-torrent config selector" /> - {/* Alternative label/dir chooser toggle */} - onChange({ ...webui, useAlternativeLabelDirChooser: v })} label="Use alternative container (window instead of popup)" /> + + {!clientChosen ? ( +
+ Select a client type above to configure this WebUI.
- )} - {webUiInstance?.isLabelSupported && ( + ) : ( <> -
- - onChange({ ...webui, defaultLabel: e.target.value })} - style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 180 }} - placeholder="Default label" - /> + {/* Host + Port + Secure + Relative Path */} +
+
+ + onChange({ ...webui, host: e.target.value })} style={{ ...fieldInputStyle, minWidth: 120 }} /> +
+
+ + onChange({ ...webui, port: Number(e.target.value) })} style={{ ...fieldInputStyle, minWidth: 80 }} /> +
+ onChange({ ...webui, secure: v })} label="Secure (HTTPS)" /> +
+ + onChange({ ...webui, relativePath: e.target.value })} style={{ ...fieldInputStyle, minWidth: 120 }} /> +
- onChange({ ...webui, labels })} placeholder="Add label" /> - - )} - {webUiInstance?.isDirSupported && ( - <> -
- - onChange({ ...webui, defaultDir: e.target.value })} - style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 180 }} - placeholder="Default directory" - /> +
Base URL for API calls: {webUiInstance?.createBaseUrl()}
+ {/* Username + Password */} +
+
+ + onChange({ ...webui, username: e.target.value })} style={{ ...fieldInputStyle, minWidth: 120 }} /> +
+
+ + onChange({ ...webui, password: e.target.value })} style={{ ...fieldInputStyle, minWidth: 120 }} /> +
- onChange({ ...webui, dirs })} placeholder="Add directory" /> + {/* Only show these fields if supported by the WebUI instance */} + {webUiInstance?.isAddPausedSupported && ( +
+ onChange({ ...webui, addPaused: v })} label="Add torrents paused" /> +
+ )} + {webUiInstance?.isLabelDirChooserSupported && ( +
+ onChange({ ...webui, showPerTorrentConfigSelector: v })} label="Show per-torrent config selector" /> + onChange({ ...webui, useAlternativeLabelDirChooser: v })} label="Use alternative container (window instead of popup)" /> +
+ )} + {webUiInstance?.isLabelSupported && ( + <> +
+ + onChange({ ...webui, defaultLabel: e.target.value })} + style={{ ...fieldInputStyle, minWidth: 180 }} + placeholder="Default label" + /> +
+ onChange({ ...webui, labels })} placeholder="Add label" /> + + )} + {webUiInstance?.isDirSupported && ( + <> +
+ + onChange({ ...webui, defaultDir: e.target.value })} + style={{ ...fieldInputStyle, minWidth: 180 }} + placeholder="Default directory" + /> +
+ onChange({ ...webui, dirs })} placeholder="Add directory" /> + + )} + {webUiInstance?.isLabelDirChooserSupported && ( + onChange({ ...webui, autoLabelDirSettings })} + showLabel={!!webUiInstance?.isLabelSupported} + showDir={!!webUiInstance?.isDirSupported} + labels={webui.labels} + dirs={webui.dirs} + /> + )} )} - {webUiInstance?.isLabelDirChooserSupported && ( - onChange({ ...webui, autoLabelDirSettings })} - showLabel={!!webUiInstance?.isLabelSupported} - showDir={!!webUiInstance?.isDirSupported} - labels={webui.labels} - dirs={webui.dirs} - /> - )} - {/*
-
ClientSpecificSettings: TODO: implement
-
*/}
); } export default function WebUIsPage() { const { settings, updateSetting, loading } = useSettings(); - const [adding, setAdding] = useState(false); - const [newName, setNewName] = useState(""); - const [newClient, setNewClient] = useState(""); + const [selectedId, setSelectedId] = useState(null); + + const webuis = settings?.webuiSettings ?? []; + + // Default to the primary WebUI so the page never opens to an empty panel, + // and keep the selection valid if the selected entry is removed. + useEffect(() => { + if (webuis.length === 0) { + if (selectedId !== null) setSelectedId(null); + } else if (!webuis.some(w => w.id === selectedId)) { + setSelectedId(webuis[0].id); + } + }, [webuis, selectedId]); if (loading || !settings) return
Loading...
; const handleAdd = () => { - if (!newName || !newClient) return; - const newWebUI: WebUISettings = { ...getDefaultWebUISettings(), name: newName, client: newClient as Client }; - updateSetting("webuiSettings", [...settings.webuiSettings, newWebUI]); - setAdding(false); - setNewName(""); - setNewClient(""); + const newWebUI = getDefaultWebUISettings(); + updateSetting("webuiSettings", [...webuis, newWebUI]); + setSelectedId(newWebUI.id); }; - const handleChange = (idx: number, updated: WebUISettings) => { - const arr = [...settings.webuiSettings]; - arr[idx] = updated; - updateSetting("webuiSettings", arr); + const handleChange = (id: string, updated: WebUISettings) => { + updateSetting("webuiSettings", webuis.map(w => (w.id === id ? updated : w))); }; - const handleRemove = (idx: number) => { - const arr = [...settings.webuiSettings]; - arr.splice(idx, 1); - updateSetting("webuiSettings", arr); + const handleRemove = (id: string) => { + updateSetting("webuiSettings", webuis.filter(w => w.id !== id)); }; - const handlePromote = (idx: number) => { - if (idx === 0) return; // already primary - const arr = [...settings.webuiSettings]; + const handlePromote = (id: string) => { + const idx = webuis.findIndex(w => w.id === id); + if (idx <= 0) return; // not found or already primary + const arr = [...webuis]; const [item] = arr.splice(idx, 1); arr.unshift(item); updateSetting("webuiSettings", arr); }; + const selected = webuis.find(w => w.id === selectedId) ?? null; + return ( -
- {adding ? ( -
-

Add New WebUI

- setNewName(e.target.value)} - style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 180 }} - /> + ) : ( +
+ Select a WebUI on the left, or click + to add a new one.
- - -
- ) : ( - - )} - {settings.webuiSettings.length === 0 && !adding && ( -
No WebUIs configured yet.
- )} - {settings.webuiSettings.map((webui, idx) => ( - handleChange(idx, updated)} - onRemove={() => handleRemove(idx)} - onPromote={() => handlePromote(idx)} - isPrimary={idx === 0} - /> - ))} + )} +
); }