Add watcher polling settings.

This commit is contained in:
Patrick Fic
2025-03-26 11:32:41 -07:00
parent 791c518920
commit e2ccbf7007
11 changed files with 247 additions and 65 deletions

View File

@@ -8,6 +8,8 @@ import {
SettingsWatchedFilePathsAdd, SettingsWatchedFilePathsAdd,
SettingsWatchedFilePathsGet, SettingsWatchedFilePathsGet,
SettingsWatchedFilePathsRemove, SettingsWatchedFilePathsRemove,
SettingsWatcherPollingGet,
SettingsWatcherPollingSet,
} from "./ipcMainHandler.settings"; } from "./ipcMainHandler.settings";
import { ipcMainHandleAuthStateChanged } from "./ipcMainHandler.user"; import { ipcMainHandleAuthStateChanged } from "./ipcMainHandler.user";
import { autoUpdater } from "electron-updater"; import { autoUpdater } from "electron-updater";
@@ -78,6 +80,14 @@ ipcMain.handle(
ipcTypes.toMain.settings.filepaths.remove, ipcTypes.toMain.settings.filepaths.remove,
SettingsWatchedFilePathsRemove, SettingsWatchedFilePathsRemove,
); );
ipcMain.handle(
ipcTypes.toMain.settings.watcher.getpolling,
SettingsWatcherPollingGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.watcher.setpolling,
SettingsWatcherPollingSet,
);
//Watcher Handlers //Watcher Handlers
ipcMain.on(ipcTypes.toMain.watcher.start, () => { ipcMain.on(ipcTypes.toMain.watcher.start, () => {

View File

@@ -2,7 +2,13 @@ import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron";
import log from "electron-log/main"; import log from "electron-log/main";
import _ from "lodash"; import _ from "lodash";
import Store from "../store/store"; 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<string[]> => { const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set. 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<string[]> => {
return filepaths; 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 { export {
SettingsWatchedFilePathsAdd, SettingsWatchedFilePathsAdd,
SettingsWatchedFilePathsGet, SettingsWatchedFilePathsGet,
SettingsWatchedFilePathsRemove, SettingsWatchedFilePathsRemove,
SettingsWatcherPollingGet,
SettingsWatcherPollingSet,
}; };

View File

@@ -1,4 +1,5 @@
import Store from "electron-store"; import Store from "electron-store";
const store = new Store({ const store = new Store({
defaults: { defaults: {
settings: { settings: {
@@ -6,7 +7,7 @@ const store = new Store({
runWatcherOnStartup: true, runWatcherOnStartup: true,
polling: { polling: {
enabled: false, enabled: false,
pollingInterval: 30000, interval: 30000,
}, },
}, },
app: { 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; export default store;

View File

@@ -7,7 +7,7 @@ import ipcTypes from "../../util/ipcTypes.json";
import ImportJob from "../decoder/decoder"; import ImportJob from "../decoder/decoder";
import store from "../store/store"; import store from "../store/store";
let watcher: FSWatcher; let watcher: FSWatcher | null;
async function StartWatcher(): Promise<boolean> { async function StartWatcher(): Promise<boolean> {
const filePaths: string[] = store.get("settings.filepaths") || []; const filePaths: string[] = store.get("settings.filepaths") || [];
@@ -33,13 +33,19 @@ async function StartWatcher(): Promise<boolean> {
} }
} }
const pollingSettings =
(store.get("settings.polling") as {
enabled?: boolean;
interval?: number;
}) || {};
watcher = chokidar.watch(filePaths, { watcher = chokidar.watch(filePaths, {
ignored: (filepath, stats) => { ignored: (filepath, stats) => {
const p = path.parse(filepath); const p = path.parse(filepath);
return !stats?.isFile() && p.ext !== "" && p.ext.toUpperCase() !== ".ENV"; //Only watch for .ENV files. return !stats?.isFile() && p.ext !== "" && p.ext.toUpperCase() !== ".ENV"; //Only watch for .ENV files.
}, },
usePolling: store.get("settings.polling").enabled || false, usePolling: pollingSettings.enabled || false,
interval: store.get("settings.polling").pollingInterval || 1000, interval: pollingSettings.interval || 30000,
persistent: true, persistent: true,
ignoreInitial: true, ignoreInitial: true,
awaitWriteFinish: { awaitWriteFinish: {
@@ -73,34 +79,39 @@ async function StartWatcher(): Promise<boolean> {
// errorTypeCheck(error) // errorTypeCheck(error)
// ); // );
}) })
.on("ready", onWatcherReady) .on("ready", onWatcherReady);
.on("raw", function (event, path, details) { // .on("raw", function (event, path, details) {
// This event should be triggered everytime something happens. // // This event should be triggered everytime something happens.
// console.log("Raw event info:", event, path, details); // // console.log("Raw event info:", event, path, details);
}); // });
return true; return true;
} }
function removeWatcherPath(path: string): void { function removeWatcherPath(path: string): void {
watcher.unwatch(path); if (watcher) {
log.debug(`Stopped watching path: ${path}`); watcher.unwatch(path);
log.debug(`Stopped watching path: ${path}`);
}
} }
function addWatcherPath(path: string | string[]): void { function addWatcherPath(path: string | string[]): void {
watcher.add(path); if (watcher) {
log.debug(`Started watching path: ${path}`); watcher.add(path);
log.debug(`Started watching path: ${path}`);
}
} }
function onWatcherReady(): void { function onWatcherReady(): void {
const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set. if (watcher) {
const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set.
new Notification({ new Notification({
title: "Watcher Started", title: "Watcher Started",
body: "Newly exported estimates will be automatically uploaded.", body: "Newly exported estimates will be automatically uploaded.",
}).show(); }).show();
log.info("Confirmed watched paths:", watcher.getWatched()); log.info("Confirmed watched paths:", watcher.getWatched());
mainWindow.webContents.send(ipcTypes.toRenderer.watcher.started); mainWindow.webContents.send(ipcTypes.toRenderer.watcher.started);
}
} }
async function StopWatcher(): Promise<boolean> { async function StopWatcher(): Promise<boolean> {

View File

@@ -7,15 +7,39 @@ import {
selectWatcherStatus, selectWatcherStatus,
} from "@renderer/redux/app.slice"; } from "@renderer/redux/app.slice";
import { useAppSelector } from "@renderer/redux/reduxHooks"; 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 { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json"; import ipcTypes from "../../../../util/ipcTypes.json";
import { useEffect, useState } from "react";
const SettingsWatcher: React.FC = () => { const SettingsWatcher: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const isWatcherStarted = useAppSelector(selectWatcherStatus); const isWatcherStarted = useAppSelector(selectWatcherStatus);
const watcherError = useAppSelector(selectWatcherError); 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 => { const handleStart = (): void => {
window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start); window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start);
}; };
@@ -24,6 +48,31 @@ const SettingsWatcher: React.FC = () => {
window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.stop); 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 ( return (
<Card title={t("settings.labels.watcherstatus")}> <Card title={t("settings.labels.watcherstatus")}>
<Space> <Space>
@@ -47,6 +96,19 @@ const SettingsWatcher: React.FC = () => {
{t("settings.labels.stopped")} {t("settings.labels.stopped")}
</Space> </Space>
)} )}
<Switch
checked={!pollingState.enabled}
onChange={toggleWatcherMode}
checkedChildren={t("settings.labels.watchermoderealtime")}
unCheckedChildren={t("settings.labels.watchermodepolling")}
/>
<InputNumber
title={t("settings.labels.pollinginterval")}
disabled={!pollingState.enabled}
min={1000}
value={pollingState.interval}
onChange={handlePollingIntervalChange}
/>
{watcherError && <Alert message={watcherError} />} {watcherError && <Alert message={watcherError} />}
</Space> </Space>
</Card> </Card>

View File

@@ -1,13 +1,15 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import log from "electron-log/renderer"; import log from "electron-log/renderer";
import type { RootState } from "./redux-store"; import type { RootState } from "./redux-store";
import { update } from "lodash";
import { notification } from "antd";
interface AppState { interface AppState {
value: number; value: number;
watcher: { watcher: {
started: boolean; started: boolean;
error: string | null; error: string | null;
polling: {
enabled: boolean;
interval: number;
};
}; };
updates: { updates: {
available: boolean; available: boolean;
@@ -24,6 +26,10 @@ const initialState: AppState = {
watcher: { watcher: {
started: false, started: false,
error: null, error: null,
polling: {
enabled: false,
interval: 30000,
},
}, },
updates: { updates: {
available: false, available: false,
@@ -58,16 +64,26 @@ export const appSlice = createSlice({
state.updates.available = true; state.updates.available = true;
state.updates.checking = false; state.updates.checking = false;
}, },
updateProgress: (state, action) => { updateProgress: (
state,
action: PayloadAction<{ progress: number; speed: number }>,
) => {
state.updates.available = true; state.updates.available = true;
state.updates.progress = action?.progress; state.updates.progress = action.payload.progress;
state.updates.speed = action?.speed; state.updates.speed = action.payload.speed;
}, },
updateDownloaded: (state) => { updateDownloaded: (state) => {
state.updates.completed = true; state.updates.completed = true;
state.updates.progress = 100; state.updates.progress = 100;
state.updates.speed = 0; 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, updateChecking,
updateDownloaded, updateDownloaded,
updateProgress, updateProgress,
setWatcherPolling,
} = appSlice.actions; } = appSlice.actions;
// Other code such as selectors can use the imported `RootState` type // 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 => export const selectAppUpdateCompleted = (state: RootState): boolean =>
state.app.updates.completed; state.app.updates.completed;
export const selectWatcherPolling = (
state: RootState,
): {
enabled: boolean;
interval: number;
} => state.app.watcher.polling;
//Async Functions - Thunks //Async Functions - Thunks
// Define a thunk that dispatches those action creators // Define a thunk that dispatches those action creators
// const fetchUsers = () => async (dispatch) => { // const fetchUsers = () => async (dispatch) => {
@@ -108,12 +132,4 @@ export const selectAppUpdateCompleted = (state: RootState): boolean =>
// // dispatch(incrementByAmount(100)); // // 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; export default appSlice.reducer;

View File

@@ -1,5 +1,7 @@
//Set up all of the IPC handlers. //Set up all of the IPC handlers.
import { import {
selectWatcherPolling,
setWatcherPolling,
updateAvailable, updateAvailable,
updateChecking, updateChecking,
updateDownloaded, updateDownloaded,
@@ -84,3 +86,7 @@ ipcRenderer.on(
dispatch(updateDownloaded()); dispatch(updateDownloaded());
}, },
); );
ipcRenderer.on(ipcTypes.toRenderer.watcher.polling, (event, arg) => {
dispatch(setWatcherPolling({ enabled: arg.enabled, interval: arg.interval }));
});

View File

@@ -19,6 +19,10 @@
"get": "toMain_settings_filepaths_get", "get": "toMain_settings_filepaths_get",
"add": "toMain_settings_filepaths_add", "add": "toMain_settings_filepaths_add",
"remove": "toMain_settings_filepaths_remove" "remove": "toMain_settings_filepaths_remove"
},
"watcher": {
"getpolling": "toMain_settings_watcher_getpolling",
"setpolling": "toMain_settings_watcher_setpolling"
} }
}, },
"user": { "user": {
@@ -30,7 +34,8 @@
"watcher": { "watcher": {
"started": "toRenderer_watcher_started", "started": "toRenderer_watcher_started",
"stopped": "toRenderer_watcher_stopped", "stopped": "toRenderer_watcher_stopped",
"error": "toRenderer_watcher_error" "error": "toRenderer_watcher_error",
"polling": "toRenderer_watcher_polling"
}, },
"updates": { "updates": {
"checking": "toRenderer_updates_checking", "checking": "toRenderer_updates_checking",

View File

@@ -1,5 +1,5 @@
{ {
"toolbar": { "toolbar": {
"help": "Help" "help": "Help"
} }
} }

View File

@@ -1,27 +1,29 @@
{ {
"translation": { "translation": {
"navigation": { "navigation": {
"home": "Home", "home": "Home",
"settings": "Settings" "settings": "Settings"
}, },
"settings": { "settings": {
"actions": { "actions": {
"addpath": "Add path", "addpath": "Add path",
"startwatcher": "Start Watcher", "startwatcher": "Start Watcher",
"stopwatcher": "Stop Watcher\n" "stopwatcher": "Stop Watcher\n"
}, },
"labels": { "labels": {
"started": "Started", "started": "Started",
"stopped": "Stopped", "stopped": "Stopped",
"watchedpaths": "Watched Paths", "watchedpaths": "Watched Paths",
"watcherstatus": "Watcher Status" "watchermodepolling": "Polling",
} "watchermoderealtime": "Real Time",
}, "watcherstatus": "Watcher Status"
"updates": { }
"apply": "Apply Update", },
"available": "An update is available.", "updates": {
"download": "Download Update", "apply": "Apply Update",
"downloading": "An update is downloading." "available": "An update is available.",
} "download": "Download Update",
} "downloading": "An update is downloading."
}
}
} }

View File

@@ -165,6 +165,32 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>watchermodepolling</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>watchermoderealtime</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>watcherstatus</name> <name>watcherstatus</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>