mirror of https://github.com/jetkvm/kvm.git
refactor(jiggler): simplify JigglerSetting and integrate with settings page
This commit is contained in:
parent
77ffdb4362
commit
0feb5a9c77
|
@ -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<JigglerConfig>({
|
||||
inactivity_limit_seconds: 20,
|
||||
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 (
|
||||
<div className="">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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" && (
|
||||
<div className="space-y-2">
|
||||
<div className="grid max-w-sm grid-cols-1 items-end gap-y-2">
|
||||
<InputFieldWithLabel
|
||||
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 * * * * *"
|
||||
defaultValue={jigglerConfigState.schedule_cron_tab}
|
||||
onChange={e => handleJigglerScheduleCronTabChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<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)
|
||||
onChange={e =>
|
||||
setJigglerConfigState({
|
||||
...jigglerConfigState,
|
||||
schedule_cron_tab: e.target.value,
|
||||
})
|
||||
}
|
||||
}}
|
||||
options={[...jigglerJitterConfigs, {value: "custom", label: "Custom"}]}
|
||||
/>
|
||||
{jitterPercentage === "custom" && (
|
||||
|
||||
<InputFieldWithLabel
|
||||
required
|
||||
label="Jitter Percentage"
|
||||
placeholder="25"
|
||||
defaultValue={jigglerConfigState.jitter_percentage}
|
||||
size="SM"
|
||||
label="Inactivity Limit Seconds"
|
||||
description="Seconds of inactivity before triggering a jiggle again"
|
||||
value={jigglerConfigState.inactivity_limit_seconds}
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
onChange={e => handleJigglerJitterPercentageChange(e.target.value)}
|
||||
onChange={e =>
|
||||
setJigglerConfigState({
|
||||
...jigglerConfigState,
|
||||
inactivity_limit_seconds: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<SelectMenuBasic
|
||||
|
||||
<InputFieldWithLabel
|
||||
required
|
||||
size="SM"
|
||||
label="Inactivity Limit Seconds"
|
||||
className="max-w-[192px]"
|
||||
value={jigglerConfigState.inactivity_limit_seconds}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
handleJigglerInactivityLimitSecondsChange(e.target.value);
|
||||
}}
|
||||
options={[...jigglerInactivityConfigs]}
|
||||
label="Random delay"
|
||||
description="To avoid recognizable patterns"
|
||||
placeholder="25"
|
||||
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
|
||||
defaultValue={jigglerConfigState.jitter_percentage}
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
onChange={e =>
|
||||
setJigglerConfigState({
|
||||
...jigglerConfigState,
|
||||
jitter_percentage: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex gap-x-2">
|
||||
<Button
|
||||
loading={loading}
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update Jiggler Config"
|
||||
onClick={() => handleJigglerConfigSave(jigglerConfigState)}
|
||||
text="Save Jiggler Config"
|
||||
onClick={() => onSave(jigglerConfigState)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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<JigglerValues | null>(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;
|
||||
|
||||
// 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;
|
||||
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]);
|
||||
|
||||
const handleJigglerChange = (enabled: boolean) => {
|
||||
send("setJigglerState", { enabled }, resp => {
|
||||
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) {
|
||||
notifications.error(
|
||||
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;
|
||||
}
|
||||
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 (
|
||||
|
@ -102,20 +206,30 @@ export default function SettingsMouseRoute() {
|
|||
title="Jiggler"
|
||||
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
|
||||
>
|
||||
<Checkbox
|
||||
checked={jiggler}
|
||||
onChange={e => handleJigglerChange(e.target.checked)}
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
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>
|
||||
|
||||
{jiggler && (
|
||||
<>
|
||||
<SettingsSectionHeader
|
||||
title="Jiggler Config"
|
||||
description="Control the jiggler schedule"
|
||||
/>
|
||||
<JigglerSetting />
|
||||
</>
|
||||
{selectedJigglerOption === "custom" && (
|
||||
<SettingsNestedSection>
|
||||
<JigglerSetting onSave={saveJigglerConfig} />
|
||||
</SettingsNestedSection>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<SettingsItem title="Modes" description="Choose the mouse input mode" />
|
||||
|
|
Loading…
Reference in New Issue