first commit

This commit is contained in:
qier222 2020-10-10 19:54:44 +08:00
commit e4ba16b9a2
102 changed files with 19066 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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>

View 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
View 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
View 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>

View 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>

View 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>

View 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>