From 2e6074337ae5beb3f624beb56141046caf1a168d Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 6 Mar 2026 22:53:20 +0100 Subject: [PATCH] feat: re-download from history, reset-all-failed, scheduled start, fix provider order dirty flag --- .gitignore | 1 + CLAUDE.md | 13 +++++-- src/main/constants.ts | 3 +- src/main/main.ts | 21 +++++++++++ src/renderer/App.tsx | 82 ++++++++++++++++++++++++++++++++++++++++- src/renderer/styles.css | 68 ++++++++++++++++++++++++++++++++++ src/shared/types.ts | 1 + 7 files changed, 183 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index a9a9334..b093b8d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ deploy/forgejo/caddy/data/ deploy/forgejo/caddy/config/ deploy/forgejo/caddy/logs/ deploy/forgejo/backups/ +.secrets diff --git a/CLAUDE.md b/CLAUDE.md index db9e22e..3a02a9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,10 +6,15 @@ ## Releasen -1. Token setzen: - - PowerShell: `$env:GITEA_TOKEN=""` -2. Release ausführen: - - `npm run release:gitea -- [notes]` +Der Token liegt in `.secrets` (gitignored) und wird automatisch geladen. + +Als KI-Agent: Token aus `.secrets` lesen und als Umgebungsvariable setzen, dann Release-Script ausführen: +```bash +export $(cat .secrets | xargs) && npm run release:gitea -- [notes] +``` + +Manuell in PowerShell (falls nötig): +- `npm run release:gitea -- [notes]` (Token ist bereits als Benutzervariable gesetzt) Das Script: - bumped `package.json` diff --git a/src/main/constants.ts b/src/main/constants.ts index 06d56fb..581b41b 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -102,6 +102,7 @@ export function defaultSettings(): AppSettings { extractCpuPriority: "high", autoExtractWhenStopped: true, disabledProviders: [], - hosterRouting: {} + hosterRouting: {}, + scheduledStartEpochMs: 0 }; } diff --git a/src/main/main.ts b/src/main/main.ts index cb9df97..56038a1 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -52,6 +52,7 @@ let mainWindow: BrowserWindow | null = null; let tray: Tray | null = null; let clipboardTimer: ReturnType | null = null; let updateQuitTimer: ReturnType | null = null; +let scheduledStartTimer: ReturnType | null = null; let lastClipboardText = ""; const controller = new AppController(); const CLIPBOARD_MAX_TEXT_CHARS = 50_000; @@ -266,6 +267,26 @@ function registerIpcHandlers(): void { const result = controller.updateSettings(validated as Partial); updateClipboardWatcher(); updateTray(); + // Manage scheduled-start timer + if (scheduledStartTimer !== null) { + clearTimeout(scheduledStartTimer); + scheduledStartTimer = null; + } + const schedMs = result.scheduledStartEpochMs || 0; + if (schedMs > 0) { + const delay = schedMs - Date.now(); + if (delay <= 0) { + // Time already passed — start immediately and clear setting + void controller.start().catch((err) => logger.warn(`Scheduled-Start Fehler: ${String(err)}`)); + controller.updateSettings({ scheduledStartEpochMs: 0 }); + } else { + scheduledStartTimer = setTimeout(() => { + scheduledStartTimer = null; + void controller.start().catch((err) => logger.warn(`Scheduled-Start Fehler: ${String(err)}`)); + controller.updateSettings({ scheduledStartEpochMs: 0 }); + }, delay); + } + } return result; }); ipcMain.handle(IPC_CHANNELS.ADD_LINKS, (_event: IpcMainInvokeEvent, payload: AddLinksPayload) => { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 8c07918..7f52c23 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1017,6 +1017,9 @@ export function App(): ReactElement { const [scheduleSpeedInputs, setScheduleSpeedInputs] = useState>({}); const [accountColumnWidths, setAccountColumnWidths] = useState>(loadAccountColumnWidths); const [settingsDirty, setSettingsDirty] = useState(false); + const [schedulePickerOpen, setSchedulePickerOpen] = useState(false); + const [scheduleTimeInput, setScheduleTimeInput] = useState(""); + const [scheduleCountdown, setScheduleCountdown] = useState(""); const settingsDirtyRef = useRef(false); const settingsDraftRevisionRef = useRef(0); const panelDirtyRevisionRef = useRef(0); @@ -1199,6 +1202,23 @@ export function App(): ReactElement { setSpeedLimitInput(formatMbpsInputFromKbps(settingsDraft.speedLimitKbps)); }, [settingsDraft.speedLimitKbps]); + useEffect(() => { + const schedMs = snapshot.settings.scheduledStartEpochMs || 0; + if (schedMs <= 0) { setScheduleCountdown(""); return; } + const update = (): void => { + const remaining = schedMs - Date.now(); + if (remaining <= 0) { setScheduleCountdown(""); return; } + const totalSec = Math.ceil(remaining / 1000); + const h = Math.floor(totalSec / 3600); + const m = Math.floor((totalSec % 3600) / 60); + const s = totalSec % 60; + setScheduleCountdown(h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m ${s}s` : `${s}s`); + }; + update(); + const timer = setInterval(update, 1000); + return () => clearInterval(timer); + }, [snapshot.settings.scheduledStartEpochMs]); + const showToast = useCallback((message: string, timeoutMs = 2200): void => { setStatusToast(message); if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); } @@ -1564,6 +1584,10 @@ export function App(): ReactElement { // Setzt providerOrder + backwards-kompatible Felder synchron const setProviderOrder = useCallback((newOrder: DebridProvider[]) => { + settingsDraftRevisionRef.current += 1; + panelDirtyRevisionRef.current += 1; + settingsDirtyRef.current = true; + setSettingsDirty(true); setSettingsDraft((prev) => ({ ...prev, providerOrder: newOrder, @@ -3187,6 +3211,44 @@ export function App(): ReactElement {
+
+ {(snapshot.settings.scheduledStartEpochMs || 0) > 0 ? ( +
+ ⏰ {scheduleCountdown || new Date(snapshot.settings.scheduledStartEpochMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} + +
+ ) : ( + + )} + {schedulePickerOpen && (snapshot.settings.scheduledStartEpochMs || 0) === 0 && ( +
+ Starten um + setScheduleTimeInput(e.target.value)} + /> + +
+ )} +
+
-
+
0 ? " stat-item-clickable" : ""}`} + title={itemStatusCounts.failed > 0 ? "Klicken zum Zurücksetzen aller fehlerhaften Downloads" : undefined} + onClick={() => { + if (itemStatusCounts.failed === 0) return; + const failedIds = Object.values(snapshot.session.items) + .filter((it) => it.status === "failed") + .map((it) => it.id); + void window.rd.resetItems(failedIds).catch(() => {}); + }} + > Fehlerhaft {itemStatusCounts.failed}
@@ -4566,6 +4638,14 @@ export function App(): ReactElement { {hasUrls && !multi && ( <>
+