diff --git a/CHANGELOG.md b/CHANGELOG.md index bf886494c..dc092d899 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 be 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..47bf68bd3 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..45c9879c2 100644 --- a/web/src/components/Card/Card.jsx +++ b/web/src/components/Card/Card.jsx @@ -13,8 +13,11 @@ const Card = ({ selected, selectable, onClick, + + primary, + secondary, }) => { - const cName = `card ${className} ${selectable && !selected ? "selectable" : ""} ${selected ? "selected" : ""}` + const cName = `card ${className} ${selectable && !selected ? "selectable" : ""} ${selected ? "selected" : ""} ${primary ? "primary" : ""} ${secondary ? "secondary" : ""}` const onClickFun = onClick === null ? () => {} : onClick; const topTransparency = selected ? 0.25 : 0.4; const bottomTransparency = selected ? 0.25 : 0.9; @@ -49,6 +52,9 @@ Card.propTypes = { selected: PropTypes.bool, selectable: PropTypes.bool, onClick: PropTypes.func, + + primary: PropTypes.bool, + secondary: PropTypes.bool, }; Card.defaultProps = { @@ -56,7 +62,10 @@ Card.defaultProps = { className: "", selected: false, selectable: false, - onClick: null + onClick: null, + + primary: true, + 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..8aa0ff904 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 = (e, val, force = false) => { + if (!isScrolling.current) { + setVol(val, force); + } + } + return (
@@ -45,29 +71,29 @@ 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(e, val); + }} + onChangeCommitted={(e, val) => { + if (isIOS() && e.type === "mouseup") { + return; + } + handleVolChange(e, 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..6712626a5 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; + } + @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..364c70291 100644 --- a/web/src/pages/Player/Player.jsx +++ b/web/src/pages/Player/Player.jsx @@ -64,6 +64,44 @@ 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 + if(window.matchMedia("(max-width: 435px)").matches){ + if(expanded){ + return( + + ) + } else { + return( + + ) + } + } else { + if(expanded){ + return( + + ) + } else { + return( + + ) + } + } + } + return (
{streamsModalOpen && ( @@ -87,6 +125,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..5f008cb85 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; } @@ -52,3 +105,16 @@ @include general.header-font; padding: 0.5rem; } + +.control-gap { + // Dynamically size the gap so that the controls are always at the bottom of the screen when screen is scrolled to the top of the page + @media(min-height: 800px) { + height: 7.5vh + } + @media(min-height: 850px) { + height: 10vh + } + @media(min-height: 875px) { + height: 12.5vh + } +}