You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
574 lines
14 KiB
574 lines
14 KiB
<template>
|
|
<div v-show="show" ref="library">
|
|
<h1>
|
|
<img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{
|
|
data.user.nickname
|
|
}}{{ $t('library.sLibrary') }}
|
|
</h1>
|
|
<div class="section-one">
|
|
<div class="liked-songs" @click="goToLikedSongsList">
|
|
<div class="top">
|
|
<p>
|
|
<span
|
|
v-for="(line, index) in pickedLyric"
|
|
v-show="line !== ''"
|
|
:key="`${line}${index}`"
|
|
>{{ line }}<br
|
|
/></span>
|
|
</p>
|
|
</div>
|
|
<div class="bottom">
|
|
<div class="titles">
|
|
<div class="title">{{ $t('library.likedSongs') }}</div>
|
|
<div class="sub-title">
|
|
{{ liked.songs.length }} {{ $t('common.songs') }}
|
|
</div>
|
|
</div>
|
|
<button @click.stop="playLikedSongs">
|
|
<svg-icon icon-class="play" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="songs">
|
|
<TrackList
|
|
:id="liked.playlists.length > 0 ? liked.playlists[0].id : 0"
|
|
:tracks="liked.songsWithDetails"
|
|
:column-number="3"
|
|
type="tracklist"
|
|
dbclick-track-func="playPlaylistByID"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-two">
|
|
<div class="tabs-row">
|
|
<div class="tabs">
|
|
<div
|
|
class="tab dropdown"
|
|
:class="{ active: currentTab === 'playlists' }"
|
|
@click="updateCurrentTab('playlists')"
|
|
>
|
|
<span class="text">{{
|
|
{
|
|
all: $t('contextMenu.allPlaylists'),
|
|
mine: $t('contextMenu.minePlaylists'),
|
|
liked: $t('contextMenu.likedPlaylists'),
|
|
}[playlistFilter]
|
|
}}</span>
|
|
<span class="icon" @click.stop="openPlaylistTabMenu"
|
|
><svg-icon icon-class="dropdown"
|
|
/></span>
|
|
</div>
|
|
<div
|
|
class="tab"
|
|
:class="{ active: currentTab === 'albums' }"
|
|
@click="updateCurrentTab('albums')"
|
|
>
|
|
{{ $t('library.albums') }}
|
|
</div>
|
|
<div
|
|
class="tab"
|
|
:class="{ active: currentTab === 'artists' }"
|
|
@click="updateCurrentTab('artists')"
|
|
>
|
|
{{ $t('library.artists') }}
|
|
</div>
|
|
<div
|
|
class="tab"
|
|
:class="{ active: currentTab === 'mvs' }"
|
|
@click="updateCurrentTab('mvs')"
|
|
>
|
|
{{ $t('library.mvs') }}
|
|
</div>
|
|
<div
|
|
class="tab"
|
|
:class="{ active: currentTab === 'cloudDisk' }"
|
|
@click="updateCurrentTab('cloudDisk')"
|
|
>
|
|
云盘
|
|
</div>
|
|
<div
|
|
class="tab"
|
|
:class="{ active: currentTab === 'playHistory' }"
|
|
@click="updateCurrentTab('playHistory')"
|
|
>
|
|
听歌排行
|
|
</div>
|
|
</div>
|
|
<button
|
|
v-show="currentTab === 'playlists'"
|
|
class="tab-button"
|
|
@click="openAddPlaylistModal"
|
|
><svg-icon icon-class="plus" />{{ $t('library.newPlayList') }}
|
|
</button>
|
|
<button
|
|
v-show="currentTab === 'cloudDisk'"
|
|
class="tab-button"
|
|
@click="selectUploadFiles"
|
|
><svg-icon icon-class="arrow-up-alt" /> 上传歌曲
|
|
</button>
|
|
</div>
|
|
|
|
<div v-show="currentTab === 'playlists'">
|
|
<div v-if="liked.playlists.length > 1">
|
|
<CoverRow
|
|
:items="filterPlaylists.slice(1)"
|
|
type="playlist"
|
|
sub-text="creator"
|
|
:show-play-button="true"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-show="currentTab === 'albums'">
|
|
<CoverRow
|
|
:items="liked.albums"
|
|
type="album"
|
|
sub-text="artist"
|
|
:show-play-button="true"
|
|
/>
|
|
</div>
|
|
|
|
<div v-show="currentTab === 'artists'">
|
|
<CoverRow
|
|
:items="liked.artists"
|
|
type="artist"
|
|
:show-play-button="true"
|
|
/>
|
|
</div>
|
|
|
|
<div v-show="currentTab === 'mvs'">
|
|
<MvRow :mvs="liked.mvs" />
|
|
</div>
|
|
|
|
<div v-show="currentTab === 'cloudDisk'">
|
|
<TrackList
|
|
:id="-8"
|
|
:tracks="liked.cloudDisk"
|
|
:column-number="3"
|
|
type="cloudDisk"
|
|
dbclick-track-func="playCloudDisk"
|
|
:extra-context-menu-item="['removeTrackFromCloudDisk']"
|
|
/>
|
|
</div>
|
|
|
|
<div v-show="currentTab === 'playHistory'">
|
|
<button class="playHistory-button" @click="playHistoryMode = 'week'">
|
|
最近一周
|
|
</button>
|
|
<button class="playHistory-button" @click="playHistoryMode = 'all'">
|
|
所有時間
|
|
</button>
|
|
<TrackList
|
|
:tracks="playHistoryList"
|
|
:column-number="1"
|
|
type="tracklist"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<input
|
|
ref="cloudDiskUploadInput"
|
|
type="file"
|
|
style="display: none"
|
|
@change="uploadSongToCloudDisk"
|
|
/>
|
|
|
|
<ContextMenu ref="playlistTabMenu">
|
|
<div class="item" @click="changePlaylistFilter('all')">{{
|
|
$t('contextMenu.allPlaylists')
|
|
}}</div>
|
|
<hr />
|
|
<div class="item" @click="changePlaylistFilter('mine')">{{
|
|
$t('contextMenu.minePlaylists')
|
|
}}</div>
|
|
<div class="item" @click="changePlaylistFilter('liked')">{{
|
|
$t('contextMenu.likedPlaylists')
|
|
}}</div>
|
|
</ContextMenu>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapActions, mapMutations, mapState } from 'vuex';
|
|
import { randomNum, dailyTask } from '@/utils/common';
|
|
import { isAccountLoggedIn } from '@/utils/auth';
|
|
import { uploadSong } from '@/api/user';
|
|
import { getLyric } from '@/api/track';
|
|
import NProgress from 'nprogress';
|
|
import locale from '@/locale';
|
|
|
|
import ContextMenu from '@/components/ContextMenu.vue';
|
|
import TrackList from '@/components/TrackList.vue';
|
|
import CoverRow from '@/components/CoverRow.vue';
|
|
import SvgIcon from '@/components/SvgIcon.vue';
|
|
import MvRow from '@/components/MvRow.vue';
|
|
|
|
/**
|
|
* Pick the lyric part from a string formed in `[timecode] lyric`.
|
|
*
|
|
* @param {string} rawLyric The raw lyric string formed in `[timecode] lyric`
|
|
* @returns {string} The lyric part
|
|
*/
|
|
function extractLyricPart(rawLyric) {
|
|
return rawLyric.split(']')[1].trim();
|
|
}
|
|
|
|
export default {
|
|
name: 'Library',
|
|
components: { SvgIcon, CoverRow, TrackList, MvRow, ContextMenu },
|
|
data() {
|
|
return {
|
|
show: false,
|
|
likedSongs: [],
|
|
lyric: undefined,
|
|
currentTab: 'playlists',
|
|
playHistoryMode: 'week',
|
|
};
|
|
},
|
|
computed: {
|
|
...mapState(['data', 'liked']),
|
|
/**
|
|
* @returns {string[]}
|
|
*/
|
|
pickedLyric() {
|
|
/** @type {string?} */
|
|
const lyric = this.lyric;
|
|
|
|
// Returns [] if we got no lyrics.
|
|
if (!lyric) return [];
|
|
|
|
const lyricLine = lyric
|
|
.split('\n')
|
|
.filter(line => !line.includes('作词') && !line.includes('作曲'));
|
|
|
|
// Pick 3 or fewer lyrics based on the lyric lines.
|
|
const lyricsToPick = Math.min(lyricLine.length, 3);
|
|
|
|
// The upperbound of the lyric line to pick
|
|
const randomUpperBound = lyricLine.length - lyricsToPick;
|
|
const startLyricLineIndex = randomNum(0, randomUpperBound - 1);
|
|
|
|
// Pick lyric lines to render.
|
|
return lyricLine
|
|
.slice(startLyricLineIndex, startLyricLineIndex + lyricsToPick)
|
|
.map(extractLyricPart);
|
|
},
|
|
playlistFilter() {
|
|
return this.data.libraryPlaylistFilter || 'all';
|
|
},
|
|
filterPlaylists() {
|
|
const playlists = this.liked.playlists;
|
|
const userId = this.data.user.userId;
|
|
if (this.playlistFilter === 'mine') {
|
|
return playlists.filter(p => p.creator.userId === userId);
|
|
} else if (this.playlistFilter === 'liked') {
|
|
return playlists.filter(p => p.creator.userId !== userId);
|
|
}
|
|
return playlists;
|
|
},
|
|
playHistoryList() {
|
|
if (this.show && this.playHistoryMode === 'week') {
|
|
return this.liked.playHistory.weekData;
|
|
} else if (this.show && this.playHistoryMode === 'all') {
|
|
return this.liked.playHistory.allData;
|
|
}
|
|
return [];
|
|
},
|
|
},
|
|
created() {
|
|
setTimeout(() => {
|
|
if (!this.show) NProgress.start();
|
|
}, 1000);
|
|
this.loadData();
|
|
},
|
|
activated() {
|
|
this.$parent.$refs.scrollbar.restorePosition();
|
|
this.loadData();
|
|
dailyTask();
|
|
},
|
|
methods: {
|
|
...mapActions(['showToast']),
|
|
...mapMutations(['updateModal', 'updateData']),
|
|
loadData() {
|
|
if (this.liked.songsWithDetails.length > 0) {
|
|
NProgress.done();
|
|
this.show = true;
|
|
this.$store.dispatch('fetchLikedSongsWithDetails');
|
|
this.getRandomLyric();
|
|
} else {
|
|
this.$store.dispatch('fetchLikedSongsWithDetails').then(() => {
|
|
NProgress.done();
|
|
this.show = true;
|
|
this.getRandomLyric();
|
|
});
|
|
}
|
|
this.$store.dispatch('fetchLikedSongs');
|
|
this.$store.dispatch('fetchLikedPlaylist');
|
|
this.$store.dispatch('fetchLikedAlbums');
|
|
this.$store.dispatch('fetchLikedArtists');
|
|
this.$store.dispatch('fetchLikedMVs');
|
|
this.$store.dispatch('fetchCloudDisk');
|
|
this.$store.dispatch('fetchPlayHistory');
|
|
},
|
|
playLikedSongs() {
|
|
this.$store.state.player.playPlaylistByID(
|
|
this.liked.playlists[0].id,
|
|
'first',
|
|
true
|
|
);
|
|
},
|
|
updateCurrentTab(tab) {
|
|
if (!isAccountLoggedIn() && tab !== 'playlists') {
|
|
this.showToast(locale.t('toast.needToLogin'));
|
|
return;
|
|
}
|
|
this.currentTab = tab;
|
|
this.$parent.$refs.main.scrollTo({ top: 375, behavior: 'smooth' });
|
|
},
|
|
goToLikedSongsList() {
|
|
this.$router.push({ path: '/library/liked-songs' });
|
|
},
|
|
getRandomLyric() {
|
|
if (this.liked.songs.length === 0) return;
|
|
getLyric(
|
|
this.liked.songs[randomNum(0, this.liked.songs.length - 1)]
|
|
).then(data => {
|
|
if (data.lrc !== undefined) {
|
|
const isInstrumental = data.lrc.lyric
|
|
.split('\n')
|
|
.filter(l => l.includes('纯音乐,请欣赏'));
|
|
if (isInstrumental.length === 0) {
|
|
this.lyric = data.lrc.lyric;
|
|
}
|
|
}
|
|
});
|
|
},
|
|
openAddPlaylistModal() {
|
|
if (!isAccountLoggedIn()) {
|
|
this.showToast(locale.t('toast.needToLogin'));
|
|
return;
|
|
}
|
|
this.updateModal({
|
|
modalName: 'newPlaylistModal',
|
|
key: 'show',
|
|
value: true,
|
|
});
|
|
},
|
|
openPlaylistTabMenu(e) {
|
|
this.$refs.playlistTabMenu.openMenu(e);
|
|
},
|
|
changePlaylistFilter(type) {
|
|
this.updateData({ key: 'libraryPlaylistFilter', value: type });
|
|
window.scrollTo({ top: 375, behavior: 'smooth' });
|
|
},
|
|
selectUploadFiles() {
|
|
this.$refs.cloudDiskUploadInput.click();
|
|
},
|
|
uploadSongToCloudDisk(e) {
|
|
const files = e.target.files;
|
|
uploadSong(files[0]).then(result => {
|
|
if (result.code === 200) {
|
|
let newCloudDisk = this.liked.cloudDisk;
|
|
newCloudDisk.unshift(result.privateCloud);
|
|
this.$store.commit('updateLikedXXX', {
|
|
name: 'cloudDisk',
|
|
data: newCloudDisk,
|
|
});
|
|
}
|
|
});
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
h1 {
|
|
font-size: 42px;
|
|
color: var(--color-text);
|
|
display: flex;
|
|
align-items: center;
|
|
.avatar {
|
|
height: 44px;
|
|
margin-right: 12px;
|
|
vertical-align: -7px;
|
|
border-radius: 50%;
|
|
border: rgba(0, 0, 0, 0.2);
|
|
}
|
|
}
|
|
|
|
.section-one {
|
|
display: flex;
|
|
margin-top: 24px;
|
|
.songs {
|
|
flex: 7;
|
|
margin-top: 8px;
|
|
margin-left: 36px;
|
|
overflow: hidden;
|
|
}
|
|
}
|
|
|
|
.liked-songs {
|
|
flex: 3;
|
|
margin-top: 8px;
|
|
cursor: pointer;
|
|
border-radius: 16px;
|
|
padding: 18px 24px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
transition: all 0.4s;
|
|
box-sizing: border-box;
|
|
|
|
background: var(--color-primary-bg);
|
|
|
|
.bottom {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
color: var(--color-primary);
|
|
|
|
.title {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
}
|
|
.sub-title {
|
|
font-size: 15px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
button {
|
|
margin-bottom: 2px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 44px;
|
|
width: 44px;
|
|
background: var(--color-primary);
|
|
border-radius: 50%;
|
|
transition: 0.2s;
|
|
box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.2);
|
|
cursor: default;
|
|
|
|
.svg-icon {
|
|
color: var(--color-primary-bg);
|
|
margin-left: 4px;
|
|
height: 16px;
|
|
width: 16px;
|
|
}
|
|
&:hover {
|
|
transform: scale(1.06);
|
|
box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.4);
|
|
}
|
|
&:active {
|
|
transform: scale(0.94);
|
|
}
|
|
}
|
|
}
|
|
|
|
.top {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
font-size: 14px;
|
|
opacity: 0.88;
|
|
color: var(--color-primary);
|
|
p {
|
|
margin-top: 2px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.section-two {
|
|
margin-top: 54px;
|
|
min-height: calc(100vh - 182px);
|
|
}
|
|
|
|
.tabs-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
font-size: 18px;
|
|
color: var(--color-text);
|
|
.tab {
|
|
font-weight: 600;
|
|
padding: 8px 14px;
|
|
margin-right: 14px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: 0.2s;
|
|
opacity: 0.68;
|
|
&:hover {
|
|
opacity: 0.88;
|
|
background-color: var(--color-secondary-bg);
|
|
}
|
|
}
|
|
.tab.active {
|
|
opacity: 0.88;
|
|
background-color: var(--color-secondary-bg);
|
|
}
|
|
.tab.dropdown {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
.text {
|
|
padding: 8px 3px 8px 14px;
|
|
}
|
|
.icon {
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 8px 0 3px;
|
|
.svg-icon {
|
|
height: 16px;
|
|
width: 16px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
button.tab-button {
|
|
color: var(--color-text);
|
|
border-radius: 8px;
|
|
padding: 0 14px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
transition: 0.2s;
|
|
opacity: 0.68;
|
|
font-weight: 500;
|
|
.svg-icon {
|
|
width: 14px;
|
|
height: 14px;
|
|
margin-right: 8px;
|
|
}
|
|
&:hover {
|
|
opacity: 1;
|
|
background: var(--color-secondary-bg);
|
|
}
|
|
&:active {
|
|
opacity: 1;
|
|
transform: scale(0.92);
|
|
}
|
|
}
|
|
|
|
button.playHistory-button {
|
|
color: var(--color-text);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
transition: 0.2s;
|
|
opacity: 0.68;
|
|
font-weight: 500;
|
|
&:hover {
|
|
opacity: 1;
|
|
background: var(--color-secondary-bg);
|
|
}
|
|
}
|
|
</style>
|