feat: personal FM (finally🎉)

This commit is contained in:
qier222 2021-03-06 17:09:08 +08:00
parent e169ee19e2
commit 11eb29b3b8
No known key found for this signature in database
GPG Key ID: 9C85007ED905F14D
9 changed files with 280 additions and 16 deletions

View File

@ -27,3 +27,24 @@ export function search(params) {
return data;
});
}
export function personalFM() {
return request({
url: "/personal_fm",
method: "get",
params: {
timestamp: new Date().getTime(),
},
});
}
export function fmTrash(id) {
return request({
url: "/fm_trash",
method: "post",
params: {
timestamp: new Date().getTime(),
id,
},
});
}

1
src/assets/icons/fm.svg Normal file
View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="radio-alt" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-radio-alt fa-w-16 fa-7x"><path fill="currentColor" d="M209 368h-64a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-16a16 16 0 0 0-16-16zm144 56a72 72 0 1 0-72-72 72.09 72.09 0 0 0 72 72zm96-296H212.5l288.83-81.21a16 16 0 0 0 11.07-19.74l-4.33-15.38A16 16 0 0 0 488.33.6L47.68 124.5A64 64 0 0 0 1 186.11V448a64 64 0 0 0 64 64h384a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64zm16 320a16 16 0 0 1-16 16H65a16 16 0 0 1-16-16V256h416zM113 336h128a16 16 0 0 0 16-16v-16a16 16 0 0 0-16-16H113a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16z" class=""></path></svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@ -0,0 +1 @@
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" width="30px" height="30px"><path d="M 5 3 C 3.895 3 3 3.895 3 5 L 3 20 C 3 21.105 3.895 22 5 22 L 9 22 L 9 26 C 9 26.552 9.448 27 10 27 C 10.3377 27 10.621648 26.821033 10.802734 26.564453 L 10.818359 26.570312 L 14.25 22 L 25 22 C 26.105 22 27 21.105 27 20 L 27 5 C 27 3.895 26.105 3 25 3 L 5 3 z M 11.5 9 C 12.881 9 14 10.119 14 11.5 L 14 12 C 14 14.214 12.899594 16.269094 11.058594 17.496094 L 10.554688 17.832031 L 9.4453125 16.167969 L 9.9492188 15.832031 C 10.647219 15.367031 11.186063 14.728094 11.539062 13.996094 C 11.525062 13.996094 11.513 14 11.5 14 C 10.119 14 9 12.881 9 11.5 C 9 10.119 10.119 9 11.5 9 z M 18.5 9 C 19.881 9 21 10.119 21 11.5 L 21 12 C 21 14.214 19.899594 16.269094 18.058594 17.496094 L 17.554688 17.832031 L 16.445312 16.167969 L 16.949219 15.832031 C 17.647219 15.367031 18.186063 14.728094 18.539062 13.996094 C 18.525063 13.996094 18.513 14 18.5 14 C 17.119 14 16 12.881 16 11.5 C 16 10.119 17.119 9 18.5 9 z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="thumbs-down" class="svg-inline--fa fa-thumbs-down fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M0 56v240c0 13.255 10.745 24 24 24h80c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24H24C10.745 32 0 42.745 0 56zm40 200c0-13.255 10.745-24 24-24s24 10.745 24 24-10.745 24-24 24-24-10.745-24-24zm272 256c-20.183 0-29.485-39.293-33.931-57.795-5.206-21.666-10.589-44.07-25.393-58.902-32.469-32.524-49.503-73.967-89.117-113.111a11.98 11.98 0 0 1-3.558-8.521V59.901c0-6.541 5.243-11.878 11.783-11.998 15.831-.29 36.694-9.079 52.651-16.178C256.189 17.598 295.709.017 343.995 0h2.844c42.777 0 93.363.413 113.774 29.737 8.392 12.057 10.446 27.034 6.148 44.632 16.312 17.053 25.063 48.863 16.382 74.757 17.544 23.432 19.143 56.132 9.308 79.469l.11.11c11.893 11.949 19.523 31.259 19.439 49.197-.156 30.352-26.157 58.098-59.553 58.098H350.723C358.03 364.34 384 388.132 384 430.548 384 504 336 512 312 512z"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

