From b59ba7b43290126f32ee9e997caea961af25d8f9 Mon Sep 17 00:00:00 2001 From: SteveMicroNova Date: Wed, 11 Feb 2026 12:53:12 -0500 Subject: [PATCH] Add [ + ] button to player volume dropdown to enable adding and removing of zones from player view --- CHANGELOG.md | 2 + .../RectangularButton/RectangularButton.jsx | 4 +- .../RectangularButton/RectangularButton.scss | 2 +- .../components/VolumeZones/VolumeZones.jsx | 38 +++++++------ .../components/VolumeZones/VolumesZones.scss | 4 ++ .../ZoneVolumeSlider/ZoneVolumeSlider.jsx | 9 +-- .../ZoneVolumeSlider/ZoneVolumeSlider.scss | 4 -- web/src/pages/Player/Player.jsx | 57 +++++++++++-------- web/src/pages/Player/Player.scss | 5 ++ 9 files changed, 73 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d82bddb6f..7502b1301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # AmpliPi Software Releases # Future Release +* Web App + * Added [ + ] button to player page to allow selection of zones and groups from player page # 0.4.11 diff --git a/web/src/components/RectangularButton/RectangularButton.jsx b/web/src/components/RectangularButton/RectangularButton.jsx index 2b2f21fc5..5af5e113c 100644 --- a/web/src/components/RectangularButton/RectangularButton.jsx +++ b/web/src/components/RectangularButton/RectangularButton.jsx @@ -11,7 +11,7 @@ export default function RectangularButton({onClick, large, label}){ > {label} - ) + ); } RectangularButton.propTypes = { onClick: PropTypes.func.isRequired, @@ -20,4 +20,4 @@ RectangularButton.propTypes = { }; RectangularButton.defaultProps = { large: false, -} +}; diff --git a/web/src/components/RectangularButton/RectangularButton.scss b/web/src/components/RectangularButton/RectangularButton.scss index bc0a18b12..22fd56185 100644 --- a/web/src/components/RectangularButton/RectangularButton.scss +++ b/web/src/components/RectangularButton/RectangularButton.scss @@ -1,7 +1,7 @@ @use "src/general"; .rectangular-button { - // @include general.low-shadow; + @include general.low-shadow; @include general.regular-font; width: 100%; height: 4rem; diff --git a/web/src/components/VolumeZones/VolumeZones.jsx b/web/src/components/VolumeZones/VolumeZones.jsx index 46f28907f..9cf5ada56 100644 --- a/web/src/components/VolumeZones/VolumeZones.jsx +++ b/web/src/components/VolumeZones/VolumeZones.jsx @@ -3,16 +3,16 @@ import "./VolumesZones.scss"; import ZoneVolumeSlider from "../ZoneVolumeSlider/ZoneVolumeSlider"; import GroupVolumeSlider from "../GroupVolumeSlider/GroupVolumeSlider"; import Card from "../Card/Card"; +import RectangularButton from "@/components/RectangularButton/RectangularButton"; import PropTypes from "prop-types"; -const VolumeZones = ({ sourceId, open, zones, groups, groupsLeft, alone }) => { +const VolumeZones = ({ sourceId, open, zones, groups, groupsLeft, setZonesModalOpen }) => { const groupVolumeSliders = []; for (const group of groups) { groupVolumeSliders.push( - + { const zoneVolumeSliders = []; zones.forEach((zone) => { zoneVolumeSliders.push( - - + + ); }); - if(open){ - return ( -
- {groupVolumeSliders} - {zoneVolumeSliders} -
- ); + const noZones = zoneVolumeSliders.length == 0 && groupVolumeSliders.length == 0; + + if (open) { + return ( +
+ {groupVolumeSliders} + {zoneVolumeSliders} + {setZonesModalOpen(true);}} /> +
+ ); + } else if (noZones){ + return( +
+ {setZonesModalOpen(true);}} /> +
+ ); } }; VolumeZones.propTypes = { @@ -45,10 +54,7 @@ VolumeZones.propTypes = { zones: PropTypes.array.isRequired, groups: PropTypes.array.isRequired, groupsLeft: PropTypes.array.isRequired, - alone: PropTypes.bool, + setZonesModalOpen: PropTypes.func.isRequired, }; -VolumeZones.defaultProps = { - alone: false, -} export default VolumeZones; diff --git a/web/src/components/VolumeZones/VolumesZones.scss b/web/src/components/VolumeZones/VolumesZones.scss index 3692be7ce..2f4f326f8 100644 --- a/web/src/components/VolumeZones/VolumesZones.scss +++ b/web/src/components/VolumeZones/VolumesZones.scss @@ -5,6 +5,10 @@ color: general.$controls-color; } +.add-padding { + padding-bottom: 1rem; +} + .zone-vol-card { @media (max-width: general.$small-mobile){ padding: 0.5rem; diff --git a/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.jsx b/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.jsx index 710b215f7..73aab782d 100644 --- a/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.jsx +++ b/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.jsx @@ -8,7 +8,8 @@ import PropTypes from "prop-types"; let sendingRequestCount = 0; // Volume slider for individual zone in volume drawer -const ZoneVolumeSlider = ({ zoneId, alone }) => { +const ZoneVolumeSlider = ({ zoneId }) => { + const setSystemState = useStatusStore((s) => s.setSystemState); const zoneName = useStatusStore((s) => s.status.zones[zoneId].name); const volume = useStatusStore((s) => s.status.zones[zoneId].vol_f); const mute = useStatusStore((s) => s.status.zones[zoneId].mute); @@ -46,7 +47,7 @@ const ZoneVolumeSlider = ({ zoneId, alone }) => { }; return ( -
+
{zoneName} { }; ZoneVolumeSlider.propTypes = { zoneId: PropTypes.number.isRequired, - alone: PropTypes.bool, }; -ZoneVolumeSlider.defaultProps = { - alone: false, -} export default ZoneVolumeSlider; diff --git a/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.scss b/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.scss index bcf567fdd..62054f46d 100644 --- a/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.scss +++ b/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.scss @@ -5,10 +5,6 @@ font-size: 1rem; } -.alone { - padding-right: 8px; -} - .grouped { // 47px is the width of the dropdown icon + padding, this causes the child volume sliders to end in the same spot as the parent padding-right: 47px; diff --git a/web/src/pages/Player/Player.jsx b/web/src/pages/Player/Player.jsx index de2cc48b6..54f03f5f6 100644 --- a/web/src/pages/Player/Player.jsx +++ b/web/src/pages/Player/Player.jsx @@ -12,9 +12,10 @@ import { useState } from "react"; import VolumeZones from "@/components/VolumeZones/VolumeZones"; import Card from "@/components/Card/Card"; import StreamsModal from "@/components/StreamsModal/StreamsModal"; +import ZonesModal from "@/components/ZonesModal/ZonesModal"; import { getSourceInputType } from "@/utils/getSourceInputType"; import Chip from "@/components/Chip/Chip"; -import Grid from "@mui/material/Grid/Grid" +import Grid from "@mui/material/Grid/Grid"; import selectActiveSource from "@/utils/selectActiveSource"; import Box from "@mui/material/Box/Box"; @@ -24,8 +25,9 @@ import { getFittestRep } from "@/utils/GroupZoneFiltering"; const Player = () => { const [streamsModalOpen, setStreamsModalOpen] = React.useState(false); + const [zonesModalOpen, setZonesModalOpen] = React.useState(false); const selectedSourceId = usePersistentStore((s) => s.selectedSource); - // TODO: dont index into sources. id isn't guarenteed to line up with order + // TODO: Don't index into sources. id isn't guaranteed to line up with order const img_url = useStatusStore( (s) => s.status.sources[selectedSourceId].info.img_url ); @@ -62,6 +64,12 @@ const Player = () => { // This is a bootleg XOR statement, only works if there is exactly one zone or exactly one group, no more than that and not both const alone = ((usedGroups.length == 1) || (zonesLeft.length == 1)) && !((usedGroups.length > 0) && (zonesLeft.length > 0)); + React.useEffect(() => { + if(zonesLeft.length == 0 && usedGroups.length == 0){ + setExpanded(false); + } + }, [zonesLeft.length, usedGroups.length]); // Automatically unexpand when no zones or groups are connected + selectActiveSource(); function DropdownArrow() { @@ -85,11 +93,19 @@ const Player = () => { onClose={() => setStreamsModalOpen(false)} /> )} + + {zonesModalOpen && ( + setZonesModalOpen(false)} + /> + )}
@@ -102,9 +118,9 @@ const Player = () => { @@ -116,26 +132,21 @@ const Player = () => {
- { (!is_streamer && zones.length > 0) ? ( - (alone) ? ( -
- -
- ) : ( - + { !is_streamer && ( + + { (zones.length > 0) && (
setExpanded(!expanded)}>
- -
- -
-
- ) - ) : null } + )} +
+ +
+
+ ) }
); }; diff --git a/web/src/pages/Player/Player.scss b/web/src/pages/Player/Player.scss index 161237686..fc0d6dc2e 100644 --- a/web/src/pages/Player/Player.scss +++ b/web/src/pages/Player/Player.scss @@ -84,6 +84,11 @@ flex-direction: column; @media (general.$is-portrait) { max-height: calc(85vh - 120px); + } +} + +.scrollable{ // Was part of .player-volume-body until it started adding scrollbars when only the [ + ] was in that div + @media (general.$is-portrait) { overflow-y: auto; } }