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