diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 67c4e1d..6c1c705 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -40,7 +40,7 @@ import { } from "../shared/provider-daily-limits"; import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order"; import { pruneSelection, shouldClearDownloadSelection, shouldClearDownloadSelectionOnEscape } from "./selection"; -import { buildBulkAccountEnabledState, buildConfiguredProviderOrder, buildScopedAccountEnabledState, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelection, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate } from "./account-ui"; +import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate, updateAccountRowSelection } from "./account-ui"; import type { AccountModeFilter } from "./account-ui"; import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit"; import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit"; @@ -1583,7 +1583,7 @@ export function App(): ReactElement { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [settingsSubTab, setSettingsSubTab] = useState("allgemein"); const [accountManagementTab, setAccountManagementTab] = useState<"overview" | "rules">("overview"); - const [selectedAccountRowKey, setSelectedAccountRowKey] = useState(null); + const [selectedAccountRowKeys, setSelectedAccountRowKeys] = useState>(() => new Set()); const [openSubmenu, setOpenSubmenu] = useState(null); const [startConflictPrompt, setStartConflictPrompt] = useState(null); const startConflictResolverRef = useRef<((result: { policy: Extract; applyToAll: boolean } | null) => void) | null>(null); @@ -2398,7 +2398,14 @@ export function App(): ReactElement { const cycleAccountStatusSort = (): void => setAccountStatusSort((s) => (s === "none" ? "desc" : s === "desc" ? "asc" : "none")); useEffect(() => { - setSelectedAccountRowKey((current) => pruneAccountRowSelection(current, accountRows.map((row) => row.rowKey))); + const existingRowKeys = accountRows.map((row) => row.rowKey); + setSelectedAccountRowKeys((current) => { + const next = pruneAccountRowSelections([...current], existingRowKeys); + if (next.length === current.size && next.every((rowKey) => current.has(rowKey))) { + return current; + } + return new Set(next); + }); }, [accountRows]); const availableAccountOptions = useMemo(() => ( ACCOUNT_OPTIONS.filter((option) => option.kind === "megadebrid-api" @@ -2938,36 +2945,21 @@ export function App(): ReactElement { } }; - const setAllAccountsEnabled = async (enabled: boolean): Promise => { - await performQuickAction(async () => { - const configuredProviderIds = [...new Set(configuredAccounts.map((entry) => entry.service as DebridProvider))]; - const nextEnabledState = buildBulkAccountEnabledState( - settingsDraft.disabledProviders || [], - configuredProviderIds, - accountRows.filter((row) => row.toggleKind === "mega" && row.accountId).map((row) => row.accountId as string), - accountRows.filter((row) => row.toggleKind === "dl" && row.accountId).map((row) => row.accountId as string), - enabled - ); - const nextDraft: RendererSettingsDraft = { - ...settingsDraft, - ...nextEnabledState, - megaDebridApiDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-api" && row.accountId).map((row) => row.accountId as string), - megaDebridWebDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-web" && row.accountId).map((row) => row.accountId as string) - }; - await persistAccountToggle(nextDraft); - showToast(enabled ? "Accounts aktiviert" : "Accounts deaktiviert", 2200); - }, (error) => { - showToast(`Accounts konnten nicht umgeschaltet werden: ${String(error)}`, 3200); - }); - }; - - const removeAccountTableRow = (row: AccountTableRow): void => { + const removeAccountTableRows = (rows: readonly AccountTableRow[]): void => { + if (rows.length === 0) { + return; + } setAccountContextMenu(null); void (async () => { - const username = resolveAccountUsername(row.username, row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId]?.email : undefined); + const row = rows[0]; + const username = rows.length === 1 + ? resolveAccountUsername(row.username, row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId]?.email : undefined) + : "—"; const confirmed = await askConfirmPrompt({ - title: `${row.hosterLabel} entfernen`, - message: `Soll ${row.hosterLabel}${username !== "—" ? ` (${username})` : ""} wirklich entfernt werden?`, + title: rows.length === 1 ? `${row.hosterLabel} entfernen` : `${rows.length} Accounts entfernen`, + message: rows.length === 1 + ? `Soll ${row.hosterLabel}${username !== "—" ? ` (${username})` : ""} wirklich entfernt werden?` + : `Sollen die ausgewählten ${rows.length} Accounts wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true }); @@ -2975,19 +2967,25 @@ export function App(): ReactElement { return; } await performQuickAction(async () => { - const result = await window.rd.deleteAccount(buildAccountDeleteCommand(row.editTarget)); - setSnapshot((current) => ({ ...current, settings: result.settings, accounts: result.accounts })); - applyPersistedSettings(result.settings); - if (row.entry.service === "alldebrid") { - setAllDebridHostInfo(null); + for (const selectedRow of rows) { + const result = await window.rd.deleteAccount(buildAccountDeleteCommand(selectedRow.editTarget)); + setSnapshot((current) => ({ ...current, settings: result.settings, accounts: result.accounts })); + applyPersistedSettings(result.settings); + if (selectedRow.entry.service === "alldebrid") { + setAllDebridHostInfo(null); + } } - showToast(`${row.hosterLabel} entfernt`, 2200); + const removedRowKeys = new Set(rows.map((selectedRow) => selectedRow.rowKey)); + setSelectedAccountRowKeys((current) => new Set([...current].filter((rowKey) => !removedRowKeys.has(rowKey)))); + showToast(rows.length === 1 ? `${row.hosterLabel} entfernt` : `${rows.length} Accounts entfernt`, 2200); }, (error) => { - showToast(`Account konnte nicht entfernt werden: ${String(error)}`, 3200); + showToast(`${rows.length === 1 ? "Account" : "Accounts"} konnte${rows.length === 1 ? "" : "n"} nicht entfernt werden: ${String(error)}`, 3200); }); })(); }; + const removeAccountTableRow = (row: AccountTableRow): void => removeAccountTableRows([row]); + const checkAccountTableRow = (row: AccountTableRow): void => { setAccountContextMenu(null); if (row.checkable) { @@ -4899,15 +4897,14 @@ export function App(): ReactElement { canCheck: row.checkable }; }), [accountRows, snapshot.settings.debridAccountStatuses]); - const selectedAccountViewId = useMemo(() => { - const selectedRow = selectedAccountRowKey ? accountRows.find((row) => row.rowKey === selectedAccountRowKey) : null; - return selectedRow ? accountRowViewId(selectedRow) : null; - }, [accountRows, selectedAccountRowKey]); + const selectedAccountViewIds = useMemo(() => accountRows + .filter((row) => selectedAccountRowKeys.has(row.rowKey)) + .map(accountRowViewId), [accountRows, selectedAccountRowKeys]); const projectedAccountRows = useMemo(() => projectAccountRows( accountSources, - selectedAccountViewId ? [selectedAccountViewId] : [], + selectedAccountViewIds, runtimeNow - ), [accountSources, runtimeNow, selectedAccountViewId]); + ), [accountSources, runtimeNow, selectedAccountViewIds]); const visibleAccountRows = useMemo(() => accountStatusSort === "none" ? projectedAccountRows : sortAccountRows(projectedAccountRows, accountStatusSort), [accountStatusSort, projectedAccountRows]); @@ -4920,9 +4917,8 @@ export function App(): ReactElement { const accountWorkspaceModel: AccountWorkspaceViewModel = { activePanel: accountManagementTab, rows: visibleAccountRows, - selectedIds: selectedAccountViewId ? [selectedAccountViewId] : [], + selectedIds: selectedAccountViewIds, busy: actionBusy || accountCheckBusy, - allEnabled: accountRows.length > 0 && accountRows.some((row) => !row.disabled), statusSort: accountStatusSort, rules: { providerOrder: activeProviderOrder.map((provider) => providerLabelWithMode(provider, settingsDraft)), @@ -4956,7 +4952,14 @@ export function App(): ReactElement { }; const accountWorkspaceActions: AccountWorkspaceActions = { onPanelChange: setAccountManagementTab, - onSelect: (rowId) => setSelectedAccountRowKey(accountRowBindings.get(rowId)?.rowKey ?? null), + onSelect: (rowId, additive) => { + const rowKey = accountRowBindings.get(rowId)?.rowKey; + if (!rowKey) { + return; + } + setSelectedAccountRowKeys((current) => new Set(updateAccountRowSelection([...current], rowKey, additive))); + }, + onClearSelection: () => setSelectedAccountRowKeys(new Set()), onToggleEnabled: (rowId) => { const row = accountRowBindings.get(rowId); if (row) toggleAccountTableRow(row); @@ -4976,12 +4979,13 @@ export function App(): ReactElement { }, onAdd: openCreateAccountDialog, onRemoveSelected: () => { - const row = selectedAccountViewId ? accountRowBindings.get(selectedAccountViewId) : null; - if (row) removeAccountTableRow(row); + const rows = selectedAccountViewIds + .map((rowId) => accountRowBindings.get(rowId)) + .filter((row): row is AccountTableRow => Boolean(row)); + removeAccountTableRows(rows); }, onCheckActive: () => { void checkAccounts("active"); }, onCheckAll: () => { void checkAccounts("all"); }, - onSetAllEnabled: (enabled) => { void setAllAccountsEnabled(enabled); }, onStatusSort: cycleAccountStatusSort, onMoveProvider: (index, direction) => { const target = index + direction; diff --git a/src/renderer/account-ui.ts b/src/renderer/account-ui.ts index 49b1efd..3edb712 100644 --- a/src/renderer/account-ui.ts +++ b/src/renderer/account-ui.ts @@ -36,6 +36,24 @@ export function pruneAccountRowSelection(selectedRowKey: string | null, existing return selectedRowKey && existingRowKeys.includes(selectedRowKey) ? selectedRowKey : null; } +export function updateAccountRowSelection(selectedRowKeys: readonly string[], rowKey: string, additive: boolean): string[] { + if (!additive) { + return [rowKey]; + } + const next = new Set(selectedRowKeys); + if (next.has(rowKey)) { + next.delete(rowKey); + } else { + next.add(rowKey); + } + return [...next]; +} + +export function pruneAccountRowSelections(selectedRowKeys: readonly string[], existingRowKeys: readonly string[]): string[] { + const existing = new Set(existingRowKeys); + return [...new Set(selectedRowKeys)].filter((rowKey) => existing.has(rowKey)); +} + export function resolveVisibleAccountKind(currentKind: T | null, visibleKinds: readonly T[]): T | null { return currentKind && visibleKinds.includes(currentKind) ? currentKind : visibleKinds[0] ?? null; } @@ -106,24 +124,3 @@ export function buildScopedAccountEnabledState( : [...new Set([...currentDisabledAccountIds, accountId])] }; } - -export function buildBulkAccountEnabledState( - currentDisabledProviders: DebridProvider[], - configuredProviders: DebridProvider[], - megaAccountIds: string[], - debridLinkKeyIds: string[], - enabled: boolean -): { - disabledProviders: DebridProvider[]; - megaDebridDisabledAccountIds: string[]; - debridLinkDisabledKeyIds: string[]; -} { - const configured = new Set(configuredProviders); - return { - disabledProviders: enabled - ? currentDisabledProviders.filter((provider) => !configured.has(provider)) - : [...new Set([...currentDisabledProviders, ...configuredProviders])], - megaDebridDisabledAccountIds: enabled ? [] : [...new Set(megaAccountIds)], - debridLinkDisabledKeyIds: enabled ? [] : [...new Set(debridLinkKeyIds)] - }; -} diff --git a/src/renderer/views/settings/AccountWorkspace.tsx b/src/renderer/views/settings/AccountWorkspace.tsx index 8eaf683..a953f64 100644 --- a/src/renderer/views/settings/AccountWorkspace.tsx +++ b/src/renderer/views/settings/AccountWorkspace.tsx @@ -98,17 +98,17 @@ export interface AccountRulesViewModel { export interface AccountWorkspaceViewModel { activePanel: AccountWorkspacePanel; rows: readonly AccountRowViewModel[]; - selectedIds: readonly string[]; - busy: boolean; - error?: string; - allEnabled?: boolean; - statusSort?: "none" | "desc" | "asc"; + selectedIds: readonly string[]; + busy: boolean; + error?: string; + statusSort?: "none" | "desc" | "asc"; rules: AccountRulesViewModel; } export interface AccountWorkspaceActions { - onPanelChange: (panel: AccountWorkspacePanel) => void; - onSelect: (rowId: string) => void; + onPanelChange: (panel: AccountWorkspacePanel) => void; + onSelect: (rowId: string, additive: boolean) => void; + onClearSelection: () => void; onToggleEnabled: (rowId: string) => void; onEdit: (rowId: string) => void; onContextMenu: (rowId: string, x: number, y: number) => void; @@ -117,7 +117,6 @@ export interface AccountWorkspaceActions { onRemoveSelected: () => void; onCheckActive: () => void; onCheckAll: () => void; - onSetAllEnabled?: (enabled: boolean) => void; onStatusSort?: () => void; onMoveProvider?: (index: number, direction: -1 | 1) => void; onProviderDragStart?: (event: DragEvent, index: number) => void; @@ -302,14 +301,14 @@ function AccountRow({ gridTemplateColumns: string; minWidth: number; }): ReactElement { - const selectRow = (): void => actions.onSelect(row.id); - const onClick = (): void => selectRow(); + const selectRow = (additive = false): void => actions.onSelect(row.id, additive); + const onClick = (event: MouseEvent): void => selectRow(event.ctrlKey || event.metaKey); const onKeyDown = (event: KeyboardEvent): void => { if (event.target !== event.currentTarget || (event.key !== "Enter" && event.key !== " ")) { return; } event.preventDefault(); - selectRow(); + selectRow(event.ctrlKey || event.metaKey); }; const openContextMenu = (event: MouseEvent): void => { event.preventDefault(); @@ -455,7 +454,7 @@ function AccountOverview({ model, actions }: AccountWorkspaceProps): ReactElemen
- +
@@ -584,7 +583,16 @@ function AccountRules({ model, actions }: AccountWorkspaceProps): ReactElement { export function AccountWorkspace({ model, actions }: AccountWorkspaceProps): ReactElement { return ( -
+
{ + if (event.key !== "Escape" || model.selectedIds.length === 0) { + return; + } + event.preventDefault(); + actions.onClearSelection(); + }} + > {model.busy ? Accountdaten werden aktualisiert. : null} {model.error ? {model.error} : null}
@@ -592,18 +600,7 @@ export function AccountWorkspace({ model, actions }: AccountWorkspaceProps): Rea

Accountverwaltung

Accounts hinzufügen, prüfen und verwalten.

- {typeof model.allEnabled === "boolean" && actions.onSetAllEnabled ? ( - - ) : null} - + {ACCOUNT_WORKSPACE_PANELS.map((panel, index) => (