From 0feb5a9c77eacb0804be74a4e369b016d06e31d2 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Tue, 12 Aug 2025 14:31:37 +0200 Subject: [PATCH] refactor(jiggler): simplify JigglerSetting and integrate with settings page --- ui/src/components/JigglerSetting.tsx | 253 +++++-------------- ui/src/routes/devices.$id.settings.mouse.tsx | 214 ++++++++++++---- 2 files changed, 228 insertions(+), 239 deletions(-) diff --git a/ui/src/components/JigglerSetting.tsx b/ui/src/components/JigglerSetting.tsx index 0f2c745..d881089 100644 --- a/ui/src/components/JigglerSetting.tsx +++ b/ui/src/components/JigglerSetting.tsx @@ -1,13 +1,9 @@ -import { useCallback, useEffect, useState } from "react"; +import { useState } from "react"; import { Button } from "@components/Button"; -import { SelectMenuBasic } from "@components/SelectMenuBasic"; - -import { useJsonRpc } from "../hooks/useJsonRpc"; -import notifications from "../notifications"; import { InputFieldWithLabel } from "./InputField"; - +import ExtLink from "./ExtLink"; export interface JigglerConfig { inactivity_limit_seconds: number; @@ -15,205 +11,84 @@ export interface JigglerConfig { schedule_cron_tab: string; } -const jigglerCrontabConfigs = [ - { - label: "Every 20 seconds", - value: "*/20 * * * * *", - }, - { - label: "Every 40 seconds", - value: "*/40 * * * * *", - }, - { - label: "Every 1 minute", - value: "0 * * * * *", - }, - { - label: "Every 3 minutes", - value: "0 */3 * * * *", - }, -]; - -const jigglerJitterConfigs = [ - { - label: "No Jitter", - value: "0", - }, - { - label: "10%", - value: "20", - }, - { - label: "25%", - value: "25", - }, - { - label: "50%", - value: "50", - }, -]; - -const jigglerInactivityConfigs = [ - { - label: "20 Seconds", - value: "20", - }, - { - label: "40 Seconds", - value: "40", - }, - { - label: "1 Minute", - value: "60", - }, - { - label: "3 Minutes", - value: "180", - }, -]; - -export function JigglerSetting() { - const [send] = useJsonRpc(); - const [loading, setLoading] = useState(false); - const [jitterPercentage, setJitterPercentage] = useState(""); - const [scheduleCronTab, setScheduleCronTab] = useState(""); - +export function JigglerSetting({ + onSave, +}: { + onSave: (jigglerConfig: JigglerConfig) => void; +}) { const [jigglerConfigState, setJigglerConfigState] = useState({ inactivity_limit_seconds: 20, - jitter_percentage: 0, - schedule_cron_tab: "*/20 * * * * *" + jitter_percentage: 0, + schedule_cron_tab: "*/20 * * * * *", }); - const syncJigglerConfig = useCallback(() => { - send("getJigglerConfig", {}, resp => { - if ("error" in resp) return; - const result = resp.result as JigglerConfig; - setJigglerConfigState(result); - - const jitterPercentage = jigglerJitterConfigs.map(u => u.value).includes(result.jitter_percentage.toString()) - ? result.jitter_percentage.toString() - : "custom"; - setJitterPercentage(jitterPercentage) - - const scheduleCronTab = jigglerCrontabConfigs.map(u => u.value).includes(result.schedule_cron_tab) - ? result.schedule_cron_tab - : "custom"; - setScheduleCronTab(scheduleCronTab) - }); - }, [send]); - - useEffect(() => { - syncJigglerConfig() - }, [send, syncJigglerConfig]); - - const handleJigglerInactivityLimitSecondsChange = (value: string) => { - setJigglerConfigState({ ...jigglerConfigState, inactivity_limit_seconds: Number(value) }); - }; - - const handleJigglerJitterPercentageChange = (value: string) => { - setJigglerConfigState({ ...jigglerConfigState, jitter_percentage: Number(value) }); - }; - - const handleJigglerScheduleCronTabChange = (value: string) => { - setJigglerConfigState({ ...jigglerConfigState, schedule_cron_tab: value }); - }; - - const handleJigglerConfigSave = useCallback( - (jigglerConfig: JigglerConfig) => { - setLoading(true); - send("setJigglerConfig", { jigglerConfig }, async resp => { - if ("error" in resp) { - notifications.error( - `Failed to set jiggler config: ${resp.error.data || "Unknown error"}`, - ); - setLoading(false); - return; - } - setLoading(false); - notifications.success( - `Jiggler Config successfully updated`, - ); - syncJigglerConfig(); - }); - }, - [send, syncJigglerConfig], - ); - return ( -
-
- +
+ { - setScheduleCronTab(e.target.value); - if (e.target.value != "custom") { - handleJigglerScheduleCronTabChange(e.target.value); - } - }} - options={[...jigglerCrontabConfigs, {value: "custom", label: "Custom"}]} + label="Cron Schedule" + description={ + + Generate with{" "} + + crontab.guru + + + } + placeholder="*/20 * * * * *" + defaultValue={jigglerConfigState.schedule_cron_tab} + onChange={e => + setJigglerConfigState({ + ...jigglerConfigState, + schedule_cron_tab: e.target.value, + }) + } /> - {scheduleCronTab === "custom" && ( - handleJigglerScheduleCronTabChange(e.target.value)} - /> - )} -
-
- { - setJitterPercentage(e.target.value); - if (e.target.value != "custom") { - handleJigglerJitterPercentageChange(e.target.value) - } - }} - options={[...jigglerJitterConfigs, {value: "custom", label: "Custom"}]} - /> - {jitterPercentage === "custom" && ( - handleJigglerJitterPercentageChange(e.target.value)} - /> - )} -
-
- { - handleJigglerInactivityLimitSecondsChange(e.target.value); - }} - options={[...jigglerInactivityConfigs]} + type="number" + min="1" + max="100" + onChange={e => + setJigglerConfigState({ + ...jigglerConfigState, + inactivity_limit_seconds: Number(e.target.value), + }) + } + /> + + %} + defaultValue={jigglerConfigState.jitter_percentage} + type="number" + min="0" + max="100" + onChange={e => + setJigglerConfigState({ + ...jigglerConfigState, + jitter_percentage: Number(e.target.value), + }) + } />
+
diff --git a/ui/src/routes/devices.$id.settings.mouse.tsx b/ui/src/routes/devices.$id.settings.mouse.tsx index 0c827cb..d683721 100644 --- a/ui/src/routes/devices.$id.settings.mouse.tsx +++ b/ui/src/routes/devices.$id.settings.mouse.tsx @@ -1,5 +1,5 @@ import { CheckCircleIcon } from "@heroicons/react/16/solid"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import MouseIcon from "@/assets/mouse-icon.svg"; import PointingFinger from "@/assets/pointing-finger.svg"; @@ -7,15 +7,63 @@ import { GridCard } from "@/components/Card"; import { Checkbox } from "@/components/Checkbox"; import { useSettingsStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; -import notifications from "@/notifications"; import { SettingsPageHeader } from "@components/SettingsPageheader"; -import { JigglerSetting } from "@components/JigglerSetting"; -import { SettingsSectionHeader } from "@components/SettingsSectionHeader"; import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { cx } from "../cva.config"; import { SettingsItem } from "./devices.$id.settings"; +import notifications from "../notifications"; +import SettingsNestedSection from "../components/SettingsNestedSection"; +import { JigglerSetting } from "@components/JigglerSetting"; + +export interface JigglerConfig { + inactivity_limit_seconds: number; + jitter_percentage: number; + schedule_cron_tab: string; +} + +const jigglerOptions = [ + { value: "disabled", label: "Disabled", config: null }, + { + value: "frequent", + label: "Frequent - 30s", + config: { + inactivity_limit_seconds: 30, + jitter_percentage: 25, + schedule_cron_tab: "*/30 * * * * *", + }, + }, + { + value: "standard", + label: "Standard - 1m", + config: { + inactivity_limit_seconds: 60, + jitter_percentage: 25, + schedule_cron_tab: "0 * * * * *", + }, + }, + { + value: "light", + label: "Light - 5m", + config: { + inactivity_limit_seconds: 300, + jitter_percentage: 25, + schedule_cron_tab: "0 */5 * * * *", + }, + }, + { + value: "business_hours", + label: "Business Hours - 1m - (Mon-Fri 9-17)", + config: { + inactivity_limit_seconds: 60, + jitter_percentage: 25, + schedule_cron_tab: "0 * 9-17 * * 1-5", + }, + }, +] as const; + +type JigglerValues = (typeof jigglerOptions)[number]["value"] | "custom"; export default function SettingsMouseRoute() { const hideCursor = useSettingsStore(state => state.isCursorHidden); @@ -24,12 +72,11 @@ export default function SettingsMouseRoute() { const mouseMode = useSettingsStore(state => state.mouseMode); const setMouseMode = useSettingsStore(state => state.setMouseMode); - const [jiggler, setJiggler] = useState(false); - const scrollThrottling = useSettingsStore(state => state.scrollThrottling); - const setScrollThrottling = useSettingsStore( - state => state.setScrollThrottling, - ); + const setScrollThrottling = useSettingsStore(state => state.setScrollThrottling); + + const [selectedJigglerOption, setSelectedJigglerOption] = + useState(null); const scrollThrottlingOptions = [ { value: "0", label: "Off" }, @@ -41,28 +88,85 @@ export default function SettingsMouseRoute() { const [send] = useJsonRpc(); - useEffect(() => { + const syncJigglerSettings = useCallback(() => { send("getJigglerState", {}, resp => { if ("error" in resp) return; - setJiggler(resp.result as boolean); - }); + const isEnabled = resp.result as boolean; - send("getJigglerConfig", {}, resp => { - if ("error" in resp) return; - setJiggler(resp.result as boolean); + // If the jiggler is disabled, set the selected option to "disabled" and nothing else + if (!isEnabled) return setSelectedJigglerOption("disabled"); + + send("getJigglerConfig", {}, resp => { + if ("error" in resp) return; + const result = resp.result as JigglerConfig; + const value = jigglerOptions.find( + o => + o?.config?.inactivity_limit_seconds === result.inactivity_limit_seconds && + o?.config?.jitter_percentage === result.jitter_percentage && + o?.config?.schedule_cron_tab === result.schedule_cron_tab, + )?.value; + + setSelectedJigglerOption(value || "custom"); + }); }); }, [send]); - const handleJigglerChange = (enabled: boolean) => { - send("setJigglerState", { enabled }, resp => { - if ("error" in resp) { - notifications.error( - `Failed to set jiggler state: ${resp.error.data || "Unknown error"}`, - ); - return; - } - setJiggler(enabled); - }); + useEffect(() => { + syncJigglerSettings(); + }, [syncJigglerSettings]); + + const saveJigglerConfig = useCallback( + (jigglerConfig: JigglerConfig) => { + // We assume the jiggler should be set to enabled if the config is being updated + send("setJigglerState", { enabled: true }, async resp => { + if ("error" in resp) { + return notifications.error( + `Failed to set jiggler state: ${resp.error.data || "Unknown error"}`, + ); + } + }); + + send("setJigglerConfig", { jigglerConfig }, async resp => { + if ("error" in resp) { + return notifications.error( + `Failed to set jiggler config: ${resp.error.data || "Unknown error"}`, + ); + } + + notifications.success(`Jiggler Config successfully updated`); + syncJigglerSettings(); + }); + }, + [send, syncJigglerSettings], + ); + + const handleJigglerChange = (option: JigglerValues) => { + if (option === "custom") { + setSelectedJigglerOption("custom"); + // We don't need to sync the jiggler settings when the option is "custom". The user will press "Save" to save the custom settings. + return; + } + + // We don't need to update the device jiggler state when the option is "disabled" + if (option === "disabled") { + send("setJigglerState", { enabled: false }, async resp => { + if ("error" in resp) { + return notifications.error( + `Failed to set jiggler state: ${resp.error.data || "Unknown error"}`, + ); + } + }); + + notifications.success(`Jiggler Config successfully updated`); + return setSelectedJigglerOption("disabled"); + } + + const jigglerConfig = jigglerOptions.find(o => o.value === option)?.config; + if (!jigglerConfig) { + return notifications.error("There was an error setting the jiggler config"); + } + + saveJigglerConfig(jigglerConfig); }; return ( @@ -83,39 +187,49 @@ export default function SettingsMouseRoute() { /> - - setScrollThrottling(parseInt(e.target.value))} - options={scrollThrottlingOptions} - /> - + + setScrollThrottling(parseInt(e.target.value))} + options={scrollThrottlingOptions} + /> + - handleJigglerChange(e.target.checked)} + ({ + value: option.value, + label: option.label, + })), + { value: "custom", label: "Custom" }, + ]} + onChange={e => { + handleJigglerChange( + e.target.value as (typeof jigglerOptions)[number]["value"], + ); + }} + fullWidth /> - {jiggler && ( - <> - - - + {selectedJigglerOption === "custom" && ( + + + )}