Release v1.7.1
This commit is contained in:
+327
-45
@@ -6,6 +6,7 @@ import type {
|
||||
AppTheme,
|
||||
BandwidthScheduleEntry,
|
||||
DebridFallbackProvider,
|
||||
DebridLinkHostLimitInfo,
|
||||
DebridProvider,
|
||||
DownloadItem,
|
||||
DownloadStats,
|
||||
@@ -105,7 +106,9 @@ interface AccountDialogState {
|
||||
interface DebridLinkAccountKeyEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
token: string;
|
||||
masked: string;
|
||||
disabled: boolean;
|
||||
dailyUsedBytes: number;
|
||||
dailyLimitBytes: number;
|
||||
dailyRemainingBytes: number | null;
|
||||
@@ -691,6 +694,7 @@ const emptyStats = (): DownloadStats => ({
|
||||
const emptySnapshot = (): UiSnapshot => ({
|
||||
settings: {
|
||||
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", linkSnappyLogin: "", linkSnappyPassword: "",
|
||||
debridLinkDisabledKeyIds: [],
|
||||
archivePasswordList: "",
|
||||
rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
|
||||
providerTertiary: "none", autoProviderFallback: true, outputDir: "", packageName: "",
|
||||
@@ -875,6 +879,39 @@ function formatAllDebridTimestamp(info: AllDebridHostInfo): string {
|
||||
return formatDateTime(info.lastCheckedAt || info.fetchedAt);
|
||||
}
|
||||
|
||||
function formatDebridLinkTraffic(info: DebridLinkHostLimitInfo | null | undefined): string {
|
||||
if (!info) {
|
||||
return "Lade...";
|
||||
}
|
||||
const toGb = (bytes: number): string => `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
if (info.trafficCurrentBytes !== null && info.trafficMaxBytes !== null) {
|
||||
return `${toGb(info.trafficCurrentBytes)} / ${toGb(info.trafficMaxBytes)}`;
|
||||
}
|
||||
if (info.trafficMaxBytes !== null) {
|
||||
return `max. ${toGb(info.trafficMaxBytes)}`;
|
||||
}
|
||||
if (info.trafficCurrentBytes !== null) {
|
||||
return toGb(info.trafficCurrentBytes);
|
||||
}
|
||||
return info.note || "Nicht verfügbar";
|
||||
}
|
||||
|
||||
function formatDebridLinkCountQuota(info: DebridLinkHostLimitInfo | null | undefined): string {
|
||||
if (!info) {
|
||||
return "Lade...";
|
||||
}
|
||||
if (info.linksCurrent !== null && info.linksMax !== null) {
|
||||
return `${info.linksCurrent} / ${info.linksMax}`;
|
||||
}
|
||||
if (info.linksMax !== null) {
|
||||
return `max. ${info.linksMax}`;
|
||||
}
|
||||
if (info.linksCurrent !== null) {
|
||||
return String(info.linksCurrent);
|
||||
}
|
||||
return info.note || "Nicht verfügbar";
|
||||
}
|
||||
|
||||
interface BandwidthChartProps {
|
||||
items: Record<string, DownloadItem>;
|
||||
running: boolean;
|
||||
@@ -1254,6 +1291,10 @@ export function App(): ReactElement {
|
||||
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
|
||||
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
|
||||
const [accountDialogSearch, setAccountDialogSearch] = useState("");
|
||||
const [keyStatsPopup, setKeyStatsPopup] = useState<string | null>(null);
|
||||
const [debridLinkHostLimits, setDebridLinkHostLimits] = useState<Record<string, DebridLinkHostLimitInfo>>({});
|
||||
const [debridLinkHostLimitsLoading, setDebridLinkHostLimitsLoading] = useState(false);
|
||||
const [debridLinkHostLimitsError, setDebridLinkHostLimitsError] = useState("");
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{ ids: Set<string>; dontAsk: boolean } | null>(null);
|
||||
const [columnOrder, setColumnOrder] = useState<string[]>(() => DEFAULT_COLUMN_ORDER);
|
||||
@@ -1271,6 +1312,7 @@ export function App(): ReactElement {
|
||||
const [allDebridHostInfo, setAllDebridHostInfo] = useState<AllDebridHostInfo | null>(null);
|
||||
const [allDebridHostLoading, setAllDebridHostLoading] = useState(false);
|
||||
const allDebridHostRequestRef = useRef(0);
|
||||
const debridLinkHostLimitsRequestRef = useRef(0);
|
||||
const accountColumnResizeRef = useRef<{ key: AccountColumnKey; startX: number; startWidth: number } | null>(null);
|
||||
const onAccountColumnResizeMove = useCallback((event: MouseEvent): void => {
|
||||
const active = accountColumnResizeRef.current;
|
||||
@@ -1435,6 +1477,149 @@ export function App(): ReactElement {
|
||||
}
|
||||
}, [showToast]);
|
||||
|
||||
const loadDebridLinkHostLimits = useCallback(async (silent = false): Promise<void> => {
|
||||
const requestId = debridLinkHostLimitsRequestRef.current + 1;
|
||||
debridLinkHostLimitsRequestRef.current = requestId;
|
||||
setDebridLinkHostLimitsLoading(true);
|
||||
setDebridLinkHostLimitsError("");
|
||||
setDebridLinkHostLimits({});
|
||||
try {
|
||||
const apiKeys = parseDebridLinkApiKeys(settingsDraft.debridLinkApiKeys || "");
|
||||
if (apiKeys.length === 0) {
|
||||
throw new Error("Debrid-Link ist nicht konfiguriert");
|
||||
}
|
||||
|
||||
let loadedAny = false;
|
||||
let firstError = "";
|
||||
for (let index = 0; index < apiKeys.length; index += 1) {
|
||||
if (!mountedRef.current || debridLinkHostLimitsRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKey = apiKeys[index];
|
||||
const controller = new AbortController();
|
||||
const timer = window.setTimeout(() => controller.abort(), 8000);
|
||||
let info: DebridLinkHostLimitInfo;
|
||||
try {
|
||||
const readLimitsPayload = async (path: "limits" | "limits/all") => {
|
||||
const response = await fetch(`https://debrid-link.com/api/v2/downloader/${path}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey.token}`
|
||||
},
|
||||
signal: controller.signal
|
||||
});
|
||||
const payload = await response.json() as {
|
||||
success?: boolean;
|
||||
value?: {
|
||||
hosters?: Array<{
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
daySize?: { current?: number; value?: number };
|
||||
dayCount?: { current?: number; value?: number };
|
||||
}>;
|
||||
};
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
};
|
||||
if (!response.ok || !payload?.success) {
|
||||
throw new Error(String(payload?.error_description || payload?.error || `HTTP ${response.status}`));
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
let payload = await readLimitsPayload("limits/all");
|
||||
let hostEntry = (payload.value?.hosters || []).find((entry) => String(entry.name || "").toLowerCase() === "rapidgator");
|
||||
if (!hostEntry) {
|
||||
payload = await readLimitsPayload("limits");
|
||||
hostEntry = (payload.value?.hosters || []).find((entry) => String(entry.name || "").toLowerCase() === "rapidgator");
|
||||
}
|
||||
if (!hostEntry) {
|
||||
info = {
|
||||
keyId: apiKey.id,
|
||||
keyLabel: apiKey.label,
|
||||
host: "rapidgator",
|
||||
fetchedAt: Date.now(),
|
||||
trafficCurrentBytes: null,
|
||||
trafficMaxBytes: null,
|
||||
linksCurrent: null,
|
||||
linksMax: null,
|
||||
note: "Rapidgator nicht in der API-Antwort gefunden."
|
||||
};
|
||||
} else {
|
||||
info = {
|
||||
keyId: apiKey.id,
|
||||
keyLabel: apiKey.label,
|
||||
host: String(hostEntry.displayName || hostEntry.name || "rapidgator"),
|
||||
fetchedAt: Date.now(),
|
||||
trafficCurrentBytes: typeof hostEntry.daySize?.current === "number" ? hostEntry.daySize.current : null,
|
||||
trafficMaxBytes: typeof hostEntry.daySize?.value === "number" ? hostEntry.daySize.value : null,
|
||||
linksCurrent: typeof hostEntry.dayCount?.current === "number" ? hostEntry.dayCount.current : null,
|
||||
linksMax: typeof hostEntry.dayCount?.value === "number" ? hostEntry.dayCount.value : null,
|
||||
note: ""
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const message = String(error || "Quota konnte nicht geladen werden");
|
||||
if (!firstError) {
|
||||
firstError = message;
|
||||
}
|
||||
info = {
|
||||
keyId: apiKey.id,
|
||||
keyLabel: apiKey.label,
|
||||
host: "rapidgator",
|
||||
fetchedAt: Date.now(),
|
||||
trafficCurrentBytes: null,
|
||||
trafficMaxBytes: null,
|
||||
linksCurrent: null,
|
||||
linksMax: null,
|
||||
note: message
|
||||
};
|
||||
} finally {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
if (!mountedRef.current || debridLinkHostLimitsRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadedAny = true;
|
||||
setDebridLinkHostLimits((prev) => ({
|
||||
...prev,
|
||||
[info.keyId]: info
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
if (!loadedAny && firstError) {
|
||||
throw new Error(firstError);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!mountedRef.current || debridLinkHostLimitsRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
setDebridLinkHostLimits({});
|
||||
setDebridLinkHostLimitsError(String(error));
|
||||
if (!silent) {
|
||||
showToast(`Debrid-Link Quota fehlgeschlagen: ${String(error)}`, 3200);
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current && debridLinkHostLimitsRequestRef.current === requestId) {
|
||||
setDebridLinkHostLimitsLoading(false);
|
||||
}
|
||||
}
|
||||
}, [settingsDraft.debridLinkApiKeys, showToast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (keyStatsPopup !== "debridlink") {
|
||||
setDebridLinkHostLimits({});
|
||||
setDebridLinkHostLimitsError("");
|
||||
setDebridLinkHostLimitsLoading(false);
|
||||
return;
|
||||
}
|
||||
void loadDebridLinkHostLimits(true);
|
||||
}, [keyStatsPopup, loadDebridLinkHostLimits]);
|
||||
|
||||
const clearImportQueueFocusListener = useCallback((): void => {
|
||||
const handler = importQueueFocusHandlerRef.current;
|
||||
if (!handler) {
|
||||
@@ -1764,7 +1949,7 @@ export function App(): ReactElement {
|
||||
continue;
|
||||
}
|
||||
const option = findAccountOption(kind);
|
||||
let statusLabel = "Konfiguriert";
|
||||
let statusLabel = "Aktiviert";
|
||||
let note = "";
|
||||
if (kind === "megadebrid-api") {
|
||||
note = "Nur API aktiv. Kein Web-Fallback.";
|
||||
@@ -1790,7 +1975,7 @@ export function App(): ReactElement {
|
||||
}
|
||||
if (kind === "debridlink-api") {
|
||||
const keyCount = parseDebridLinkApiKeys(settingsDraft.debridLinkApiKeys || "").length;
|
||||
statusLabel = keyCount > 1 ? `${keyCount} API-Keys` : "Konfiguriert";
|
||||
statusLabel = keyCount > 1 ? `${keyCount} API-Keys` : "Aktiviert";
|
||||
}
|
||||
const provider = getAccountServiceProvider(service);
|
||||
const dailyUsedBytes = getProviderDailyUsageBytes(snapshot.settings, provider);
|
||||
@@ -1816,7 +2001,9 @@ export function App(): ReactElement {
|
||||
return {
|
||||
id: key.id,
|
||||
label: key.label,
|
||||
token: key.token,
|
||||
masked: key.masked,
|
||||
disabled: (settingsDraft.debridLinkDisabledKeyIds || []).includes(key.id),
|
||||
dailyUsedBytes: keyDailyUsedBytes,
|
||||
dailyLimitBytes: keyDailyLimitBytes,
|
||||
dailyRemainingBytes: keyDailyRemainingBytes,
|
||||
@@ -1826,11 +2013,19 @@ export function App(): ReactElement {
|
||||
: [];
|
||||
if (kind === "debridlink-api" && debridLinkKeys.length > 0) {
|
||||
const limitedCount = debridLinkKeys.filter((entry) => entry.dailyLimitReached).length;
|
||||
const disabledKeyCount = debridLinkKeys.filter((entry) => entry.disabled).length;
|
||||
const keyNotes: string[] = [];
|
||||
if (limitedCount > 0) {
|
||||
const limitNote = `${limitedCount}/${debridLinkKeys.length} API-Keys am Limit.`;
|
||||
note = note ? `${limitNote} ${note}` : limitNote;
|
||||
keyNotes.push(`${limitedCount}/${debridLinkKeys.length} API-Keys am Limit.`);
|
||||
}
|
||||
if (limitedCount === debridLinkKeys.length) {
|
||||
if (disabledKeyCount > 0) {
|
||||
keyNotes.push(`${disabledKeyCount}/${debridLinkKeys.length} API-Keys deaktiviert.`);
|
||||
}
|
||||
if (keyNotes.length > 0) {
|
||||
const combinedKeyNote = keyNotes.join(" ");
|
||||
note = note ? `${combinedKeyNote} ${note}` : combinedKeyNote;
|
||||
}
|
||||
if (debridLinkKeys.every((entry) => entry.disabled || entry.dailyLimitReached)) {
|
||||
dailyLimitReached = true;
|
||||
}
|
||||
}
|
||||
@@ -2185,6 +2380,28 @@ export function App(): ReactElement {
|
||||
});
|
||||
};
|
||||
|
||||
const onToggleDebridLinkApiKeyEnabled = async (entry: ConfiguredAccountEntry, key: DebridLinkAccountKeyEntry): Promise<void> => {
|
||||
await performQuickAction(async () => {
|
||||
const currentDisabledIds = settingsDraft.debridLinkDisabledKeyIds || [];
|
||||
const nextDisabledIds = key.disabled
|
||||
? currentDisabledIds.filter((existingId) => existingId !== key.id)
|
||||
: [...currentDisabledIds, key.id];
|
||||
const nextDraft: AppSettings = {
|
||||
...settingsDraft,
|
||||
debridLinkDisabledKeyIds: nextDisabledIds
|
||||
};
|
||||
await persistSpecificSettings(nextDraft);
|
||||
showToast(
|
||||
key.disabled
|
||||
? `${entry.serviceLabel} ${key.label} aktiviert`
|
||||
: `${entry.serviceLabel} ${key.label} deaktiviert`,
|
||||
2200
|
||||
);
|
||||
}, (error) => {
|
||||
showToast(`${entry.serviceLabel} ${key.label}: Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
||||
});
|
||||
};
|
||||
|
||||
const onAccountRowQuickAction = async (entry: ConfiguredAccountEntry): Promise<void> => {
|
||||
const meta = getAccountQuickActionMeta(entry.kind);
|
||||
if (!meta) {
|
||||
@@ -4005,6 +4222,9 @@ export function App(): ReactElement {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="account-header-cell">
|
||||
<span>Info</span>
|
||||
</div>
|
||||
<div className="account-header-cell">
|
||||
<span>Zugang</span>
|
||||
<button
|
||||
@@ -4016,7 +4236,9 @@ export function App(): ReactElement {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span>Aktionen</span>
|
||||
<div className="account-header-cell">
|
||||
<span>Aktionen</span>
|
||||
</div>
|
||||
</div>
|
||||
{configuredAccounts.map((entry) => {
|
||||
const option = findAccountOption(entry.kind);
|
||||
@@ -4034,46 +4256,21 @@ export function App(): ReactElement {
|
||||
<span className="account-mode-pill">{entry.modeLabel}</span>
|
||||
</div>
|
||||
<div className="account-cell account-status-cell">
|
||||
<span className={`account-status-pill${allDebridStateClass}`}>{entry.statusLabel}</span>
|
||||
<span className={`account-status-pill${entry.disabled ? " account-status-disabled" : ""}${allDebridStateClass}`}>{entry.statusLabel}</span>
|
||||
{entry.note && <span className="account-note">{entry.note}</span>}
|
||||
<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>
|
||||
)}
|
||||
{entry.dailyLimitBytes <= 0 && entry.dailyLimitReached && entry.debridLinkKeys.length > 0 && (
|
||||
<span>Fallback aktiv</span>
|
||||
)}
|
||||
</div>
|
||||
{entry.debridLinkKeys.length > 0 && (
|
||||
<div className="account-subkey-list">
|
||||
{entry.debridLinkKeys.map((key) => (
|
||||
<div key={key.id} className={`account-subkey-row${key.dailyLimitReached ? " warning" : ""}`}>
|
||||
<div className="account-subkey-main">
|
||||
<div className="account-subkey-head">
|
||||
<strong>{key.label}</strong>
|
||||
<span>{key.masked}</span>
|
||||
</div>
|
||||
<div className="account-subkey-stats">
|
||||
<span>Heute: {humanSize(key.dailyUsedBytes)}</span>
|
||||
<span>{key.dailyLimitBytes > 0 ? `Limit: ${humanSize(key.dailyLimitBytes)}` : "Kein Limit"}</span>
|
||||
{key.dailyLimitBytes > 0 && (
|
||||
<span>{key.dailyLimitReached ? "Fallback aktiv" : `Rest: ${humanSize(key.dailyRemainingBytes || 0)}`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="account-subkey-actions">
|
||||
<button
|
||||
className="btn"
|
||||
disabled={actionBusy || key.dailyUsedBytes <= 0}
|
||||
onClick={() => { void onResetDebridLinkApiKeyDailyUsage(entry, key.id, key.label); }}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="account-cell account-info-cell">
|
||||
{entry.debridLinkKeys.length > 0 ? (
|
||||
<button className="btn btn-sm" onClick={() => setKeyStatsPopup(entry.service)}>
|
||||
Statistik
|
||||
</button>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
@@ -5016,6 +5213,91 @@ export function App(): ReactElement {
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{keyStatsPopup && (() => {
|
||||
const entry = configuredAccounts.find((a) => a.service === keyStatsPopup);
|
||||
if (!entry || entry.debridLinkKeys.length === 0) return null;
|
||||
const totalUsed = entry.debridLinkKeys.reduce((s, k) => s + k.dailyUsedBytes, 0);
|
||||
const limitedCount = entry.debridLinkKeys.filter((k) => k.dailyLimitReached).length;
|
||||
const disabledCount = entry.debridLinkKeys.filter((k) => k.disabled).length;
|
||||
const loadedQuotaCount = entry.debridLinkKeys.filter((k) => Boolean(debridLinkHostLimits[k.id])).length;
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={() => setKeyStatsPopup(null)}>
|
||||
<div className="modal-card key-stats-popup" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="key-stats-popup-header">
|
||||
<div>
|
||||
<h3>API-Key Statistik</h3>
|
||||
<p className="key-stats-summary">
|
||||
{entry.debridLinkKeys.length} Keys · Heute: {humanSize(totalUsed)}
|
||||
{limitedCount > 0 && <span className="key-stats-warn"> · {limitedCount} am Limit</span>}
|
||||
{disabledCount > 0 && <span className="key-stats-warn"> · {disabledCount} deaktiviert</span>}
|
||||
{debridLinkHostLimitsLoading && <span> · Rapidgator-Quota wird geladen ({loadedQuotaCount}/{entry.debridLinkKeys.length})</span>}
|
||||
{!debridLinkHostLimitsLoading && !debridLinkHostLimitsError && <span> · Rapidgator API-Quota</span>}
|
||||
{debridLinkHostLimitsError && <span className="key-stats-warn"> · API-Quota konnte nicht geladen werden</span>}
|
||||
</p>
|
||||
</div>
|
||||
<button className="update-popup-close" onClick={() => setKeyStatsPopup(null)}>×</button>
|
||||
</div>
|
||||
<div className="account-subkey-table">
|
||||
<div className="account-subkey-table-head">
|
||||
<span className="col-key">#</span>
|
||||
<span className="col-masked">Key</span>
|
||||
<span className="col-usage">Heute</span>
|
||||
<span className="col-limit">Lokal</span>
|
||||
<span className="col-traffic">RG Traffic</span>
|
||||
<span className="col-links">RG Links</span>
|
||||
<span className="col-action"></span>
|
||||
</div>
|
||||
{entry.debridLinkKeys.map((key, ki) => (
|
||||
<div key={key.id} className={`account-subkey-table-row${key.dailyLimitReached ? " warning" : ""}${key.disabled ? " disabled" : ""}`}>
|
||||
{(() => {
|
||||
const hostInfo = debridLinkHostLimits[key.id];
|
||||
return (
|
||||
<>
|
||||
<span className="col-key">{ki + 1}</span>
|
||||
<span
|
||||
className="col-masked link-popup-click"
|
||||
title={`${key.masked}\nKlicken zum Kopieren`}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(key.token)
|
||||
.then(() => showToast(`${key.label} kopiert`, 1800))
|
||||
.catch(() => showToast("Kopieren fehlgeschlagen", 2200));
|
||||
}}
|
||||
>
|
||||
{key.masked}
|
||||
</span>
|
||||
<span className="col-usage">{humanSize(key.dailyUsedBytes)}</span>
|
||||
<span className="col-limit">{key.disabled ? "Deaktiviert" : key.dailyLimitBytes > 0 ? humanSize(key.dailyLimitBytes) : "Kein Limit"}</span>
|
||||
<span className="col-traffic" title={hostInfo?.note || ""}>{formatDebridLinkTraffic(hostInfo)}</span>
|
||||
<span className="col-links" title={hostInfo?.note || ""}>{formatDebridLinkCountQuota(hostInfo)}</span>
|
||||
<span className="col-action">
|
||||
<button
|
||||
className={`btn btn-sm ${key.disabled ? "success" : "danger"}`}
|
||||
disabled={actionBusy}
|
||||
onClick={() => { void onToggleDebridLinkApiKeyEnabled(entry, key); }}
|
||||
>
|
||||
{key.disabled ? "Aktivieren" : "Deaktivieren"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={actionBusy || key.dailyUsedBytes <= 0}
|
||||
onClick={() => { void onResetDebridLinkApiKeyDailyUsage(entry, key.id, key.label); }}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button className="btn" onClick={() => setKeyStatsPopup(null)}>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{linkPopup && (
|
||||
<div className="modal-backdrop" onClick={() => setLinkPopup(null)}>
|
||||
<div className="modal-card link-popup" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
Reference in New Issue
Block a user