Adjusted mobile player layout

This commit is contained in:
lucast committed 2026-10-06 03:39:08 +02:00
1 parent 4c883c60a4
commit ae52b1754b
6 files changed
+373 -29

No files matched your search

+47 -11
View File
@@ -4,9 +4,14 @@ import { GET_FILE } from "@/utils/file"
const { fullscreen, close: closeFullscreen } = useFullscreen() const { fullscreen, close: closeFullscreen } = useFullscreen()
const song = useSong() const song = useSong()
const mouseMovedRecently = ref(true) const mouseMovedRecently = ref(true)
const mobileView = ref(false)
onMounted(() => { onMounted(() => {
let timeoutId: NodeJS.Timeout let timeoutId: NodeJS.Timeout
const mobileQuery = window.matchMedia("(max-width: 767px)")
const updateMobileView = () => (mobileView.value = mobileQuery.matches)
updateMobileView()
mobileQuery.addEventListener("change", updateMobileView)
const handleMouseMove = () => { const handleMouseMove = () => {
mouseMovedRecently.value = true mouseMovedRecently.value = true
@@ -28,6 +33,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("mousemove", handleMouseMove) window.removeEventListener("mousemove", handleMouseMove)
window.removeEventListener("keydown", handleKeydown) window.removeEventListener("keydown", handleKeydown)
mobileQuery.removeEventListener("change", updateMobileView)
}) })
}) })
@@ -68,7 +74,7 @@ nuxtApp.hook("page:finish", () => {
/> />
<PlayerFullscreenLyricsDisplay <PlayerFullscreenLyricsDisplay
:animation-duration="0.2" :animation-duration="0.2"
:offset="250" :offset="mobileView ? 100 : 250"
:show-scroll-bar="mouseMovedRecently" :show-scroll-bar="mouseMovedRecently"
/> />
</div> </div>
@@ -79,11 +85,7 @@ nuxtApp.hook("page:finish", () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.fullscreen { .fullscreen {
position: fixed; position: fixed;
top: 0; inset: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 150; z-index: 150;
cursor: none; cursor: none;
@@ -95,11 +97,7 @@ nuxtApp.hook("page:finish", () => {
.screen-container { .screen-container {
position: fixed; position: fixed;
top: 0; inset: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.34); background-color: rgba(0, 0, 0, 0.34);
} }
@@ -149,4 +147,42 @@ nuxtApp.hook("page:finish", () => {
} }
} }
} }
@media (max-width: 767px) {
.close-btn {
top: calc(0.75rem + env(safe-area-inset-top));
right: 0.75rem;
z-index: 2;
padding: 0.6rem;
opacity: 1;
font-size: 1.4rem;
&.show {
opacity: 1;
}
}
.content-container {
max-width: none;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
gap: 1.25rem;
overflow-y: auto;
overscroll-behavior: contain;
padding: calc(3.75rem + env(safe-area-inset-top)) 1rem
calc(1rem + env(safe-area-inset-bottom));
& > * {
max-height: none;
margin: 0 auto;
}
&.lyrics {
grid-template-columns: 1fr;
}
}
}
</style> </style>
@@ -50,6 +50,7 @@ const scrollActiveIndex = computed(() => {
const height = ref<number>() const height = ref<number>()
const top = ref(0) const top = ref(0)
const syncedHeight = ref<number>() const syncedHeight = ref<number>()
const mobileView = ref(false)
const layoutVersion = ref(0) const layoutVersion = ref(0)
const lyricOffsets = ref<number[]>([]) const lyricOffsets = ref<number[]>([])
const topFadeExtension = 75 const topFadeExtension = 75
@@ -60,7 +61,14 @@ const updateLyricsLayout = () => {
if (trackDisplay) { if (trackDisplay) {
height.value = trackDisplay.clientHeight height.value = trackDisplay.clientHeight
top.value = trackDisplay.getBoundingClientRect().top top.value = trackDisplay.getBoundingClientRect().top
syncedHeight.value = window.innerHeight - top.value - 100 syncedHeight.value = mobileView.value
? Math.max(
180,
window.innerHeight -
trackDisplay.getBoundingClientRect().bottom -
100,
)
: window.innerHeight - top.value - 100
} }
const lines = containerRef.value?.children const lines = containerRef.value?.children
@@ -93,8 +101,20 @@ watch(
{ immediate: true }, { immediate: true },
) )
onMounted(() => window.addEventListener("resize", updateLyricsLayout)) onMounted(() => {
onUnmounted(() => window.removeEventListener("resize", updateLyricsLayout)) const mobileQuery = window.matchMedia("(max-width: 767px)")
const updateMobileView = () => {
mobileView.value = mobileQuery.matches
updateLyricsLayout()
}
updateMobileView()
window.addEventListener("resize", updateLyricsLayout)
mobileQuery.addEventListener("change", updateMobileView)
onUnmounted(() => {
window.removeEventListener("resize", updateLyricsLayout)
mobileQuery.removeEventListener("change", updateMobileView)
})
})
const containerRef = useTemplateRef("container-ref") const containerRef = useTemplateRef("container-ref")
const outerRef = useTemplateRef("outer-ref") const outerRef = useTemplateRef("outer-ref")
@@ -185,7 +205,7 @@ const finalTransform = computed(() => {
class="synced-lyrics-container" class="synced-lyrics-container"
:style="{ :style="{
height: `${(syncedHeight ?? 0) + topFadeExtension}px`, height: `${(syncedHeight ?? 0) + topFadeExtension}px`,
marginTop: `${top - topFadeExtension}px`, marginTop: mobileView ? '0px' : `${top - topFadeExtension}px`,
}" }"
@wheel.prevent="onWheel" @wheel.prevent="onWheel"
> >
@@ -306,4 +326,26 @@ const finalTransform = computed(() => {
background-color: $color-secondary-background; background-color: $color-secondary-background;
} }
} }
@media (max-width: 767px) {
.synced-lyrics-container {
align-self: stretch;
mask-image: linear-gradient(
to bottom,
transparent 0%,
#000 12%,
#000 calc(100% - 70px),
transparent 100%
);
}
.synced-lyrics > p {
@include fontSize(30px);
padding: 0.4rem 0;
}
.lyrics {
height: auto !important;
}
}
</style> </style>
@@ -182,4 +182,31 @@ const durationFormatted = computed(() =>
pointer-events: none; pointer-events: none;
} }
} }
@media (max-width: 767px) {
.track-display {
gap: 0.85rem;
}
.btns {
gap: clamp(0.4rem, 3vw, 0.85rem);
font-size: 1.75rem;
& > * {
padding: 0.45rem;
}
}
.volume-slider {
margin-inline: 0;
}
}
@media (max-width: 767px) and (hover: none) {
.btns,
.volume-slider {
opacity: 1 !important;
pointer-events: auto !important;
}
}
</style> </style>
+123
View File
@@ -79,6 +79,15 @@ const playAt = (index: number) =>
queueRows.value.map((row) => row.track), queueRows.value.map((row) => row.track),
index, index,
) )
const moveRow = (index: number, direction: -1 | 1) => {
const destination = index + direction
if (destination < 0 || destination >= queueRows.value.length) return
const [row] = queueRows.value.splice(index, 1)
queueRows.value.splice(destination, 0, row!)
queue.value = queueRows.value.map((item) => item.track)
}
</script> </script>
<template> <template>
@@ -118,6 +127,7 @@ const playAt = (index: number) =>
@drop="finishDrag" @drop="finishDrag"
@dragend="cancelDrag" @dragend="cancelDrag"
> >
<div class="queue-row">
<TrackRow <TrackRow
:track="row.track" :track="row.track"
:index="index" :index="index"
@@ -126,6 +136,29 @@ const playAt = (index: number) =>
class="queue-track" class="queue-track"
@play-album-at-index="playAt(index)" @play-album-at-index="playAt(index)"
/> />
<div
class="queue-order-actions"
role="group"
:aria-label="`Reorder ${row.track.name}`"
>
<button
type="button"
:aria-label="`Move ${row.track.name} up in queue`"
:disabled="index === 0"
@click="moveRow(index, -1)"
>
<Icon name="lucide:chevron-up" aria-hidden="true" />
</button>
<button
type="button"
:aria-label="`Move ${row.track.name} down in queue`"
:disabled="index === queueRows.length - 1"
@click="moveRow(index, 1)"
>
<Icon name="lucide:chevron-down" aria-hidden="true" />
</button>
</div>
</div>
</li> </li>
</ol> </ol>
</aside> </aside>
@@ -210,6 +243,14 @@ const playAt = (index: number) =>
} }
} }
.queue-row {
position: relative;
}
.queue-order-actions {
display: none;
}
.empty { .empty {
flex: 1; flex: 1;
display: grid; display: grid;
@@ -235,4 +276,86 @@ const playAt = (index: number) =>
transform: translateX(100%); transform: translateX(100%);
} }
} }
@media (max-width: 767px) {
.queue-root {
z-index: 120;
}
.drawer {
inset-block: 18dvh 0;
inset-inline: 0;
inline-size: 100%;
border-inline-start: 0;
border-top: 1px solid $color-border;
border-radius: 1rem 1rem 0 0;
padding-bottom: env(safe-area-inset-bottom);
}
.header {
padding: 0.85rem 1rem;
}
.close {
padding: 0.75rem;
}
.tracks {
min-height: 0;
flex: 1;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
.queue-row {
display: flex;
align-items: center;
gap: 0.25rem;
}
.queue-track {
flex: 1;
min-width: 0;
grid-template-columns: 40px minmax(0, 1fr);
padding-inline: 0.5rem;
}
.queue-row :deep(.queue-track > :nth-last-child(-n + 2)) {
display: none;
}
.queue-order-actions {
display: flex;
flex-direction: column;
button {
display: grid;
place-items: center;
border: 0;
padding: 0.875rem;
color: inherit;
background: transparent;
font-size: 1rem;
}
button:disabled {
color: rgb(255 255 255 / 30%);
cursor: default;
}
}
.queue-enter-active,
.queue-leave-active {
.drawer {
transition: transform 0.2s ease;
}
}
.queue-enter-from,
.queue-leave-to {
.drawer {
transform: translateY(100%);
}
}
}
</style> </style>
+129 -13
View File
@@ -66,20 +66,50 @@ const queueOpen = ref(false)
</div> </div>
<div class="controls"> <div class="controls">
<div class="btns"> <div class="btns">
<Icon name="lucide:shuffle" @click="shuffle()" /> <button
<Icon name="lucide:skip-back" @click="prev()" /> class="icon-button"
aria-label="Shuffle queue"
@click="shuffle()"
>
<Icon name="lucide:shuffle" aria-hidden="true" />
</button>
<button
class="icon-button"
aria-label="Previous track"
@click="prev()"
>
<Icon name="lucide:skip-back" aria-hidden="true" />
</button>
<button
class="icon-button"
:aria-label="isPlaying ? 'Pause' : 'Play'"
@click="isPlaying ? pause() : play()"
>
<Icon <Icon
name="lucide:pause" :name="isPlaying ? 'lucide:pause' : 'lucide:play'"
v-if="isPlaying" aria-hidden="true"
@click="pause()"
/> />
<Icon name="lucide:play" v-else @click="play()" /> </button>
<Icon name="lucide:skip-forward" @click="next()" /> <button class="icon-button" aria-label="Next track" @click="next()">
<Icon name="lucide:skip-forward" aria-hidden="true" />
</button>
<button
class="icon-button"
:class="{ repeating: repeat || repeatOnce }"
:aria-label="
repeatOnce
? 'Repeat current track'
: repeat
? 'Repeat queue'
: 'Repeat off'
"
@click="cycleRepeat()"
>
<Icon <Icon
:name="repeatOnce ? 'lucide:repeat-1' : 'lucide:repeat'" :name="repeatOnce ? 'lucide:repeat-1' : 'lucide:repeat'"
:class="{ repeating: repeat || repeatOnce }" aria-hidden="true"
@click="cycleRepeat()"
/> />
</button>
</div> </div>
<div class="slider"> <div class="slider">
<p>{{ secondsPlayedFormatted }}</p> <p>{{ secondsPlayedFormatted }}</p>
@@ -108,11 +138,20 @@ const queueOpen = ref(false)
:step="0.01" :step="0.01"
/> />
</div> </div>
<Icon <button
name="lucide:list" class="icon-button"
aria-label="Open queue"
@click="queueOpen = true" @click="queueOpen = true"
/> >
<Icon name="lucide:expand" @click="openFullscreen()" /> <Icon name="lucide:list" aria-hidden="true" />
</button>
<button
class="icon-button"
aria-label="Open fullscreen player"
@click="openFullscreen()"
>
<Icon name="lucide:expand" aria-hidden="true" />
</button>
</div> </div>
</template> </template>
</article> </article>
@@ -161,6 +200,17 @@ const queueOpen = ref(false)
cursor: pointer; cursor: pointer;
transition: color 0.2s ease; transition: color 0.2s ease;
} }
.icon-button {
display: grid;
place-items: center;
border: 0;
padding: 0;
color: inherit;
background: transparent;
cursor: pointer;
transition: color 0.2s ease;
}
} }
.track, .track,
@@ -241,4 +291,70 @@ const queueOpen = ref(false)
} }
} }
} }
@media (max-width: 767px) {
#width-fixer {
width: calc(100% + 1rem);
margin-inline: -0.5rem;
}
.player {
bottom: calc(4.5rem + env(safe-area-inset-bottom));
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 0.25rem;
padding: 0.5rem 0.5rem;
margin-inline: -0.5rem;
border-radius: $border-radius-lg;
background-color: rgba(26, 32, 44, 0.96);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
}
.track {
min-width: 0;
gap: 0.6rem;
.details {
min-width: 0;
}
img {
flex: none;
}
}
.controls {
width: auto;
align-items: center;
.slider,
.btns > :not(:nth-child(3)) {
display: none;
}
.btns {
width: auto;
padding: 0;
font-size: 1.5rem;
}
}
.misc-btns {
width: auto;
gap: 0.1rem;
.volume {
display: none;
}
.icon-button {
padding: 0.625rem;
font-size: 1.35rem;
}
}
.controls .icon-button {
padding: 0.625rem;
}
}
</style> </style>
+1 -1
View File
@@ -26,7 +26,7 @@ export const useFullscreen = () => {
await document.documentElement.requestFullscreen() await document.documentElement.requestFullscreen()
return true return true
} catch { } catch {
fullscreen.value = false // Keep the in-app fullscreen player open when native fullscreen is denied.
return false return false
} }
} }