From d594afe93ae90421b6214212a7c3bd08271c128a Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Mon, 15 Jun 2026 01:11:56 +0200 Subject: [PATCH] UI Phase 3: Account-Verwaltung als JDownloader-artige Liste (eine Zeile pro Account, Status inline) + Pruefung beim Hinzufuegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Account-Rework (User-Wunsch nach JDownloader-Vorlage-Screenshot): - Tabelle zeigt jetzt EINE ZEILE PRO EINZELNEM ACCOUNT statt eine Zeile pro Anbieter. Mega-Debrid mit 4 Accounts = 4 Zeilen, Debrid-Link je Key eine Zeile, Single-Token-Anbieter je eine Zeile. Neues geflachtes Modell accountRows (useMemo) aus configuredAccounts: Mega ueber parseMegaDebridAccounts, DL ueber entry.debridLinkKeys, Rest 1:1. - Spalten wie JDownloader: aktiviert-Checkbox | Hoster (+Modus) | Download-Traffic (Rest von Limit "X von Y uebrig" bzw. "Unbeschraenkt") | Status (farbig) | Benutzername | Verfallsdatum | Aktion (Bearbeiten/Entfernen). - STATUS INLINE + farbig (gruen Premium / gelb Free / rot ungueltig / grau nicht geprueft / Deaktiviert) direkt in der Zeile, ohne erst "Bearbeiten" zu oeffnen. Quelle: settings.debridAccountStatuses[accountId] (Mega/DL werden geprueft), Benutzername aus status.email, Verfallsdatum aus premiumUntilMs. Single-Token- Anbieter (Real-Debrid, AllDebrid, 1Fichier, ...) werden nicht geprueft -> "Konfiguriert". - Problemzeilen (Login ungueltig) rot hinterlegt, deaktivierte Zeilen ausgegraut. - aktiviert-Checkbox schaltet den EINZELNEN Account: Mega via neuem onToggleMegaAccountEnabled (megaDebridDisabledAccountIds), DL via bestehendem onToggleDebridLinkApiKeyEnabled, Single via onToggleAccountEnabled. Entfernen je Account: Mega/DL ueber neue Handler (Zeile aus megaCredentials/debridLinkApiKeys raus), Single ueber onRemoveAccount. - PRUEFUNG BEIM HINZUFUEGEN: onSaveAccountDialog stoesst nach dem Speichern checkAllAccounts() an -> Status erscheint sofort in der Liste + Toast meldet "X/Y Login gueltig, Z Premium" (Speichern bleibt erlaubt, wie gewuenscht). - Hoster-Reihenfolge- und Rotations-Verlauf-Panel unveraendert darunter. Alte resizable Spalten + "Zugang einzeln"-Toggle entfernt (durch Zeilen ersetzt). Web-Login-/AllDebrid-Status-Aktion bleibt als Knopf in der Single-Zeile. - account-validity-badge.ok von Neon-Verlauf auf flaches Gruen, .disabled ergaenzt. Reine Renderer-Aenderung. 810 Tests gruen, tsc=6 Baseline, build ok. Erste Version nach Screenshot — Feinschliff der Optik nach User-Feedback. --- src/renderer/App.tsx | 326 +++++++++++++++++++++++----------------- src/renderer/styles.css | 47 +++++- 2 files changed, 231 insertions(+), 142 deletions(-) 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; }