fix(accounts): unlock parent provider when enabling rows

Make individual Debrid-Link and Mega-Debrid switches express an explicit enabled target. Enabling a row now clears both its own disabled identifier and the effective provider lock; Mega-Debrid also re-enables the selected API or web mode.

Keep row-level disabling scoped to the selected account, align the legacy key view with the effective provider state, and cover the provider-plus-account lock combination with a regression test.
This commit is contained in:
Sucukdeluxe
2026-08-15 01:19:29 +02:00
parent 0119534e07
commit 479c2b600a
4 changed files with 80 additions and 22 deletions
+1
View File
@@ -9,6 +9,7 @@ All notable changes to Multi-Debrid Downloader are documented in this file.
- Split account refresh into active-account and all-account checks with matching result counts.
- Show failed check results for disabled accounts when all configured accounts are refreshed.
- Apply individual and bulk account enablement changes immediately while settings are saved, with automatic rollback after a failed save.
- Re-enable parent providers and Mega-Debrid modes when an individual disabled account or API key is enabled.
## [2.0.40] - 2026-08-15
+33 -20
View File
@@ -40,7 +40,7 @@ import {
} from "../shared/provider-daily-limits";
import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order";
import { pruneSelection, shouldClearDownloadSelection, shouldClearDownloadSelectionOnEscape } from "./selection";
import { buildBulkAccountEnabledState, buildConfiguredProviderOrder, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelection, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate } from "./account-ui";
import { buildBulkAccountEnabledState, buildConfiguredProviderOrder, buildScopedAccountEnabledState, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelection, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate } from "./account-ui";
import type { AccountModeFilter } from "./account-ui";
import { buildAccountDeleteCommand, buildAccountReplaceCommand, createAccountEditState, validateAccountEdit } from "./account-edit";
import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit";
@@ -2319,8 +2319,8 @@ export function App(): ReactElement {
accountId: acc.accountId,
checkable: true,
disabled: entry.disabled || (entry.kind === "megadebrid-api"
? settingsDraft.megaDebridApiDisabledAccountIds.includes(acc.accountId)
: settingsDraft.megaDebridWebDisabledAccountIds.includes(acc.accountId)),
? !settingsDraft.megaDebridApiEnabled || settingsDraft.megaDebridApiDisabledAccountIds.includes(acc.accountId)
: !settingsDraft.megaDebridWebEnabled || settingsDraft.megaDebridWebDisabledAccountIds.includes(acc.accountId)),
dailyUsedBytes: used,
dailyLimitBytes: limit,
dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0,
@@ -2820,20 +2820,23 @@ export function App(): ReactElement {
);
};
const onToggleDebridLinkApiKeyEnabled = async (entry: ConfiguredAccountEntry, key: DebridLinkAccountKeyEntry): Promise<void> => {
const onToggleDebridLinkApiKeyEnabled = async (entry: ConfiguredAccountEntry, key: DebridLinkAccountKeyEntry, enabled: boolean): Promise<void> => {
await performQuickAction(async () => {
const currentDisabledIds = settingsDraft.debridLinkDisabledKeyIds || [];
const currentlyDisabled = currentDisabledIds.includes(key.id);
const nextDisabledIds = currentlyDisabled
? currentDisabledIds.filter((existingId) => existingId !== key.id)
: [...currentDisabledIds, key.id];
const nextState = buildScopedAccountEnabledState(
settingsDraft.disabledProviders || [],
["debridlink"],
settingsDraft.debridLinkDisabledKeyIds || [],
key.id,
enabled
);
const nextDraft: RendererSettingsDraft = {
...settingsDraft,
debridLinkDisabledKeyIds: nextDisabledIds
disabledProviders: nextState.disabledProviders,
debridLinkDisabledKeyIds: nextState.disabledAccountIds
};
await persistAccountToggle(nextDraft);
showToast(
currentlyDisabled
enabled
? `${entry.serviceLabel} ${key.label} aktiviert`
: `${entry.serviceLabel} ${key.label} deaktiviert`,
2200
@@ -2855,20 +2858,30 @@ export function App(): ReactElement {
});
};
const onToggleMegaAccountEnabled = async (kind: "megadebrid-api" | "megadebrid-web", accountId: string, currentlyDisabled: boolean): Promise<void> => {
const onToggleMegaAccountEnabled = async (kind: "megadebrid-api" | "megadebrid-web", accountId: string, enabled: boolean): Promise<void> => {
await performQuickAction(async () => {
const mode = kind === "megadebrid-web" ? "web" : "api";
const current = mode === "api" ? settingsDraft.megaDebridApiDisabledAccountIds : settingsDraft.megaDebridWebDisabledAccountIds;
const next = currentlyDisabled ? current.filter((id) => id !== accountId) : [...current, accountId];
const nextState = buildScopedAccountEnabledState(
settingsDraft.disabledProviders || [],
["megadebrid", kind],
current,
accountId,
enabled
);
const next = nextState.disabledAccountIds;
const apiDisabledIds = mode === "api" ? next : settingsDraft.megaDebridApiDisabledAccountIds;
const webDisabledIds = mode === "web" ? next : settingsDraft.megaDebridWebDisabledAccountIds;
await persistAccountToggle({
...settingsDraft,
disabledProviders: nextState.disabledProviders,
megaDebridApiEnabled: mode === "api" && enabled ? true : settingsDraft.megaDebridApiEnabled,
megaDebridWebEnabled: mode === "web" && enabled ? true : settingsDraft.megaDebridWebEnabled,
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
megaDebridApiDisabledAccountIds: apiDisabledIds,
megaDebridWebDisabledAccountIds: webDisabledIds
});
showToast(currentlyDisabled ? "Account aktiviert" : "Account deaktiviert", 2000);
showToast(enabled ? "Account aktiviert" : "Account deaktiviert", 2000);
}, (error) => {
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
});
@@ -2913,7 +2926,7 @@ export function App(): ReactElement {
if (row.toggleKind === "mega" && row.accountId) {
void onToggleMegaAccountEnabled(row.entry.kind as "megadebrid-api" | "megadebrid-web", row.accountId, row.disabled);
} else if (row.toggleKind === "dl" && row.dlKey) {
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey);
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey, row.disabled);
} else {
void onToggleAccountEnabled(row.entry);
}
@@ -6175,7 +6188,7 @@ export function App(): ReactElement {
<span className="col-action"></span>
</div>
{entry.debridLinkKeys.map((key, ki) => (
<div key={key.id} className={`account-subkey-table-row${key.dailyLimitReached || (debridLinkHostLimits[key.id] && debridLinkHostLimits[key.id].state !== "ready") ? " warning" : ""}${key.disabled ? " disabled" : ""}`}>
<div key={key.id} className={`account-subkey-table-row${key.dailyLimitReached || (debridLinkHostLimits[key.id] && debridLinkHostLimits[key.id].state !== "ready") ? " warning" : ""}${entry.disabled || key.disabled ? " disabled" : ""}`}>
{(() => {
const hostInfo = debridLinkHostLimits[key.id];
const statusDisplay = getDebridLinkKeyStatusDisplay(key, hostInfo);
@@ -6196,17 +6209,17 @@ export function App(): ReactElement {
{key.masked}
</button>
<span className="col-usage">{humanSize(key.dailyUsedBytes)}</span>
<span className="col-limit">{key.disabled ? "Deaktiviert" : key.dailyLimitBytes > 0 ? humanSize(key.dailyLimitBytes) : "Kein Limit"}</span>
<span className="col-limit">{entry.disabled || key.disabled ? "Deaktiviert" : key.dailyLimitBytes > 0 ? humanSize(key.dailyLimitBytes) : "Kein Limit"}</span>
<span className={`col-status status-pill status-pill-${statusDisplay.tone}`} title={statusDisplay.title}>{statusDisplay.label}</span>
<span className="col-traffic" title={hostInfo?.note || ""}>{formatDebridLinkTraffic(hostInfo)}</span>
<span className="col-links" title={hostInfo?.note || ""}>{formatDebridLinkCountQuota(hostInfo)}</span>
<span className="col-action">
<button
className={`btn btn-sm ${key.disabled ? "success" : "danger"}`}
className={`btn btn-sm ${entry.disabled || key.disabled ? "success" : "danger"}`}
disabled={actionBusy}
onClick={() => { void onToggleDebridLinkApiKeyEnabled(entry, key); }}
onClick={() => { void onToggleDebridLinkApiKeyEnabled(entry, key, entry.disabled || key.disabled); }}
>
{key.disabled ? "Aktivieren" : "Deaktivieren"}
{entry.disabled || key.disabled ? "Aktivieren" : "Deaktivieren"}
</button>
<button
className="btn btn-sm"
+18
View File
@@ -89,6 +89,24 @@ export async function runOptimisticAccountUpdate<T>(
}
}
export function buildScopedAccountEnabledState(
currentDisabledProviders: DebridProvider[],
providerIds: DebridProvider[],
currentDisabledAccountIds: string[],
accountId: string,
enabled: boolean
): { disabledProviders: DebridProvider[]; disabledAccountIds: string[] } {
const providers = new Set(providerIds);
return {
disabledProviders: enabled
? currentDisabledProviders.filter((provider) => !providers.has(provider))
: [...currentDisabledProviders],
disabledAccountIds: enabled
? currentDisabledAccountIds.filter((id) => id !== accountId)
: [...new Set([...currentDisabledAccountIds, accountId])]
};
}
export function buildBulkAccountEnabledState(
currentDisabledProviders: DebridProvider[],
configuredProviders: DebridProvider[],
+26
View File
@@ -7,6 +7,7 @@ import { createRendererSettings, createRendererState } from "../src/main/rendere
import { buildAccountAddFields, createAccountDialogState } from "../src/renderer/App";
import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit";
import {
buildScopedAccountEnabledState,
buildBulkAccountEnabledState,
buildConfiguredProviderOrder,
resolveAccountStatusState,
@@ -958,6 +959,31 @@ describe("account workspace", () => {
});
describe("settings App integration", () => {
it("enables an account by clearing both its row-level and provider-level locks", () => {
expect(buildScopedAccountEnabledState(
["debridlink", "linksnappy"],
["debridlink"],
["key-disabled", "key-other"],
"key-disabled",
true
)).toEqual({
disabledProviders: ["linksnappy"],
disabledAccountIds: ["key-other"]
});
expect(buildScopedAccountEnabledState([], ["debridlink"], [], "key-active", false)).toEqual({
disabledProviders: [],
disabledAccountIds: ["key-active"]
});
const debridLinkBlock = sourceBlock(appSource, "const onToggleDebridLinkApiKeyEnabled", "const onAccountRowQuickAction");
const megaBlock = sourceBlock(appSource, "const onToggleMegaAccountEnabled", "const onRemoveDebridLinkKey");
expect(debridLinkBlock).toContain("buildScopedAccountEnabledState");
expect(megaBlock).toContain("buildScopedAccountEnabledState");
expect(megaBlock).toContain('megaDebridApiEnabled: mode === "api" && enabled ? true');
expect(megaBlock).toContain('megaDebridWebEnabled: mode === "web" && enabled ? true');
});
it("shows a failed all-account check even when the account is disabled", () => {
expect(resolveAccountStatusState(true, { valid: false, isPremium: false })).toBe("invalid");
expect(resolveAccountStatusState(true, { valid: true, isPremium: true })).toBe("disabled");