From 5747a50932221b6d248663e7722b218a62eb7a41 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 15 Aug 2026 18:46:18 +0200 Subject: [PATCH] feat(accounts): add precise service filtering Add an explicit service selector to the account creation dialog while keeping the text field focused on access types such as API and web login. Preserve the result area dimensions during filtering and allow Enter to choose the first visible account type. Cover filtering, keyboard selection, translations, and stable list geometry with focused regression tests. --- src/renderer/App.tsx | 40 ++++--------- src/renderer/account-ui.ts | 21 +++++++ src/renderer/i18n.ts | 2 +- .../views/settings/AccountWorkspace.tsx | 54 ++++++++++++------ src/renderer/views/settings/settings.css | 16 +++++- tests/account-ui.test.ts | 14 +++++ tests/settings-view.test.tsx | 56 ++++++++++++++++--- 7 files changed, 147 insertions(+), 56 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 23891c7..631084e 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -40,8 +40,7 @@ import { } from "../shared/provider-daily-limits"; import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order"; import { pruneSelection, releaseAccountSelectionFocus, resolveEscapeSelectionScope, shouldClearDownloadSelection } from "./selection"; -import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate, updateAccountRowSelection } from "./account-ui"; -import type { AccountModeFilter } from "./account-ui"; +import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, filterAccountDialogOptions, getAccountDialogSelectableOptions, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate, updateAccountRowSelection } from "./account-ui"; import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit"; import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit"; import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons"; @@ -1614,7 +1613,7 @@ export function App(): ReactElement { const [accountEditSecretVisible, setAccountEditSecretVisible] = useState>({}); const [accountEditSecretBusy, setAccountEditSecretBusy] = useState(null); const [accountDialogSearch, setAccountDialogSearch] = useState(""); - const [accountDialogModeFilter, setAccountDialogModeFilter] = useState("all"); + const [accountDialogServiceFilter, setAccountDialogServiceFilter] = useState("all"); const [keyStatsPopup, setKeyStatsPopup] = useState(null); const [debridLinkHostLimits, setDebridLinkHostLimits] = useState>({}); const [debridLinkHostLimitsLoading, setDebridLinkHostLimitsLoading] = useState(false); @@ -2432,28 +2431,12 @@ export function App(): ReactElement { accountDialog.service ); }, [accountDialog, availableAccountOptions]); - const accountDialogSearchQuery = accountDialogSearch.trim().toLowerCase(); + const accountDialogServiceFilters = useMemo(() => ( + [...new Set(accountDialogSelectableOptions.map((option) => option.serviceLabel))] + ), [accountDialogSelectableOptions]); const filteredAccountDialogOptions = useMemo(() => ( - accountDialogSelectableOptions.filter((option) => { - const matchesMode = accountDialogModeFilter === "all" - || (accountDialogModeFilter === "api" && option.modeLabel === "API") - || (accountDialogModeFilter === "web" && (option.modeLabel.startsWith("Web") || option.kind === "ddownload-login" || option.kind === "linksnappy-login")); - if (!matchesMode) { - return false; - } - if (!accountDialogSearchQuery) { - return true; - } - const haystack = [ - option.title, - option.serviceLabel, - option.modeLabel, - option.pickerDescription, - getAccountPickerFunctionLabel(option) - ].join(" ").toLowerCase(); - return haystack.includes(accountDialogSearchQuery); - }) - ), [accountDialogModeFilter, accountDialogSearchQuery, accountDialogSelectableOptions]); + filterAccountDialogOptions(accountDialogSelectableOptions, accountDialogSearch, accountDialogServiceFilter) + ), [accountDialogSearch, accountDialogSelectableOptions, accountDialogServiceFilter]); const handleUpdateResult = async ( result: UpdateCheckResult, source: "manual" | "startup", @@ -2686,7 +2669,7 @@ export function App(): ReactElement { const openCreateAccountDialog = (): void => { setAccountDialogSearch(""); - setAccountDialogModeFilter("all"); + setAccountDialogServiceFilter("all"); setAccountDialog(createAccountDialogState("create", availableAccountOptions[0]?.kind ?? null, settingsDraft)); }; @@ -2725,7 +2708,7 @@ export function App(): ReactElement { const closeAccountDialog = useCallback((): void => { setAccountDialog(null); setAccountDialogSearch(""); - setAccountDialogModeFilter("all"); + setAccountDialogServiceFilter("all"); }, []); const closeAccountEditDialog = useCallback((): void => { @@ -5186,7 +5169,7 @@ export function App(): ReactElement { updateAccountDialogKind(optionId as AccountKind), onFieldChange: (fieldId, value) => setAccountDialog((current) => current ? { ...current, [fieldId]: value } : current), onClose: closeAccountDialog, @@ -5198,7 +5181,8 @@ export function App(): ReactElement { model={{ open: Boolean(accountDialog), query: accountDialogSearch, - filter: accountDialogModeFilter, + serviceFilter: accountDialogServiceFilter, + serviceFilters: accountDialogServiceFilters, options: accountAddOptions, selectedOptionId: accountDialog?.kind ?? null, fields: accountAddFields, diff --git a/src/renderer/account-ui.ts b/src/renderer/account-ui.ts index 3edb712..f27f2c8 100644 --- a/src/renderer/account-ui.ts +++ b/src/renderer/account-ui.ts @@ -16,6 +16,27 @@ export function matchesAccountModeFilter(option: AccountModeOption, filter: Acco return option.modeLabel.startsWith("Web"); } +export function filterAccountDialogOptions(options: readonly T[], query: string, serviceFilter: string): T[] { + const normalizedQuery = query.trim().toLocaleLowerCase("de-DE"); + return options.filter((option) => { + if (serviceFilter !== "all" && option.serviceLabel !== serviceFilter) { + return false; + } + if (!normalizedQuery) { + return true; + } + return [option.title, option.modeLabel, option.pickerDescription] + .join(" ") + .toLocaleLowerCase("de-DE") + .includes(normalizedQuery); + }); +} + export function buildConfiguredProviderOrder( currentOrder: readonly DebridProvider[], configuredProviders: readonly DebridProvider[] diff --git a/src/renderer/i18n.ts b/src/renderer/i18n.ts index abc4f67..9a54ddf 100644 --- a/src/renderer/i18n.ts +++ b/src/renderer/i18n.ts @@ -75,7 +75,7 @@ const pairs = [ ["Füge einen Account hinzu, um Downloads über einen Anbieter zu starten.", "Add an account to start downloads through a provider."], ["Noch keine Accounts", "No accounts yet"], ["Keine Provider konfiguriert.", "No providers configured."], ["Keine eigenen Zuordnungen.", "No custom assignments."], ["Hoster-Routing hinzufügen", "Add hoster routing"], ["Hoster hinzufügen…", "Add hoster…"], ["Eigener Hoster…", "Custom hoster…"], ["Noch keine Rotations-Ereignisse.", "No rotation events yet."], ["Prüfen und speichern", "Check and save"], ["Wähle einen Dienst und trage die passenden Zugangsdaten ein.", "Choose a service and enter the matching credentials."], ["Accounts durchsuchen", "Search accounts"], - ["Dienst / Zugangstyp", "Service / access type"], ["Dienst", "Service"], ["Typ/Funktion", "Type/function"], ["Dienst oder Zugangstyp suchen", "Search service or access type"], ["Account-Typ filtern", "Filter account type"], ["Verfügbare Account-Typen", "Available account types"], ["Keine passenden Account-Typen.", "No matching account types."], + ["Dienst / Zugangstyp", "Service / access type"], ["Dienst", "Service"], ["Typ/Funktion", "Type/function"], ["Dienst oder Zugangstyp suchen", "Search service or access type"], ["Zugangstyp suchen, z. B. API oder Web", "Search access type, e.g. API or web"], ["Dienst filtern", "Filter service"], ["Alle Dienste", "All services"], ["Account-Typ filtern", "Filter account type"], ["Verfügbare Account-Typen", "Available account types"], ["Keine passenden Account-Typen.", "No matching account types."], ["Prüfen", "Check"], ["Bearbeite ausschließlich den ausgewählten Account.", "Edit only the selected account."], ["Account bearbeiten", "Edit account"], ["Account aktiviert", "Account enabled"], ["Immer erste Tonspur", "Always first audio track"], ["Pro Download", "Per download"], ["Keine Archive löschen", "Do not delete archives"], ["Archive in Papierkorb", "Move archives to recycle bin"], ["Archive löschen", "Delete archives"], ["Accounts und Verwendungsregeln.", "Accounts and usage rules."], ["Premium Account", "Premium account"], ["Zugang ungültig", "Invalid access"], ["Prüft…", "Checking…"], ["Geschützter Zugang", "Protected access"], diff --git a/src/renderer/views/settings/AccountWorkspace.tsx b/src/renderer/views/settings/AccountWorkspace.tsx index 5f3b0d2..97f2930 100644 --- a/src/renderer/views/settings/AccountWorkspace.tsx +++ b/src/renderer/views/settings/AccountWorkspace.tsx @@ -23,7 +23,6 @@ import { getAccountTableMinWidth, getSettingsSelectNavigationIndex, resizeAccountTableColumn, - type AccountAddFilter, type AccountAddOption, type AccountRowViewModel, type AccountTableColumnId, @@ -176,10 +175,11 @@ export interface AccountDialogField { secretBusy?: boolean; } -export interface AccountAddDialogModel { - open: boolean; - query: string; - filter: AccountAddFilter; +export interface AccountAddDialogModel { + open: boolean; + query: string; + serviceFilter: string; + serviceFilters: readonly string[]; options: readonly AccountAddOption[]; selectedOptionId: string | null; fields: readonly AccountDialogField[]; @@ -187,9 +187,9 @@ export interface AccountAddDialogModel { busy: boolean; } -export interface AccountAddDialogActions { - onQueryChange: (value: string) => void; - onFilterChange: (filter: AccountAddFilter) => void; +export interface AccountAddDialogActions { + onQueryChange: (value: string) => void; + onServiceFilterChange: (service: string) => void; onOptionSelect: (optionId: string) => void; onFieldChange: (fieldId: string, value: string) => void; onClose: () => void; @@ -667,16 +667,34 @@ export function AccountAddDialog({ >
Dienst / Zugangstyp - actions.onQueryChange(event.target.value)} - placeholder="Dienst oder Zugangstyp suchen" - type="search" - value={model.query} - /> +
+ actions.onQueryChange(event.target.value)} + onKeyDown={(event) => { + if (event.key !== "Enter" || !model.options[0]) { + return; + } + event.preventDefault(); + actions.onOptionSelect(model.options[0].id); + }} + placeholder="Zugangstyp suchen, z. B. API oder Web" + type="search" + value={model.query} + /> + +