refactor(jiggler): simplify JigglerSetting and integrate with settings page

This commit is contained in:
Adam Shiervani 2025-08-12 14:31:37 +02:00
parent 77ffdb4362
commit 0feb5a9c77
2 changed files with 228 additions and 239 deletions

View File

@ -1,13 +1,9 @@
import { useCallback, useEffect, useState } from "react"; import { useState } from "react";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import { InputFieldWithLabel } from "./InputField"; import { InputFieldWithLabel } from "./InputField";
import ExtLink from "./ExtLink";
export interface JigglerConfig { export interface JigglerConfig {
inactivity_limit_seconds: number; inactivity_limit_seconds: number;
@ -15,205 +11,84 @@ export interface JigglerConfig {
schedule_cron_tab: string; schedule_cron_tab: string;
} }
const jigglerCrontabConfigs = [ export function JigglerSetting({
{ onSave,
label: "Every 20 seconds", }: {
value: "*/20 * * * * *", onSave: (jigglerConfig: JigglerConfig) => void;
}, }) {
{
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("");
const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>({ const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>({
inactivity_limit_seconds: 20, inactivity_limit_seconds: 20,
jitter_percentage: 0, jitter_percentage: 0,
schedule_cron_tab: "*/20 * * * * *" 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 ( return (
<div className=""> <div className="space-y-2">
<div className="grid grid-cols-2 gap-4"> <div className="grid max-w-sm grid-cols-1 items-end gap-y-2">
<SelectMenuBasic
size="SM"
label="Schedule"
className="max-w-[192px]"
value={scheduleCronTab}
fullWidth
onChange={e => {
setScheduleCronTab(e.target.value);
if (e.target.value != "custom") {
handleJigglerScheduleCronTabChange(e.target.value);
}
}}
options={[...jigglerCrontabConfigs, {value: "custom", label: "Custom"}]}
/>
{scheduleCronTab === "custom" && (
<InputFieldWithLabel <InputFieldWithLabel
required required
label="Jiggler Crontab" size="SM"
label="Cron Schedule"
description={
<span>
Generate with{" "}
<ExtLink className="text-blue-700 underline" href="https://crontab.guru/">
crontab.guru
</ExtLink>
</span>
}
placeholder="*/20 * * * * *" placeholder="*/20 * * * * *"
defaultValue={jigglerConfigState.schedule_cron_tab} defaultValue={jigglerConfigState.schedule_cron_tab}
onChange={e => handleJigglerScheduleCronTabChange(e.target.value)} onChange={e =>
/> setJigglerConfigState({
)} ...jigglerConfigState,
</div> schedule_cron_tab: e.target.value,
<div className="grid grid-cols-2 gap-4"> })
<SelectMenuBasic
size="SM"
label="Jitter Percentage"
className="max-w-[192px]"
value={jitterPercentage}
fullWidth
onChange={e => {
setJitterPercentage(e.target.value);
if (e.target.value != "custom") {
handleJigglerJitterPercentageChange(e.target.value)
} }
}}
options={[...jigglerJitterConfigs, {value: "custom", label: "Custom"}]}
/> />
{jitterPercentage === "custom" && (
<InputFieldWithLabel <InputFieldWithLabel
required size="SM"
label="Jitter Percentage" label="Inactivity Limit Seconds"
placeholder="25" description="Seconds of inactivity before triggering a jiggle again"
defaultValue={jigglerConfigState.jitter_percentage} value={jigglerConfigState.inactivity_limit_seconds}
type="number" type="number"
min="1" min="1"
max="100" max="100"
onChange={e => handleJigglerJitterPercentageChange(e.target.value)} onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
inactivity_limit_seconds: Number(e.target.value),
})
}
/> />
)}
</div> <InputFieldWithLabel
<div className="grid grid-cols-2 gap-4"> required
<SelectMenuBasic
size="SM" size="SM"
label="Inactivity Limit Seconds" label="Random delay"
className="max-w-[192px]" description="To avoid recognizable patterns"
value={jigglerConfigState.inactivity_limit_seconds} placeholder="25"
fullWidth TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
onChange={e => { defaultValue={jigglerConfigState.jitter_percentage}
handleJigglerInactivityLimitSecondsChange(e.target.value); type="number"
}} min="0"
options={[...jigglerInactivityConfigs]} max="100"
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
jitter_percentage: Number(e.target.value),
})
}
/> />
</div> </div>
<div className="mt-6 flex gap-x-2"> <div className="mt-6 flex gap-x-2">
<Button <Button
loading={loading}
size="SM" size="SM"
theme="primary" theme="primary"
text="Update Jiggler Config" text="Save Jiggler Config"
onClick={() => handleJigglerConfigSave(jigglerConfigState)} onClick={() => onSave(jigglerConfigState)}
/> />
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
import { CheckCircleIcon } from "@heroicons/react/16/solid"; 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 MouseIcon from "@/assets/mouse-icon.svg";
import PointingFinger from "@/assets/pointing-finger.svg"; import PointingFinger from "@/assets/pointing-finger.svg";
@ -7,15 +7,63 @@ import { GridCard } from "@/components/Card";
import { Checkbox } from "@/components/Checkbox"; import { Checkbox } from "@/components/Checkbox";
import { useSettingsStore } from "@/hooks/stores"; import { useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JigglerSetting } from "@components/JigglerSetting";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { cx } from "../cva.config"; import { cx } from "../cva.config";
import { SettingsItem } from "./devices.$id.settings"; 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() { export default function SettingsMouseRoute() {
const hideCursor = useSettingsStore(state => state.isCursorHidden); const hideCursor = useSettingsStore(state => state.isCursorHidden);
@ -24,12 +72,11 @@ export default function SettingsMouseRoute() {
const mouseMode = useSettingsStore(state => state.mouseMode); const mouseMode = useSettingsStore(state => state.mouseMode);
const setMouseMode = useSettingsStore(state => state.setMouseMode); const setMouseMode = useSettingsStore(state => state.setMouseMode);
const [jiggler, setJiggler] = useState(false);
const scrollThrottling = useSettingsStore(state => state.scrollThrottling); const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
const setScrollThrottling = useSettingsStore( const setScrollThrottling = useSettingsStore(state => state.setScrollThrottling);
state => state.setScrollThrottling,
); const [selectedJigglerOption, setSelectedJigglerOption] =
useState<JigglerValues | null>(null);
const scrollThrottlingOptions = [ const scrollThrottlingOptions = [
{ value: "0", label: "Off" }, { value: "0", label: "Off" },
@ -41,28 +88,85 @@ export default function SettingsMouseRoute() {
const [send] = useJsonRpc(); const [send] = useJsonRpc();
useEffect(() => { const syncJigglerSettings = useCallback(() => {
send("getJigglerState", {}, resp => { send("getJigglerState", {}, resp => {
if ("error" in resp) return; if ("error" in resp) return;
setJiggler(resp.result as boolean); const isEnabled = 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 => { send("getJigglerConfig", {}, resp => {
if ("error" in resp) return; if ("error" in resp) return;
setJiggler(resp.result as boolean); 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]); }, [send]);
const handleJigglerChange = (enabled: boolean) => { useEffect(() => {
send("setJigglerState", { enabled }, resp => { 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) { if ("error" in resp) {
notifications.error( return notifications.error(
`Failed to set jiggler state: ${resp.error.data || "Unknown 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; return;
} }
setJiggler(enabled);
// 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 ( return (
@ -102,20 +206,30 @@ export default function SettingsMouseRoute() {
title="Jiggler" title="Jiggler"
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating" description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
> >
<Checkbox <SelectMenuBasic
checked={jiggler} size="SM"
onChange={e => handleJigglerChange(e.target.checked)} label=""
value={selectedJigglerOption || "disabled"}
options={[
...jigglerOptions.map(option => ({
value: option.value,
label: option.label,
})),
{ value: "custom", label: "Custom" },
]}
onChange={e => {
handleJigglerChange(
e.target.value as (typeof jigglerOptions)[number]["value"],
);
}}
fullWidth
/> />
</SettingsItem> </SettingsItem>
{jiggler && ( {selectedJigglerOption === "custom" && (
<> <SettingsNestedSection>
<SettingsSectionHeader <JigglerSetting onSave={saveJigglerConfig} />
title="Jiggler Config" </SettingsNestedSection>
description="Control the jiggler schedule"
/>
<JigglerSetting />
</>
)} )}
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem title="Modes" description="Choose the mouse input mode" /> <SettingsItem title="Modes" description="Choose the mouse input mode" />