backsplash-app
Version:
An AI powered wallpaper app.
320 lines (274 loc) • 12.4 kB
text/typescript
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,
};
}