143
src/components/FMCard.vue Normal file
View File

@ -0,0 +1,143 @@
<template>
<div class="fm">
<img
class="cover"
:src="track.album && track.album.picUrl | resizeImage(512)"
@click="goToAlbum"
/>
<div class="right-part">
<div class="info">
<div class="title">{{ track.name }}</div>
<div class="artist"><ArtistsInLine :artists="track.artists" /></div>
</div>
<div class="controls">
<div class="buttons">
<button-icon @click.native="moveToFMTrash" title="不喜欢"
><svg-icon icon-class="thumbs-down" id="thumbs-down"
/></button-icon>
<button-icon
class="play"
@click.native="play"
:title="$t(isPlaying ? 'player.pause' : 'player.play')"
>
<svg-icon :iconClass="isPlaying ? 'pause' : 'play'"
/></button-icon>
<button-icon @click.native="next" :title="$t('player.next')"
><svg-icon icon-class="next" /></button-icon
></div>
<div class="card-name"><svg-icon icon-class="fm" />私人FM</div>
</div>
</div>
</div>
</template>
<script>
import ButtonIcon from "@/components/ButtonIcon.vue";
import ArtistsInLine from "@/components/ArtistsInLine.vue";
import { mapState } from "vuex";
export default {
name: "FMCard",
components: { ButtonIcon, ArtistsInLine },
computed: {
...mapState(["player"]),
track() {
return this.player.personalFMTrack;
},
isPlaying() {
return this.player.playing && this.player.isPersonalFM;
},
},
methods: {
play() {
this.player.playPersonalFM();
},
next() {
this.player.playNextTrack(true);
},
goToAlbum() {
if (this.track.album.id === 0) return;
this.$router.push({ path: "/album/" + this.track.album.id });
},
moveToFMTrash() {
this.player.moveToFMTrash();
},
},
};
</script>
<style lang="scss" scoped>
.fm {
padding: 1rem;
background: var(--color-secondary-bg);
border-radius: 1rem;
display: flex;
}
.cover {
height: 164px;
clip-path: border-box;
border-radius: 0.75rem;
margin-right: 1.2rem;
border: 1px solid rgb(243, 243, 243);
cursor: pointer;
user-select: none;
}
.right-part {
display: flex;
flex-direction: column;
justify-content: space-between;
color: var(--color-text);
width: 100%;
.title {
font-size: 1.6rem;
font-weight: 600;
margin-bottom: 0.6rem;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
word-break: break-all;
}
.artist {
opacity: 0.68;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
word-break: break-all;
}
.controls {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-left: -0.4rem;
.buttons {
display: flex;
}
.button-icon {
margin: 0 8px 0 0;
}
.svg-icon {
width: 24px;
height: 24px;
}
.svg-icon#thumbs-down {
width: 22px;
height: 22px;
}
.card-name {
font-size: 1rem;
opacity: 0.18;
display: flex;
align-items: center;
font-weight: 600;
user-select: none;
.svg-icon {
width: 18px;
height: 18px;
margin-right: 6px;
}
}
}
}
</style>

View File

