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:
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 }} {{ 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