feat: add per-provider account priority rules
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user