diff --git a/README.md b/README.md index 80a6601..cabe654 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,18 @@ Desktop downloader for Windows with package-based queue management, multi-provid ## Changelog +### 2.0.10 + +- Add locally bundled provider logos throughout account management. +- Reorder the account table to Hoster, Status, Download Traffic Remaining, Username, Expiry Date, and Password / Access. +- Increase account-row and status text sizes while keeping the table compact. +- Show full account usernames while continuing to protect passwords and API keys. +- Replace the provider-wide edit form with a compact editor for the selected account or API key. +- Preserve sibling accounts, usage counters, limits, and status metadata during account-specific edits. + ### 2.0.9 -- Rebuild the Accounts view as a viewport-filling, JDownloader-style table with flat 24-pixel rows and bottom-aligned actions. +- Rebuild the Accounts view as a viewport-filling compact table with flat 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. @@ -35,7 +44,7 @@ Desktop downloader for Windows with package-based queue management, multi-provid ## Why this tool? -- JDownloader-style workflow with packages, progress, extraction, history, and clean post-processing. +- Package-based workflow with progress, extraction, history, and clean post-processing. - Multiple debrid accounts in one app, including provider order, automatic fallback, and per-hoster routing. - Built for large queues with session persistence, retries, reconnect handling, resume support, and integrity checks. - Includes an in-app updater for releases published on GitHub. @@ -344,3 +353,5 @@ Detailed release history is published on [GitHub Releases](https://github.com/Su The project is licensed under the MIT License. See `LICENSE`. Bundled JVM extractor licenses and redistribution notices are available in `resources/extractor-jvm`. + +Bundled provider icon notices are available in `THIRD_PARTY_NOTICES.md`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..b02ff6c --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,14 @@ +# Third-Party Notices + +The provider names, logos, and favicons bundled with this application are trademarks or brand assets of their respective owners. They are included unchanged solely to identify the supported services and are not covered by this project's MIT license. Inclusion does not imply endorsement or affiliation. + +| Provider | Source | Retrieved | SHA-256 | +| --- | --- | --- | --- | +| Real-Debrid | https://fcdn.real-debrid.com/0831/favicons/favicon-32x32.png | 2026-08-09 | `542D80BDDB948F240B100C1872E83B0228BC1BFE5C46E8FFD95AFA419CB11F52` | +| Mega-Debrid | https://cdn.mega-debrid.eu/images/favicon.png | 2026-08-09 | `12D3DE939965D13F852853A2A5BB873B0D7A89D411BEAFA02518B48356803AE4` | +| BestDebrid | https://bestdebrid.com/en/files/assets/images/favicon.ico | 2026-08-09 | `11E54B1B0DCBBB8F5BCAA21D0EDC0CF182276C513ED733ACEEEBC35120CD9F33` | +| AllDebrid | https://cdn.alldebrid.com/lib/images/default/favicon.png | 2026-08-09 | `AE296FBE43A6CCEABC85033E5C063BFCA01932EB3677FB679AF63C23535CD9EF` | +| DDownload | https://ddownload.com/images/favicon.ico | 2026-08-09 | `E0413F73F8A2A5AD85BE8082B0897E82AE3B354FE2490884D7A992BB18F34DF3` | +| 1Fichier | https://img.1fichier.com/favicon.png | 2026-08-09 | `C3567E0299879FE67924C6119BF082BE128A5E8962FC0D61A0D8AAA0B1304CB5` | +| Debrid-Link | https://cdn.debrid-link.com/img/fav/favicon.ico?i=2 | 2026-08-09 | `1AB89ADA0EB9CB665F5E05209F27B50869FCA1972620C90D57BF682E87F98FA4` | +| LinkSnappy | https://linksnappy.com/templates/v3/img/favicon.png | 2026-08-09 | `D9DB0F5E4BDB2BF659ADD6D666F8464455196EC47201F27B126083754126497E` | diff --git a/assets/provider-icons/alldebrid.png b/assets/provider-icons/alldebrid.png new file mode 100644 index 0000000..844166b Binary files /dev/null and b/assets/provider-icons/alldebrid.png differ diff --git a/assets/provider-icons/bestdebrid.ico b/assets/provider-icons/bestdebrid.ico new file mode 100644 index 0000000..32f10be Binary files /dev/null and b/assets/provider-icons/bestdebrid.ico differ diff --git a/assets/provider-icons/ddownload.ico b/assets/provider-icons/ddownload.ico new file mode 100644 index 0000000..d4e9316 Binary files /dev/null and b/assets/provider-icons/ddownload.ico differ diff --git a/assets/provider-icons/debrid-link.ico b/assets/provider-icons/debrid-link.ico new file mode 100644 index 0000000..41be0fe Binary files /dev/null and b/assets/provider-icons/debrid-link.ico differ diff --git a/assets/provider-icons/linksnappy.png b/assets/provider-icons/linksnappy.png new file mode 100644 index 0000000..add8700 Binary files /dev/null and b/assets/provider-icons/linksnappy.png differ diff --git a/assets/provider-icons/mega-debrid.png b/assets/provider-icons/mega-debrid.png new file mode 100644 index 0000000..6416a0c Binary files /dev/null and b/assets/provider-icons/mega-debrid.png differ diff --git a/assets/provider-icons/onefichier.png b/assets/provider-icons/onefichier.png new file mode 100644 index 0000000..7554c6c Binary files /dev/null and b/assets/provider-icons/onefichier.png differ diff --git a/assets/provider-icons/real-debrid.png b/assets/provider-icons/real-debrid.png new file mode 100644 index 0000000..ab9bf95 Binary files /dev/null and b/assets/provider-icons/real-debrid.png differ diff --git a/package-lock.json b/package-lock.json index 6eb9193..fd4a437 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "2.0.9", + "version": "2.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "2.0.9", + "version": "2.0.10", "license": "MIT", "dependencies": { "adm-zip": "0.6.0", diff --git a/package.json b/package.json index 986af41..c90ce88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "2.0.9", + "version": "2.0.10", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", @@ -65,12 +65,17 @@ "build/renderer/**/*", "resources/extractor-jvm/**/*", "LICENSE", + "THIRD_PARTY_NOTICES.md", "package.json" ], "extraResources": [ { "from": "LICENSE", "to": "LICENSE" + }, + { + "from": "THIRD_PARTY_NOTICES.md", + "to": "THIRD_PARTY_NOTICES.md" } ], "asarUnpack": [ diff --git a/scripts/verify_public_release.mjs b/scripts/verify_public_release.mjs index a7af7c6..187e0d4 100644 --- a/scripts/verify_public_release.mjs +++ b/scripts/verify_public_release.mjs @@ -16,7 +16,8 @@ const EXPECTED_NSIS_ARTIFACT_NAME = "${productName}-Setup-${version}.${ext}"; const EXPECTED_PORTABLE_ARTIFACT_NAME = "${productName}-${version}-portable.${ext}"; const REQUIRED_BUILD_FILES = Object.freeze([ "resources/extractor-jvm/**/*", - "LICENSE" + "LICENSE", + "THIRD_PARTY_NOTICES.md" ]); const EXPECTED_EXTRA_RESOURCE = Object.freeze({ from: "LICENSE", @@ -28,6 +29,11 @@ const REDISTRIBUTION_FILES = Object.freeze([ packagedPath: "resources/LICENSE", sha256: "f2c1bc02d9ba5235cc67dfea734e7dc90559b00d8cb2d142bad7a984ff96d3f6" }), + Object.freeze({ + sourcePath: "THIRD_PARTY_NOTICES.md", + packagedPath: "resources/THIRD_PARTY_NOTICES.md", + sha256: "b5d923900e9bf932fbf173191973d0892a091ac5afdc4907b8b9f4f846e8126c" + }), Object.freeze({ sourcePath: "resources/extractor-jvm/licenses/LGPL-2.1.txt", packagedPath: "resources/app.asar.unpacked/resources/extractor-jvm/licenses/LGPL-2.1.txt", diff --git a/src/main/account-status-persistence.ts b/src/main/account-status-persistence.ts new file mode 100644 index 0000000..68c73ec --- /dev/null +++ b/src/main/account-status-persistence.ts @@ -0,0 +1,8 @@ +import type { DebridAccountStatus } from "../shared/types"; + +export function canPersistExpectedAccountStatus(statuses: readonly DebridAccountStatus[], expectedAccountId: string | undefined): boolean { + return Boolean(expectedAccountId) + && statuses.length === 1 + && statuses[0].valid + && statuses[0].accountId === expectedAccountId; +} diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 09d848d..93a49db 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -2,7 +2,6 @@ import path from "node:path"; import os from "node:os"; import v8 from "node:v8"; import { app } from "electron"; -import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys"; import { AddLinksPayload, AllDebridHostInfo, @@ -29,7 +28,7 @@ import { APP_VERSION, ONLINE_BACKUP_API_URL } from "./constants"; import { DownloadManager } from "./download-manager"; import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid"; import { checkAllDebridAccounts, checkMegaDebridAccount } from "./account-check"; -import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts"; +import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts"; import { parseCollectorInput } from "./link-parser"; import { configureLogger, getLogFilePath, logger } from "./logger"; import { AllDebridWebFallback } from "./all-debrid-web"; @@ -59,6 +58,8 @@ import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-b import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log"; import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types"; import { createOnlineBackup, downloadOnlineBackup, uploadOnlineBackup } from "./online-backup"; +import { overlayLiveUsageCounters } from "./settings-live-overlay"; +import { canPersistExpectedAccountStatus } from "./account-status-persistence"; function sanitizeSettingsPatch(partial: Partial): Partial { const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined); @@ -417,20 +418,8 @@ export class AppController { // All-time totals take the max; daily/total usage and account statuses are taken // live; per-key Debrid-Link usage is filtered to keys that still exist. private overlayLiveUsageCounters(target: AppSettings): void { - const liveSettings = this.manager.getSettings(); - target.totalDownloadedAllTime = Math.max(target.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0); - target.totalCompletedFilesAllTime = Math.max(target.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0); - target.totalRuntimeAllTimeMs = Math.max(target.totalRuntimeAllTimeMs || 0, this.manager.getLiveTotalRuntimeMs()); - target.providerDailyUsageDay = liveSettings.providerDailyUsageDay; - target.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) }; - target.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) }; - target.debridLinkApiKeyDailyUsageBytes = Object.fromEntries( - Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(target.debridLinkApiKeys).includes(keyId)) - ); - target.debridLinkApiKeyTotalUsageBytes = Object.fromEntries( - Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(target.debridLinkApiKeys).includes(keyId)) - ); - target.debridAccountStatuses = { ...(liveSettings.debridAccountStatuses || {}) }; + const liveSettings = this.manager.getSettings(); + overlayLiveUsageCounters(target, liveSettings, this.manager.getLiveTotalRuntimeMs()); } private applySettingsOnlyBackup(importedSettings: AppSettings, remoteDiagnostics?: unknown, restoreRemoteDiagnostics = false): void { @@ -546,10 +535,12 @@ export class AppController { return fetchDebridLinkHostLimits(this.settings.debridLinkApiKeys, host); } -public async checkDebridAccounts(settingsOverride?: AppSettings): Promise { +public async checkDebridAccounts(settingsOverride?: AppSettings, persistValidOverride = false, expectedAccountId?: string): Promise { const statuses = await checkAllDebridAccounts(settingsOverride ? normalizeSettings(settingsOverride) : this.settings); - if (!settingsOverride) { + if (!settingsOverride || (persistValidOverride && canPersistExpectedAccountStatus(statuses, expectedAccountId))) { this.manager.applyDebridAccountStatuses(statuses); + } + if (!settingsOverride) { this.audit("INFO", "Debrid-Accounts geprueft", { total: statuses.length, valid: statuses.filter((s) => s.valid).length, diff --git a/src/main/main.ts b/src/main/main.ts index 98da62c..82d1c84 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -747,8 +747,8 @@ function registerIpcHandlers(): void { return controller.getDebridLinkHostLimits(); }); - ipcMain.handle(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async (_event, settings?: AppSettings) => { - return controller.checkDebridAccounts(settings); + ipcMain.handle(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async (_event, settings?: AppSettings, persistValidOverride = false, expectedAccountId?: string) => { + return controller.checkDebridAccounts(settings, persistValidOverride === true, expectedAccountId); }); ipcMain.handle(IPC_CHANNELS.CHECK_MEGA_DEBRID_ACCOUNT, async (_event, login: string, password: string) => { diff --git a/src/main/settings-live-overlay.ts b/src/main/settings-live-overlay.ts new file mode 100644 index 0000000..bc7a4ce --- /dev/null +++ b/src/main/settings-live-overlay.ts @@ -0,0 +1,30 @@ +import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys"; +import { getMegaDebridAccountIds } from "../shared/mega-debrid-accounts"; +import type { AppSettings } from "../shared/types"; + +export function overlayLiveUsageCounters(target: AppSettings, liveSettings: AppSettings, liveTotalRuntimeMs: number): void { + const debridLinkKeyIds = new Set(getDebridLinkApiKeyIds(target.debridLinkApiKeys)); + const megaAccountIds = new Set(getMegaDebridAccountIds(target.megaCredentials || "", target.megaPassword || "")); + const validAccountIds = new Set([...debridLinkKeyIds, ...megaAccountIds]); + target.totalDownloadedAllTime = Math.max(target.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0); + target.totalCompletedFilesAllTime = Math.max(target.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0); + target.totalRuntimeAllTimeMs = Math.max(target.totalRuntimeAllTimeMs || 0, liveTotalRuntimeMs); + target.providerDailyUsageDay = liveSettings.providerDailyUsageDay; + target.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) }; + target.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) }; + target.debridLinkApiKeyDailyUsageBytes = Object.fromEntries( + Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId)) + ); + target.debridLinkApiKeyTotalUsageBytes = Object.fromEntries( + Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId)) + ); + target.megaDebridAccountDailyUsageBytes = Object.fromEntries( + Object.entries(liveSettings.megaDebridAccountDailyUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId)) + ); + target.megaDebridAccountTotalUsageBytes = Object.fromEntries( + Object.entries(liveSettings.megaDebridAccountTotalUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId)) + ); + target.debridAccountStatuses = Object.fromEntries( + Object.entries(liveSettings.debridAccountStatuses || {}).filter(([accountId]) => validAccountIds.has(accountId)) + ); +} diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 380373c..9c3dc16 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -87,7 +87,7 @@ const api: ElectronApi = { importBestDebridCookies: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES), getAllDebridHostInfo: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO), getDebridLinkHostLimits: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS), - checkDebridAccounts: (settings?: AppSettings): Promise => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, settings), + checkDebridAccounts: (settings?: AppSettings, persistValidOverride = false, expectedAccountId?: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, settings, persistValidOverride, expectedAccountId), checkMegaDebridAccount: (login: string, password: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.CHECK_MEGA_DEBRID_ACCOUNT, login, password), retryExtraction: (packageId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId), extractNow: (packageId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 917d6b3..400a18a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -38,8 +38,11 @@ import { } from "../shared/provider-daily-limits"; import { reorderPackageOrderByDrop, sortPackageOrderByName, sortPackagesForDisplay } from "./package-order"; import { pruneSelection } from "./selection"; -import { buildBulkAccountEnabledState, buildConfiguredProviderOrder, getAccountDialogSelectableOptions, isAccountRowSelectionKey, matchesAccountModeFilter, pruneAccountRowSelection, resolveVisibleAccountKind } from "./account-ui"; +import { buildBulkAccountEnabledState, buildConfiguredProviderOrder, getAccountDialogSelectableOptions, isAccountRowSelectionKey, matchesAccountModeFilter, pruneAccountRowSelection, resolveAccountUsername, resolveVisibleAccountKind } from "./account-ui"; import type { AccountModeFilter } from "./account-ui"; +import { applyAccountEdit, buildAccountEditCheckSettings, createAccountEditState, getAccountEditExpectedStatusId, removeAccountTarget, validateAccountEdit, validateAccountEditStatuses } from "./account-edit"; +import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit"; +import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons"; import { DOWNLOAD_SPEED_MAX_SAMPLES, updateDownloadSpeedHistory } from "./download-speed-state"; import type { DownloadSpeedHistoryState } from "./download-speed-state"; @@ -87,22 +90,7 @@ interface LinkPopupState { isPackage: boolean; } -type AccountService = "realdebrid" | "megadebrid-api" | "megadebrid-web" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy"; -type AccountKind = - | "realdebrid-api" - | "realdebrid-web" - | "megadebrid-api" - | "megadebrid-web" - | "bestdebrid-api" - | "bestdebrid-web" - | "alldebrid-api" - | "alldebrid-web" - | "ddownload-login" - | "onefichier-api" - | "debridlink-api" - | "linksnappy-login"; - -type AccountQuickAction = "realdebrid-login" | "bestdebrid-cookies" | "alldebrid-login" | "alldebrid-status"; +type AccountQuickAction = "realdebrid-login" | "bestdebrid-cookies" | "alldebrid-login" | "alldebrid-status"; interface AccountOption { kind: AccountKind; service: AccountService; @@ -183,6 +171,7 @@ interface AccountTableRow { toggleKind: "mega" | "dl" | "single"; megaLogin?: string; dlKey?: DebridLinkAccountKeyEntry; + editTarget: AccountEditTarget; } interface AccountContextMenuState { @@ -190,6 +179,25 @@ interface AccountContextMenuState { y: number; row: AccountTableRow; } + +function AccountServiceLogo({ service, className }: { service: AccountService; className: string }): ReactElement { + return ; +} + +function getAccountQuickActionMeta(kind: AccountKind): { label: string; action: AccountQuickAction } | null { + switch (kind) { + case "realdebrid-web": + return { label: "Login", action: "realdebrid-login" }; + case "bestdebrid-web": + return { label: "Cookies", action: "bestdebrid-cookies" }; + case "alldebrid-api": + return { label: "Status", action: "alldebrid-status" }; + case "alldebrid-web": + return { label: "Login", action: "alldebrid-login" }; + default: + return null; + } +} function buildDebugSetupDetails(setup: DebugSetupCheckResult): string { const formatDiskLine = (label: string, value: DebugSetupCheckResult["diskSpace"]["runtime"]): string => { @@ -586,7 +594,7 @@ function summarizeAccount(kind: AccountKind, settings: AppSettings): string { } } -function summarizeAccountLines(kind: AccountKind, settings: AppSettings): string[] { +function summarizeAccountLines(kind: AccountKind, settings: AppSettings): string[] { if (kind === "debridlink-api" && settings.accountListShowDetailedDebridLinkKeys) { const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || ""); if (keys.length > 1) { @@ -599,8 +607,24 @@ function summarizeAccountLines(kind: AccountKind, settings: AppSettings): string return accounts.map((entry) => `${entry.label}: ${entry.maskedLogin}`); } } - return [summarizeAccount(kind, settings)]; -} + return [summarizeAccount(kind, settings)]; +} + +function getStoredAccountUsername(kind: AccountKind, settings: AppSettings): string { + switch (kind) { + case "ddownload-login": + return settings.ddownloadLogin; + case "linksnappy-login": + return settings.linkSnappyLogin; + case "realdebrid-web": + case "alldebrid-web": + return "Browser-Login"; + case "bestdebrid-web": + return "Cookies.txt"; + default: + return "API-Account"; + } +} function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | null, settings: AppSettings): AccountDialogState { const baseMega: Pick = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "", megaDisabledIds: [] }; @@ -1795,6 +1819,7 @@ export function App(): ReactElement { const accountContextMenuRef = useRef(null); const [linkPopup, setLinkPopup] = useState(null); const [accountDialog, setAccountDialog] = useState(null); + const [accountEditDialog, setAccountEditDialog] = useState(null); const [accountDialogSearch, setAccountDialogSearch] = useState(""); const [accountDialogModeFilter, setAccountDialogModeFilter] = useState("all"); const [keyStatsPopup, setKeyStatsPopup] = useState(null); @@ -2525,10 +2550,10 @@ export function App(): ReactElement { const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id); rows.push({ rowKey: `mega-${entry.kind}-${acc.id}`, - entry, + entry, hosterLabel: entry.serviceLabel, modeLabel: entry.modeLabel, - username: acc.maskedLogin, + username: acc.login, credentialLabel: "••••••", accountId: acc.id, checkable: true, @@ -2537,8 +2562,15 @@ export function App(): ReactElement { dailyLimitBytes: limit, dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0, totalUsedBytes: getMegaDebridAccountTotalUsageBytes(snapshot.settings, acc.id), - toggleKind: "mega", - megaLogin: acc.login + toggleKind: "mega", + megaLogin: acc.login, + editTarget: { + type: "mega", + rowKey: `mega-${entry.kind}-${acc.id}`, + kind: entry.kind, + service: entry.service as "megadebrid-api" | "megadebrid-web", + accountId: acc.id + } }); } } else if (entry.kind === "debridlink-api") { @@ -2548,7 +2580,7 @@ export function App(): ReactElement { entry, hosterLabel: entry.serviceLabel, modeLabel: entry.modeLabel, - username: key.masked, + username: "", credentialLabel: "API-Key", accountId: key.id, checkable: true, @@ -2557,8 +2589,15 @@ export function App(): ReactElement { dailyLimitBytes: key.dailyLimitBytes, dailyRemainingBytes: key.dailyLimitBytes > 0 ? Math.max(0, key.dailyLimitBytes - key.dailyUsedBytes) : 0, totalUsedBytes: key.totalUsedBytes, - toggleKind: "dl", - dlKey: key + toggleKind: "dl", + dlKey: key, + editTarget: { + type: "debridlink", + rowKey: `dl-${key.id}`, + kind: "debridlink-api", + service: "debridlink", + keyId: key.id + } }); } } else { @@ -2567,7 +2606,7 @@ export function App(): ReactElement { entry, hosterLabel: entry.serviceLabel, modeLabel: entry.modeLabel, - username: entry.summary, + username: getStoredAccountUsername(entry.kind, settingsDraft), credentialLabel: getAccountCredentialLabel(entry.kind), accountId: null, checkable: false, @@ -2576,7 +2615,14 @@ export function App(): ReactElement { dailyLimitBytes: entry.dailyLimitBytes, dailyRemainingBytes: entry.dailyLimitBytes > 0 ? Math.max(0, entry.dailyRemainingBytes ?? 0) : 0, totalUsedBytes: entry.totalUsedBytes, - toggleKind: "single" + toggleKind: "single", + editTarget: { + type: "single", + rowKey: `svc-${entry.service}`, + kind: entry.kind as SingleAccountKind, + service: entry.service, + provider: entry.provider + } }); } } @@ -2624,8 +2670,8 @@ export function App(): ReactElement { else if (!st.isPremium) { statusCls = "free"; statusText = "Free Account"; } else { statusCls = "ok"; statusText = st.message || "Premium Account"; } const isProblem = statusCls === "invalid"; - const username = row.username; - const usernameTitle = st && st.email && st.email.toLowerCase() !== row.username.toLowerCase() ? `${row.username} (Mail: ${st.email})` : row.username; + const username = resolveAccountUsername(row.username, st?.email); + const usernameTitle = 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` @@ -2644,7 +2690,7 @@ export function App(): ReactElement { setSelectedAccountRowKey(row.rowKey); } }} - onDoubleClick={() => openEditAccountDialog(row.entry.kind)} + onDoubleClick={() => openEditAccountDialog(row)} onContextMenu={(event) => { event.preventDefault(); event.stopPropagation(); @@ -2654,30 +2700,31 @@ export function App(): ReactElement { { 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.slice(0, 1).toUpperCase()} + {row.hosterLabel} {row.modeLabel} - - {traffic} - - {statusCls === "none" - ? - : {statusText}} + + {statusCls === "none" + ? + : {statusText}} + + {traffic} {username} {expiry} - {row.credentialLabel} + {row.credentialLabel} + Download-Traffic übrig Benutzername Verfallsdatum Passwort / Zugang @@ -6306,8 +6396,82 @@ export function App(): ReactElement { - )} - + )} + + {accountEditDialog && accountEditOption && ( +
+
event.stopPropagation()}> +
+ +
+

