From e2ccbf70079f0c19f24fac47b14f266b30b1dffc Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 26 Mar 2025 11:32:41 -0700 Subject: [PATCH] Add watcher polling settings. --- src/main/ipc/ipcMainConfig.ts | 10 +++ src/main/ipc/ipcMainHandler.settings.ts | 39 ++++++++++- src/main/store/store.ts | 9 ++- src/main/watcher/watcher.ts | 51 +++++++++------ .../components/Settings/Settings.Watcher.tsx | 64 ++++++++++++++++++- src/renderer/src/redux/app.slice.ts | 42 ++++++++---- src/renderer/src/util/ipcRendererHandler.ts | 6 ++ src/util/ipcTypes.json | 7 +- src/util/translations/en-US/main.json | 6 +- src/util/translations/en-US/renderer.json | 52 +++++++-------- translations.babel | 26 ++++++++ 11 files changed, 247 insertions(+), 65 deletions(-) diff --git a/src/main/ipc/ipcMainConfig.ts b/src/main/ipc/ipcMainConfig.ts index 9d690af..df9aaf6 100644 --- a/src/main/ipc/ipcMainConfig.ts +++ b/src/main/ipc/ipcMainConfig.ts @@ -8,6 +8,8 @@ import { SettingsWatchedFilePathsAdd, SettingsWatchedFilePathsGet, SettingsWatchedFilePathsRemove, + SettingsWatcherPollingGet, + SettingsWatcherPollingSet, } from "./ipcMainHandler.settings"; import { ipcMainHandleAuthStateChanged } from "./ipcMainHandler.user"; import { autoUpdater } from "electron-updater"; @@ -78,6 +80,14 @@ ipcMain.handle( ipcTypes.toMain.settings.filepaths.remove, SettingsWatchedFilePathsRemove, ); +ipcMain.handle( + ipcTypes.toMain.settings.watcher.getpolling, + SettingsWatcherPollingGet, +); +ipcMain.handle( + ipcTypes.toMain.settings.watcher.setpolling, + SettingsWatcherPollingSet, +); //Watcher Handlers ipcMain.on(ipcTypes.toMain.watcher.start, () => { diff --git a/src/main/ipc/ipcMainHandler.settings.ts b/src/main/ipc/ipcMainHandler.settings.ts index 20084f6..90909ba 100644 --- a/src/main/ipc/ipcMainHandler.settings.ts +++ b/src/main/ipc/ipcMainHandler.settings.ts @@ -2,7 +2,13 @@ import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron"; import log from "electron-log/main"; import _ from "lodash"; import Store from "../store/store"; -import { addWatcherPath, removeWatcherPath, watcher } from "../watcher/watcher"; +import { + addWatcherPath, + removeWatcherPath, + StartWatcher, + StopWatcher, + watcher, +} from "../watcher/watcher"; const SettingsWatchedFilePathsAdd = async (): Promise => { const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set. @@ -41,8 +47,39 @@ const SettingsWatchedFilePathsGet = async (): Promise => { return filepaths; }; +const SettingsWatcherPollingGet = async (): Promise<{ + enabled: boolean; + interval: number; +}> => { + const pollingEnabled: { enabled: boolean; interval: number } = + Store.get("settings.polling"); + return { enabled: pollingEnabled.enabled, interval: pollingEnabled.interval }; +}; +const SettingsWatcherPollingSet = async ( + event: IpcMainInvokeEvent, + pollingSettings: { + enabled: boolean; + interval: number; + }, +): Promise<{ + enabled: boolean; + interval: number; +}> => { + log.info("Polling set", pollingSettings); + const { enabled, interval } = pollingSettings; + Store.set("settings.polling", { enabled, interval }); + + //Restart the watcher with these new settings. + await StopWatcher(); + StartWatcher(); + + return { enabled, interval }; +}; + export { SettingsWatchedFilePathsAdd, SettingsWatchedFilePathsGet, SettingsWatchedFilePathsRemove, + SettingsWatcherPollingGet, + SettingsWatcherPollingSet, }; diff --git a/src/main/store/store.ts b/src/main/store/store.ts index d6b6ea2..7256589 100644 --- a/src/main/store/store.ts +++ b/src/main/store/store.ts @@ -1,4 +1,5 @@ import Store from "electron-store"; + const store = new Store({ defaults: { settings: { @@ -6,7 +7,7 @@ const store = new Store({ runWatcherOnStartup: true, polling: { enabled: false, - pollingInterval: 30000, + interval: 30000, }, }, app: { @@ -25,4 +26,10 @@ const store = new Store({ }, }); +// store.onDidAnyChange((newValue, oldValue) => { +// console.log("Settings changed", newValue, oldValue); +// const mainWindow = BrowserWindow.getAllWindows()[0]; +// mainWindow?.webContents.send(ipcTypes.toRenderer.store.didChange, newValue); +// }); + export default store; diff --git a/src/main/watcher/watcher.ts b/src/main/watcher/watcher.ts index ab15c99..766f23d 100644 --- a/src/main/watcher/watcher.ts +++ b/src/main/watcher/watcher.ts @@ -7,7 +7,7 @@ import ipcTypes from "../../util/ipcTypes.json"; import ImportJob from "../decoder/decoder"; import store from "../store/store"; -let watcher: FSWatcher; +let watcher: FSWatcher | null; async function StartWatcher(): Promise { const filePaths: string[] = store.get("settings.filepaths") || []; @@ -33,13 +33,19 @@ async function StartWatcher(): Promise { } } + const pollingSettings = + (store.get("settings.polling") as { + enabled?: boolean; + interval?: number; + }) || {}; + watcher = chokidar.watch(filePaths, { ignored: (filepath, stats) => { const p = path.parse(filepath); return !stats?.isFile() && p.ext !== "" && p.ext.toUpperCase() !== ".ENV"; //Only watch for .ENV files. }, - usePolling: store.get("settings.polling").enabled || false, - interval: store.get("settings.polling").pollingInterval || 1000, + usePolling: pollingSettings.enabled || false, + interval: pollingSettings.interval || 30000, persistent: true, ignoreInitial: true, awaitWriteFinish: { @@ -73,34 +79,39 @@ async function StartWatcher(): Promise { // errorTypeCheck(error) // ); }) - .on("ready", onWatcherReady) - .on("raw", function (event, path, details) { - // This event should be triggered everytime something happens. - // console.log("Raw event info:", event, path, details); - }); + .on("ready", onWatcherReady); + // .on("raw", function (event, path, details) { + // // This event should be triggered everytime something happens. + // // console.log("Raw event info:", event, path, details); + // }); return true; } function removeWatcherPath(path: string): void { - watcher.unwatch(path); - log.debug(`Stopped watching path: ${path}`); + if (watcher) { + watcher.unwatch(path); + log.debug(`Stopped watching path: ${path}`); + } } function addWatcherPath(path: string | string[]): void { - watcher.add(path); - log.debug(`Started watching path: ${path}`); + if (watcher) { + watcher.add(path); + log.debug(`Started watching path: ${path}`); + } } function onWatcherReady(): void { - const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set. - - new Notification({ - title: "Watcher Started", - body: "Newly exported estimates will be automatically uploaded.", - }).show(); - log.info("Confirmed watched paths:", watcher.getWatched()); - mainWindow.webContents.send(ipcTypes.toRenderer.watcher.started); + if (watcher) { + const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set. + new Notification({ + title: "Watcher Started", + body: "Newly exported estimates will be automatically uploaded.", + }).show(); + log.info("Confirmed watched paths:", watcher.getWatched()); + mainWindow.webContents.send(ipcTypes.toRenderer.watcher.started); + } } async function StopWatcher(): Promise { diff --git a/src/renderer/src/components/Settings/Settings.Watcher.tsx b/src/renderer/src/components/Settings/Settings.Watcher.tsx index ea73691..06becdb 100644 --- a/src/renderer/src/components/Settings/Settings.Watcher.tsx +++ b/src/renderer/src/components/Settings/Settings.Watcher.tsx @@ -7,15 +7,39 @@ import { selectWatcherStatus, } from "@renderer/redux/app.slice"; import { useAppSelector } from "@renderer/redux/reduxHooks"; -import { Alert, Button, Card, Space, Typography } from "antd"; +import { Alert, Button, Card, InputNumber, Space, Switch } from "antd"; import { useTranslation } from "react-i18next"; import ipcTypes from "../../../../util/ipcTypes.json"; +import { useEffect, useState } from "react"; const SettingsWatcher: React.FC = () => { const { t } = useTranslation(); const isWatcherStarted = useAppSelector(selectWatcherStatus); const watcherError = useAppSelector(selectWatcherError); + const [pollingState, setPollingState] = useState<{ + enabled: boolean; + interval: number; + }>({ + enabled: false, + interval: 0, + }); + + console.log("*** ~ pollingState:", pollingState); + const getPollingStateFromStore = (): void => { + window.electron.ipcRenderer + .invoke(ipcTypes.toMain.settings.watcher.getpolling) + .then((storePollingState: { enabled: boolean; interval: number }) => { + console.log("*** ~ .then ~ storePollingState:", storePollingState); + setPollingState(storePollingState); + }); + }; + + //Get state first time it renders. + useEffect(() => { + getPollingStateFromStore(); + }, []); + const handleStart = (): void => { window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start); }; @@ -24,6 +48,31 @@ const SettingsWatcher: React.FC = () => { window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.stop); }; + const toggleWatcherMode = (checked: boolean): void => { + window.electron.ipcRenderer + .invoke(ipcTypes.toMain.settings.watcher.setpolling, { + enabled: !checked, + interval: pollingState.interval, + }) + .then((storePollingState: { enabled: boolean; interval: number }) => { + setPollingState(storePollingState); + }); + }; + + const handlePollingIntervalChange = (value: number | null): void => { + if (value) { + window.electron.ipcRenderer + .invoke(ipcTypes.toMain.settings.watcher.setpolling, { + enabled: pollingState.enabled, + interval: value, + }) + .then((storePollingState: { enabled: boolean; interval: number }) => { + setPollingState(storePollingState); + }); + } + getPollingStateFromStore(); + }; + return ( @@ -47,6 +96,19 @@ const SettingsWatcher: React.FC = () => { {t("settings.labels.stopped")} )} + + {watcherError && } diff --git a/src/renderer/src/redux/app.slice.ts b/src/renderer/src/redux/app.slice.ts index 1cafcf5..09dc261 100644 --- a/src/renderer/src/redux/app.slice.ts +++ b/src/renderer/src/redux/app.slice.ts @@ -1,13 +1,15 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import log from "electron-log/renderer"; import type { RootState } from "./redux-store"; -import { update } from "lodash"; -import { notification } from "antd"; interface AppState { value: number; watcher: { started: boolean; error: string | null; + polling: { + enabled: boolean; + interval: number; + }; }; updates: { available: boolean; @@ -24,6 +26,10 @@ const initialState: AppState = { watcher: { started: false, error: null, + polling: { + enabled: false, + interval: 30000, + }, }, updates: { available: false, @@ -58,16 +64,26 @@ export const appSlice = createSlice({ state.updates.available = true; state.updates.checking = false; }, - updateProgress: (state, action) => { + updateProgress: ( + state, + action: PayloadAction<{ progress: number; speed: number }>, + ) => { state.updates.available = true; - state.updates.progress = action?.progress; - state.updates.speed = action?.speed; + state.updates.progress = action.payload.progress; + state.updates.speed = action.payload.speed; }, updateDownloaded: (state) => { state.updates.completed = true; state.updates.progress = 100; state.updates.speed = 0; }, + setWatcherPolling: ( + state, + action: PayloadAction<{ enabled: boolean; interval: number }>, + ) => { + state.watcher.polling.enabled = action.payload.enabled; + state.watcher.polling.interval = action.payload.interval; + }, }, }); @@ -79,6 +95,7 @@ export const { updateChecking, updateDownloaded, updateProgress, + setWatcherPolling, } = appSlice.actions; // Other code such as selectors can use the imported `RootState` type @@ -100,6 +117,13 @@ export const selectAppUpdateSpeed = (state: RootState): number => export const selectAppUpdateCompleted = (state: RootState): boolean => state.app.updates.completed; +export const selectWatcherPolling = ( + state: RootState, +): { + enabled: boolean; + interval: number; +} => state.app.watcher.polling; + //Async Functions - Thunks // Define a thunk that dispatches those action creators // const fetchUsers = () => async (dispatch) => { @@ -108,12 +132,4 @@ export const selectAppUpdateCompleted = (state: RootState): boolean => // // dispatch(incrementByAmount(100)); // }; -const updateAvailableThunk = () => async (dispatch) => { - notification.info({ - message: "Update Available", - key: "app-update", - description: "An update is available for download.", - }); -}; - export default appSlice.reducer; diff --git a/src/renderer/src/util/ipcRendererHandler.ts b/src/renderer/src/util/ipcRendererHandler.ts index 5279ec1..0a00862 100644 --- a/src/renderer/src/util/ipcRendererHandler.ts +++ b/src/renderer/src/util/ipcRendererHandler.ts @@ -1,5 +1,7 @@ //Set up all of the IPC handlers. import { + selectWatcherPolling, + setWatcherPolling, updateAvailable, updateChecking, updateDownloaded, @@ -84,3 +86,7 @@ ipcRenderer.on( dispatch(updateDownloaded()); }, ); + +ipcRenderer.on(ipcTypes.toRenderer.watcher.polling, (event, arg) => { + dispatch(setWatcherPolling({ enabled: arg.enabled, interval: arg.interval })); +}); diff --git a/src/util/ipcTypes.json b/src/util/ipcTypes.json index 907ffa5..5dc8b86 100644 --- a/src/util/ipcTypes.json +++ b/src/util/ipcTypes.json @@ -19,6 +19,10 @@ "get": "toMain_settings_filepaths_get", "add": "toMain_settings_filepaths_add", "remove": "toMain_settings_filepaths_remove" + }, + "watcher": { + "getpolling": "toMain_settings_watcher_getpolling", + "setpolling": "toMain_settings_watcher_setpolling" } }, "user": { @@ -30,7 +34,8 @@ "watcher": { "started": "toRenderer_watcher_started", "stopped": "toRenderer_watcher_stopped", - "error": "toRenderer_watcher_error" + "error": "toRenderer_watcher_error", + "polling": "toRenderer_watcher_polling" }, "updates": { "checking": "toRenderer_updates_checking", diff --git a/src/util/translations/en-US/main.json b/src/util/translations/en-US/main.json index 6dd0159..8dba742 100644 --- a/src/util/translations/en-US/main.json +++ b/src/util/translations/en-US/main.json @@ -1,5 +1,5 @@ { - "toolbar": { - "help": "Help" - } + "toolbar": { + "help": "Help" + } } diff --git a/src/util/translations/en-US/renderer.json b/src/util/translations/en-US/renderer.json index 5cb7ef4..0e2e4be 100644 --- a/src/util/translations/en-US/renderer.json +++ b/src/util/translations/en-US/renderer.json @@ -1,27 +1,29 @@ { - "translation": { - "navigation": { - "home": "Home", - "settings": "Settings" - }, - "settings": { - "actions": { - "addpath": "Add path", - "startwatcher": "Start Watcher", - "stopwatcher": "Stop Watcher\n" - }, - "labels": { - "started": "Started", - "stopped": "Stopped", - "watchedpaths": "Watched Paths", - "watcherstatus": "Watcher Status" - } - }, - "updates": { - "apply": "Apply Update", - "available": "An update is available.", - "download": "Download Update", - "downloading": "An update is downloading." - } - } + "translation": { + "navigation": { + "home": "Home", + "settings": "Settings" + }, + "settings": { + "actions": { + "addpath": "Add path", + "startwatcher": "Start Watcher", + "stopwatcher": "Stop Watcher\n" + }, + "labels": { + "started": "Started", + "stopped": "Stopped", + "watchedpaths": "Watched Paths", + "watchermodepolling": "Polling", + "watchermoderealtime": "Real Time", + "watcherstatus": "Watcher Status" + } + }, + "updates": { + "apply": "Apply Update", + "available": "An update is available.", + "download": "Download Update", + "downloading": "An update is downloading." + } + } } diff --git a/translations.babel b/translations.babel index 8a6b353..a66532d 100644 --- a/translations.babel +++ b/translations.babel @@ -165,6 +165,32 @@ + + watchermodepolling + false + + + + + + en-US + false + + + + + watchermoderealtime + false + + + + + + en-US + false + + + watcherstatus false