From 6982181adde5672712196ceb5459d9ac615d7ad7 Mon Sep 17 00:00:00 2001 From: suu <46421931+0suu@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:00:52 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=87=E3=83=90=E3=82=A4=E3=82=B9=E3=82=AB?= =?UTF-8?q?=E3=83=BC=E3=83=89=E5=86=85=E3=81=AE=E5=80=A4=E3=82=92=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/main.ts | 9 ++ .../device-controls/CurtainControls.tsx | 13 +- .../device-controls/DynamicControls.tsx | 18 +-- .../device-controls/FanControls.tsx | 13 +- .../device-controls/HumidifierControls.tsx | 28 ++++- .../InfraredRemoteControls.tsx | 43 +++++-- .../device-controls/LightControls.tsx | 22 +++- .../device-controls/VacuumControls.tsx | 16 ++- .../src/components/device-controls/utils.tsx | 2 + .../usePersistedDeviceControlState.test.tsx | 87 +++++++++++++ .../hooks/usePersistedDeviceControlState.ts | 119 ++++++++++++++++++ 11 files changed, 332 insertions(+), 38 deletions(-) create mode 100644 src/renderer/src/hooks/__tests__/usePersistedDeviceControlState.test.tsx create mode 100644 src/renderer/src/hooks/usePersistedDeviceControlState.ts diff --git a/src/main/main.ts b/src/main/main.ts index fb59650..f389f0e 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -19,6 +19,7 @@ interface AppSettings { nightLightSceneAssignments?: Record; lastView?: "list" | "settings" | "scenes"; confirmOnOffPressActions?: Record; + deviceControlDrafts?: Record>; windowBounds?: { width: number; height: number; x: number; y: number }; } @@ -50,6 +51,14 @@ const schema = { default: {}, additionalProperties: { type: "boolean" }, }, + deviceControlDrafts: { + type: "object", + default: {}, + additionalProperties: { + type: "object", + additionalProperties: true, + }, + }, windowBounds: { type: "object", properties: { diff --git a/src/renderer/src/components/device-controls/CurtainControls.tsx b/src/renderer/src/components/device-controls/CurtainControls.tsx index d00ad7f..7359bf6 100644 --- a/src/renderer/src/components/device-controls/CurtainControls.tsx +++ b/src/renderer/src/components/device-controls/CurtainControls.tsx @@ -1,8 +1,9 @@ -import React, { useState } from "react"; +import React from "react"; import { Box, Button, Slider, Typography } from "@mui/material"; import { useTranslation } from "../../useTranslation"; import { DeviceControlProps } from "./DeviceControls.types"; import { clamp, SectionLabel } from "./utils"; +import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState"; import { COMMAND_SET_POSITION, COMMAND_TURN_OFF, @@ -10,12 +11,16 @@ import { } from "../../constants/commandConstants"; export const CurtainControls: React.FC = ({ + device, sendCommand, controlsDisabled, dense, }) => { const { t } = useTranslation(); - const [position, setPosition] = useState(50); + const [draft, setDraft] = usePersistedDeviceControlState(device.deviceId, "curtain", { + position: 50, + }); + const { position } = draft; const gridColumns = dense ? 2 : 3; const maxControlWidth = dense ? 360 : 520; const buttonGridSx = { @@ -68,7 +73,9 @@ export const CurtainControls: React.FC = ({ min={0} max={100} step={1} - onChange={(_, value) => setPosition(value as number)} + onChange={(_, value) => + setDraft((current) => ({ ...current, position: value as number })) + } onChangeCommitted={(_, value) => sendCommand( COMMAND_SET_POSITION, diff --git a/src/renderer/src/components/device-controls/DynamicControls.tsx b/src/renderer/src/components/device-controls/DynamicControls.tsx index cfeddf2..9c5be51 100644 --- a/src/renderer/src/components/device-controls/DynamicControls.tsx +++ b/src/renderer/src/components/device-controls/DynamicControls.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useMemo } from "react"; import { Box, Button, @@ -16,6 +16,7 @@ import { COMMAND_TYPE_COMMAND, DEFAULT_PARAMETER, } from "../../constants/commandConstants"; +import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState"; export const DynamicControls: React.FC = ({ device, @@ -24,10 +25,7 @@ export const DynamicControls: React.FC = ({ dense, }) => { const { definition, isHiddenByDefinition } = useDeviceType(device); - const [dynamicParams, setDynamicParams] = useState>({}); - const maxControlWidth = dense ? 360 : 520; - - useEffect(() => { + const defaultDynamicParams = useMemo(() => { const defaults: Record = {}; (definition?.commands || []).forEach((cmd) => { const param = cmd.parameter; @@ -39,8 +37,14 @@ export const DynamicControls: React.FC = ({ if (param.type === "text" && param.defaultValue !== undefined) defaults[cmd.command] = param.defaultValue; }); - setDynamicParams(defaults); - }, [definition, device.deviceId]); + return defaults; + }, [definition]); + const [dynamicParams, setDynamicParams] = usePersistedDeviceControlState( + device.deviceId, + "dynamicControls", + defaultDynamicParams + ); + const maxControlWidth = dense ? 360 : 520; const resolveParameterValue = (cmd: DeviceCommandDefinition) => { const spec = cmd.parameter; diff --git a/src/renderer/src/components/device-controls/FanControls.tsx b/src/renderer/src/components/device-controls/FanControls.tsx index ee6cd42..a76fe74 100644 --- a/src/renderer/src/components/device-controls/FanControls.tsx +++ b/src/renderer/src/components/device-controls/FanControls.tsx @@ -1,20 +1,25 @@ -import React, { useState } from "react"; +import React from "react"; import { Box, Button, Slider, Typography } from "@mui/material"; import { useTranslation } from "../../useTranslation"; import { DeviceControlProps } from "./DeviceControls.types"; import { clamp, SectionLabel } from "./utils"; +import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState"; import { COMMAND_TURN_OFF, COMMAND_TURN_ON, } from "../../constants/commandConstants"; export const FanControls: React.FC = ({ + device, sendCommand, controlsDisabled, dense, }) => { const { t } = useTranslation(); - const [fanSpeed, setFanSpeed] = useState(50); + const [draft, setDraft] = usePersistedDeviceControlState(device.deviceId, "fan", { + fanSpeed: 50, + }); + const { fanSpeed } = draft; const gridColumns = dense ? 2 : 3; const maxControlWidth = dense ? 360 : 520; @@ -93,7 +98,9 @@ export const FanControls: React.FC = ({ min={1} max={100} step={1} - onChange={(_, value) => setFanSpeed(value as number)} + onChange={(_, value) => + setDraft((current) => ({ ...current, fanSpeed: value as number })) + } onChangeCommitted={(_, value) => sendCommand("setWindSpeed", clamp(value as number, 1, 100)) } diff --git a/src/renderer/src/components/device-controls/HumidifierControls.tsx b/src/renderer/src/components/device-controls/HumidifierControls.tsx index e130bd9..1cb7db0 100644 --- a/src/renderer/src/components/device-controls/HumidifierControls.tsx +++ b/src/renderer/src/components/device-controls/HumidifierControls.tsx @@ -1,9 +1,10 @@ -import React, { useState } from "react"; +import React from "react"; import { Box, Button, Slider, Stack, Typography } from "@mui/material"; import { useTranslation } from "../../useTranslation"; import { DeviceControlProps } from "./DeviceControls.types"; import { clamp, SectionLabel } from "./utils"; import { useDeviceType } from "../../hooks/useDeviceType"; +import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState"; import { COMMAND_SET_MODE, COMMAND_TURN_OFF, @@ -20,8 +21,15 @@ export const HumidifierControls: React.FC = ({ const { t } = useTranslation(); const { isEvaporativeHumidifier } = useDeviceType(device); - const [targetHumidify, setTargetHumidify] = useState(60); - const [humidifierLevel, setHumidifierLevel] = useState(50); + const [draft, setDraft] = usePersistedDeviceControlState( + device.deviceId, + "humidifier", + { + targetHumidify: 60, + humidifierLevel: 50, + } + ); + const { targetHumidify, humidifierLevel } = draft; type EvaporativeModeKey = | "level1" @@ -182,7 +190,12 @@ export const HumidifierControls: React.FC = ({ min={0} max={100} step={1} - onChange={(_, value) => setHumidifierLevel(value as number)} + onChange={(_, value) => + setDraft((current) => ({ + ...current, + humidifierLevel: value as number, + })) + } onChangeCommitted={(_, value) => sendCommand(COMMAND_SET_MODE, clamp(value as number, 0, 100)) } @@ -229,7 +242,12 @@ export const HumidifierControls: React.FC = ({ min={0} max={100} step={1} - onChange={(_, value) => setTargetHumidify(value as number)} + onChange={(_, value) => + setDraft((current) => ({ + ...current, + targetHumidify: value as number, + })) + } valueLabelDisplay="auto" aria-label="Target humidity" disabled={controlsDisabled} diff --git a/src/renderer/src/components/device-controls/InfraredRemoteControls.tsx b/src/renderer/src/components/device-controls/InfraredRemoteControls.tsx index aa98bbf..dd9f418 100644 --- a/src/renderer/src/components/device-controls/InfraredRemoteControls.tsx +++ b/src/renderer/src/components/device-controls/InfraredRemoteControls.tsx @@ -1,9 +1,10 @@ -import React, { useState } from "react"; +import React from "react"; import { Box, Button, MenuItem, Slider, Stack, TextField, Typography } from "@mui/material"; import { useTranslation } from "../../useTranslation"; import { DeviceControlProps } from "./DeviceControls.types"; import { SectionLabel, clamp } from "./utils"; import { useDeviceType } from "../../hooks/useDeviceType"; +import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState"; import { CustomCommandControls } from "./CustomCommandControls"; import { COMMAND_TURN_OFF, @@ -23,11 +24,18 @@ export const InfraredRemoteControls: React.FC = (props) => { isRemoteLight, } = useDeviceType(device); - const [acTemperature, setAcTemperature] = useState(26); - const [acMode, setAcMode] = useState("2"); // 2: cool (per docs) - const [acFanSpeed, setAcFanSpeed] = useState("3"); // 3: medium - const [acPower, setAcPower] = useState<"on" | "off">("on"); - const [tvChannel, setTvChannel] = useState(""); + const [draft, setDraft] = usePersistedDeviceControlState( + device.deviceId, + "infraredRemote", + { + acTemperature: 26, + acMode: "2", // 2: cool (per docs) + acFanSpeed: "3", // 3: medium + acPower: "on" as "on" | "off", + tvChannel: "", + } + ); + const { acTemperature, acMode, acFanSpeed, acPower, tvChannel } = draft; const gridColumns = dense ? 2 : 3; const maxControlWidth = dense ? 360 : 520; @@ -87,7 +95,9 @@ export const InfraredRemoteControls: React.FC = (props) => { min={16} max={32} step={1} - onChange={(_, v) => setAcTemperature(v as number)} + onChange={(_, v) => + setDraft((current) => ({ ...current, acTemperature: v as number })) + } valueLabelDisplay="auto" aria-label="AC temperature" disabled={controlsDisabled} @@ -99,7 +109,9 @@ export const InfraredRemoteControls: React.FC = (props) => { size="small" label={t("Mode")} value={acMode} - onChange={(e) => setAcMode(e.target.value)} + onChange={(e) => + setDraft((current) => ({ ...current, acMode: e.target.value })) + } fullWidth > {t("Auto")} @@ -113,7 +125,9 @@ export const InfraredRemoteControls: React.FC = (props) => { size="small" label={t("Fan speed")} value={acFanSpeed} - onChange={(e) => setAcFanSpeed(e.target.value)} + onChange={(e) => + setDraft((current) => ({ ...current, acFanSpeed: e.target.value })) + } fullWidth > {t("Auto")} @@ -126,7 +140,12 @@ export const InfraredRemoteControls: React.FC = (props) => { size="small" label={t("Power")} value={acPower} - onChange={(e) => setAcPower(e.target.value as "on" | "off")} + onChange={(e) => + setDraft((current) => ({ + ...current, + acPower: e.target.value as "on" | "off", + })) + } fullWidth > {t("On")} @@ -199,7 +218,9 @@ export const InfraredRemoteControls: React.FC = (props) => { label="Channel number" size="small" value={tvChannel} - onChange={(e) => setTvChannel(e.target.value)} + onChange={(e) => + setDraft((current) => ({ ...current, tvChannel: e.target.value })) + } placeholder="e.g. 15" />