Account bearbeiten

+ {accountEditOption.serviceLabel} · {accountEditOption.modeLabel} +
+
+ +
+
+ Benutzername + {resolveAccountUsername(accountEditRow?.username || accountEditDialog.login, accountEditStatus?.email)} +
+
+ Status + {accountEditRow?.disabled + ? "Deaktiviert" + : accountEditStatus + ? accountEditStatus.valid + ? accountEditStatus.isPremium ? accountEditStatus.message || "Premium Account" : "Free Account" + : accountEditStatus.message || "Ungültig" + : accountEditRow?.entry.statusLabel || "Noch nicht geprüft"} +
+
+ +
+ {(accountEditDialog.target.type === "mega" || accountEditOption.needsCredentials) && ( +
+
+ + setAccountEditDialog((prev) => prev ? { ...prev, login: event.target.value } : prev)} /> +
+
+ + setAccountEditDialog((prev) => prev ? { ...prev, password: event.target.value } : prev)} /> +
+
+ )} + + {(accountEditDialog.target.type === "debridlink" || (accountEditOption.needsToken && accountEditDialog.target.type === "single")) && ( +
+ + setAccountEditDialog((prev) => prev ? { ...prev, token: event.target.value } : prev)} /> +
+ )} + +
+ + setAccountEditDialog((prev) => prev ? { ...prev, dailyLimitGb: event.target.value } : prev)} /> +
Leer oder 0 bedeutet unbegrenzt. Der Zähler wird täglich um 00:00 Uhr zurückgesetzt.
+
+ + {!accountEditOption.needsCredentials && !accountEditOption.needsToken && accountEditDialog.target.type === "single" && ( +
Dieser Account verwendet den gespeicherten Browser- oder Cookie-Login.
+ )} + + {accountEditQuickAction && ( + + )} +
+ +
+ + +
+
+
+ )} + {accountDialog && (
event.stopPropagation()}> @@ -6358,7 +6522,7 @@ export function App(): ReactElement { className={`account-picker-row${accountDialog.kind === option.kind ? " active" : ""}`} onClick={() => updateAccountDialogKind(option.kind)} > - {option.serviceLabel.slice(0, 1).toUpperCase()}{option.title} + {option.title} {getAccountPickerFunctionLabel(option)} ))} @@ -6702,7 +6866,7 @@ export function App(): ReactElement { {accountContextMenu.row.modeLabel} · {accountContextMenu.row.username}
-