From 1c52c1ae2ad24289a61e6ff3cf4b7ff85f6962eb Mon Sep 17 00:00:00 2001 From: Steven Engelbert Date: Thu, 19 Dec 2024 15:16:57 -0500 Subject: [PATCH] Move song controls, preset and add buttons to bottom of the screen on mobile Add many CSS breakpoints to ensure elements continue to work on the smallest screens Add touch denial to volume bars based on vertical angle Make pill-scrollbar a global class Add primary and secondary color theming support to Cards, use this in the volume dropdown --- CHANGELOG.md | 4 + web/src/App.jsx | 11 +-- web/src/App.scss | 40 +++++++++- web/src/components/Card/Card.jsx | 10 ++- web/src/components/Card/Card.scss | 8 +- .../CustomMarquee/CustomMarquee.jsx | 5 +- .../GroupVolumeSlider/GroupVolumeSlider.scss | 1 - .../components/MediaControl/MediaControl.scss | 6 +- web/src/components/ModalCard/ModalCard.scss | 30 -------- web/src/components/SongInfo/SongInfo.scss | 5 ++ web/src/components/StreamBar/StreamBar.scss | 18 ++++- .../components/VolumeSlider/VolumeSlider.jsx | 74 +++++++++++++------ .../components/VolumeZones/VolumeZones.jsx | 13 +++- .../components/VolumeZones/VolumesZones.scss | 20 ++++- .../ZoneVolumeSlider/ZoneVolumeSlider.jsx | 8 +- .../ZoneVolumeSlider/ZoneVolumeSlider.scss | 9 +++ web/src/pages/Home/Home.jsx | 12 +-- web/src/pages/Home/Home.scss | 13 +++- web/src/pages/Player/Player.jsx | 64 +++++++++------- web/src/pages/Player/Player.scss | 57 +++++++++++++- 20 files changed, 289 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf886494c..cfd01b170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ * Update Airplay icon to modern variant * Add gradient to background to break up the solid color * Reformat Admin Settings portion of Admin Panel (previously known as the Updater) + * Move home screen, player page controls to bottom of screen on mobile + * Update CSS breakpoints to scale the player page better on the smallest of screens + * Reformat Player page volume controls to look more modern + * Add safeguards in an attempt to reduce volume slider misinputs ## 0.4.5 * Web App diff --git a/web/src/App.jsx b/web/src/App.jsx index b5891ef77..2d3a7cd30 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -222,13 +222,14 @@ Page.propTypes = { const App = ({ selectedPage }) => { return ( -
+
-
- +
{/* Used to make sure the background doesn't stretch or stop prematurely on scrollable pages */} +
+ +
+
- -
); }; App.propTypes = { diff --git a/web/src/App.scss b/web/src/App.scss index 918bbcb54..38c7b0600 100644 --- a/web/src/App.scss +++ b/web/src/App.scss @@ -1,10 +1,16 @@ @use "src/general"; +.background-gradient { + background: general.$bg-gradient; + position: fixed; + z-index: -1; + height: 100vh; + width: 100vw; +} + .app { // display: flex; // width: 100vw; - height: 100vh; // Required to not have weird paneling with gradient backgrounds - background: general.$bg-gradient; // padding-top: 0.6rem; // padding-bottom: 0.6rem; // padding: 0.6rem; @@ -24,3 +30,33 @@ .app-body { padding-bottom: general.$navbar-height; } + + +.pill-scrollbar { + max-height: inherit; + overflow-y: auto; +} + +.pill-scrollbar::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +.pill-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.pill-scrollbar::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.5); + border-radius: 999px; + border: 3px solid rgba(255, 255, 255, 0.5); +} + +.pill-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: rgba(0, 0, 0, 0.7); +} + +.pill-scrollbar { + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.5) transparent; +} diff --git a/web/src/components/Card/Card.jsx b/web/src/components/Card/Card.jsx index 86aff4c0d..e74823f24 100644 --- a/web/src/components/Card/Card.jsx +++ b/web/src/components/Card/Card.jsx @@ -13,8 +13,10 @@ const Card = ({ selected, selectable, onClick, + + secondary, }) => { - const cName = `card ${className} ${selectable && !selected ? "selectable" : ""} ${selected ? "selected" : ""}` + const cName = `card ${className} ${selectable && !selected ? "selectable" : ""} ${selected ? "selected" : ""} ${secondary ? "secondary" : "primary"}` const onClickFun = onClick === null ? () => {} : onClick; const topTransparency = selected ? 0.25 : 0.4; const bottomTransparency = selected ? 0.25 : 0.9; @@ -49,6 +51,8 @@ Card.propTypes = { selected: PropTypes.bool, selectable: PropTypes.bool, onClick: PropTypes.func, + + secondary: PropTypes.bool, }; Card.defaultProps = { @@ -56,7 +60,9 @@ Card.defaultProps = { className: "", selected: false, selectable: false, - onClick: null + onClick: null, + + secondary: false, }; export default Card; diff --git a/web/src/components/Card/Card.scss b/web/src/components/Card/Card.scss index 4266c568c..899cb90dc 100644 --- a/web/src/components/Card/Card.scss +++ b/web/src/components/Card/Card.scss @@ -2,12 +2,18 @@ .card { @include general.low-shadow; - background-color: general.$primary; border-radius: 18px; color: general.$text-color; } +.primary { + background-color: general.$primary +} +.secondary { + background-color: general.$secondary +} + .selected { @include general.selected-shadow; } diff --git a/web/src/components/CustomMarquee/CustomMarquee.jsx b/web/src/components/CustomMarquee/CustomMarquee.jsx index 72c4befe7..9bcaa22a8 100644 --- a/web/src/components/CustomMarquee/CustomMarquee.jsx +++ b/web/src/components/CustomMarquee/CustomMarquee.jsx @@ -42,14 +42,13 @@ export default function CustomMarquee(props) { } } - let resizeTimout; + let resizeTimeout; // Your IDE will say this is unused, it's actually used to make sure the timeout below is limited to one instance at a time by taking up a specific variable function handleResize(){ if(!resizeCooldown.current){ resizeCooldown.current = true; assessMarquee() - - resizeTimout = setTimeout(()=>{resizeCooldown.current = false;}, 1000) // set a cooldown for resize checks to avoid excessive renders + resizeTimeout = setTimeout(()=>{resizeCooldown.current = false;}, 1000) } } window.addEventListener("resize", handleResize()); // Doesn't call assessMarquee directly to avoid calling thousands of times per second when resizing window diff --git a/web/src/components/GroupVolumeSlider/GroupVolumeSlider.scss b/web/src/components/GroupVolumeSlider/GroupVolumeSlider.scss index f6975cc3d..067767b52 100644 --- a/web/src/components/GroupVolumeSlider/GroupVolumeSlider.scss +++ b/web/src/components/GroupVolumeSlider/GroupVolumeSlider.scss @@ -23,5 +23,4 @@ .group-volume-slider { width: 100%; - margin: 0 1rem; } diff --git a/web/src/components/MediaControl/MediaControl.scss b/web/src/components/MediaControl/MediaControl.scss index 01e8cbc65..d116fc2c7 100644 --- a/web/src/components/MediaControl/MediaControl.scss +++ b/web/src/components/MediaControl/MediaControl.scss @@ -8,7 +8,6 @@ justify-content: center; margin-top: 1rem; margin-bottom: 1rem; - width: 90vw; } .media-inner { @@ -19,6 +18,7 @@ } .media-control { + font-size: 3.5rem; color: general.$controls-color; padding-left: 1.5rem; padding-right: 1.5rem; @@ -30,11 +30,11 @@ padding-right: 1.25rem; } - @media (min-width: 365px) and (max-width: 425px) { + @media (min-width: 365px) and (max-width: 435px) { font-size: 2.5rem; } - @media (min-width: 425px) { + @media (min-width: 435px) { font-size: 3.5rem; } } diff --git a/web/src/components/ModalCard/ModalCard.scss b/web/src/components/ModalCard/ModalCard.scss index f05961579..632bf447b 100644 --- a/web/src/components/ModalCard/ModalCard.scss +++ b/web/src/components/ModalCard/ModalCard.scss @@ -47,33 +47,3 @@ .modal-footer-button { @include general.button-hover; } - - -.pill-scrollbar { - max-height: inherit; - overflow-y: auto; -} - -.pill-scrollbar::-webkit-scrollbar { - width: 12px; - height: 12px; -} - -.pill-scrollbar::-webkit-scrollbar-track { - background: transparent; -} - -.pill-scrollbar::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.5); - border-radius: 999px; - border: 3px solid rgba(255, 255, 255, 0.5); -} - -.pill-scrollbar::-webkit-scrollbar-thumb:hover { - background-color: rgba(0, 0, 0, 0.7); -} - -.pill-scrollbar { - scrollbar-width: thin; - scrollbar-color: rgba(0, 0, 0, 0.5) transparent; -} diff --git a/web/src/components/SongInfo/SongInfo.scss b/web/src/components/SongInfo/SongInfo.scss index ed3b965bf..973f47688 100644 --- a/web/src/components/SongInfo/SongInfo.scss +++ b/web/src/components/SongInfo/SongInfo.scss @@ -10,6 +10,11 @@ white-space: nowrap; overflow: hidden; + + @media (max-width: 435px){ + max-height: 50vh; + max-width: 50vh; + } } .artist-name { diff --git a/web/src/components/StreamBar/StreamBar.scss b/web/src/components/StreamBar/StreamBar.scss index 01c847eaf..d83eebc13 100644 --- a/web/src/components/StreamBar/StreamBar.scss +++ b/web/src/components/StreamBar/StreamBar.scss @@ -12,6 +12,7 @@ max-width: 22rem; // The same width as the album art, until the album art starts to shrink at 375px wide } @media (max-width: 375px) { + max-height: 7.5vh; max-width: 85vw; } } @@ -21,15 +22,26 @@ color: general.$text-color; width: 100%; max-width: 80%; // Leave space for icon - font-size: 2.5rem; font-weight: medium; white-space: nowrap; overflow: hidden; @include general.header-font; padding: 0.5rem; + @media (max-height: 500px){ + font-size: 1.5rem; + } + @media (min-height: 500px){ + font-size: 2.5rem; + } } .stream-bar-icon { - width: 4rem; - height: 4rem; + @media (max-height: 500px){ + width: 2rem; + height: 2rem; + } + @media (min-height: 500px){ + width: 4rem; + height: 4rem; + } } diff --git a/web/src/components/VolumeSlider/VolumeSlider.jsx b/web/src/components/VolumeSlider/VolumeSlider.jsx index dc9be5ddc..8fa967f32 100644 --- a/web/src/components/VolumeSlider/VolumeSlider.jsx +++ b/web/src/components/VolumeSlider/VolumeSlider.jsx @@ -34,6 +34,32 @@ VolIcon.propTypes = { // generic volume slider used by other volume sliders const VolumeSlider = ({ vol, mute, setVol, setMute, disabled }) => { + const touchStartX = React.useRef(0); + const touchStartY = React.useRef(0); + const isScrolling = React.useRef(false); + + const handleTouchStart = (e) => { + const touch = e.touches[0]; + touchStartX.current = touch.clientX; + touchStartY.current = touch.clientY; + isScrolling.current = false; + }; + + const handleTouchMove = (e) => { + const touch = e.touches[0]; + const diffX = touch.clientX - touchStartX.current; + const diffY = touch.clientY - touchStartY.current; + + // Detect vertical drag, allow user to continue dragging within safe boundaries without needing to re-drag the slider + isScrolling.current = (Math.abs(diffY) / Math.abs(diffX)) > 1.65; // Equivalent to approximately 60 deg + }; + + const handleVolChange = (val, force = false) => { + if (!isScrolling.current) { + setVol(val, force); + } + } + return (
@@ -45,29 +71,31 @@ const VolumeSlider = ({ vol, mute, setVol, setMute, disabled }) => { >
- { - if(isIOS() && e.type === "mousedown"){ - return; - } - setVol(val); - }} - onChangeCommitted={(e, val) => { - if(isIOS() && e.type === "mouseup"){ - return; - } - setVol(val, true); - }} - /> -
+ { + if (isIOS() && e.type === "mousedown") { + return; + } + handleVolChange(val); + }} + onChangeCommitted={(e, val) => { + if (isIOS() && e.type === "mouseup") { + return; + } + handleVolChange(val, true); + }} + /> + ); }; diff --git a/web/src/components/VolumeZones/VolumeZones.jsx b/web/src/components/VolumeZones/VolumeZones.jsx index 74f753af3..46f28907f 100644 --- a/web/src/components/VolumeZones/VolumeZones.jsx +++ b/web/src/components/VolumeZones/VolumeZones.jsx @@ -6,12 +6,13 @@ import Card from "../Card/Card"; import PropTypes from "prop-types"; -const VolumeZones = ({ sourceId, open, zones, groups, groupsLeft }) => { +const VolumeZones = ({ sourceId, open, zones, groups, groupsLeft, alone }) => { const groupVolumeSliders = []; for (const group of groups) { groupVolumeSliders.push( - + { const zoneVolumeSliders = []; zones.forEach((zone) => { zoneVolumeSliders.push( - - + + ); }); @@ -44,6 +45,10 @@ VolumeZones.propTypes = { zones: PropTypes.array.isRequired, groups: PropTypes.array.isRequired, groupsLeft: PropTypes.array.isRequired, + alone: PropTypes.bool, }; +VolumeZones.defaultProps = { + alone: false, +} export default VolumeZones; diff --git a/web/src/components/VolumeZones/VolumesZones.scss b/web/src/components/VolumeZones/VolumesZones.scss index e874d0080..532d11a26 100644 --- a/web/src/components/VolumeZones/VolumesZones.scss +++ b/web/src/components/VolumeZones/VolumesZones.scss @@ -6,13 +6,25 @@ } .zone-vol-card { - margin-top: 1rem; - padding: 0.75rem; + @media (max-width: 365px){ + padding: 0.5rem; + } + @media (min-width: 365px){ + padding: 0.75rem; + } color: general.$controls-color; } .group-vol-card { - margin-top: 1rem; - padding: 0.75rem; + @media (max-width: 365px){ + padding: 0.5rem; + } + @media (min-width: 365px){ + padding: 0.75rem; + } color: general.$controls-color; } + +.vol-margin { + margin-bottom: 1rem; +} diff --git a/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.jsx b/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.jsx index c01e5e4de..710b215f7 100644 --- a/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.jsx +++ b/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.jsx @@ -8,7 +8,7 @@ import PropTypes from "prop-types"; let sendingRequestCount = 0; // Volume slider for individual zone in volume drawer -const ZoneVolumeSlider = ({ zoneId }) => { +const ZoneVolumeSlider = ({ zoneId, alone }) => { 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 +46,7 @@ const ZoneVolumeSlider = ({ zoneId }) => { }; 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 73669bc62..bcf567fdd 100644 --- a/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.scss +++ b/web/src/components/ZoneVolumeSlider/ZoneVolumeSlider.scss @@ -4,3 +4,12 @@ @include general.header-font; 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/Home/Home.jsx b/web/src/pages/Home/Home.jsx index 215a53bf4..d12882577 100644 --- a/web/src/pages/Home/Home.jsx +++ b/web/src/pages/Home/Home.jsx @@ -55,11 +55,13 @@ const PresetAndAdd = ({ ); } else { return ( -
setPresetsModalOpen(true)} - > - Presets +
+
setPresetsModalOpen(true)} + > + Presets +
); } diff --git a/web/src/pages/Home/Home.scss b/web/src/pages/Home/Home.scss index 121fa8212..b56d9a10c 100644 --- a/web/src/pages/Home/Home.scss +++ b/web/src/pages/Home/Home.scss @@ -2,7 +2,12 @@ .home-outer { padding-top: 10px; - padding-bottom: 10px; + @media (max-width: 435px) { + padding-bottom: 85px; // Used to ensure the screen is still scrollable to just above the PresetAndAdd component on mobile + } + @media (min-width: 435px) { + padding-bottom: 10px; + } display: flex; flex-direction: column; align-items: center; @@ -69,6 +74,12 @@ } .home-presets-container { + @media (max-width: 435px) { + position: fixed; + bottom: calc(general.$navbar-height + 10px); + } + + z-index: 1; // Needed to ensure that scrolling marquees do not appear on top of the presets button display: flex; flex-direction: row; diff --git a/web/src/pages/Player/Player.jsx b/web/src/pages/Player/Player.jsx index 872ee214f..de2cc48b6 100644 --- a/web/src/pages/Player/Player.jsx +++ b/web/src/pages/Player/Player.jsx @@ -64,6 +64,19 @@ const Player = () => { selectActiveSource(); + function DropdownArrow() { + // If on mobile, inital dropdown is at the bottom of the screen and expands upwards so the arrow should point up + // If on desktop, initial dropdown is in the middle of the screen and expands downwards so the arrow should point down + const isMobile = window.matchMedia("(max-width: 435px)").matches; + const Icon = isMobile ? (expanded ? KeyboardArrowDownIcon : KeyboardArrowUpIcon) : (expanded ? KeyboardArrowUpIcon : KeyboardArrowDownIcon); + return( + + ) + } + return (
{streamsModalOpen && ( @@ -87,6 +100,7 @@ const Player = () => { { - - - - {/* There are many sub-divs classed player-inner here because formatting was strange otherwise */} -
-
-
-
-
+ +
+
+ { (!is_streamer && zones.length > 0) ? ( + (alone) ? ( +
+ +
+ ) : ( + +
+ + setExpanded(!expanded)}> + + +
- {!alone && !is_streamer && zones.length > 0 && ( - - - setExpanded(!expanded)}> - {expanded ? ( - - ) : ( - - )} - - - )} - +
+ +
+
+ ) + ) : null }
); }; diff --git a/web/src/pages/Player/Player.scss b/web/src/pages/Player/Player.scss index 0da4b71a0..f1df1080c 100644 --- a/web/src/pages/Player/Player.scss +++ b/web/src/pages/Player/Player.scss @@ -28,12 +28,49 @@ .player-album-art { align-self: center; - max-width: 95vw; max-height: 22rem; border-radius: 2.5%; + @media (max-width: 435px) { + max-width: 85vw; + } + @media (max-width: 330px) { + max-width: 50vw; + } } -.player-volume-slider { +// Solo vs Grouped media controls are different because solo volumes have titles, making them tall enough to need further adjustment +.grouped-media-controls { + @media (max-width: 435px) { + position: fixed; + z-index: 0; + bottom: 130px + } +} +.solo-media-controls { + @media (max-width: 435px) { + position: fixed; + z-index: 0; + bottom: 150px; + } +} + +.player-volume-container { + @media (max-width: 435px) { + position: fixed; + bottom: calc(general.$navbar-height + 7px); + z-index: 1; + } +} + +.solo-volume { + @media (max-width: 435px) { + position: fixed; + bottom: calc(general.$navbar-height + 7px); + z-index: 1; + } +} + +.player-volume-header { display: flex; flex-direction: row; align-items: center; @@ -41,6 +78,22 @@ width: 90vw; } +.player-volume-body { + display: flex; + align-items: center; + flex-direction: column; + @media (max-width: 435px) { + max-height: calc(85vh - 120px); + overflow-y: auto; + } +} + +.expanded-volume-body { + @media (max-width: 435px) { + height: 100vh; + } +} + .player-volume-expand-button { color: general.$controls-color; }