UI Phase 3: Account-Verwaltung als JDownloader-artige Liste (eine Zeile pro Account, Status inline) + Pruefung beim Hinzufuegen
Account-Rework (User-Wunsch nach JDownloader-Vorlage-Screenshot): - Tabelle zeigt jetzt EINE ZEILE PRO EINZELNEM ACCOUNT statt eine Zeile pro Anbieter. Mega-Debrid mit 4 Accounts = 4 Zeilen, Debrid-Link je Key eine Zeile, Single-Token-Anbieter je eine Zeile. Neues geflachtes Modell accountRows (useMemo) aus configuredAccounts: Mega ueber parseMegaDebridAccounts, DL ueber entry.debridLinkKeys, Rest 1:1. - Spalten wie JDownloader: aktiviert-Checkbox | Hoster (+Modus) | Download-Traffic (Rest von Limit "X von Y uebrig" bzw. "Unbeschraenkt") | Status (farbig) | Benutzername | Verfallsdatum | Aktion (Bearbeiten/Entfernen). - STATUS INLINE + farbig (gruen Premium / gelb Free / rot ungueltig / grau nicht geprueft / Deaktiviert) direkt in der Zeile, ohne erst "Bearbeiten" zu oeffnen. Quelle: settings.debridAccountStatuses[accountId] (Mega/DL werden geprueft), Benutzername aus status.email, Verfallsdatum aus premiumUntilMs. Single-Token- Anbieter (Real-Debrid, AllDebrid, 1Fichier, ...) werden nicht geprueft -> "Konfiguriert". - Problemzeilen (Login ungueltig) rot hinterlegt, deaktivierte Zeilen ausgegraut. - aktiviert-Checkbox schaltet den EINZELNEN Account: Mega via neuem onToggleMegaAccountEnabled (megaDebridDisabledAccountIds), DL via bestehendem onToggleDebridLinkApiKeyEnabled, Single via onToggleAccountEnabled. Entfernen je Account: Mega/DL ueber neue Handler (Zeile aus megaCredentials/debridLinkApiKeys raus), Single ueber onRemoveAccount. - PRUEFUNG BEIM HINZUFUEGEN: onSaveAccountDialog stoesst nach dem Speichern checkAllAccounts() an -> Status erscheint sofort in der Liste + Toast meldet "X/Y Login gueltig, Z Premium" (Speichern bleibt erlaubt, wie gewuenscht). - Hoster-Reihenfolge- und Rotations-Verlauf-Panel unveraendert darunter. Alte resizable Spalten + "Zugang einzeln"-Toggle entfernt (durch Zeilen ersetzt). Web-Login-/AllDebrid-Status-Aktion bleibt als Knopf in der Single-Zeile. - account-validity-badge.ok von Neon-Verlauf auf flaches Gruen, .disabled ergaenzt. Reine Renderer-Aenderung. 810 Tests gruen, tsc=6 Baseline, build ok. Erste Version nach Screenshot — Feinschliff der Optik nach User-Feedback.
This commit is contained in:
parent
1ea9c42d04
commit
d594afe93a
@ -26,6 +26,9 @@ import {
|
||||
getDebridLinkApiKeyDailyLimitBytes,
|
||||
getDebridLinkApiKeyDailyRemainingBytes,
|
||||
getDebridLinkApiKeyDailyUsageBytes,
|
||||
getMegaDebridAccountDailyLimitBytes,
|
||||
getMegaDebridAccountDailyUsageBytes,
|
||||
getMegaDebridAccountTotalUsageBytes,
|
||||
getProviderDailyLimitBytes,
|
||||
getProviderDailyRemainingBytes,
|
||||
getProviderTotalUsageBytes,
|
||||
@ -2531,6 +2534,88 @@ export function App(): ReactElement {
|
||||
}, [settingsDraft, snapshot.settings, allDebridHostInfo, allDebridHostLoading, hasSavedAllDebridAccount, allDebridSettingsDirty]);
|
||||
|
||||
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[] = [];
|
||||
for (const entry of configuredAccounts) {
|
||||
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
||||
const accounts = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "");
|
||||
for (const acc of accounts) {
|
||||
const used = getMegaDebridAccountDailyUsageBytes(snapshot.settings, acc.id);
|
||||
const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id);
|
||||
rows.push({
|
||||
rowKey: `mega-${acc.id}`,
|
||||
entry,
|
||||
hosterLabel: entry.serviceLabel,
|
||||
modeLabel: entry.modeLabel,
|
||||
username: acc.maskedLogin,
|
||||
accountId: acc.id,
|
||||
checkable: true,
|
||||
disabled: (settingsDraft.megaDebridDisabledAccountIds || []).includes(acc.id),
|
||||
dailyUsedBytes: used,
|
||||
dailyLimitBytes: limit,
|
||||
dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0,
|
||||
totalUsedBytes: getMegaDebridAccountTotalUsageBytes(snapshot.settings, acc.id),
|
||||
toggleKind: "mega",
|
||||
megaLogin: acc.login
|
||||
});
|
||||
}
|
||||
} else if (entry.kind === "debridlink-api") {
|
||||
for (const key of entry.debridLinkKeys) {
|
||||
rows.push({
|
||||
rowKey: `dl-${key.id}`,
|
||||
entry,
|
||||
hosterLabel: entry.serviceLabel,
|
||||
modeLabel: entry.modeLabel,
|
||||
username: key.masked,
|
||||
accountId: key.id,
|
||||
checkable: true,
|
||||
disabled: key.disabled,
|
||||
dailyUsedBytes: key.dailyUsedBytes,
|
||||
dailyLimitBytes: key.dailyLimitBytes,
|
||||
dailyRemainingBytes: key.dailyLimitBytes > 0 ? Math.max(0, key.dailyLimitBytes - key.dailyUsedBytes) : 0,
|
||||
totalUsedBytes: key.totalUsedBytes,
|
||||
toggleKind: "dl",
|
||||
dlKey: key
|
||||
});
|
||||
}
|
||||
} else {
|
||||
rows.push({
|
||||
rowKey: `svc-${entry.service}`,
|
||||
entry,
|
||||
hosterLabel: entry.serviceLabel,
|
||||
modeLabel: entry.modeLabel,
|
||||
username: entry.summary,
|
||||
accountId: null,
|
||||
checkable: false,
|
||||
disabled: entry.disabled,
|
||||
dailyUsedBytes: entry.dailyUsedBytes,
|
||||
dailyLimitBytes: entry.dailyLimitBytes,
|
||||
dailyRemainingBytes: entry.dailyLimitBytes > 0 ? Math.max(0, entry.dailyRemainingBytes ?? 0) : 0,
|
||||
totalUsedBytes: entry.totalUsedBytes,
|
||||
toggleKind: "single"
|
||||
});
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}, [configuredAccounts, settingsDraft, snapshot.settings]);
|
||||
const availableAccountOptions = useMemo(() => (
|
||||
ACCOUNT_OPTIONS.filter((option) => !configuredAccountServices.has(option.service))
|
||||
), [configuredAccountServices]);
|
||||
@ -2851,6 +2936,7 @@ export function App(): ReactElement {
|
||||
} else if (selectedOption) {
|
||||
showToast(`${selectedOption.title} gespeichert`, 2200);
|
||||
}
|
||||
void checkAllAccounts();
|
||||
}, (error) => {
|
||||
showToast(`Account konnte nicht gespeichert werden: ${String(error)}`, 3200);
|
||||
});
|
||||
@ -2932,6 +3018,40 @@ export function App(): ReactElement {
|
||||
});
|
||||
};
|
||||
|
||||
const onToggleMegaAccountEnabled = async (login: string, currentlyDisabled: boolean): Promise<void> => {
|
||||
const accId = getMegaDebridAccountId(login.trim());
|
||||
await performQuickAction(async () => {
|
||||
const current = settingsDraft.megaDebridDisabledAccountIds || [];
|
||||
const next = currentlyDisabled ? current.filter((id) => id !== accId) : [...current, accId];
|
||||
await persistSpecificSettings({ ...settingsDraft, megaDebridDisabledAccountIds: next });
|
||||
showToast(currentlyDisabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
||||
}, (error) => {
|
||||
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
||||
});
|
||||
};
|
||||
|
||||
const onRemoveMegaAccount = async (login: string): Promise<void> => {
|
||||
const confirmed = await askConfirmPrompt({ title: "Account entfernen", message: `Soll der Mega-Debrid-Account ${maskMegaDebridLogin(login)} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true });
|
||||
if (!confirmed) return;
|
||||
await performQuickAction(async () => {
|
||||
const remaining = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "")
|
||||
.filter((a) => a.login.trim().toLowerCase() !== login.trim().toLowerCase());
|
||||
const nextCreds = serializeMegaDebridAccounts(remaining.map((a) => ({ login: a.login, password: a.password })));
|
||||
await persistSpecificSettings({ ...settingsDraft, megaCredentials: nextCreds, megaLogin: remaining[0]?.login ?? "", megaPassword: remaining[0]?.password ?? "" });
|
||||
showToast("Account entfernt", 2000);
|
||||
}, (error) => { showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 3200); });
|
||||
};
|
||||
|
||||
const onRemoveDebridLinkKey = async (key: DebridLinkAccountKeyEntry): Promise<void> => {
|
||||
const confirmed = await askConfirmPrompt({ title: "Key entfernen", message: `Soll der Debrid-Link-Key ${key.masked} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true });
|
||||
if (!confirmed) return;
|
||||
await performQuickAction(async () => {
|
||||
const remaining = parseDebridLinkApiKeys(settingsDraft.debridLinkApiKeys || "").filter((k) => k.id !== key.id);
|
||||
await persistSpecificSettings({ ...settingsDraft, debridLinkApiKeys: remaining.map((k) => k.token).join("\n") });
|
||||
showToast("Key entfernt", 2000);
|
||||
}, (error) => { showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 3200); });
|
||||
};
|
||||
|
||||
const onToggleAccountEnabled = async (entry: ConfiguredAccountEntry): Promise<void> => {
|
||||
await performQuickAction(async () => {
|
||||
const provider = entry.service as DebridProvider;
|
||||
@ -5183,160 +5303,84 @@ export function App(): ReactElement {
|
||||
</div>
|
||||
|
||||
<div className="account-board-summary">
|
||||
<span className="account-inline-stat">{configuredAccounts.length} aktiv</span>
|
||||
<span className="account-inline-stat">{accountRows.length} {accountRows.length === 1 ? "Account" : "Accounts"}</span>
|
||||
<span className="account-inline-stat">{availableAccountOptions.length} weitere Typen verfügbar</span>
|
||||
</div>
|
||||
|
||||
<label className="toggle-line account-display-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settingsDraft.accountListShowDetailedDebridLinkKeys}
|
||||
onChange={(e) => setBool("accountListShowDetailedDebridLinkKeys", e.target.checked)}
|
||||
/>
|
||||
Debrid-Link-Keys im Feld "Zugang" einzeln untereinander anzeigen
|
||||
</label>
|
||||
<div className="account-display-actions">
|
||||
<button className="btn btn-sm" disabled={actionBusy} onClick={resetAccountColumnWidths}>
|
||||
Spalten zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{configuredAccounts.length === 0 && (
|
||||
{accountRows.length === 0 && (
|
||||
<div className="account-empty-state">
|
||||
<strong>Noch keine Accounts hinterlegt</strong>
|
||||
<span>Füge über "Account hinzufügen" den ersten Dienst hinzu. Danach erscheinen hier Status, Zugang und Aktionen als Liste.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{configuredAccounts.length > 0 && (
|
||||
<div className="account-table" style={accountTableStyle}>
|
||||
<div className="account-table-head">
|
||||
<div className="account-header-cell">
|
||||
<span>Account</span>
|
||||
<button
|
||||
className="account-resize-handle"
|
||||
title="Spalte ziehen"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
startAccountColumnResize("service", event.clientX);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="account-header-cell">
|
||||
<span>Typ</span>
|
||||
<button
|
||||
className="account-resize-handle"
|
||||
title="Spalte ziehen"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
startAccountColumnResize("mode", event.clientX);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="account-header-cell">
|
||||
<span>Status</span>
|
||||
<button
|
||||
className="account-resize-handle"
|
||||
title="Spalte ziehen"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
startAccountColumnResize("status", event.clientX);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="account-header-cell">
|
||||
<span>Info</span>
|
||||
</div>
|
||||
<div className="account-header-cell">
|
||||
<span>Zugang</span>
|
||||
<button
|
||||
className="account-resize-handle"
|
||||
title="Spalte ziehen"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
startAccountColumnResize("secret", event.clientX);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="account-header-cell">
|
||||
<span>Aktionen</span>
|
||||
</div>
|
||||
{accountRows.length > 0 && (
|
||||
<div className="acct2-table">
|
||||
<div className="acct2-head">
|
||||
<span className="acct2-c-check" />
|
||||
<span>Hoster</span>
|
||||
<span>Download-Traffic</span>
|
||||
<span>Status</span>
|
||||
<span>Benutzername</span>
|
||||
<span>Verfallsdatum</span>
|
||||
<span className="acct2-c-actions">Aktion</span>
|
||||
</div>
|
||||
{configuredAccounts.map((entry) => {
|
||||
const option = findAccountOption(entry.kind);
|
||||
const quickAction = getAccountQuickActionMeta(entry.kind);
|
||||
const showStatusButton = entry.service === "alldebrid";
|
||||
const showQuickActionButton = Boolean(quickAction && !(showStatusButton && quickAction.action === "alldebrid-status"));
|
||||
const allDebridStateClass = entry.service === "alldebrid" && allDebridHostInfo ? ` account-status-${allDebridHostInfo.state}` : "";
|
||||
{accountRows.map((row) => {
|
||||
const st = row.accountId ? (snapshot.settings?.debridAccountStatuses?.[row.accountId] ?? null) : null;
|
||||
const checking = row.accountId ? megaCheckingIds.has(row.accountId) : false;
|
||||
let statusCls = "ok";
|
||||
let statusText = "Konfiguriert";
|
||||
if (row.disabled) { statusCls = "disabled"; statusText = "Deaktiviert"; }
|
||||
else if (!row.checkable) { statusCls = "ok"; statusText = "Konfiguriert"; }
|
||||
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"; }
|
||||
else if (!st.isPremium) { statusCls = "free"; statusText = "Free Account"; }
|
||||
else { statusCls = "ok"; statusText = st.message || "Premium Account"; }
|
||||
const isProblem = statusCls === "invalid";
|
||||
const username = st && st.email ? st.email : row.username;
|
||||
const expiry = st && st.premiumUntilMs && st.premiumUntilMs > 0 ? new Date(st.premiumUntilMs).toLocaleDateString("de-DE") : "—";
|
||||
const traffic = row.dailyLimitBytes > 0
|
||||
? `${humanSize(row.dailyRemainingBytes)} von ${humanSize(row.dailyLimitBytes)} übrig`
|
||||
: "Unbeschränkt";
|
||||
return (
|
||||
<div key={entry.service} className={`account-row${entry.disabled ? " account-row-disabled" : ""}`}>
|
||||
<div className="account-cell account-service-cell">
|
||||
<strong>{entry.serviceLabel}</strong>
|
||||
<span>{option.title}</span>
|
||||
</div>
|
||||
<div className="account-cell">
|
||||
<span className="account-mode-pill">{entry.modeLabel}</span>
|
||||
</div>
|
||||
<div className="account-cell account-status-cell">
|
||||
<span className={`account-status-pill${entry.disabled ? " account-status-disabled" : ""}${allDebridStateClass}`}>{entry.statusLabel}</span>
|
||||
{entry.note && <span className="account-note">{entry.note}</span>}
|
||||
</div>
|
||||
<div className="account-cell account-info-cell">
|
||||
{entry.debridLinkKeys.length > 0 ? (
|
||||
<div className="account-usage-stack">
|
||||
<button className="btn btn-sm" onClick={() => setKeyStatsPopup(entry.service)}>
|
||||
Statistik
|
||||
</button>
|
||||
<span className="account-usage-total">Insgesamt: {humanSize(entry.totalUsedBytes)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="account-usage-stack">
|
||||
<div className={`account-usage-stats${entry.dailyLimitReached ? " warning" : ""}`}>
|
||||
<span>Heute: {humanSize(entry.dailyUsedBytes)}</span>
|
||||
<span>{entry.dailyLimitBytes > 0 ? `Limit: ${humanSize(entry.dailyLimitBytes)}` : "Kein Tageslimit"}</span>
|
||||
{entry.dailyLimitBytes > 0 && (
|
||||
<span>{entry.dailyLimitReached ? "Fallback aktiv" : `Rest: ${humanSize(entry.dailyRemainingBytes || 0)}`}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="account-usage-total">Insgesamt: {humanSize(entry.totalUsedBytes)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="account-cell">
|
||||
{entry.summaryLines.length > 1 ? (
|
||||
<div className="account-secret account-secret-multiline">
|
||||
{entry.summaryLines.map((line) => (
|
||||
<span key={line}>{line}</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="account-secret">{entry.summary}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="account-cell account-row-actions">
|
||||
{showStatusButton && (
|
||||
<button className="btn" disabled={actionBusy || allDebridHostLoading || !hasSavedAllDebridAccount} onClick={() => { void performQuickAction(async () => { await runAccountQuickAction("alldebrid-status"); }, (error) => { showToast(`AllDebrid Status fehlgeschlagen: ${String(error)}`, 3200); }); }}>
|
||||
Status
|
||||
<div key={row.rowKey} className={`acct2-row${row.disabled ? " acct2-disabled" : ""}${isProblem ? " acct2-problem" : ""}`}>
|
||||
<span className="acct2-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!row.disabled}
|
||||
disabled={actionBusy}
|
||||
title={row.disabled ? "Account aktivieren" : "Account deaktivieren (bleibt gespeichert)"}
|
||||
onChange={() => {
|
||||
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); }
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="acct2-hoster">
|
||||
<strong>{row.hosterLabel}</strong>
|
||||
<span className="acct2-mode">{row.modeLabel}</span>
|
||||
</span>
|
||||
<span className="acct2-traffic">{traffic}</span>
|
||||
<span className="acct2-status">
|
||||
<span className={`account-validity-badge ${statusCls}`}>{statusText}</span>
|
||||
</span>
|
||||
<span className="acct2-user" title={username}>{username}</span>
|
||||
<span className="acct2-expiry">{expiry}</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>
|
||||
)}
|
||||
{showQuickActionButton && quickAction && (
|
||||
<button className="btn" disabled={actionBusy} onClick={() => { void onAccountRowQuickAction(entry); }}>
|
||||
{quickAction.label}
|
||||
</button>
|
||||
)}
|
||||
<button className="btn" disabled={actionBusy} onClick={() => { void onToggleAccountEnabled(entry); }}>
|
||||
{entry.disabled ? "Aktivieren" : "Deaktivieren"}
|
||||
</button>
|
||||
<button className="btn" disabled={actionBusy || entry.dailyUsedBytes <= 0} onClick={() => { void onResetAccountDailyUsage(entry); }}>
|
||||
Reset Heute
|
||||
</button>
|
||||
<button className="btn" disabled={actionBusy} onClick={() => openEditAccountDialog(entry.kind)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="btn danger" disabled={actionBusy} onClick={() => { void onRemoveAccount(entry); }}>
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -3255,10 +3255,55 @@ td {
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.account-validity-badge.ok { color: #1c1206; background: linear-gradient(90deg, #7bd88f, #4fb96a); border-color: #4fb96a; }
|
||||
.account-validity-badge.ok { color: #10240f; background: #4fb96a; border-color: #3f9d57; }
|
||||
.account-validity-badge.free { color: #2a2113; background: #f2c14e; border-color: #d9a72f; }
|
||||
.account-validity-badge.invalid { color: #fff; background: #d9534f; border-color: #c0392b; }
|
||||
.account-validity-badge.unknown { color: var(--muted, #a59c8e); background: transparent; border-color: var(--line, #4a4032); }
|
||||
.account-validity-badge.disabled { color: var(--muted, #a59c8e); background: transparent; border-color: var(--border); opacity: 0.8; }
|
||||
|
||||
.acct2-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.acct2-head,
|
||||
.acct2-row {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(110px, 1.1fr) minmax(150px, 1.5fr) minmax(120px, 1.4fr) minmax(140px, 1.6fr) minmax(92px, 0.9fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 12px;
|
||||
}
|
||||
.acct2-head {
|
||||
background: color-mix(in srgb, var(--card) 60%, transparent);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.acct2-row {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
|
||||
font-size: 13px;
|
||||
}
|
||||
.acct2-row:last-child { border-bottom: 0; }
|
||||
.acct2-row:nth-child(even) { background: color-mix(in srgb, var(--card) 28%, transparent); }
|
||||
.acct2-row.acct2-problem { background: color-mix(in srgb, var(--danger) 15%, transparent); }
|
||||
.acct2-row.acct2-disabled { opacity: 0.55; }
|
||||
.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-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); }
|
||||
.acct2-traffic,
|
||||
.acct2-expiry { font-variant-numeric: tabular-nums; color: var(--muted); }
|
||||
.acct2-user { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.acct2-status .account-validity-badge { margin-top: 0; }
|
||||
|
||||
.rotation-panel { display: flex; flex-direction: column; gap: 6px; max-height: 320px; overflow-y: auto; }
|
||||
.rotation-empty { color: var(--muted, #a59c8e); font-size: 12px; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user