import { useCallback, useEffect, useMemo, useState } from 'react'; import { invokeCommand as invoke } from '../ipc'; import { AlertCircle, CheckCircle2, Clock3, List, Moon, LockKeyhole, Pause, Play, Power, RotateCcw, Save } from 'lucide-react'; import { PostQueueAction, SchedulerSettings, useSettingsStore } from '../store/useSettingsStore'; import { useDownloadStore, MAIN_QUEUE_ID } from '../store/useDownloadStore'; import { WindowDragRegion } from './WindowDragRegion'; import { useToast } from '../contexts/ToastContext'; const days = [ { value: 0, label: 'Su' }, { value: 1, label: 'Mo' }, { value: 2, label: 'Tu' }, { value: 3, label: 'We' }, { value: 4, label: 'Th' }, { value: 5, label: 'Fr' }, { value: 6, label: 'Sa' }, ]; const postActions: { value: PostQueueAction; label: string; icon: typeof Moon }[] = [ { value: 'none', label: 'Do nothing', icon: CheckCircle2 }, { value: 'sleep', label: 'Sleep', icon: Moon }, { value: 'restart', label: 'Restart', icon: RotateCcw }, { value: 'shutdown', label: 'Shut down', icon: Power }, ]; function nextScheduledRun(settings: SchedulerSettings): string { if (!settings.enabled) return 'Scheduler is disabled'; const [hour, minute] = settings.startTime.split(':').map(Number); const now = new Date(); for (let offset = 0; offset < 8; offset += 1) { const candidate = new Date(now); candidate.setDate(now.getDate() + offset); candidate.setHours(hour, minute, 0, 0); const allowedDay = settings.everyday || settings.selectedDays.includes(candidate.getDay()); if (allowedDay && candidate > now) { return candidate.toLocaleString(undefined, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } } return 'No scheduled day selected'; } export default function SchedulerView() { const savedSettings = useSettingsStore(state => state.scheduler); const schedulerRunning = useSettingsStore(state => state.schedulerRunning); const setScheduler = useSettingsStore(state => state.setScheduler); const [draft, setDraft] = useState(savedSettings); const { addToast } = useToast(); const [permissionMessage, setPermissionMessage] = useState(''); const [automationPermissionGranted, setAutomationPermissionGranted] = useState(null); const isMac = navigator.userAgent.includes('Mac'); useEffect(() => { setDraft(savedSettings); }, [savedSettings]); const nextRun = useMemo(() => nextScheduledRun(draft), [draft]); const updateDraft = (key: K, value: SchedulerSettings[K]) => { setDraft(current => ({ ...current, [key]: value })); }; const toggleDay = (day: number) => { setDraft(current => ({ ...current, selectedDays: current.selectedDays.includes(day) ? current.selectedDays.filter(value => value !== day) : [...current.selectedDays, day].sort() })); }; const save = () => { const normalized = { ...draft, selectedDays: draft.everyday || draft.selectedDays.length > 0 ? draft.selectedDays : savedSettings.selectedDays }; setScheduler(normalized); setDraft(normalized); addToast({ message: 'Scheduler settings saved', variant: 'success' }); }; const runNow = async () => { const count = await useDownloadStore.getState().startQueue(MAIN_QUEUE_ID); if (count > 0) { useSettingsStore.getState().setSchedulerRunning(true); addToast({ message: `Started ${count} download${count === 1 ? '' : 's'}`, variant: 'success' }); } else { addToast({ message: 'No paused or failed downloads to start', variant: 'info' }); } }; const pauseNow = async () => { const count = await useDownloadStore.getState().pauseQueue(MAIN_QUEUE_ID); useSettingsStore.getState().setSchedulerRunning(false); addToast({ message: count > 0 ? `Paused ${count} active download${count === 1 ? '' : 's'}` : 'No active downloads', variant: 'info' }); }; const refreshPermissionStatus = useCallback(async (showMessage = false) => { if (!isMac) return; try { await invoke('request_automation_permission'); setAutomationPermissionGranted(true); if (showMessage) { setPermissionMessage('Automation permission is available.'); } } catch { setAutomationPermissionGranted(false); if (showMessage) { setPermissionMessage('Automation permission is missing. Enable Firelink under Automation for Finder in System Settings.'); } } }, [isMac]); useEffect(() => { if (!isMac) return; void refreshPermissionStatus(); const refreshOnFocus = () => { void refreshPermissionStatus(); }; const refreshOnVisibility = () => { if (document.visibilityState === 'visible') { void refreshPermissionStatus(); } }; window.addEventListener('focus', refreshOnFocus); document.addEventListener('visibilitychange', refreshOnVisibility); return () => { window.removeEventListener('focus', refreshOnFocus); document.removeEventListener('visibilitychange', refreshOnVisibility); }; }, [isMac, refreshPermissionStatus]); const openAutomationSettings = async (message: string) => { setPermissionMessage(message); try { await invoke('open_automation_settings'); } catch (error) { setPermissionMessage(String(error)); } }; const handlePermissionAction = async () => { if (automationPermissionGranted) { await openAutomationSettings('macOS does not allow Firelink to revoke Automation permission directly. Revoke it in System Settings, then return to Firelink.'); return; } setPermissionMessage('Requesting Automation permission...'); try { await invoke('request_automation_permission'); setAutomationPermissionGranted(true); setPermissionMessage('Automation permission is available.'); } catch { setAutomationPermissionGranted(false); await openAutomationSettings('Enable Firelink under Automation for Finder in System Settings, then return to Firelink.'); } }; return (
{schedulerRunning ? 'Running' : nextRun}
Timing
updateDraft('stopTime', event.target.value)} disabled={!draft.enabled || !draft.stopTimeEnabled} className="app-control px-3 py-2 text-text-primary disabled:opacity-50" />
{!draft.everyday && (
{days.map(day => { const selected = draft.selectedDays.includes(day.value); return ( ); })}
)}
Queues to Schedule
After Completion

Choose what happens after downloads started by the scheduler finish.

{postActions.map(action => { const Icon = action.icon; return ( ); })}
{draft.postQueueAction !== 'none' && (

This action can interrupt other work on the computer. Firelink invokes it immediately after the scheduled queue finishes.

)}
{isMac && (
System Permissions

Sleep, restart, and shut down require macOS Automation permission for Finder.

{automationPermissionGranted ? ( <> Automation permission granted ) : ( <> {automationPermissionGranted === null ? 'Checking Automation permission...' : 'Automation permission missing'} )}
{permissionMessage &&

{permissionMessage}

}
)}
); }