diff --git a/src/assets/icons/full_sound.svg b/src/assets/icons/full_sound.svg new file mode 100644 index 0000000..147dc86 --- /dev/null +++ b/src/assets/icons/full_sound.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/icon.tsx b/src/assets/icons/icon.tsx index 1f7e112..c0761af 100644 --- a/src/assets/icons/icon.tsx +++ b/src/assets/icons/icon.tsx @@ -9,6 +9,10 @@ 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"; +import NoSound from "./no_sound.svg?react"; +import FullSound from "./full_sound.svg?react"; +import Minus from "./minus.svg?react"; +import Plus from "./plus.svg?react"; import ChevronDown from "./chevron_down.svg?react"; import ChevronUp from "./chevron_up.svg?react"; import Headset from "./headset.svg?react"; @@ -35,6 +39,14 @@ export const StepRightIcon = () => ; export const SettingsIcon = () => ; +export const NoSoundIcon = () => ; + +export const FullSoundIcon = () => ; + +export const MinusIcon = () => ; + +export const PlusIcon = () => ; + export const ChevronDownIcon = () => ; export const ChevronUpIcon = () => ; diff --git a/src/assets/icons/minus.svg b/src/assets/icons/minus.svg new file mode 100644 index 0000000..eacef25 --- /dev/null +++ b/src/assets/icons/minus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/no_sound.svg b/src/assets/icons/no_sound.svg new file mode 100644 index 0000000..17b8f3a --- /dev/null +++ b/src/assets/icons/no_sound.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/plus.svg b/src/assets/icons/plus.svg new file mode 100644 index 0000000..be10a07 --- /dev/null +++ b/src/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/production-line/exit-call-button.tsx b/src/components/production-line/exit-call-button.tsx index 94b4370..0c3c548 100644 --- a/src/components/production-line/exit-call-button.tsx +++ b/src/components/production-line/exit-call-button.tsx @@ -6,6 +6,8 @@ const StyledBackBtn = styled(PrimaryButton)` padding: 0; margin: 0; width: 4rem; + background: #32383b; + border: 0.2rem solid #6d6d6d; `; export const ExitCallButton = ({ diff --git a/src/components/production-line/production-line.tsx b/src/components/production-line/production-line.tsx index afbb254..0f7d3f9 100644 --- a/src/components/production-line/production-line.tsx +++ b/src/components/production-line/production-line.tsx @@ -26,6 +26,7 @@ import { useCheckBadLineData } from "./use-check-bad-line-data.ts"; import { useAudioCue } from "./use-audio-cue.ts"; import { DisplayWarning } from "../display-box.tsx"; import { SettingsModal, Hotkeys } from "./settings-modal.tsx"; +import { VolumeSlider } from "../volume-slider/volume-slider.tsx"; import { CallState } from "../../global-state/types.ts"; import { ExitCallButton } from "./exit-call-button.tsx"; import { Modal } from "../modal/modal.tsx"; @@ -74,11 +75,11 @@ const FlexButtonWrapper = styled.div` width: 50%; padding: 0 1rem 2rem 1rem; - :first-of-type { + &.first { padding-left: 0; } - :last-of-type { + &.last { padding-right: 0; } `; @@ -145,11 +146,15 @@ export const ProductionLine = ({ muteHotkey: "m", speakerHotkey: "n", pressToTalkHotkey: "t", + increaseVolumeHotkey: "u", + decreaseVolumeHotkey: "d", }); const [savedHotkeys, setSavedHotkeys] = useState({ muteHotkey: "m", speakerHotkey: "n", pressToTalkHotkey: "t", + increaseVolumeHotkey: "u", + decreaseVolumeHotkey: "d", }); const { joinProductionOptions, @@ -333,18 +338,21 @@ export const ProductionLine = ({ }} > Controls - + - + muteOutput()}> {isOutputMuted ? : } - {inputAudioStream && inputAudioStream !== "no-device" && ( - + muteInput(!isInputMuted)} @@ -401,6 +409,18 @@ export const ProductionLine = ({ Push to Talk + + + {savedHotkeys.increaseVolumeHotkey.toUpperCase()}:{" "} + + Increase Volume + + + + {savedHotkeys.decreaseVolumeHotkey.toUpperCase()}:{" "} + + Decrease Volume + {isSettingsModalOpen && ( void; onSave: () => void; }; + export const SettingsModal = ({ hotkeys, lineName, @@ -87,39 +90,39 @@ export const SettingsModal = ({ muteHotkey: "", speakerHotkey: "", pressToTalkHotkey: "", + increaseVolumeHotkey: "", + decreaseVolumeHotkey: "", }); - const validateFields = (key: string, value: string) => { + const validateFields = (key: keyof Hotkeys, 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."; + const newErrors = ( + Object.keys(currentValues) as Array + ).reduce( + (acc, field) => { + const isDuplicate = + Object.values(currentValues).filter( + (val) => val && val === currentValues[field] + ).length > 1; + + if (!currentValues[field]) { + acc[field] = "This field can not be empty."; + } else if (isDuplicate) { + acc[field] = "This key is already in use."; + } else { + acc[field] = ""; } + return acc; }, - {} as { [key: string]: string } + {} as { [K in keyof Hotkeys]: 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, - })); + setErrors(newErrors); }; const handleInputChange = (key: keyof typeof hotkeys, value: string) => { @@ -134,9 +137,13 @@ export const SettingsModal = ({ const handleSave = () => { const hasErrors = Object.values(errors).some((error) => error !== ""); - if (!hasErrors) { - onSave(); + const hasEmptyFields = Object.values(hotkeys).some((value) => !value); + + if (hasErrors || hasEmptyFields) { + return; } + + onSave(); }; const handleKeyDown = (e: React.KeyboardEvent, index: number) => { @@ -237,6 +244,56 @@ export const SettingsModal = ({ /> )} + + Increase volume: + setInputRef(3, el)} + type="text" + value={hotkeys.increaseVolumeHotkey} + onChange={(e) => + handleInputChange("increaseVolumeHotkey", e.target.value) + } + placeholder="Enter hotkey" + maxLength={1} + onKeyDown={(e) => handleKeyDown(e, 3)} + /> + {errors.increaseVolumeHotkey && ( + + )} + + + Decrease volume: + setInputRef(4, el)} + type="text" + value={hotkeys.decreaseVolumeHotkey} + onChange={(e) => + handleInputChange("decreaseVolumeHotkey", e.target.value) + } + placeholder="Enter hotkey" + maxLength={1} + onKeyDown={(e) => handleKeyDown(e, 4)} + /> + {errors.decreaseVolumeHotkey && ( + + )} + Cancel diff --git a/src/components/volume-slider/volume-slider.tsx b/src/components/volume-slider/volume-slider.tsx new file mode 100644 index 0000000..d7dbf44 --- /dev/null +++ b/src/components/volume-slider/volume-slider.tsx @@ -0,0 +1,180 @@ +import styled from "@emotion/styled"; +import { FC, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { + NoSoundIcon, + FullSoundIcon, + MinusIcon, + PlusIcon, +} from "../../assets/icons/icon"; +import { isMobile } from "../../bowser"; +import { ActionButton } from "../landing-page/form-elements"; + +const SliderWrapper = styled.div` + width: 100%; + margin: 2rem 0; + display: flex; + flex-direction: column; + align-items: center; + position: relative; +`; + +const VolumeContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const IconWrapper = styled.div` + width: 5rem; + height: 5rem; + padding: 0.5rem; +`; + +const SliderTrack = styled.div` + width: 80%; + height: 0.4rem; + background-color: #e0e0e0; + border-radius: 0.2rem; + position: relative; +`; + +const SliderThumb = styled.div<{ position: number }>` + width: 1.5rem; + height: 1.5rem; + background-color: #59cbe8; + border-radius: 50%; + position: absolute; + top: -0.6rem; + left: ${({ position }) => `${position}%`}; + transform: translateX(-50%); + cursor: pointer; +`; + +const VolumeButton = styled(ActionButton)` + background-color: #32383b; + width: 7rem; + align-items: center; + height: 4.5rem; + padding: 1.5rem; + cursor: pointer; + margin-top: 1rem; + border: 0.2rem solid #6d6d6d; +`; + +const VolumeButtonContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +`; + +const VolumeWrapper = styled.div` + display: flex; + flex-direction: row; + width: 100%; + align-items: center; +`; + +type TVolumeSliderProps = { + audioElements: HTMLAudioElement[]; + increaseVolumeKey?: string; + decreaseVolumeKey?: string; +}; + +export const VolumeSlider: FC = ({ + audioElements, + increaseVolumeKey, + decreaseVolumeKey, +}) => { + const [value, setValue] = useState(0.75); + + const handleInputChange = (e: React.ChangeEvent) => { + const newValue = parseFloat(e.target.value); + setValue(newValue); + + audioElements.forEach((audioElement) => { + console.log("Setting volume to: ", newValue); + // eslint-disable-next-line no-param-reassign + audioElement.volume = newValue; + }); + }; + + const thumbPosition = value * 100; + + useHotkeys(increaseVolumeKey || "u", () => { + const newValue = Math.min(value + 0.05, 1); + setValue(newValue); + + audioElements.forEach((audioElement) => { + // eslint-disable-next-line no-param-reassign + audioElement.volume = newValue; + }); + }); + + useHotkeys(decreaseVolumeKey || "d", () => { + const newValue = Math.max(value - 0.05, 0); + setValue(newValue); + + audioElements.forEach((audioElement) => { + // eslint-disable-next-line no-param-reassign + audioElement.volume = newValue; + }); + }); + + const handleVolumeButtonClick = (type: "increase" | "decrease") => { + const newValue = + type === "increase" + ? Math.min(value + 0.05, 1) + : Math.max(value - 0.05, 0); + setValue(newValue); + // TODO: Fix for iOS + }; + + return ( + + + + + + + + + + + + + + + + + + {isMobile && ( + + handleVolumeButtonClick("decrease")}> + + + handleVolumeButtonClick("increase")}> + + + + )} + + ); +};