diff --git a/src/App.js b/src/App.js index 1cae1dd..f826b18 100644 --- a/src/App.js +++ b/src/App.js @@ -41,6 +41,14 @@ function makeThemes(rawCustomCardColors) { }; } +function parseVolume(raw) { + // Old versions stored "on"/"off"; new versions store a 0-100 string. + if (raw === "on") return 100; + if (raw === "off") return 0; + const n = Number(raw); + return Number.isFinite(n) ? Math.max(0, Math.min(100, Math.round(n))) : 100; +} + function makeKeyboardLayout(keyboardLayoutName, customKeyboardLayout) { const emptyLayout = { verticalLayout: "", horizontalLayout: "" }; if (keyboardLayoutName !== "Custom") { @@ -88,7 +96,12 @@ function App() { "orientation", "vertical" ); - const [volume, setVolume] = useStorage("volume", "on"); + const [rawVolume, setRawVolume] = useStorage("volume", "100"); + const volume = parseVolume(rawVolume); + const setVolume = (next) => + setRawVolume((prev) => + String(typeof next === "function" ? next(parseVolume(prev)) : next) + ); const [notifications, setNotifications] = useStorage("notifications", "on"); const [focusMode, setFocusMode] = useStorage("focusMode", "off"); diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 9047617..3f2dcdf 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import AppBar from "@material-ui/core/AppBar"; import Divider from "@material-ui/core/Divider"; @@ -6,12 +6,15 @@ import IconButton from "@material-ui/core/IconButton"; import Link from "@material-ui/core/Link"; import Menu from "@material-ui/core/Menu"; import MenuItem from "@material-ui/core/MenuItem"; +import Slider from "@material-ui/core/Slider"; import Toolbar from "@material-ui/core/Toolbar"; import Tooltip from "@material-ui/core/Tooltip"; import Typography from "@material-ui/core/Typography"; import { makeStyles } from "@material-ui/core/styles"; import EditIcon from "@material-ui/icons/Edit"; import SettingsIcon from "@material-ui/icons/Settings"; +import VolumeOffIcon from "@material-ui/icons/VolumeOff"; +import VolumeUpIcon from "@material-ui/icons/VolumeUp"; import { version } from "../config"; import { SettingsContext, UserContext } from "../context"; @@ -44,6 +47,16 @@ const useStyles = makeStyles({ visibility: "visible", }, }, + volumeRow: { + display: "flex", + alignItems: "center", + gap: 8, + padding: "6px 16px", + minWidth: 200, + }, + volumeButton: { + padding: 4, + }, }); function Navbar() { @@ -99,8 +112,15 @@ function Navbar() { } } - function handleChangeVolume() { - settings.setVolume((volume) => (volume === "on" ? "off" : "on")); + // Remembers the level to restore when unmuting via the keyboard shortcut + // or speaker icon, so toggling back doesn't always snap to 100%. + const lastVolume = useRef(settings.volume > 0 ? settings.volume : 100); + useEffect(() => { + if (settings.volume > 0) lastVolume.current = settings.volume; + }, [settings.volume]); + + function handleToggleMute() { + settings.setVolume((v) => (v > 0 ? 0 : lastVolume.current)); } function handleChangeTheme() { @@ -124,7 +144,7 @@ function Navbar() { if (modifier === "Control|Shift") { if (key === "S") { event.preventDefault(); - handleChangeVolume(); + handleToggleMute(); } else if (key === "F") { event.preventDefault(); handleChangeFocusMode(); @@ -212,14 +232,25 @@ function Navbar() { )} - { - handleChangeVolume(); - handleCloseMenu(); - }} - > - {settings.volume === "on" ? "Mute" : "Unmute"} sound - +
+ 0 ? "Mute" : "Unmute"}> + 0 ? "Mute sound" : "Unmute sound"} + > + {settings.volume > 0 ? : } + + + settings.setVolume(v)} + min={0} + max={100} + step={5} + aria-label="Sound volume" + /> +
{ handleChangeNotifications(); diff --git a/src/pages/GamePage.js b/src/pages/GamePage.js index 25d1c48..d805c6a 100644 --- a/src/pages/GamePage.js +++ b/src/pages/GamePage.js @@ -127,9 +127,10 @@ function GamePage({ match }) { const [hasNextGame] = useFirebaseRef( spectating && game?.status === "done" ? `games/${nextGameId}/status` : null ); - const [playSuccess] = useSound(foundSfx); - const [playFail1] = useSound(failSfx1); - const [playFail2] = useSound(failSfx2); + const soundVolume = volume / 100; + const [playSuccess] = useSound(foundSfx, { volume: soundVolume }); + const [playFail1] = useSound(failSfx1, { volume: soundVolume }); + const [playFail2] = useSound(failSfx2, { volume: soundVolume }); const playFail = () => [playFail1, playFail2][Math.floor(Math.random() * 2)](); @@ -298,7 +299,7 @@ function GamePage({ match }) { return state.cards; case "set": handleSet(state.cards); - if (volume === "on") playSuccess(); + if (volume > 0) playSuccess(); if (notifications === "on") { setSnack({ open: true, @@ -308,7 +309,7 @@ function GamePage({ match }) { } return []; case "error": - if (volume === "on") playFail(); + if (volume > 0) playFail(); if (notifications === "on") { setSnack({ open: true,