feat: preserve speed history and improve account setup
This commit is contained in:
@@ -8,6 +8,14 @@ Desktop downloader for Windows with package-based queue management, multi-provid
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
## 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?
|
## Why this tool?
|
||||||
|
|
||||||
- JDownloader-style workflow with packages, progress, extraction, history, and clean post-processing.
|
- JDownloader-style workflow with packages, progress, extraction, history, and clean post-processing.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.5",
|
"version": "2.0.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.5",
|
"version": "2.0.6",
|
||||||
"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.5",
|
"version": "2.0.6",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@@ -546,14 +546,16 @@ export class AppController {
|
|||||||
return fetchDebridLinkHostLimits(this.settings.debridLinkApiKeys, host);
|
return fetchDebridLinkHostLimits(this.settings.debridLinkApiKeys, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
public async checkDebridAccounts(settingsOverride?: AppSettings): Promise<DebridAccountStatus[]> {
|
||||||
const statuses = await checkAllDebridAccounts(this.settings);
|
const statuses = await checkAllDebridAccounts(settingsOverride ? normalizeSettings(settingsOverride) : this.settings);
|
||||||
this.manager.applyDebridAccountStatuses(statuses);
|
if (!settingsOverride) {
|
||||||
this.audit("INFO", "Debrid-Accounts geprueft", {
|
this.manager.applyDebridAccountStatuses(statuses);
|
||||||
total: statuses.length,
|
this.audit("INFO", "Debrid-Accounts geprueft", {
|
||||||
valid: statuses.filter((s) => s.valid).length,
|
total: statuses.length,
|
||||||
premium: statuses.filter((s) => s.isPremium).length
|
valid: statuses.filter((s) => s.valid).length,
|
||||||
});
|
premium: statuses.filter((s) => s.isPremium).length
|
||||||
|
});
|
||||||
|
}
|
||||||
return statuses;
|
return statuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -747,8 +747,8 @@ function registerIpcHandlers(): void {
|
|||||||
return controller.getDebridLinkHostLimits();
|
return controller.getDebridLinkHostLimits();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async () => {
|
ipcMain.handle(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async (_event, settings?: AppSettings) => {
|
||||||
return controller.checkDebridAccounts();
|
return controller.checkDebridAccounts(settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.CHECK_MEGA_DEBRID_ACCOUNT, async (_event, login: string, password: string) => {
|
ipcMain.handle(IPC_CHANNELS.CHECK_MEGA_DEBRID_ACCOUNT, async (_event, login: string, password: string) => {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const api: ElectronApi = {
|
|||||||
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
||||||
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
|
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
|
||||||
getDebridLinkHostLimits: (): Promise<DebridLinkHostLimitInfo[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS),
|
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),
|
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),
|
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
|
||||||
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
|
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
|
||||||
|
|||||||
+76
-44
@@ -38,6 +38,10 @@ import {
|
|||||||
} from "../shared/provider-daily-limits";
|
} from "../shared/provider-daily-limits";
|
||||||
import { reorderPackageOrderByDrop, sortPackageOrderByName, sortPackagesForDisplay } from "./package-order";
|
import { reorderPackageOrderByDrop, sortPackageOrderByName, sortPackagesForDisplay } from "./package-order";
|
||||||
import { pruneSelection } from "./selection";
|
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 Tab = "collector" | "downloads" | "history" | "statistics" | "settings";
|
||||||
type SettingsSubTab = "allgemein" | "accounts" | "entpacken" | "geschwindigkeit" | "bereinigung" | "updates";
|
type SettingsSubTab = "allgemein" | "accounts" | "entpacken" | "geschwindigkeit" | "bereinigung" | "updates";
|
||||||
@@ -245,8 +249,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
|
|||||||
kind: "realdebrid-web",
|
kind: "realdebrid-web",
|
||||||
service: "realdebrid",
|
service: "realdebrid",
|
||||||
serviceLabel: "Real-Debrid",
|
serviceLabel: "Real-Debrid",
|
||||||
title: "Real-Debrid Web",
|
title: "Real-Debrid Web-Login",
|
||||||
modeLabel: "Web",
|
modeLabel: "Web-Login",
|
||||||
pickerDescription: "Login über Browserfenster statt Token."
|
pickerDescription: "Login über Browserfenster statt Token."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -262,8 +266,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
|
|||||||
kind: "megadebrid-web",
|
kind: "megadebrid-web",
|
||||||
service: "megadebrid-web",
|
service: "megadebrid-web",
|
||||||
serviceLabel: "Mega-Debrid",
|
serviceLabel: "Mega-Debrid",
|
||||||
title: "Mega-Debrid Web",
|
title: "Mega-Debrid Web-Login",
|
||||||
modeLabel: "Web",
|
modeLabel: "Web-Login",
|
||||||
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account.",
|
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account.",
|
||||||
needsToken: true
|
needsToken: true
|
||||||
},
|
},
|
||||||
@@ -280,8 +284,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
|
|||||||
kind: "bestdebrid-web",
|
kind: "bestdebrid-web",
|
||||||
service: "bestdebrid",
|
service: "bestdebrid",
|
||||||
serviceLabel: "BestDebrid",
|
serviceLabel: "BestDebrid",
|
||||||
title: "BestDebrid Web",
|
title: "BestDebrid Web-Login",
|
||||||
modeLabel: "Web",
|
modeLabel: "Web-Login",
|
||||||
pickerDescription: "Cookie-Import aus dem Browser statt API-Token."
|
pickerDescription: "Cookie-Import aus dem Browser statt API-Token."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -297,8 +301,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
|
|||||||
kind: "alldebrid-web",
|
kind: "alldebrid-web",
|
||||||
service: "alldebrid",
|
service: "alldebrid",
|
||||||
serviceLabel: "AllDebrid",
|
serviceLabel: "AllDebrid",
|
||||||
title: "AllDebrid Web",
|
title: "AllDebrid Web-Login",
|
||||||
modeLabel: "Web",
|
modeLabel: "Web-Login",
|
||||||
pickerDescription: "Login über Browserfenster für reCAPTCHA.",
|
pickerDescription: "Login über Browserfenster für reCAPTCHA.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -332,8 +336,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
|
|||||||
kind: "linksnappy-login",
|
kind: "linksnappy-login",
|
||||||
service: "linksnappy",
|
service: "linksnappy",
|
||||||
serviceLabel: "LinkSnappy",
|
serviceLabel: "LinkSnappy",
|
||||||
title: "LinkSnappy Web",
|
title: "LinkSnappy Web-Login",
|
||||||
modeLabel: "Web",
|
modeLabel: "Web-Login",
|
||||||
pickerDescription: "Login für linksnappy.com mit Benutzername und Passwort.",
|
pickerDescription: "Login für linksnappy.com mit Benutzername und Passwort.",
|
||||||
needsCredentials: true
|
needsCredentials: true
|
||||||
}
|
}
|
||||||
@@ -803,6 +807,9 @@ function validateAccountDialog(dialog: AccountDialogState): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dialog.kind === "debridlink-api") {
|
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)) {
|
for (const key of parseDebridLinkApiKeys(dialog.token)) {
|
||||||
const raw = dialog.keyDailyLimitGbById?.[key.id] || "";
|
const raw = dialog.keyDailyLimitGbById?.[key.id] || "";
|
||||||
if (!raw.trim()) {
|
if (!raw.trim()) {
|
||||||
@@ -1486,14 +1493,12 @@ interface DownloadSpeedSparklineProps {
|
|||||||
items: Record<string, DownloadItem>;
|
items: Record<string, DownloadItem>;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
|
speedStateRef: React.MutableRefObject<DownloadSpeedHistoryState>;
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SPARKLINE_MAX_SAMPLES = 160;
|
const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, running, paused, speedStateRef, hidden = false }: DownloadSpeedSparklineProps): ReactElement {
|
||||||
|
|
||||||
const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, running, paused }: DownloadSpeedSparklineProps): ReactElement {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const histRef = useRef<number[]>([]);
|
|
||||||
const displayRef = useRef<number>(0);
|
|
||||||
const itemsRef = useRef(items);
|
const itemsRef = useRef(items);
|
||||||
const activeRef = useRef(running && !paused);
|
const activeRef = useRef(running && !paused);
|
||||||
const [liveSpeed, setLiveSpeed] = useState(0);
|
const [liveSpeed, setLiveSpeed] = useState(0);
|
||||||
@@ -1515,7 +1520,7 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run
|
|||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
ctx.clearRect(0, 0, cssW, cssH);
|
ctx.clearRect(0, 0, cssW, cssH);
|
||||||
|
|
||||||
const hist = histRef.current;
|
const hist = speedStateRef.current.history;
|
||||||
if (hist.length < 2) return;
|
if (hist.length < 2) return;
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute("data-theme") !== "light";
|
const isDark = document.documentElement.getAttribute("data-theme") !== "light";
|
||||||
@@ -1528,8 +1533,8 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run
|
|||||||
|
|
||||||
const pad = 2;
|
const pad = 2;
|
||||||
const h = cssH - pad * 2;
|
const h = cssH - pad * 2;
|
||||||
const step = cssW / (SPARKLINE_MAX_SAMPLES - 1);
|
const step = cssW / (DOWNLOAD_SPEED_MAX_SAMPLES - 1);
|
||||||
const startIdx = SPARKLINE_MAX_SAMPLES - hist.length;
|
const startIdx = DOWNLOAD_SPEED_MAX_SAMPLES - hist.length;
|
||||||
const px = (i: number): number => (startIdx + i) * step;
|
const px = (i: number): number => (startIdx + i) * step;
|
||||||
const py = (v: number): number => pad + h - (v / maxV) * h;
|
const py = (v: number): number => pad + h - (v / maxV) * h;
|
||||||
|
|
||||||
@@ -1558,14 +1563,7 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run
|
|||||||
if (it.status === "downloading") target += it.speedBps || 0;
|
if (it.status === "downloading") target += it.speedBps || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const cur = displayRef.current;
|
speedStateRef.current = updateDownloadSpeedHistory(speedStateRef.current, target);
|
||||||
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);
|
setLiveSpeed(target);
|
||||||
draw();
|
draw();
|
||||||
};
|
};
|
||||||
@@ -1575,7 +1573,7 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="speed-sparkline" title="Aktuelle Download-Geschwindigkeit (geglättet)">
|
<div className={`speed-sparkline${hidden ? " speed-sparkline-hidden" : ""}`} aria-hidden={hidden} title="Aktuelle Download-Geschwindigkeit (geglättet)">
|
||||||
<canvas ref={canvasRef} className="speed-sparkline-canvas" />
|
<canvas ref={canvasRef} className="speed-sparkline-canvas" />
|
||||||
<span className="speed-sparkline-value">{liveSpeed > 0 ? formatSpeedMbps(liveSpeed) : "0 B/s"}</span>
|
<span className="speed-sparkline-value">{liveSpeed > 0 ? formatSpeedMbps(liveSpeed) : "0 B/s"}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1786,6 +1784,7 @@ export function App(): ReactElement {
|
|||||||
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("");
|
||||||
|
const [accountDialogModeFilter, setAccountDialogModeFilter] = useState<AccountModeFilter>("all");
|
||||||
const [keyStatsPopup, setKeyStatsPopup] = useState<string | null>(null);
|
const [keyStatsPopup, setKeyStatsPopup] = useState<string | null>(null);
|
||||||
const [debridLinkHostLimits, setDebridLinkHostLimits] = useState<Record<string, DebridLinkHostLimitInfo>>({});
|
const [debridLinkHostLimits, setDebridLinkHostLimits] = useState<Record<string, DebridLinkHostLimitInfo>>({});
|
||||||
const [debridLinkHostLimitsLoading, setDebridLinkHostLimitsLoading] = useState(false);
|
const [debridLinkHostLimitsLoading, setDebridLinkHostLimitsLoading] = useState(false);
|
||||||
@@ -2769,6 +2768,9 @@ export function App(): ReactElement {
|
|||||||
const accountDialogSearchQuery = accountDialogSearch.trim().toLowerCase();
|
const accountDialogSearchQuery = accountDialogSearch.trim().toLowerCase();
|
||||||
const filteredAccountDialogOptions = useMemo(() => (
|
const filteredAccountDialogOptions = useMemo(() => (
|
||||||
accountDialogSelectableOptions.filter((option) => {
|
accountDialogSelectableOptions.filter((option) => {
|
||||||
|
if (!matchesAccountModeFilter(option, accountDialogModeFilter)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!accountDialogSearchQuery) {
|
if (!accountDialogSearchQuery) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2781,7 +2783,7 @@ export function App(): ReactElement {
|
|||||||
].join(" ").toLowerCase();
|
].join(" ").toLowerCase();
|
||||||
return haystack.includes(accountDialogSearchQuery);
|
return haystack.includes(accountDialogSearchQuery);
|
||||||
})
|
})
|
||||||
), [accountDialogSearchQuery, accountDialogSelectableOptions]);
|
), [accountDialogModeFilter, accountDialogSearchQuery, accountDialogSelectableOptions]);
|
||||||
const accountTableStyle = useMemo(() => ({
|
const accountTableStyle = useMemo(() => ({
|
||||||
"--account-col-service": `${accountColumnWidths.service}px`,
|
"--account-col-service": `${accountColumnWidths.service}px`,
|
||||||
"--account-col-mode": `${accountColumnWidths.mode}px`,
|
"--account-col-mode": `${accountColumnWidths.mode}px`,
|
||||||
@@ -3002,11 +3004,13 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
const openCreateAccountDialog = (): void => {
|
const openCreateAccountDialog = (): void => {
|
||||||
setAccountDialogSearch("");
|
setAccountDialogSearch("");
|
||||||
|
setAccountDialogModeFilter("all");
|
||||||
setAccountDialog(createAccountDialogState("create", null, settingsDraft));
|
setAccountDialog(createAccountDialogState("create", null, settingsDraft));
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditAccountDialog = (kind: AccountKind): void => {
|
const openEditAccountDialog = (kind: AccountKind): void => {
|
||||||
setAccountDialogSearch("");
|
setAccountDialogSearch("");
|
||||||
|
setAccountDialogModeFilter("all");
|
||||||
setAccountDialog(createAccountDialogState("edit", kind, settingsDraft));
|
setAccountDialog(createAccountDialogState("edit", kind, settingsDraft));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3036,6 +3040,7 @@ export function App(): ReactElement {
|
|||||||
const closeAccountDialog = useCallback((): void => {
|
const closeAccountDialog = useCallback((): void => {
|
||||||
setAccountDialog(null);
|
setAccountDialog(null);
|
||||||
setAccountDialogSearch("");
|
setAccountDialogSearch("");
|
||||||
|
setAccountDialogModeFilter("all");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSaveAccountDialog = async (quickAction?: AccountQuickAction): Promise<void> => {
|
const onSaveAccountDialog = async (quickAction?: AccountQuickAction): Promise<void> => {
|
||||||
@@ -3051,6 +3056,20 @@ export function App(): ReactElement {
|
|||||||
const selectedOption = dialogSnapshot.kind ? findAccountOption(dialogSnapshot.kind) : null;
|
const selectedOption = dialogSnapshot.kind ? findAccountOption(dialogSnapshot.kind) : null;
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
const nextDraft = applyAccountDialogToSettings(settingsDraft, dialogSnapshot);
|
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);
|
await persistSpecificSettings(nextDraft);
|
||||||
closeAccountDialog();
|
closeAccountDialog();
|
||||||
if (quickAction) {
|
if (quickAction) {
|
||||||
@@ -3917,6 +3936,7 @@ export function App(): ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
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 dragSelectRef = useRef(false);
|
||||||
const dragAnchorRef = useRef<string | null>(null);
|
const dragAnchorRef = useRef<string | null>(null);
|
||||||
const dragDidMoveRef = useRef(false);
|
const dragDidMoveRef = useRef(false);
|
||||||
@@ -4999,13 +5019,13 @@ export function App(): ReactElement {
|
|||||||
<button className={tab === "history" ? "tab active" : "tab"} onClick={() => setTab("history")}>Verlauf</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>
|
<button className={tab === "statistics" ? "tab active" : "tab"} onClick={() => setTab("statistics")}>Statistiken</button>
|
||||||
<div className="tab-actions">
|
<div className="tab-actions">
|
||||||
{tab === "downloads" && (
|
<DownloadSpeedSparkline
|
||||||
<DownloadSpeedSparkline
|
items={snapshot.session.items}
|
||||||
items={snapshot.session.items}
|
running={snapshot.session.running}
|
||||||
running={snapshot.session.running}
|
paused={snapshot.session.paused}
|
||||||
paused={snapshot.session.paused}
|
speedStateRef={speedSparklineStateRef}
|
||||||
/>
|
hidden={tab !== "downloads"}
|
||||||
)}
|
/>
|
||||||
{tab === "downloads" && (
|
{tab === "downloads" && (
|
||||||
<input
|
<input
|
||||||
className="search-input tab-search"
|
className="search-input tab-search"
|
||||||
@@ -5549,7 +5569,7 @@ 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">
|
<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"}
|
{accountCheckBusy ? "Prüfe Accounts…" : "Alle prüfen"}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn accent" disabled={actionBusy || availableAccountOptions.length === 0} onClick={openCreateAccountDialog}>
|
<button className="btn account-add-button" disabled={actionBusy || availableAccountOptions.length === 0} onClick={openCreateAccountDialog}>
|
||||||
Account hinzufügen
|
Account hinzufügen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -6300,12 +6320,24 @@ export function App(): ReactElement {
|
|||||||
<div className="account-modal-body">
|
<div className="account-modal-body">
|
||||||
<div className="account-dialog-step">
|
<div className="account-dialog-step">
|
||||||
<div className="account-dialog-step-label">1. Account-Typ auswaehlen</div>
|
<div className="account-dialog-step-label">1. Account-Typ auswaehlen</div>
|
||||||
<input
|
<div className="account-picker-toolbar">
|
||||||
className="account-picker-search"
|
<select
|
||||||
placeholder="Dienst oder Typ suchen"
|
className="account-picker-filter"
|
||||||
value={accountDialogSearch}
|
aria-label="Account-Typ filtern"
|
||||||
onChange={(event) => setAccountDialogSearch(event.target.value)}
|
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-table">
|
||||||
<div className="account-picker-head">
|
<div className="account-picker-head">
|
||||||
<span>Account</span>
|
<span>Account</span>
|
||||||
@@ -6502,7 +6534,7 @@ export function App(): ReactElement {
|
|||||||
value={accountDialog.dailyLimitGb}
|
value={accountDialog.dailyLimitGb}
|
||||||
onChange={(event) => setAccountDialog((prev) => prev ? { ...prev, dailyLimitGb: event.target.value } : prev)}
|
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>
|
</div>
|
||||||
|
|
||||||
{accountDialog.kind === "debridlink-api" && parseDebridLinkApiKeys(accountDialog.token).length > 0 && (
|
{accountDialog.kind === "debridlink-api" && parseDebridLinkApiKeys(accountDialog.token).length > 0 && (
|
||||||
@@ -6598,7 +6630,7 @@ export function App(): ReactElement {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button className="btn accent" disabled={actionBusy || !accountDialog.kind} onClick={() => { void onSaveAccountDialog(); }}>
|
<button className="btn accent" disabled={actionBusy || !accountDialog.kind} onClick={() => { void onSaveAccountDialog(); }}>
|
||||||
Speichern
|
Prüfen und speichern
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -475,6 +475,17 @@ body,
|
|||||||
border-color: #f0982f;
|
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 {
|
.btn.danger {
|
||||||
border-color: rgba(244, 63, 94, 0.7);
|
border-color: rgba(244, 63, 94, 0.7);
|
||||||
color: #fda4af;
|
color: #fda4af;
|
||||||
@@ -547,6 +558,11 @@ body,
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.speed-sparkline-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.speed-sparkline-canvas {
|
.speed-sparkline-canvas {
|
||||||
width: 116px;
|
width: 116px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
@@ -1925,6 +1941,12 @@ body,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-picker-toolbar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(145px, 0.32fr) minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.account-modal label {
|
.account-modal label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
@@ -2088,6 +2110,10 @@ body,
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-daily-limit-note {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.account-mode-note {
|
.account-mode-note {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -3231,6 +3257,10 @@ td {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-picker-toolbar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.account-picker-head,
|
.account-picker-head,
|
||||||
.account-picker-row {
|
.account-picker-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export interface ElectronApi {
|
|||||||
importBestDebridCookies: () => Promise<number>;
|
importBestDebridCookies: () => Promise<number>;
|
||||||
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
|
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
|
||||||
getDebridLinkHostLimits: () => Promise<DebridLinkHostLimitInfo[]>;
|
getDebridLinkHostLimits: () => Promise<DebridLinkHostLimitInfo[]>;
|
||||||
checkDebridAccounts: () => Promise<DebridAccountStatus[]>;
|
checkDebridAccounts: (settings?: AppSettings) => Promise<DebridAccountStatus[]>;
|
||||||
checkMegaDebridAccount: (login: string, password: string) => Promise<DebridAccountStatus | null>;
|
checkMegaDebridAccount: (login: string, password: string) => Promise<DebridAccountStatus | null>;
|
||||||
retryExtraction: (packageId: string) => Promise<void>;
|
retryExtraction: (packageId: string) => Promise<void>;
|
||||||
extractNow: (packageId: string) => Promise<void>;
|
extractNow: (packageId: string) => Promise<void>;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user