feat: refine account management and targeted editing
Add local provider icons, larger compact account rows, the requested column order, and full username display while keeping secrets masked. Replace provider-wide editing with a row-specific editor that preserves sibling accounts, exact limits, live usage counters, and status metadata. Add targeted credential validation, release notices, and regression coverage.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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<AppSettings>): Partial<AppSettings> {
|
||||
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<DebridAccountStatus[]> {
|
||||
public async checkDebridAccounts(settingsOverride?: AppSettings, persistValidOverride = false, expectedAccountId?: string): Promise<DebridAccountStatus[]> {
|
||||
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,
|
||||
|
||||
+2
-2
@@ -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) => {
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ const api: ElectronApi = {
|
||||
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
||||
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
|
||||
getDebridLinkHostLimits: (): Promise<DebridLinkHostLimitInfo[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS),
|
||||
checkDebridAccounts: (settings?: AppSettings): Promise<DebridAccountStatus[]> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, settings),
|
||||
checkDebridAccounts: (settings?: AppSettings, persistValidOverride = false, expectedAccountId?: string): Promise<DebridAccountStatus[]> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, settings, persistValidOverride, expectedAccountId),
|
||||
checkMegaDebridAccount: (login: string, password: string): Promise<DebridAccountStatus | null> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_MEGA_DEBRID_ACCOUNT, login, password),
|
||||
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
|
||||
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
|
||||
|
||||
+248
-84
@@ -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 <img className={className} src={ACCOUNT_SERVICE_ICONS[service]} alt="" aria-hidden="true" draggable={false} />;
|
||||
}
|
||||
|
||||
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<AccountDialogState, "megaAccounts" | "megaNewLogin" | "megaNewPassword" | "megaDisabledIds"> = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "", megaDisabledIds: [] };
|
||||
@@ -1795,6 +1819,7 @@ export function App(): ReactElement {
|
||||
const accountContextMenuRef = useRef<HTMLDivElement>(null);
|
||||
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
|
||||
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
|
||||
const [accountEditDialog, setAccountEditDialog] = useState<AccountEditState | null>(null);
|
||||
const [accountDialogSearch, setAccountDialogSearch] = useState("");
|
||||
const [accountDialogModeFilter, setAccountDialogModeFilter] = useState<AccountModeFilter>("all");
|
||||
const [keyStatsPopup, setKeyStatsPopup] = useState<string | null>(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 {
|
||||
<span className="acct2-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!row.disabled}
|
||||
disabled={actionBusy}
|
||||
title={row.disabled ? "Account aktivieren" : "Account deaktivieren (bleibt gespeichert)"}
|
||||
checked={!row.disabled}
|
||||
disabled={actionBusy}
|
||||
title={row.disabled ? "Account aktivieren" : "Account deaktivieren (bleibt gespeichert)"}
|
||||
aria-label={`${row.hosterLabel} ${row.modeLabel} ${row.disabled ? "aktivieren" : "deaktivieren"}`}
|
||||
onChange={() => {
|
||||
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); }
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<span className="acct2-hoster" title={`${row.hosterLabel} · ${row.modeLabel}`}>
|
||||
<span className={`acct2-service-icon service-${row.entry.service}`}>{row.hosterLabel.slice(0, 1).toUpperCase()}</span>
|
||||
<AccountServiceLogo service={row.entry.service} className="acct2-service-icon" />
|
||||
<strong>{row.hosterLabel}</strong>
|
||||
<span className="acct2-mode">{row.modeLabel}</span>
|
||||
</span>
|
||||
<span className="acct2-traffic">{traffic}</span>
|
||||
<span className="acct2-status">
|
||||
{statusCls === "none"
|
||||
? <span className="acct2-nostatus" title="Für diesen Anbieter gibt es keine Status-Prüfung">—</span>
|
||||
: <span className={`account-validity-badge ${statusCls}`}>{statusText}</span>}
|
||||
</span>
|
||||
<span className="acct2-status">
|
||||
{statusCls === "none"
|
||||
? <span className="acct2-nostatus" title="Für diesen Anbieter gibt es keine Status-Prüfung">—</span>
|
||||
: <span className={`account-validity-badge ${statusCls}`}>{statusText}</span>}
|
||||
</span>
|
||||
<span className="acct2-traffic">{traffic}</span>
|
||||
<span className="acct2-user" title={usernameTitle}>{username}</span>
|
||||
<span className="acct2-expiry">{expiry}</span>
|
||||
<span className="acct2-credential" title="Zugangsdaten werden niemals im Klartext angezeigt">{row.credentialLabel}</span>
|
||||
<span className="acct2-credential" title="Passwörter und API-Schlüssel bleiben geschützt">{row.credentialLabel}</span>
|
||||
<span className="acct2-c-actions">
|
||||
<button
|
||||
className="btn btn-sm acct2-menu-button"
|
||||
@@ -2696,10 +2743,14 @@ export function App(): ReactElement {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const availableAccountOptions = useMemo(() => (
|
||||
ACCOUNT_OPTIONS.filter((option) => !configuredAccountServices.has(option.service))
|
||||
), [configuredAccountServices]);
|
||||
const accountDialogOption = accountDialog?.kind ? findAccountOption(accountDialog.kind) : null;
|
||||
const availableAccountOptions = useMemo(() => (
|
||||
ACCOUNT_OPTIONS.filter((option) => !configuredAccountServices.has(option.service))
|
||||
), [configuredAccountServices]);
|
||||
const accountEditOption = accountEditDialog ? findAccountOption(accountEditDialog.target.kind) : null;
|
||||
const accountEditRow = accountEditDialog ? accountRows.find((row) => row.rowKey === accountEditDialog.target.rowKey) ?? null : null;
|
||||
const accountEditStatus = accountEditRow?.accountId ? snapshot.settings.debridAccountStatuses?.[accountEditRow.accountId] ?? null : null;
|
||||
const accountEditQuickAction = accountEditOption ? getAccountQuickActionMeta(accountEditOption.kind) : null;
|
||||
const accountDialogOption = accountDialog?.kind ? findAccountOption(accountDialog.kind) : null;
|
||||
const accountDialogSelectableOptions = useMemo(() => {
|
||||
if (!accountDialog) {
|
||||
return [];
|
||||
@@ -2851,11 +2902,13 @@ export function App(): ReactElement {
|
||||
totalDownloadedAllTime: Math.max(prev.totalDownloadedAllTime, result.totalDownloadedAllTime),
|
||||
providerDailyUsageDay: result.providerDailyUsageDay,
|
||||
providerDailyUsageBytes: { ...(result.providerDailyUsageBytes || {}) },
|
||||
providerTotalUsageBytes: { ...(result.providerTotalUsageBytes || {}) },
|
||||
debridLinkApiKeyDailyUsageBytes: { ...(result.debridLinkApiKeyDailyUsageBytes || {}) },
|
||||
debridLinkApiKeyTotalUsageBytes: { ...(result.debridLinkApiKeyTotalUsageBytes || {}) }
|
||||
}));
|
||||
};
|
||||
providerTotalUsageBytes: { ...(result.providerTotalUsageBytes || {}) },
|
||||
debridLinkApiKeyDailyUsageBytes: { ...(result.debridLinkApiKeyDailyUsageBytes || {}) },
|
||||
debridLinkApiKeyTotalUsageBytes: { ...(result.debridLinkApiKeyTotalUsageBytes || {}) },
|
||||
megaDebridAccountDailyUsageBytes: { ...(result.megaDebridAccountDailyUsageBytes || {}) },
|
||||
megaDebridAccountTotalUsageBytes: { ...(result.megaDebridAccountTotalUsageBytes || {}) }
|
||||
}));
|
||||
};
|
||||
|
||||
const persistSpecificSettings = async (nextDraft: AppSettings): Promise<AppSettings> => {
|
||||
const normalizedDraft = {
|
||||
@@ -2890,21 +2943,6 @@ export function App(): ReactElement {
|
||||
}
|
||||
};
|
||||
|
||||
const 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;
|
||||
}
|
||||
};
|
||||
|
||||
const checkAllAccounts = useCallback(async (): Promise<void> => {
|
||||
setAccountCheckBusy(true);
|
||||
try {
|
||||
@@ -2947,10 +2985,12 @@ export function App(): ReactElement {
|
||||
setAccountDialog(createAccountDialogState("create", availableAccountOptions[0]?.kind ?? null, settingsDraft));
|
||||
};
|
||||
|
||||
const openEditAccountDialog = (kind: AccountKind): void => {
|
||||
setAccountDialogSearch("");
|
||||
setAccountDialogModeFilter("all");
|
||||
setAccountDialog(createAccountDialogState("edit", kind, settingsDraft));
|
||||
const openEditAccountDialog = (row: AccountTableRow): void => {
|
||||
try {
|
||||
setAccountEditDialog(createAccountEditState(row.editTarget, settingsDraft));
|
||||
} catch (error) {
|
||||
showToast(String(error), 3200);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAccountDialogKind = useCallback((kind: AccountKind): void => {
|
||||
@@ -2999,6 +3039,42 @@ export function App(): ReactElement {
|
||||
setAccountDialogSearch("");
|
||||
setAccountDialogModeFilter("all");
|
||||
}, []);
|
||||
|
||||
const closeAccountEditDialog = useCallback((): void => {
|
||||
setAccountEditDialog(null);
|
||||
}, []);
|
||||
|
||||
const onSaveAccountEditDialog = async (quickAction?: AccountQuickAction): Promise<void> => {
|
||||
if (!accountEditDialog) {
|
||||
return;
|
||||
}
|
||||
const editSnapshot = accountEditDialog;
|
||||
const validationError = validateAccountEdit(editSnapshot, settingsDraft);
|
||||
if (validationError) {
|
||||
showToast(validationError, 2800);
|
||||
return;
|
||||
}
|
||||
await performQuickAction(async () => {
|
||||
const nextDraft = applyAccountEdit(settingsDraft, editSnapshot);
|
||||
if (editSnapshot.target.type === "mega" || editSnapshot.target.type === "debridlink") {
|
||||
const statuses = await window.rd.checkDebridAccounts(buildAccountEditCheckSettings(nextDraft, editSnapshot), true, getAccountEditExpectedStatusId(editSnapshot) || undefined);
|
||||
const statusError = validateAccountEditStatuses(editSnapshot, statuses);
|
||||
if (statusError) {
|
||||
showToast(`Prüfung fehlgeschlagen: ${statusError}`, 4200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await persistSpecificSettings(nextDraft);
|
||||
closeAccountEditDialog();
|
||||
if (quickAction) {
|
||||
await runAccountQuickAction(quickAction);
|
||||
} else {
|
||||
showToast(`${findAccountOption(editSnapshot.target.kind).title} gespeichert`, 2200);
|
||||
}
|
||||
}, (error) => {
|
||||
showToast(`Account konnte nicht gespeichert werden: ${String(error)}`, 3200);
|
||||
});
|
||||
};
|
||||
|
||||
const onSaveAccountDialog = async (quickAction?: AccountQuickAction): Promise<void> => {
|
||||
if (!accountDialog) {
|
||||
@@ -3207,13 +3283,27 @@ export function App(): ReactElement {
|
||||
|
||||
const removeAccountTableRow = (row: AccountTableRow): void => {
|
||||
setAccountContextMenu(null);
|
||||
if (row.toggleKind === "mega" && row.megaLogin) {
|
||||
void onRemoveMegaAccount(row.megaLogin);
|
||||
} else if (row.toggleKind === "dl" && row.dlKey) {
|
||||
void onRemoveDebridLinkKey(row.dlKey);
|
||||
} else {
|
||||
void onRemoveAccount(row.entry);
|
||||
}
|
||||
void (async () => {
|
||||
const username = resolveAccountUsername(row.username, row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId]?.email : undefined);
|
||||
const confirmed = await askConfirmPrompt({
|
||||
title: `${row.hosterLabel} entfernen`,
|
||||
message: `Soll ${row.hosterLabel}${username !== "—" ? ` (${username})` : ""} wirklich entfernt werden?`,
|
||||
confirmLabel: "Entfernen",
|
||||
danger: true
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
await performQuickAction(async () => {
|
||||
await persistSpecificSettings(removeAccountTarget(settingsDraft, row.editTarget));
|
||||
if (row.entry.service === "alldebrid") {
|
||||
setAllDebridHostInfo(null);
|
||||
}
|
||||
showToast(`${row.hosterLabel} entfernt`, 2200);
|
||||
}, (error) => {
|
||||
showToast(`Account konnte nicht entfernt werden: ${String(error)}`, 3200);
|
||||
});
|
||||
})();
|
||||
};
|
||||
|
||||
const checkAccountTableRow = (row: AccountTableRow): void => {
|
||||
@@ -5621,8 +5711,8 @@ export function App(): ReactElement {
|
||||
<div className="acct2-head" role="row">
|
||||
<span className="acct2-c-check" role="columnheader" aria-label="Aktiviert" />
|
||||
<span role="columnheader">Hoster</span>
|
||||
<span role="columnheader">Download-Traffic übrig</span>
|
||||
<span role="columnheader"><button type="button" className="acct2-sortable" title="Nach Premium-Restlaufzeit sortieren" onClick={cycleAccountStatusSort}>Status{accountStatusSort === "desc" ? " ▼" : accountStatusSort === "asc" ? " ▲" : ""}</button></span>
|
||||
<span role="columnheader">Download-Traffic übrig</span>
|
||||
<span role="columnheader">Benutzername</span>
|
||||
<span role="columnheader">Verfallsdatum</span>
|
||||
<span role="columnheader">Passwort / Zugang</span>
|
||||
@@ -6306,8 +6396,82 @@ export function App(): ReactElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
)}
|
||||
|
||||
{accountEditDialog && accountEditOption && (
|
||||
<div className="modal-backdrop" onClick={closeAccountEditDialog}>
|
||||
<div className="modal-card account-edit-modal" role="dialog" aria-modal="true" aria-labelledby="account-edit-dialog-title" onClick={(event) => event.stopPropagation()}>
|
||||
<div className="account-edit-header">
|
||||
<AccountServiceLogo service={accountEditOption.service} className="account-edit-logo" />
|
||||
<div>
|
||||
<h3 id="account-edit-dialog-title">Account bearbeiten</h3>
|
||||
<span>{accountEditOption.serviceLabel} · {accountEditOption.modeLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-edit-summary">
|
||||
<div>
|
||||
<span>Benutzername</span>
|
||||
<strong>{resolveAccountUsername(accountEditRow?.username || accountEditDialog.login, accountEditStatus?.email)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Status</span>
|
||||
<strong>{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"}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-edit-fields">
|
||||
{(accountEditDialog.target.type === "mega" || accountEditOption.needsCredentials) && (
|
||||
<div className="field-grid two">
|
||||
<div>
|
||||
<label htmlFor="account-edit-login">Login / E-Mail</label>
|
||||
<input id="account-edit-login" autoComplete="username" value={accountEditDialog.login} onChange={(event) => setAccountEditDialog((prev) => prev ? { ...prev, login: event.target.value } : prev)} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="account-edit-password">Passwort</label>
|
||||
<input id="account-edit-password" type="password" autoComplete="current-password" value={accountEditDialog.password} onChange={(event) => setAccountEditDialog((prev) => prev ? { ...prev, password: event.target.value } : prev)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(accountEditDialog.target.type === "debridlink" || (accountEditOption.needsToken && accountEditDialog.target.type === "single")) && (
|
||||
<div>
|
||||
<label htmlFor="account-edit-token">{accountEditDialog.target.type === "debridlink" ? "API-Key" : "Token / API-Key"}</label>
|
||||
<input id="account-edit-token" type="password" autoComplete="off" value={accountEditDialog.token} onChange={(event) => setAccountEditDialog((prev) => prev ? { ...prev, token: event.target.value } : prev)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="account-edit-daily-limit">Tageslimit (GB, optional)</label>
|
||||
<input id="account-edit-daily-limit" inputMode="decimal" placeholder="z.B. 250" value={accountEditDialog.dailyLimitGb} onChange={(event) => setAccountEditDialog((prev) => prev ? { ...prev, dailyLimitGb: event.target.value } : prev)} />
|
||||
<div className="account-edit-note">Leer oder 0 bedeutet unbegrenzt. Der Zähler wird täglich um 00:00 Uhr zurückgesetzt.</div>
|
||||
</div>
|
||||
|
||||
{!accountEditOption.needsCredentials && !accountEditOption.needsToken && accountEditDialog.target.type === "single" && (
|
||||
<div className="account-edit-note prominent">Dieser Account verwendet den gespeicherten Browser- oder Cookie-Login.</div>
|
||||
)}
|
||||
|
||||
{accountEditQuickAction && (
|
||||
<button className="btn account-edit-quick-action" disabled={actionBusy} onClick={() => { void runAccountQuickAction(accountEditQuickAction.action); }}>
|
||||
{accountEditQuickAction.label === "Cookies" ? "Cookies importieren" : accountEditQuickAction.label === "Login" ? "Login öffnen" : "Status prüfen"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-actions account-edit-actions">
|
||||
<button className="btn" onClick={closeAccountEditDialog}>Abbrechen</button>
|
||||
<button className="btn accent" disabled={actionBusy} onClick={() => { void onSaveAccountEditDialog(); }}>Prüfen und speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{accountDialog && (
|
||||
<div className="modal-backdrop" onClick={closeAccountDialog}>
|
||||
<div className="modal-card account-modal" role="dialog" aria-modal="true" aria-labelledby="account-dialog-title" onClick={(event) => event.stopPropagation()}>
|
||||
@@ -6358,7 +6522,7 @@ export function App(): ReactElement {
|
||||
className={`account-picker-row${accountDialog.kind === option.kind ? " active" : ""}`}
|
||||
onClick={() => updateAccountDialogKind(option.kind)}
|
||||
>
|
||||
<span className="account-picker-domain"><span className={`account-picker-icon service-${option.service}`}>{option.serviceLabel.slice(0, 1).toUpperCase()}</span><strong>{option.title}</strong></span>
|
||||
<span className="account-picker-domain"><AccountServiceLogo service={option.service} className="account-picker-icon" /><strong>{option.title}</strong></span>
|
||||
<span className="account-picker-function">{getAccountPickerFunctionLabel(option)}</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -6702,7 +6866,7 @@ export function App(): ReactElement {
|
||||
<span>{accountContextMenu.row.modeLabel} · {accountContextMenu.row.username}</span>
|
||||
</div>
|
||||
<div className="ctx-menu-sep" />
|
||||
<button className="ctx-menu-item" onClick={() => { const row = accountContextMenu.row; setAccountContextMenu(null); openEditAccountDialog(row.entry.kind); }}>
|
||||
<button className="ctx-menu-item" onClick={() => { const row = accountContextMenu.row; setAccountContextMenu(null); openEditAccountDialog(row); }}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="ctx-menu-item" onClick={() => checkAccountTableRow(accountContextMenu.row)}>
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
import { parseDebridLinkApiKeys, getDebridLinkApiKeyId } from "../shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId, parseMegaDebridAccounts, serializeMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
||||
import type { AppSettings, DebridAccountStatus, DebridProvider } from "../shared/types";
|
||||
|
||||
export type AccountService = "realdebrid" | "megadebrid-api" | "megadebrid-web" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy";
|
||||
|
||||
export 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";
|
||||
|
||||
export type SingleAccountKind = Exclude<AccountKind, "megadebrid-api" | "megadebrid-web" | "debridlink-api">;
|
||||
|
||||
export type AccountEditTarget =
|
||||
| {
|
||||
type: "single";
|
||||
rowKey: string;
|
||||
kind: SingleAccountKind;
|
||||
service: AccountService;
|
||||
provider: DebridProvider;
|
||||
}
|
||||
| {
|
||||
type: "mega";
|
||||
rowKey: string;
|
||||
kind: "megadebrid-api" | "megadebrid-web";
|
||||
service: "megadebrid-api" | "megadebrid-web";
|
||||
accountId: string;
|
||||
}
|
||||
| {
|
||||
type: "debridlink";
|
||||
rowKey: string;
|
||||
kind: "debridlink-api";
|
||||
service: "debridlink";
|
||||
keyId: string;
|
||||
};
|
||||
|
||||
export interface AccountEditState {
|
||||
target: AccountEditTarget;
|
||||
login: string;
|
||||
password: string;
|
||||
token: string;
|
||||
dailyLimitGb: string;
|
||||
originalDailyLimitBytes: number;
|
||||
}
|
||||
|
||||
const BYTES_PER_GIB = 1024 * 1024 * 1024;
|
||||
|
||||
function formatDailyLimit(limitBytes: number): string {
|
||||
if (!Number.isFinite(limitBytes) || limitBytes <= 0) {
|
||||
return "";
|
||||
}
|
||||
const gib = limitBytes / BYTES_PER_GIB;
|
||||
const precision = gib >= 100 ? 0 : gib >= 10 ? 1 : 2;
|
||||
return gib.toFixed(precision).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
|
||||
}
|
||||
|
||||
function parseDailyLimit(value: string): number | null {
|
||||
const normalized = value.trim().replace(",", ".");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(normalized);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.floor(parsed * BYTES_PER_GIB);
|
||||
}
|
||||
|
||||
function withoutRecordKeys<T>(record: Record<string, T>, ...keys: string[]): Record<string, T> {
|
||||
const blocked = new Set(keys);
|
||||
return Object.fromEntries(Object.entries(record || {}).filter(([key]) => !blocked.has(key)));
|
||||
}
|
||||
|
||||
function resolveDailyLimit(value: string, originalBytes: number): number | null {
|
||||
return value === formatDailyLimit(originalBytes) ? (originalBytes > 0 ? originalBytes : null) : parseDailyLimit(value);
|
||||
}
|
||||
|
||||
function updateTargetLimit(record: Record<string, number>, oldId: string, newId: string, value: string, originalBytes: number): Record<string, number> {
|
||||
const next = withoutRecordKeys(record || {}, oldId, newId);
|
||||
const limit = resolveDailyLimit(value, originalBytes);
|
||||
if (limit && limit > 0) {
|
||||
next[newId] = limit;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function migrateDisabledId(ids: readonly string[], oldId: string, newId: string): string[] {
|
||||
const wasDisabled = ids.includes(oldId);
|
||||
const next = ids.filter((id) => id !== oldId && id !== newId);
|
||||
if (wasDisabled) {
|
||||
next.push(newId);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function updateProviderLimit(settings: AppSettings, provider: DebridProvider, value: string, originalBytes: number): AppSettings["providerDailyLimitBytes"] {
|
||||
const next = { ...(settings.providerDailyLimitBytes || {}) };
|
||||
const limit = resolveDailyLimit(value, originalBytes);
|
||||
if (limit && limit > 0) {
|
||||
next[provider] = limit;
|
||||
} else {
|
||||
delete next[provider];
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function createSingleEditState(target: Extract<AccountEditTarget, { type: "single" }>, settings: AppSettings): AccountEditState {
|
||||
const originalDailyLimitBytes = settings.providerDailyLimitBytes?.[target.provider] || 0;
|
||||
const base = {
|
||||
target,
|
||||
login: "",
|
||||
password: "",
|
||||
token: "",
|
||||
dailyLimitGb: formatDailyLimit(originalDailyLimitBytes),
|
||||
originalDailyLimitBytes
|
||||
};
|
||||
switch (target.kind) {
|
||||
case "realdebrid-api":
|
||||
return { ...base, token: settings.token };
|
||||
case "bestdebrid-api":
|
||||
return { ...base, token: settings.bestToken };
|
||||
case "alldebrid-api":
|
||||
return { ...base, token: settings.allDebridToken };
|
||||
case "onefichier-api":
|
||||
return { ...base, token: settings.oneFichierApiKey };
|
||||
case "ddownload-login":
|
||||
return { ...base, login: settings.ddownloadLogin, password: settings.ddownloadPassword };
|
||||
case "linksnappy-login":
|
||||
return { ...base, login: settings.linkSnappyLogin, password: settings.linkSnappyPassword };
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
export function createAccountEditState(target: AccountEditTarget, settings: AppSettings): AccountEditState {
|
||||
if (target.type === "single") {
|
||||
return createSingleEditState(target, settings);
|
||||
}
|
||||
if (target.type === "mega") {
|
||||
const account = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "")
|
||||
.find((entry) => entry.id === target.accountId);
|
||||
if (!account) {
|
||||
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
||||
}
|
||||
return {
|
||||
target,
|
||||
login: account.login,
|
||||
password: account.password,
|
||||
token: "",
|
||||
dailyLimitGb: formatDailyLimit(settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0),
|
||||
originalDailyLimitBytes: settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0
|
||||
};
|
||||
}
|
||||
const key = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").find((entry) => entry.id === target.keyId);
|
||||
if (!key) {
|
||||
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
||||
}
|
||||
return {
|
||||
target,
|
||||
login: "",
|
||||
password: "",
|
||||
token: key.token,
|
||||
dailyLimitGb: formatDailyLimit(settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0),
|
||||
originalDailyLimitBytes: settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0
|
||||
};
|
||||
}
|
||||
|
||||
function validateDailyLimit(value: string): string | null {
|
||||
const normalized = value.trim().replace(",", ".");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? null : "Das Tageslimit muss eine positive Zahl oder 0 sein.";
|
||||
}
|
||||
|
||||
export function validateAccountEdit(state: AccountEditState, settings: AppSettings): string | null {
|
||||
const limitError = validateDailyLimit(state.dailyLimitGb);
|
||||
if (limitError) {
|
||||
return limitError;
|
||||
}
|
||||
if (state.target.type === "mega") {
|
||||
const target = state.target;
|
||||
const login = state.login.trim();
|
||||
if (!login || !state.password.trim()) {
|
||||
return "Login und Passwort werden benötigt.";
|
||||
}
|
||||
if (/[:\r\n]/.test(login)) {
|
||||
return "Der Login darf keinen Doppelpunkt oder Zeilenumbruch enthalten.";
|
||||
}
|
||||
if (/[\r\n]/.test(state.password)) {
|
||||
return "Das Passwort darf keinen Zeilenumbruch enthalten.";
|
||||
}
|
||||
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "");
|
||||
if (!accounts.some((entry) => entry.id === target.accountId)) {
|
||||
return "Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.";
|
||||
}
|
||||
if (accounts.some((entry) => entry.id !== target.accountId && entry.login.toLowerCase() === login.toLowerCase())) {
|
||||
return "Dieser Mega-Debrid-Login ist bereits vorhanden.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (state.target.type === "debridlink") {
|
||||
const target = state.target;
|
||||
const token = state.token.trim();
|
||||
if (!token) {
|
||||
return "Der API-Key wird benötigt.";
|
||||
}
|
||||
if (/[,\r\n]/.test(token)) {
|
||||
return "Beim Bearbeiten ist genau ein API-Key erlaubt.";
|
||||
}
|
||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||
if (!keys.some((entry) => entry.id === target.keyId)) {
|
||||
return "Der ausgewählte Debrid-Link-Key wurde nicht gefunden.";
|
||||
}
|
||||
if (keys.some((entry) => entry.id !== target.keyId && entry.token === token)) {
|
||||
return "Dieser Debrid-Link-Key ist bereits vorhanden.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (["realdebrid-api", "bestdebrid-api", "alldebrid-api", "onefichier-api"].includes(state.target.kind) && !state.token.trim()) {
|
||||
return "Der Zugangstoken wird benötigt.";
|
||||
}
|
||||
if (["ddownload-login", "linksnappy-login"].includes(state.target.kind)) {
|
||||
if (!state.login.trim() || !state.password.trim()) {
|
||||
return "Login und Passwort werden benötigt.";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function applySingleEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> }): AppSettings {
|
||||
const providerDailyLimitBytes = updateProviderLimit(settings, state.target.provider, state.dailyLimitGb, state.originalDailyLimitBytes);
|
||||
const token = state.token.trim();
|
||||
const login = state.login.trim();
|
||||
switch (state.target.kind) {
|
||||
case "realdebrid-api":
|
||||
return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||
case "realdebrid-web":
|
||||
return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||
case "bestdebrid-api":
|
||||
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||
case "bestdebrid-web":
|
||||
return { ...settings, bestToken: "", bestDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||
case "alldebrid-api":
|
||||
return { ...settings, allDebridToken: token, allDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||
case "alldebrid-web":
|
||||
return { ...settings, allDebridToken: "", allDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||
case "ddownload-login":
|
||||
return { ...settings, ddownloadLogin: login, ddownloadPassword: state.password, providerDailyLimitBytes };
|
||||
case "onefichier-api":
|
||||
return { ...settings, oneFichierApiKey: token, providerDailyLimitBytes };
|
||||
case "linksnappy-login":
|
||||
return { ...settings, linkSnappyLogin: login, linkSnappyPassword: state.password, providerDailyLimitBytes };
|
||||
}
|
||||
}
|
||||
|
||||
function applyMegaEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "mega" }> }): AppSettings {
|
||||
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "");
|
||||
const index = accounts.findIndex((entry) => entry.id === state.target.accountId);
|
||||
if (index < 0) {
|
||||
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
||||
}
|
||||
const oldId = state.target.accountId;
|
||||
const login = state.login.trim();
|
||||
const newId = getMegaDebridAccountId(login);
|
||||
const nextAccounts = accounts.map((entry, accountIndex) => accountIndex === index
|
||||
? { login, password: state.password }
|
||||
: { login: entry.login, password: entry.password });
|
||||
const idChanged = oldId !== newId;
|
||||
const first = nextAccounts[0] || { login: "", password: "" };
|
||||
return {
|
||||
...settings,
|
||||
megaCredentials: serializeMegaDebridAccounts(nextAccounts),
|
||||
megaLogin: first.login,
|
||||
megaPassword: first.password,
|
||||
megaDebridDisabledAccountIds: idChanged
|
||||
? migrateDisabledId(settings.megaDebridDisabledAccountIds || [], oldId, newId)
|
||||
: [...(settings.megaDebridDisabledAccountIds || [])],
|
||||
megaDebridAccountDailyLimitBytes: updateTargetLimit(settings.megaDebridAccountDailyLimitBytes || {}, oldId, newId, state.dailyLimitGb, state.originalDailyLimitBytes),
|
||||
megaDebridAccountDailyUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.megaDebridAccountDailyUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.megaDebridAccountDailyUsageBytes || {}) },
|
||||
megaDebridAccountTotalUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.megaDebridAccountTotalUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.megaDebridAccountTotalUsageBytes || {}) },
|
||||
debridAccountStatuses: idChanged
|
||||
? withoutRecordKeys(settings.debridAccountStatuses || {}, oldId, newId)
|
||||
: { ...(settings.debridAccountStatuses || {}) }
|
||||
};
|
||||
}
|
||||
|
||||
function applyDebridLinkEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> }): AppSettings {
|
||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||
const index = keys.findIndex((entry) => entry.id === state.target.keyId);
|
||||
if (index < 0) {
|
||||
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
||||
}
|
||||
const oldId = state.target.keyId;
|
||||
const token = state.token.trim();
|
||||
const newId = getDebridLinkApiKeyId(token);
|
||||
const tokens = keys.map((entry, keyIndex) => keyIndex === index ? token : entry.token);
|
||||
const idChanged = oldId !== newId;
|
||||
return {
|
||||
...settings,
|
||||
debridLinkApiKeys: tokens.join("\n"),
|
||||
debridLinkDisabledKeyIds: idChanged
|
||||
? migrateDisabledId(settings.debridLinkDisabledKeyIds || [], oldId, newId)
|
||||
: [...(settings.debridLinkDisabledKeyIds || [])],
|
||||
debridLinkApiKeyDailyLimitBytes: updateTargetLimit(settings.debridLinkApiKeyDailyLimitBytes || {}, oldId, newId, state.dailyLimitGb, state.originalDailyLimitBytes),
|
||||
debridLinkApiKeyDailyUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.debridLinkApiKeyDailyUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) },
|
||||
debridLinkApiKeyTotalUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.debridLinkApiKeyTotalUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) },
|
||||
debridAccountStatuses: idChanged
|
||||
? withoutRecordKeys(settings.debridAccountStatuses || {}, oldId, newId)
|
||||
: { ...(settings.debridAccountStatuses || {}) }
|
||||
};
|
||||
}
|
||||
|
||||
export function applyAccountEdit(settings: AppSettings, state: AccountEditState): AppSettings {
|
||||
if (state.target.type === "single") {
|
||||
return applySingleEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> });
|
||||
}
|
||||
if (state.target.type === "mega") {
|
||||
return applyMegaEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "mega" }> });
|
||||
}
|
||||
return applyDebridLinkEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> });
|
||||
}
|
||||
|
||||
function clearSingleAccount(settings: AppSettings, target: Extract<AccountEditTarget, { type: "single" }>): AppSettings {
|
||||
const providerDailyLimitBytes = { ...(settings.providerDailyLimitBytes || {}) };
|
||||
const providerDailyUsageBytes = { ...(settings.providerDailyUsageBytes || {}) };
|
||||
const providerTotalUsageBytes = { ...(settings.providerTotalUsageBytes || {}) };
|
||||
delete providerDailyLimitBytes[target.provider];
|
||||
delete providerDailyUsageBytes[target.provider];
|
||||
delete providerTotalUsageBytes[target.provider];
|
||||
const base = { ...settings, providerDailyLimitBytes, providerDailyUsageBytes, providerTotalUsageBytes };
|
||||
switch (target.service) {
|
||||
case "realdebrid":
|
||||
return { ...base, token: "", realDebridUseWebLogin: false };
|
||||
case "bestdebrid":
|
||||
return { ...base, bestToken: "", bestDebridUseWebLogin: false };
|
||||
case "alldebrid":
|
||||
return { ...base, allDebridToken: "", allDebridUseWebLogin: false };
|
||||
case "ddownload":
|
||||
return { ...base, ddownloadLogin: "", ddownloadPassword: "" };
|
||||
case "onefichier":
|
||||
return { ...base, oneFichierApiKey: "" };
|
||||
case "linksnappy":
|
||||
return { ...base, linkSnappyLogin: "", linkSnappyPassword: "" };
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeAccountTarget(settings: AppSettings, target: AccountEditTarget): AppSettings {
|
||||
if (target.type === "single") {
|
||||
return clearSingleAccount(settings, target);
|
||||
}
|
||||
if (target.type === "mega") {
|
||||
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "")
|
||||
.filter((entry) => entry.id !== target.accountId)
|
||||
.map((entry) => ({ login: entry.login, password: entry.password }));
|
||||
const first = accounts[0] || { login: "", password: "" };
|
||||
return {
|
||||
...settings,
|
||||
megaCredentials: serializeMegaDebridAccounts(accounts),
|
||||
megaLogin: first.login,
|
||||
megaPassword: first.password,
|
||||
megaDebridApiEnabled: accounts.length > 0 && settings.megaDebridApiEnabled,
|
||||
megaDebridWebEnabled: accounts.length > 0 && settings.megaDebridWebEnabled,
|
||||
megaDebridDisabledAccountIds: (settings.megaDebridDisabledAccountIds || []).filter((id) => id !== target.accountId),
|
||||
megaDebridAccountDailyLimitBytes: withoutRecordKeys(settings.megaDebridAccountDailyLimitBytes || {}, target.accountId),
|
||||
megaDebridAccountDailyUsageBytes: withoutRecordKeys(settings.megaDebridAccountDailyUsageBytes || {}, target.accountId),
|
||||
megaDebridAccountTotalUsageBytes: withoutRecordKeys(settings.megaDebridAccountTotalUsageBytes || {}, target.accountId),
|
||||
debridAccountStatuses: withoutRecordKeys(settings.debridAccountStatuses || {}, target.accountId)
|
||||
};
|
||||
}
|
||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").filter((entry) => entry.id !== target.keyId);
|
||||
return {
|
||||
...settings,
|
||||
debridLinkApiKeys: keys.map((entry) => entry.token).join("\n"),
|
||||
debridLinkDisabledKeyIds: (settings.debridLinkDisabledKeyIds || []).filter((id) => id !== target.keyId),
|
||||
debridLinkApiKeyDailyLimitBytes: withoutRecordKeys(settings.debridLinkApiKeyDailyLimitBytes || {}, target.keyId),
|
||||
debridLinkApiKeyDailyUsageBytes: withoutRecordKeys(settings.debridLinkApiKeyDailyUsageBytes || {}, target.keyId),
|
||||
debridLinkApiKeyTotalUsageBytes: withoutRecordKeys(settings.debridLinkApiKeyTotalUsageBytes || {}, target.keyId),
|
||||
debridAccountStatuses: withoutRecordKeys(settings.debridAccountStatuses || {}, target.keyId)
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAccountEditCheckSettings(settings: AppSettings, state: AccountEditState): AppSettings {
|
||||
if (state.target.type === "mega") {
|
||||
const login = state.login.trim();
|
||||
return {
|
||||
...settings,
|
||||
megaCredentials: serializeMegaDebridAccounts([{ login, password: state.password }]),
|
||||
megaLogin: login,
|
||||
megaPassword: state.password,
|
||||
debridLinkApiKeys: ""
|
||||
};
|
||||
}
|
||||
if (state.target.type === "debridlink") {
|
||||
return {
|
||||
...settings,
|
||||
megaCredentials: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
debridLinkApiKeys: state.token.trim()
|
||||
};
|
||||
}
|
||||
return {
|
||||
...settings,
|
||||
megaCredentials: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
debridLinkApiKeys: ""
|
||||
};
|
||||
}
|
||||
|
||||
export function validateAccountEditStatuses(state: AccountEditState, statuses: readonly DebridAccountStatus[]): string | null {
|
||||
if (state.target.type === "single") {
|
||||
return null;
|
||||
}
|
||||
if (statuses.length === 0) {
|
||||
return "Die Prüfung hat keinen Account zurückgegeben.";
|
||||
}
|
||||
if (statuses.length !== 1) {
|
||||
return "Die Prüfung hat mehr als den ausgewählten Account zurückgegeben.";
|
||||
}
|
||||
const expectedId = getAccountEditExpectedStatusId(state);
|
||||
const status = statuses[0];
|
||||
if (status.accountId !== expectedId) {
|
||||
return "Die Prüfung hat den falschen Account zurückgegeben.";
|
||||
}
|
||||
return status.valid ? null : status.message || "Zugangsdaten ungültig";
|
||||
}
|
||||
|
||||
export function getAccountEditExpectedStatusId(state: AccountEditState): string | null {
|
||||
if (state.target.type === "mega") {
|
||||
return getMegaDebridAccountId(state.login);
|
||||
}
|
||||
if (state.target.type === "debridlink") {
|
||||
return getDebridLinkApiKeyId(state.token);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { AccountService } from "./account-edit";
|
||||
|
||||
export const ACCOUNT_SERVICE_ICONS = {
|
||||
realdebrid: "./provider-icons/real-debrid.png",
|
||||
"megadebrid-api": "./provider-icons/mega-debrid.png",
|
||||
"megadebrid-web": "./provider-icons/mega-debrid.png",
|
||||
bestdebrid: "./provider-icons/bestdebrid.ico",
|
||||
alldebrid: "./provider-icons/alldebrid.png",
|
||||
ddownload: "./provider-icons/ddownload.ico",
|
||||
onefichier: "./provider-icons/onefichier.png",
|
||||
debridlink: "./provider-icons/debrid-link.ico",
|
||||
linksnappy: "./provider-icons/linksnappy.png"
|
||||
} satisfies Record<AccountService, string>;
|
||||
@@ -55,6 +55,10 @@ export function isAccountRowSelectionKey(key: string, originatedOnRow: boolean):
|
||||
return originatedOnRow && (key === "Enter" || key === " ");
|
||||
}
|
||||
|
||||
export function resolveAccountUsername(storedUsername: string, checkedEmail?: string): string {
|
||||
return checkedEmail?.trim() || storedUsername.trim() || "—";
|
||||
}
|
||||
|
||||
export function buildBulkAccountEnabledState(
|
||||
currentDisabledProviders: DebridProvider[],
|
||||
configuredProviders: DebridProvider[],
|
||||
|
||||
+114
-18
@@ -2207,16 +2207,12 @@ body,
|
||||
|
||||
.account-picker-icon,
|
||||
.acct2-service-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
flex: 0 0 15px;
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 18px;
|
||||
border-radius: 3px;
|
||||
background: color-mix(in srgb, var(--accent) 24%, var(--field));
|
||||
color: color-mix(in srgb, var(--accent) 78%, var(--text));
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.account-modal-fields {
|
||||
@@ -3491,7 +3487,7 @@ td {
|
||||
.acct2-head,
|
||||
.acct2-row {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(150px, 1.25fr) minmax(165px, 1.3fr) minmax(190px, 1.5fr) minmax(165px, 1.3fr) minmax(100px, 0.72fr) minmax(140px, 0.9fr) 30px;
|
||||
grid-template-columns: 24px minmax(170px, 1.25fr) minmax(210px, 1.5fr) minmax(180px, 1.3fr) minmax(180px, 1.3fr) minmax(110px, 0.72fr) minmax(145px, 0.9fr) 30px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 1120px;
|
||||
@@ -3519,9 +3515,10 @@ td {
|
||||
color: var(--muted);
|
||||
}
|
||||
.acct2-row {
|
||||
min-height: 24px;
|
||||
min-height: 28px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
|
||||
font-size: 11px;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
cursor: default;
|
||||
}
|
||||
.acct2-row:last-child { border-bottom: 0; }
|
||||
@@ -3536,20 +3533,119 @@ td {
|
||||
.acct2-c-check input { cursor: pointer; }
|
||||
.acct2-c-actions { display: flex; gap: 6px; justify-content: flex-end; }
|
||||
.acct2-menu-button { width: 22px; min-width: 22px; height: 19px; padding: 0; font-size: 14px; line-height: 1; }
|
||||
.acct2-hoster { display: grid; grid-template-columns: 15px auto minmax(0, 1fr); align-items: center; gap: 5px; min-width: 0; }
|
||||
.acct2-hoster strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.acct2-mode { overflow: hidden; color: var(--muted); font-size: 9.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.acct2-hoster { display: grid; grid-template-columns: 18px auto minmax(0, 1fr); align-items: center; gap: 6px; min-width: 0; }
|
||||
.acct2-hoster strong { overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.acct2-mode { overflow: hidden; color: var(--muted); font-size: 12.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.acct2-traffic,
|
||||
.acct2-expiry { font-variant-numeric: tabular-nums; color: var(--muted); }
|
||||
.acct2-head > span:nth-child(3),
|
||||
.acct2-traffic { text-align: center; }
|
||||
.acct2-head > span:nth-child(4),
|
||||
.acct2-traffic { text-align: center; }
|
||||
.acct2-sortable { width: 100%; padding: 0; border: 0; color: inherit; background: transparent; font: inherit; letter-spacing: inherit; text-align: left; text-transform: inherit; cursor: pointer; user-select: none; }
|
||||
.acct2-sortable:hover { color: var(--text); }
|
||||
.acct2-user { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.acct2-credential { color: var(--muted); white-space: nowrap; }
|
||||
.acct2-status .account-validity-badge { height: 16px; margin-top: 0; padding: 0 5px; font-size: 9px; line-height: 14px; }
|
||||
.acct2-status .account-validity-badge { max-width: 100%; height: 20px; margin-top: 0; padding: 0 7px; overflow: hidden; font-size: 12px; line-height: 18px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.acct2-nostatus { color: var(--muted); opacity: 0.6; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.account-edit-modal {
|
||||
width: min(600px, calc(100vw - 36px));
|
||||
max-height: calc(100vh - 36px);
|
||||
gap: 14px;
|
||||
overflow-y: auto;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.account-edit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.account-edit-logo {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex: 0 0 34px;
|
||||
border-radius: 6px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.account-edit-header h3 {
|
||||
margin: 0 0 2px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.account-edit-header span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.account-edit-summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.account-edit-summary > div {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--field) 70%, transparent);
|
||||
}
|
||||
|
||||
.account-edit-summary span {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.account-edit-summary strong {
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.account-edit-fields {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.account-edit-fields label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.account-edit-note {
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.account-edit-note.prominent {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border));
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--accent) 7%, transparent);
|
||||
}
|
||||
|
||||
.account-edit-quick-action {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.account-edit-actions {
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.account-table-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -84,7 +84,7 @@ export interface ElectronApi {
|
||||
importBestDebridCookies: () => Promise<number>;
|
||||
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
|
||||
getDebridLinkHostLimits: () => Promise<DebridLinkHostLimitInfo[]>;
|
||||
checkDebridAccounts: (settings?: AppSettings) => Promise<DebridAccountStatus[]>;
|
||||
checkDebridAccounts: (settings?: AppSettings, persistValidOverride?: boolean, expectedAccountId?: string) => Promise<DebridAccountStatus[]>;
|
||||
checkMegaDebridAccount: (login: string, password: string) => Promise<DebridAccountStatus | null>;
|
||||
retryExtraction: (packageId: string) => Promise<void>;
|
||||
extractNow: (packageId: string) => Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user