feat: add per-provider account priority rules

This commit is contained in:
Sucukdeluxe
2026-09-06 12:39:11 +02:00
parent 8957aa3c23
commit dd4a39e47d
22 changed files with 607 additions and 31 deletions
@@ -0,0 +1,49 @@
import type { ReactElement } from "react";
import type { AccountSelectionMode } from "../../../shared/account-usage-rules";
export interface AccountPriorityModel {
mode: AccountSelectionMode;
accounts: readonly { id: string; label: string; mode: string; status: string }[];
}
export function AccountPriorityControls({ provider, model, busy, onModeChange, onMove }: {
provider: string;
model: AccountPriorityModel;
busy: boolean;
onModeChange?: (provider: string, mode: AccountSelectionMode) => void;
onMove?: (provider: string, accountId: string, targetId: string) => void;
}): ReactElement {
const locale = typeof document !== "undefined" && document.documentElement.lang === "de" ? "de-DE" : "en-US";
return <details className="settings-account-priority" onDragStart={(event) => event.stopPropagation()} onDragOver={(event) => event.stopPropagation()} onDrop={(event) => event.stopPropagation()}>
<summary>Account-Reihenfolge</summary>
<fieldset disabled={busy} className="settings-account-priority-mode">
<legend>Account-Auswahl</legend>
{(["automatic", "priority"] as const).map((mode) => <label key={mode}>
<input type="radio" name={`account-selection-${provider}`} checked={model.mode === mode} disabled={!onModeChange} onChange={() => onModeChange?.(provider, mode)} />
{mode === "automatic" ? "Automatisch verteilen" : "Feste Reihenfolge"}
</label>)}
</fieldset>
<p>{model.mode === "priority" ? "Der erste nutzbare Account hat Vorrang. Gesperrte Accounts werden übersprungen; laufende Downloads bleiben unverändert." : "Die bisherige automatische Account-Verteilung bleibt aktiv. Die Reihenfolge gilt erst im Modus Feste Reihenfolge."}</p>
<ol aria-label="Account-Prioritäten">
{model.accounts.map((account, index) => <li key={account.id} data-priority-account={account.id} draggable={!busy && Boolean(onMove)}
onDragStart={(event) => { event.stopPropagation(); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("application/x-mdd-account-priority", JSON.stringify({ provider, id: account.id })); }}
onDragOver={(event) => { event.stopPropagation(); if (!busy && event.dataTransfer.types.includes("application/x-mdd-account-priority")) event.preventDefault(); }}
onDrop={(event) => {
event.stopPropagation();
if (busy) return;
event.preventDefault();
try {
const source = JSON.parse(event.dataTransfer.getData("application/x-mdd-account-priority"));
if (source.provider === provider && model.accounts.some((candidate) => candidate.id === source.id)) onMove?.(provider, source.id, account.id);
} catch {}
}}>
<span className="settings-account-priority-identity"><span aria-hidden="true">{(index + 1).toLocaleString(locale)}.</span> <span>{account.label}</span><small>{account.mode} · {account.status}</small></span>
<span className="settings-provider-order-actions">
<button type="button" disabled={busy || !onMove || index === 0} aria-label={`${account.label} nach oben`} onClick={() => onMove?.(provider, account.id, model.accounts[index - 1].id)}></button>
<button type="button" disabled={busy || !onMove || index === model.accounts.length - 1} aria-label={`${account.label} nach unten`} onClick={() => onMove?.(provider, account.id, model.accounts[index + 1].id)}></button>
</span>
</li>)}
</ol>
{model.accounts.length === 0 ? <p>Keine Accounts vorhanden.</p> : null}
</details>;
}
@@ -8,6 +8,8 @@ import {
type UIEvent
} from "react";
import { SlidingSelection } from "../../ui/SlidingSelection";
import { AccountPriorityControls, type AccountPriorityModel } from "./AccountPriorityControls";
import type { AccountSelectionMode } from "../../../shared/account-usage-rules";
import {
DataTable,
DataTableBody,
@@ -81,7 +83,7 @@ function getAccountPanelNavigationIndex(currentIndex: number, key: string): numb
}
export interface AccountRulesViewModel {
providerOrder: readonly { id: string; label: string; icon: string }[];
providerOrder: readonly { id: string; label: string; icon: string; accountSelection?: AccountPriorityModel }[];
routing: readonly string[];
autoFallback: boolean;
rememberCredentials?: boolean;
@@ -146,7 +148,9 @@ export interface AccountWorkspaceActions {
onCheckActive: () => void;
onCheckAll: () => void;
onStatusSort?: () => void;
onMoveProvider?: (index: number, direction: -1 | 1) => void;
onMoveProvider?: (index: number, direction: -1 | 1) => void;
onAccountSelectionMode?: (provider: string, mode: AccountSelectionMode) => void;
onMovePriorityAccount?: (provider: string, accountId: string, targetId: string) => void;
onProviderDragStart?: (event: DragEvent<HTMLElement>, index: number) => void;
onProviderDragOver?: (event: DragEvent<HTMLElement>, index: number) => void;
onProviderDrop?: (event: DragEvent<HTMLElement>, index: number) => void;
@@ -524,8 +528,9 @@ function AccountRules({ model, actions }: AccountWorkspaceProps): ReactElement {
<button aria-label={`${provider.label} nach oben`} disabled={index === 0} onClick={() => actions.onMoveProvider?.(index, -1)} type="button"></button>
<button aria-label={`${provider.label} nach unten`} disabled={index === model.rules.providerOrder.length - 1} onClick={() => actions.onMoveProvider?.(index, 1)} type="button"></button>
</span>
) : null}
</li>
) : null}
{provider.accountSelection ? <AccountPriorityControls provider={provider.id} model={provider.accountSelection} busy={model.busy} onModeChange={actions.onAccountSelectionMode} onMove={actions.onMovePriorityAccount} /> : null}
</li>
))}
</ol>
)}
+51
View File
@@ -1071,6 +1071,57 @@
gap: 4px;
}
.settings-provider-order > li {
flex-wrap: wrap;
}
.settings-account-priority {
flex-basis: 100%;
min-width: 0;
padding: 8px 12px;
border-top: 1px solid var(--ui-border);
font-size: 12px;
}
.settings-account-priority summary {
cursor: pointer;
padding: 4px 0;
color: var(--ui-text);
}
.settings-account-priority-mode {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 12px 0;
border: 0;
padding: 0;
}
.settings-account-priority-mode label {
display: inline-flex;
align-items: center;
gap: 6px;
}
.settings-account-priority ol {
display: grid;
gap: 6px;
list-style: none;
padding: 0;
}
.settings-account-priority-identity {
min-width: 0;
overflow-wrap: anywhere;
}
.settings-account-priority-identity small {
display: block;
margin-top: 3px;
font-size: 11px;
}
.settings-provider-order-provider {
display: inline-flex;
align-items: center;