From 23245dd54924401f042cc3c7e69d9042d046ee16 Mon Sep 17 00:00:00 2001 From: Fritz Heiden Date: Mon, 14 Apr 2025 23:39:34 +0200 Subject: [PATCH] feat: add remote control to integration view --- www/src/components/remote-control.jsx | 180 +++++++++++++++++++++ www/src/services/remotes-service.js | 9 ++ www/src/views/devices/integration-view.jsx | 57 ++++++- 3 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 www/src/components/remote-control.jsx diff --git a/www/src/components/remote-control.jsx b/www/src/components/remote-control.jsx new file mode 100644 index 0000000..d09f686 --- /dev/null +++ b/www/src/components/remote-control.jsx @@ -0,0 +1,180 @@ +import { createEffect, createMemo, mergeProps } from "solid-js"; +import Remote from "../data/remote"; +import Command from "../data/command"; + +function RemoteControl(props) { + props = mergeProps( + { + remote: new Remote(), + onCommand: () => {}, + }, + props + ); + + const BUTTON_SIZE_REGULAR = "regular"; + const BUTTON_SIZE_SMALL = "small"; + + const BUTTON_HEIGHT = 2; + const BUTTON_WIDTH = BUTTON_HEIGHT * 1.4; + const SMALL_BUTTON_HEIGHT = 1.5; + const SMALL_BUTTON_WIDTH = SMALL_BUTTON_HEIGHT * 1.3334; + const TYPES = Command.TYPES; + + const commandMap = createMemo(() => + props.remote + .getCommands() + .reduce( + (map, command) => ({ ...map, [command.getCommandType()]: command }), + {} + ) + ); + + let layout = [ + [TYPES.POWER, null, TYPES.INPUT], + [TYPES.ONE, TYPES.TWO, TYPES.THREE], + [TYPES.FOUR, TYPES.FIVE, TYPES.SIX], + [TYPES.SEVEN, TYPES.EIGHT, TYPES.NINE], + [TYPES.VOLUME_UP, TYPES.ZERO, TYPES.CHANNEL_UP], + [TYPES.VOLUME_DOWN, TYPES.MUTE, TYPES.CHANNEL_DOWN], + [TYPES.MENU, TYPES.HOME, TYPES.SETTINGS], + [TYPES.OPTIONS, TYPES.UP, TYPES.INFO], + [TYPES.LEFT, TYPES.ENTER, TYPES.RIGHT], + [TYPES.RETURN, TYPES.DOWN, TYPES.EXIT], + [TYPES.RED, TYPES.GREEN, TYPES.YELLOW, TYPES.BLUE], + [TYPES.PLAY, TYPES.PAUSE, TYPES.STOP], + [TYPES.REWIND, null, TYPES.FORWARD], + ]; + + function toButtonProps(type) { + let mapping = { + [TYPES.POWER]: () => ({ icon: "bi-power", text: "" }), + [TYPES.INPUT]: () => ({ icon: "bi-box-arrow-in-right", text: "" }), + [TYPES.VOLUME_UP]: () => ({ icon: "bi-volume-up", text: "" }), + [TYPES.VOLUME_DOWN]: () => ({ icon: "bi-volume-down", text: "" }), + [TYPES.MUTE]: () => ({ icon: "bi-volume-mute", text: "" }), + [TYPES.CHANNEL_UP]: () => ({ icon: "bi-chevron-up", text: "" }), + [TYPES.CHANNEL_DOWN]: () => ({ icon: "bi-chevron-down", text: "" }), + [TYPES.HOME]: () => ({ icon: "bi-house-door", text: "" }), + [TYPES.SETTINGS]: () => ({ icon: "bi-gear", text: "" }), + [TYPES.INFO]: () => ({ icon: "bi-info-circle", text: "" }), + [TYPES.UP]: () => ({ icon: "bi-arrow-up", text: "" }), + [TYPES.DOWN]: () => ({ icon: "bi-arrow-down", text: "" }), + [TYPES.LEFT]: () => ({ icon: "bi-arrow-left", text: "" }), + [TYPES.RIGHT]: () => ({ icon: "bi-arrow-right", text: "" }), + [TYPES.ENTER]: () => ({ icon: "", text: "OK" }), + [TYPES.EXIT]: () => ({ icon: "", text: "EXIT" }), + [TYPES.OPTIONS]: () => ({ icon: "bi-list-task", text: "" }), + [TYPES.RETURN]: () => ({ icon: "bi-arrow-return-left", text: "" }), + [TYPES.ONE]: () => ({ icon: "", text: "1" }), + [TYPES.TWO]: () => ({ icon: "", text: "2" }), + [TYPES.THREE]: () => ({ icon: "", text: "3" }), + [TYPES.FOUR]: () => ({ icon: "", text: "4" }), + [TYPES.FIVE]: () => ({ icon: "", text: "5" }), + [TYPES.SIX]: () => ({ icon: "", text: "6" }), + [TYPES.SEVEN]: () => ({ icon: "", text: "7" }), + [TYPES.EIGHT]: () => ({ icon: "", text: "8" }), + [TYPES.NINE]: () => ({ icon: "", text: "9" }), + [TYPES.ZERO]: () => ({ icon: "", text: "0" }), + [TYPES.MENU]: () => ({ icon: "", text: "MENU" }), + [TYPES.PLAY]: () => ({ icon: "bi-play", text: "" }), + [TYPES.PAUSE]: () => ({ icon: "bi-pause", text: "" }), + [TYPES.STOP]: () => ({ icon: "bi-stop", text: "" }), + [TYPES.REWIND]: () => ({ icon: "bi-rewind", text: "" }), + [TYPES.FORWARD]: () => ({ icon: "bi-fast-forward", text: "" }), + [TYPES.RED]: () => ({ + class: "bg-danger", + buttonSize: BUTTON_SIZE_SMALL, + }), + [TYPES.GREEN]: () => ({ + class: "bg-success", + buttonSize: BUTTON_SIZE_SMALL, + }), + [TYPES.YELLOW]: () => ({ + class: "bg-warning", + buttonSize: BUTTON_SIZE_SMALL, + }), + [TYPES.BLUE]: () => ({ + class: "bg-primary", + buttonSize: BUTTON_SIZE_SMALL, + }), + }; + + if (!(type in mapping)) return {}; + let props = mapping[type](); + props.type = type; + props.text = props.text || ""; + return props; + } + + function PlaceholderButton() { + return ( +
+ ); + } + + function RemoteButton(props) { + props = mergeProps( + { + onClick: () => {}, + disabled: false, + class: "", + buttonSize: BUTTON_SIZE_REGULAR, + icon: "", + text: "", + }, + props + ); + + let buttonWidth = BUTTON_WIDTH; + let buttonHeight = BUTTON_HEIGHT; + if (props.buttonSize === BUTTON_SIZE_SMALL) { + buttonWidth = SMALL_BUTTON_WIDTH; + buttonHeight = SMALL_BUTTON_HEIGHT; + } + let buttonFontSize = buttonHeight * 0.4; + let iconSize = buttonHeight * 0.5; + return ( + + ); + } + + return ( +
+ {layout.map((row) => ( +
+ {row + .map(toButtonProps) + .map(({ type, class: className, buttonSize, icon, text }) => + !type ? ( + + ) : ( + props.onCommand(commandMap()[type])} + disabled={!commandMap()[type]} + icon={icon} + text={text} + /> + ) + )} +
+ ))} +
+ ); +} + +export default RemoteControl; diff --git a/www/src/services/remotes-service.js b/www/src/services/remotes-service.js index 6d31651..33fbd1a 100644 --- a/www/src/services/remotes-service.js +++ b/www/src/services/remotes-service.js @@ -1,6 +1,9 @@ import Command from "../data/command"; import Serializer from "../data/serializer"; import Net from "../tools/net"; +import WebRTCService from "./webrtc-service"; + +const MESSAGE_TYPE_COMMAND = "command"; function RemoteService() { async function getRemote(remoteId) { @@ -104,6 +107,11 @@ function RemoteService() { } } + function sendCommand(command) { + let commandObject = Serializer.serializeCommand(command); + WebRTCService.sendDataJson({type: MESSAGE_TYPE_COMMAND, data: commandObject}); + } + return { getRemote, getRemotes, @@ -113,6 +121,7 @@ function RemoteService() { createCommand, createCommands, deleteCommand, + sendCommand, }; } diff --git a/www/src/views/devices/integration-view.jsx b/www/src/views/devices/integration-view.jsx index e10ff32..ce2a8e4 100644 --- a/www/src/views/devices/integration-view.jsx +++ b/www/src/views/devices/integration-view.jsx @@ -1,8 +1,16 @@ -import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"; +import { + createEffect, + createMemo, + createResource, + createSignal, + onCleanup, +} from "solid-js"; import WebRTCService from "../../services/webrtc-service"; import Integration from "../../data/integration"; import DeviceService from "../../services/device-service"; import UrlUtils from "../../tools/url-utils"; +import RemoteControl from "../../components/remote-control"; +import RemoteService from "../../services/remotes-service"; const [integration, setIntegration] = createSignal(new Integration()); @@ -25,6 +33,13 @@ function IntegrationView(props) { WebRTCService.onDataChannelOpen(handleDataChannelOpen); let videoElement = null; + const [availableRemotes] = createResource(RemoteService.getRemotes, { + initialValue: [], + }); + const [selectedRemote, setSelectedRemote] = createSignal(); + + createEffect(() => handleRemoteSelected(availableRemotes().shift()?.getId())); + createEffect(() => { let url = UrlUtils.getUrl(); url = UrlUtils.addQueryParameter(url, "id", integration()?.getId()); @@ -65,6 +80,16 @@ function IntegrationView(props) { }, 1000); } + async function handleRemoteSelected(remoteId) { + if (!remoteId) return; + let remote = await RemoteService.getRemote(remoteId); + setSelectedRemote(remote); + } + + function handleRemoteButtonPressed(command) { + RemoteService.sendCommand(command); + } + return (
-
- +
+
+ +
+
+ + +
);