Code owners
Assign users and groups as approvers for specific file changes. Learn more.
HomePage.vue 37.07 KiB
/* eslint-disable */
<script setup>
// Vue Basics
import {onMounted, ref, watch, computed} from "vue"
import {router} from "../router";
// Assets
import defaultBg from '../assets/pictures/Eason.png'
import {LOOP_MODE, NORMAL_MODE, PAUSE, PLAY, RANDOM_MODE} from "../assets/base64";
// Components
import Header from "../components/Header";
import Comment from "../components/Comment";
import LeftSideBar from "../components/LeftSideBar";
import SearchView from "@/components/SearchView.vue";
import MusicAlbumView from "../components/MusicAlbumView.vue";
// APIs
import {getSongsByPlaylist} from "../api/song";
import {getPlaylistsByUser} from "../api/playlist";
// Others
import {useTheme} from "../store/theme";
import ColorThief from "colorthief";
import {parseLrc, parseLrcOld} from "../utils/parseLyrics"
import {updateBackground} from "../utils/getBackgroundColor";
/*
BACKGROUND
*/
const textColor = ref("#000");
const backgroundColor = ref("#ffffff");
const gradientColor = computed(() => `linear-gradient(to top right, ${backgroundColor.value}, #000000)`)
const isFullScreen = ref(false);
function toggleFullScreen() {
isFullScreen.value = !isFullScreen.value;
if (isFullScreen.value) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
}
/*
LYRICS
*/
const lyrics = ref([]); // 歌词数组
const currentTime = ref(0); // 当前播放时间
const currentLineIndex = ref(0); // 当前歌词行的索引
const isLyricsDisplaying = ref(true);
function toggleLyrics() {
isLyricsDisplaying.value = !isLyricsDisplaying.value;
}
function formatTime(time) {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
function updateCurrentTime(event) {
currentTime.value = event.target.currentTime;
updateCurrentLine();
}
function updateCurrentLine() {
for (let i = 0; i < lyrics.value.length; i++) {
if (
currentTime.value >= lyrics.value[i].time &&
(!lyrics.value[i + 1] || currentTime.value < lyrics.value[i + 1].time)
) {
currentLineIndex.value = i;
break;
}
}
}
setInterval(() => {
// console.log(progresses.length, controlIcons.length, playModeIcons.length);
}, 1000);
const theme = useTheme()
const album_selected = ref(false);
const showRightContent = ref(true)
const selectAlbum = () => {
console.log("selectAlbum");
album_selected.value = true;
}
const unSelectAlbum = () => {
console.log("unSelectAlbum");
album_selected.value = false;
}
/*
ORIGINAL DESIGNS
*/
// main-elements
let song;
let progress;
// buttons
let playPauseButtons;
let forwardButtons;
let backwardButtons;
let playModeButtons;
let shareButtons;
// icons
let controlIcon;
let playModeIcon;
// test items
let progresses;
let controlIcons;
let playModeIcons;
const registerDOMs = () => {
/*
Navs & Containers
*/
const navItems = document.querySelectorAll(".nav-item");
const containers = document.querySelectorAll(".containers");
navItems.forEach((navItem) => {
navItem.addEventListener("click", () => {
navItems.forEach((item) => {
item.className = "nav-item";
});
navItem.className = "nav-item active";
});
});
containers.forEach((container) => {
let isDragging = false;
let startX;
let scrollLeft;
container.addEventListener("mousedown", (e) => {
isDragging = true;
startX = e.pageX - container.offsetLeft;
scrollLeft = container.scrollLeft;
});
container.addEventListener("mousemove", (e) => {
if (!isDragging) {
return;
}
e.preventDefault();
const x = e.pageX - container.offsetLeft;
const step = (x - startX) * 0.6;
container.scrollLeft = scrollLeft - step;
});
container.addEventListener("mouseup", () => {
isDragging = false;
});
container.addEventListener("mouseleave", () => {
isDragging = false;
});
container.addEventListener('wheel', (e) => {
container.scrollLeft += e.deltaY / 2;
});
});
/*
Songs Related
*/
song = document.getElementById("song");
playPauseButtons = document.querySelectorAll(".play-pause-btn");
forwardButtons = document.querySelectorAll(".controls button.forward");
backwardButtons = document.querySelectorAll(".controls button.backward");
playModeButtons = document.querySelectorAll(".play-mode-btn");
shareButtons = document.querySelectorAll(".share-btn");
progresses = document.querySelectorAll('.idProgress');
controlIcons = document.querySelectorAll('.idControlIcon');
playModeIcons = document.querySelectorAll('.idPlayModeIcon');
function updateSongInfo() {
try {
if (songs.value[currentSongIndex.value]) {
controlIcons.forEach(controlIcon => {
controlIcon.src = PLAY;
});
song.src = songs.value[currentSongIndex.value].filePath;
parseLrc(songs.value[currentSongIndex.value].lyricsPath).then(res => {
lyrics.value = res;
});
song.load();
song.play();
theme.change(songs.value[currentSongIndex.value].picPath);
}
} catch (e) {
console.log("Uncaught Error in updateSongInfo!", e);
}
}
function shareSong() {
console.log("Hello!");
}
function playPause() {
isPaused.value = !isPaused.value;
try {
if (song.paused) {
song.play();
controlIcons.forEach(controlIcon => {
controlIcon.src = PLAY;
});
} else {
song.pause();
controlIcons.forEach(controlIcon => {
controlIcon.src = PAUSE;
});
}
} catch (e) {
console.log("Uncaught Error in playPause!", e);
}
}
function switchPlayMode() {
playingMode.value = (playingMode.value + 1) % 3
switch (playingMode.value) {
case 0:
playModeIcons.forEach(playModeIcon => {
playModeIcon.src = NORMAL_MODE;
});
break;
case 1:
playModeIcons.forEach(playModeIcon => {
playModeIcon.src = LOOP_MODE;
});
break;
case 2:
playModeIcons.forEach(playModeIcon => {
playModeIcon.src = RANDOM_MODE;
});
break;
default:
break;
}
}
song.addEventListener("loadedmetadata", function () {
progresses.forEach(progress => {
duration.value = song.duration;
progress.max = song.duration;
progress.value = song.currentTime;
});
});
song.addEventListener("ended", function () {
switchSongs(1);
updateSongInfo();
});
song.addEventListener("timeupdate", function () {
if (!song.paused) {
progresses.forEach(progress => {
progress.value = song.currentTime;
});
}
});
playPauseButtons.forEach(playPauseButton => {
if (!playPauseButton._hasClickListener) {
playPauseButton.addEventListener("click", playPause);
playPauseButton._hasClickListener = true;
}
});
playModeButtons.forEach(playModeButton => {
if (!playModeButton._hasClickListener) {
playModeButton.addEventListener("click", switchPlayMode);
playModeButton._hasClickListener = true;
}
});
shareButtons.forEach(shareButton => {
if (!shareButton._hasClickListener) {
shareButton.addEventListener("click", shareSong);
shareButton._hasClickListener = true;
}
});
forwardButtons.forEach(forwardButton => {
if (!forwardButton._hasClickListener) {
forwardButton.addEventListener("click", function () {
switchSongs(1);
updateSongInfo();
});
forwardButton._hasClickListener = true;
}
});
backwardButtons.forEach(backwardButton => {
if (!backwardButton._hasClickListener) {
backwardButton.addEventListener("click", function () {
switchSongs(-1);
updateSongInfo();
});
backwardButton._hasClickListener = true;
}
});
progresses.forEach(progress => {
progress.addEventListener("input", function () {
if (!song.paused) {
song.currentTime = progress.value;
}
});
progress.addEventListener("change", function () {
try {
if (song.paused) {
song.play();
}
} catch (e) {
console.log("Uncaught Error in change!", e);
}
});
});
// updateSongInfo();
}
/*
USER
*/
const userToken = ref(JSON.parse(sessionStorage.getItem('user-token')));
const currentUserId = ref(userToken.value.id);
/*
SONGS
*/
// Playing Status
const songs = ref([]);
const isPaused = ref(false);
const duration = ref(0);
const playingMode = ref(0); /* 0 - Normal, 1 - Loop, 2 - Random */
// Current Playing
const currentSongId = ref(1);
const currentSongIndex = ref(0);
const isPlayingPage = ref(false);
const togglePlayingPage = () => {
isPlayingPage.value =!isPlayingPage.value;
registerDOMs();
}
const switchSongs = (del) => {
console.log(playingMode.value, currentSongIndex.value, songs.value.length)
switch (playingMode.value) {
case 0:
console.log("normal mode")
currentSongIndex.value = (currentSongIndex.value + del + songs.value.length) % songs.value.length;
break;
case 1:
console.log("loop mode")
currentSongIndex.value = currentSongIndex.value;
break;
case 2:
console.log("random mode")
currentSongIndex.value = Math.floor(Math.random() * songs.value.length);
break;
default:
break;
}
currentSongId.value = songs.value[currentSongIndex.value].id;
}
const switchToSong = (index) => {
if (index === currentSongIndex.value) {
return;
}
currentSongIndex.value = index;
currentSongId.value = songs.value[index].id;
if (song) {
controlIcons.forEach(controlIcon => {
controlIcon.src = PLAY;
});
song.src = songs.value[index].filePath;
parseLrc(songs.value[index].lyricsPath).then(res => {
lyrics.value = res;
});
song.load();
song.play();
theme.change(songs.value[index].picPath);
}
}
/*
PLAYLISTS
*/
const playlists = ref([]);
const currentPlaylist = ref(2);
const currentPlaylistId = ref(2);
const receivePlaylistId = (value) => {
console.log(value)
currentPlaylist.value = value;
currentPlaylistId.value = value.id;
console.log("Current Playlist Id:", currentPlaylistId.value)
getSongsByPlaylist({
playlist_id: currentPlaylistId.value,
}).then((res) => {
songs.value = res.data.result;
console.log("Songs:", res.data.result);
}).catch(e => {
console.log("Failed to get songs!");
});
};
/*
SEARCH
*/
const songResult = ref();
const playlistResult = ref();
function receiveDataFromHeader(data) {
songResult.value = data.songResult;
playlistResult.value = data.playlistResult;
setMidComponents(3);
}
/*
MID COMPONENTS
1 - Music Albums
2 - Comments
3 - Search Results
*/
const midComponents = ref(1);
const setMidComponents = (val) => {
midComponents.value = val;
}
onMounted(() => {
/*
DOMS & EVENTS
*/
theme.change(defaultBg);
registerDOMs();
/*
API
*/
getPlaylistsByUser({
user_id: currentUserId.value,
}).then((res) => {
playlists.value = res.data.result;
currentPlaylist.value = playlists.value[0];
currentPlaylistId.value = currentPlaylist.value.id;
theme.change(currentPlaylist.value.picPath);
getSongsByPlaylist({
playlist_id: currentPlaylistId.value,
}).then((res) => {
songs.value = res.data.result;
currentSongId.value = songs.value[0].id;
// TODO: currentSongIndex != currentSongId ?
parseLrc(songs.value[currentSongIndex.value].lyricsPath).then(res => {
lyrics.value = res;
});
}).catch(e => {
console.log("Failed to get songs!");
});
}).catch(e => {
console.log("Failed to get playlists!");
});
})
</script>
<template>
<div class="body" v-show="!isPlayingPage" @click="unSelectAlbum">
<!-- MAIN & RIGHT CONTENT -->
<Header class="header" @headData="receiveDataFromHeader"/>
<left-side-bar class="left-side-bar" @setCurrentPlaylist="receivePlaylistId"/>
<div class="content" :class="{ 'full-width': !showRightContent }">
<div class="main-view" :class="{ 'expanded': !showRightContent }">
<el-container v-if="midComponents == 1" class="playlist-container" style="overflow: auto; height: 698px ;border-radius: 12px">
<MusicAlbumView :album-info="currentPlaylist" :music-list="songs"/>
</el-container>
<el-container v-if="midComponents == 2" class="playlist-container" style="overflow: auto; height: 668px">
<el-button class="exit-search"
:class="{ 'adjusted-position': showRightContent }"
@click="setMidComponents(1)"></el-button>
<Comment :song-id=currentSongId :user-id=currentUserId></Comment>
</el-container>
<el-container v-if="midComponents == 3" class="playlist-container" style="overflow: auto; height: 698px">
<el-button class="exit-search"
:class="{ 'adjusted-position': showRightContent }"
@click="setMidComponents(1)"></el-button>
<SearchView :songResult="songResult" :playlistResult="playlistResult"/>
</el-container>
</div>
<div v-if="showRightContent" class="right-content">
<div v-if="songs[currentSongIndex] !== undefined" class="music-player music-info">
<div class="album-cover" @click="togglePlayingPage">
<img :src="songs[currentSongIndex].picPath" id="rotatingImage" alt=""/>
<span class="point"></span>
</div>
<h2>{{songs[currentSongIndex].title}}</h2>
<p>{{songs[currentSongIndex].artist}}</p>
</div>
<div class="current-playlist" style="margin-top: 20px">
<el-container class="playlist-container" style="height: 64px">
<div class="playlist-item" style="display: flex; flex-direction: row">
<img src="../assets/icons/add.png" alt="" style=""/>
<div style="display: flex; flex-direction: column; align-items: center; margin-left: 10px">
<p class="playlist-container-desc" style="
color: white;
font-size: 16px;
text-align: left;
margin-top: 16px;
">New Song</p>
</div>
</div>
</el-container>
<el-container class="playlist-container" style="overflow: auto; height: 320px">
<div v-for="(song, index) in songs" class="playlist-item" style="display: flex; flex-direction: row">
<div @click="switchToSong(index)" style="cursor: pointer">
<img :src="song.picPath" alt=""
:class="{ 'playing': index === currentSongIndex }"
/>
</div>
<div style="display: flex; flex-direction: column; margin-left: 10px">
<p class="playlist-container-desc" style="
color: white;
font-size: 18px;
font-family: Candara, serif;
text-align: left;
overflow: auto;
width: 240px;
height: 24px;
">{{ songs[index].title }}</p>
<p class="playlist-container-desc" style="
color: #949494;
font-size: 12px;
text-align: left;
overflow: auto;
width: 240px;
height: 18px
">{{ songs[index].artist }}</p>
</div>
</div>
</el-container>
</div>
</div>
</div>
<!-- FOOTER -->
<footer>
<div class="bottom-description bottom-component"
style="display: flex; flex-direction: row; justify-content: center;">
<div @click="togglePlayingPage">
<img v-if="songs[currentSongIndex] !== undefined"
:src="songs[currentSongIndex].picPath" alt=""
style="
width: 60px;
margin: 0 0 0 10px;
border-radius: 5%;
border: 2px solid rgba(222, 215, 255, 0.9);
max-width: 120px;
box-shadow: 0 10px 60px rgba(200, 187, 255);
"/>
<audio id="song" @timeupdate="updateCurrentTime">
<source
v-if="songs[currentSongIndex] !== undefined"
:src="songs[currentSongIndex].filePath"
type="audio/mpeg"/>
</audio>
</div>
<div v-if="songs[currentSongIndex] !== undefined"
style="display: flex; flex-direction: column; justify-content: center;">
<p style="font-family: Consolas, serif; color: white; font-size: 16px; text-align: left; margin-left: 5px">
{{songs[currentSongIndex].title}}</p>
<p style="font-family: Consolas, serif; color: white; font-size: 16px; text-align: left; margin-left: 5px">
{{songs[currentSongIndex].artist}}</p>
</div>
</div>
<el-card class="bottom-controller bottom-component" style="
position: absolute;
left: 50%;
transform: translateX(-50%);
">
<div class="controls" style="display: flex; flex-direction: row; margin: 10px 0 0 0">
<button class="share-btn" style="margin: 0">
<img src="../assets/icons/controller/mute.png" alt="" style="width: 60%">
</button>
<button class="backward" style="margin: 0 10px 0 10px">
<img src="../assets/icons/controller/last.png" alt="" style="width: 60%">
</button>
<button class="play-pause-btn" style="margin: 0 10px 0 10px">
<img id="controlIcon" class="idControlIcon" src="../assets/icons/controller/play.png" alt="" style="width: 60%">
</button>
<button class="forward" style="margin: 0 10px 0 10px">
<img src="../assets/icons/controller/next.png" alt="" style="width: 60%">
</button>
<button class="play-mode-btn" style="margin: 0">
<img id="playModeIcon" class="idPlayModeIcon" src="../assets/icons/controller/normal.png" alt="" style="width: 60%">
</button>
</div>
<div style="display: flex; flex-direction: row; margin-top: 10px">
<p style="margin-right: 10px; margin-bottom: 10px; color: white">{{formatTime(currentTime)}}</p>
<input type="range" value="0" id="progress" class="idProgress" style="margin: 0 0 10px 0; width: 500px"/>
<p style="margin-left: 10px; color: white">{{formatTime(duration)}}</p>
</div>
</el-card>
<div class="share-icon bottom-component" style="
position: absolute;
left: 92%;
transform: translateX(-50%);
color: white;
cursor: pointer;
">
<img src="../assets/icons/comment/share.png" alt="" style="width: 24px; height: 24px;"
@click="">
</div>
<div class="comment-icon bottom-component" style="
position: absolute;
left: 95%;
transform: translateX(-50%);
color: white;
cursor: pointer;
">
<img src="../assets/icons/comment/comment.png" alt="" style="width: 24px; height: 24px;"
@click="setMidComponents(2)">
</div>
<div class="queue-icon bottom-component" style="
position: absolute;
left: 98%;
transform: translateX(-50%);
color: white;
cursor: pointer;
">
<img src="../assets/icons/queue.png" alt="" style="width: 24px; height: 24px;"
@click="showRightContent = !showRightContent">
</div>
</footer>
</div>
<!-- PLAYING PAGE -->
<div v-show="isPlayingPage" class="playing-page">
<div v-if="isLyricsDisplaying" class="lyrics-container">
<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>
</div>
</div>
<!-- <div class="player" :style="{ backgroundImage: gradientColor }">-->
<div class="player">
<div class="background"></div>
<div class="player-content">
<div v-if="songs[currentSongIndex] !== undefined" class="album-cover-container">
<img :src="songs[currentSongIndex].picPath" alt="Album Cover" class="album-cover" @load="updateBackground" />
</div>
<div class="track-info-container">
<div v-if="songs[currentSongIndex] !== undefined" class="music-info" style="display: flex; flex-direction: column; justify-content: center;">
<p style="
font-family: Consolas, serif;
color: white;
font-size: 32px;
text-align: left;
margin: 0">{{songs[currentSongIndex].title}}</p>
<span style="
font-family: Consolas, serif;
color: white;
font-size: 16px;
text-align: left;
margin: 0">{{songs[currentSongIndex].artist}}</span>
</div>
<div class="bottom-controller bottom-component" style="
position: absolute;
left: 50%;
bottom: 2%;
transform: translateX(-50%);
">
<div class="controls" style="display: flex; flex-direction: row; margin: 10px 0 0 0">
<button class="share-btn" style="margin: 0">
<img src="../assets/icons/controller/share.png" alt="" style="width: 60%">
</button>
<button class="backward" style="margin: 0 10px 0 10px">
<img src="../assets/icons/controller/last.png" alt="" style="width: 60%">
</button>
<button class="play-pause-btn" style="margin: 0 10px 0 10px">
<img id="controlIcon" class="idControlIcon" src="../assets/icons/controller/play.png" alt="" style="width: 60%">
</button>
<button class="forward" style="margin: 0 10px 0 10px">
<img src="../assets/icons/controller/next.png" alt="" style="width: 60%">
</button>
<button class="play-mode-btn" style="margin: 0">
<img id="playModeIcon" class="idPlayModeIcon" src="../assets/icons/controller/normal.png" alt="" style="width: 60%">
</button>
</div>
<div v-if="songs[currentSongIndex] !== undefined" style="display: flex; flex-direction: row;">
<p style="margin-right: 10px">{{formatTime(currentTime)}}</p>
<input type="range" value="0" id="progress" class="idProgress" style="margin: 20px 0 10px 0; width: 700px"/>
<p style="margin-left: 10px">{{formatTime(duration)}}</p>
</div>
</div>
</div>
</div>
<div class="corner-buttons">
<button @click="toggleLyrics" class="corner-button">
<span v-if="isLyricsDisplaying" style="text-decoration: underline">A</span>
<span v-else>A</span>
</button>
<button @click="toggleFullScreen" class="corner-button">
<span v-if="isFullScreen">↖</span>
<span v-else>⛶</span>
</button>
<button @click="togglePlayingPage" class="corner-button">
<span>◀</span>
</button>
</div>
</div>
</div>
</template>
<style scoped>
*,
*::before,
*::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
nav {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
}
nav ul,
nav ul li {
outline: 0;
}
nav ul li a {
text-decoration: none;
}
img {
width: 100%;
}
h1 {
font-size: clamp(1.2rem, 3vw, 1.5rem);
}
.body {
font-family: "Nunito", sans-serif;
height: 100%;
/*没用的样式*/
/*
align-items: center;
justify-content: space-between;
flex-direction: column;
*/
min-height: 100vh;
background-color: rgb(19, 19, 19); /* rgba(0, 0, 0, 1); */
background-repeat: no-repeat;
background-size: cover;
/* 原先main中的内容
height: 700px;
width: 95%;
margin: 20px 0 0 0;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 15px;
box-shadow: 0 0.5px 0 1px rgba(255, 255, 255, 0.23) inset,
0 1px 0 0 rgba(255, 255, 255, 0.6) inset, 0 4px 16px rgba(0, 0, 0, 0.12);
z-index: 10;
*/
display: grid;
grid-template-areas:
"global-nav global-nav global-nav"
"left-sidebar main-view main-view"
"now-playing-bar now-playing-bar now-playing-bar";
grid-template-columns: auto 1fr ;
grid-template-rows: auto 1fr auto;
row-gap: 8px;
column-gap: 8px;
padding:8px;
}
/* HEADER */
.header{
grid-area: global-nav;
z-index: 1000;
}
/* TEMP */
left-side-bar{
grid-area: left-sideBar;
}
.content{
grid-area: main-view;
}
footer{
grid-area: now-playing-bar;
}
/* MAIN MENU */
/*
main {
height: 700px;
width: 95%;
margin: 20px 0 0 0;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 15px;
box-shadow: 0 0.5px 0 1px rgba(255, 255, 255, 0.23) inset,
0 1px 0 0 rgba(255, 255, 255, 0.6) inset, 0 4px 16px rgba(0, 0, 0, 0.12);
z-index: 10;
}
*/
footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 75px;
width: 100%;
margin: 20px 0 0 0;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.transparent-btn {
background-color: transparent !important;
border-color: transparent !important;
color: #333;
}
.main-menu {
display: flex;
flex-direction: column;
justify-content: space-between;
border-radius: 15px 0 0 15px;
border-right: 1px solid rgba(255, 255, 255, 0.5);
padding: 12px 0 20px;
overflow: hidden;
font-family: inherit;
}
.user-info img {
padding: 12px 24px 6px;
border-radius: 50%;
}
.user-info p {
color: #fff;
font-size: clamp(0.8rem, 3vw, 1rem);
font-weight: 500;
text-align: center;
line-height: 1;
padding: 0 6px 32px;
}
.nav-item {
display: block;
}
.nav-item a {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1rem;
padding: 12px 0;
margin: 0 8px;
border-radius: 5px;
}
.nav-item.active a {
background: rgba(106, 109, 155, 0.5);
text-decoration: none;
}
.nav-icon {
width: 40px;
height: 20px;
font-size: 1.1rem;
}
/* CONTENT 包含中间和右边栏 是grid布局*/
.content {
display: grid;
grid-template-columns: 1fr auto;
transition: all 0.3s ease;
column-gap: 8px;
}
.content.full-width {
grid-template-columns: 100% !important;
}
/* LEFT CONTENT */
.main-view > {
display: flex;
flex-direction: column;
justify-content: center;
color: #e5e5e5;
transition: all 0.3s ease;
margin: 0;
padding: 0;
}
.main-view.expanded {
margin: 0;
padding: 0;
width: 100%;
}
.swiper-slide img {
border-radius: 20px;
height: 300px;
object-fit: cover;
border: 2px solid rgba(159, 160, 168, 0.5);
}
/* Containers of Artist and Albums */
.containers {
display: flex;
align-items: center;
padding: 0 0 12px;
overflow-x: auto;
cursor: grab;
}
/* ALBUMS */
.albums {
animation: fadeIn 0.5s ease-in-out;
}
.playlist-item img {
inset: 0;
width: 60px;
object-fit: cover;
transition: transform 0.8s;
pointer-events: none;
aspect-ratio: 1/1;
border: 2px solid rgba(169, 150, 253, 0.5);
border-radius: 10px;
box-shadow: rgba(221, 221, 221, 0.3) 0px 8px 18px -3px,
rgba(221, 221, 221, 0.2) 0px -3px 0px inset;
}
.playlist-item:hover img {
transform: rotate(3deg) scale(1.14514);
}
.album-container {
column-gap: 24px;
}
.album {
display: grid;
grid-auto-flow: dense;
grid-template-rows: 5fr 2fr;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
height: 240px;
}
.album-frame {
position: relative;
width: 144px;
aspect-ratio: 1/1;
border: 2px solid rgba(169, 150, 253, 0.5);
border-radius: 10px;
box-shadow: rgba(221, 221, 221, 0.3) 0px 8px 18px -3px,
rgba(221, 221, 221, 0.2) 0px -3px 0px inset;
overflow: hidden;
}
.album-frame img {
position: absolute;
inset: 0;
height: 100%;
object-fit: cover;
transition: transform 0.8s;
pointer-events: none;
}
.album-frame:hover img {
transform: rotate(3deg) scale(1.2);
}
.album h2 {
font-size: clamp(0.9rem, 4vw, 1.1rem);
font-weight: 500;
line-height: 1.3;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
max-width: 150px;
@supports (-webkit-line-clamp: 2) {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.album p {
font-size: clamp(0.9rem, 4vw, 1rem);
opacity: 0.5;
}
/* Containers Scrollbar Style */
.playlist-container::-webkit-scrollbar {
height: 10px;
display: none;
}
.playlist-container-desc::-webkit-scrollbar {
height: 10px;
display: none;
}
.album-container::-webkit-scrollbar {
height: 10px;
display: none;
}
.album-container::-webkit-scrollbar-track {
box-shadow: inset 0 0 0.3rem rgb(79, 78, 78);
border-radius: 40px;
}
.album-container::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 0.5rem rgb(116, 116, 116);
background-color: rgba(25, 43, 206, 0.2);
outline: none;
border-radius: 40px;
}
/* RIGHT CONTENT */
.right-content {
background-color: #171717;
display: flex;
flex-direction: column;
border-radius: 12px;
padding: 30px 20px;
color: #e5e5e5;
}
/* SONGS */
.song-img img {
aspect-ratio: 4/3;
border-radius: inherit;
object-fit: cover;
border: 2px solid rgba(159, 160, 168, 0.5);
box-shadow: rgba(221, 221, 221, 0.3) 0px 6px 18px -3px,
rgba(221, 221, 221, 0.2) 0px -3px 0px inset;
}
.song-img .overlay {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
inset: 0;
width: 100%;
height: 92%;
background-color: rgba(169, 150, 253, 0.6);
border-radius: inherit;
font-size: 1.75rem;
opacity: 0;
transition: all 0.4s linear;
cursor: pointer;
}
.song-img:hover .overlay {
opacity: 1;
}
.song h2 {
font-size: 1rem;
}
.song p,
.song span {
font-size: 0.8rem;
font-weight: 300;
}
.song p {
opacity: 0.8;
}
/* MUSIC PLAYER */
.music-player {
display: flex;
flex-direction: column;
color: #fff;
background: rgba(188, 184, 198, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: inset 2px -2px 6px rgba(214, 214, 214, 0.2),
inset -3px 3px 3px rgba(255, 255, 255, 0.3);
border-radius: 16px;
height: 200px;
}
.album-cover {
position: relative;
}
.album-cover img {
border-radius: 50%;
border: 2px solid rgba(222, 215, 255, 0.9);
max-width: 120px;
aspect-ratio: 1/1;
object-fit: cover;
box-shadow: 0 10px 60px rgba(200, 187, 255);
transition: transform 0.5s ease-out;
pointer-events: none;
user-select: none;
}
.point {
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
width: 16px;
background-color: rgba(17, 6, 58, 0.7);
border: 2px solid rgba(222, 215, 255, 0.9);
aspect-ratio: 1/1;
border-radius: 50%;
z-index: 2;
}
.music-player h2 {
font-size: 1.2rem;
font-weight: 500;
margin: 8px 0 0 0;
}
.music-player p {
font-size: 1rem;
font-weight: 300;
margin-bottom: 26px;
opacity: 0.8;
}
/* Music Player Controls */
#progress {
appearance: none;
-webkit-appearance: none;
width: 100%;
height: 6px;
background: rgba(200, 187, 255, 0.6);
border-radius: 4px;
margin-bottom: 16px;
cursor: pointer;
}
#progress::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
background: rgb(77, 58, 162);
width: 20px;
aspect-ratio: 1/1;
border-radius: 50%;
border: 4px solid rgb(234, 229, 255);
box-shadow: 0 6px 10px rgba(200, 187, 255, 0.4);
}
.controls {
display: flex;
justify-content: center;
align-items: center;
}
.controls button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
aspect-ratio: 1/1;
margin: 20px;
background: rgba(255, 255, 255, 0.6);
border-radius: 50%;
border: 0;
outline: 0;
color: #fff;
font-size: 1.1rem;
box-shadow: 0 4px 8px rgba(200, 187, 255, 0.3);
cursor: pointer;
transition: all 0.3s linear;
}
.controls button:is(:hover, :focus-visible) {
transform: scale(0.96);
}
.controls button:nth-child(3) {
transform: scale(1.3);
}
.controls button:nth-child(3):is(:hover, :focus-visible) {
transform: scale(1.25);
}
.controls button:nth-child(1) {
transform: scale(0.8);
}
.controls button:nth-child(5) {
transform: scale(0.8);
}
/* MEDIA QUERIES */
@media (max-width: 1500px) {
main {
grid-template-columns: 15% 85%;
}
.user-info img {
border-radius: 50%;
padding: 12px 12px 6px;
}
.nav-icon {
text-align: center;
transform: translateY(2px);
}
.content {
grid-template-columns: 70% 30%;
}
}
@media (max-width: 1310px) {
main {
grid-template-columns: 8% 92%;
margin: 30px;
}
}
@media (max-width: 1250px) {
.swiper-slide {
width: 500px;
}
.swiper-slide img {
border-radius: 16px;
height: 280px;
}
.album-frame {
width: 160px;
}
.song {
grid-template-columns: 29% auto 10%;
}
.controls button {
margin: 15px;
}
}
@media (max-width: 1100px) {
.content:not(.full-width) {
grid-template-columns: 60% 40%;
}
.main-view {
}
.swiper-slide {
width: 410px;
}
.swiper-slide img {
height: 220px;
}
.album {
grid-template-rows: 4fr 2fr;
}
.album-frame {
width: 130px;
}
.song {
grid-template-columns: 26% auto 10%;
}
.song:nth-child(8),
.song:nth-child(9) {
display: none;
}
}
@media (max-width: 910px) {
main {
grid-template-columns: 10% 90%;
margin: 20px;
}
.main-view {
padding: 30px 20px 20px;
}
.swiper-slide img {
height: 180px;
}
.album {
grid-template-rows: 3fr 2fr;
}
.album-frame {
width: 100px;
}
.right-content {
grid-template-rows: 55% 45%;
}
}
@media (max-width: 825px) {
.content:not(.full-width) {
grid-template-columns: 52% 48%;
}
.swiper-slide {
width: 280px;
}
.swiper-slide img {
height: 200px;
}
.slide-overlay button {
padding: 8px 12px;
}
}
@media (max-width: 750px) {
main {
grid-template-columns: 15% 85%;
}
.content:not(.full-width) {
grid-template-columns: 100%;
grid-template-areas:
"leftContent"
"rightContent";
}
.main-view {
grid-area: leftContent;
}
.album {
grid-template-rows: 3fr 2fr;
}
.album-frame {
width: 140px;
}
.right-content {
grid-area: rightContent;
border-left: unset;
grid-template-rows: 60% 40%;
row-gap: 16px;
}
#progress {
width: 60%;
}
.controls button {
margin: 20px;
}
}
@media (max-width: 580px) {
.swiper-slide {
width: 290px;
}
.swiper-slide img {
height: 180px;
}
.artist img {
width: 80px;
}
.album {
grid-template-rows: 3fr 2fr;
}
.album-frame {
width: 100px;
}
}
@media (max-width: 450px) {
.user-info img {
border-radius: 50%;
padding: 6px 6px 2px;
}
}
/* 动画:专辑列表移到顶部 */
.move-up {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
justify-content: center;
gap: 15px;
}
/* 专辑简介 */
.album-details {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
animation: fadeIn 0.5s ease-in-out;
}
.album-details img {
width: 150px;
height: 150px;
border-radius: 10px;
}
.details-text {
text-align: center;
}
.details-text h2 {
margin: 10px 0;
font-size: 24px;
}
.details-text p {
font-size: 16px;
color: #bbb;
}
/* 动画:淡入效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.share-icon,
.queue-icon,
.comment-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.share-icon:hover,
.queue-icon:hover,
.comment-icon:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateX(-50%) scale(1.1);
}
/* 退出搜索图标 */
.exit-search {
position: absolute;
top: 90px;
right: 10px;
width: 30px;
height: 30px;
background-color: transparent;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 999;
transition: background-color 0.3s ease;
border: 2px solid #fff;
}
.exit-search:hover {
background-color: rgba(255, 0, 0, 0.8);
}
.exit-search::before {
content: "\2716";
font-size: 20px;
color: #fff;
}
.exit-search:hover::after {
content: "Exit";
position: absolute;
top: 35px;
right: 0;
background-color: #fff;
color: #000;
padding: 5px 10px;
border-radius: 5px;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s ease;
}
.exit-search:hover::after {
opacity: 1;
}
.exit-search.adjusted-position {
right: calc(23%);
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.player {
position: relative;
width: 100%;
height: 100vh;
color: white;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
}
.background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: cover;
background-position: center;
z-index: -1;
}
.player-content {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
padding: 20px;
}
.album-cover-container {
position: relative;
width: 240px;
height: 240px;
margin: 0 0 10px 10px;
z-index: 1;
}
.album-cover {
width: 240px;
height: 240px;
border-radius: 10px;
object-fit: cover;
}
.track-info-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
margin-left: 20px; /* 专辑封面与歌曲信息之间的间距 */
max-width: 60%; /* 限制内容的最大宽度 */
width: 100%;
}
.seek-bar input {
width: 100%; /* 让进度条宽度填满可用空间 */
margin-top: 10px;
}
.time-info {
margin-top: 10px;
font-size: 1rem;
}
.corner-buttons {
position: absolute;
bottom: 20px;
right: 20px;
}
.corner-button {
background: none;
border: none;
color: white;
font-size: 2.4rem;
cursor: pointer;
margin-right: 15px;
}
.lyrics-container {
z-index: 10;
overflow: hidden;
height: 440px;
position: absolute;
top: 40%; /* 距离顶部50% */
left: 50%; /* 距离左边50% */
transform: translate(-50%, -50%); /* 偏移自身宽高的50%来实现完全居中 */
}
.lyrics-lines {
transition: transform 0.3s;
}
.lyrics-line {
text-align: center;
font-size: 1.2rem;
padding: 10px 0;
color: #aaa;
transition: color 0.3s;
}
.lyrics-line.active {
color: #30e1f1;
font-weight: bold;
}
.playlist-item img {
transition: all 0.3s ease;
}
.playlist-item img.playing {
border-color: #ddc323;
box-shadow: 0 0 10px rgba(221, 195, 35, 0.5);
transform: scale(1.05);
}
.playlist-item:hover img:not(.playing) {
transform: scale(1.05);
border-color: rgba(255, 255, 255, 0.8);
}
</style>