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) +} @@ -118,14 +127,38 @@ const playAt = (index: number) => @drop="finishDrag" @dragend="cancelDrag" > - + + + + + + + + + + + @@ -210,6 +243,14 @@ const playAt = (index: number) => } } +.queue-row { + position: relative; +} + +.queue-order-actions { + display: none; +} + .empty { flex: 1; display: grid; @@ -235,4 +276,86 @@ const playAt = (index: number) => 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%); + } + } +} diff --git a/app/components/Player/index.vue b/app/components/Player/index.vue index 64b3233..37134ec 100644 --- a/app/components/Player/index.vue +++ b/app/components/Player/index.vue @@ -66,20 +66,50 @@ const queueOpen = ref(false) - - - - - - + + + + + + + + + + + + + > + + {{ secondsPlayedFormatted }} @@ -108,11 +138,20 @@ const queueOpen = ref(false) :step="0.01" /> - - + > + + + + + @@ -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 } }
{{ secondsPlayedFormatted }}