From cf72ea2db14c7ccb54ed75a95931c5510088e1e3 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sun, 9 Aug 2026 16:13:10 +0200 Subject: [PATCH] feat: rebuild account management like JDownloader Replace the grouped account cards with a viewport-filling 24px table, compact provider dialog, and separate usage rules view. Preserve account order and selections across bulk toggles, filtering, deletion, keyboard interaction, and narrow-window scrolling. Add focused regression coverage and publish the 2.0.9 changelog. --- README.md | 8 +- package-lock.json | 4 +- package.json | 2 +- src/renderer/App.tsx | 584 +++++++++--------------- src/renderer/account-ui.ts | 62 +++ src/renderer/styles.css | 556 +++++++++++++--------- tests/account-dialog-prefer-api.test.ts | 1 + tests/account-ui.test.ts | 79 +++- 8 files changed, 728 insertions(+), 568 deletions(-) diff --git a/README.md b/README.md index f153d7c..80a6601 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ Desktop downloader for Windows with package-based queue management, multi-provid ## Changelog +### 2.0.9 + +- Rebuild the Accounts view as a viewport-filling, JDownloader-style table with flat 24-pixel rows and bottom-aligned actions. +- Separate account usage rules from the overview so rotation and routing controls no longer reduce the visible account list. +- Redesign the account dialog with one-line provider rows, an immediate default selection, compact credential fields, and split cancel/save actions. +- Add a global account enable/disable control while preserving unrelated provider state. + ### 2.0.8 - Add a compact account-management table with traffic, status, expiry, username, and credential columns. @@ -85,7 +92,6 @@ Desktop downloader for Windows with package-based queue management, multi-provid - AllDebrid browser-login flow and in-app Rapidgator host status display. - Debrid-Link multi-key management with optional detailed line-by-line key display. - Debrid-Link API-key statistics popup with per-key Rapidgator traffic quota, link quota, reset, activate/deactivate, and click-to-copy masked keys. -- Reset button for stored account column widths in the Accounts table. ### Download engine diff --git a/package-lock.json b/package-lock.json index 007f07a..6eb9193 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "2.0.8", + "version": "2.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "2.0.8", + "version": "2.0.9", "license": "MIT", "dependencies": { "adm-zip": "0.6.0", diff --git a/package.json b/package.json index 28e0385..986af41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "2.0.8", + "version": "2.0.9", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 64d9d72..917d6b3 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, DragEvent, KeyboardEvent as ReactKeyboardEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { DragEvent, KeyboardEvent as ReactKeyboardEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; import { getMegaDebridAccountId, parseMegaDebridAccounts, serializeMegaDebridAccounts, maskMegaDebridLogin } from "../shared/mega-debrid-accounts"; import type { @@ -38,7 +38,7 @@ import { } from "../shared/provider-daily-limits"; import { reorderPackageOrderByDrop, sortPackageOrderByName, sortPackagesForDisplay } from "./package-order"; import { pruneSelection } from "./selection"; -import { matchesAccountModeFilter } from "./account-ui"; +import { buildBulkAccountEnabledState, buildConfiguredProviderOrder, getAccountDialogSelectableOptions, isAccountRowSelectionKey, matchesAccountModeFilter, pruneAccountRowSelection, resolveVisibleAccountKind } from "./account-ui"; import type { AccountModeFilter } from "./account-ui"; import { DOWNLOAD_SPEED_MAX_SAMPLES, updateDownloadSpeedHistory } from "./download-speed-state"; import type { DownloadSpeedHistoryState } from "./download-speed-state"; @@ -103,8 +103,6 @@ type AccountKind = | "linksnappy-login"; type AccountQuickAction = "realdebrid-login" | "bestdebrid-cookies" | "alldebrid-login" | "alldebrid-status"; -type AccountColumnKey = "service" | "mode" | "status" | "secret"; - interface AccountOption { kind: AccountKind; service: AccountService; @@ -121,9 +119,10 @@ interface MegaDialogAccount { password: string; } -export interface AccountDialogState { - mode: "create" | "edit"; - kind: AccountKind | null; +export interface AccountDialogState { + mode: "create" | "edit"; + kind: AccountKind | null; + service: AccountService | null; token: string; login: string; password: string; @@ -370,41 +369,6 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ const ACCOUNT_SERVICES: AccountService[] = ["realdebrid", "megadebrid-api", "megadebrid-web", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]; const ACCOUNT_LIMIT_BYTES_PER_GIB = 1024 * 1024 * 1024; -const ACCOUNT_COLUMN_STORAGE_KEY = "rd-account-column-widths-v2"; -const ACCOUNT_COLUMN_DEFAULT_WIDTHS: Record = { - service: 240, - mode: 96, - status: 320, - secret: 210 -}; -const ACCOUNT_COLUMN_MIN_WIDTHS: Record = { - service: 180, - mode: 80, - status: 180, - secret: 140 -}; - -function loadAccountColumnWidths(): Record { - if (typeof window === "undefined") { - return { ...ACCOUNT_COLUMN_DEFAULT_WIDTHS }; - } - try { - const raw = window.localStorage.getItem(ACCOUNT_COLUMN_STORAGE_KEY); - if (!raw) { - return { ...ACCOUNT_COLUMN_DEFAULT_WIDTHS }; - } - const parsed = JSON.parse(raw) as Partial>; - return { - service: Math.max(ACCOUNT_COLUMN_MIN_WIDTHS.service, Number(parsed.service) || ACCOUNT_COLUMN_DEFAULT_WIDTHS.service), - mode: Math.max(ACCOUNT_COLUMN_MIN_WIDTHS.mode, Number(parsed.mode) || ACCOUNT_COLUMN_DEFAULT_WIDTHS.mode), - status: Math.max(ACCOUNT_COLUMN_MIN_WIDTHS.status, Number(parsed.status) || ACCOUNT_COLUMN_DEFAULT_WIDTHS.status), - secret: Math.max(ACCOUNT_COLUMN_MIN_WIDTHS.secret, Number(parsed.secret) || ACCOUNT_COLUMN_DEFAULT_WIDTHS.secret) - }; - } catch { - return { ...ACCOUNT_COLUMN_DEFAULT_WIDTHS }; - } -} - function findAccountOption(kind: AccountKind): AccountOption { const option = ACCOUNT_OPTIONS.find((entry) => entry.kind === kind); if (!option) { @@ -498,7 +462,7 @@ function hasMegaDebridCredentials(settings: AppSettings): boolean { return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim()); } -function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvider[] { +function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvider[] { const list: DebridProvider[] = []; if (settings.token.trim() || settings.realDebridUseWebLogin) { list.push("realdebrid"); @@ -521,25 +485,20 @@ function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvid if ((settings.linkSnappyLogin || "").trim() && (settings.linkSnappyPassword || "").trim()) { list.push("linksnappy"); } - return list; -} + return list; +} + +function getActiveProvidersFromSettings(settings: AppSettings): DebridProvider[] { + const disabled = new Set(settings.disabledProviders || []); + return getConfiguredProvidersFromSettings(settings).filter((provider) => !disabled.has(provider)); +} -function getActiveProvidersFromSettings(settings: AppSettings): DebridProvider[] { - const disabled = new Set(settings.disabledProviders || []); - return getConfiguredProvidersFromSettings(settings).filter((p) => !disabled.has(p)); -} - -const DIRECT_HOSTERS: ReadonlySet = new Set(["onefichier", "ddownload"]); - -function normalizeProviderOrderForSettings(settings: AppSettings): DebridProvider[] { - const active = new Set(getActiveProvidersFromSettings(settings).filter((p) => !DIRECT_HOSTERS.has(p))); - const ordered = (settings.providerOrder || []).filter((p) => active.has(p)); - const inOrder = new Set(ordered); - for (const p of active) { - if (!inOrder.has(p)) ordered.push(p); - } - return ordered; -} +const DIRECT_HOSTERS: ReadonlySet = new Set(["onefichier", "ddownload"]); + +function normalizeProviderOrderForSettings(settings: AppSettings): DebridProvider[] { + const configured = getConfiguredProvidersFromSettings(settings).filter((provider) => !DIRECT_HOSTERS.has(provider)); + return buildConfiguredProviderOrder(settings.providerOrder || [], configured); +} function normalizeProviderSelectionForSettings( settings: AppSettings @@ -643,12 +602,13 @@ function summarizeAccountLines(kind: AccountKind, settings: AppSettings): string return [summarizeAccount(kind, settings)]; } -function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | null, settings: AppSettings): AccountDialogState { - const baseMega: Pick = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "", megaDisabledIds: [] }; - if (!kind) { - return { - mode, - kind: null, +function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | null, settings: AppSettings): AccountDialogState { + const baseMega: Pick = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "", megaDisabledIds: [] }; + if (!kind) { + return { + mode, + kind: null, + service: null, token: "", login: "", password: "", @@ -656,14 +616,15 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n keyDailyLimitGbById: {}, ...baseMega }; - } - const provider = getAccountServiceProvider(findAccountOption(kind).service); + } + const service = findAccountOption(kind).service; + const provider = getAccountServiceProvider(service); const dailyLimitGb = formatAccountDailyLimitInput(getProviderDailyLimitBytes(settings, provider)); switch (kind) { case "realdebrid-api": - return { mode, kind, token: settings.token, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: settings.token, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; case "realdebrid-web": - return { mode, kind, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; case "megadebrid-api": case "megadebrid-web": { let megaToken = (settings.megaCredentials || "").trim(); @@ -674,24 +635,25 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n const megaAccounts = parsed.map((a) => ({ login: a.login, password: a.password })); const loadedIds = new Set(parsed.map((a) => a.id)); const megaDisabledIds = (settings.megaDebridDisabledAccountIds || []).filter((id) => loadedIds.has(id)); - return { mode, kind, token: megaToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, megaAccounts, megaNewLogin: "", megaNewPassword: "", megaDisabledIds }; + return { mode, kind, service, token: megaToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, megaAccounts, megaNewLogin: "", megaNewPassword: "", megaDisabledIds }; } case "bestdebrid-api": - return { mode, kind, token: settings.bestToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: settings.bestToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; case "bestdebrid-web": - return { mode, kind, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; case "alldebrid-api": - return { mode, kind, token: settings.allDebridToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: settings.allDebridToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; case "alldebrid-web": - return { mode, kind, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; case "ddownload-login": - return { mode, kind, token: "", login: settings.ddownloadLogin, password: settings.ddownloadPassword, dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: "", login: settings.ddownloadLogin, password: settings.ddownloadPassword, dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; case "onefichier-api": - return { mode, kind, token: settings.oneFichierApiKey, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: settings.oneFichierApiKey, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; case "debridlink-api": return { - mode, - kind, + mode, + kind, + service, token: settings.debridLinkApiKeys || "", login: "", password: "", @@ -700,9 +662,9 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n ...baseMega }; case "linksnappy-login": - return { mode, kind, token: "", login: settings.linkSnappyLogin || "", password: settings.linkSnappyPassword || "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: "", login: settings.linkSnappyLogin || "", password: settings.linkSnappyPassword || "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; default: - return { mode, kind, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; + return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; } } @@ -1757,7 +1719,6 @@ export function App(): ReactElement { const [settingsDraft, setSettingsDraft] = useState(emptySnapshot().settings); const [speedLimitInput, setSpeedLimitInput] = useState(() => formatMbpsInputFromKbps(emptySnapshot().settings.speedLimitKbps)); const [scheduleSpeedInputs, setScheduleSpeedInputs] = useState>({}); - const [accountColumnWidths, setAccountColumnWidths] = useState>(loadAccountColumnWidths); const [settingsDirty, setSettingsDirty] = useState(false); const [schedulePickerOpen, setSchedulePickerOpen] = useState(false); const [scheduleTimeInput, setScheduleTimeInput] = useState(""); @@ -1808,9 +1769,11 @@ export function App(): ReactElement { const [supportTraceEnabled, setSupportTraceEnabled] = useState(false); const dragOverRef = useRef(false); const dragDepthRef = useRef(0); - const [openMenu, setOpenMenu] = useState(null); - const [settingsSubTab, setSettingsSubTab] = useState("allgemein"); - const [openSubmenu, setOpenSubmenu] = useState(null); + const [openMenu, setOpenMenu] = useState(null); + const [settingsSubTab, setSettingsSubTab] = useState("allgemein"); + const [accountManagementTab, setAccountManagementTab] = useState<"overview" | "rules">("overview"); + const [selectedAccountRowKey, setSelectedAccountRowKey] = useState(null); + const [openSubmenu, setOpenSubmenu] = useState(null); const [startConflictPrompt, setStartConflictPrompt] = useState(null); const startConflictResolverRef = useRef<((result: { policy: Extract; applyToAll: boolean } | null) => void) | null>(null); const [confirmPrompt, setConfirmPrompt] = useState(null); @@ -1856,37 +1819,6 @@ export function App(): ReactElement { const [allDebridHostLoading, setAllDebridHostLoading] = useState(false); const allDebridHostRequestRef = useRef(0); const debridLinkHostLimitsRequestRef = useRef(0); - const accountColumnResizeRef = useRef<{ key: AccountColumnKey; startX: number; startWidth: number } | null>(null); - const onAccountColumnResizeMove = useCallback((event: MouseEvent): void => { - const active = accountColumnResizeRef.current; - if (!active) { - return; - } - const nextWidth = Math.max( - ACCOUNT_COLUMN_MIN_WIDTHS[active.key], - Math.round(active.startWidth + (event.clientX - active.startX)) - ); - setAccountColumnWidths((prev) => ( - prev[active.key] === nextWidth ? prev : { ...prev, [active.key]: nextWidth } - )); - }, []); - - const stopAccountColumnResize = useCallback((): void => { - accountColumnResizeRef.current = null; - window.removeEventListener("mousemove", onAccountColumnResizeMove); - window.removeEventListener("mouseup", stopAccountColumnResize); - }, [onAccountColumnResizeMove]); - - const startAccountColumnResize = useCallback((key: AccountColumnKey, clientX: number): void => { - accountColumnResizeRef.current = { - key, - startX: clientX, - startWidth: accountColumnWidths[key] - }; - window.addEventListener("mousemove", onAccountColumnResizeMove); - window.addEventListener("mouseup", stopAccountColumnResize); - }, [accountColumnWidths, onAccountColumnResizeMove, stopAccountColumnResize]); - useEffect(() => { if (tab !== "history") return; const loadHistory = async (): Promise => { @@ -1904,22 +1836,6 @@ export function App(): ReactElement { useEffect(() => { historyEntriesRef.current = historyEntries; }, [historyEntries]); - useEffect(() => { - try { - window.localStorage.setItem(ACCOUNT_COLUMN_STORAGE_KEY, JSON.stringify(accountColumnWidths)); - } catch { - } - }, [accountColumnWidths]); - - const resetAccountColumnWidths = useCallback((): void => { - setAccountColumnWidths({ ...ACCOUNT_COLUMN_DEFAULT_WIDTHS }); - try { - window.localStorage.removeItem(ACCOUNT_COLUMN_STORAGE_KEY); - } catch { - } - showToast("Accounts-Spalten zurückgesetzt", 1800); - }, []); - const columnOrderKey = useMemo( () => (snapshot.settings.columnOrder || []).join("|"), [snapshot.settings.columnOrder] @@ -2217,12 +2133,11 @@ export function App(): ReactElement { const request = confirmQueueRef.current.shift(); request?.resolve(false); } - stopAccountColumnResize(); - if (unsubscribe) { unsubscribe(); } + if (unsubscribe) { unsubscribe(); } if (unsubClipboard) { unsubClipboard(); } if (unsubUpdateInstallProgress) { unsubUpdateInstallProgress(); } }; - }, [clearImportQueueFocusListener, stopAccountColumnResize]); + }, [clearImportQueueFocusListener]); const downloadsTabActive = tab === "downloads"; const deferredDownloadSearch = useDeferredValue(downloadSearch); @@ -2671,58 +2586,29 @@ export function App(): ReactElement { const [accountStatusSort, setAccountStatusSort] = useState<"none" | "desc" | "asc">("none"); const cycleAccountStatusSort = (): void => setAccountStatusSort((s) => (s === "none" ? "desc" : s === "desc" ? "asc" : "none")); - const groupedAccountRows = useMemo(() => { - const groups: { service: string; serviceLabel: string; rows: typeof accountRows }[] = []; - const byService = new Map(); - for (const row of accountRows) { - const key = row.entry.service; - let group = byService.get(key); - if (!group) { - group = { service: key, serviceLabel: row.hosterLabel, rows: [] }; - byService.set(key, group); - groups.push(group); - } - group.rows.push(row); - } - if (accountStatusSort !== "none") { - const statuses = snapshot.settings?.debridAccountStatuses || {}; - const now = Date.now(); - const remainingMs = (row: (typeof accountRows)[number]): number => { - const st = row.accountId ? statuses[row.accountId] : null; - return st && st.premiumUntilMs && st.premiumUntilMs > now ? st.premiumUntilMs - now : -1; - }; - for (const group of groups) { - group.rows = [...group.rows].sort((a, b) => { - const ra = remainingMs(a); - const rb = remainingMs(b); - if (ra > 0 && rb > 0) return accountStatusSort === "desc" ? rb - ra : ra - rb; - if (ra > 0) return -1; - if (rb > 0) return 1; - return 0; - }); - } - } - return groups; - }, [accountRows, accountStatusSort, snapshot.settings]); - - const [collapsedAccountGroups, setCollapsedAccountGroups] = useState>(() => { - try { - const raw = localStorage.getItem("rd-account-collapsed-groups-v1"); - if (raw) return new Set(JSON.parse(raw) as string[]); - } catch { /* ignore */ } - return new Set(); - }); - - const toggleAccountGroup = (service: string): void => { - setCollapsedAccountGroups((prev) => { - const next = new Set(prev); - if (next.has(service)) next.delete(service); else next.add(service); - try { localStorage.setItem("rd-account-collapsed-groups-v1", JSON.stringify([...next])); } catch { /* ignore */ } - return next; - }); - }; - - const renderAccountRow = (row: AccountTableRow, isMember: boolean): ReactElement => { + const sortedAccountRows = useMemo(() => { + if (accountStatusSort === "none") return accountRows; + const statuses = snapshot.settings?.debridAccountStatuses || {}; + const now = Date.now(); + const remainingMs = (row: (typeof accountRows)[number]): number => { + const status = row.accountId ? statuses[row.accountId] : null; + return status && status.premiumUntilMs && status.premiumUntilMs > now ? status.premiumUntilMs - now : -1; + }; + return [...accountRows].sort((a, b) => { + const remainingA = remainingMs(a); + const remainingB = remainingMs(b); + if (remainingA > 0 && remainingB > 0) return accountStatusSort === "desc" ? remainingB - remainingA : remainingA - remainingB; + if (remainingA > 0) return -1; + if (remainingB > 0) return 1; + return 0; + }); + }, [accountRows, accountStatusSort, snapshot.settings]); + + useEffect(() => { + setSelectedAccountRowKey((current) => pruneAccountRowSelection(current, accountRows.map((row) => row.rowKey))); + }, [accountRows]); + + const renderAccountRow = (row: AccountTableRow): ReactElement => { const st = row.accountId ? (snapshot.settings?.debridAccountStatuses?.[row.accountId] ?? null) : null; const checking = row.accountId ? megaCheckingIds.has(row.accountId) : false; let statusCls = "none"; @@ -2747,7 +2633,17 @@ export function App(): ReactElement { return (
setSelectedAccountRowKey(row.rowKey)} + onKeyDown={(event) => { + if (isAccountRowSelectionKey(event.key, event.target === event.currentTarget)) { + event.preventDefault(); + setSelectedAccountRowKey(row.rowKey); + } + }} onDoubleClick={() => openEditAccountDialog(row.entry.kind)} onContextMenu={(event) => { event.preventDefault(); @@ -2768,9 +2664,10 @@ export function App(): ReactElement { }} /> - - {row.hosterLabel} - {row.modeLabel} + + {row.hosterLabel.slice(0, 1).toUpperCase()} + {row.hosterLabel} + {row.modeLabel} {traffic} @@ -2803,18 +2700,17 @@ export function App(): ReactElement { ACCOUNT_OPTIONS.filter((option) => !configuredAccountServices.has(option.service)) ), [configuredAccountServices]); const accountDialogOption = accountDialog?.kind ? findAccountOption(accountDialog.kind) : null; - const accountDialogSelectableOptions = useMemo(() => { - if (!accountDialog) { - return []; - } - if (accountDialog.mode === "edit") { - if (!accountDialogOption) { - return []; - } - return ACCOUNT_OPTIONS.filter((option) => option.service === accountDialogOption.service); - } - return availableAccountOptions; - }, [accountDialog, accountDialogOption, availableAccountOptions]); + const accountDialogSelectableOptions = useMemo(() => { + if (!accountDialog) { + return []; + } + return getAccountDialogSelectableOptions( + ACCOUNT_OPTIONS, + availableAccountOptions, + accountDialog.mode, + accountDialog.service + ); + }, [accountDialog, availableAccountOptions]); const accountDialogSearchQuery = accountDialogSearch.trim().toLowerCase(); const filteredAccountDialogOptions = useMemo(() => ( accountDialogSelectableOptions.filter((option) => { @@ -2834,13 +2730,6 @@ export function App(): ReactElement { return haystack.includes(accountDialogSearchQuery); }) ), [accountDialogModeFilter, accountDialogSearchQuery, accountDialogSelectableOptions]); - const accountTableStyle = useMemo(() => ({ - "--account-col-service": `${accountColumnWidths.service}px`, - "--account-col-mode": `${accountColumnWidths.mode}px`, - "--account-col-status": `${accountColumnWidths.status}px`, - "--account-col-secret": `${accountColumnWidths.secret}px` - } as CSSProperties), [accountColumnWidths]); - const handleUpdateResult = async (result: UpdateCheckResult, source: "manual" | "startup"): Promise => { if (!mountedRef.current) { return; @@ -3055,7 +2944,7 @@ export function App(): ReactElement { const openCreateAccountDialog = (): void => { setAccountDialogSearch(""); setAccountDialogModeFilter("all"); - setAccountDialog(createAccountDialogState("create", null, settingsDraft)); + setAccountDialog(createAccountDialogState("create", availableAccountOptions[0]?.kind ?? null, settingsDraft)); }; const openEditAccountDialog = (kind: AccountKind): void => { @@ -3064,8 +2953,8 @@ export function App(): ReactElement { setAccountDialog(createAccountDialogState("edit", kind, settingsDraft)); }; - const updateAccountDialogKind = (kind: AccountKind): void => { - setAccountDialog((prev) => { + const updateAccountDialogKind = useCallback((kind: AccountKind): void => { + setAccountDialog((prev) => { const next = createAccountDialogState(prev?.mode ?? "create", kind, settingsDraft); if (!prev) { return next; @@ -3082,10 +2971,28 @@ export function App(): ReactElement { next.megaNewLogin = prev.megaNewLogin; next.megaNewPassword = prev.megaNewPassword; } - next.dailyLimitGb = prev.dailyLimitGb; - return next; - }); - }; + next.dailyLimitGb = prev.dailyLimitGb; + return next; + }); + }, [settingsDraft]); + + useEffect(() => { + if (!accountDialog) { + return; + } + const visibleKind = resolveVisibleAccountKind( + accountDialog.kind, + filteredAccountDialogOptions.map((option) => option.kind) + ); + if (visibleKind === accountDialog.kind) { + return; + } + if (visibleKind) { + updateAccountDialogKind(visibleKind); + return; + } + setAccountDialog((current) => current ? { ...current, kind: null } : current); + }, [accountDialog?.kind, filteredAccountDialogOptions, updateAccountDialogKind]); const closeAccountDialog = useCallback((): void => { setAccountDialog(null); @@ -3277,6 +3184,27 @@ 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: AppSettings = { + ...settingsDraft, + ...nextEnabledState + }; + await persistSpecificSettings(nextDraft); + showToast(enabled ? "Accounts aktiviert" : "Accounts deaktiviert", 2200); + }, (error) => { + showToast(`Accounts konnten nicht umgeschaltet werden: ${String(error)}`, 3200); + }); + }; + const removeAccountTableRow = (row: AccountTableRow): void => { setAccountContextMenu(null); if (row.toggleKind === "mega" && row.megaLogin) { @@ -5580,7 +5508,7 @@ export function App(): ReactElement { ))} -
+
{settingsSubTab === "allgemein" && (

Allgemein

@@ -5672,97 +5600,52 @@ export function App(): ReactElement {
Sendet eine Meldung, wenn die ganze Warteschlange abgearbeitet ist.
- )} - {settingsSubTab === "accounts" && ( -
-
-
-
-

Accountverwaltung

-
Kompakte Übersicht mit Hoster, Traffic, Status, Benutzername, Ablauf und Zugang. Rechtsklick oder Doppelklick öffnet die Aktionen.
-
-
- - -
-
- -
- {accountRows.length} {accountRows.length === 1 ? "Account" : "Accounts"} - {availableAccountOptions.length} weitere Typen verfügbar -
- - {configuredAccountServices.has("megadebrid-api") && configuredAccountServices.has("megadebrid-web") && ( -
- Mega-Debrid API + Web sind derselbe Account, nur zwei Zugriffsarten: API (schnell) wird zuerst versucht, Web dient als Fallback. Gleicher Login, kein zweites Konto — beide Zeilen teilen sich dasselbe Tageslimit und denselben Premium-Status. -
- )} - - {accountRows.length === 0 && ( -
- Noch keine Accounts hinterlegt - Füge über "Account hinzufügen" den ersten Dienst hinzu. Danach erscheinen hier Status, Zugang und Aktionen als Liste. -
- )} - - {accountRows.length > 0 && ( -
-
- - Hoster - Download-Traffic - Status{accountStatusSort === "desc" ? " ▼" : accountStatusSort === "asc" ? " ▲" : ""} - Benutzername - Verfallsdatum - Zugang - Aktion -
- {groupedAccountRows.flatMap((group) => { - if (group.rows.length < 2) { - return group.rows.map((row) => renderAccountRow(row, false)); - } - const collapsed = collapsedAccountGroups.has(group.service); - let okCount = 0; - let problemCount = 0; - let offCount = 0; - for (const r of group.rows) { - if (r.disabled) { offCount += 1; continue; } - const s = r.accountId ? (snapshot.settings?.debridAccountStatuses?.[r.accountId] ?? null) : null; - if (r.checkable && s && !s.valid) { problemCount += 1; } else { okCount += 1; } - } - const elements: ReactElement[] = [ -
toggleAccountGroup(group.service)}> - {"▶"} - - {group.serviceLabel} - {group.rows.length} Accounts - - - - {okCount > 0 && {okCount} OK} - {problemCount > 0 && {problemCount} Problem} - {offCount > 0 && {offCount} aus} - - - - - -
- ]; - if (!collapsed) { - for (const row of group.rows) elements.push(renderAccountRow(row, true)); - } - return elements; - })} -
- )} -
- -
+ )} + {settingsSubTab === "accounts" && ( +
+
+
+

Accountverwaltung

+
Hier kannst Du Deine Accounts hinzufügen, prüfen und verwalten.
+
+ +
+ +
+ + +
+ + + +