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.
|
- 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.
|
- 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.
|
- 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
|
## [2.0.40] - 2026-08-15
|
||||||
|
|
||||||
|
|||||||
+35
-22
@@ -40,7 +40,7 @@ import {
|
|||||||
} from "../shared/provider-daily-limits";
|
} from "../shared/provider-daily-limits";
|
||||||
import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order";
|
import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order";
|
||||||
import { pruneSelection, shouldClearDownloadSelection, shouldClearDownloadSelectionOnEscape } from "./selection";
|
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 type { AccountModeFilter } from "./account-ui";
|
||||||
import { buildAccountDeleteCommand, buildAccountReplaceCommand, createAccountEditState, validateAccountEdit } from "./account-edit";
|
import { buildAccountDeleteCommand, buildAccountReplaceCommand, createAccountEditState, validateAccountEdit } from "./account-edit";
|
||||||
import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit";
|
import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit";
|
||||||
@@ -2319,8 +2319,8 @@ export function App(): ReactElement {
|
|||||||
accountId: acc.accountId,
|
accountId: acc.accountId,
|
||||||
checkable: true,
|
checkable: true,
|
||||||
disabled: entry.disabled || (entry.kind === "megadebrid-api"
|
disabled: entry.disabled || (entry.kind === "megadebrid-api"
|
||||||
? settingsDraft.megaDebridApiDisabledAccountIds.includes(acc.accountId)
|
? !settingsDraft.megaDebridApiEnabled || settingsDraft.megaDebridApiDisabledAccountIds.includes(acc.accountId)
|
||||||
: settingsDraft.megaDebridWebDisabledAccountIds.includes(acc.accountId)),
|
: !settingsDraft.megaDebridWebEnabled || settingsDraft.megaDebridWebDisabledAccountIds.includes(acc.accountId)),
|
||||||
dailyUsedBytes: used,
|
dailyUsedBytes: used,
|
||||||
dailyLimitBytes: limit,
|
dailyLimitBytes: limit,
|
||||||
dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0,
|
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 () => {
|
await performQuickAction(async () => {
|
||||||
const currentDisabledIds = settingsDraft.debridLinkDisabledKeyIds || [];
|
const nextState = buildScopedAccountEnabledState(
|
||||||
const currentlyDisabled = currentDisabledIds.includes(key.id);
|
settingsDraft.disabledProviders || [],
|
||||||
const nextDisabledIds = currentlyDisabled
|
["debridlink"],
|
||||||
? currentDisabledIds.filter((existingId) => existingId !== key.id)
|
settingsDraft.debridLinkDisabledKeyIds || [],
|
||||||
: [...currentDisabledIds, key.id];
|
key.id,
|
||||||
|
enabled
|
||||||
|
);
|
||||||
const nextDraft: RendererSettingsDraft = {
|
const nextDraft: RendererSettingsDraft = {
|
||||||
...settingsDraft,
|
...settingsDraft,
|
||||||
debridLinkDisabledKeyIds: nextDisabledIds
|
disabledProviders: nextState.disabledProviders,
|
||||||
|
debridLinkDisabledKeyIds: nextState.disabledAccountIds
|
||||||
};
|
};
|
||||||
await persistAccountToggle(nextDraft);
|
await persistAccountToggle(nextDraft);
|
||||||
showToast(
|
showToast(
|
||||||
currentlyDisabled
|
enabled
|
||||||
? `${entry.serviceLabel} ${key.label} aktiviert`
|
? `${entry.serviceLabel} ${key.label} aktiviert`
|
||||||
: `${entry.serviceLabel} ${key.label} deaktiviert`,
|
: `${entry.serviceLabel} ${key.label} deaktiviert`,
|
||||||
2200
|
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 () => {
|
await performQuickAction(async () => {
|
||||||
const mode = kind === "megadebrid-web" ? "web" : "api";
|
const mode = kind === "megadebrid-web" ? "web" : "api";
|
||||||
const current = mode === "api" ? settingsDraft.megaDebridApiDisabledAccountIds : settingsDraft.megaDebridWebDisabledAccountIds;
|
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 apiDisabledIds = mode === "api" ? next : settingsDraft.megaDebridApiDisabledAccountIds;
|
||||||
const webDisabledIds = mode === "web" ? next : settingsDraft.megaDebridWebDisabledAccountIds;
|
const webDisabledIds = mode === "web" ? next : settingsDraft.megaDebridWebDisabledAccountIds;
|
||||||
await persistAccountToggle({
|
await persistAccountToggle({
|
||||||
...settingsDraft,
|
...settingsDraft,
|
||||||
|
disabledProviders: nextState.disabledProviders,
|
||||||
|
megaDebridApiEnabled: mode === "api" && enabled ? true : settingsDraft.megaDebridApiEnabled,
|
||||||
|
megaDebridWebEnabled: mode === "web" && enabled ? true : settingsDraft.megaDebridWebEnabled,
|
||||||
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||||
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||||
megaDebridWebDisabledAccountIds: webDisabledIds
|
megaDebridWebDisabledAccountIds: webDisabledIds
|
||||||
});
|
});
|
||||||
showToast(currentlyDisabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
showToast(enabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
||||||
});
|
});
|
||||||
@@ -2913,7 +2926,7 @@ export function App(): ReactElement {
|
|||||||
if (row.toggleKind === "mega" && row.accountId) {
|
if (row.toggleKind === "mega" && row.accountId) {
|
||||||
void onToggleMegaAccountEnabled(row.entry.kind as "megadebrid-api" | "megadebrid-web", row.accountId, row.disabled);
|
void onToggleMegaAccountEnabled(row.entry.kind as "megadebrid-api" | "megadebrid-web", row.accountId, row.disabled);
|
||||||
} else if (row.toggleKind === "dl" && row.dlKey) {
|
} else if (row.toggleKind === "dl" && row.dlKey) {
|
||||||
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey);
|
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey, row.disabled);
|
||||||
} else {
|
} else {
|
||||||
void onToggleAccountEnabled(row.entry);
|
void onToggleAccountEnabled(row.entry);
|
||||||
}
|
}
|
||||||
@@ -6175,7 +6188,7 @@ export function App(): ReactElement {
|
|||||||
<span className="col-action"></span>
|
<span className="col-action"></span>
|
||||||
</div>
|
</div>
|
||||||
{entry.debridLinkKeys.map((key, ki) => (
|
{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 hostInfo = debridLinkHostLimits[key.id];
|
||||||
const statusDisplay = getDebridLinkKeyStatusDisplay(key, hostInfo);
|
const statusDisplay = getDebridLinkKeyStatusDisplay(key, hostInfo);
|
||||||
@@ -6196,17 +6209,17 @@ export function App(): ReactElement {
|
|||||||
{key.masked}
|
{key.masked}
|
||||||
</button>
|
</button>
|
||||||
<span className="col-usage">{humanSize(key.dailyUsedBytes)}</span>
|
<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-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-traffic" title={hostInfo?.note || ""}>{formatDebridLinkTraffic(hostInfo)}</span>
|
||||||
<span className="col-links" title={hostInfo?.note || ""}>{formatDebridLinkCountQuota(hostInfo)}</span>
|
<span className="col-links" title={hostInfo?.note || ""}>{formatDebridLinkCountQuota(hostInfo)}</span>
|
||||||
<span className="col-action">
|
<span className="col-action">
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm ${key.disabled ? "success" : "danger"}`}
|
className={`btn btn-sm ${entry.disabled || key.disabled ? "success" : "danger"}`}
|
||||||
disabled={actionBusy}
|
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>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm"
|
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(
|
export function buildBulkAccountEnabledState(
|
||||||
currentDisabledProviders: DebridProvider[],
|
currentDisabledProviders: DebridProvider[],
|
||||||
configuredProviders: DebridProvider[],
|
configuredProviders: DebridProvider[],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { createRendererSettings, createRendererState } from "../src/main/rendere
|
|||||||
import { buildAccountAddFields, createAccountDialogState } from "../src/renderer/App";
|
import { buildAccountAddFields, createAccountDialogState } from "../src/renderer/App";
|
||||||
import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit";
|
import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit";
|
||||||
import {
|
import {
|
||||||
|
buildScopedAccountEnabledState,
|
||||||
buildBulkAccountEnabledState,
|
buildBulkAccountEnabledState,
|
||||||
buildConfiguredProviderOrder,
|
buildConfiguredProviderOrder,
|
||||||
resolveAccountStatusState,
|
resolveAccountStatusState,
|
||||||
@@ -958,6 +959,31 @@ describe("account workspace", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("settings App integration", () => {
|
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", () => {
|
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: false, isPremium: false })).toBe("invalid");
|
||||||
expect(resolveAccountStatusState(true, { valid: true, isPremium: true })).toBe("disabled");
|
expect(resolveAccountStatusState(true, { valid: true, isPremium: true })).toBe("disabled");
|
||||||
|
|||||||
Reference in New Issue
Block a user