feat: add remote control to integration view
This commit is contained in:
parent
b0c3a48da6
commit
23245dd549
180
www/src/components/remote-control.jsx
Normal file
180
www/src/components/remote-control.jsx
Normal file
@ -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 (
|
||||
<div
|
||||
style={`width: ${BUTTON_WIDTH}em; height: ${BUTTON_HEIGHT}em; margin: 0.2em;`}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<button
|
||||
class={
|
||||
"btn btn-dark d-flex justify-content-center align-items-center " +
|
||||
props.class
|
||||
}
|
||||
style={`width: ${buttonWidth}em; height: ${buttonHeight}em; margin: 0.2em;`}
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<i style={`font-size: ${iconSize}em;`} class={"bi " + props.icon} />
|
||||
<span style={`font-size: ${buttonFontSize}em;`}>{props.text}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="d-flex flex-column justify-content-center align-items-center p-1 bg-secondary rounded">
|
||||
{layout.map((row) => (
|
||||
<div class="d-flex flex-row justify-content-center align-items-center">
|
||||
{row
|
||||
.map(toButtonProps)
|
||||
.map(({ type, class: className, buttonSize, icon, text }) =>
|
||||
!type ? (
|
||||
<PlaceholderButton />
|
||||
) : (
|
||||
<RemoteButton
|
||||
class={className}
|
||||
buttonSize={buttonSize}
|
||||
onClick={() => props.onCommand(commandMap()[type])}
|
||||
disabled={!commandMap()[type]}
|
||||
icon={icon}
|
||||
text={text}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoteControl;
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div
|
||||
class={
|
||||
@ -99,12 +124,30 @@ function IntegrationView(props) {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-fill d-flex flex-column justify-content-center align-items-center rounded overflow-hidden">
|
||||
<video
|
||||
ref={videoElement}
|
||||
class="w-100 h-100"
|
||||
style="background-color: #000"
|
||||
></video>
|
||||
<div class="flex-fill d-flex flex-row overflow-hidden">
|
||||
<div class="flex-fill rounded overflow-hidden">
|
||||
<video
|
||||
ref={videoElement}
|
||||
class="w-100 h-100"
|
||||
style="background-color: #000"
|
||||
></video>
|
||||
</div>
|
||||
<div class="d-flex flex-column ps-3">
|
||||
<select
|
||||
class="form-select mb-3"
|
||||
onChange={(e) => handleRemoteSelected(e.target.value)}
|
||||
>
|
||||
{availableRemotes().map((remote, index) => (
|
||||
<option value={remote.getId()} selected={index === 0}>
|
||||
{remote.getTitle()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<RemoteControl
|
||||
onCommand={handleRemoteButtonPressed}
|
||||
remote={selectedRemote()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user