mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-17 05:38:04 +00:00
first commit
This commit is contained in:
commit
e4ba16b9a2
102 changed files with 19066 additions and 0 deletions
33
src/components/ArtistsInLine.vue
Normal file
33
src/components/ArtistsInLine.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<span class="artist-in-line">
|
||||
<span v-for="(ar, index) in slicedArtists" :key="ar.id">
|
||||
<router-link :to="`/artist/${ar.id}`">{{ ar.name }}</router-link>
|
||||
<span v-if="index !== slicedArtists.length - 1">, </span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ArtistInLine",
|
||||
props: {
|
||||
artists: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
showFirstArtist: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
slicedArtists() {
|
||||
return this.showFirstArtist
|
||||
? this.artists
|
||||
: this.artists.slice(1, this.artists.length);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
345
src/components/BottomBar.vue
Normal file
345
src/components/BottomBar.vue
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
<template>
|
||||
<div class="player">
|
||||
<div class="progress-bar">
|
||||
<vue-slider
|
||||
v-model="progress"
|
||||
:min="0"
|
||||
:max="progressMax"
|
||||
:interval="1"
|
||||
:drag-on-click="true"
|
||||
:duration="0"
|
||||
:dotSize="12"
|
||||
:height="2"
|
||||
:tooltipFormatter="formatTrackTime"
|
||||
@drag-end="setSeek"
|
||||
ref="progress"
|
||||
></vue-slider>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="playing">
|
||||
<router-link :to="`/album/${player.currentTrack.album.id}`"
|
||||
><img :src="player.currentTrack.album.picUrl | resizeImage" />
|
||||
</router-link>
|
||||
<div class="track-info">
|
||||
<div class="name">
|
||||
<router-link
|
||||
:to="'/' + player.listInfo.type + '/' + player.listInfo.id"
|
||||
>{{ player.currentTrack.name }}</router-link
|
||||
>
|
||||
</div>
|
||||
<div class="artist">
|
||||
<span
|
||||
v-for="(ar, index) in player.currentTrack.artists"
|
||||
:key="ar.id"
|
||||
>
|
||||
<router-link :to="`/artist/${ar.id}`">{{ ar.name }}</router-link>
|
||||
<span v-if="index !== player.currentTrack.artists.length - 1"
|
||||
>,
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="middle-control-buttons">
|
||||
<button-icon @click.native="previous" title="Previous Song"
|
||||
><svg-icon icon-class="previous"
|
||||
/></button-icon>
|
||||
<button-icon
|
||||
class="play"
|
||||
@click.native="play"
|
||||
:title="playing ? 'Pause' : 'Play'"
|
||||
>
|
||||
<svg-icon :iconClass="playing ? 'pause' : 'play'"
|
||||
/></button-icon>
|
||||
<button-icon @click.native="next" title="Next Song"
|
||||
><svg-icon icon-class="next"
|
||||
/></button-icon>
|
||||
</div>
|
||||
<div class="right-control-buttons">
|
||||
<button-icon
|
||||
@click.native="goToNextTracksPage"
|
||||
title="Next Up"
|
||||
:class="{ active: this.$route.name === 'next' }"
|
||||
><svg-icon icon-class="list"
|
||||
/></button-icon>
|
||||
<button-icon
|
||||
title="Repeat"
|
||||
@click.native="repeat"
|
||||
:class="{ active: player.repeat !== 'off' }"
|
||||
>
|
||||
<svg-icon icon-class="repeat" v-show="player.repeat !== 'one'" />
|
||||
<svg-icon icon-class="repeat-1" v-show="player.repeat === 'one'" />
|
||||
</button-icon>
|
||||
<button-icon
|
||||
@click.native="shuffle"
|
||||
:class="{ active: player.shuffle }"
|
||||
title="Shuffle"
|
||||
><svg-icon icon-class="shuffle"
|
||||
/></button-icon>
|
||||
<div class="volume-control">
|
||||
<button-icon title="Mute" @click.native="mute">
|
||||
<svg-icon icon-class="volume" v-show="volume > 0.5" />
|
||||
<svg-icon icon-class="volume-mute" v-show="volume === 0" />
|
||||
<svg-icon
|
||||
icon-class="volume-half"
|
||||
v-show="volume <= 0.5 && volume !== 0"
|
||||
/>
|
||||
</button-icon>
|
||||
<div class="volume-bar">
|
||||
<vue-slider
|
||||
v-model="volume"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:interval="0.01"
|
||||
:drag-on-click="true"
|
||||
:duration="0"
|
||||
:tooltip="`none`"
|
||||
:dotSize="12"
|
||||
></vue-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations, mapActions } from "vuex";
|
||||
import "@/assets/css/slider.css";
|
||||
|
||||
import ButtonIcon from "@/components/ButtonIcon.vue";
|
||||
import VueSlider from "vue-slider-component";
|
||||
|
||||
export default {
|
||||
name: "Player",
|
||||
components: {
|
||||
ButtonIcon,
|
||||
VueSlider,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
interval: null,
|
||||
progress: 0,
|
||||
oldVolume: 0.5,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
setInterval(() => {
|
||||
this.progress = ~~this.howler.seek();
|
||||
}, 1000);
|
||||
},
|
||||
computed: {
|
||||
...mapState(["player", "howler", "Howler"]),
|
||||
volume: {
|
||||
get() {
|
||||
return this.player.volume;
|
||||
},
|
||||
set(value) {
|
||||
this.updatePlayerState({ key: "volume", value });
|
||||
this.Howler.volume(value);
|
||||
},
|
||||
},
|
||||
playing() {
|
||||
if (this.howler.state() === "loading") {
|
||||
return true;
|
||||
}
|
||||
return this.howler.playing();
|
||||
},
|
||||
progressMax() {
|
||||
let max = ~~(this.player.currentTrack.time / 1000);
|
||||
return max > 1 ? max - 1 : max;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
"updatePlayingStatus",
|
||||
"updateShuffleStatus",
|
||||
"updatePlayerList",
|
||||
"shuffleTheList",
|
||||
"updatePlayerState",
|
||||
"updateRepeatStatus",
|
||||
]),
|
||||
...mapActions(["nextTrack", "previousTrack", "playTrackOnListByID"]),
|
||||
play() {
|
||||
if (this.playing) {
|
||||
this.howler.pause();
|
||||
} else {
|
||||
if (this.howler.state() === "unloaded") {
|
||||
this.playTrackOnListByID(this.player.currentTrack.id);
|
||||
}
|
||||
this.howler.play();
|
||||
}
|
||||
},
|
||||
next() {
|
||||
this.nextTrack(true);
|
||||
this.progress = 0;
|
||||
},
|
||||
previous() {
|
||||
this.previousTrack();
|
||||
this.progress = 0;
|
||||
},
|
||||
shuffle() {
|
||||
if (this.player.shuffle === true) {
|
||||
this.updateShuffleStatus(false);
|
||||
this.updatePlayerList(this.player.notShuffledList);
|
||||
} else {
|
||||
this.updateShuffleStatus(true);
|
||||
this.shuffleTheList();
|
||||
}
|
||||
},
|
||||
repeat() {
|
||||
if (this.player.repeat === "on") {
|
||||
this.updateRepeatStatus("one");
|
||||
} else if (this.player.repeat === "one") {
|
||||
this.updateRepeatStatus("off");
|
||||
} else {
|
||||
this.updateRepeatStatus("on");
|
||||
}
|
||||
},
|
||||
mute() {
|
||||
if (this.volume === 0) {
|
||||
this.volume = this.oldVolume;
|
||||
} else {
|
||||
this.oldVolume = this.volume;
|
||||
this.volume = 0;
|
||||
}
|
||||
},
|
||||
setSeek() {
|
||||
this.progress = this.$refs.progress.getValue();
|
||||
this.howler.seek(this.$refs.progress.getValue());
|
||||
},
|
||||
goToNextTracksPage() {
|
||||
this.$route.name === "next"
|
||||
? this.$router.go(-1)
|
||||
: this.$router.push({ name: "next" });
|
||||
},
|
||||
formatTrackTime(value) {
|
||||
if (!value) return "";
|
||||
let min = ~~((value / 60) % 60);
|
||||
let sec = (~~(value % 60)).toString().padStart(2, "0");
|
||||
return `${min}:${sec}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.player {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
height: 64px;
|
||||
backdrop-filter: saturate(180%) blur(30px);
|
||||
background-color: rgba(255, 255, 255, 0.86);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
margin-top: -6px;
|
||||
margin-bottom: -4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex;
|
||||
align-items: center;
|
||||
padding: {
|
||||
right: 10vw;
|
||||
left: 10vw;
|
||||
}
|
||||
}
|
||||
|
||||
.playing {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
img {
|
||||
height: 46px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 6px 8px -2px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
.track-info {
|
||||
height: 46px;
|
||||
margin-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.artist {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.58);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
a {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.middle-control-buttons {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.button-icon {
|
||||
margin: 0 8px;
|
||||
}
|
||||
.play {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
.svg-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right-control-buttons {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
.expand {
|
||||
margin-left: 24px;
|
||||
.svg-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
.active .svg-icon {
|
||||
color: #335eea;
|
||||
}
|
||||
.volume-control {
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.volume-bar {
|
||||
width: 84px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
src/components/ButtonIcon.vue
Normal file
44
src/components/ButtonIcon.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<button class="button-icon"><slot></slot></button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ButtonIcon",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: transparent;
|
||||
margin: 4px;
|
||||
border-radius: 25%;
|
||||
transition: 0.2s;
|
||||
.svg-icon {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
&:hover {
|
||||
// background: #eaeffd;
|
||||
// .svg-icon {
|
||||
// color: #335eea;
|
||||
// }
|
||||
background: #f5f5f7;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.92);
|
||||
// background: #eaeffd;
|
||||
// .svg-icon {
|
||||
// color: #335eea;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
62
src/components/ButtonTwoTone.vue
Normal file
62
src/components/ButtonTwoTone.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<button :style="{ padding: `8px ${horizontalPadding}px` }" :class="color">
|
||||
<svg-icon
|
||||
v-if="iconClass !== null"
|
||||
:iconClass="iconClass"
|
||||
:style="{ marginRight: iconButton ? '0px' : '8px' }"
|
||||
/>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ButtonTwoTone",
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
iconButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
horizontalPadding: {
|
||||
type: Number,
|
||||
default: 16,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "blue",
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
background-color: rgba(51, 94, 234, 0.1);
|
||||
color: #335eea;
|
||||
border-radius: 8px;
|
||||
margin-right: 12px;
|
||||
transition: 0.2s;
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
&:hover {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
}
|
||||
button.grey {
|
||||
background-color: #f5f5f7;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
91
src/components/ContextMenu.vue
Normal file
91
src/components/ContextMenu.vue
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div class="context-menu">
|
||||
<div
|
||||
class="menu"
|
||||
tabindex="-1"
|
||||
ref="menu"
|
||||
v-if="showMenu"
|
||||
@blur="closeMenu"
|
||||
:style="{ top: top, left: left }"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ContextMenu",
|
||||
data() {
|
||||
return {
|
||||
showMenu: false,
|
||||
top: "0px",
|
||||
left: "0px",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setMenu: function(top, left) {
|
||||
let largestHeight =
|
||||
window.innerHeight - this.$refs.menu.offsetHeight - 25;
|
||||
let largestWidth = window.innerWidth - this.$refs.menu.offsetWidth - 25;
|
||||
if (top > largestHeight) top = largestHeight;
|
||||
if (left > largestWidth) left = largestWidth;
|
||||
this.top = top + "px";
|
||||
this.left = left + "px";
|
||||
},
|
||||
|
||||
closeMenu: function() {
|
||||
this.showMenu = false;
|
||||
},
|
||||
|
||||
openMenu: function(e) {
|
||||
this.showMenu = true;
|
||||
this.$nextTick(
|
||||
function() {
|
||||
this.$refs.menu.focus();
|
||||
this.setMenu(e.y, e.x);
|
||||
}.bind(this)
|
||||
);
|
||||
e.preventDefault();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.context-menu {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
min-width: 136px;
|
||||
list-style: none;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
z-index: 1000;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu .item {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 7px;
|
||||
cursor: default;
|
||||
&:hover {
|
||||
background: #eaeffd;
|
||||
color: #335eea;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
196
src/components/Cover.vue
Normal file
196
src/components/Cover.vue
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<div style="position: relative">
|
||||
<transition name="zoom">
|
||||
<div
|
||||
class="cover"
|
||||
@mouseover="focus = true"
|
||||
@mouseleave="focus = false"
|
||||
:style="coverStyle"
|
||||
:class="{
|
||||
'hover-float': hoverEffect,
|
||||
'hover-play-button': showPlayButton,
|
||||
}"
|
||||
@click="clickToPlay ? play() : goTo()"
|
||||
>
|
||||
<button
|
||||
class="play-button"
|
||||
v-if="showPlayButton"
|
||||
:style="playButtonStyle"
|
||||
@click.stop="playButtonClicked"
|
||||
>
|
||||
<svg-icon icon-class="play" />
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition name="fade" v-if="hoverEffect">
|
||||
<img class="shadow" v-show="focus" :src="url" :style="shadowStyle"
|
||||
/></transition>
|
||||
<img
|
||||
class="shadow"
|
||||
v-if="alwaysShowShadow"
|
||||
:src="url"
|
||||
:style="shadowStyle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { playAlbumByID, playPlaylistByID, playArtistByID } from "@/utils/play";
|
||||
|
||||
export default {
|
||||
name: "Cover",
|
||||
props: {
|
||||
id: Number,
|
||||
type: String,
|
||||
url: String,
|
||||
hoverEffect: Boolean,
|
||||
showPlayButton: Boolean,
|
||||
alwaysShowShadow: Boolean,
|
||||
showBlackShadow: Boolean,
|
||||
clickToPlay: Boolean,
|
||||
size: {
|
||||
type: Number,
|
||||
default: 208,
|
||||
},
|
||||
shadowMargin: {
|
||||
type: Number,
|
||||
default: 12,
|
||||
},
|
||||
radius: {
|
||||
type: Number,
|
||||
default: 12,
|
||||
},
|
||||
playButtonSize: {
|
||||
type: Number,
|
||||
default: 48,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
focus: false,
|
||||
shadowStyle: {},
|
||||
playButtonStyle: {},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.shadowStyle = {
|
||||
height: `${this.size}px`,
|
||||
width: `${this.size}px`,
|
||||
top: `${this.shadowMargin}px`,
|
||||
borderRadius: `${this.radius}px`,
|
||||
};
|
||||
this.playButtonStyle = {
|
||||
height: `${this.playButtonSize}px`,
|
||||
width: `${this.playButtonSize}px`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
coverStyle() {
|
||||
return {
|
||||
backgroundImage: `url('${this.url}')`,
|
||||
boxShadow: this.showBlackShadow
|
||||
? "0 12px 16px -8px rgba(0, 0, 0, 0.2)"
|
||||
: "",
|
||||
height: `${this.size}px`,
|
||||
width: `${this.size}px`,
|
||||
borderRadius: `${this.radius}px`,
|
||||
cursor: this.clickToPlay ? "default" : "pointer",
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
play() {
|
||||
if (this.type === "album") {
|
||||
playAlbumByID(this.id);
|
||||
} else if (this.type === "playlist") {
|
||||
playPlaylistByID(this.id);
|
||||
}
|
||||
},
|
||||
playButtonClicked() {
|
||||
if (this.type === "album") {
|
||||
playAlbumByID(this.id);
|
||||
} else if (this.type === "playlist") {
|
||||
playPlaylistByID(this.id);
|
||||
} else if (this.type === "artist") {
|
||||
playArtistByID(this.id);
|
||||
}
|
||||
},
|
||||
goTo() {
|
||||
this.$router.push({ name: this.type, params: { id: this.id } });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cover {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.hover-float {
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 12px 16px -8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.hover-play-button {
|
||||
&:hover {
|
||||
.play-button {
|
||||
visibility: visible;
|
||||
transform: unset;
|
||||
}
|
||||
}
|
||||
.play-button {
|
||||
&:hover {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shadow {
|
||||
position: absolute;
|
||||
filter: blur(16px) opacity(0.6);
|
||||
z-index: -1;
|
||||
height: 208px;
|
||||
}
|
||||
.play-button {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// right: 72px;
|
||||
// top: 72px;
|
||||
border: none;
|
||||
backdrop-filter: blur(12px) brightness(96%);
|
||||
background: transparent;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
cursor: default;
|
||||
transition: 0.2s;
|
||||
.svg-icon {
|
||||
height: 50%;
|
||||
margin: {
|
||||
left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
177
src/components/CoverRow.vue
Normal file
177
src/components/CoverRow.vue
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<template>
|
||||
<div class="cover-row">
|
||||
<div
|
||||
class="item"
|
||||
:class="{ artist: type === 'artist' }"
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:style="{ marginBottom: subText === 'none' ? '32px' : '24px' }"
|
||||
>
|
||||
<Cover
|
||||
class="cover"
|
||||
:id="item.id"
|
||||
:type="type === 'chart' ? 'playlist' : type"
|
||||
:url="getUrl(item) | resizeImage(imageSize)"
|
||||
:hoverEffect="true"
|
||||
:showBlackShadow="true"
|
||||
:showPlayButton="showPlayButton"
|
||||
:radius="type === 'artist' ? 100 : 12"
|
||||
:size="type === 'artist' ? 192 : 208"
|
||||
/>
|
||||
|
||||
<div class="text">
|
||||
<div class="info" v-if="showPlayCount">
|
||||
<span class="play-count"
|
||||
><svg-icon icon-class="play" />{{
|
||||
item.playCount | formatPlayCount
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="name">
|
||||
<span
|
||||
class="explicit-symbol"
|
||||
v-if="type === 'album' && item.mark === 1056768"
|
||||
><ExplicitSymbol
|
||||
/></span>
|
||||
<router-link
|
||||
:to="`/${type === 'chart' ? 'playlist' : type}/${item.id}`"
|
||||
>{{ item.name }}</router-link
|
||||
>
|
||||
</div>
|
||||
<div class="info" v-if="type !== 'artist' && subText !== 'none'">
|
||||
<span v-html="getSubText(item)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ExplicitSymbol from "@/components/ExplicitSymbol.vue";
|
||||
import Cover from "@/components/Cover.vue";
|
||||
|
||||
export default {
|
||||
name: "CoverRow",
|
||||
components: {
|
||||
Cover,
|
||||
ExplicitSymbol,
|
||||
},
|
||||
props: {
|
||||
items: Array,
|
||||
type: String,
|
||||
subText: {
|
||||
type: String,
|
||||
default: "none",
|
||||
},
|
||||
imageSize: {
|
||||
type: Number,
|
||||
default: 512,
|
||||
},
|
||||
showPlayButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showPlayCount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getUrl(item) {
|
||||
if (item.picUrl !== undefined) return item.picUrl;
|
||||
if (item.coverImgUrl !== undefined) return item.coverImgUrl;
|
||||
if (item.img1v1Url !== undefined) return item.img1v1Url;
|
||||
},
|
||||
getSubText(item) {
|
||||
if (this.subText === "copywriter") return item.copywriter;
|
||||
if (this.subText === "description") return item.description;
|
||||
if (this.subText === "updateFrequency") return item.updateFrequency;
|
||||
if (this.subText === "creator") return "by " + item.creator.nickname;
|
||||
if (this.subText === "releaseYear")
|
||||
return new Date(item.publishTime).getFullYear();
|
||||
if (this.subText === "artist")
|
||||
return `<a href="/#/artist/${item.artist.id}">${item.artist.name}</a>`;
|
||||
if (this.subText === "albumType+releaseYear")
|
||||
return `${item.size === 1 ? "Single" : "EP"} · ${new Date(
|
||||
item.publishTime
|
||||
).getFullYear()}`;
|
||||
if (this.subText === "appleMusic") return "by Apple Music";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cover-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: {
|
||||
right: -12px;
|
||||
left: -12px;
|
||||
}
|
||||
.index-playlist {
|
||||
margin: 12px 12px 24px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
margin: 12px 12px 24px 12px;
|
||||
.text {
|
||||
width: 208px;
|
||||
margin-top: 8px;
|
||||
.name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
line-height: 20px;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.info {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.68);
|
||||
line-height: 18px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
// margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item.artist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
.cover {
|
||||
display: flex;
|
||||
}
|
||||
.name {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.explicit-symbol {
|
||||
color: rgba(0, 0, 0, 0.28);
|
||||
float: right;
|
||||
.svg-icon {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.play-count {
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.58);
|
||||
font-size: 12px;
|
||||
.svg-icon {
|
||||
margin-right: 3px;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
33
src/components/ExplicitSymbol.vue
Normal file
33
src/components/ExplicitSymbol.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<svg-icon icon-class="explicit" :style="svgStyle"></svg-icon>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
|
||||
export default {
|
||||
name: "ExplicitSymbol",
|
||||
components: {
|
||||
SvgIcon,
|
||||
},
|
||||
props: {
|
||||
size: {
|
||||
type: Number,
|
||||
default: 16,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
svgStyle: {},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.svgStyle = {
|
||||
height: this.size + "px",
|
||||
width: this.size + "px",
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
26
src/components/Footer.vue
Normal file
26
src/components/Footer.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<footer>
|
||||
<ButtonTwoTone :iconClass="'settings'" :color="'grey'">
|
||||
Settings
|
||||
</ButtonTwoTone>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ButtonTwoTone from "@/components/ButtonTwoTone.vue";
|
||||
|
||||
export default {
|
||||
name: "Footer",
|
||||
components: {
|
||||
ButtonTwoTone,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 48px;
|
||||
}
|
||||
</style>
|
||||
196
src/components/Navbar.vue
Normal file
196
src/components/Navbar.vue
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<nav>
|
||||
<div class="navigation-buttons">
|
||||
<button-icon @click.native="go('back')"
|
||||
><svg-icon icon-class="arrow-left"
|
||||
/></button-icon>
|
||||
<button-icon @click.native="go('forward')"
|
||||
><svg-icon icon-class="arrow-right"
|
||||
/></button-icon>
|
||||
</div>
|
||||
<div class="navigation-links">
|
||||
<router-link to="/" :class="{ active: this.$route.name === 'home' }"
|
||||
>Home</router-link
|
||||
>
|
||||
<router-link
|
||||
to="/explore"
|
||||
:class="{ active: this.$route.name === 'explore' }"
|
||||
>Explore</router-link
|
||||
>
|
||||
<router-link
|
||||
to="/library"
|
||||
:class="{ active: this.$route.name === 'library' }"
|
||||
>Library</router-link
|
||||
>
|
||||
</div>
|
||||
<div class="right-part">
|
||||
<a href="https://github.com/qier222/YesPlayMusic" target="blank"
|
||||
><svg-icon icon-class="github" class="github"
|
||||
/></a>
|
||||
<div class="search-box">
|
||||
<div class="container" :class="{ active: inputFocus }">
|
||||
<svg-icon icon-class="search" />
|
||||
<div class="input">
|
||||
<input
|
||||
:placeholder="inputFocus ? '' : 'Search'"
|
||||
v-model="keywords"
|
||||
@keydown.enter="goToSearchPage"
|
||||
@focus="inputFocus = true"
|
||||
@blur="inputFocus = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ButtonIcon from "@/components/ButtonIcon.vue";
|
||||
|
||||
export default {
|
||||
name: "Navbar",
|
||||
components: {
|
||||
ButtonIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputFocus: false,
|
||||
keywords: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
go(where) {
|
||||
if (where === "back") this.$router.go(-1);
|
||||
else this.$router.go(1);
|
||||
},
|
||||
goToSearchPage() {
|
||||
this.$router.push({
|
||||
name: "search",
|
||||
query: { keywords: this.keywords },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
padding: {
|
||||
right: 10vw;
|
||||
left: 10vw;
|
||||
}
|
||||
backdrop-filter: saturate(180%) blur(30px);
|
||||
background-color: rgba(255, 255, 255, 0.86);
|
||||
z-index: 100;
|
||||
// border-bottom: 1px solid rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.navigation-buttons {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.svg-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
.navigation-links {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
a {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
color: black;
|
||||
transition: 0.2s;
|
||||
margin: {
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
&:hover {
|
||||
background: #eaeffd;
|
||||
color: #335eea;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.92);
|
||||
transition: 0.2s;
|
||||
}
|
||||
}
|
||||
a.active {
|
||||
color: #335eea;
|
||||
}
|
||||
}
|
||||
.search {
|
||||
.svg-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
|
||||
justify-content: flex-end;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
border-radius: 8px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
color: #aaaaaa;
|
||||
margin: {
|
||||
left: 8px;
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 96%;
|
||||
font-weight: 600;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: #eaeffd;
|
||||
input,
|
||||
.svg-icon {
|
||||
color: #335eea;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-part {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.github {
|
||||
margin-right: 16px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
src/components/SvgIcon.vue
Normal file
39
src/components/SvgIcon.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<svg :class="svgClass" aria-hidden="true">
|
||||
<use :xlink:href="iconName" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SvgIcon",
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return `#icon-${this.iconClass}`;
|
||||
},
|
||||
svgClass() {
|
||||
if (this.className) {
|
||||
return "svg-icon " + this.className;
|
||||
} else {
|
||||
return "svg-icon";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
91
src/components/TrackList.vue
Normal file
91
src/components/TrackList.vue
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div class="track-list" :style="listStyles">
|
||||
<ContextMenu ref="menu">
|
||||
<div class="item" @click="play">Play</div>
|
||||
<div class="item" @click="playNext">Play Next</div>
|
||||
</ContextMenu>
|
||||
<TrackListItem
|
||||
v-for="track in tracks"
|
||||
:track="track"
|
||||
:key="track.id"
|
||||
@dblclick.native="playThisList(track.id)"
|
||||
@click.right.native="openMenu($event, track)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from "vuex";
|
||||
import {
|
||||
playPlaylistByID,
|
||||
playAlbumByID,
|
||||
playAList,
|
||||
appendTrackToPlayerList,
|
||||
} from "@/utils/play";
|
||||
|
||||
import TrackListItem from "@/components/TrackListItem.vue";
|
||||
import ContextMenu from "@/components/ContextMenu.vue";
|
||||
|
||||
export default {
|
||||
name: "TrackList",
|
||||
components: {
|
||||
TrackListItem,
|
||||
ContextMenu,
|
||||
},
|
||||
props: {
|
||||
tracks: Array,
|
||||
type: String,
|
||||
id: Number,
|
||||
itemWidth: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
dbclickTrackFunc: {
|
||||
type: String,
|
||||
default: "none",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
clickTrack: null,
|
||||
listStyles: {},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.type === "tracklist")
|
||||
this.listStyles = { display: "flex", flexWrap: "wrap" };
|
||||
},
|
||||
methods: {
|
||||
...mapActions(["nextTrack"]),
|
||||
openMenu(e, track) {
|
||||
if (!track.playable) {
|
||||
return;
|
||||
}
|
||||
this.clickTrack = track;
|
||||
this.$refs.menu.openMenu(e);
|
||||
},
|
||||
playThisList(trackID) {
|
||||
if (this.type === "playlist") {
|
||||
playPlaylistByID(this.id, trackID);
|
||||
} else if (this.type === "album") {
|
||||
playAlbumByID(this.id, trackID);
|
||||
} else if (this.type === "tracklist") {
|
||||
if (this.dbclickTrackFunc === "none") {
|
||||
playAList(this.tracks, this.tracks[0].ar[0].id, "artist", trackID);
|
||||
} else {
|
||||
if (this.dbclickTrackFunc === "playPlaylistByID")
|
||||
playPlaylistByID(this.id, trackID);
|
||||
}
|
||||
}
|
||||
},
|
||||
play() {
|
||||
appendTrackToPlayerList(this.clickTrack, true);
|
||||
},
|
||||
playNext() {
|
||||
appendTrackToPlayerList(this.clickTrack);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
250
src/components/TrackListItem.vue
Normal file
250
src/components/TrackListItem.vue
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
<template>
|
||||
<div class="track" :class="trackClass" :style="trackStyle">
|
||||
<img :src="imgUrl | resizeImage" v-if="!isAlbum" @click="goToAlbum" />
|
||||
<div class="no" v-if="isAlbum">{{ track.no }}</div>
|
||||
<div class="title-and-artist">
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
{{ track.name }}
|
||||
<span class="featured" v-if="isAlbum && track.ar.length > 1">
|
||||
-
|
||||
<ArtistsInLine :artists="track.ar" :showFirstArtist="false"
|
||||
/></span>
|
||||
<span v-if="isAlbum && track.mark === 1318912" class="explicit-symbol"
|
||||
><ExplicitSymbol
|
||||
/></span>
|
||||
</div>
|
||||
<div class="artist" v-if="!isAlbum">
|
||||
<span
|
||||
v-if="track.mark === 1318912"
|
||||
class="explicit-symbol before-artist"
|
||||
><ExplicitSymbol
|
||||
/></span>
|
||||
<ArtistsInLine :artists="artists" />
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="album" v-if="!isTracklist && !isAlbum">
|
||||
<div class="container">
|
||||
<router-link :to="`/album/${track.al.id}`">{{
|
||||
track.al.name
|
||||
}}</router-link>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="time" v-if="!isTracklist">
|
||||
{{ track.dt | formatTime }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ArtistsInLine from "@/components/ArtistsInLine.vue";
|
||||
import ExplicitSymbol from "@/components/ExplicitSymbol.vue";
|
||||
|
||||
export default {
|
||||
name: "TrackListItem",
|
||||
components: { ArtistsInLine, ExplicitSymbol },
|
||||
props: {
|
||||
track: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
trackClass: [],
|
||||
trackStyle: {},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.trackClass.push(this.type);
|
||||
if (!this.track.playable) this.trackClass.push("disable");
|
||||
if (this.$parent.itemWidth !== -1)
|
||||
this.trackStyle = { width: this.$parent.itemWidth + "px" };
|
||||
},
|
||||
computed: {
|
||||
imgUrl() {
|
||||
if (this.track.al !== undefined) return this.track.al.picUrl;
|
||||
if (this.track.album !== undefined) return this.track.album.picUrl;
|
||||
return "";
|
||||
},
|
||||
artists() {
|
||||
if (this.track.ar !== undefined) return this.track.ar;
|
||||
if (this.track.artists !== undefined) return this.track.artists;
|
||||
return [];
|
||||
},
|
||||
type() {
|
||||
return this.$parent.type;
|
||||
},
|
||||
isAlbum() {
|
||||
return this.type === "album";
|
||||
},
|
||||
isTracklist() {
|
||||
return this.type === "tracklist";
|
||||
},
|
||||
isPlaylist() {
|
||||
return this.type === "playlist";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goToAlbum() {
|
||||
this.$router.push({ path: "/album/" + this.track.al.id });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
|
||||
.no {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
margin: 0 20px 0 10px;
|
||||
width: 12px;
|
||||
color: rgba(0, 0, 0, 0.58);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.explicit-symbol {
|
||||
color: rgba(0, 0, 0, 0.28);
|
||||
.svg-icon {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.explicit-symbol.before-artist {
|
||||
margin-right: 2px;
|
||||
.svg-icon {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 8px;
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
margin-right: 20px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
cursor: pointer;
|
||||
}
|
||||
.title-and-artist {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
cursor: default;
|
||||
padding-right: 16px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
.featured {
|
||||
margin-right: 2px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.72);
|
||||
}
|
||||
}
|
||||
.artist {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.68);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
a {
|
||||
span {
|
||||
margin-right: 3px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.album {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
font-size: 16px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
.time {
|
||||
font-size: 16px;
|
||||
width: 50px;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: 10px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
&:hover {
|
||||
transition: all 0.3s;
|
||||
background: #f5f5f7;
|
||||
}
|
||||
}
|
||||
.track.disable {
|
||||
img {
|
||||
filter: grayscale(1) opacity(0.6);
|
||||
}
|
||||
.title,
|
||||
.artist,
|
||||
.album,
|
||||
.time,
|
||||
.featured {
|
||||
color: rgba(0, 0, 0, 0.28) !important;
|
||||
}
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.track.tracklist {
|
||||
width: 256px;
|
||||
img {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
border-radius: 6px;
|
||||
margin-right: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
.artist {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.track.album {
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue