diff --git a/src/components/MusicView.vue b/src/components/MusicView.vue new file mode 100644 index 0000000000000000000000000000000000000000..bb941e7d80a94f89b155328d36af2ec846f5e641 --- /dev/null +++ b/src/components/MusicView.vue @@ -0,0 +1,655 @@ +<script setup> +import {computed, nextTick, onMounted, onUnmounted, ref, watch} from "vue"; +import playButton from "../icon/playButton.vue"; +import {ElMessage} from "element-plus"; +import {backgroundColor, updateBackground} from "../utils/getBackgroundColor"; +import pauseButton from "../icon/pauseButton.vue"; + +import {addSongToPlaylist} from "../api/playlist"; +import { loadSongDurations } from '../utils/loadSongDurations'; + +/* + USER + */ +const userToken = ref(JSON.parse(sessionStorage.getItem('user-token'))); +const currentUserId = ref(userToken.value.id); + +/* +姝岃瘝 + */ +const lyrics = ref([]); // 姝岃瘝鏁扮粍 + +function toggleLyrics() { + isLyricsDisplaying.value = !isLyricsDisplaying.value; +} + +const emit = defineEmits(['pauseSong', 'switchSongs', 'switchToArtist', 'back']); +const props = defineProps({ + songInfo: {// 绫诲瀷 锛歩d, title, artist, album, description, picPath,uploadTime + type: Object, + required: true, + }, + playFromLeftBar: null, + currentSongId: { + type: Number, + required: true + }, + isPaused: { + type: Boolean, + } +}); + + + +const recMusicList = ref([ + { + id: 4, + number: 1, + name: "NightTheater", + author: "Wakadori", + img: require("../assets/pictures/songs/1.jpg"), + time: "3:30", + album: "NightTheater" + }, +]) + +let musicHoveredIndex = ref(null); +let musicClickedIndex = ref(null); +let musicPlayIndex = ref(null); +let musicPauseIndex = ref(null); + +const resizeObserver = ref(null) +const gradientColor = computed(() => `linear-gradient(to bottom, ${backgroundColor.value} , #1F1F1F 50%)`) + +//鑾峰彇姝屾洸鏃堕暱 +const songDurations = ref(new Map()); +watch(() => props.musicList, (newSongs) => { + loadSongDurations(newSongs, songDurations); +}, { immediate: true }); + +const debounce = (fn, delay) => { + let timer + return (...args) => { + if (timer) { + clearTimeout(timer) + } + timer = setTimeout(() => { + fn(...args) + }, delay) + } +} + + +onMounted(() => { + resizeObserver.value = new ResizeObserver(debounce(handleResize, 50)); + console.log(resizeObserver.value) + nextTick(() => { + const albumContent = document.querySelector(".album-content"); + if (albumContent) { + resizeObserver.value.observe(albumContent); + } + }) + + musicPlayIndex = props.currentSongId; + musicClickedIndex = props.currentSongId; + musicPauseIndex = props.isPaused ? props.currentSongId : null; +}) + +onUnmounted(() => { + if (resizeObserver.value) { + resizeObserver.value.disconnect(); + } +}) + +const handelScroll = (event) => { + const playArea = document.querySelector(".play-area"); + const fixedPlayArea = document.querySelector(".fixed-play-area"); + const tipArea = document.querySelector(".tips"); + const fixedTipArea = document.querySelector(".fixed-tips"); + const albumContent = document.querySelector(".album-content"); + + const offsetHeight = albumContent.offsetTop; + const stickyPlayY = playArea.offsetTop - offsetHeight; + const stickyTipY = tipArea.offsetTop - offsetHeight; + const curOffset = offsetHeight - albumContent.getBoundingClientRect().top; + + console.log(stickyPlayY, stickyTipY); + if (curOffset >= stickyPlayY) { + fixedPlayArea.style.opacity = "1"; + fixedPlayArea.style.top = offsetHeight + "px"; + + + fixedPlayArea.style.width = (albumContent.clientWidth - 20) + "px"; + } else { + fixedPlayArea.style.opacity = "0"; + } + if (curOffset + fixedPlayArea.scrollHeight >= stickyTipY) { + fixedTipArea.style.display = "flex"; + fixedTipArea.style.top = offsetHeight + fixedPlayArea.scrollHeight + 'px'; + + } else { + fixedTipArea.style.display = "none"; + } +} + + +watch(props.playFromLeftBar, () => { + playFromId(props.playFromLeftBar) +}) + +const playFromId = (musicId) => { + musicPlayIndex = musicId; + emit('switchSongs', musicPlayIndex); + musicPauseIndex = null; +} + +const addToFavorite = (musicId, albumId) => { + addSongToPlaylist({ + user_id: currentUserId.value, + playlist_id: albumId, + song_id: musicId, + }).then(() => { + ElMessage({ + message: "娣诲姞鑷�: " + props.albumInfo.title, + grouping: true, + type: 'info', + offset: 16, + customClass: "reco-message", + duration: 4000, + }) + }) +} +watch(() => props.isPaused, (newValue) => { + if (newValue) { + musicPauseIndex = musicPlayIndex; + } else { + musicPauseIndex = null; + } +}); + +const enterAuthorDescription = (artistName) => { + emit('switchToArtist', artistName); +} + +const pauseMusic = (musicId) => { + musicPauseIndex = musicId; + emit('pauseSong'); +} + +const addRecommendMusic = (musicId) => { + console.log(musicId); + //TODO:娣诲姞姝屾洸鍒版寚瀹氱殑姝屽崟 + ElMessage({ + message: "娣诲姞鑷抽粯璁ゆ敹钘忓す", + grouping: true, + type: 'info', + offset: 16, + customClass: "reco-message", + duration: 4000, + }) +} + +watch(() => props.currentSongId, (newId) => { + if (newId) { + musicPlayIndex = newId; + musicClickedIndex = newId; + musicPauseIndex = props.isPaused ? newId : null; + } +}, { immediate: true }); +// 鍒ゆ柇褰撳墠鎾斁鐨勬瓕鏇叉槸鍚︽槸杩欓姝� +const isCurrentSong = computed(() => { + return (musicPlayIndex === props.musicId); +}); +const enterMusicDescription = (musicId) => { +} +</script> + +<template> + <div v-if="songInfo !== null"> + <div class="album-content" :style="{backgroundImage: gradientColor}" @mousewheel="handelScroll"> + <div class="back-button" data-tooltip="杩斿洖" @click="$emit('back')"> + <svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor"> + <path d="M11.03.47a.75.75 0 0 1 0 1.06L4.56 8l6.47 6.47a.75.75 0 1 1-1.06 1.06L2.44 8 9.97.47a.75.75 0 0 1 1.06 0z"></path> + </svg> + </div> + <div class="header"> + <img :src="songInfo.picPath" alt="" class="album-image" @load="updateBackground(songInfo.picPath)"/> + <div class="header-content"> + <p style="text-align: left;margin:20px 0 0 15px">姝屾洸</p> + <p class="header-album-name" style="font-weight: bolder;font-size:80px;margin:10px 0 35px 10px;"> + {{ songInfo.title }}</p> + <div class="header-content-detail"> + <p class="header-creator" @click="">{{ songInfo.artist }}</p> + <p style="padding: 0 2px 0 2px ">鈥�</p> + <p class="header-creator" @click="">{{ songInfo?.album }}</p> + <p style="padding: 0 2px 0 2px ">鈥�</p> + <p v-if="songInfo.uploadTime !== undefined"> + {{ songInfo.uploadTime.substring(0, 10) }} </p> + </div> + </div> + </div> + + <div class="content"> + <div class="play-area"> + <div class="play-button"> + <play-button v-if="!isCurrentSong||musicPauseIndex!==null" + @click="playFromId(musicPauseIndex)" + style="position: absolute; top:20%;left:24%;color: #000000"/> + <pause-button v-if="isCurrentSong&&musicPauseIndex===null" + @click="pauseMusic(musicPlayIndex)" + style="position: absolute; top:24%;left:25%;color: #000000"/> + </div> + </div> + + <div class="fixed-play-area" :style="{background :`${backgroundColor}`}"> + <div class="opacity-wrapper"> + <div class="play-button"> + <play-button v-if="!isCurrentSong||musicPauseIndex!==null" + @click="playFromId(musicPauseIndex)" + style="position: absolute; top:20%;left:24%;color: #000000"/> + <pause-button v-if="isCurrentSong&&musicPauseIndex===null" + @click="pauseMusic(musicPlayIndex)" + style="position: absolute; top:24%;left:25%;color: #000000"/> + </div> + <p style="padding-left: 10px;font-weight: bold;font-size: 26px">{{ songInfo.title }}</p> + </div> + </div> + +<!-- <div class="other-info"> + <div class="lyrics-lines" :style="{ transform: `translateY(${-currentLineIndex * 40}px)` }"> + <div + v-for="(line, index) in lyrics" + :key="index" + :class="{ active: index === currentLineIndex }" + class="lyrics-line" + >{{ line.text }} + </div> + <h1 v-if="lyrics.length === 0" style=" + font-size: 24px; + color: #9d9d9d; + margin-top: 240px; + font-family: Consolas, 骞煎渾, serif; + ">Ouch锛佽姝屾洸鏆傛棤姝岃瘝锛�</h1> + </div> + </div>--> + + <!--TODO:鎺ㄨ崘姝屾洸鐨勭粏鑺傚鐞�--> + <div class="other-info"> + <div style="margin-left:20px;margin-bottom:20px;"> + <div style="display: flex;text-align: left;justify-content: center;flex-direction: column"> + <span style="color:white;font-size: 30px;font-weight: bolder">鎺ㄨ崘</span> + <span style="color:grey;font-size: 20px">鍩轰簬姝ゆ瓕鏇� + </span> + </div> + </div> + + <div class="recMusicList"> + <div class="music-item" + v-for="music in recMusicList" + :key="music.id" + :aria-selected="musicClickedIndex === music.id ? 'True':'False'" + @mouseenter="()=>{musicHoveredIndex = music.id;}" + @mouseleave="()=>{musicHoveredIndex = null}" + @click="musicClickedIndex=music.id" + @dblclick="playFromId(music.id)" + :style="{backgroundColor: musicClickedIndex===music.id? '#404040': + musicHoveredIndex === music.id ? 'rgba(54,54,54,0.7)' :'rgba(0,0,0,0)', + }"> + + + <div + :style="{visibility: musicHoveredIndex === music.id||musicPlayIndex === music.id ? 'hidden' : 'visible' }"> + {{ + recMusicList.indexOf(music) + 1 + }} + </div> + <play-button @click="playFromId(music.id)" style="position: absolute;left: 33px;cursor: pointer" + v-if="(musicHoveredIndex === music.id&&musicPlayIndex!==music.id)||musicPauseIndex===music.id" + :style="{color: musicPauseIndex===music.id ? '#1ed660' : 'white'}"/> + <pause-button @click="pauseMusic(music.id)" + style="color:#1ed660 ;position: absolute;left: 37px;cursor: pointer" + v-if="musicPlayIndex===music.id&&musicHoveredIndex === music.id&&musicPauseIndex!==music.id"/> + <img width="17" height="17" alt="" + style="position: absolute;left: 42px;" + v-if="musicPlayIndex===music.id&&musicHoveredIndex !== music.id&&musicPauseIndex!==music.id" + src="https://open.spotifycdn.com/cdn/images/equaliser-animated-green.f5eb96f2.gif"> + + <div class="music-detailed-info"> + <img class="music-image" + :src="music.picPath" + alt="姝屾洸鍥剧墖"/> + <div class="music-name-author" style="padding-left: 5px;"> + <p @click="enterMusicDescription(music.id)" class="music-name" + :style="{color : musicPlayIndex ===music.id? '#1ED660':''}" + :class="[musicPlayIndex === music.id ? 'music-after-click' : '']" + >{{ music.title }}</p> + + <p @click="enterAuthorDescription(music.artist)" class="music-author" + :style="{color:musicHoveredIndex === music.id? 'white' : '#b2b2b2'}"> + {{ music.artist }}</p> + </div> + </div> + + <div class="music-album-info" + :style="{color:musicHoveredIndex === music.id? 'white' : '#b2b2b2'}"> + {{ music.album }} + </div> + <div class="music-right-info"> + <button class="reco-add-button" @click="addRecommendMusic(music.id)">娣诲姞</button> + + </div> + + </div> + + </div> + </div> + + </div> + + </div> + </div> + +</template> + +<style scoped> +li { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + +} + +li:hover { + background-color: #363636; + border-radius: 12px; +} + + +p { + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin: 0; +} + +.header, .play-area, .other-info { + z-index: 0; + padding: 20px; + width: 100%; + box-sizing: border-box; + user-select: none; +} + +.album-content { + margin: 0; + padding: 0; + color: white; + background-color: #121212; + transition: background-color ease 0.6s; + display: flex; + flex-direction: column; + width: 100%; + overflow-x: auto; /*鍗冧竾涓嶈兘鍒狅紝涓嶇劧鑳屾櫙榛戜竴鍗�*/ +} + + +.header { + display: flex; + flex-direction: row; +} + +.content { + z-index: 1; + background-color: rgba(0, 0, 0, 0.20); +} + +.album-image { + border-radius: 6%; + width: 160px; + height: 160px; + margin-top: 30px; + margin-left: 15px; + margin-right: 20px; +} + +.header-content { + display: flex; + flex-direction: column; + height: 100%; + position: relative; + flex-grow: 1; + min-width: 0; +} + +.header-content-detail { + padding: 10px; + align-items: center; + display: flex; + flex-direction: row; +} + +.header-creator { + margin: 0 6px; + cursor: pointer; + font-weight: bolder +} + +.header-creator:hover { + text-decoration: underline; +} + +.play-area { + position: relative; +} + +.fixed-play-area { + top: 0; + z-index: 11; + opacity: 0; + transition: opacity 0.2s ease-out; + border-radius: 12px 12px 0 0; + position: fixed; /**/ + display: flex; + padding: 10px 0 10px 20px; + width: inherit; + +} + +.opacity-wrapper { + display: flex; + align-items: center; + width: 100%; + height: 100%; + margin: -10px 0 -10px -20px; + padding: 10px 0 10px 20px; + background-color: rgba(0, 0, 0, 0.50); +} + +.play-button { + margin-left: 40px; + background-color: #1ed660; + border-radius: 50%; + width: 60px; + height: 60px; + position: relative; + align-items: center; + justify-content: center; + transition: all 0.1s; +} + +.play-button:hover { + cursor: pointer; + transform: scale(1.05); + background-color: #1ed683; +} + + +.other-info { + border-top: 1px solid #363636; + margin-top: 10px; +} + +/*姣忚闊充箰鐨勬牱寮�*/ +.music-item { + position: relative; + border-radius: 10px; + display: flex; + align-items: center; + padding: 10px 0 10px 25px; + flex-grow: 1; + min-width: 0; +} + +/*宸︿晶淇℃伅*/ +.music-detailed-info { + display: flex; + flex-direction: row; +} + +.music-image { + padding-left: 30px; + height: 50px; + width: 50px; /* Adjust as needed */ + border-radius: 10px; +} + +.music-name { + padding-bottom: 5px; + font-size: 18px +} + +.music-name:hover { + cursor: pointer; + text-decoration: underline; +} + +.music-author { + color: #b2b2b2; + font-size: 15px +} + +.music-author:hover { + cursor: pointer; + text-decoration: underline; +} + +/*涓撹緫淇℃伅*/ +.music-album-info { + position: absolute; + left: 60%; + color: #b2b2b2; + text-overflow: ellipsis; +} + +.music-album-info:hover { + cursor: pointer; + text-decoration: underline; +} + +/*鍙充晶淇℃伅*/ +.music-right-info { + margin-left: auto; + display: flex; + align-items: center; + flex-direction: row; +} + + +ul { + background-color: #282828; + list-style-type: none; + padding: 0; + margin: 0; + border-radius: 10px; +} + +li { + color: white; + padding: 15px 12px; +} + +li:hover { + cursor: pointer; + text-decoration: underline; +} + +.other-info { + margin-top: 20px; +} + +.reco-add-button { + color: white; + margin-right: 16px; + box-sizing: border-box; + background-color: transparent; + border-radius: 9999px; + cursor: pointer; + position: relative; + text-align: center; + transition-duration: 33ms; + transition-property: background-color, border-color, color, box-shadow, filter, transform; + user-select: none; + vertical-align: middle; + transform: translate3d(0px, 0px, 0px); + padding-block: 3px; + padding-inline: 15px; + border: 1px solid #818181; + min-inline-size: 0; + min-block-size: 32px; + display: inline-flex; + align-items: center; + + &:hover { + border: 1px solid #f5f5f5; + scale: 1.1; + } +} + +.back-button { + position: relative; + margin: 24px 0 0 24px; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 50%; + color: #fff; + transition: all 0.2s ease; + + &:hover { + transform: scale(1.1); + background-color: rgba(0, 0, 0, .8); + } +} + +.back-button[data-tooltip]:hover::after { + content: attr(data-tooltip); + position: absolute; + top: 38px; + left: 50%; + transform: translateX(-50%); + background-color: #282828; + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + z-index: 1000; + pointer-events: none; +} + +.lyrics-lines { + transition: transform 0.3s; +} + +.lyrics-line { + text-align: center; + font-size: 1.2rem; + padding: 10px 0; + color: #aaa; + transition: color 0.3s; +} +</style> diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue index 39f96c3cc805126c0137f13ba20db8a44a722e85..ab1fb70878242a651d38167b3a7b0dac02526918 100644 --- a/src/views/HomePage.vue +++ b/src/views/HomePage.vue @@ -26,7 +26,8 @@ import {useTheme} from "../store/theme"; import {parseLrc} from "../utils/parseLyrics" import {updateBackground} from "../utils/getBackgroundColor"; import { formatTime } from '../utils/formatTime'; -import {getPlaylistById} from "../api/resolve"; +import {getPlaylistById, getSongById} from "../api/resolve"; +import MusicView from "@/components/MusicView.vue"; /* @@ -537,6 +538,23 @@ function receiveDataFromHome() { setMidComponents(0); } +/* + Music View + */ +const displayingMusic = ref(); +const displayingMusicId = ref(2); + +const receiveDisplayingMusic = (value) => { + setMidComponents(6); + displayingMusicId.value = value; + getSongById( + { + song_id: displayingMusicId.value + }).then((res) => { + displayingMusic.value = res.data.result; + }); +}; + /* MID COMPONENTS 0 - Main View @@ -545,6 +563,7 @@ function receiveDataFromHome() { 3 - Search Results 4 - Episodes 5 - Artist View + 6 - Music View */ const midComponents = ref(0); const currentArtist = ref(null); @@ -573,6 +592,8 @@ const setMidComponents = (val, prop = null, isBack = false) => { if (val === 5) { currentArtist.value = prop; + } else if (val === 6) { + displayingMusicId.value = prop; } }; @@ -696,7 +717,7 @@ const updateSongs = (newSongs) => { style="overflow: scroll; border-radius: 12px"> <MainView @openArtistView="(name) => setMidComponents(5, name)" @openEpisodeView="(name) => setMidComponents(4, name)" - @openMusicView="" + @openMusicView="(songId) => receiveDisplayingMusic(songId)" @openAlbumView="(album) => receiveDisplayingPlaylist(album)" /> </div> @@ -738,6 +759,14 @@ const updateSongs = (newSongs) => { @back="goBack" @updateSongs="updateSongs"/> </div> + <div v-if="midComponents === 6" class="playlist-container" + style="overflow: scroll; border-radius: 12px"> + <MusicView :song-info="displayingMusic" + :current-song-id="currentSongId" + :is-paused="isPaused" + :play-from-left-bar="playFromLeftBarAlbum" + /> + </div> </div> <div v-if="showRightContent" class="right-content"> <div v-if="songs[currentSongIndex] !== undefined" class="music-player music-info">