@ -62,9 +62,18 @@
<div class="middle-control-buttons">
<div class="blank"></div>
<div class="container" @click.stop>
<button-icon @click.native="previous" :title="$t('player.previous')"
<button-icon
v-show="!player.isPersonalFM"
@click.native="previous"
:title="$t('player.previous')"
><svg-icon icon-class="previous"
/></button-icon>
<button-icon
v-show="player.isPersonalFM"
@click.native="moveToFMTrash"
title="不喜欢"
><svg-icon icon-class="thumbs-down"
/></button-icon>
<button-icon
class="play"
@click.native="play"
@ -84,7 +93,10 @@
<button-icon
@click.native="goToNextTracksPage"
:title="$t('player.nextUp')"
:class="{ active: this.$route.name === 'next' }"
:class="{
active: this.$route.name === 'next',
disabled: player.isPersonalFM,
}"
><svg-icon icon-class="list"
/></button-icon>
<button-icon
@ -94,7 +106,10 @@
: $t('player.repeat')
"
@click.native="repeat"
:class="{ active: player.repeatMode !== 'off' }"
:class="{
active: player.repeatMode !== 'off',
disabled: player.isPersonalFM,
}"
>
<svg-icon
icon-class="repeat"
@ -107,7 +122,7 @@
</button-icon>
<button-icon
@click.native="shuffle"
:class="{ active: player.shuffle }"
:class="{ active: player.shuffle, disabled: player.isPersonalFM }"
:title="$t('player.shuffle')"
><svg-icon icon-class="shuffle"
/></button-icon>
@ -221,9 +236,11 @@ export default {
if (this.player.playPrevTrack()) this.progress = 0;
},
shuffle() {
if (this.player.isPersonalFM) return;
this.player.shuffle = !this.player.shuffle;
},
repeat() {
if (this.player.isPersonalFM) return;
if (this.player.repeatMode === "on") {
this.player.repeatMode = "one";
} else if (this.player.repeatMode === "one") {
@ -240,6 +257,7 @@ export default {
this.progress = value;
},
goToNextTracksPage() {
if (this.player.isPersonalFM) return;
this.$route.name === "next"
? this.$router.go(-1)
: this.$router.push({ name: "next" });
@ -268,6 +286,9 @@ export default {
}
});
},
moveToFMTrash() {
this.player.moveToFMTrash();
},
goToList() {
if (this.player.playlistSource.id === this.data.likedSongPlaylistID)
this.$router.push({ path: "/library/liked-songs" });
@ -351,6 +372,7 @@ export default {
border-radius: 5px;
box-shadow: 0 6px 8px -2px rgba(0, 0, 0, 0.16);
cursor: pointer;
user-select: none;
}
.track-info {
height: 46px;
@ -448,12 +470,14 @@ export default {
margin-left: 16px;
}
// .lyrics-button {
// position: fixed;
// right: 18px;
// .svg-icon {
// height: 20px;
// width: 20px;
// }
// }
.button-icon.disabled {
cursor: default;
opacity: 0.38;
&:hover {
background: none;
}
&:active {
transform: unset;
}
}
</style>

View File

@ -6,6 +6,7 @@ import { cacheTrack } from "@/utils/db";
import { getAlbum } from "@/api/album";
import { getPlaylistDetail } from "@/api/playlist";
import { getArtist } from "@/api/artist";
import { personalFM, fmTrash } from "@/api/others";
import store from "@/store";
import { isAccountLoggedIn } from "@/utils/auth";
@ -29,6 +30,8 @@ export default class {
this._currentTrack = { id: 86827685 };
this._playNextList = []; // 当这个list不为空时会优先播放这个list的歌
this._playing = false;
this._isPersonalFM = false;
this._personalFMTrack = { id: 0 };
this._howler = null;
Object.defineProperty(this, "_howler", {
@ -99,6 +102,12 @@ export default class {
get playNextList() {
return this._playNextList;
}
get isPersonalFM() {
return this._isPersonalFM;
}
get personalFMTrack() {
return this._personalFMTrack;
}
_init() {
Howler.autoUnlock = false;
@ -115,6 +124,7 @@ export default class {
}); // update audio source and init howler
this._initMediaSession();
Howler.volume(this.volume);
this._loadPersonalFMTrack();
}
_getNextTrack() {
// 返回 [trackID, index]
@ -288,6 +298,12 @@ export default class {
this.playNextTrack();
}
}
_loadPersonalFMTrack() {
return personalFM().then((result) => {
this._personalFMTrack = result.data[0];
return this._personalFMTrack;
});
}
currentTrackID() {
const { list, current } = this._getListAndCurrent();
@ -296,11 +312,19 @@ export default class {
appendTrack(trackID) {
this.list.append(trackID);
}
playNextTrack() {
playNextTrack(isFM = false) {
if (this._isPersonalFM || isFM) {
this._isPersonalFM = true;
this._loadPersonalFMTrack().then(() => {
this._replaceCurrentTrack(this._personalFMTrack.id);
});
return true;
}
// TODO: 切换歌曲时增加加载中的状态
const [trackID, index] = this._getNextTrack();
if (trackID === undefined) {
this._howler.stop();
this._playing = false;
return false;
}
this.current = index;
@ -330,10 +354,18 @@ export default class {
document.title = "YesPlayMusic";
}
play() {
if (this._howler.playing()) return;
this._howler.play();
this._playing = true;
document.title = `${this._currentTrack.name} · ${this._currentTrack.ar[0].name} - YesPlayMusic`;
}
playOrPause() {
if (this._howler.playing()) {
this.pause();
} else {
this.play();
}
}
seek(time = null) {
if (time !== null) this._howler.seek(time);
return this._howler === null ? 0 : this._howler.seek();
@ -357,6 +389,7 @@ export default class {
playlistSourceType,
autoPlayTrackID = "first"
) {
this._isPersonalFM = false;
if (!this._enabled) this._enabled = true;
this.list = trackIDs;
this.current = 0;
@ -394,6 +427,19 @@ export default class {
this._playNextList.push(trackID);
if (playNow) this.playNextTrack();
}
playPersonalFM() {
this._isPersonalFM = true;
if (!this._enabled) this._enabled = true;
if (this._currentTrack.id !== this._personalFMTrack.id) {
this._replaceCurrentTrack(this._personalFMTrack.id);
}
this.playOrPause();
}
moveToFMTrash() {
this._isPersonalFM = true;
this.playNextTrack();
fmTrash(this._personalFMTrack.id);
}
sendSelfToIpcMain() {
if (process.env.IS_ELECTRON !== true) return false;

View File

@ -22,6 +22,13 @@
:subText="'copywriter'"
/>
</div>
<div class="index-row">
<div class="title"> For You </div>
<div class="for-you-row">
<FMCard />
<div></div>
</div>
</div>
<div class="index-row">
<div class="title">{{ $t("home.recommendArtist") }}</div>
<CoverRow
@ -61,12 +68,12 @@ import { byAppleMusic } from "@/utils/staticData";
import { newAlbums } from "@/api/album";
import NProgress from "nprogress";
import { mapState } from "vuex";
import CoverRow from "@/components/CoverRow.vue";
import FMCard from "@/components/FMCard.vue";
export default {
name: "Home",
components: { CoverRow },
components: { CoverRow, FMCard },
data() {
return {
show: false,
@ -164,4 +171,11 @@ footer {
justify-content: center;
margin-top: 48px;
}
.for-you-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin-bottom: 78px;
}
</style>

View File

@ -72,6 +72,7 @@
</div>
<div class="media-controls">
<button-icon
v-show="!player.isPersonalFM"
@click.native="playerRef.repeat"
:title="
player.repeatMode === 'one'
@ -91,11 +92,19 @@
</button-icon>
<div class="middle">
<button-icon
v-show="!player.isPersonalFM"
@click.native="playerRef.previous"
:title="$t('player.previous')"
><svg-icon icon-class="previous"
/></button-icon>
<button-icon
v-show="player.isPersonalFM"
@click.native="moveToFMTrash"
title="不喜欢"
><svg-icon icon-class="thumbs-down"
/></button-icon>
<button-icon
id="play"
@click.native="playerRef.play"
:title="$t(player.playing ? 'player.pause' : 'player.play')"
><svg-icon :icon-class="playerRef.playing ? 'pause' : 'play'"
@ -107,6 +116,7 @@
/></button-icon>
</div>
<button-icon
v-show="!player.isPersonalFM"
@click.native="playerRef.shuffle"
:title="$t('player.shuffle')"
:class="{ active: player.shuffle }"
@ -353,6 +363,9 @@ export default {
return `<span>${line.contents[0]}</span>`;
}
},
moveToFMTrash() {
this.player.moveToFMTrash();
},
},
watch: {
currentTrack() {
@ -470,7 +483,7 @@ $layoutBreakpoint: 1000px;
button {
margin: 0 8px;
}
button:nth-child(2) .svg-icon {
button#play .svg-icon {
height: 28px;
width: 28px;
padding: 2px;