Skip to content

Commit

Permalink
feat: possible to change hotkeys for line
Browse files Browse the repository at this point in the history
  • Loading branch information
Saelmala committed Nov 19, 2024
1 parent 9a4c477 commit 13f4b32
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions src/assets/icons/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => <MicMute />;

Expand All @@ -28,3 +29,5 @@ export const ConfirmIcon = () => <ConfirmSvg />;
export const StepLeftIcon = () => <StepLeftSvg />;

export const StepRightIcon = () => <StepRightSvg />;

export const SettingsIcon = () => <Settings />;
4 changes: 4 additions & 0 deletions src/assets/icons/settings.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 71 additions & 5 deletions src/components/production-line/production-line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 [
Expand All @@ -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<Hotkeys>({
muteHotkey: "m",
speakerHotkey: "n",
pressToTalkHotkey: "t",
});
const [savedHotkeys, setSavedHotkeys] = useState<Hotkeys>({
muteHotkey: "m",
speakerHotkey: "n",
pressToTalkHotkey: "t",
});

const inputAudioStream = useAudioInput({
inputId: joinProductionOptions?.audioinput ?? null,
Expand Down Expand Up @@ -141,6 +176,8 @@ export const ProductionLine: FC = () => {
useLineHotkeys({
muteInput,
isInputMuted,
customKeyMute: savedHotkeys.muteHotkey,
customKeyPress: savedHotkeys.pressToTalkHotkey,
});

const { sessionId, sdpOffer } = useEstablishSession({
Expand Down Expand Up @@ -172,6 +209,7 @@ export const ProductionLine: FC = () => {
useSpeakerHotkeys({
muteOutput,
isOutputMuted,
customKey: savedHotkeys.speakerHotkey,
});

const line = useLinePolling({ joinProductionOptions });
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -315,18 +362,37 @@ export const ProductionLine: FC = () => {
inputAudioStream !== "no-device" &&
!isMobile && (
<>
<TempDiv>
<HotkeyDiv>
<strong>Hotkeys</strong>
</TempDiv>
<SettingsBtn onClick={handleSettingsClick}>
<SettingsIcon />
</SettingsBtn>
</HotkeyDiv>
<TempDiv>
<strong>M:</strong> Toggle Input Mute
<strong>{savedHotkeys.muteHotkey.toUpperCase()}:</strong>{" "}
Toggle Input Mute
</TempDiv>
<TempDiv>
<strong>N:</strong> Toggle Output Mute
<strong>
{savedHotkeys.speakerHotkey.toUpperCase()}:
</strong>{" "}
Toggle Output Mute
</TempDiv>
<TempDiv>
<strong>T:</strong> Push to Talk
<strong>
{savedHotkeys.pressToTalkHotkey.toUpperCase()}:
</strong>{" "}
Push to Talk
</TempDiv>
{isSettingsModalOpen && (
<SettingsModal
hotkeys={hotkeys}
lineName={line?.name}
setHotkeys={setHotkeys}
onSave={saveHotkeys}
onClose={handleSettingsClick}
/>
)}
</>
)}
</div>
Expand Down
191 changes: 191 additions & 0 deletions src/components/production-line/settings-modal.tsx
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<Hotkeys>>;
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 (
<ModalOverlay onClick={onClose}>
<ModalContent onClick={stopPropagation}>
<ModalCloseButton onClick={onClose}>X</ModalCloseButton>
<ModalHeader>Hotkey settings for line: {lineName}</ModalHeader>
<Form>
<FormField>
<Label>Toggle mute: </Label>
<Input
id="hotkeyMute"
ref={(el) => 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)}
/>
</FormField>
<FormField>
<Label>Toggle speaker: </Label>
<Input
id="hotkeySpeaker"
ref={(el) => 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)}
/>
</FormField>
<FormField>
<Label>Toggle press to speak: </Label>
<Input
id="hotkeyPress"
ref={(el) => 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)}
/>
</FormField>
<ButtonDiv>
<CancelButton type="button" onClick={onClose}>
Cancel
</CancelButton>
<PrimaryButton type="button" onClick={onSave}>
Save settings
</PrimaryButton>
</ButtonDiv>
</Form>
</ModalContent>
</ModalOverlay>
);
};
Loading

0 comments on commit 13f4b32

Please sign in to comment.