feat: add compact account management actions
This commit is contained in:
@@ -10,6 +10,12 @@ Desktop downloader for Windows with package-based queue management, multi-provid
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2.0.8
|
||||
|
||||
- Add a compact account-management table with traffic, status, expiry, username, and credential columns.
|
||||
- Add account actions through right-click and the row action menu.
|
||||
- Support quick edit, status checks, enable/disable, and removal without expanding the full account form.
|
||||
|
||||
### 2.0.7
|
||||
|
||||
- Improve account filter readability in dark and light themes.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "0.6.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.8",
|
||||
"description": "Desktop downloader",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
+183
-33
@@ -167,6 +167,31 @@ interface ConfiguredAccountEntry {
|
||||
debridLinkKeys: DebridLinkAccountKeyEntry[];
|
||||
}
|
||||
|
||||
interface AccountTableRow {
|
||||
rowKey: string;
|
||||
entry: ConfiguredAccountEntry;
|
||||
hosterLabel: string;
|
||||
modeLabel: string;
|
||||
username: string;
|
||||
credentialLabel: string;
|
||||
accountId: string | null;
|
||||
checkable: boolean;
|
||||
disabled: boolean;
|
||||
dailyUsedBytes: number;
|
||||
dailyLimitBytes: number;
|
||||
dailyRemainingBytes: number;
|
||||
totalUsedBytes: number;
|
||||
toggleKind: "mega" | "dl" | "single";
|
||||
megaLogin?: string;
|
||||
dlKey?: DebridLinkAccountKeyEntry;
|
||||
}
|
||||
|
||||
interface AccountContextMenuState {
|
||||
x: number;
|
||||
y: number;
|
||||
row: AccountTableRow;
|
||||
}
|
||||
|
||||
function buildDebugSetupDetails(setup: DebugSetupCheckResult): string {
|
||||
const formatDiskLine = (label: string, value: DebugSetupCheckResult["diskSpace"]["runtime"]): string => {
|
||||
if (value.freeBytes === null || value.totalBytes === null) {
|
||||
@@ -446,6 +471,28 @@ function getAccountPickerFunctionLabel(option: AccountOption): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getAccountCredentialLabel(kind: AccountKind): string {
|
||||
switch (kind) {
|
||||
case "megadebrid-api":
|
||||
case "megadebrid-web":
|
||||
case "ddownload-login":
|
||||
case "linksnappy-login":
|
||||
return "••••••";
|
||||
case "bestdebrid-web":
|
||||
case "realdebrid-web":
|
||||
case "alldebrid-web":
|
||||
return "Login gespeichert";
|
||||
case "realdebrid-api":
|
||||
case "bestdebrid-api":
|
||||
case "alldebrid-api":
|
||||
case "onefichier-api":
|
||||
case "debridlink-api":
|
||||
return "API-Key gespeichert";
|
||||
default:
|
||||
return "Zugang gespeichert";
|
||||
}
|
||||
}
|
||||
|
||||
function hasMegaDebridCredentials(settings: AppSettings): boolean {
|
||||
if (parseMegaDebridAccounts(settings.megaCredentials || "").length > 0) return true;
|
||||
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
|
||||
@@ -1781,6 +1828,8 @@ export function App(): ReactElement {
|
||||
const importQueueFocusHandlerRef = useRef<(() => void) | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||
const ctxMenuRef = useRef<HTMLDivElement>(null);
|
||||
const [accountContextMenu, setAccountContextMenu] = useState<AccountContextMenuState | null>(null);
|
||||
const accountContextMenuRef = useRef<HTMLDivElement>(null);
|
||||
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
|
||||
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
|
||||
const [accountDialogSearch, setAccountDialogSearch] = useState("");
|
||||
@@ -2552,24 +2601,7 @@ export function App(): ReactElement {
|
||||
const configuredAccountServices = useMemo(() => new Set(configuredAccounts.map((entry) => entry.service)), [configuredAccounts]);
|
||||
|
||||
const accountRows = useMemo(() => {
|
||||
type AccountRow = {
|
||||
rowKey: string;
|
||||
entry: ConfiguredAccountEntry;
|
||||
hosterLabel: string;
|
||||
modeLabel: string;
|
||||
username: string;
|
||||
accountId: string | null;
|
||||
checkable: boolean;
|
||||
disabled: boolean;
|
||||
dailyUsedBytes: number;
|
||||
dailyLimitBytes: number;
|
||||
dailyRemainingBytes: number;
|
||||
totalUsedBytes: number;
|
||||
toggleKind: "mega" | "dl" | "single";
|
||||
megaLogin?: string;
|
||||
dlKey?: DebridLinkAccountKeyEntry;
|
||||
};
|
||||
const rows: AccountRow[] = [];
|
||||
const rows: AccountTableRow[] = [];
|
||||
for (const entry of configuredAccounts) {
|
||||
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
||||
const accounts = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "");
|
||||
@@ -2582,6 +2614,7 @@ export function App(): ReactElement {
|
||||
hosterLabel: entry.serviceLabel,
|
||||
modeLabel: entry.modeLabel,
|
||||
username: acc.maskedLogin,
|
||||
credentialLabel: "••••••",
|
||||
accountId: acc.id,
|
||||
checkable: true,
|
||||
disabled: (settingsDraft.megaDebridDisabledAccountIds || []).includes(acc.id),
|
||||
@@ -2601,6 +2634,7 @@ export function App(): ReactElement {
|
||||
hosterLabel: entry.serviceLabel,
|
||||
modeLabel: entry.modeLabel,
|
||||
username: key.masked,
|
||||
credentialLabel: "API-Key",
|
||||
accountId: key.id,
|
||||
checkable: true,
|
||||
disabled: key.disabled,
|
||||
@@ -2619,6 +2653,7 @@ export function App(): ReactElement {
|
||||
hosterLabel: entry.serviceLabel,
|
||||
modeLabel: entry.modeLabel,
|
||||
username: entry.summary,
|
||||
credentialLabel: getAccountCredentialLabel(entry.kind),
|
||||
accountId: null,
|
||||
checkable: false,
|
||||
disabled: entry.disabled,
|
||||
@@ -2687,13 +2722,16 @@ export function App(): ReactElement {
|
||||
});
|
||||
};
|
||||
|
||||
const renderAccountRow = (row: (typeof accountRows)[number], isMember: boolean): ReactElement => {
|
||||
const renderAccountRow = (row: AccountTableRow, isMember: boolean): ReactElement => {
|
||||
const st = row.accountId ? (snapshot.settings?.debridAccountStatuses?.[row.accountId] ?? null) : null;
|
||||
const checking = row.accountId ? megaCheckingIds.has(row.accountId) : false;
|
||||
let statusCls = "none";
|
||||
let statusText = "—";
|
||||
if (row.disabled) { statusCls = "disabled"; statusText = "Deaktiviert"; }
|
||||
else if (!row.checkable) { statusCls = "none"; statusText = "—"; }
|
||||
else if (!row.checkable) {
|
||||
statusCls = row.entry.statusLabel === "Aktiviert" ? "unknown" : "ok";
|
||||
statusText = row.entry.statusLabel === "Aktiviert" ? "Aktiv" : row.entry.statusLabel;
|
||||
}
|
||||
else if (checking) { statusCls = "unknown"; statusText = "Prüfe…"; }
|
||||
else if (!st) { statusCls = "unknown"; statusText = "Noch nicht geprüft"; }
|
||||
else if (!st.valid) { statusCls = "invalid"; statusText = st.message || "Login ungültig"; }
|
||||
@@ -2707,7 +2745,16 @@ export function App(): ReactElement {
|
||||
? `${humanSize(row.dailyRemainingBytes)} von ${humanSize(row.dailyLimitBytes)} übrig`
|
||||
: "Unbeschränkt";
|
||||
return (
|
||||
<div key={row.rowKey} className={`acct2-row${row.disabled ? " acct2-disabled" : ""}${isProblem ? " acct2-problem" : ""}${isMember ? " acct2-member" : ""}`}>
|
||||
<div
|
||||
key={row.rowKey}
|
||||
className={`acct2-row${row.disabled ? " acct2-disabled" : ""}${isProblem ? " acct2-problem" : ""}${isMember ? " acct2-member" : ""}`}
|
||||
onDoubleClick={() => openEditAccountDialog(row.entry.kind)}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setAccountContextMenu({ x: event.clientX, y: event.clientY, row });
|
||||
}}
|
||||
>
|
||||
<span className="acct2-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -2733,18 +2780,21 @@ export function App(): ReactElement {
|
||||
</span>
|
||||
<span className="acct2-user" title={usernameTitle}>{username}</span>
|
||||
<span className="acct2-expiry">{expiry}</span>
|
||||
<span className="acct2-credential" title="Zugangsdaten werden niemals im Klartext angezeigt">{row.credentialLabel}</span>
|
||||
<span className="acct2-c-actions">
|
||||
{row.toggleKind === "single" && getAccountQuickActionMeta(row.entry.kind) && (
|
||||
<button className="btn btn-sm" disabled={actionBusy} onClick={() => { void onAccountRowQuickAction(row.entry); }}>
|
||||
{getAccountQuickActionMeta(row.entry.kind)?.label}
|
||||
<button
|
||||
className="btn btn-sm acct2-menu-button"
|
||||
disabled={actionBusy}
|
||||
title="Account-Aktionen"
|
||||
aria-label={`${row.hosterLabel} Account-Aktionen`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
setAccountContextMenu({ x: rect.right, y: rect.bottom, row });
|
||||
}}
|
||||
>
|
||||
⋯
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-sm" disabled={actionBusy} title="Account bearbeiten" onClick={() => openEditAccountDialog(row.entry.kind)}>Bearbeiten</button>
|
||||
<button className="btn btn-sm danger" disabled={actionBusy} title="Account entfernen" onClick={() => {
|
||||
if (row.toggleKind === "mega" && row.megaLogin) { void onRemoveMegaAccount(row.megaLogin); }
|
||||
else if (row.toggleKind === "dl" && row.dlKey) { void onRemoveDebridLinkKey(row.dlKey); }
|
||||
else { void onRemoveAccount(row.entry); }
|
||||
}}>Entfernen</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -3216,6 +3266,49 @@ export function App(): ReactElement {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleAccountTableRow = (row: AccountTableRow): void => {
|
||||
setAccountContextMenu(null);
|
||||
if (row.toggleKind === "mega" && row.megaLogin) {
|
||||
void onToggleMegaAccountEnabled(row.megaLogin, row.disabled);
|
||||
} else if (row.toggleKind === "dl" && row.dlKey) {
|
||||
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey);
|
||||
} else {
|
||||
void onToggleAccountEnabled(row.entry);
|
||||
}
|
||||
};
|
||||
|
||||
const removeAccountTableRow = (row: AccountTableRow): void => {
|
||||
setAccountContextMenu(null);
|
||||
if (row.toggleKind === "mega" && row.megaLogin) {
|
||||
void onRemoveMegaAccount(row.megaLogin);
|
||||
} else if (row.toggleKind === "dl" && row.dlKey) {
|
||||
void onRemoveDebridLinkKey(row.dlKey);
|
||||
} else {
|
||||
void onRemoveAccount(row.entry);
|
||||
}
|
||||
};
|
||||
|
||||
const checkAccountTableRow = (row: AccountTableRow): void => {
|
||||
setAccountContextMenu(null);
|
||||
if (row.toggleKind === "mega" && row.megaLogin) {
|
||||
const account = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "")
|
||||
.find((entry) => entry.id === row.accountId);
|
||||
if (account) {
|
||||
void runMegaAccountCheck(account.login, account.password);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (row.toggleKind === "dl") {
|
||||
void checkAllAccounts();
|
||||
return;
|
||||
}
|
||||
if (getAccountQuickActionMeta(row.entry.kind)) {
|
||||
void onAccountRowQuickAction(row.entry);
|
||||
return;
|
||||
}
|
||||
showToast("Für diesen Account ist keine direkte Statusprüfung verfügbar.", 2800);
|
||||
};
|
||||
|
||||
const onCheckUpdates = async (): Promise<void> => {
|
||||
let updateResult: UpdateCheckResult | null = null;
|
||||
await performQuickAction(async () => {
|
||||
@@ -4136,6 +4229,17 @@ export function App(): ReactElement {
|
||||
};
|
||||
}, [contextMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountContextMenu) { return; }
|
||||
const close = (): void => setAccountContextMenu(null);
|
||||
window.addEventListener("click", close);
|
||||
window.addEventListener("contextmenu", close);
|
||||
return () => {
|
||||
window.removeEventListener("click", close);
|
||||
window.removeEventListener("contextmenu", close);
|
||||
};
|
||||
}, [accountContextMenu]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!contextMenu || !ctxMenuRef.current) return;
|
||||
const el = ctxMenuRef.current;
|
||||
@@ -4148,6 +4252,18 @@ export function App(): ReactElement {
|
||||
}
|
||||
}, [contextMenu]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!accountContextMenu || !accountContextMenuRef.current) return;
|
||||
const el = accountContextMenuRef.current;
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.bottom > window.innerHeight) {
|
||||
el.style.top = `${Math.max(0, accountContextMenu.y - rect.height)}px`;
|
||||
}
|
||||
if (rect.right > window.innerWidth) {
|
||||
el.style.left = `${Math.max(0, accountContextMenu.x - rect.width)}px`;
|
||||
}
|
||||
}, [accountContextMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!colHeaderCtx) return;
|
||||
const close = (e: MouseEvent): void => {
|
||||
@@ -5562,8 +5678,8 @@ export function App(): ReactElement {
|
||||
<div className="settings-section card account-board">
|
||||
<div className="account-board-header">
|
||||
<div>
|
||||
<h3>Accounts</h3>
|
||||
<div className="hint">Accounts werden als Liste verwaltet. Neue Einträge kommen über den Dialog oben rechts dazu.</div>
|
||||
<h3>Accountverwaltung</h3>
|
||||
<div className="hint">Kompakte Übersicht mit Hoster, Traffic, Status, Benutzername, Ablauf und Zugang. Rechtsklick oder Doppelklick öffnet die Aktionen.</div>
|
||||
</div>
|
||||
<div className="account-board-header-actions">
|
||||
<button className="btn" disabled={actionBusy || accountCheckBusy} onClick={() => { void checkAllAccounts(); }} title="Prüft Login-Gültigkeit und Premium-Restlaufzeit aller Mega-Debrid-/Debrid-Link-Accounts">
|
||||
@@ -5602,6 +5718,7 @@ export function App(): ReactElement {
|
||||
<span className="acct2-sortable" title="Nach Premium-Restlaufzeit sortieren (längste/kürzeste)" onClick={cycleAccountStatusSort}>Status{accountStatusSort === "desc" ? " ▼" : accountStatusSort === "asc" ? " ▲" : ""}</span>
|
||||
<span>Benutzername</span>
|
||||
<span>Verfallsdatum</span>
|
||||
<span>Zugang</span>
|
||||
<span className="acct2-c-actions">Aktion</span>
|
||||
</div>
|
||||
{groupedAccountRows.flatMap((group) => {
|
||||
@@ -5633,6 +5750,7 @@ export function App(): ReactElement {
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
];
|
||||
if (!collapsed) {
|
||||
@@ -6702,6 +6820,38 @@ export function App(): ReactElement {
|
||||
)}
|
||||
{statusToast && <div className="toast">{statusToast}</div>}
|
||||
{dragOver && <div className="drop-overlay">Links, .dlc oder Export-Dateien hier ablegen</div>}
|
||||
{accountContextMenu && (
|
||||
<div
|
||||
ref={accountContextMenuRef}
|
||||
className="ctx-menu account-context-menu"
|
||||
style={{ left: accountContextMenu.x, top: accountContextMenu.y }}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="account-context-heading">
|
||||
<strong>{accountContextMenu.row.hosterLabel}</strong>
|
||||
<span>{accountContextMenu.row.modeLabel} · {accountContextMenu.row.username}</span>
|
||||
</div>
|
||||
<div className="ctx-menu-sep" />
|
||||
<button className="ctx-menu-item" onClick={() => { const row = accountContextMenu.row; setAccountContextMenu(null); openEditAccountDialog(row.entry.kind); }}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="ctx-menu-item" onClick={() => checkAccountTableRow(accountContextMenu.row)}>
|
||||
Account prüfen
|
||||
</button>
|
||||
{getAccountQuickActionMeta(accountContextMenu.row.entry.kind) && (
|
||||
<button className="ctx-menu-item" onClick={() => { const row = accountContextMenu.row; setAccountContextMenu(null); void onAccountRowQuickAction(row.entry); }}>
|
||||
{getAccountQuickActionMeta(accountContextMenu.row.entry.kind)?.label}
|
||||
</button>
|
||||
)}
|
||||
<button className="ctx-menu-item" onClick={() => toggleAccountTableRow(accountContextMenu.row)}>
|
||||
{accountContextMenu.row.disabled ? "Account aktivieren" : "Account deaktivieren"}
|
||||
</button>
|
||||
<div className="ctx-menu-sep" />
|
||||
<button className="ctx-menu-item ctx-danger" onClick={() => removeAccountTableRow(accountContextMenu.row)}>
|
||||
Account entfernen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{contextMenu && (() => {
|
||||
const multi = selectedIds.size > 1;
|
||||
const selectedPackageIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
|
||||
|
||||
@@ -3370,7 +3370,7 @@ td {
|
||||
.acct2-head,
|
||||
.acct2-row {
|
||||
display: grid;
|
||||
grid-template-columns: 32px minmax(0, 1.4fr) minmax(0, 1.4fr) minmax(0, 1.3fr) minmax(0, 1.7fr) minmax(0, 1fr) 196px;
|
||||
grid-template-columns: 32px minmax(0, 1.35fr) minmax(0, 1.25fr) minmax(0, 1.3fr) minmax(0, 1.45fr) minmax(0, 1fr) minmax(0, 0.9fr) 56px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 12px;
|
||||
@@ -3412,6 +3412,7 @@ td {
|
||||
.acct2-c-check { display: flex; justify-content: center; }
|
||||
.acct2-c-check input { cursor: pointer; }
|
||||
.acct2-c-actions { display: flex; gap: 6px; justify-content: flex-end; }
|
||||
.acct2-menu-button { min-width: 38px; padding-inline: 8px; font-size: 18px; line-height: 1; }
|
||||
.acct2-hoster { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
||||
.acct2-hoster strong { font-size: 13px; }
|
||||
.acct2-mode { font-size: 11px; color: var(--muted); }
|
||||
@@ -3422,9 +3423,15 @@ td {
|
||||
.acct2-sortable { cursor: pointer; user-select: none; }
|
||||
.acct2-sortable:hover { color: var(--text); }
|
||||
.acct2-user { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.acct2-credential { color: var(--muted); white-space: nowrap; }
|
||||
.acct2-status .account-validity-badge { margin-top: 0; }
|
||||
.acct2-nostatus { color: var(--muted); opacity: 0.6; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.account-context-menu { min-width: 230px; }
|
||||
.account-context-heading { display: grid; gap: 3px; padding: 8px 12px; }
|
||||
.account-context-heading strong { color: var(--text); font-size: 13px; }
|
||||
.account-context-heading span { color: var(--muted); font-size: 11px; }
|
||||
|
||||
.rotation-panel { display: flex; flex-direction: column; gap: 6px; max-height: 320px; overflow-y: auto; }
|
||||
.rotation-empty { color: var(--muted, #a59c8e); font-size: 12px; }
|
||||
.rotation-event {
|
||||
|
||||
Reference in New Issue
Block a user