feat: preserve speed history and improve account setup

This commit is contained in:
Sucukdeluxe
2026-08-09 14:59:52 +02:00
parent 403051e3cf
commit 29bdf589eb
13 changed files with 279 additions and 132 deletions
+11 -3
View File
@@ -6,9 +6,17 @@ Desktop downloader for Windows with package-based queue management, multi-provid
![Electron](https://img.shields.io/badge/Electron-31.x-47848F)
![React](https://img.shields.io/badge/React-18.x-149ECA)
![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6)
![License](https://img.shields.io/badge/license-MIT-green)
## Why this tool?
![License](https://img.shields.io/badge/license-MIT-green)
## Changelog
### 2.0.6
- Keeps the download speed history alive when switching between tabs.
- Adds API and Web-Login filters to the account picker.
- Uses clearer Web-Login account labels and validates supported credentials before saving.
## Why this tool?
- JDownloader-style workflow with packages, progress, extraction, history, and clean post-processing.
- Multiple debrid accounts in one app, including provider order, automatic fallback, and per-hoster routing.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "real-debrid-downloader",
"version": "2.0.5",
"version": "2.0.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "real-debrid-downloader",
"version": "2.0.5",
"version": "2.0.6",
"license": "MIT",
"dependencies": {
"adm-zip": "0.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "real-debrid-downloader",
"version": "2.0.5",
"version": "2.0.6",
"description": "Desktop downloader",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",
+12 -10
View File
@@ -546,16 +546,18 @@ export class AppController {
return fetchDebridLinkHostLimits(this.settings.debridLinkApiKeys, host);
}
public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
const statuses = await checkAllDebridAccounts(this.settings);
this.manager.applyDebridAccountStatuses(statuses);
this.audit("INFO", "Debrid-Accounts geprueft", {
total: statuses.length,
valid: statuses.filter((s) => s.valid).length,
premium: statuses.filter((s) => s.isPremium).length
});
return statuses;
}
public async checkDebridAccounts(settingsOverride?: AppSettings): Promise<DebridAccountStatus[]> {
const statuses = await checkAllDebridAccounts(settingsOverride ? normalizeSettings(settingsOverride) : this.settings);
if (!settingsOverride) {
this.manager.applyDebridAccountStatuses(statuses);
this.audit("INFO", "Debrid-Accounts geprueft", {
total: statuses.length,
valid: statuses.filter((s) => s.valid).length,
premium: statuses.filter((s) => s.isPremium).length
});
}
return statuses;
}
public async checkSingleMegaDebridAccount(login: string, password: string): Promise<DebridAccountStatus | null> {
const entry = parseMegaDebridAccounts(`${login.trim()}:${password.trim()}`)[0];
+3 -3
View File
@@ -747,9 +747,9 @@ function registerIpcHandlers(): void {
return controller.getDebridLinkHostLimits();
});
ipcMain.handle(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async () => {
return controller.checkDebridAccounts();
});
ipcMain.handle(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async (_event, settings?: AppSettings) => {
return controller.checkDebridAccounts(settings);
});
ipcMain.handle(IPC_CHANNELS.CHECK_MEGA_DEBRID_ACCOUNT, async (_event, login: string, password: string) => {
return controller.checkSingleMegaDebridAccount(String(login || ""), String(password || ""));
+1 -1
View File
@@ -87,7 +87,7 @@ const api: ElectronApi = {
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
getDebridLinkHostLimits: (): Promise<DebridLinkHostLimitInfo[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS),
checkDebridAccounts: (): Promise<DebridAccountStatus[]> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS),
checkDebridAccounts: (settings?: AppSettings): Promise<DebridAccountStatus[]> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, settings),
checkMegaDebridAccount: (login: string, password: string): Promise<DebridAccountStatus | null> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_MEGA_DEBRID_ACCOUNT, login, password),
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
+124 -92
View File
@@ -36,8 +36,12 @@ import {
getProviderDailyUsageBytes,
getProviderUsageDayKey
} from "../shared/provider-daily-limits";
import { reorderPackageOrderByDrop, sortPackageOrderByName, sortPackagesForDisplay } from "./package-order";
import { pruneSelection } from "./selection";
import { reorderPackageOrderByDrop, sortPackageOrderByName, sortPackagesForDisplay } from "./package-order";
import { pruneSelection } from "./selection";
import { matchesAccountModeFilter } from "./account-ui";
import type { AccountModeFilter } from "./account-ui";
import { DOWNLOAD_SPEED_MAX_SAMPLES, updateDownloadSpeedHistory } from "./download-speed-state";
import type { DownloadSpeedHistoryState } from "./download-speed-state";
type Tab = "collector" | "downloads" | "history" | "statistics" | "settings";
type SettingsSubTab = "allgemein" | "accounts" | "entpacken" | "geschwindigkeit" | "bereinigung" | "updates";
@@ -245,8 +249,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
kind: "realdebrid-web",
service: "realdebrid",
serviceLabel: "Real-Debrid",
title: "Real-Debrid Web",
modeLabel: "Web",
title: "Real-Debrid Web-Login",
modeLabel: "Web-Login",
pickerDescription: "Login über Browserfenster statt Token."
},
{
@@ -262,8 +266,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
kind: "megadebrid-web",
service: "megadebrid-web",
serviceLabel: "Mega-Debrid",
title: "Mega-Debrid Web",
modeLabel: "Web",
title: "Mega-Debrid Web-Login",
modeLabel: "Web-Login",
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account.",
needsToken: true
},
@@ -280,8 +284,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
kind: "bestdebrid-web",
service: "bestdebrid",
serviceLabel: "BestDebrid",
title: "BestDebrid Web",
modeLabel: "Web",
title: "BestDebrid Web-Login",
modeLabel: "Web-Login",
pickerDescription: "Cookie-Import aus dem Browser statt API-Token."
},
{
@@ -297,8 +301,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
kind: "alldebrid-web",
service: "alldebrid",
serviceLabel: "AllDebrid",
title: "AllDebrid Web",
modeLabel: "Web",
title: "AllDebrid Web-Login",
modeLabel: "Web-Login",
pickerDescription: "Login über Browserfenster für reCAPTCHA.",
},
{
@@ -332,8 +336,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
kind: "linksnappy-login",
service: "linksnappy",
serviceLabel: "LinkSnappy",
title: "LinkSnappy Web",
modeLabel: "Web",
title: "LinkSnappy Web-Login",
modeLabel: "Web-Login",
pickerDescription: "Login für linksnappy.com mit Benutzername und Passwort.",
needsCredentials: true
}
@@ -802,8 +806,11 @@ function validateAccountDialog(dialog: AccountDialogState): string | null {
return `${option.title}: Tageslimit muss eine Zahl >= 0 sein.`;
}
}
if (dialog.kind === "debridlink-api") {
for (const key of parseDebridLinkApiKeys(dialog.token)) {
if (dialog.kind === "debridlink-api") {
if (parseDebridLinkApiKeys(dialog.token).length === 0) {
return `${option.title}: Bitte mindestens einen gültigen API-Key eintragen.`;
}
for (const key of parseDebridLinkApiKeys(dialog.token)) {
const raw = dialog.keyDailyLimitGbById?.[key.id] || "";
if (!raw.trim()) {
continue;
@@ -1482,20 +1489,18 @@ const BandwidthChart = memo(function BandwidthChart({ items, running, paused, sp
);
});
interface DownloadSpeedSparklineProps {
items: Record<string, DownloadItem>;
running: boolean;
paused: boolean;
}
const SPARKLINE_MAX_SAMPLES = 160;
const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, running, paused }: DownloadSpeedSparklineProps): ReactElement {
const canvasRef = useRef<HTMLCanvasElement>(null);
const histRef = useRef<number[]>([]);
const displayRef = useRef<number>(0);
const itemsRef = useRef(items);
const activeRef = useRef(running && !paused);
interface DownloadSpeedSparklineProps {
items: Record<string, DownloadItem>;
running: boolean;
paused: boolean;
speedStateRef: React.MutableRefObject<DownloadSpeedHistoryState>;
hidden?: boolean;
}
const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, running, paused, speedStateRef, hidden = false }: DownloadSpeedSparklineProps): ReactElement {
const canvasRef = useRef<HTMLCanvasElement>(null);
const itemsRef = useRef(items);
const activeRef = useRef(running && !paused);
const [liveSpeed, setLiveSpeed] = useState(0);
itemsRef.current = items;
activeRef.current = running && !paused;
@@ -1515,7 +1520,7 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, cssW, cssH);
const hist = histRef.current;
const hist = speedStateRef.current.history;
if (hist.length < 2) return;
const isDark = document.documentElement.getAttribute("data-theme") !== "light";
@@ -1528,8 +1533,8 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run
const pad = 2;
const h = cssH - pad * 2;
const step = cssW / (SPARKLINE_MAX_SAMPLES - 1);
const startIdx = SPARKLINE_MAX_SAMPLES - hist.length;
const step = cssW / (DOWNLOAD_SPEED_MAX_SAMPLES - 1);
const startIdx = DOWNLOAD_SPEED_MAX_SAMPLES - hist.length;
const px = (i: number): number => (startIdx + i) * step;
const py = (v: number): number => pad + h - (v / maxV) * h;
@@ -1558,24 +1563,17 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run
if (it.status === "downloading") target += it.speedBps || 0;
}
}
const cur = displayRef.current;
const alpha = target > cur ? 0.45 : 0.12;
let next = cur + (target - cur) * alpha;
if (next < 1) next = 0;
displayRef.current = next;
const hist = histRef.current;
hist.push(next);
if (hist.length > SPARKLINE_MAX_SAMPLES) hist.splice(0, hist.length - SPARKLINE_MAX_SAMPLES);
setLiveSpeed(target);
draw();
speedStateRef.current = updateDownloadSpeedHistory(speedStateRef.current, target);
setLiveSpeed(target);
draw();
};
const id = window.setInterval(tick, 250);
return () => window.clearInterval(id);
}, []);
return (
<div className="speed-sparkline" title="Aktuelle Download-Geschwindigkeit (geglättet)">
return (
<div className={`speed-sparkline${hidden ? " speed-sparkline-hidden" : ""}`} aria-hidden={hidden} title="Aktuelle Download-Geschwindigkeit (geglättet)">
<canvas ref={canvasRef} className="speed-sparkline-canvas" />
<span className="speed-sparkline-value">{liveSpeed > 0 ? formatSpeedMbps(liveSpeed) : "0 B/s"}</span>
</div>
@@ -1783,9 +1781,10 @@ export function App(): ReactElement {
const importQueueFocusHandlerRef = useRef<(() => void) | null>(null);
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const ctxMenuRef = useRef<HTMLDivElement>(null);
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
const [accountDialogSearch, setAccountDialogSearch] = useState("");
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
const [accountDialogSearch, setAccountDialogSearch] = useState("");
const [accountDialogModeFilter, setAccountDialogModeFilter] = useState<AccountModeFilter>("all");
const [keyStatsPopup, setKeyStatsPopup] = useState<string | null>(null);
const [debridLinkHostLimits, setDebridLinkHostLimits] = useState<Record<string, DebridLinkHostLimitInfo>>({});
const [debridLinkHostLimitsLoading, setDebridLinkHostLimitsLoading] = useState(false);
@@ -2766,12 +2765,15 @@ export function App(): ReactElement {
}
return availableAccountOptions;
}, [accountDialog, accountDialogOption, availableAccountOptions]);
const accountDialogSearchQuery = accountDialogSearch.trim().toLowerCase();
const filteredAccountDialogOptions = useMemo(() => (
accountDialogSelectableOptions.filter((option) => {
if (!accountDialogSearchQuery) {
return true;
}
const accountDialogSearchQuery = accountDialogSearch.trim().toLowerCase();
const filteredAccountDialogOptions = useMemo(() => (
accountDialogSelectableOptions.filter((option) => {
if (!matchesAccountModeFilter(option, accountDialogModeFilter)) {
return false;
}
if (!accountDialogSearchQuery) {
return true;
}
const haystack = [
option.title,
option.serviceLabel,
@@ -2781,7 +2783,7 @@ export function App(): ReactElement {
].join(" ").toLowerCase();
return haystack.includes(accountDialogSearchQuery);
})
), [accountDialogSearchQuery, accountDialogSelectableOptions]);
), [accountDialogModeFilter, accountDialogSearchQuery, accountDialogSelectableOptions]);
const accountTableStyle = useMemo(() => ({
"--account-col-service": `${accountColumnWidths.service}px`,
"--account-col-mode": `${accountColumnWidths.mode}px`,
@@ -3000,15 +3002,17 @@ export function App(): ReactElement {
}
}, [showToast]);
const openCreateAccountDialog = (): void => {
setAccountDialogSearch("");
setAccountDialog(createAccountDialogState("create", null, settingsDraft));
};
const openEditAccountDialog = (kind: AccountKind): void => {
setAccountDialogSearch("");
setAccountDialog(createAccountDialogState("edit", kind, settingsDraft));
};
const openCreateAccountDialog = (): void => {
setAccountDialogSearch("");
setAccountDialogModeFilter("all");
setAccountDialog(createAccountDialogState("create", null, settingsDraft));
};
const openEditAccountDialog = (kind: AccountKind): void => {
setAccountDialogSearch("");
setAccountDialogModeFilter("all");
setAccountDialog(createAccountDialogState("edit", kind, settingsDraft));
};
const updateAccountDialogKind = (kind: AccountKind): void => {
setAccountDialog((prev) => {
@@ -3033,10 +3037,11 @@ export function App(): ReactElement {
});
};
const closeAccountDialog = useCallback((): void => {
setAccountDialog(null);
setAccountDialogSearch("");
}, []);
const closeAccountDialog = useCallback((): void => {
setAccountDialog(null);
setAccountDialogSearch("");
setAccountDialogModeFilter("all");
}, []);
const onSaveAccountDialog = async (quickAction?: AccountQuickAction): Promise<void> => {
if (!accountDialog) {
@@ -3048,10 +3053,24 @@ export function App(): ReactElement {
return;
}
const dialogSnapshot = accountDialog;
const selectedOption = dialogSnapshot.kind ? findAccountOption(dialogSnapshot.kind) : null;
await performQuickAction(async () => {
const nextDraft = applyAccountDialogToSettings(settingsDraft, dialogSnapshot);
await persistSpecificSettings(nextDraft);
const selectedOption = dialogSnapshot.kind ? findAccountOption(dialogSnapshot.kind) : null;
await performQuickAction(async () => {
const nextDraft = applyAccountDialogToSettings(settingsDraft, dialogSnapshot);
const requiresCredentialCheck = selectedOption?.kind === "megadebrid-api"
|| selectedOption?.kind === "megadebrid-web"
|| selectedOption?.kind === "debridlink-api";
if (requiresCredentialCheck) {
const statuses = await window.rd.checkDebridAccounts(nextDraft);
const invalidStatuses = statuses.filter((status) => !status.valid);
if (invalidStatuses.length > 0) {
const details = invalidStatuses
.map((status) => status.message || "Zugangsdaten ungültig")
.join(" | ");
showToast(`Prüfung fehlgeschlagen: ${details}`, 4200);
return;
}
}
await persistSpecificSettings(nextDraft);
closeAccountDialog();
if (quickAction) {
await runAccountQuickAction(quickAction);
@@ -3916,7 +3935,8 @@ export function App(): ReactElement {
setContextMenu({ x, y, packageId, itemId });
}, []);
const speedHistoryRef = useRef<{ time: number; speed: number }[]>([]);
const speedHistoryRef = useRef<{ time: number; speed: number }[]>([]);
const speedSparklineStateRef = useRef<DownloadSpeedHistoryState>({ history: [], display: 0 });
const dragSelectRef = useRef(false);
const dragAnchorRef = useRef<string | null>(null);
const dragDidMoveRef = useRef(false);
@@ -4998,14 +5018,14 @@ export function App(): ReactElement {
<button className={tab === "settings" ? "tab active" : "tab"} onClick={() => setTab("settings")}>Einstellungen</button>
<button className={tab === "history" ? "tab active" : "tab"} onClick={() => setTab("history")}>Verlauf</button>
<button className={tab === "statistics" ? "tab active" : "tab"} onClick={() => setTab("statistics")}>Statistiken</button>
<div className="tab-actions">
{tab === "downloads" && (
<DownloadSpeedSparkline
items={snapshot.session.items}
running={snapshot.session.running}
paused={snapshot.session.paused}
/>
)}
<div className="tab-actions">
<DownloadSpeedSparkline
items={snapshot.session.items}
running={snapshot.session.running}
paused={snapshot.session.paused}
speedStateRef={speedSparklineStateRef}
hidden={tab !== "downloads"}
/>
{tab === "downloads" && (
<input
className="search-input tab-search"
@@ -5549,9 +5569,9 @@ export function App(): ReactElement {
<button className="btn" disabled={actionBusy || accountCheckBusy} onClick={() => { void checkAllAccounts(); }} title="Prüft Login-Gültigkeit und Premium-Restlaufzeit aller Mega-Debrid-/Debrid-Link-Accounts">
{accountCheckBusy ? "Prüfe Accounts…" : "Alle prüfen"}
</button>
<button className="btn accent" disabled={actionBusy || availableAccountOptions.length === 0} onClick={openCreateAccountDialog}>
Account hinzufügen
</button>
<button className="btn account-add-button" disabled={actionBusy || availableAccountOptions.length === 0} onClick={openCreateAccountDialog}>
Account hinzufügen
</button>
</div>
</div>
@@ -6300,12 +6320,24 @@ export function App(): ReactElement {
<div className="account-modal-body">
<div className="account-dialog-step">
<div className="account-dialog-step-label">1. Account-Typ auswaehlen</div>
<input
className="account-picker-search"
placeholder="Dienst oder Typ suchen"
value={accountDialogSearch}
onChange={(event) => setAccountDialogSearch(event.target.value)}
/>
<div className="account-picker-toolbar">
<select
className="account-picker-filter"
aria-label="Account-Typ filtern"
value={accountDialogModeFilter}
onChange={(event) => setAccountDialogModeFilter(event.target.value as AccountModeFilter)}
>
<option value="all">Alle anzeigen</option>
<option value="api">Nur API</option>
<option value="web">Nur Web-Login</option>
</select>
<input
className="account-picker-search"
placeholder="Dienst oder Typ suchen"
value={accountDialogSearch}
onChange={(event) => setAccountDialogSearch(event.target.value)}
/>
</div>
<div className="account-picker-table">
<div className="account-picker-head">
<span>Account</span>
@@ -6502,7 +6534,7 @@ export function App(): ReactElement {
value={accountDialog.dailyLimitGb}
onChange={(event) => setAccountDialog((prev) => prev ? { ...prev, dailyLimitGb: event.target.value } : prev)}
/>
<div className="account-modal-note">Ab 00:00 wird der Zähler automatisch zurückgesetzt. Wenn das Limit erreicht ist, nutzt die App den nächsten Hoster aus der Reihenfolge.</div>
<div className="account-modal-note account-daily-limit-note">Ab 00:00 wird der Zähler automatisch zurückgesetzt. Wenn das Limit erreicht ist, nutzt die App den nächsten Hoster aus der Reihenfolge.</div>
</div>
{accountDialog.kind === "debridlink-api" && parseDebridLinkApiKeys(accountDialog.token).length > 0 && (
@@ -6597,9 +6629,9 @@ export function App(): ReactElement {
Speichern + Login
</button>
)}
<button className="btn accent" disabled={actionBusy || !accountDialog.kind} onClick={() => { void onSaveAccountDialog(); }}>
Speichern
</button>
<button className="btn accent" disabled={actionBusy || !accountDialog.kind} onClick={() => { void onSaveAccountDialog(); }}>
Prüfen und speichern
</button>
</div>
</>
)}
+15
View File
@@ -0,0 +1,15 @@
export type AccountModeFilter = "all" | "api" | "web";
export interface AccountModeOption {
modeLabel: string;
}
export function matchesAccountModeFilter(option: AccountModeOption, filter: AccountModeFilter): boolean {
if (filter === "all") {
return true;
}
if (filter === "api") {
return option.modeLabel === "API";
}
return option.modeLabel.startsWith("Web");
}
+24
View File
@@ -0,0 +1,24 @@
export interface DownloadSpeedHistoryState {
history: number[];
display: number;
}
export const DOWNLOAD_SPEED_MAX_SAMPLES = 160;
export function updateDownloadSpeedHistory(
state: DownloadSpeedHistoryState,
target: number,
maxSamples = DOWNLOAD_SPEED_MAX_SAMPLES
): DownloadSpeedHistoryState {
const safeTarget = Math.max(0, Number.isFinite(target) ? target : 0);
const alpha = safeTarget > state.display ? 0.45 : 0.12;
let display = state.display + (safeTarget - state.display) * alpha;
if (display < 1) {
display = 0;
}
const history = [...state.history, display];
if (history.length > maxSamples) {
history.splice(0, history.length - maxSamples);
}
return { history, display };
}
+49 -19
View File
@@ -469,11 +469,22 @@ body,
background: var(--button-bg-hover);
}
.btn.accent {
background: linear-gradient(180deg, #f9a948, #e9881f);
color: #1c1206;
border-color: #f0982f;
}
.btn.accent {
background: linear-gradient(180deg, #f9a948, #e9881f);
color: #1c1206;
border-color: #f0982f;
}
.btn.account-add-button {
background: linear-gradient(180deg, #63b3ed, #3182ce);
color: #061424;
border-color: #63b3ed;
}
.btn.account-add-button:hover:not(:disabled) {
background: linear-gradient(180deg, #7cc4f4, #3b8ed8);
border-color: #90cdf4;
}
.btn.danger {
border-color: rgba(244, 63, 94, 0.7);
@@ -536,7 +547,7 @@ body,
z-index: 1;
}
.speed-sparkline {
.speed-sparkline {
display: flex;
align-items: center;
gap: 8px;
@@ -544,8 +555,13 @@ body,
padding: 0 10px;
background: var(--field);
border: 1px solid var(--border);
border-radius: 8px;
}
border-radius: 8px;
}
.speed-sparkline-hidden {
visibility: hidden;
pointer-events: none;
}
.speed-sparkline-canvas {
width: 116px;
@@ -1921,9 +1937,15 @@ body,
padding-right: 4px;
}
.account-picker-search {
width: 100%;
}
.account-picker-search {
width: 100%;
}
.account-picker-toolbar {
display: grid;
grid-template-columns: minmax(145px, 0.32fr) minmax(0, 1fr);
gap: 8px;
}
.account-modal label {
display: block;
@@ -2078,15 +2100,19 @@ body,
gap: 12px;
}
.account-modal-note {
.account-modal-note {
padding: 12px 14px;
border-radius: 12px;
border: 1px solid color-mix(in srgb, var(--accent) 26%, transparent);
background: color-mix(in srgb, var(--accent) 8%, transparent);
color: var(--text);
font-size: 13px;
line-height: 1.5;
}
line-height: 1.5;
}
.account-daily-limit-note {
margin-top: 10px;
}
.account-mode-note {
padding: 10px 12px;
@@ -3227,11 +3253,15 @@ td {
gap: 2px;
}
.account-picker-list {
grid-template-columns: 1fr;
}
.account-picker-head,
.account-picker-list {
grid-template-columns: 1fr;
}
.account-picker-toolbar {
grid-template-columns: 1fr;
}
.account-picker-head,
.account-picker-row {
grid-template-columns: 1fr;
}
+1 -1
View File
@@ -84,7 +84,7 @@ export interface ElectronApi {
importBestDebridCookies: () => Promise<number>;
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
getDebridLinkHostLimits: () => Promise<DebridLinkHostLimitInfo[]>;
checkDebridAccounts: () => Promise<DebridAccountStatus[]>;
checkDebridAccounts: (settings?: AppSettings) => Promise<DebridAccountStatus[]>;
checkMegaDebridAccount: (login: string, password: string) => Promise<DebridAccountStatus | null>;
retryExtraction: (packageId: string) => Promise<void>;
extractNow: (packageId: string) => Promise<void>;
+14
View File
@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { matchesAccountModeFilter } from "../src/renderer/account-ui";
describe("account mode filter", () => {
it("shows only API options for the API filter", () => {
expect(matchesAccountModeFilter({ modeLabel: "API" }, "api")).toBe(true);
expect(matchesAccountModeFilter({ modeLabel: "Web-Login" }, "api")).toBe(false);
});
it("shows Web-Login options for the Web filter", () => {
expect(matchesAccountModeFilter({ modeLabel: "Web-Login" }, "web")).toBe(true);
expect(matchesAccountModeFilter({ modeLabel: "API" }, "web")).toBe(false);
});
});
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { updateDownloadSpeedHistory } from "../src/renderer/download-speed-state";
describe("download speed history", () => {
it("keeps the smoothed display and history across successive updates", () => {
const first = updateDownloadSpeedHistory({ history: [], display: 0 }, 100, 10);
const second = updateDownloadSpeedHistory(first, 100, 10);
expect(second.display).toBeGreaterThan(first.display);
expect(second.history).toHaveLength(2);
expect(second.history[0]).toBe(first.display);
});
it("keeps only the configured number of samples", () => {
let state = { history: [] as number[], display: 0 };
for (let index = 0; index < 5; index += 1) {
state = updateDownloadSpeedHistory(state, index + 1, 3);
}
expect(state.history).toHaveLength(3);
});
});