hw-gs-player

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

commit a52a220c5f60cfb008ffa15df4fb1c5fc6f221ae
parent f84d149cd3b5ca9a877fd6569451e35a446c7e98
Author: Sunny <122193933+Sunny-JP@users.noreply.github.com>
Date:   Thu, 20 Mar 2025 17:02:54 +0900

Dev 6 (#17)

* fix style

* Update queue.ts (#16)

* Update queue.ts

* Update queue.ts

* delete to show queue

* install cookies

* adjust p-p-button

* adjust loop, album

---------

Co-authored-by: Minerva_juppiter
Diffstat:
Mpackage-lock.json | 26+++++++++++++++++++++++++-
Mpackage.json | 2++
Msrc/App.vue | 2--
Msrc/assets/AlbumsList.json | 26+++++++++++++-------------
Msrc/components/Player.vue | 211++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/queue.ts | 49+++++++++++++++++++++++++++----------------------
Msrc/style.css | 6++++--
Msrc/vite-env.d.ts | 1+
8 files changed, 223 insertions(+), 100 deletions(-)

diff --git a/package-lock.json b/package-lock.json @@ -8,10 +8,13 @@ "name": "gs-player", "version": "0.0.0", "dependencies": { + "@jamescoyle/vue-icon": "^0.1.2", + "@mdi/js": "^7.4.47", "@vue-youtube/component": "^0.0.6", "@vue-youtube/core": "^0.0.6", "vue": "3.5.13", - "vue-router": "^4.5.0" + "vue-router": "^4.5.0", + "vue3-cookies": "^1.0.6" }, "devDependencies": { "@vitejs/plugin-vue": "5.2.1", @@ -492,12 +495,24 @@ "node": ">=18" } }, + "node_modules/@jamescoyle/vue-icon": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@jamescoyle/vue-icon/-/vue-icon-0.1.2.tgz", + "integrity": "sha512-KFrImXx5TKIi6iQXlnyLEBl4rNosNKbTeRnr70ucTdUaciVmd9qK9d/pZAaKt1Ob/8xNnX2GMp8LisyHdKtEgw==", + "license": "MIT" + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "license": "MIT" }, + "node_modules/@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", + "license": "Apache-2.0" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.35.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", @@ -1463,6 +1478,15 @@ "peerDependencies": { "typescript": ">=5.0.0" } + }, + "node_modules/vue3-cookies": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vue3-cookies/-/vue3-cookies-1.0.6.tgz", + "integrity": "sha512-a1UvVD0qIgxyOqjlSOwnLnqAnz8ASltugEv8yX+96i/WGZAN9fEDci7xO4HIWZE1uToUnRq9JnFhvfDCSo45OA==", + "license": "MIT", + "dependencies": { + "vue": "^3.0.0" + } } } } diff --git a/package.json b/package.json @@ -9,6 +9,8 @@ "preview": "vite preview" }, "dependencies": { + "@jamescoyle/vue-icon": "^0.1.2", + "@mdi/js": "^7.4.47", "@vue-youtube/component": "^0.0.6", "@vue-youtube/core": "^0.0.6", "vue": "3.5.13", diff --git a/src/App.vue b/src/App.vue @@ -34,8 +34,6 @@ onBeforeUnmount(() => { <ul> <!-- 将来的には多言語対応する.中,韓ではそれぞれNoto sans C,Kを用いる --> <li><router-link to="/" @click="MenuButton = false">Top</router-link></li> - <li><router-link to="/" @click="MenuButton = false">My List</router-link></li> - <hr> <li><router-link to="/about" @click="MenuButton = false">About</router-link></li> <li><router-link to="/terms" @click="MenuButton = false">利用規約</router-link></li> <li><router-link to="/policy" @click="MenuButton = false">プライバシーポリシー</router-link></li> diff --git a/src/assets/AlbumsList.json b/src/assets/AlbumsList.json @@ -39,43 +39,43 @@ { "albumArt": "/AlbumArts/Featured_Original_Soundtrack.png", "albumTitle": { - "ja": "原神OST(公式)", + "ja": "原神OST(公式)あああああああああああああああああああああああ", "en": "Genshin-OST(Official)" }, - "songs":[ + "songs": [ { - "title":{ + "title": { "ja": "スペシャルEP「幽谷の舟唄」" }, "videoId": "QK7ZG2JXYB4" }, { - "title":{ - "ja":"輝く星星Vol.5" + "title": { + "ja": "輝く星星Vol.5" }, "videoId": "rnQvbUSf1TM" }, { - "title":{ - "ja":"流星の奇跡Vol.3" + "title": { + "ja": "流星の奇跡Vol.3" }, - "videoId":"q6V8_s7F-1A" + "videoId": "q6V8_s7F-1A" }, { - "title":{ + "title": { "ja": "真珠の歌4 集う光彩" }, "videoId": "a657IulU7g0" }, { - "title":{ - "ja":"真珠の歌4 言葉なき歌" + "title": { + "ja": "真珠の歌4 言葉なき歌" }, "videoId": "8Is8VZ7kXCc" }, { - "title":{ - "ja":"真珠の歌 真夏の歌" + "title": { + "ja": "真珠の歌 真夏の歌" }, "videoId": "5Xj4cxvNxkk" } diff --git a/src/components/Player.vue b/src/components/Player.vue @@ -1,19 +1,27 @@ <script setup lang="ts"> import { YoutubeIframe } from '@vue-youtube/component'; -import { ref, computed, onUnmounted, nextTick } from 'vue'; +import { ref, computed, onUnmounted, nextTick, watch } from 'vue'; import queue from '../queue'; +import SvgIcon from '@jamescoyle/vue-icon'; +import { mdiRepeatOnce, mdiRepeatOff } from '@mdi/js'; -// let videoId = ref('zxt-_P_WXtM'); // example of long titleName -// let videoId = ref('XAwXaGQwoZ0'); // example of short titleName +// YouTube player instance const player = ref<any>(null); + +// Video time const nowTime = ref(0); const endTime = ref(0); -let title = ref(); -let titleName = ref(); const intervalId = ref<number | null>(null); const isPlaying = ref(false); + +// Video title +const title = ref(); +const titleName = ref(); + +// Album info let albumTitle = queue.get_albumTitle(); +// Title scrolling const titleElement = ref<HTMLElement | null>(null); const needsScroll = ref(false); const parentWidth = ref(0); @@ -22,58 +30,75 @@ const isAnimating = ref(false); const SCROLL_SPEED = 40; // px/s const WAIT_TIME = 2000; // ms +// Fullscreen mode const isFullscreen = ref(false); +const animationClass = ref(""); +// Toggle fullscreen mode const toggleFullscreen = () => { isFullscreen.value = !isFullscreen.value; }; +// Watch fullscreen state changes +watch(isFullscreen, (newVal, oldVal) => { + if (newVal !== oldVal) { + animationClass.value = newVal ? "slide-up" : "slide-down"; + + // Remove animation class after transition + setTimeout(() => { + animationClass.value = ""; + }, 250); + } +}); + +// Toggle play/pause const togglePlay = async () => { - player.value?.togglePlay(); // Change play-pause + player.value?.togglePlay(); }; +// When player is ready const onReady = (event: { target: any }) => { - endTime.value = Math.floor(event.target.getDuration()); // Get total time and video title + endTime.value = Math.floor(event.target.getDuration()); 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; + nextTick(() => { + 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(); + if (needsScroll.value) { + startScrolling(); + } } - } -}); + }); }; -// Calculate the distance to scroll +// Compute animation duration const animationDuration = computed(() => { - const distance = textWidth.value; - return needsScroll.value ? distance / SCROLL_SPEED : 0; + return needsScroll.value ? textWidth.value / SCROLL_SPEED : 0; }); -// scrolling function +// Start scrolling if needed const startScrolling = async () => { if (!needsScroll.value) return; await new Promise(resolve => setTimeout(resolve, WAIT_TIME)); isAnimating.value = true; }; -// When the playback state is changed +// Handle video state changes const onStateChange = (event: { target: any; data: number }) => { - if (event.data === 1) { // start - isPlaying.value = true; // play-pause-button - intervalId.value = window.setInterval(() => { // Get current time per 250ms + if (event.data === 1) { // Playing + isPlaying.value = true; + intervalId.value = window.setInterval(() => { nowTime.value = Math.floor(event.target.getCurrentTime()); }, 250); - } else if (event.data === 0) { // end(0) + } else if (event.data === 0) { // End + isPlaying.value = false; queue.get_next(); - } else if (event.data === 2) { // stop (2) - isPlaying.value = false; // play-pause-button + } else if (event.data === 2) { // Paused + isPlaying.value = false; if (intervalId.value !== null) { clearInterval(intervalId.value); intervalId.value = null; @@ -82,18 +107,18 @@ const onStateChange = (event: { target: any; data: number }) => { albumTitle = queue.get_albumTitle(); }; -// Format to mm:ss +// Format time as mm:ss const formatTime = (time: number) => { const minutes = Math.floor(time / 60); const seconds = time % 60; return `${minutes}:${seconds.toString().padStart(2, '0')}`; }; -// Get formatted time with computed +// Computed formatted time const formattedNowTime = computed(() => formatTime(nowTime.value)); const formattedEndTime = computed(() => formatTime(endTime.value)); -// Clear setInterval when component is destroyed +// Clear interval on component unmount onUnmounted(() => { if (intervalId.value !== null) { clearInterval(intervalId.value); @@ -104,26 +129,37 @@ const renderKey = queue.get_playerRenderKey(); </script> <template> - <div :class="{ 'container': !isFullscreen, 'fscontainer': isFullscreen }" :key="renderKey"> + <div + :class="[ + isFullscreen ? 'fscontainer' : 'container', + animationClass + ]" + :key="renderKey" + > <youtube-iframe - :video-id=queue.get_nowSong() + :video-id="queue.get_nowSong()" :player-vars="{ autoplay: 1, iv_load_policy: 3, - loop: 1, + loop: 0, controls: 0, playsinline: 1, rel: 0 - }" + }" ref="player" @ready="onReady" - @state-change="onStateChange" /> + @state-change="onStateChange" + /> + <div class="info_box"> + <!-- Video title --> <div class="title" @click="toggleFullscreen"> - <div ref="titleElement" - class="scroll-container" - :class="{ scrolling: isAnimating }" - :style="isAnimating ? `--animation-duration: ${animationDuration}s;` : ''"> + <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> @@ -132,16 +168,28 @@ const renderKey = queue.get_playerRenderKey(); </span> </div> </div> - <div class="album">{{albumTitle}}</div> + + <!-- Time and album info --> <div class="time_container"> <div class="nowtime">{{ formattedNowTime }}</div> <div class="endtime">{{ formattedEndTime }}</div> + <div class="album">{{ albumTitle }}</div> </div> + + <div class="fs_album_container"> + <div class="fs_album">{{ albumTitle }}</div> + <div class="fs_loop" @click="queue.onLoopButton()"> + <SvgIcon type="mdi" :path="queue.isLoop.value ? mdiRepeatOnce : mdiRepeatOff" /> + </div> + </div> + <div class="fs_time_container"> <div class="nowtime">{{ formattedNowTime }}</div> <div class="endtime">{{ formattedEndTime }}</div> </div> </div> + + <!-- Play/Pause button --> <button @click="togglePlay" class="play-pause-button"> <div class="play-pause-icon" :class="{ playing: isPlaying }"> <div class="bar bar-left"></div> @@ -149,11 +197,9 @@ const renderKey = queue.get_playerRenderKey(); </div> </button> </div> - <button @click="queue.onLoopButton()" class="toggle-btn-loop"> - Loop - </button> </template> + <style scoped> /* overall frame */ .container { @@ -170,7 +216,6 @@ const renderKey = queue.get_playerRenderKey(); padding-top: clamp(1px, 2vw, 10px); padding-bottom: clamp(1px, 2vw, 10px); flex-direction: row; - animation: slideDown 0.25s ease forwards; } .fscontainer { @@ -179,6 +224,13 @@ const renderKey = queue.get_playerRenderKey(); align-items: center; padding: 30px; background: #000000; +} + +.slide-down { + animation: slideDown 0.25s ease forwards; +} + +.slide-up { animation: slideUp 0.25s ease forwards; } @@ -219,13 +271,14 @@ const renderKey = queue.get_playerRenderKey(); .fscontainer .info_box { position: relative; - top: 55px; + top: 10px; margin: 5px; width: 100%; max-width: 500px; } .title { + cursor: pointer; overflow: hidden; white-space: nowrap; position: relative; @@ -284,14 +337,58 @@ const renderKey = queue.get_playerRenderKey(); opacity: 1; } +.fs_loop { + cursor: pointer; +} + .container .album { + font-family: "Poppins", "Noto Sans JP", "Helvetica Neue", "Helvetica", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Meiryo", sans-serif; + font-size: clamp(1.5px, calc(3vw - 1px), 15px); + margin-left: 10px; + cursor: pointer; + white-space: nowrap; + overflow-x: scroll; + -ms-overflow-style: none; /* Scrollbar hidden (IE・Edge) */ + scrollbar-width: none; /* Scrollbar hidden (Firefox) */ +} + +.container .album::-webkit-scrollbar{ /* Scrollbar hidden (Chrome・Safari) */ + display: none; +} + +.container .fs_album { font-size: 0; } .fscontainer .album { + font-size: 0; +} + +.container .fs_album_container { + font-size: 0; + display: none; +} + +.fscontainer .fs_album_container { + display: flex; + justify-content: flex-start; + align-items: center; font-size: clamp(2px, calc(4vw - 1.3px), 20px); } +.fscontainer .fs_album { + margin-right: 40px; + cursor: pointer; + white-space: nowrap; + overflow-x: scroll; + -ms-overflow-style: none; /* Scrollbar hidden (IE・Edge) */ + scrollbar-width: none; /* Scrollbar hidden (Firefox) */ +} + +.container .fs_album::-webkit-scrollbar{ /* Scrollbar hidden (Chrome・Safari) */ + display: none; +} + .container .queue { font-size: 0; } @@ -305,7 +402,7 @@ const renderKey = queue.get_playerRenderKey(); display: flex; justify-content: flex-start; align-items: center; - font-size: clamp(1.5px, calc(3vw - 1px), 15px); + font-size: clamp(1.75px, calc(3.5vw - 1.15px), 17.5px); } .container .fs_time_container { @@ -323,7 +420,7 @@ const renderKey = queue.get_playerRenderKey(); align-items: center; margin-top: 0.5em; margin-bottom: 2em; - font-size: clamp(1.75px, calc(3.5vw - 1.15px), 17.5px); + font-size: clamp(2px, calc(4vw - 1.3px), 20px); } .container .endtime { @@ -353,9 +450,15 @@ const renderKey = queue.get_playerRenderKey(); justify-content: center; } +.container .play-pause-button{ + font-size: clamp(1.5px, 3vw, 15px); + padding-left: clamp(1px, 1vw, 5px); + padding-right: clamp(1px, 1vw, 5px); +} + .fscontainer .play-pause-button { position: relative; - top: 55px; + top: 35px; } .play-pause-icon { @@ -389,16 +492,4 @@ const renderKey = queue.get_playerRenderKey(); clip-path: inset(0 0 0 0); width: 0.5em; } - - -.toggle-btn-loop { - position: fixed; - bottom: 90px; - left: 60px; - background: #4c4c4c; - color: #ffffff; - border: none; - padding: 5px; - cursor: pointer; -} </style> diff --git a/src/queue.ts b/src/queue.ts @@ -1,5 +1,5 @@ /* -This Script manage the queue for play. +This Script manages the queue for play. */ export interface Queue { @@ -9,44 +9,39 @@ export interface Queue { import { ref } from "vue"; - let queue: Queue[] = []; let albumTitle: string = "Unknown Album"; let nextSong; let playerRenderKey = ref(0); -let isLoop: boolean = false; +let isLoop = ref(false); function get_albumTitle(): string { return albumTitle; } -function get_next(): (string | null) { +function get_next(): string | null { console.log("get_next was called"); - if (isLoop) { + if (isLoop.value) { playerRenderKey.value += 1; - return queue[0].videoId; + return queue[0]?.videoId || null; } else { - if (queue.length == 1) { + if (queue.length === 1) { console.log("queue was ended"); return null; } else { nextSong = queue[1].videoId; queue.shift(); - console.log("i will play" + nextSong); + console.log("i will play " + nextSong); playerRenderKey.value += 1; return nextSong; } - }; -}; - -function get_nowSong(): (string|undefined) { - if(queue.length < 1) { - return undefined - } else { - return queue[0].videoId; } } +function get_nowSong(): string | undefined { + return queue.length > 0 ? queue[0].videoId : undefined; +} + function add_album(Album: Queue[], AlbumTitle: string) { queue = Album; albumTitle = AlbumTitle; @@ -57,21 +52,31 @@ function add_album(Album: Queue[], AlbumTitle: string) { function del_all() { queue = []; albumTitle = "Unknown Album"; -}; +} function get_playerRenderKey() { return playerRenderKey; } function onLoopButton() { - isLoop = !isLoop; - console.log("loop is " + isLoop); + isLoop.value = !isLoop.value; + console.log("loop is " + isLoop.value); } -function get_queueTitleList(): (string[]) { - console.log("get_queueTitleList was called") +function get_queueTitleList(): string[] { + console.log("get_queueTitleList was called"); console.log(queue.map((queue) => queue.SongName)); return queue.map((map) => map.SongName); } -export default {get_albumTitle, get_next, get_nowSong, add_album, del_all, get_playerRenderKey, onLoopButton, get_queueTitleList }; +export default { + get_albumTitle, + get_next, + get_nowSong, + add_album, + del_all, + get_playerRenderKey, + onLoopButton, + get_queueTitleList, + isLoop +}; diff --git a/src/style.css b/src/style.css @@ -11,6 +11,7 @@ body { font-weight: 400; background: #000000; -webkit-text-size-adjust: none; + user-select: none; } @@ -32,6 +33,7 @@ body { background-color: #1f1f1f; -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 95%, rgba(0, 0, 0, 0) 100%); mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 95%, rgba(0, 0, 0, 0) 100%); + z-index: -10; } .title_bar p { @@ -55,7 +57,7 @@ body { width: 80px; height: 55px; cursor: pointer; - z-index: 50; + z-index: -10; } .hamburger_btn .line { @@ -109,6 +111,7 @@ body { font-weight: 500; transform: translateX(100%); transition: transform 0.4s ease; + z-index: -10; } .menu.open { @@ -156,7 +159,6 @@ body { .fscontainer iframe { position: relative; - top: 55px; margin-bottom: 5px; width: 100%; max-width: 500px; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts @@ -1 +1,2 @@ /// <reference types="vite/client" /> +declare module '@jamescoyle/vue-icon';