diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 8df1886..dd8eeaa 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2616,6 +2616,99 @@ export function App(): ReactElement { } return rows; }, [configuredAccounts, settingsDraft, snapshot.settings]); + + 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); + } + return groups; + }, [accountRows]); + + 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: (typeof accountRows)[number], isMember: boolean): ReactElement => { + const st = row.accountId ? (snapshot.settings?.debridAccountStatuses?.[row.accountId] ?? null) : null; + const checking = row.accountId ? megaCheckingIds.has(row.accountId) : false; + let statusCls = "ok"; + let statusText = "Konfiguriert"; + if (row.disabled) { statusCls = "disabled"; statusText = "Deaktiviert"; } + else if (!row.checkable) { statusCls = "ok"; statusText = "Konfiguriert"; } + else if (checking) { statusCls = "unknown"; statusText = "Prüfe…"; } + else if (!st) { statusCls = "unknown"; statusText = "Noch nicht geprüft"; } + else if (!st.valid) { statusCls = "invalid"; statusText = st.message || "Login ungültig"; } + else if (!st.isPremium) { statusCls = "free"; statusText = "Free Account"; } + else { statusCls = "ok"; statusText = st.message || "Premium Account"; } + const isProblem = statusCls === "invalid"; + const username = st && st.email ? st.email : row.username; + const expiry = st && st.premiumUntilMs && st.premiumUntilMs > 0 ? new Date(st.premiumUntilMs).toLocaleDateString("de-DE") : "—"; + const traffic = row.dailyLimitBytes > 0 + ? `${humanSize(row.dailyRemainingBytes)} von ${humanSize(row.dailyLimitBytes)} übrig` + : "Unbeschränkt"; + return ( +
+ + { + if (row.toggleKind === "mega" && row.megaLogin) { void onToggleMegaAccountEnabled(row.megaLogin, row.disabled); } + else if (row.toggleKind === "dl" && row.dlKey) { void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey); } + else { void onToggleAccountEnabled(row.entry); } + }} + /> + + + {row.hosterLabel} + {row.modeLabel} + + {traffic} + + {statusText} + + {username} + {expiry} + + {row.toggleKind === "single" && getAccountQuickActionMeta(row.entry.kind) && ( + + )} + + + +
+ ); + }; const availableAccountOptions = useMemo(() => ( ACCOUNT_OPTIONS.filter((option) => !configuredAccountServices.has(option.service)) ), [configuredAccountServices]); @@ -5325,64 +5418,41 @@ export function App(): ReactElement { Verfallsdatum Aktion - {accountRows.map((row) => { - const st = row.accountId ? (snapshot.settings?.debridAccountStatuses?.[row.accountId] ?? null) : null; - const checking = row.accountId ? megaCheckingIds.has(row.accountId) : false; - let statusCls = "ok"; - let statusText = "Konfiguriert"; - if (row.disabled) { statusCls = "disabled"; statusText = "Deaktiviert"; } - else if (!row.checkable) { statusCls = "ok"; statusText = "Konfiguriert"; } - else if (checking) { statusCls = "unknown"; statusText = "Prüfe…"; } - else if (!st) { statusCls = "unknown"; statusText = "Noch nicht geprüft"; } - else if (!st.valid) { statusCls = "invalid"; statusText = st.message || "Login ungültig"; } - else if (!st.isPremium) { statusCls = "free"; statusText = "Free Account"; } - else { statusCls = "ok"; statusText = st.message || "Premium Account"; } - const isProblem = statusCls === "invalid"; - const username = st && st.email ? st.email : row.username; - const expiry = st && st.premiumUntilMs && st.premiumUntilMs > 0 ? new Date(st.premiumUntilMs).toLocaleDateString("de-DE") : "—"; - const traffic = row.dailyLimitBytes > 0 - ? `${humanSize(row.dailyRemainingBytes)} von ${humanSize(row.dailyLimitBytes)} übrig` - : "Unbeschränkt"; - return ( -
- - { - if (row.toggleKind === "mega" && row.megaLogin) { void onToggleMegaAccountEnabled(row.megaLogin, row.disabled); } - else if (row.toggleKind === "dl" && row.dlKey) { void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey); } - else { void onToggleAccountEnabled(row.entry); } - }} - /> - + {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)}> + {"▶"} - {row.hosterLabel} - {row.modeLabel} + {group.serviceLabel} + {group.rows.length} Accounts - {traffic} - - {statusText} - - {username} - {expiry} - - {row.toggleKind === "single" && getAccountQuickActionMeta(row.entry.kind) && ( - - )} - - + + + {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; })}
)} diff --git a/src/renderer/styles.css b/src/renderer/styles.css index b7b18d0..806dc1e 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -3291,9 +3291,18 @@ td { font-size: 13px; } .acct2-row:last-child { border-bottom: 0; } -.acct2-row:nth-child(even) { background: color-mix(in srgb, var(--card) 28%, transparent); } .acct2-row.acct2-problem { background: color-mix(in srgb, var(--danger) 15%, transparent); } .acct2-row.acct2-disabled { opacity: 0.55; } +.acct2-group-head { cursor: pointer; background: color-mix(in srgb, var(--card) 50%, transparent); } +.acct2-group-head:hover { background: color-mix(in srgb, var(--card) 68%, transparent); } +.acct2-group-head .acct2-hoster strong { font-size: 13px; } +.acct2-chevron { display: inline-block; color: var(--muted); font-size: 9px; transition: transform 0.12s ease; } +.acct2-chevron.open { transform: rotate(90deg); } +.acct2-grp-sum { display: flex; gap: 6px; flex-wrap: wrap; } +.acct2-row.acct2-member .acct2-hoster { + padding-left: 12px; + border-left: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); +} .acct2-c-check { display: flex; justify-content: center; } .acct2-c-check input { cursor: pointer; } .acct2-c-actions { display: flex; gap: 6px; justify-content: flex-end; }