From ae52b1754bb76b27ca6573f3a7285315395c8279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20T=C3=A4kker?= Date: Tue, 6 Oct 2026 03:39:08 +0200 Subject: [PATCH] Adjusted mobile player layout --- app/components/Player/Fullscreen.vue | 58 +++++-- .../Player/Fullscreen/LyricsDisplay.vue | 50 +++++- .../Player/Fullscreen/TrackDisplay.vue | 27 ++++ app/components/Player/Queue.vue | 139 +++++++++++++++- app/components/Player/index.vue | 148 ++++++++++++++++-- app/composables/useFullscreen.ts | 2 +- 6 files changed, 384 insertions(+), 40 deletions(-) diff --git a/app/components/Player/Fullscreen.vue b/app/components/Player/Fullscreen.vue index 0a5a52c..df8c5d2 100644 --- a/app/components/Player/Fullscreen.vue +++ b/app/components/Player/Fullscreen.vue @@ -4,9 +4,14 @@ import { GET_FILE } from "@/utils/file" const { fullscreen, close: closeFullscreen } = useFullscreen() const song = useSong() const mouseMovedRecently = ref(true) +const mobileView = ref(false) onMounted(() => { let timeoutId: NodeJS.Timeout + const mobileQuery = window.matchMedia("(max-width: 767px)") + const updateMobileView = () => (mobileView.value = mobileQuery.matches) + updateMobileView() + mobileQuery.addEventListener("change", updateMobileView) const handleMouseMove = () => { mouseMovedRecently.value = true @@ -28,6 +33,7 @@ onMounted(() => { onUnmounted(() => { window.removeEventListener("mousemove", handleMouseMove) window.removeEventListener("keydown", handleKeydown) + mobileQuery.removeEventListener("change", updateMobileView) }) }) @@ -68,7 +74,7 @@ nuxtApp.hook("page:finish", () => { /> @@ -79,11 +85,7 @@ nuxtApp.hook("page:finish", () => { diff --git a/app/components/Player/Fullscreen/LyricsDisplay.vue b/app/components/Player/Fullscreen/LyricsDisplay.vue index 8c76b81..25b199f 100644 --- a/app/components/Player/Fullscreen/LyricsDisplay.vue +++ b/app/components/Player/Fullscreen/LyricsDisplay.vue @@ -50,6 +50,7 @@ const scrollActiveIndex = computed(() => { const height = ref() const top = ref(0) const syncedHeight = ref() +const mobileView = ref(false) const layoutVersion = ref(0) const lyricOffsets = ref([]) const topFadeExtension = 75 @@ -60,7 +61,14 @@ const updateLyricsLayout = () => { if (trackDisplay) { height.value = trackDisplay.clientHeight 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 @@ -93,8 +101,20 @@ watch( { immediate: true }, ) -onMounted(() => window.addEventListener("resize", updateLyricsLayout)) -onUnmounted(() => window.removeEventListener("resize", updateLyricsLayout)) +onMounted(() => { + 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 outerRef = useTemplateRef("outer-ref") @@ -185,7 +205,7 @@ const finalTransform = computed(() => { class="synced-lyrics-container" :style="{ height: `${(syncedHeight ?? 0) + topFadeExtension}px`, - marginTop: `${top - topFadeExtension}px`, + marginTop: mobileView ? '0px' : `${top - topFadeExtension}px`, }" @wheel.prevent="onWheel" > @@ -306,4 +326,26 @@ const finalTransform = computed(() => { 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; + } +} diff --git a/app/components/Player/Fullscreen/TrackDisplay.vue b/app/components/Player/Fullscreen/TrackDisplay.vue index b974d03..dea5f16 100644 --- a/app/components/Player/Fullscreen/TrackDisplay.vue +++ b/app/components/Player/Fullscreen/TrackDisplay.vue @@ -182,4 +182,31 @@ const durationFormatted = computed(() => 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; + } +} diff --git a/app/components/Player/Queue.vue b/app/components/Player/Queue.vue index 07c6fe5..01572e2 100644 --- a/app/components/Player/Queue.vue +++ b/app/components/Player/Queue.vue @@ -79,6 +79,15 @@ const playAt = (index: number) => queueRows.value.map((row) => row.track), 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) +} @@ -161,6 +200,17 @@ const queueOpen = ref(false) cursor: pointer; 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, @@ -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; + } +} diff --git a/app/composables/useFullscreen.ts b/app/composables/useFullscreen.ts index 0a4436d..631a754 100644 --- a/app/composables/useFullscreen.ts +++ b/app/composables/useFullscreen.ts @@ -26,7 +26,7 @@ export const useFullscreen = () => { await document.documentElement.requestFullscreen() return true } catch { - fullscreen.value = false + // Keep the in-app fullscreen player open when native fullscreen is denied. return false } }