diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 430db28..137c22c 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1534,6 +1534,21 @@ export function buildDailyScheduleSettingsUpdate( }; } +export async function activateDailyScheduleSettings( + time: string, + startDay: DailyScheduleStartDay, + persist: (update: RendererSettingsUpdate) => Promise, + showError: (message: string) => void, + now = new Date() +): Promise { + const update = buildDailyScheduleSettingsUpdate(time, startDay, now); + if (!update) { + showError("Bitte eine gültige Startzeit auswählen."); + return false; + } + return persist(update); +} + export async function persistDailyScheduleSettingsUpdate( update: RendererSettingsUpdate, operation: "activate" | "cancel", @@ -4771,14 +4786,17 @@ export function App(): ReactElement { ), [applyAuthoritativeDailyScheduleSnapshot, applyDailyScheduleSettings, showToast]); const activateDownloadSchedule = useCallback((): void => { - const update = buildDailyScheduleSettingsUpdate(scheduleTimeInput, scheduleStartDay); - if (!update) return; - void persistDownloadSchedule(update, "activate").then((persisted) => { + void activateDailyScheduleSettings( + scheduleTimeInput, + scheduleStartDay, + (update) => persistDownloadSchedule(update, "activate"), + (message) => showToast(message, 2800) + ).then((persisted) => { if (persisted) { setSchedulePickerOpen(false); } }); - }, [persistDownloadSchedule, scheduleStartDay, scheduleTimeInput]); + }, [persistDownloadSchedule, scheduleStartDay, scheduleTimeInput, showToast]); const removeActionableDownloads = useCallback((): void => { const ids = new Set(downloadsViewCore.actionableSelectedIds); @@ -4812,6 +4830,7 @@ export function App(): ReactElement { scheduleActive: snapshot.settings.dailyStartEnabled || snapshot.settings.scheduledStartEpochMs > 0, scheduleOpen: schedulePickerOpen, scheduleTime: scheduleTimeInput, + scheduleTimeValid: buildDailyScheduleSettingsUpdate(scheduleTimeInput, scheduleStartDay) !== null, scheduleStartDay, scheduleLabel: scheduleCountdown || (snapshot.settings.dailyStartEnabled ? formatDailyScheduleTime(snapshot.settings.dailyStartMinuteOfDay) diff --git a/src/renderer/i18n.ts b/src/renderer/i18n.ts index 973569a..2e0a312 100644 --- a/src/renderer/i18n.ts +++ b/src/renderer/i18n.ts @@ -63,7 +63,7 @@ const pairs = [ ["Abgeschlossene und gelöschte Pakete erscheinen hier.", "Completed and deleted packages appear here."], ["Passe Filter oder Suche an.", "Adjust the filter or search."], ["Öffne die Ansicht erneut, um es noch einmal zu versuchen.", "Open the view again to retry."], ["Alle sichtbaren Einträge auswählen", "Select all visible entries"], ["Details anzeigen", "Show details"], ["Details ausblenden", "Hide details"], ["Sichtbar:", "Visible:"], ["pro Seite", "per page"], - ["Verfügbarkeit", "Availability"], ["Hinzugefügt am", "Added on"], ["Ungeprüft", "Unchecked"], ["Paket gestoppt", "Package stopped"], ["Alle anzeigen", "Show all"], ["Planen", "Schedule"], ["Startzeit", "Start time"], ["Starttag", "Start day"], ["Ab heute", "Starting today"], ["Ab morgen", "Starting tomorrow"], + ["Verfügbarkeit", "Availability"], ["Hinzugefügt am", "Added on"], ["Ungeprüft", "Unchecked"], ["Paket gestoppt", "Package stopped"], ["Alle anzeigen", "Show all"], ["Planen", "Schedule"], ["Startzeit", "Start time"], ["Starttag", "Start day"], ["Ab heute", "Starting today"], ["Ab morgen", "Starting tomorrow"], ["Bitte eine gültige Startzeit auswählen.", "Select a valid start time."], ["Keine Downloads", "No downloads"], ["Keine passenden Downloads", "No matching downloads"], ["Füge Links hinzu, um Downloads vorzubereiten.", "Add links to prepare downloads."], ["Passe Filter oder Suche an.", "Adjust the filter or search."], ["Keine Links gesammelt", "No links collected"], ["Keine passenden Links", "No matching links"], ["Füge Links oder Text ein, um sie zu sammeln.", "Paste links or text to collect them."], ["Links durchsuchen", "Search links"], ["Datenmenge", "Data volume"], ["Sitzungszähler", "Session counter"], ["Sieben Tage", "Seven days"], ["30 Tage", "30 days"], ["Zeitraum", "Period"], ["Erfolgreich", "Successful"], diff --git a/src/renderer/views/downloads/DownloadsView.tsx b/src/renderer/views/downloads/DownloadsView.tsx index 870c86a..0a28093 100644 --- a/src/renderer/views/downloads/DownloadsView.tsx +++ b/src/renderer/views/downloads/DownloadsView.tsx @@ -39,9 +39,10 @@ export interface DownloadsViewModel extends DownloadsViewModelCore { reconnectSeconds: number; reconnectReason: string; clipboardWatcher: boolean; - scheduleActive: boolean; + scheduleActive: boolean; scheduleOpen: boolean; scheduleTime: string; + scheduleTimeValid: boolean; scheduleStartDay: DailyScheduleStartDay; scheduleLabel: string; packageSpeedBps: Record; @@ -141,7 +142,7 @@ export function DownloadsToolbar({ actions, model }: { actions: DownloadsViewAct {model.scheduleActive ? <>Geplant: {model.scheduleLabel} - : <> actions.onScheduleTimeChange(event.target.value)} type="time" value={model.scheduleTime} />} + : <> actions.onScheduleTimeChange(event.target.value)} type="time" value={model.scheduleTime} />} diff --git a/tests/downloads-view.test.tsx b/tests/downloads-view.test.tsx index 08324e3..396f240 100644 --- a/tests/downloads-view.test.tsx +++ b/tests/downloads-view.test.tsx @@ -791,6 +791,7 @@ function withRuntime(input: DownloadsModelInput, overrides: Partial { expect(calls).toEqual(["time:09:45", "day:today"]); }); + it.each(["", "8:15", "24:00", "12:60"])("disables schedule activation for the invalid time %j", (scheduleTime) => { + const toolbar = DownloadsToolbar({ + actions: createActions(), + model: withRuntime(createInput(), { scheduleOpen: true, scheduleTime, scheduleTimeValid: false }) + }); + + expect(findButton(toolbar, "Planen").props.disabled).toBe(true); + }); + it("animates the persistent schedule slot horizontally and disables it through the global motion setting", () => { const closed = renderToStaticMarkup(); const open = renderToStaticMarkup(); diff --git a/tests/renderer-settings-form.test.tsx b/tests/renderer-settings-form.test.tsx index 2830e6b..8e91012 100644 --- a/tests/renderer-settings-form.test.tsx +++ b/tests/renderer-settings-form.test.tsx @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { RendererSettings, UiSnapshot } from "../src/shared/types"; import { + activateDailyScheduleSettings, buildDailyScheduleSettingsUpdate, persistDailyScheduleSettingsUpdate } from "../src/renderer/App"; @@ -21,6 +22,22 @@ describe("daily schedule settings form", () => { }); }); + it.each(["", "8:15", "24:00", "12:60"])("reports the invalid time %j when activation is invoked directly", async (time) => { + const persist = vi.fn(); + const showError = vi.fn(); + + await expect(activateDailyScheduleSettings( + time, + "today", + persist, + showError, + new Date(2026, 7, 22, 18, 30, 0, 0) + )).resolves.toBe(false); + + expect(showError).toHaveBeenCalledWith("Bitte eine gültige Startzeit auswählen."); + expect(persist).not.toHaveBeenCalled(); + }); + it("applies persisted settings after a successful activation", async () => { const persisted = { dailyStartEnabled: true } as RendererSettings; const updateSettings = vi.fn().mockResolvedValue(persisted);