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
|
## 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
|
### 2.0.7
|
||||||
|
|
||||||
- Improve account filter readability in dark and light themes.
|
- Improve account filter readability in dark and light themes.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.7",
|
"version": "2.0.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.7",
|
"version": "2.0.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "0.6.0",
|
"adm-zip": "0.6.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.7",
|
"version": "2.0.8",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
+193
-43
@@ -167,6 +167,31 @@ interface ConfiguredAccountEntry {
|
|||||||
debridLinkKeys: DebridLinkAccountKeyEntry[];
|
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 {
|
function buildDebugSetupDetails(setup: DebugSetupCheckResult): string {
|
||||||
const formatDiskLine = (label: string, value: DebugSetupCheckResult["diskSpace"]["runtime"]): string => {
|
const formatDiskLine = (label: string, value: DebugSetupCheckResult["diskSpace"]["runtime"]): string => {
|
||||||
if (value.freeBytes === null || value.totalBytes === null) {
|
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 {
|
function hasMegaDebridCredentials(settings: AppSettings): boolean {
|
||||||
if (parseMegaDebridAccounts(settings.megaCredentials || "").length > 0) return true;
|
if (parseMegaDebridAccounts(settings.megaCredentials || "").length > 0) return true;
|
||||||
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
|
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
|
||||||
@@ -1781,6 +1828,8 @@ export function App(): ReactElement {
|
|||||||
const importQueueFocusHandlerRef = useRef<(() => void) | null>(null);
|
const importQueueFocusHandlerRef = useRef<(() => void) | null>(null);
|
||||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||||
const ctxMenuRef = useRef<HTMLDivElement>(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 [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
|
||||||
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
|
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
|
||||||
const [accountDialogSearch, setAccountDialogSearch] = useState("");
|
const [accountDialogSearch, setAccountDialogSearch] = useState("");
|
||||||
@@ -2552,24 +2601,7 @@ export function App(): ReactElement {
|
|||||||
const configuredAccountServices = useMemo(() => new Set(configuredAccounts.map((entry) => entry.service)), [configuredAccounts]);
|
const configuredAccountServices = useMemo(() => new Set(configuredAccounts.map((entry) => entry.service)), [configuredAccounts]);
|
||||||
|
|
||||||
const accountRows = useMemo(() => {
|
const accountRows = useMemo(() => {
|
||||||
type AccountRow = {
|
const rows: AccountTableRow[] = [];
|
||||||
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[] = [];
|
|
||||||
for (const entry of configuredAccounts) {
|
for (const entry of configuredAccounts) {
|
||||||
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
||||||
const accounts = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "");
|
const accounts = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "");
|
||||||
@@ -2582,6 +2614,7 @@ export function App(): ReactElement {
|
|||||||
hosterLabel: entry.serviceLabel,
|
hosterLabel: entry.serviceLabel,
|
||||||
modeLabel: entry.modeLabel,
|
modeLabel: entry.modeLabel,
|
||||||
username: acc.maskedLogin,
|
username: acc.maskedLogin,
|
||||||
|
credentialLabel: "••••••",
|
||||||
accountId: acc.id,
|
accountId: acc.id,
|
||||||
checkable: true,
|
checkable: true,
|
||||||
disabled: (settingsDraft.megaDebridDisabledAccountIds || []).includes(acc.id),
|
disabled: (settingsDraft.megaDebridDisabledAccountIds || []).includes(acc.id),
|
||||||
@@ -2601,6 +2634,7 @@ export function App(): ReactElement {
|
|||||||
hosterLabel: entry.serviceLabel,
|
hosterLabel: entry.serviceLabel,
|
||||||
modeLabel: entry.modeLabel,
|
modeLabel: entry.modeLabel,
|
||||||
username: key.masked,
|
username: key.masked,
|
||||||
|
credentialLabel: "API-Key",
|
||||||
accountId: key.id,
|
accountId: key.id,
|
||||||
checkable: true,
|
checkable: true,
|
||||||
disabled: key.disabled,
|
disabled: key.disabled,
|
||||||
@@ -2619,6 +2653,7 @@ export function App(): ReactElement {
|
|||||||
hosterLabel: entry.serviceLabel,
|
hosterLabel: entry.serviceLabel,
|
||||||
modeLabel: entry.modeLabel,
|
modeLabel: entry.modeLabel,
|
||||||
username: entry.summary,
|
username: entry.summary,
|
||||||
|
credentialLabel: getAccountCredentialLabel(entry.kind),
|
||||||
accountId: null,
|
accountId: null,
|
||||||
checkable: false,
|
checkable: false,
|
||||||
disabled: entry.disabled,
|
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 st = row.accountId ? (snapshot.settings?.debridAccountStatuses?.[row.accountId] ?? null) : null;
|
||||||
const checking = row.accountId ? megaCheckingIds.has(row.accountId) : false;
|
const checking = row.accountId ? megaCheckingIds.has(row.accountId) : false;
|
||||||
let statusCls = "none";
|
let statusCls = "none";
|
||||||
let statusText = "—";
|
let statusText = "—";
|
||||||
if (row.disabled) { statusCls = "disabled"; statusText = "Deaktiviert"; }
|
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 (checking) { statusCls = "unknown"; statusText = "Prüfe…"; }
|
||||||
else if (!st) { statusCls = "unknown"; statusText = "Noch nicht geprüft"; }
|
else if (!st) { statusCls = "unknown"; statusText = "Noch nicht geprüft"; }
|
||||||
else if (!st.valid) { statusCls = "invalid"; statusText = st.message || "Login ungültig"; }
|
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`
|
? `${humanSize(row.dailyRemainingBytes)} von ${humanSize(row.dailyLimitBytes)} übrig`
|
||||||
: "Unbeschränkt";
|
: "Unbeschränkt";
|
||||||
return (
|
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">
|
<span className="acct2-c-check">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -2733,18 +2780,21 @@ export function App(): ReactElement {
|
|||||||
</span>
|
</span>
|
||||||
<span className="acct2-user" title={usernameTitle}>{username}</span>
|
<span className="acct2-user" title={usernameTitle}>{username}</span>
|
||||||
<span className="acct2-expiry">{expiry}</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">
|
<span className="acct2-c-actions">
|
||||||
{row.toggleKind === "single" && getAccountQuickActionMeta(row.entry.kind) && (
|
<button
|
||||||
<button className="btn btn-sm" disabled={actionBusy} onClick={() => { void onAccountRowQuickAction(row.entry); }}>
|
className="btn btn-sm acct2-menu-button"
|
||||||
{getAccountQuickActionMeta(row.entry.kind)?.label}
|
disabled={actionBusy}
|
||||||
</button>
|
title="Account-Aktionen"
|
||||||
)}
|
aria-label={`${row.hosterLabel} Account-Aktionen`}
|
||||||
<button className="btn btn-sm" disabled={actionBusy} title="Account bearbeiten" onClick={() => openEditAccountDialog(row.entry.kind)}>Bearbeiten</button>
|
onClick={(event) => {
|
||||||
<button className="btn btn-sm danger" disabled={actionBusy} title="Account entfernen" onClick={() => {
|
event.stopPropagation();
|
||||||
if (row.toggleKind === "mega" && row.megaLogin) { void onRemoveMegaAccount(row.megaLogin); }
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
else if (row.toggleKind === "dl" && row.dlKey) { void onRemoveDebridLinkKey(row.dlKey); }
|
setAccountContextMenu({ x: rect.right, y: rect.bottom, row });
|
||||||
else { void onRemoveAccount(row.entry); }
|
}}
|
||||||
}}>Entfernen</button>
|
>
|
||||||
|
⋯
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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> => {
|
const onCheckUpdates = async (): Promise<void> => {
|
||||||
let updateResult: UpdateCheckResult | null = null;
|
let updateResult: UpdateCheckResult | null = null;
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
@@ -4136,6 +4229,17 @@ export function App(): ReactElement {
|
|||||||
};
|
};
|
||||||
}, [contextMenu]);
|
}, [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(() => {
|
useLayoutEffect(() => {
|
||||||
if (!contextMenu || !ctxMenuRef.current) return;
|
if (!contextMenu || !ctxMenuRef.current) return;
|
||||||
const el = ctxMenuRef.current;
|
const el = ctxMenuRef.current;
|
||||||
@@ -4148,6 +4252,18 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
}, [contextMenu]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!colHeaderCtx) return;
|
if (!colHeaderCtx) return;
|
||||||
const close = (e: MouseEvent): void => {
|
const close = (e: MouseEvent): void => {
|
||||||
@@ -5562,8 +5678,8 @@ export function App(): ReactElement {
|
|||||||
<div className="settings-section card account-board">
|
<div className="settings-section card account-board">
|
||||||
<div className="account-board-header">
|
<div className="account-board-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>Accounts</h3>
|
<h3>Accountverwaltung</h3>
|
||||||
<div className="hint">Accounts werden als Liste verwaltet. Neue Einträge kommen über den Dialog oben rechts dazu.</div>
|
<div className="hint">Kompakte Übersicht mit Hoster, Traffic, Status, Benutzername, Ablauf und Zugang. Rechtsklick oder Doppelklick öffnet die Aktionen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="account-board-header-actions">
|
<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">
|
<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 className="acct2-sortable" title="Nach Premium-Restlaufzeit sortieren (längste/kürzeste)" onClick={cycleAccountStatusSort}>Status{accountStatusSort === "desc" ? " ▼" : accountStatusSort === "asc" ? " ▲" : ""}</span>
|
||||||
<span>Benutzername</span>
|
<span>Benutzername</span>
|
||||||
<span>Verfallsdatum</span>
|
<span>Verfallsdatum</span>
|
||||||
|
<span>Zugang</span>
|
||||||
<span className="acct2-c-actions">Aktion</span>
|
<span className="acct2-c-actions">Aktion</span>
|
||||||
</div>
|
</div>
|
||||||
{groupedAccountRows.flatMap((group) => {
|
{groupedAccountRows.flatMap((group) => {
|
||||||
@@ -5625,15 +5742,16 @@ export function App(): ReactElement {
|
|||||||
<span className="acct2-mode">{group.rows.length} Accounts</span>
|
<span className="acct2-mode">{group.rows.length} Accounts</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="acct2-traffic" />
|
<span className="acct2-traffic" />
|
||||||
<span className="acct2-status acct2-grp-sum">
|
<span className="acct2-status acct2-grp-sum">
|
||||||
{okCount > 0 && <span className="account-validity-badge ok">{okCount} OK</span>}
|
{okCount > 0 && <span className="account-validity-badge ok">{okCount} OK</span>}
|
||||||
{problemCount > 0 && <span className="account-validity-badge invalid">{problemCount} Problem</span>}
|
{problemCount > 0 && <span className="account-validity-badge invalid">{problemCount} Problem</span>}
|
||||||
{offCount > 0 && <span className="account-validity-badge disabled">{offCount} aus</span>}
|
{offCount > 0 && <span className="account-validity-badge disabled">{offCount} aus</span>}
|
||||||
</span>
|
</span>
|
||||||
<span />
|
<span />
|
||||||
<span />
|
<span />
|
||||||
<span />
|
<span />
|
||||||
</div>
|
<span />
|
||||||
|
</div>
|
||||||
];
|
];
|
||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
for (const row of group.rows) elements.push(renderAccountRow(row, true));
|
for (const row of group.rows) elements.push(renderAccountRow(row, true));
|
||||||
@@ -6702,6 +6820,38 @@ export function App(): ReactElement {
|
|||||||
)}
|
)}
|
||||||
{statusToast && <div className="toast">{statusToast}</div>}
|
{statusToast && <div className="toast">{statusToast}</div>}
|
||||||
{dragOver && <div className="drop-overlay">Links, .dlc oder Export-Dateien hier ablegen</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 && (() => {
|
{contextMenu && (() => {
|
||||||
const multi = selectedIds.size > 1;
|
const multi = selectedIds.size > 1;
|
||||||
const selectedPackageIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
|
const selectedPackageIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
|
||||||
|
|||||||
@@ -3370,7 +3370,7 @@ td {
|
|||||||
.acct2-head,
|
.acct2-head,
|
||||||
.acct2-row {
|
.acct2-row {
|
||||||
display: grid;
|
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;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
@@ -3412,6 +3412,7 @@ td {
|
|||||||
.acct2-c-check { display: flex; justify-content: center; }
|
.acct2-c-check { display: flex; justify-content: center; }
|
||||||
.acct2-c-check input { cursor: pointer; }
|
.acct2-c-check input { cursor: pointer; }
|
||||||
.acct2-c-actions { display: flex; gap: 6px; justify-content: flex-end; }
|
.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 { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
||||||
.acct2-hoster strong { font-size: 13px; }
|
.acct2-hoster strong { font-size: 13px; }
|
||||||
.acct2-mode { font-size: 11px; color: var(--muted); }
|
.acct2-mode { font-size: 11px; color: var(--muted); }
|
||||||
@@ -3422,9 +3423,15 @@ td {
|
|||||||
.acct2-sortable { cursor: pointer; user-select: none; }
|
.acct2-sortable { cursor: pointer; user-select: none; }
|
||||||
.acct2-sortable:hover { color: var(--text); }
|
.acct2-sortable:hover { color: var(--text); }
|
||||||
.acct2-user { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.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-status .account-validity-badge { margin-top: 0; }
|
||||||
.acct2-nostatus { color: var(--muted); opacity: 0.6; font-variant-numeric: tabular-nums; }
|
.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-panel { display: flex; flex-direction: column; gap: 6px; max-height: 320px; overflow-y: auto; }
|
||||||
.rotation-empty { color: var(--muted, #a59c8e); font-size: 12px; }
|
.rotation-empty { color: var(--muted, #a59c8e); font-size: 12px; }
|
||||||
.rotation-event {
|
.rotation-event {
|
||||||
|
|||||||
Reference in New Issue
Block a user