release: publish v2.0.32 account controls

Prevent account enable checkboxes from opening the account editor during rapid interactions.

Remove the redundant global account toggle and keep every configured account independently controlled.
This commit is contained in:
Sucukdeluxe
2026-08-13 17:22:52 +02:00
parent 25ebc55f4f
commit d287056a2a
9 changed files with 56 additions and 119 deletions
+7
View File
@@ -2,6 +2,13 @@
All notable changes to Multi-Debrid Downloader are documented in this file.
## [2.0.32] - 2026-08-13
### Account controls
- Prevented rapid account enable and disable interactions from opening the account editor.
- Removed the redundant global account toggle so every configured account remains independently controlled.
## [2.0.31] - 2026-08-13
### Account rotation and lifecycle
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "real-debrid-downloader",
"version": "2.0.31",
"version": "2.0.32",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "real-debrid-downloader",
"version": "2.0.31",
"version": "2.0.32",
"license": "MIT",
"dependencies": {
"adm-zip": "0.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "real-debrid-downloader",
"version": "2.0.31",
"version": "2.0.32",
"description": "Desktop downloader",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",
+1 -31
View File
@@ -45,7 +45,7 @@ import {
sortPackageOrderByName
} from "./package-order";
import { pruneSelection, shouldClearDownloadSelection, shouldClearDownloadSelectionOnEscape } from "./selection";
import { buildBulkAccountEnabledState, buildConfiguredProviderOrder, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelection, resolveAccountUsername, resolveVisibleAccountKind } from "./account-ui";
import { buildConfiguredProviderOrder, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelection, resolveAccountUsername, resolveVisibleAccountKind } 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";
@@ -3038,34 +3038,6 @@ export function App(): ReactElement {
}
};
const setAllAccountsEnabled = (enabled: boolean): void => {
const configuredProviderIds = [...new Set(configuredAccounts.map((entry) => entry.service as DebridProvider))];
const nextEnabledState = buildBulkAccountEnabledState(
settingsDraftRef.current.disabledProviders || [],
configuredProviderIds,
accountRows.filter((row) => row.toggleKind === "mega" && row.accountId).map((row) => row.accountId as string),
accountRows.filter((row) => row.toggleKind === "dl" && row.accountId).map((row) => row.accountId as string),
enabled
);
const nextDraft: RendererSettingsDraft = {
...settingsDraftRef.current,
...nextEnabledState,
megaDebridApiDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-api" && row.accountId).map((row) => row.accountId as string),
megaDebridWebDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-web" && row.accountId).map((row) => row.accountId as string)
};
nextDraft.megaDebridDisabledAccountIds = [...new Set([
...nextDraft.megaDebridApiDisabledAccountIds,
...nextDraft.megaDebridWebDisabledAccountIds
])];
const nextOverrides = Object.fromEntries(accountRows.map((row) => [row.rowKey, enabled]));
enqueueAccountSettingsChange(
nextDraft,
nextOverrides,
enabled ? "Accounts aktiviert" : "Accounts deaktiviert",
"Accounts konnten nicht umgeschaltet werden"
);
};
const removeAccountTableRow = (row: AccountTableRow): void => {
setAccountContextMenu(null);
void (async () => {
@@ -5068,7 +5040,6 @@ export function App(): ReactElement {
rows: visibleAccountRows,
selectedIds: selectedAccountViewId ? [selectedAccountViewId] : [],
busy: actionBusy || accountCheckBusy,
allEnabled: accountRows.length > 0 && accountRows.some((row) => !row.disabled),
statusSort: accountStatusSort,
rules: {
providerOrder: activeProviderOrder.map((provider) => providerLabelWithMode(provider, settingsDraft)),
@@ -5121,7 +5092,6 @@ export function App(): ReactElement {
if (row) removeAccountTableRow(row);
},
onCheckAll: () => { void checkAllAccounts(); },
onSetAllEnabled: (enabled) => { void setAllAccountsEnabled(enabled); },
onStatusSort: cycleAccountStatusSort,
onMoveProvider: (index, direction) => {
const target = index + direction;
-21
View File
@@ -58,24 +58,3 @@ export function isAccountRowSelectionKey(key: string, originatedOnRow: boolean):
export function resolveAccountUsername(storedUsername: string, checkedEmail?: string): string {
return checkedEmail?.trim() || storedUsername.trim() || "—";
}
export function buildBulkAccountEnabledState(
currentDisabledProviders: DebridProvider[],
configuredProviders: DebridProvider[],
megaAccountIds: string[],
debridLinkKeyIds: string[],
enabled: boolean
): {
disabledProviders: DebridProvider[];
megaDebridDisabledAccountIds: string[];
debridLinkDisabledKeyIds: string[];
} {
const configured = new Set(configuredProviders);
return {
disabledProviders: enabled
? currentDisabledProviders.filter((provider) => !configured.has(provider))
: [...new Set([...currentDisabledProviders, ...configuredProviders])],
megaDebridDisabledAccountIds: enabled ? [] : [...new Set(megaAccountIds)],
debridLinkDisabledKeyIds: enabled ? [] : [...new Set(debridLinkKeyIds)]
};
}
+1 -1
View File
@@ -29,7 +29,7 @@ const pairs = [
["Hoch", "High"], ["Normal", "Normal"], ["Niedrig", "Low"], ["In Warteschlange", "Queued"], ["Abgeschlossen", "Completed"], ["Entpackt", "Extracted"], ["Automatisch entpacken", "Extract automatically"],
["Liste leeren", "Clear list"], ["Sitzung", "Session"], ["Gesamt", "Total"], ["Bereit", "Ready"], ["Download läuft", "Download running"], ["Wartet", "Waiting"], ["Offline", "Offline"],
["Übersicht", "Overview"], ["Verwendungsregeln", "Usage rules"], ["Accountverwaltung", "Account management"], ["Accounts hinzufügen, prüfen und verwalten.", "Add, check and manage accounts."],
["Accounts zum Herunterladen verwenden", "Use accounts for downloads"], ["Download-Traffic übrig", "Download traffic remaining"], ["Benutzername", "Username"], ["E-Mail", "Email"], ["Verfallsdatum", "Expiration date"], ["Passwort/Zugang", "Password/access"],
["Download-Traffic übrig", "Download traffic remaining"], ["Benutzername", "Username"], ["E-Mail", "Email"], ["Verfallsdatum", "Expiration date"], ["Passwort/Zugang", "Password/access"],
["Account hinzufügen", "Add account"], ["Ausgewählte prüfen", "Check selected"], ["Ausgewählte entfernen", "Remove selected"], ["Aktivieren", "Enable"], ["Deaktivieren", "Disable"], ["Noch nicht geprüft", "Not checked yet"],
["Aktiviert", "Enabled"], ["Aktionen", "Actions"], ["Deaktiviert", "Disabled"], ["Premium aktiv", "Premium active"], ["API-Key aktiv", "API key active"], ["API-Account", "API account"], ["API-Key", "API key"],
["Ungültiger API-Key (nicht autorisiert)", "Invalid API key (not authorized)"], ["Free Account", "Free account"], ["Unbeschränkt", "Unlimited"], ["Keine Accounts eingerichtet", "No accounts configured"],
@@ -63,7 +63,6 @@ export interface AccountWorkspaceViewModel {
selectedIds: readonly string[];
busy: boolean;
error?: string;
allEnabled?: boolean;
statusSort?: "none" | "desc" | "asc";
rules: AccountRulesViewModel;
}
@@ -77,7 +76,6 @@ export interface AccountWorkspaceActions {
onAdd: () => void;
onRemoveSelected: () => void;
onCheckAll: () => void;
onSetAllEnabled?: (enabled: boolean) => void;
onStatusSort?: () => void;
onMoveProvider?: (index: number, direction: -1 | 1) => void;
onProviderDragStart?: (event: DragEvent<HTMLElement>, index: number) => void;
@@ -226,6 +224,7 @@ function AccountRow({
disabled={busy}
onChange={() => actions.onToggleEnabled(row.id)}
onClick={(event) => event.stopPropagation()}
onDoubleClick={(event) => event.stopPropagation()}
type="checkbox"
/>
</span>
@@ -440,17 +439,6 @@ export function AccountWorkspace({ model, actions }: AccountWorkspaceProps): Rea
<h2>Accountverwaltung</h2>
<p>Accounts hinzufügen, prüfen und verwalten.</p>
</div>
{typeof model.allEnabled === "boolean" && actions.onSetAllEnabled ? (
<label className="settings-rule-toggle settings-account-all-enabled">
<input
checked={model.allEnabled}
disabled={model.busy || model.rows.length === 0}
onChange={(event) => actions.onSetAllEnabled?.(event.target.checked)}
type="checkbox"
/>
<span>Accounts zum Herunterladen verwenden</span>
</label>
) : null}
</header>
<SlidingSelection activeKey={model.activePanel} aria-label="Accountverwaltung" aria-orientation="horizontal" axis="horizontal" className="settings-account-tabs" role="tablist">
{ACCOUNT_WORKSPACE_PANELS.map((panel, index) => (
+1 -30
View File
@@ -1,6 +1,5 @@
import { describe, expect, it } from "vitest";
import {
buildBulkAccountEnabledState,
buildConfiguredProviderOrder,
getAccountDialogSelectableOptions,
isAccountRowSelectionKey,
@@ -22,35 +21,7 @@ describe("account mode filter", () => {
});
});
describe("bulk account activation", () => {
it("disables each configured provider and account exactly once", () => {
expect(buildBulkAccountEnabledState(
["alldebrid"],
["megadebrid-api", "alldebrid"],
["mega-1", "mega-1", "mega-2"],
["debrid-link-1", "debrid-link-1"],
false
)).toEqual({
disabledProviders: ["alldebrid", "megadebrid-api"],
megaDebridDisabledAccountIds: ["mega-1", "mega-2"],
debridLinkDisabledKeyIds: ["debrid-link-1"]
});
});
it("enables configured providers without changing unrelated provider state", () => {
expect(buildBulkAccountEnabledState(
["realdebrid", "alldebrid", "megadebrid-api"],
["alldebrid", "megadebrid-api"],
["mega-1"],
["debrid-link-1"],
true
)).toEqual({
disabledProviders: ["realdebrid"],
megaDebridDisabledAccountIds: [],
debridLinkDisabledKeyIds: []
});
});
describe("account provider order", () => {
it("preserves the custom provider order while accounts are disabled", () => {
expect(buildConfiguredProviderOrder(
["debridlink", "realdebrid", "alldebrid"],
+38 -16
View File
@@ -6,10 +6,7 @@ import { defaultSettings } from "../src/main/constants";
import { createRendererSettings, createRendererState } from "../src/main/renderer-state";
import { buildAccountAddFields, createAccountDialogState } from "../src/renderer/App";
import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit";
import {
buildBulkAccountEnabledState,
buildConfiguredProviderOrder
} from "../src/renderer/account-ui";
import { buildConfiguredProviderOrder } from "../src/renderer/account-ui";
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
import {
@@ -391,22 +388,11 @@ describe("settings model", () => {
expect(buildTargetedAccountCheck(options[1], "ddownload-new")).toBeNull();
});
it("preserves provider order and deduplicates bulk account identities", () => {
it("preserves provider order", () => {
expect(buildConfiguredProviderOrder(
["debridlink", "realdebrid", "alldebrid"],
["realdebrid", "alldebrid", "debridlink", "bestdebrid"]
)).toEqual(["debridlink", "realdebrid", "alldebrid", "bestdebrid"]);
expect(buildBulkAccountEnabledState(
["alldebrid"],
["megadebrid-api", "alldebrid"],
["mega-1", "mega-1"],
["dl-1", "dl-1"],
false
)).toEqual({
disabledProviders: ["alldebrid", "megadebrid-api"],
megaDebridDisabledAccountIds: ["mega-1"],
debridLinkDisabledKeyIds: ["dl-1"]
});
});
it("keeps exact rounded limits and migrates edited identity metadata", () => {
@@ -740,6 +726,42 @@ describe("account workspace", () => {
]);
});
it("does not open account editing when the enable checkbox is double-clicked", () => {
const calls: string[] = [];
const tree = AccountWorkspace({
model: workspaceModel(),
actions: workspaceActions({
onToggleEnabled: (id) => calls.push(`toggle:${id}`),
onEdit: (id) => calls.push(`edit:${id}`)
})
});
const row = findElement(tree, (element) => element.props.role === "row" && element.props["aria-selected"] === true);
const checkbox = findElement(row, (element) => element.type === "input" && element.props.type === "checkbox");
const event = {
propagationStopped: false,
stopPropagation() {
this.propagationStopped = true;
}
};
checkbox.props.onChange();
checkbox.props.onDoubleClick(event);
if (!event.propagationStopped) {
row.props.onDoubleClick();
}
expect(calls).toEqual([`toggle:${workspaceModel().rows[0].id}`]);
});
it("does not expose a global switch that enables or disables every account", () => {
const legacyModel = { ...workspaceModel(), allEnabled: true } as AccountWorkspaceViewModel;
const legacyActions = { ...workspaceActions(), onSetAllEnabled: () => {} } as AccountWorkspaceActions;
const html = renderToStaticMarkup(<AccountWorkspace actions={legacyActions} model={legacyModel} />);
expect(html).not.toContain("Accounts zum Herunterladen verwenden");
expect(html).not.toContain("settings-account-all-enabled");
});
it("keeps overview and rules in the same workspace while only one panel is active", () => {
const html = renderToStaticMarkup(<AccountWorkspace actions={workspaceActions()} model={workspaceModel()} />);
expect(count(html, "class=\"settings-account-panel\"")).toBe(2);