From 13f4b3249f96a3bae10983e093f897cdab8756bc Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 19 Nov 2024 11:12:24 +0100 Subject: [PATCH 1/5] feat: possible to change hotkeys for line --- src/api/api.ts | 2 +- src/assets/icons/icon.tsx | 3 + src/assets/icons/settings.svg | 4 + .../production-line/production-line.tsx | 76 ++++++- .../production-line/settings-modal.tsx | 191 ++++++++++++++++++ .../production-line/use-line-hotkeys.ts | 17 +- 6 files changed, 284 insertions(+), 9 deletions(-) create mode 100644 src/assets/icons/settings.svg create mode 100644 src/components/production-line/settings-modal.tsx diff --git a/src/api/api.ts b/src/api/api.ts index d9c6a80c..df74c4bf 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -2,7 +2,7 @@ import { handleFetchRequest } from "./handle-fetch-request.ts"; const API_VERSION = import.meta.env.VITE_BACKEND_API_VERSION ?? "api/v1/"; const API_URL = - `${import.meta.env.VITE_BACKEND_URL}${API_VERSION}` ?? + `${import.meta.env.VITE_BACKEND_URL}${API_VERSION}` || `${window.location.origin}/${API_VERSION}`; const API_KEY = import.meta.env.VITE_BACKEND_API_KEY; diff --git a/src/assets/icons/icon.tsx b/src/assets/icons/icon.tsx index 9c709d36..98af7638 100644 --- a/src/assets/icons/icon.tsx +++ b/src/assets/icons/icon.tsx @@ -8,6 +8,7 @@ import UserSvg from "./user.svg?react"; import ConfirmSvg from "./done.svg?react"; import StepLeftSvg from "./chevron_left.svg?react"; import StepRightSvg from "./navigate_next.svg?react"; +import Settings from "./settings.svg?react"; export const MicMuted = () => ; @@ -28,3 +29,5 @@ export const ConfirmIcon = () => ; export const StepLeftIcon = () => ; export const StepRightIcon = () => ; + +export const SettingsIcon = () => ; diff --git a/src/assets/icons/settings.svg b/src/assets/icons/settings.svg new file mode 100644 index 00000000..2dd4809b --- /dev/null +++ b/src/assets/icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/production-line/production-line.tsx b/src/components/production-line/production-line.tsx index 33a581d0..654e1361 100644 --- a/src/components/production-line/production-line.tsx +++ b/src/components/production-line/production-line.tsx @@ -12,6 +12,7 @@ import { MicUnmuted, SpeakerOff, SpeakerOn, + SettingsIcon, } from "../../assets/icons/icon.tsx"; import { Spinner } from "../loader/loader.tsx"; import { DisplayContainerHeader } from "../landing-page/display-container-header.tsx"; @@ -29,11 +30,19 @@ import { useCheckBadLineData } from "./use-check-bad-line-data.ts"; import { NavigateToRootButton } from "../navigate-to-root-button/navigate-to-root-button.tsx"; import { useAudioCue } from "./use-audio-cue.ts"; import { DisplayWarning } from "../display-box.tsx"; +import { SettingsModal } from "./settings-modal.tsx"; const TempDiv = styled.div` padding: 0 0 2rem 0; `; +const HotkeyDiv = styled.div` + padding: 0 0 2rem 0; + flex-direction: row; + display: flex; + align-items: center; +`; + const HeaderWrapper = styled.div` padding: 2rem; display: flex; @@ -51,6 +60,15 @@ const ButtonIcon = styled.div` margin: 0 auto; `; +const SettingsBtn = styled.div` + padding: 0; + margin-left: 1.5rem; + width: 3rem; + cursor: pointer; + color: white; + background: transparent; +`; + const FlexButtonWrapper = styled.div` width: 50%; padding: 0 1rem 2rem 1rem; @@ -102,6 +120,12 @@ const ConnectionErrorWrapper = styled(FlexContainer)` padding-top: 12rem; `; +type Hotkeys = { + muteHotkey: string; + speakerHotkey: string; + pressToTalkHotkey: string; +}; + export const ProductionLine: FC = () => { const { productionId: paramProductionId, lineId: paramLineId } = useParams(); const [ @@ -110,6 +134,17 @@ export const ProductionLine: FC = () => { ] = useGlobalState(); const [isInputMuted, setIsInputMuted] = useState(true); const [isOutputMuted, setIsOutputMuted] = useState(false); + const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); + const [hotkeys, setHotkeys] = useState({ + muteHotkey: "m", + speakerHotkey: "n", + pressToTalkHotkey: "t", + }); + const [savedHotkeys, setSavedHotkeys] = useState({ + muteHotkey: "m", + speakerHotkey: "n", + pressToTalkHotkey: "t", + }); const inputAudioStream = useAudioInput({ inputId: joinProductionOptions?.audioinput ?? null, @@ -141,6 +176,8 @@ export const ProductionLine: FC = () => { useLineHotkeys({ muteInput, isInputMuted, + customKeyMute: savedHotkeys.muteHotkey, + customKeyPress: savedHotkeys.pressToTalkHotkey, }); const { sessionId, sdpOffer } = useEstablishSession({ @@ -172,6 +209,7 @@ export const ProductionLine: FC = () => { useSpeakerHotkeys({ muteOutput, isOutputMuted, + customKey: savedHotkeys.speakerHotkey, }); const line = useLinePolling({ joinProductionOptions }); @@ -207,6 +245,15 @@ export const ProductionLine: FC = () => { dispatch, }); + const handleSettingsClick = () => { + setIsSettingsModalOpen(!isSettingsModalOpen); + }; + + const saveHotkeys = () => { + setSavedHotkeys({ ...hotkeys }); + setIsSettingsModalOpen(false); + }; + // TODO detect if browser back button is pressed and run exit(); return ( @@ -315,18 +362,37 @@ export const ProductionLine: FC = () => { inputAudioStream !== "no-device" && !isMobile && ( <> - + Hotkeys - + + + + - M: Toggle Input Mute + {savedHotkeys.muteHotkey.toUpperCase()}:{" "} + Toggle Input Mute - N: Toggle Output Mute + + {savedHotkeys.speakerHotkey.toUpperCase()}: + {" "} + Toggle Output Mute - T: Push to Talk + + {savedHotkeys.pressToTalkHotkey.toUpperCase()}: + {" "} + Push to Talk + {isSettingsModalOpen && ( + + )} )} diff --git a/src/components/production-line/settings-modal.tsx b/src/components/production-line/settings-modal.tsx new file mode 100644 index 00000000..c498c089 --- /dev/null +++ b/src/components/production-line/settings-modal.tsx @@ -0,0 +1,191 @@ +import styled from "@emotion/styled"; +import React, { useRef } from "react"; +import { PrimaryButton, SecondaryButton } from "../landing-page/form-elements"; + +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + z-index: 100; + display: flex; + justify-content: center; + align-items: center; +`; + +const ModalContent = styled.div` + background: #383838; + border-radius: 0.5rem; + padding: 2rem; + width: 80%; + max-width: 40rem; + box-shadow: 0 0.4rem 0.8rem rgba(0, 0, 0, 0.2); + text-color: white; +`; + +const ModalHeader = styled.h2` + font-size: 2rem; + margin-bottom: 2rem; + font-weight: 600; +`; + +const ModalCloseButton = styled.button` + background: none; + border: none; + position: absolute; + top: 1rem; + right: 1rem; + cursor: pointer; + font-size: 1.6rem; +`; + +const Form = styled.form` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +const FormField = styled.div` + display: flex; + align-items: center; + gap: 1rem; +`; + +const Label = styled.label` + font-size: 1.4rem; + width: 30%; + color: white; +`; + +const Input = styled.input` + flex: 1; + padding: 0.5rem; + border: 0.1rem solid #ccc; + border-radius: 0.25rem; + font-size: 1.2rem; +`; + +const CancelButton = styled(SecondaryButton)` + background-color: #000000; +`; + +const ButtonDiv = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 3rem; +`; + +type Hotkeys = { + muteHotkey: string; + speakerHotkey: string; + pressToTalkHotkey: string; +}; + +type TSettingsModalProps = { + hotkeys: Hotkeys; + lineName?: string; + setHotkeys: React.Dispatch>; + onClose: () => void; + onSave: () => void; +}; + +export const SettingsModal = ({ + hotkeys, + lineName, + setHotkeys, + onClose, + onSave, +}: TSettingsModalProps) => { + const stopPropagation = (e: React.MouseEvent) => e.stopPropagation(); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + const handleInputChange = (key: string, value: string) => { + if (value.length <= 1 && /^[a-zA-Z]?$/.test(value)) { + setHotkeys((prev: Hotkeys) => ({ + ...prev, + [key]: value, + })); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent, index: number) => { + if (e.key === "Enter") { + e.preventDefault(); + const nextInput = inputRefs.current[index + 1]; + if (nextInput) { + nextInput.focus(); + } else { + onSave(); + } + } + }; + + const setInputRef = (index: number, el: HTMLInputElement | null) => { + inputRefs.current[index] = el; + }; + + return ( + + + X + Hotkey settings for line: {lineName} +
+ + + setInputRef(0, el)} + type="text" + placeholder="Enter hotkey" + value={hotkeys.muteHotkey} + onChange={(e) => handleInputChange("muteHotkey", e.target.value)} + maxLength={1} + onKeyDown={(e) => handleKeyDown(e, 0)} + /> + + + + setInputRef(1, el)} + type="text" + value={hotkeys.speakerHotkey} + onChange={(e) => + handleInputChange("speakerHotkey", e.target.value) + } + placeholder="Enter hotkey" + maxLength={1} + onKeyDown={(e) => handleKeyDown(e, 1)} + /> + + + + setInputRef(2, el)} + type="text" + value={hotkeys.pressToTalkHotkey} + onChange={(e) => + handleInputChange("pressToTalkHotkey", e.target.value) + } + placeholder="Enter hotkey" + maxLength={1} + onKeyDown={(e) => handleKeyDown(e, 2)} + /> + + + + Cancel + + + Save settings + + +
+
+
+ ); +}; diff --git a/src/components/production-line/use-line-hotkeys.ts b/src/components/production-line/use-line-hotkeys.ts index 48680349..bb3193b3 100644 --- a/src/components/production-line/use-line-hotkeys.ts +++ b/src/components/production-line/use-line-hotkeys.ts @@ -3,23 +3,31 @@ import { useHotkeys } from "react-hotkeys-hook"; type TuseLineHotkeys = { muteInput: (mute: boolean) => void; isInputMuted: boolean; + customKeyMute?: string; + customKeyPress?: string; }; type TuseSpeakerHotkeys = { muteOutput: (mute: boolean) => void; isOutputMuted: boolean; + customKey?: string; }; export const useLineHotkeys = ({ muteInput, isInputMuted, + customKeyMute, + customKeyPress, }: TuseLineHotkeys) => { - useHotkeys("m", () => { + const muteInputKey = customKeyMute || "m"; + const mutePressKey = customKeyPress || "t"; + + useHotkeys(muteInputKey, () => { muteInput(!isInputMuted); }); useHotkeys( - "t", + mutePressKey, (e) => { if (e.type === "keydown") { muteInput(false); @@ -37,8 +45,11 @@ export const useLineHotkeys = ({ export const useSpeakerHotkeys = ({ muteOutput, isOutputMuted, + customKey, }: TuseSpeakerHotkeys) => { - useHotkeys("n", () => { + const muteOutputKey = customKey || "n"; + + useHotkeys(muteOutputKey, () => { muteOutput(!isOutputMuted); }); }; From 3735dc13555041082b9f0c9449b4d75982aea7ef Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 19 Nov 2024 11:22:26 +0100 Subject: [PATCH 2/5] fix: cleanup --- .../production-line/production-line.tsx | 18 ++++++------------ .../production-line/settings-modal.tsx | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/components/production-line/production-line.tsx b/src/components/production-line/production-line.tsx index 654e1361..d186ebe5 100644 --- a/src/components/production-line/production-line.tsx +++ b/src/components/production-line/production-line.tsx @@ -30,7 +30,7 @@ import { useCheckBadLineData } from "./use-check-bad-line-data.ts"; import { NavigateToRootButton } from "../navigate-to-root-button/navigate-to-root-button.tsx"; import { useAudioCue } from "./use-audio-cue.ts"; import { DisplayWarning } from "../display-box.tsx"; -import { SettingsModal } from "./settings-modal.tsx"; +import { SettingsModal, Hotkeys } from "./settings-modal.tsx"; const TempDiv = styled.div` padding: 0 0 2rem 0; @@ -120,12 +120,6 @@ const ConnectionErrorWrapper = styled(FlexContainer)` padding-top: 12rem; `; -type Hotkeys = { - muteHotkey: string; - speakerHotkey: string; - pressToTalkHotkey: string; -}; - export const ProductionLine: FC = () => { const { productionId: paramProductionId, lineId: paramLineId } = useParams(); const [ @@ -369,19 +363,19 @@ export const ProductionLine: FC = () => { - {savedHotkeys.muteHotkey.toUpperCase()}:{" "} + {savedHotkeys.muteHotkey.toUpperCase()}: Toggle Input Mute - {savedHotkeys.speakerHotkey.toUpperCase()}: - {" "} + {savedHotkeys.speakerHotkey.toUpperCase()}:{" "} + Toggle Output Mute - {savedHotkeys.pressToTalkHotkey.toUpperCase()}: - {" "} + {savedHotkeys.pressToTalkHotkey.toUpperCase()}:{" "} + Push to Talk {isSettingsModalOpen && ( diff --git a/src/components/production-line/settings-modal.tsx b/src/components/production-line/settings-modal.tsx index c498c089..20073ec0 100644 --- a/src/components/production-line/settings-modal.tsx +++ b/src/components/production-line/settings-modal.tsx @@ -78,7 +78,7 @@ const ButtonDiv = styled.div` margin-top: 3rem; `; -type Hotkeys = { +export type Hotkeys = { muteHotkey: string; speakerHotkey: string; pressToTalkHotkey: string; From 9e35890b6982e72b75d4b4187c3fe19da0527ca4 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 19 Nov 2024 12:10:49 +0100 Subject: [PATCH 3/5] fix: change to common form elements --- .../production-line/settings-modal.tsx | 65 +++++++------------ 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/src/components/production-line/settings-modal.tsx b/src/components/production-line/settings-modal.tsx index 20073ec0..28f21d0d 100644 --- a/src/components/production-line/settings-modal.tsx +++ b/src/components/production-line/settings-modal.tsx @@ -1,6 +1,13 @@ import styled from "@emotion/styled"; import React, { useRef } from "react"; -import { PrimaryButton, SecondaryButton } from "../landing-page/form-elements"; +import { + PrimaryButton, + SecondaryButton, + FormLabel, + FormInput, + FormContainer, + DecorativeLabel, +} from "../landing-page/form-elements"; const ModalOverlay = styled.div` position: fixed; @@ -22,7 +29,7 @@ const ModalContent = styled.div` width: 80%; max-width: 40rem; box-shadow: 0 0.4rem 0.8rem rgba(0, 0, 0, 0.2); - text-color: white; + color: white; `; const ModalHeader = styled.h2` @@ -41,32 +48,6 @@ const ModalCloseButton = styled.button` font-size: 1.6rem; `; -const Form = styled.form` - display: flex; - flex-direction: column; - gap: 1rem; -`; - -const FormField = styled.div` - display: flex; - align-items: center; - gap: 1rem; -`; - -const Label = styled.label` - font-size: 1.4rem; - width: 30%; - color: white; -`; - -const Input = styled.input` - flex: 1; - padding: 0.5rem; - border: 0.1rem solid #ccc; - border-radius: 0.25rem; - font-size: 1.2rem; -`; - const CancelButton = styled(SecondaryButton)` background-color: #000000; `; @@ -132,10 +113,10 @@ export const SettingsModal = ({ X Hotkey settings for line: {lineName} -
- - - + + Toggle mute: + setInputRef(0, el)} type="text" @@ -145,10 +126,10 @@ export const SettingsModal = ({ maxLength={1} onKeyDown={(e) => handleKeyDown(e, 0)} /> - - - - + + Toggle speaker: + setInputRef(1, el)} type="text" @@ -160,10 +141,10 @@ export const SettingsModal = ({ maxLength={1} onKeyDown={(e) => handleKeyDown(e, 1)} /> - - - - + + Toggle press to speak: + setInputRef(2, el)} type="text" @@ -175,7 +156,7 @@ export const SettingsModal = ({ maxLength={1} onKeyDown={(e) => handleKeyDown(e, 2)} /> - + Cancel @@ -184,7 +165,7 @@ export const SettingsModal = ({ Save settings -
+
); From 13d357b750c9a0add2f76b695e8d2dd9d6e991ad Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 19 Nov 2024 16:12:08 +0100 Subject: [PATCH 4/5] fix: add error on duplicate + keyboard navigation --- .../production-line/settings-modal.tsx | 96 +++++++++++++++++-- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/src/components/production-line/settings-modal.tsx b/src/components/production-line/settings-modal.tsx index 28f21d0d..4957dc48 100644 --- a/src/components/production-line/settings-modal.tsx +++ b/src/components/production-line/settings-modal.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; -import React, { useRef } from "react"; +import React, { useRef, useState } from "react"; +import { ErrorMessage } from "@hookform/error-message"; import { PrimaryButton, SecondaryButton, @@ -7,6 +8,7 @@ import { FormInput, FormContainer, DecorativeLabel, + StyledWarningMessage, } from "../landing-page/form-elements"; const ModalOverlay = styled.div` @@ -72,7 +74,6 @@ type TSettingsModalProps = { onClose: () => void; onSave: () => void; }; - export const SettingsModal = ({ hotkeys, lineName, @@ -82,13 +83,52 @@ export const SettingsModal = ({ }: TSettingsModalProps) => { const stopPropagation = (e: React.MouseEvent) => e.stopPropagation(); const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + const [errors, setErrors] = useState<{ [key: string]: string }>({ + muteHotkey: "", + speakerHotkey: "", + pressToTalkHotkey: "", + }); + + const validateFields = (key: string, value: string) => { + const currentValues = { + ...hotkeys, + [key]: value, + }; + + const duplicates = Object.entries(currentValues).reduce( + (acc, [field, val]) => { + if (val && value && val === value && field !== key) { + acc[key] = "This key is already in use."; + } + return acc; + }, + {} as { [key: string]: string } + ); - const handleInputChange = (key: string, value: string) => { + setErrors((prevErrors) => ({ + ...prevErrors, + muteHotkey: + key === "muteHotkey" + ? duplicates.muteHotkey || "" + : prevErrors.muteHotkey, + speakerHotkey: + key === "speakerHotkey" + ? duplicates.speakerHotkey || "" + : prevErrors.speakerHotkey, + pressToTalkHotkey: + key === "pressToTalkHotkey" + ? duplicates.pressToTalkHotkey || "" + : prevErrors.pressToTalkHotkey, + })); + }; + + const handleInputChange = (key: keyof typeof hotkeys, value: string) => { if (value.length <= 1 && /^[a-zA-Z]?$/.test(value)) { - setHotkeys((prev: Hotkeys) => ({ + setHotkeys((prev) => ({ ...prev, [key]: value, })); + validateFields(key, value); } }; @@ -99,7 +139,22 @@ export const SettingsModal = ({ if (nextInput) { nextInput.focus(); } else { - onSave(); + const hasErrors = Object.values(errors).some((error) => error !== ""); + if (!hasErrors) { + onSave(); + } + } + } else if (e.key === "ArrowUp") { + e.preventDefault(); + const previousInput = inputRefs.current[index - 1]; + if (previousInput) { + previousInput.focus(); + } + } else if (e.key === "ArrowDown") { + e.preventDefault(); + const nextInput = inputRefs.current[index + 1]; + if (nextInput) { + nextInput.focus(); } } }; @@ -126,6 +181,13 @@ export const SettingsModal = ({ maxLength={1} onKeyDown={(e) => handleKeyDown(e, 0)} /> + {errors.muteHotkey && ( + + )} Toggle speaker: @@ -141,6 +203,13 @@ export const SettingsModal = ({ maxLength={1} onKeyDown={(e) => handleKeyDown(e, 1)} /> + {errors.speakerHotkey && ( + + )} Toggle press to speak: @@ -156,12 +225,27 @@ export const SettingsModal = ({ maxLength={1} onKeyDown={(e) => handleKeyDown(e, 2)} /> + {errors.pressToTalkHotkey && ( + + )} Cancel - + { + const hasErrors = Object.values(errors).some( + (error) => error !== "" + ); + if (!hasErrors) onSave(); + }} + > Save settings From 757dcbf9620b459af1e11af63aae0264a437f44e Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 19 Nov 2024 16:21:38 +0100 Subject: [PATCH 5/5] fixup! --- .../production-line/settings-modal.tsx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/components/production-line/settings-modal.tsx b/src/components/production-line/settings-modal.tsx index 4957dc48..9f578e97 100644 --- a/src/components/production-line/settings-modal.tsx +++ b/src/components/production-line/settings-modal.tsx @@ -132,6 +132,13 @@ export const SettingsModal = ({ } }; + const handleSave = () => { + const hasErrors = Object.values(errors).some((error) => error !== ""); + if (!hasErrors) { + onSave(); + } + }; + const handleKeyDown = (e: React.KeyboardEvent, index: number) => { if (e.key === "Enter") { e.preventDefault(); @@ -139,10 +146,7 @@ export const SettingsModal = ({ if (nextInput) { nextInput.focus(); } else { - const hasErrors = Object.values(errors).some((error) => error !== ""); - if (!hasErrors) { - onSave(); - } + handleSave(); } } else if (e.key === "ArrowUp") { e.preventDefault(); @@ -237,15 +241,7 @@ export const SettingsModal = ({ Cancel - { - const hasErrors = Object.values(errors).some( - (error) => error !== "" - ); - if (!hasErrors) onSave(); - }} - > + Save settings