diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index dba2e96..8df1886 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -26,6 +26,9 @@ import { getDebridLinkApiKeyDailyLimitBytes, getDebridLinkApiKeyDailyRemainingBytes, getDebridLinkApiKeyDailyUsageBytes, + getMegaDebridAccountDailyLimitBytes, + getMegaDebridAccountDailyUsageBytes, + getMegaDebridAccountTotalUsageBytes, getProviderDailyLimitBytes, getProviderDailyRemainingBytes, getProviderTotalUsageBytes, @@ -2531,6 +2534,88 @@ export function App(): ReactElement { }, [settingsDraft, snapshot.settings, allDebridHostInfo, allDebridHostLoading, hasSavedAllDebridAccount, allDebridSettingsDirty]); const configuredAccountServices = useMemo(() => new Set(configuredAccounts.map((entry) => entry.service)), [configuredAccounts]); + + const accountRows = useMemo(() => { + type AccountRow = { + rowKey: string; + entry: ConfiguredAccountEntry; + hosterLabel: string; + modeLabel: string; + username: string; + accountId: string | null; + checkable: boolean; + disabled: boolean; + dailyUsedBytes: number; + dailyLimitBytes: number; + dailyRemainingBytes: number; + totalUsedBytes: number; + toggleKind: "mega" | "dl" | "single"; + megaLogin?: string; + dlKey?: DebridLinkAccountKeyEntry; + }; + const rows: AccountRow[] = []; + for (const entry of configuredAccounts) { + if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") { + const accounts = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || ""); + for (const acc of accounts) { + const used = getMegaDebridAccountDailyUsageBytes(snapshot.settings, acc.id); + const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id); + rows.push({ + rowKey: `mega-${acc.id}`, + entry, + hosterLabel: entry.serviceLabel, + modeLabel: entry.modeLabel, + username: acc.maskedLogin, + accountId: acc.id, + checkable: true, + disabled: (settingsDraft.megaDebridDisabledAccountIds || []).includes(acc.id), + dailyUsedBytes: used, + dailyLimitBytes: limit, + dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0, + totalUsedBytes: getMegaDebridAccountTotalUsageBytes(snapshot.settings, acc.id), + toggleKind: "mega", + megaLogin: acc.login + }); + } + } else if (entry.kind === "debridlink-api") { + for (const key of entry.debridLinkKeys) { + rows.push({ + rowKey: `dl-${key.id}`, + entry, + hosterLabel: entry.serviceLabel, + modeLabel: entry.modeLabel, + username: key.masked, + accountId: key.id, + checkable: true, + disabled: key.disabled, + dailyUsedBytes: key.dailyUsedBytes, + dailyLimitBytes: key.dailyLimitBytes, + dailyRemainingBytes: key.dailyLimitBytes > 0 ? Math.max(0, key.dailyLimitBytes - key.dailyUsedBytes) : 0, + totalUsedBytes: key.totalUsedBytes, + toggleKind: "dl", + dlKey: key + }); + } + } else { + rows.push({ + rowKey: `svc-${entry.service}`, + entry, + hosterLabel: entry.serviceLabel, + modeLabel: entry.modeLabel, + username: entry.summary, + accountId: null, + checkable: false, + disabled: entry.disabled, + dailyUsedBytes: entry.dailyUsedBytes, + dailyLimitBytes: entry.dailyLimitBytes, + dailyRemainingBytes: entry.dailyLimitBytes > 0 ? Math.max(0, entry.dailyRemainingBytes ?? 0) : 0, + totalUsedBytes: entry.totalUsedBytes, + toggleKind: "single" + }); + } + } + return rows; + }, [configuredAccounts, settingsDraft, snapshot.settings]); const availableAccountOptions = useMemo(() => ( ACCOUNT_OPTIONS.filter((option) => !configuredAccountServices.has(option.service)) ), [configuredAccountServices]); @@ -2851,6 +2936,7 @@ export function App(): ReactElement { } else if (selectedOption) { showToast(`${selectedOption.title} gespeichert`, 2200); } + void checkAllAccounts(); }, (error) => { showToast(`Account konnte nicht gespeichert werden: ${String(error)}`, 3200); }); @@ -2932,6 +3018,40 @@ export function App(): ReactElement { }); }; + const onToggleMegaAccountEnabled = async (login: string, currentlyDisabled: boolean): Promise => { + const accId = getMegaDebridAccountId(login.trim()); + await performQuickAction(async () => { + const current = settingsDraft.megaDebridDisabledAccountIds || []; + const next = currentlyDisabled ? current.filter((id) => id !== accId) : [...current, accId]; + await persistSpecificSettings({ ...settingsDraft, megaDebridDisabledAccountIds: next }); + showToast(currentlyDisabled ? "Account aktiviert" : "Account deaktiviert", 2000); + }, (error) => { + showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200); + }); + }; + + const onRemoveMegaAccount = async (login: string): Promise => { + const confirmed = await askConfirmPrompt({ title: "Account entfernen", message: `Soll der Mega-Debrid-Account ${maskMegaDebridLogin(login)} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true }); + if (!confirmed) return; + await performQuickAction(async () => { + const remaining = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "") + .filter((a) => a.login.trim().toLowerCase() !== login.trim().toLowerCase()); + const nextCreds = serializeMegaDebridAccounts(remaining.map((a) => ({ login: a.login, password: a.password }))); + await persistSpecificSettings({ ...settingsDraft, megaCredentials: nextCreds, megaLogin: remaining[0]?.login ?? "", megaPassword: remaining[0]?.password ?? "" }); + showToast("Account entfernt", 2000); + }, (error) => { showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 3200); }); + }; + + const onRemoveDebridLinkKey = async (key: DebridLinkAccountKeyEntry): Promise => { + const confirmed = await askConfirmPrompt({ title: "Key entfernen", message: `Soll der Debrid-Link-Key ${key.masked} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true }); + if (!confirmed) return; + await performQuickAction(async () => { + const remaining = parseDebridLinkApiKeys(settingsDraft.debridLinkApiKeys || "").filter((k) => k.id !== key.id); + await persistSpecificSettings({ ...settingsDraft, debridLinkApiKeys: remaining.map((k) => k.token).join("\n") }); + showToast("Key entfernt", 2000); + }, (error) => { showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 3200); }); + }; + const onToggleAccountEnabled = async (entry: ConfiguredAccountEntry): Promise => { await performQuickAction(async () => { const provider = entry.service as DebridProvider; @@ -5183,160 +5303,84 @@ export function App(): ReactElement {
- {configuredAccounts.length} aktiv + {accountRows.length} {accountRows.length === 1 ? "Account" : "Accounts"} {availableAccountOptions.length} weitere Typen verfügbar
- -
- -
- - {configuredAccounts.length === 0 && ( + {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.
)} - {configuredAccounts.length > 0 && ( -
-
-
- Account -
-
- Typ -
-
- Status -
-
- Info -
-
- Zugang -
-
- Aktionen -
+ {accountRows.length > 0 && ( +
+
+ + Hoster + Download-Traffic + Status + Benutzername + Verfallsdatum + Aktion
- {configuredAccounts.map((entry) => { - const option = findAccountOption(entry.kind); - const quickAction = getAccountQuickActionMeta(entry.kind); - const showStatusButton = entry.service === "alldebrid"; - const showQuickActionButton = Boolean(quickAction && !(showStatusButton && quickAction.action === "alldebrid-status")); - const allDebridStateClass = entry.service === "alldebrid" && allDebridHostInfo ? ` account-status-${allDebridHostInfo.state}` : ""; + {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 ( -
-
- {entry.serviceLabel} - {option.title} -
-
- {entry.modeLabel} -
-
- {entry.statusLabel} - {entry.note && {entry.note}} -
-
- {entry.debridLinkKeys.length > 0 ? ( -
- - Insgesamt: {humanSize(entry.totalUsedBytes)} -
- ) : ( -
-
- Heute: {humanSize(entry.dailyUsedBytes)} - {entry.dailyLimitBytes > 0 ? `Limit: ${humanSize(entry.dailyLimitBytes)}` : "Kein Tageslimit"} - {entry.dailyLimitBytes > 0 && ( - {entry.dailyLimitReached ? "Fallback aktiv" : `Rest: ${humanSize(entry.dailyRemainingBytes || 0)}`} - )} -
- Insgesamt: {humanSize(entry.totalUsedBytes)} -
- )} -
-
- {entry.summaryLines.length > 1 ? ( -
- {entry.summaryLines.map((line) => ( - {line} - ))} -
- ) : ( - {entry.summary} - )} -
-
- {showStatusButton && ( - )} - {showQuickActionButton && quickAction && ( - - )} - - - - -
+ + +
); })} diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 128cc07..b7b18d0 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -3255,10 +3255,55 @@ td { border: 1px solid transparent; white-space: nowrap; } -.account-validity-badge.ok { color: #1c1206; background: linear-gradient(90deg, #7bd88f, #4fb96a); border-color: #4fb96a; } +.account-validity-badge.ok { color: #10240f; background: #4fb96a; border-color: #3f9d57; } .account-validity-badge.free { color: #2a2113; background: #f2c14e; border-color: #d9a72f; } .account-validity-badge.invalid { color: #fff; background: #d9534f; border-color: #c0392b; } .account-validity-badge.unknown { color: var(--muted, #a59c8e); background: transparent; border-color: var(--line, #4a4032); } +.account-validity-badge.disabled { color: var(--muted, #a59c8e); background: transparent; border-color: var(--border); opacity: 0.8; } + +.acct2-table { + display: flex; + flex-direction: column; + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; + margin-top: 4px; +} +.acct2-head, +.acct2-row { + display: grid; + grid-template-columns: 34px minmax(110px, 1.1fr) minmax(150px, 1.5fr) minmax(120px, 1.4fr) minmax(140px, 1.6fr) minmax(92px, 0.9fr) auto; + align-items: center; + gap: 10px; + padding: 7px 12px; +} +.acct2-head { + background: color-mix(in srgb, var(--card) 60%, transparent); + border-bottom: 1px solid var(--border); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted); +} +.acct2-row { + border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); + 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-c-check { display: flex; justify-content: center; } +.acct2-c-check input { cursor: pointer; } +.acct2-c-actions { display: flex; gap: 6px; justify-content: flex-end; } +.acct2-hoster { display: flex; flex-direction: column; gap: 1px; min-width: 0; } +.acct2-hoster strong { font-size: 13px; } +.acct2-mode { font-size: 11px; color: var(--muted); } +.acct2-traffic, +.acct2-expiry { font-variant-numeric: tabular-nums; color: var(--muted); } +.acct2-user { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.acct2-status .account-validity-badge { margin-top: 0; } .rotation-panel { display: flex; flex-direction: column; gap: 6px; max-height: 320px; overflow-y: auto; } .rotation-empty { color: var(--muted, #a59c8e); font-size: 12px; }