UNPKG

backsplash-app

Version:
320 lines (274 loc) 12.4 kB
import { useState, useEffect, useRef, useCallback } from "react"; import { useLastWallpaper } from "./useLastWallpaper"; import { useCurrentWallpaperMode } from "./useCurrentWallpaperMode"; import { WallpaperUpdates } from "@/ipc/channels/wallpaperChannels"; import { useWallpaperGeneration } from "./useWallpaperGeneration"; // Default interval in minutes (e.g., 6 hours) const DEFAULT_INTERVAL_MINUTES = 360; const SCHEDULER_ACTIVE_STORE_KEY = "scheduler.active"; const SCHEDULER_INTERVAL_MINUTES_STORE_KEY = "scheduler.intervalMinutes"; export function useScheduler() { const [isActive, setIsActiveInternal] = useState<boolean>(false); const [intervalMinutes, setIntervalMinutesInternal] = useState<number>(DEFAULT_INTERVAL_MINUTES); const [nextChangeCountdown, setNextChangeCountdownInternal] = useState<string>(""); const [nextRunTime, setNextRunTime] = useState<number | null>(null); const { lastWallpaper } = useLastWallpaper(); const { currentMode } = useCurrentWallpaperMode(); const { isGenerating, isUpscaling } = useWallpaperGeneration(); const previousStateRef = useRef({ isGenerating, isUpscaling, lastWallpaper }); // Initialize scheduler state (isActive, intervalMinutes) from store useEffect(() => { const loadSchedulerState = async () => { let loadedIsActive = false; let loadedIntervalMinutes = DEFAULT_INTERVAL_MINUTES; try { const ipcIsActive = await window.electron.ipcRenderer.invoke("store:get:schedulerActive"); if (typeof ipcIsActive === "boolean") { loadedIsActive = ipcIsActive; } else { console.warn("Invalid isActive type from IPC:", ipcIsActive); } } catch (error) { console.error("Error loading scheduler active state from IPC:", error); } try { const ipcIntervalMinutes = await window.electron.ipcRenderer.invoke("store:get:schedulerIntervalMinutes"); if (typeof ipcIntervalMinutes === "number" && ipcIntervalMinutes > 0) { loadedIntervalMinutes = ipcIntervalMinutes; } else { console.warn("Invalid intervalMinutes type or value from IPC:", ipcIntervalMinutes); } } catch (error) { console.error("Error loading scheduler interval from IPC:", error); } // Also fetch the next run time if the scheduler is active if (loadedIsActive) { try { const nextRun = await window.electron.ipcRenderer.invoke("scheduler:getNextRunTime"); if (typeof nextRun === "number" || nextRun === null) { setNextRunTime(nextRun as number | null); } } catch (error) { console.error("Error loading next run time from IPC:", error); setNextRunTime(null); } } // Always set the states even if some operations failed setIsActiveInternal(loadedIsActive); setIntervalMinutesInternal(loadedIntervalMinutes); }; loadSchedulerState(); }, []); // Run once on mount // Listen for direct store updates pushed from main process useEffect(() => { const activeUpdateChannel = `store:updated:${SCHEDULER_ACTIVE_STORE_KEY}`; const intervalUpdateChannel = `store:updated:${SCHEDULER_INTERVAL_MINUTES_STORE_KEY}`; const handleActiveUpdate = (_event: any, newIsActive: boolean) => { if (typeof newIsActive === "boolean") { setIsActiveInternal(newIsActive); } }; const handleIntervalUpdate = (_event: any, newIntervalMinutes: number) => { if (typeof newIntervalMinutes === "number" && newIntervalMinutes > 0) { setIntervalMinutesInternal(newIntervalMinutes); } }; window.electron.ipcRenderer.on(activeUpdateChannel, handleActiveUpdate); window.electron.ipcRenderer.on(intervalUpdateChannel, handleIntervalUpdate); // Also listen to status updates from SchedulerService itself (e.g., after start/stop actions) const handleSchedulerServiceStatus = (_event: any, status: { data?: any; type?: string; message?: string }) => { if (status && status.type === "scheduler-changed" && status.data) { const { active, intervalMinutes: newInterval, nextRunTime: newNextRunTime } = status.data; if (typeof active === "boolean") { setIsActiveInternal(active); } if (typeof newInterval === "number" && newInterval > 0) { setIntervalMinutesInternal(newInterval); } if (typeof newNextRunTime === "number" || newNextRunTime === null) { setNextRunTime(newNextRunTime as number | null); } } }; window.electron.ipcRenderer.on(WallpaperUpdates.STATUS_UPDATE, handleSchedulerServiceStatus); return () => { window.electron.ipcRenderer.removeListener(activeUpdateChannel, handleActiveUpdate); window.electron.ipcRenderer.removeListener(intervalUpdateChannel, handleIntervalUpdate); window.electron.ipcRenderer.removeListener(WallpaperUpdates.STATUS_UPDATE, handleSchedulerServiceStatus); }; }, []); // Fetch the next run time at regular intervals useEffect(() => { if (!isActive) { setNextRunTime(null); return; } const fetchNextRunTime = async () => { try { const nextRun = await window.electron.ipcRenderer.invoke("scheduler:getNextRunTime"); if (typeof nextRun === "number" || nextRun === null) { setNextRunTime(nextRun as number | null); } } catch (error) { console.error("Error fetching next run time:", error); } }; // Initial fetch fetchNextRunTime(); // Use variable polling frequency // Poll every second if nextRunTime is null (initializing state) // Otherwise poll every 15 seconds (normal operation) const pollInterval = nextRunTime === null ? 1000 : 15000; const intervalId = setInterval(fetchNextRunTime, pollInterval); return () => clearInterval(intervalId); }, [isActive, nextRunTime]); // Countdown logic (useEffect) - replaced with server-side data useEffect(() => { if (!isActive) { setNextChangeCountdownInternal(""); return; } // Show generation/upscaling status with priority if (isGenerating || isUpscaling) { setNextChangeCountdownInternal("Changing now..."); return; } // Calculate countdown based on nextRunTime from scheduler service const calculateCountdown = () => { if (nextRunTime === null) { setNextChangeCountdownInternal("Waiting..."); return; } const now = Date.now(); const diffMs = nextRunTime - now; // If time has elapsed if (diffMs <= 0) { setNextChangeCountdownInternal("Changing now..."); return; } // Format the countdown string const d = Math.floor(diffMs / (1000 * 60 * 60 * 24)), h = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), m = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)), s = Math.floor((diffMs % (1000 * 60)) / 1000); let str = "Next In: "; if (d > 0) str += `${d}d ${h}h`; else if (h > 0) str += `${h}h ${m}m`; else if (m > 0) str += `${m}m ${s}s`; else if (s >= 0) str += `${s}s`; // Always show down to 0s setNextChangeCountdownInternal(str); }; calculateCountdown(); // Initial calculation const intervalId = setInterval(calculateCountdown, 1000); // Update countdown every second return () => clearInterval(intervalId); // Cleanup interval }, [isActive, nextRunTime, isGenerating, isUpscaling]); // Track transitions between states, especially when generation completes useEffect(() => { const wasGeneratingOrUpscaling = previousStateRef.current.isGenerating || previousStateRef.current.isUpscaling; const isNowCompleted = !isGenerating && !isUpscaling; const wallpaperJustChanged = previousStateRef.current.lastWallpaper !== lastWallpaper && lastWallpaper?.generatedAt; // After successful generation/upscaling with a new wallpaper, update the next run time if (wasGeneratingOrUpscaling && isNowCompleted && wallpaperJustChanged) { console.log("[Scheduler] Generation/upscaling just completed with new wallpaper, refreshing next run time."); // Fetch updated next run time window.electron.ipcRenderer .invoke("scheduler:getNextRunTime") .then((nextRun) => { if (typeof nextRun === "number" || nextRun === null) { setNextRunTime(nextRun as number | null); } }) .catch((error) => { console.error("Error refreshing next run time:", error); }); } // IMPORTANT: Update previous state after comparison previousStateRef.current = { isGenerating, isUpscaling, lastWallpaper }; }, [isGenerating, isUpscaling, lastWallpaper]); const actualSetIntervalMinutes = useCallback( async (minutes: number) => { const validMinutes = Math.max(1, minutes); setIntervalMinutesInternal(validMinutes); try { await window.electron.ipcRenderer.invoke("store:set:schedulerIntervalMinutes", validMinutes); } catch (err) { console.error("Error setting interval minutes IPC:", err); } }, [setIntervalMinutesInternal], ); const saveCurrentModeParamsToStore = useCallback(async () => { if (!currentMode) { console.warn("Cannot save mode params, currentMode unset."); return; } try { await window.electron.ipcRenderer.invoke("store:set:currentMode", currentMode); } catch (error) { console.error("Error saving mode params:", error); } }, [currentMode]); const toggleScheduler = useCallback(async () => { await saveCurrentModeParamsToStore(); const currentlyActive = isActive; try { if (!currentlyActive) { // Starting scheduler console.log("[Scheduler] Starting scheduler"); await window.electron.ipcRenderer.invoke("scheduler:start"); } else { // Stopping scheduler - make multiple attempts to ensure it stops console.log("[Scheduler] Stopping scheduler"); // First attempt: use the scheduler:stop API const stopResult = await window.electron.ipcRenderer.invoke("scheduler:stop"); console.log("[Scheduler] Stop result:", stopResult); // Second attempt: directly set the store value (belt and suspenders) await window.electron.ipcRenderer.invoke("store:set:schedulerActive", false); // Clear the UI state setNextRunTime(null); setNextChangeCountdownInternal(""); } } catch (error) { console.error(`Error toggling scheduler:`, error); // If we were trying to turn it off and got an error, make one more attempt if (currentlyActive) { try { await window.electron.ipcRenderer.invoke("store:set:schedulerActive", false); console.log("[Scheduler] Made emergency attempt to turn off scheduler"); } catch (e) { console.error("[Scheduler] Failed even emergency attempt:", e); } } } }, [isActive, saveCurrentModeParamsToStore]); const saveSchedulerSettings = useCallback(async () => { console.log("[useScheduler] Saving scheduler settings..."); try { // Ensure the desired interval is stored (it might have been changed locally) await actualSetIntervalMinutes(intervalMinutes); // Ensure current mode and its specific params are stored await saveCurrentModeParamsToStore(); if (isActive) { console.log("[useScheduler] Scheduler is active, invoking scheduler:start to apply new settings."); // SchedulerService.startScheduler will cancel existing job and use latest store settings await window.electron.ipcRenderer.invoke("scheduler:start"); } else { console.log("[useScheduler] Scheduler is not active, settings saved. Will be used if started."); } } catch (error) { console.error("[useScheduler] Error in saveSchedulerSettings:", error); // Potentially show error to user } }, [isActive, intervalMinutes, actualSetIntervalMinutes, saveCurrentModeParamsToStore]); return { scheduler: { isActive, intervalMinutes, nextChangeCountdown, }, setIntervalMinutes: actualSetIntervalMinutes, toggleScheduler, saveSchedulerSettings, saveCurrentModeParamsToStore, }; }