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:
@@ -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
@@ -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"
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user