hw-gs-player

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 5782acfab141c06f0e0714e016de34a1445f9e0c
parent 3a8a36db02ac0bdeb8f334658af4babcb149604d
Author: Sunny <122193933+Sunny-JP@users.noreply.github.com>
Date:   Sun, 16 Mar 2025 15:00:16 +0900

add title name scrolling (#8)


Diffstat:
Msrc/components/Player.vue | 57+++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/style.css | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
2 files changed, 118 insertions(+), 30 deletions(-)

diff --git a/src/components/Player.vue b/src/components/Player.vue @@ -1,9 +1,10 @@ <script setup lang="ts"> import { YoutubeIframe } from '@vue-youtube/component'; -import { ref, computed, onUnmounted } from 'vue'; +import { ref, computed, onUnmounted, nextTick } from 'vue'; import queue from '../queue'; -let videoId = ref('1QkKDY1-NN0'); +let videoId = ref('zxt-_P_WXtM'); // example of long titleName +// let videoId = ref('XAwXaGQwoZ0'); // example of short titleName const player = ref<any>(null); const nowTime = ref(0); const endTime = ref(0); @@ -13,16 +14,48 @@ const intervalId = ref<number | null>(null); const isPlaying = ref(false); let albumTitle = queue.get_albumTitle(); +const titleElement = ref<HTMLElement | null>(null); +const needsScroll = ref(false); +const parentWidth = ref(0); +const textWidth = ref(0); +const isAnimating = ref(false); +const SCROLL_SPEED = 40; // px/s +const WAIT_TIME = 2000; // ms + let next_song; const togglePlay = async () => { - player.value?.togglePlay(); // change play-pause + player.value?.togglePlay(); // Change play-pause }; -// Get total time and video title const onReady = (event: { target: any }) => { - endTime.value = Math.floor(event.target.getDuration()); + endTime.value = Math.floor(event.target.getDuration()); // Get total time and video title title.value = event.target.getVideoData(); titleName.value = title.value['title']; + + nextTick(() => { // Determine if scrolling is necessary + if (titleElement.value) { + parentWidth.value = titleElement.value.parentElement?.offsetWidth || 0; + textWidth.value = titleElement.value.scrollWidth; + needsScroll.value = textWidth.value > parentWidth.value; + + if (needsScroll.value) { + startScrolling(); + } + } +}); +}; + +// Calculate the distance to scroll +const animationDuration = computed(() => { + const distance = textWidth.value; + return needsScroll.value ? distance / SCROLL_SPEED : 0; +}); + +// scrolling function +const startScrolling = async () => { + if (!needsScroll.value) return; + await new Promise(resolve => setTimeout(resolve, WAIT_TIME)); + isAnimating.value = true; }; // When the playback state is changed @@ -93,7 +126,19 @@ onUnmounted(() => { @ready="onReady" @state-change="onStateChange" /> <div class="info_box"> - <div class="title">{{ titleName }}</div> + <div class="title"> + <div ref="titleElement" + class="scroll-container" + :class="{ scrolling: isAnimating }" + :style="isAnimating ? `--animation-duration: ${animationDuration}s;` : ''"> + <span v-if="needsScroll" class="scroll-text"> + {{ titleName }}&nbsp;&nbsp;&nbsp;{{ titleName }} + </span> + <span v-else class="single-text"> + {{ titleName }} + </span> + </div> + </div> <div class="album">{{albumTitle}}</div> <div class="time_container"> <div class="nowtime">{{ formattedNowTime }}</div> diff --git a/src/style.css b/src/style.css @@ -16,7 +16,7 @@ body { display: flex; flex-direction: column; align-items: center; - padding: 15px; + padding: 30px; } /* title bar */ @@ -134,8 +134,8 @@ body { /* player */ iframe { - margin: 10px; - width: 80%; + margin-bottom: 5px; + width: 100%; height: 100%; max-width: 500px; aspect-ratio: 1/1; @@ -143,24 +143,72 @@ iframe { .info_box { margin: 5px; - width: 80%; + width: 100%; max-width: 500px; } .title { - font-size: 1.8rem; + overflow: hidden; + white-space: nowrap; + position: relative; + width: 100%; +} + +.scroll-container { + display: flex; + align-items: center; + white-space: nowrap; + font-size: clamp(3px, calc(6vw - 2px), 30px); +} + +.scrolling .scroll-text { + display: inline-block; + animation: scrollText var(--animation-duration) linear infinite; +} + +@keyframes scrollText { + 0% { + transform: translateX(0); + } + + 20% { + transform: translateX(0); + } + + 97% { + opacity: 1; + } + + 98% { + opacity: 0; + transform: translateX(calc(-50%)); + } + + 99% { + opacity: 0; + transform: translateX(0); + } + + 100% { + opacity: 1; + } +} + +.single-text { + display: inline-block; + opacity: 1; } .album { - font-size: 1.1rem; + font-size: clamp(2px, calc(4vw - 1.3px), 20px); } .time_container { display: flex; justify-content: flex-start; align-items: center; - margin-top: 5px; - font-size: 0.8rem; + margin-top: 0.5em; + font-size: clamp(1.5px, calc(3vw - 1px), 15px); } .endtime { @@ -171,48 +219,43 @@ iframe { /* control button */ /* play-pause button */ .play-pause-button { + font-size: clamp(2px, 4vw, 20px); background: none; border: none; - padding-top: 50px; - padding-left: 10px; + margin-top: 2em; + padding: 0.5em 0.5em 0.5em 1em; cursor: pointer; display: flex; align-items: center; justify-content: center; - width: 100%; - height: 60px; } .play-pause-icon { position: relative; - width: 36px; - height: 40px; + width: 2em; + height: 2.2em; } .bar { position: absolute; background: white; - width: 17.3px; - height: 40px; + width: 0.9em; + height: 2.2em; transition: all 0.3s ease-in-out; } .bar-left { - left: 1px; + left: 0.12em; clip-path: polygon(0% 0%, 100% 26%, 100% 74%, 0% 100%); } .bar-right { - right: 1px; + right: 0.12em; clip-path: polygon(0% 25%, 100% 50%, 0% 75%); } -.playing .bar-left { - clip-path: inset(0 0 0 0); - width: 10px; -} - +.playing .bar-left, .playing .bar-right { clip-path: inset(0 0 0 0); - width: 10px; + width: 0.5em; } \ No newline at end of file