Compare commits

..

No commits in common. "04b8ba1dc4121747cb07909bd723ae90006d1b78" and "1ea9c42d045199362be2c13cd0534688751aa634" have entirely different histories.

3 changed files with 143 additions and 232 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.7.201", "version": "1.7.200",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -26,9 +26,6 @@ import {
getDebridLinkApiKeyDailyLimitBytes, getDebridLinkApiKeyDailyLimitBytes,
getDebridLinkApiKeyDailyRemainingBytes, getDebridLinkApiKeyDailyRemainingBytes,
getDebridLinkApiKeyDailyUsageBytes, getDebridLinkApiKeyDailyUsageBytes,
getMegaDebridAccountDailyLimitBytes,
getMegaDebridAccountDailyUsageBytes,
getMegaDebridAccountTotalUsageBytes,
getProviderDailyLimitBytes, getProviderDailyLimitBytes,
getProviderDailyRemainingBytes, getProviderDailyRemainingBytes,
getProviderTotalUsageBytes, getProviderTotalUsageBytes,
@ -2534,88 +2531,6 @@ export function App(): ReactElement {
}, [settingsDraft, snapshot.settings, allDebridHostInfo, allDebridHostLoading, hasSavedAllDebridAccount, allDebridSettingsDirty]); }, [settingsDraft, snapshot.settings, allDebridHostInfo, allDebridHostLoading, hasSavedAllDebridAccount, allDebridSettingsDirty]);
const configuredAccountServices = useMemo(() => new Set(configuredAccounts.map((entry) => entry.service)), [configuredAccounts]); const configuredAccountServices = useMemo(() => new Set(configuredAccounts.map((entry) => entry.service)), [configuredAccounts]);
const accountRows = useMemo(() => {
type AccountRow = {
rowKey: string;
entry: ConfiguredAccountEntry;
hosterLabel: string;
modeLabel: string;
username: string;
accountId: string | null;
checkable: boolean;
disabled: boolean;
dailyUsedBytes: number;
dailyLimitBytes: number;
dailyRemainingBytes: number;
totalUsedBytes: number;
toggleKind: "mega" | "dl" | "single";
megaLogin?: string;
dlKey?: DebridLinkAccountKeyEntry;
};
const rows: AccountRow[] = [];
for (const entry of configuredAccounts) {
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
const accounts = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "");
for (const acc of accounts) {
const used = getMegaDebridAccountDailyUsageBytes(snapshot.settings, acc.id);
const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id);
rows.push({
rowKey: `mega-${acc.id}`,
entry,
hosterLabel: entry.serviceLabel,
modeLabel: entry.modeLabel,
username: acc.maskedLogin,
accountId: acc.id,
checkable: true,
disabled: (settingsDraft.megaDebridDisabledAccountIds || []).includes(acc.id),
dailyUsedBytes: used,
dailyLimitBytes: limit,
dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0,
totalUsedBytes: getMegaDebridAccountTotalUsageBytes(snapshot.settings, acc.id),
toggleKind: "mega",
megaLogin: acc.login
});
}
} else if (entry.kind === "debridlink-api") {
for (const key of entry.debridLinkKeys) {
rows.push({
rowKey: `dl-${key.id}`,
entry,
hosterLabel: entry.serviceLabel,
modeLabel: entry.modeLabel,
username: key.masked,
accountId: key.id,
checkable: true,
disabled: key.disabled,
dailyUsedBytes: key.dailyUsedBytes,
dailyLimitBytes: key.dailyLimitBytes,
dailyRemainingBytes: key.dailyLimitBytes > 0 ? Math.max(0, key.dailyLimitBytes - key.dailyUsedBytes) : 0,
totalUsedBytes: key.totalUsedBytes,
toggleKind: "dl",
dlKey: key
});
}
} else {
rows.push({
rowKey: `svc-${entry.service}`,
entry,
hosterLabel: entry.serviceLabel,
modeLabel: entry.modeLabel,
username: entry.summary,
accountId: null,
checkable: false,
disabled: entry.disabled,
dailyUsedBytes: entry.dailyUsedBytes,
dailyLimitBytes: entry.dailyLimitBytes,
dailyRemainingBytes: entry.dailyLimitBytes > 0 ? Math.max(0, entry.dailyRemainingBytes ?? 0) : 0,
totalUsedBytes: entry.totalUsedBytes,
toggleKind: "single"
});
}
}
return rows;
}, [configuredAccounts, settingsDraft, snapshot.settings]);
const availableAccountOptions = useMemo(() => ( const availableAccountOptions = useMemo(() => (
ACCOUNT_OPTIONS.filter((option) => !configuredAccountServices.has(option.service)) ACCOUNT_OPTIONS.filter((option) => !configuredAccountServices.has(option.service))
), [configuredAccountServices]); ), [configuredAccountServices]);
@ -2936,7 +2851,6 @@ export function App(): ReactElement {
} else if (selectedOption) { } else if (selectedOption) {
showToast(`${selectedOption.title} gespeichert`, 2200); showToast(`${selectedOption.title} gespeichert`, 2200);
} }
void checkAllAccounts();
}, (error) => { }, (error) => {
showToast(`Account konnte nicht gespeichert werden: ${String(error)}`, 3200); showToast(`Account konnte nicht gespeichert werden: ${String(error)}`, 3200);
}); });
@ -3018,40 +2932,6 @@ export function App(): ReactElement {
}); });
}; };
const onToggleMegaAccountEnabled = async (login: string, currentlyDisabled: boolean): Promise<void> => {
const accId = getMegaDebridAccountId(login.trim());
await performQuickAction(async () => {
const current = settingsDraft.megaDebridDisabledAccountIds || [];
const next = currentlyDisabled ? current.filter((id) => id !== accId) : [...current, accId];
await persistSpecificSettings({ ...settingsDraft, megaDebridDisabledAccountIds: next });
showToast(currentlyDisabled ? "Account aktiviert" : "Account deaktiviert", 2000);
}, (error) => {
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
});
};
const onRemoveMegaAccount = async (login: string): Promise<void> => {
const confirmed = await askConfirmPrompt({ title: "Account entfernen", message: `Soll der Mega-Debrid-Account ${maskMegaDebridLogin(login)} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true });
if (!confirmed) return;
await performQuickAction(async () => {
const remaining = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "")
.filter((a) => a.login.trim().toLowerCase() !== login.trim().toLowerCase());
const nextCreds = serializeMegaDebridAccounts(remaining.map((a) => ({ login: a.login, password: a.password })));
await persistSpecificSettings({ ...settingsDraft, megaCredentials: nextCreds, megaLogin: remaining[0]?.login ?? "", megaPassword: remaining[0]?.password ?? "" });
showToast("Account entfernt", 2000);
}, (error) => { showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 3200); });
};
const onRemoveDebridLinkKey = async (key: DebridLinkAccountKeyEntry): Promise<void> => {
const confirmed = await askConfirmPrompt({ title: "Key entfernen", message: `Soll der Debrid-Link-Key ${key.masked} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true });
if (!confirmed) return;
await performQuickAction(async () => {
const remaining = parseDebridLinkApiKeys(settingsDraft.debridLinkApiKeys || "").filter((k) => k.id !== key.id);
await persistSpecificSettings({ ...settingsDraft, debridLinkApiKeys: remaining.map((k) => k.token).join("\n") });
showToast("Key entfernt", 2000);
}, (error) => { showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 3200); });
};
const onToggleAccountEnabled = async (entry: ConfiguredAccountEntry): Promise<void> => { const onToggleAccountEnabled = async (entry: ConfiguredAccountEntry): Promise<void> => {
await performQuickAction(async () => { await performQuickAction(async () => {
const provider = entry.service as DebridProvider; const provider = entry.service as DebridProvider;
@ -5303,84 +5183,160 @@ export function App(): ReactElement {
</div> </div>
<div className="account-board-summary"> <div className="account-board-summary">
<span className="account-inline-stat">{accountRows.length} {accountRows.length === 1 ? "Account" : "Accounts"}</span> <span className="account-inline-stat">{configuredAccounts.length} aktiv</span>
<span className="account-inline-stat">{availableAccountOptions.length} weitere Typen verfügbar</span> <span className="account-inline-stat">{availableAccountOptions.length} weitere Typen verfügbar</span>
</div> </div>
{accountRows.length === 0 && ( <label className="toggle-line account-display-toggle">
<input
type="checkbox"
checked={settingsDraft.accountListShowDetailedDebridLinkKeys}
onChange={(e) => setBool("accountListShowDetailedDebridLinkKeys", e.target.checked)}
/>
Debrid-Link-Keys im Feld "Zugang" einzeln untereinander anzeigen
</label>
<div className="account-display-actions">
<button className="btn btn-sm" disabled={actionBusy} onClick={resetAccountColumnWidths}>
Spalten zurücksetzen
</button>
</div>
{configuredAccounts.length === 0 && (
<div className="account-empty-state"> <div className="account-empty-state">
<strong>Noch keine Accounts hinterlegt</strong> <strong>Noch keine Accounts hinterlegt</strong>
<span>Füge über "Account hinzufügen" den ersten Dienst hinzu. Danach erscheinen hier Status, Zugang und Aktionen als Liste.</span> <span>Füge über "Account hinzufügen" den ersten Dienst hinzu. Danach erscheinen hier Status, Zugang und Aktionen als Liste.</span>
</div> </div>
)} )}
{accountRows.length > 0 && ( {configuredAccounts.length > 0 && (
<div className="acct2-table"> <div className="account-table" style={accountTableStyle}>
<div className="acct2-head"> <div className="account-table-head">
<span className="acct2-c-check" /> <div className="account-header-cell">
<span>Hoster</span> <span>Account</span>
<span>Download-Traffic</span> <button
<span>Status</span> className="account-resize-handle"
<span>Benutzername</span> title="Spalte ziehen"
<span>Verfallsdatum</span> onMouseDown={(event) => {
<span className="acct2-c-actions">Aktion</span> event.preventDefault();
startAccountColumnResize("service", event.clientX);
}}
/>
</div>
<div className="account-header-cell">
<span>Typ</span>
<button
className="account-resize-handle"
title="Spalte ziehen"
onMouseDown={(event) => {
event.preventDefault();
startAccountColumnResize("mode", event.clientX);
}}
/>
</div>
<div className="account-header-cell">
<span>Status</span>
<button
className="account-resize-handle"
title="Spalte ziehen"
onMouseDown={(event) => {
event.preventDefault();
startAccountColumnResize("status", event.clientX);
}}
/>
</div>
<div className="account-header-cell">
<span>Info</span>
</div>
<div className="account-header-cell">
<span>Zugang</span>
<button
className="account-resize-handle"
title="Spalte ziehen"
onMouseDown={(event) => {
event.preventDefault();
startAccountColumnResize("secret", event.clientX);
}}
/>
</div>
<div className="account-header-cell">
<span>Aktionen</span>
</div>
</div> </div>
{accountRows.map((row) => { {configuredAccounts.map((entry) => {
const st = row.accountId ? (snapshot.settings?.debridAccountStatuses?.[row.accountId] ?? null) : null; const option = findAccountOption(entry.kind);
const checking = row.accountId ? megaCheckingIds.has(row.accountId) : false; const quickAction = getAccountQuickActionMeta(entry.kind);
let statusCls = "ok"; const showStatusButton = entry.service === "alldebrid";
let statusText = "Konfiguriert"; const showQuickActionButton = Boolean(quickAction && !(showStatusButton && quickAction.action === "alldebrid-status"));
if (row.disabled) { statusCls = "disabled"; statusText = "Deaktiviert"; } const allDebridStateClass = entry.service === "alldebrid" && allDebridHostInfo ? ` account-status-${allDebridHostInfo.state}` : "";
else if (!row.checkable) { statusCls = "ok"; statusText = "Konfiguriert"; }
else if (checking) { statusCls = "unknown"; statusText = "Prüfe…"; }
else if (!st) { statusCls = "unknown"; statusText = "Noch nicht geprüft"; }
else if (!st.valid) { statusCls = "invalid"; statusText = st.message || "Login ungültig"; }
else if (!st.isPremium) { statusCls = "free"; statusText = "Free Account"; }
else { statusCls = "ok"; statusText = st.message || "Premium Account"; }
const isProblem = statusCls === "invalid";
const username = st && st.email ? st.email : row.username;
const expiry = st && st.premiumUntilMs && st.premiumUntilMs > 0 ? new Date(st.premiumUntilMs).toLocaleDateString("de-DE") : "—";
const traffic = row.dailyLimitBytes > 0
? `${humanSize(row.dailyRemainingBytes)} von ${humanSize(row.dailyLimitBytes)} übrig`
: "Unbeschränkt";
return ( return (
<div key={row.rowKey} className={`acct2-row${row.disabled ? " acct2-disabled" : ""}${isProblem ? " acct2-problem" : ""}`}> <div key={entry.service} className={`account-row${entry.disabled ? " account-row-disabled" : ""}`}>
<span className="acct2-c-check"> <div className="account-cell account-service-cell">
<input <strong>{entry.serviceLabel}</strong>
type="checkbox" <span>{option.title}</span>
checked={!row.disabled} </div>
disabled={actionBusy} <div className="account-cell">
title={row.disabled ? "Account aktivieren" : "Account deaktivieren (bleibt gespeichert)"} <span className="account-mode-pill">{entry.modeLabel}</span>
onChange={() => { </div>
if (row.toggleKind === "mega" && row.megaLogin) { void onToggleMegaAccountEnabled(row.megaLogin, row.disabled); } <div className="account-cell account-status-cell">
else if (row.toggleKind === "dl" && row.dlKey) { void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey); } <span className={`account-status-pill${entry.disabled ? " account-status-disabled" : ""}${allDebridStateClass}`}>{entry.statusLabel}</span>
else { void onToggleAccountEnabled(row.entry); } {entry.note && <span className="account-note">{entry.note}</span>}
}} </div>
/> <div className="account-cell account-info-cell">
</span> {entry.debridLinkKeys.length > 0 ? (
<span className="acct2-hoster"> <div className="account-usage-stack">
<strong>{row.hosterLabel}</strong> <button className="btn btn-sm" onClick={() => setKeyStatsPopup(entry.service)}>
<span className="acct2-mode">{row.modeLabel}</span> Statistik
</span> </button>
<span className="acct2-traffic">{traffic}</span> <span className="account-usage-total">Insgesamt: {humanSize(entry.totalUsedBytes)}</span>
<span className="acct2-status"> </div>
<span className={`account-validity-badge ${statusCls}`}>{statusText}</span> ) : (
</span> <div className="account-usage-stack">
<span className="acct2-user" title={username}>{username}</span> <div className={`account-usage-stats${entry.dailyLimitReached ? " warning" : ""}`}>
<span className="acct2-expiry">{expiry}</span> <span>Heute: {humanSize(entry.dailyUsedBytes)}</span>
<span className="acct2-c-actions"> <span>{entry.dailyLimitBytes > 0 ? `Limit: ${humanSize(entry.dailyLimitBytes)}` : "Kein Tageslimit"}</span>
{row.toggleKind === "single" && getAccountQuickActionMeta(row.entry.kind) && ( {entry.dailyLimitBytes > 0 && (
<button className="btn btn-sm" disabled={actionBusy} onClick={() => { void onAccountRowQuickAction(row.entry); }}> <span>{entry.dailyLimitReached ? "Fallback aktiv" : `Rest: ${humanSize(entry.dailyRemainingBytes || 0)}`}</span>
{getAccountQuickActionMeta(row.entry.kind)?.label} )}
</div>
<span className="account-usage-total">Insgesamt: {humanSize(entry.totalUsedBytes)}</span>
</div>
)}
</div>
<div className="account-cell">
{entry.summaryLines.length > 1 ? (
<div className="account-secret account-secret-multiline">
{entry.summaryLines.map((line) => (
<span key={line}>{line}</span>
))}
</div>
) : (
<span className="account-secret">{entry.summary}</span>
)}
</div>
<div className="account-cell account-row-actions">
{showStatusButton && (
<button className="btn" disabled={actionBusy || allDebridHostLoading || !hasSavedAllDebridAccount} onClick={() => { void performQuickAction(async () => { await runAccountQuickAction("alldebrid-status"); }, (error) => { showToast(`AllDebrid Status fehlgeschlagen: ${String(error)}`, 3200); }); }}>
Status
</button> </button>
)} )}
<button className="btn btn-sm" disabled={actionBusy} title="Account bearbeiten" onClick={() => openEditAccountDialog(row.entry.kind)}>Bearbeiten</button> {showQuickActionButton && quickAction && (
<button className="btn btn-sm danger" disabled={actionBusy} title="Account entfernen" onClick={() => { <button className="btn" disabled={actionBusy} onClick={() => { void onAccountRowQuickAction(entry); }}>
if (row.toggleKind === "mega" && row.megaLogin) { void onRemoveMegaAccount(row.megaLogin); } {quickAction.label}
else if (row.toggleKind === "dl" && row.dlKey) { void onRemoveDebridLinkKey(row.dlKey); } </button>
else { void onRemoveAccount(row.entry); } )}
}}>Entfernen</button> <button className="btn" disabled={actionBusy} onClick={() => { void onToggleAccountEnabled(entry); }}>
</span> {entry.disabled ? "Aktivieren" : "Deaktivieren"}
</button>
<button className="btn" disabled={actionBusy || entry.dailyUsedBytes <= 0} onClick={() => { void onResetAccountDailyUsage(entry); }}>
Reset Heute
</button>
<button className="btn" disabled={actionBusy} onClick={() => openEditAccountDialog(entry.kind)}>
Bearbeiten
</button>
<button className="btn danger" disabled={actionBusy} onClick={() => { void onRemoveAccount(entry); }}>
Entfernen
</button>
</div>
</div> </div>
); );
})} })}

View File

@ -3255,55 +3255,10 @@ td {
border: 1px solid transparent; border: 1px solid transparent;
white-space: nowrap; white-space: nowrap;
} }
.account-validity-badge.ok { color: #10240f; background: #4fb96a; border-color: #3f9d57; } .account-validity-badge.ok { color: #1c1206; background: linear-gradient(90deg, #7bd88f, #4fb96a); border-color: #4fb96a; }
.account-validity-badge.free { color: #2a2113; background: #f2c14e; border-color: #d9a72f; } .account-validity-badge.free { color: #2a2113; background: #f2c14e; border-color: #d9a72f; }
.account-validity-badge.invalid { color: #fff; background: #d9534f; border-color: #c0392b; } .account-validity-badge.invalid { color: #fff; background: #d9534f; border-color: #c0392b; }
.account-validity-badge.unknown { color: var(--muted, #a59c8e); background: transparent; border-color: var(--line, #4a4032); } .account-validity-badge.unknown { color: var(--muted, #a59c8e); background: transparent; border-color: var(--line, #4a4032); }
.account-validity-badge.disabled { color: var(--muted, #a59c8e); background: transparent; border-color: var(--border); opacity: 0.8; }
.acct2-table {
display: flex;
flex-direction: column;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
margin-top: 4px;
}
.acct2-head,
.acct2-row {
display: grid;
grid-template-columns: 34px minmax(110px, 1.1fr) minmax(150px, 1.5fr) minmax(120px, 1.4fr) minmax(140px, 1.6fr) minmax(92px, 0.9fr) auto;
align-items: center;
gap: 10px;
padding: 7px 12px;
}
.acct2-head {
background: color-mix(in srgb, var(--card) 60%, transparent);
border-bottom: 1px solid var(--border);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.acct2-row {
border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
font-size: 13px;
}
.acct2-row:last-child { border-bottom: 0; }
.acct2-row:nth-child(even) { background: color-mix(in srgb, var(--card) 28%, transparent); }
.acct2-row.acct2-problem { background: color-mix(in srgb, var(--danger) 15%, transparent); }
.acct2-row.acct2-disabled { opacity: 0.55; }
.acct2-c-check { display: flex; justify-content: center; }
.acct2-c-check input { cursor: pointer; }
.acct2-c-actions { display: flex; gap: 6px; justify-content: flex-end; }
.acct2-hoster { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.acct2-hoster strong { font-size: 13px; }
.acct2-mode { font-size: 11px; color: var(--muted); }
.acct2-traffic,
.acct2-expiry { font-variant-numeric: tabular-nums; color: var(--muted); }
.acct2-user { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct2-status .account-validity-badge { margin-top: 0; }
.rotation-panel { display: flex; flex-direction: column; gap: 6px; max-height: 320px; overflow-y: auto; } .rotation-panel { display: flex; flex-direction: column; gap: 6px; max-height: 320px; overflow-y: auto; }
.rotation-empty { color: var(--muted, #a59c8e); font-size: 12px; } .rotation-empty { color: var(--muted, #a59c8e); font-size: 12px; }