feat: prettier task supported (#40)

* feat: add config to resolve path alias.

* feat: use vue-i18n for language switch

* feat: add .editorconfig for ide

* fix: add no-referrer to avoid CROB

* fix: setCookie and fix typo

* feat: integrate vue-i18n

* feat: player component i18n support

* fix: duplicate key warning in explore page

* fix: like songs number changed in library page

* fire: remove todo

* fix: same text search on enter will cause error

* fix: scrobble error params type

* feat: prettier task supported

* fix: prettier ignore config update

* fix: conflict
master
Hawtim Zhang 4 years ago committed by GitHub
parent 56fe497db9
commit c042faa001
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,4 @@
build
coverage
dist

@ -0,0 +1,13 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"jsxSingleQuote": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "strict"
}

@ -1,5 +1,3 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
presets: ["@vue/cli-plugin-babel/preset"],
};

@ -5,7 +5,13 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"lint": "vue-cli-service lint",
"prettier": "npx prettier --write ./src"
},
"husky": {
"hooks": {
"pre-commit": "npm run prettier"
}
},
"dependencies": {
"@sentry/browser": "^5.27.0",
@ -19,6 +25,7 @@
"js-cookie": "^2.2.1",
"nprogress": "^0.2.0",
"plyr": "^3.6.2",
"prettier": "2.1.2",
"register-service-worker": "^1.7.1",
"svg-sprite-loader": "^5.0.0",
"vue": "^2.6.11",
@ -38,6 +45,7 @@
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"husky": "^4.3.0",
"sass": "^1.26.11",
"sass-loader": "^10.0.2",
"vue-template-compiler": "^2.6.11"

@ -1,22 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="referrer" content="no-referrer" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

@ -31,7 +31,7 @@ export default {
components: {
Navbar,
Player,
GlobalEvents
GlobalEvents,
},
methods: {
play(e) {
@ -42,8 +42,8 @@ export default {
if (event.target.tagName === "INPUT") return false;
if (this.$route.name === "mv") return false;
return true;
}
}
},
},
};
</script>

@ -6,9 +6,9 @@ export function getArtist(id) {
url: "/artists",
method: "get",
params: {
id
}
}).then(data => {
id,
},
}).then((data) => {
data.hotSongs = mapTrackPlayableStatus(data.hotSongs);
return data;
});
@ -21,7 +21,7 @@ export function getArtistAlbum(params) {
return request({
url: "/artist/album",
method: "get",
params
params,
});
}
@ -35,8 +35,8 @@ export function toplistOfArtists(type = null) {
url: "/toplist/artist",
method: "get",
params: {
type
}
type,
},
});
}
@ -45,7 +45,7 @@ export function artistMv(id) {
url: "/artist/mv",
method: "get",
params: {
id
}
id,
},
});
}

@ -6,7 +6,7 @@ export function recommendPlaylist(params) {
return request({
url: "/personalized",
method: "get",
params
params,
});
}
export function dailyRecommendPlaylist(params) {
@ -14,7 +14,7 @@ export function dailyRecommendPlaylist(params) {
return request({
url: "/recommend/resource",
method: "get",
params
params,
});
}
@ -24,8 +24,8 @@ export function getPlaylistDetail(id, noCache = false) {
return request({
url: "/playlist/detail",
method: "get",
params
}).then(data => {
params,
}).then((data) => {
data.playlist.tracks = mapTrackPlayableStatus(data.playlist.tracks);
return data;
});
@ -38,7 +38,7 @@ export function highQualityPlaylist(params) {
return request({
url: "/top/playlist/highquality",
method: "get",
params
params,
});
}
@ -50,21 +50,21 @@ export function topPlaylist(params) {
return request({
url: "/top/playlist",
method: "get",
params
params,
});
}
export function playlistCatlist() {
return request({
url: "/playlist/catlist",
method: "get"
method: "get",
});
}
export function toplists() {
return request({
url: "/toplist",
method: "get"
method: "get",
});
}
@ -74,6 +74,6 @@ export function subscribePlaylist(params) {
return request({
url: "/playlist/subscribe",
method: "get",
params
params,
});
}

@ -1,41 +1,40 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
pointer-events: none;
}
#nprogress .bar {
background: #335eea;
background: #335eea;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #335eea,
0 0 5px #335eea;
opacity: 1.0;
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #335eea, 0 0 5px #335eea;
opacity: 1;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .bar {
position: absolute;
position: absolute;
}

File diff suppressed because it is too large Load Diff

@ -1,65 +1,63 @@
/* rail style */
.vue-slider-rail {
background-color: #eee;
border-radius: 15px;
background-color: #eee;
border-radius: 15px;
}
/* process style */
.vue-slider-process {
background-color: #335eea;
border-radius: 15px;
background-color: #335eea;
border-radius: 15px;
}
/* dot style */
.vue-slider-dot-handle {
cursor: pointer;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #fff;
box-sizing: border-box;
box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.12);
visibility: hidden;
cursor: pointer;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #fff;
box-sizing: border-box;
box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.12);
visibility: hidden;
}
/* tooltip style */
.vue-slider-dot-tooltip-wrapper {
opacity: 0;
transition: all 1s;
opacity: 0;
transition: all 1s;
}
.vue-slider-dot-tooltip-wrapper-show {
opacity: 1;
opacity: 1;
}
.vue-slider-dot-tooltip-inner {
font-size: 14px;
white-space: nowrap;
padding: 2px 6px;
min-width: 20px;
text-align: center;
color: #000;
border-radius: 5px;
border-color: #fff;
background-color: #fff;
box-sizing: content-box;
box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.08);
font-size: 14px;
white-space: nowrap;
padding: 2px 6px;
min-width: 20px;
text-align: center;
color: #000;
border-radius: 5px;
border-color: #fff;
background-color: #fff;
box-sizing: content-box;
box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.08);
}
/* hover */
.vue-slider:hover .vue-slider-dot-handle,
.vue-slider:active .vue-slider-dot-handle {
visibility: visible;
visibility: visible;
}
/* volume style */
.volume-control .vue-slider-process {
background-color: rgba(0, 0, 0, 0.8);
border-radius: 15px;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 15px;
}
.volume-control:hover .vue-slider-process {
background-color: #335eea;
}
background-color: #335eea;
}

@ -42,7 +42,7 @@ export default {
openMenu(e) {
this.showMenu = true;
this.$nextTick(
function() {
function () {
this.$refs.menu.focus();
this.setMenu(e.y, e.x);
}.bind(this)

@ -60,27 +60,27 @@ export default {
name: "CoverRow",
components: {
Cover,
ExplicitSymbol
ExplicitSymbol,
},
props: {
items: Array,
type: String,
subText: {
type: String,
default: "none"
default: "none",
},
imageSize: {
type: Number,
default: 512
default: 512,
},
showPlayButton: {
type: Boolean,
default: false
default: false,
},
showPlayCount: {
type: Boolean,
default: false
}
default: false,
},
},
methods: {
getUrl(item) {
@ -102,8 +102,8 @@ export default {
item.publishTime
).getFullYear()}`;
if (this.subText === "appleMusic") return "by Apple Music";
}
}
},
},
};
</script>

@ -54,13 +54,13 @@ import ButtonIcon from "@/components/ButtonIcon.vue";
export default {
name: "Navbar",
components: {
ButtonIcon
ButtonIcon,
},
data() {
return {
inputFocus: false,
keywords: "",
langs: ["zh-CN", "en"]
langs: ["zh-CN", "en"],
};
},
methods: {
@ -77,7 +77,7 @@ export default {
return;
this.$router.push({
name: "search",
query: { keywords: this.keywords }
query: { keywords: this.keywords },
});
},
changeLang() {
@ -89,8 +89,8 @@ export default {
}
this.$i18n.locale = lang;
this.$store.commit("changeLang", lang);
}
}
},
},
};
</script>

@ -131,13 +131,13 @@ export default {
name: "Player",
components: {
ButtonIcon,
VueSlider
VueSlider,
},
data() {
return {
interval: null,
progress: 0,
oldVolume: 0.5
oldVolume: 0.5,
};
},
created() {
@ -145,7 +145,7 @@ export default {
this.progress = ~~this.howler.seek();
}, 1000);
if (this.isLoggedIn) {
userLikedSongsIDs(this.settings.user.userId).then(data => {
userLikedSongsIDs(this.settings.user.userId).then((data) => {
this.updateLikedSongs(data.ids);
});
}
@ -162,7 +162,7 @@ export default {
set(value) {
this.updatePlayerState({ key: "volume", value });
Howler.volume(value);
}
},
},
playing() {
if (this.howler.state() === "loading") {
@ -176,7 +176,7 @@ export default {
},
isLoggedIn() {
return isLoggedIn();
}
},
},
methods: {
...mapMutations([
@ -184,13 +184,13 @@ export default {
"turnOffShuffleMode",
"updatePlayerState",
"updateRepeatStatus",
"updateLikedSongs"
"updateLikedSongs",
]),
...mapActions([
"nextTrack",
"previousTrack",
"playTrackOnListByID",
"addNextTrackEvent"
"addNextTrackEvent",
]),
play() {
if (this.playing) {
@ -259,7 +259,7 @@ export default {
if (this.liked.songs.includes(id)) like = false;
likeATrack({ id, like }).then(() => {
if (like === false) {
this.updateLikedSongs(this.liked.songs.filter(d => d !== id));
this.updateLikedSongs(this.liked.songs.filter((d) => d !== id));
} else {
let newLikeSongs = this.liked.songs;
newLikeSongs.push(id);
@ -272,7 +272,7 @@ export default {
this.$router.push({ path: "/library/liked-songs" });
else
this.$router.push({
path: "/" + this.player.listInfo.type + "/" + this.player.listInfo.id
path: "/" + this.player.listInfo.type + "/" + this.player.listInfo.id,
});
},
goToAlbum() {
@ -280,8 +280,8 @@ export default {
},
goToArtist(id) {
this.$router.push({ path: "/artist/" + id });
}
}
},
},
};
</script>

@ -27,7 +27,7 @@ import {
playPlaylistByID,
playAlbumByID,
playAList,
appendTrackToPlayerList
appendTrackToPlayerList,
} from "@/utils/play";
import TrackListItem from "@/components/TrackListItem.vue";
@ -37,7 +37,7 @@ export default {
name: "TrackList",
components: {
TrackListItem,
ContextMenu
ContextMenu,
},
props: {
tracks: Array,
@ -45,17 +45,17 @@ export default {
id: Number,
itemWidth: {
type: Number,
default: -1
default: -1,
},
dbclickTrackFunc: {
type: String,
default: "default"
}
default: "default",
},
},
data() {
return {
rightClickedTrack: null,
listStyles: {}
listStyles: {},
};
},
created() {
@ -66,7 +66,7 @@ export default {
...mapState(["liked"]),
isRightClickedTrackLiked() {
return this.liked.songs.includes(this.rightClickedTrack?.id);
}
},
},
methods: {
...mapMutations(["updateLikedSongs"]),
@ -95,7 +95,7 @@ export default {
} else if (this.type === "album") {
playAlbumByID(this.id, trackID);
} else if (this.type === "tracklist") {
let trackIDs = this.tracks.map(t => t.id);
let trackIDs = this.tracks.map((t) => t.id);
playAList(trackIDs, this.tracks[0].ar[0].id, "artist", trackID);
}
},
@ -112,17 +112,17 @@ export default {
let like = true;
let likedSongs = this.liked.songs;
if (likedSongs.includes(id)) like = false;
likeATrack({ id, like }).then(data => {
likeATrack({ id, like }).then((data) => {
if (data.code !== 200) return;
if (like === false) {
this.updateLikedSongs(likedSongs.filter(d => d !== id));
this.updateLikedSongs(likedSongs.filter((d) => d !== id));
} else {
likedSongs.push(id);
this.updateLikedSongs(likedSongs);
}
});
}
}
},
},
};
</script>

@ -21,7 +21,10 @@
track.no
}}</span>
<button v-show="isPlaying">
<svg-icon icon-class="volume" style="height:16px;width:16px"></svg-icon>
<svg-icon
icon-class="volume"
style="height: 16px; width: 16px"
></svg-icon>
</button>
</div>
<div class="title-and-artist">
@ -59,7 +62,7 @@
icon-class="heart"
:style="{
visibility:
focus && !isLiked && track.playable ? 'visible' : 'hidden'
focus && !isLiked && track.playable ? 'visible' : 'hidden',
}"
></svg-icon>
<svg-icon icon-class="heart-solid" v-show="isLiked"></svg-icon>
@ -81,7 +84,7 @@ export default {
name: "TrackListItem",
components: { ArtistsInLine, ExplicitSymbol },
props: {
track: Object
track: Object,
},
data() {
return { focus: false, trackStyle: {} };
@ -123,7 +126,7 @@ export default {
},
isLoggedIn() {
return isLoggedIn();
}
},
},
methods: {
goToAlbum() {
@ -134,12 +137,12 @@ export default {
},
likeThisSong() {
this.$parent.likeASong(this.track.id);
}
},
},
created() {
if (this.$parent.itemWidth !== -1)
this.trackStyle = { width: this.$parent.itemWidth + "px" };
}
},
};
</script>

@ -11,8 +11,8 @@ const i18n = new VueI18n({
locale: store.state.settings.lang,
messages: {
en,
"zh-CN": zhCN
}
"zh-CN": zhCN,
},
});
export default i18n;

@ -4,26 +4,26 @@ export default {
home: "Home",
explore: "Explore",
library: "Library",
search: "Search"
search: "Search",
},
footer: {
settings: "Settings"
settings: "Settings",
},
home: {
recommendPlaylist: "Recommended Playlists",
recommendArtist: "Recommended Artists",
newAlbum: "Latest Albums",
seeMore: "SEE MORE",
charts: "Charts"
charts: "Charts",
},
library: {
sLibrary: "'s Library",
likedSongs: "Liked Songs",
sLikedSongs: "'s LikedSongs"
sLikedSongs: "'s LikedSongs",
},
explore: {
explore: "Explore",
loadMore: "Load More"
loadMore: "Load More",
},
artist: {
latestRelease: "Latest Releases",
@ -34,14 +34,14 @@ export default {
albums: "Albums",
withAlbums: "Albums",
artist: "Artist",
videos: "Music Videos"
videos: "Music Videos",
},
album: {
released: "Released"
released: "Released",
},
playlist: {
playlist: "Playlists",
updatedAt: "Updated at"
updatedAt: "Updated at",
},
login: {
accessToAll: "Access to all data",
@ -62,14 +62,14 @@ export default {
loginWithPhone: "Login with Phone",
notice: `YesPlayMusic promises not to save any of your account information to the cloud.<br />
Your password will be MD5 encrypted locally and then transmitted to NetEase Cloud API.<br />
YesPlayMusic is not the official website of NetEase Cloud Music, please consider carefully before entering account information. You can also go to <a href="https://github.com/qier222/YesPlayMusic">YesPlayMusic's GitHub repository</a> to build and use the self-hosted NetEase Cloud Music API.`
YesPlayMusic is not the official website of NetEase Cloud Music, please consider carefully before entering account information. You can also go to <a href="https://github.com/qier222/YesPlayMusic">YesPlayMusic's GitHub repository</a> to build and use the self-hosted NetEase Cloud Music API.`,
},
mv: {
moreVideo: "More Videos"
moreVideo: "More Videos",
},
next: {
nowPlaying: "Now Playing",
nextUp: "Next Up"
nextUp: "Next Up",
},
player: {
like: "Like",
@ -80,10 +80,10 @@ export default {
play: "Play",
pause: "Pause",
mute: "Mute",
nextUp: "Next Up"
nextUp: "Next Up",
},
modal: {
close: "Close"
close: "Close",
},
search: {
artist: "Artists",
@ -92,9 +92,9 @@ export default {
mv: "MVs",
playlist: "Playlists",
noResult: "No Results",
searchFor: "Search for"
searchFor: "Search for",
},
common: {
songs: "Songs",
}
},
};

@ -4,26 +4,26 @@ export default {
home: "首页",
explore: "发现",
library: "资料库",
search: "搜索"
search: "搜索",
},
footer: {
settings: "设置"
settings: "设置",
},
home: {
recommendPlaylist: "推荐歌单",
recommendArtist: "推荐歌手",
newAlbum: "新专速递",
seeMore: "更多",
charts: "排行榜"
charts: "排行榜",
},
library: {
sLibrary: "的资料库",
likedSongs: "我喜欢的歌",
sLikedSongs: "喜欢的歌"
sLikedSongs: "喜欢的歌",
},
explore: {
explore: "发现",
loadMore: "加载更多"
loadMore: "加载更多",
},
artist: {
latestRelease: "最新发布",
@ -34,14 +34,14 @@ export default {
albums: "专辑",
withAlbums: "张专辑",
artist: "歌手",
videos: "个视频"
videos: "个视频",
},
album: {
released: "发行于"
released: "发行于",
},
playlist: {
playlist: "歌单",
updatedAt: "最后更新于"
updatedAt: "最后更新于",
},
login: {
accessToAll: "可访问全部数据",
@ -67,14 +67,14 @@ export default {
<a href="https://github.com/qier222/YesPlayMusic"
>YesPlayMusic GitHub 源代码仓库</a
>
自行构建并使用自托管的网易云 API`
自行构建并使用自托管的网易云 API`,
},
mv: {
moreVideo: "更多视频"
moreVideo: "更多视频",
},
next: {
nowPlaying: "正在播放",
nextUp: "即将播放"
nextUp: "即将播放",
},
player: {
like: "喜欢",
@ -85,10 +85,10 @@ export default {
play: "播放",
pause: "暂停",
mute: "静音",
nextUp: "播放列表"
nextUp: "播放列表",
},
modal: {
close: "关闭"
close: "关闭",
},
search: {
artist: "歌手",
@ -97,9 +97,9 @@ export default {
mv: "视频",
playlist: "歌单",
noResult: "暂无结果",
searchFor: "搜索"
searchFor: "搜索",
},
common: {
songs: "首歌",
}
},
};

@ -15,7 +15,7 @@ import { Integrations } from "@sentry/tracing";
Vue.use(VueAnalytics, {
id: "UA-180189423-1",
router
router,
});
Vue.config.productionTip = false;
@ -30,14 +30,14 @@ if (process.env.VUE_APP_ENABLE_SENTRY === "true") {
integrations: [
new VueIntegration({
Vue,
tracing: true
tracing: true,
}),
new Integrations.BrowserTracing()
new Integrations.BrowserTracing(),
],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0
tracesSampleRate: 1.0,
});
}
@ -45,5 +45,5 @@ new Vue({
i18n,
store,
router,
render: h => h(App)
render: (h) => h(App),
}).$mount("#app");

@ -1,32 +1,34 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker'
import { register } from "register-service-worker";
if (process.env.NODE_ENV === 'production') {
if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
ready() {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
"App is being served from cache by a service worker.\n" +
"For more details, visit https://goo.gl/AFskqB"
);
},
registered () {
console.log('Service worker has been registered.')
registered() {
console.log("Service worker has been registered.");
},
cached () {
console.log('Content has been cached for offline use.')
cached() {
console.log("Content has been cached for offline use.");
},
updatefound () {
console.log('New content is downloading.')
updatefound() {
console.log("New content is downloading.");
},
updated () {
console.log('New content is available; please refresh.')
updated() {
console.log("New content is available; please refresh.");
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
offline() {
console.log(
"No internet connection found. App is running in offline mode."
);
},
error(error) {
console.error("Error during service worker registration:", error);
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
});
}

@ -4,27 +4,27 @@ import { isLoggedIn } from "@/utils/auth";
export default {
switchTrack({ state, dispatch, commit }, basicTrack) {
getTrackDetail(basicTrack.id).then(data => {
getTrackDetail(basicTrack.id).then((data) => {
let track = data.songs[0];
track.sort = basicTrack.sort;
// 获取当前的播放时间。初始化为 loading 状态时返回 howler 的实例而不是浮点数时间,比如 1.332
let time = state.howler.seek();
let currentTime = 0
let currentTime = 0;
if (time === 0) {
// state.howler._duration 可以获得当前实例的播放时长
currentTime = 180
currentTime = 180;
}
if (time.toString() === '[object Object]') {
currentTime = 0
if (time.toString() === "[object Object]") {
currentTime = 0;
}
if (time > 0) {
currentTime = time
currentTime = time;
}
scrobble({
id: state.player.currentTrack.id,
sourceid: state.player.listInfo.id,
time: currentTime
})
time: currentTime,
});
commit("updateCurrentTrack", track);
updateMediaSessionMetaData(track);
document.title = `${track.name} · ${track.ar[0].name} - YesPlayMusic`;
@ -42,7 +42,7 @@ export default {
}
if (isLoggedIn) {
getMP3(track.id).then(data => {
getMP3(track.id).then((data) => {
commitMP3(data.data[0].url.replace(/^http:/, "https:"));
});
} else {
@ -53,24 +53,24 @@ export default {
playFirstTrackOnList({ state, dispatch }) {
dispatch(
"switchTrack",
state.player.list.find(t => t.sort === 0)
state.player.list.find((t) => t.sort === 0)
);
},
playTrackOnListByID({ state, commit, dispatch }, trackID) {
let track = state.player.list.find(t => t.id === trackID);
let track = state.player.list.find((t) => t.id === trackID);
dispatch("switchTrack", track);
if (state.player.shuffle) {
// 当随机模式开启时双击列表的一首歌进行播放此时要把这首歌的sort调到第一(0),这样用户就能随机播放完整的歌单
let otherTrack = state.player.list.find(t => t.sort === 0);
let otherTrack = state.player.list.find((t) => t.sort === 0);
commit("switchSortBetweenTwoTracks", {
trackID1: track.id,
trackID2: otherTrack.id
trackID2: otherTrack.id,
});
}
},
nextTrack({ state, dispatch }, realNext = false) {
let nextTrack = state.player.list.find(
track => track.sort === state.player.currentTrack.sort + 1
(track) => track.sort === state.player.currentTrack.sort + 1
);
if (state.player.repeat === "one" && realNext === false) {
@ -79,7 +79,7 @@ export default {
if (nextTrack === undefined) {
if (state.player.repeat !== "off") {
nextTrack = state.player.list.find(t => t.sort === 0);
nextTrack = state.player.list.find((t) => t.sort === 0);
} else {
return;
}
@ -89,13 +89,13 @@ export default {
},
previousTrack({ state, dispatch }) {
let previousTrack = state.player.list.find(
track => track.sort === state.player.currentTrack.sort - 1
(track) => track.sort === state.player.currentTrack.sort - 1
);
if (previousTrack == undefined) {
if (state.player.repeat !== "off") {
previousTrack = state.player.list.reduce((x, y) => (x > y ? x : y));
} else {
previousTrack = state.player.list.find(t => t.sort === 0);
previousTrack = state.player.list.find((t) => t.sort === 0);
}
}
dispatch("switchTrack", previousTrack);
@ -104,5 +104,5 @@ export default {
state.howler.once("end", () => {
dispatch("nextTrack");
});
}
},
};

@ -13,7 +13,7 @@ if (localStorage.getItem("appVersion") === null) {
window.location.reload();
}
const saveToLocalStorage = store => {
const saveToLocalStorage = (store) => {
store.subscribe((mutation, state) => {
// console.log(mutation);
localStorage.setItem("player", JSON.stringify(state.player));
@ -26,15 +26,15 @@ const store = new Vuex.Store({
state: state,
mutations,
actions,
plugins: [saveToLocalStorage]
plugins: [saveToLocalStorage],
});
store.state.howler = new Howl({
src: [
`https://music.163.com/song/media/outer/url?id=${store.state.player.currentTrack.id}`
`https://music.163.com/song/media/outer/url?id=${store.state.player.currentTrack.id}`,
],
html5: true,
format: ["mp3"]
format: ["mp3"],
});
Howler.volume(store.state.player.volume);

@ -1,11 +1,11 @@
const initState = {
howler: null,
liked: {
songs: []
songs: [],
},
contextMenu: {
clickObjectID: 0,
showMenu: false
showMenu: false,
},
player: {
enable: false,
@ -26,51 +26,51 @@ const initState = {
"https://p1.music.126.net/kHNNN-VxufjlBtyNPIP3kg==/109951165306614548.jpg",
tns: [],
pic_str: "109951165306614548",
pic: 109951165306614540
pic: 109951165306614540,
},
time: 196022,
playable: true
playable: true,
},
notShuffledList: [],
list: [],
listInfo: {
type: "",
id: ""
}
id: "",
},
},
settings: {
playlistCategories: [
{
name: "全部",
enable: true
enable: true,
},
{
name: "推荐歌单",
enable: true
enable: true,
},
{
name: "精品歌单",
enable: true
enable: true,
},
{
name: "官方",
enable: true
enable: true,
},
{
name: "流行",
enable: true
enable: true,
},
{
name: "电子",
enable: true
enable: true,
},
{
name: "摇滚",
enable: true
enable: true,
},
{
name: "ACG",
enable: true
enable: true,
},
// {
// name: "最新专辑",
@ -78,14 +78,14 @@ const initState = {
// },
{
name: "排行榜",
enable: true
}
enable: true,
},
],
user: {
id: 0
id: 0,
},
lang: null
}
lang: null,
},
};
export default initState;

@ -13,7 +13,7 @@ export default {
state.howler = new Howl({
src: [mp3],
autoplay: true,
html5: true
html5: true,
});
state.howler.play();
},
@ -28,7 +28,7 @@ export default {
state.player.repeat = status;
},
appendTrackToPlayerList(state, { track, playNext = false }) {
let existTrack = state.player.list.find(t => t.id === track.id);
let existTrack = state.player.list.find((t) => t.id === track.id);
if (
(existTrack === null || existTrack === undefined) &&
playNext === false
@ -38,7 +38,7 @@ export default {
}
// 把track加入到正在播放歌曲的下一首位置
state.player.list = state.player.list.map(t => {
state.player.list = state.player.list.map((t) => {
if (t.sort > state.player.currentTrack.sort) {
t.sort = t.sort + 1;
}
@ -54,10 +54,10 @@ export default {
state.player.shuffle = true;
let newSorts = shuffleAList(
state.player.list.filter(t => t.sort > state.player.currentTrack.sort)
state.player.list.filter((t) => t.sort > state.player.currentTrack.sort)
);
state.player.list = state.player.list.map(track => {
state.player.list = state.player.list.map((track) => {
if (newSorts[track.id] !== undefined) track.sort = newSorts[track.id];
return track;
});
@ -68,7 +68,7 @@ export default {
JSON.stringify(state.player.notShuffledList)
);
state.player.currentTrack.sort = state.player.list.find(
t => t.id === state.player.currentTrack.id
(t) => t.id === state.player.currentTrack.id
).sort;
},
shuffleTheListBeforePlay(state) {
@ -76,7 +76,7 @@ export default {
JSON.stringify(state.player.list)
);
let newSorts = shuffleAList(state.player.list);
state.player.list = state.player.list.map(track => {
state.player.list = state.player.list.map((track) => {
track.sort = newSorts[track.id];
return track;
});
@ -91,10 +91,10 @@ export default {
state.liked.songs = trackIDs;
},
switchSortBetweenTwoTracks(state, { trackID1, trackID2 }) {
let t1 = state.player.list.find(t => t.id === trackID1);
let t2 = state.player.list.find(t => t.id === trackID2);
let t1 = state.player.list.find((t) => t.id === trackID1);
let t2 = state.player.list.find((t) => t.id === trackID2);
let sorts = [t1.sort, t2.sort];
state.player.list = state.player.list.map(t => {
state.player.list = state.player.list.map((t) => {
if (t.id === t1.id) t.sort = sorts[1];
if (t.id === t2.id) t.sort = sorts[0];
return t;
@ -102,5 +102,5 @@ export default {
},
changeLang(state, lang) {
state.settings.lang = lang;
}
},
};

@ -4,7 +4,7 @@ import store from "@/store";
export function isTrackPlayable(track) {
let result = {
playable: true,
reason: ""
reason: "",
};
if (track.fee === 1 || track.privilege?.fee === 1) {
if (isLoggedIn && store.state.settings.user.vipType === 11) {
@ -27,7 +27,7 @@ export function isTrackPlayable(track) {
}
export function mapTrackPlayableStatus(tracks) {
return tracks.map(t => {
return tracks.map((t) => {
let result = isTrackPlayable(t);
t.playable = result.playable;
t.reason = result.reason;
@ -47,13 +47,13 @@ export function randomNum(minNum, maxNum) {
}
export function shuffleAList(list) {
let sortsList = list.map(t => t.sort);
let sortsList = list.map((t) => t.sort);
for (let i = 1; i < sortsList.length; i++) {
const random = Math.floor(Math.random() * (i + 1));
[sortsList[i], sortsList[random]] = [sortsList[random], sortsList[i]];
}
let newSorts = {};
list.map(track => {
list.map((track) => {
newSorts[track.id] = sortsList.pop();
});
return newSorts;
@ -65,6 +65,8 @@ export function throttle(fn, time) {
if (isRun) return;
isRun = true;
fn.apply(this, arguments);
setTimeout(() => { isRun = false }, time);
}
setTimeout(() => {
isRun = false;
}, time);
};
}

@ -12,10 +12,7 @@ Vue.filter("formatTime", (Milliseconds, format = "HH:MM:SS") => {
let time = dayjs.duration(Milliseconds);
let hours = time.hours().toString();
let mins = time.minutes().toString();
let seconds = time
.seconds()
.toString()
.padStart(2, "0");
let seconds = time.seconds().toString().padStart(2, "0");
if (format === "HH:MM:SS") {
return hours !== "0"
@ -24,7 +21,9 @@ Vue.filter("formatTime", (Milliseconds, format = "HH:MM:SS") => {
} else if (format === "Human") {
const hoursUnit = locale.locale === "zh-CN" ? "小时" : "hr";
const minitesUnit = locale.locale === "zh-CN" ? "分钟" : "min";
return hours !== "0" ? `${hours} ${hoursUnit} ${mins} ${minitesUnit}` : `${mins} ${minitesUnit}`;
return hours !== "0"
? `${hours} ${hoursUnit} ${mins} ${minitesUnit}`
: `${mins} ${minitesUnit}`;
}
});
@ -56,7 +55,7 @@ Vue.filter("resizeImage", (imgUrl, size = 512) => {
return `${httpsImgUrl}?param=${size}y${size}`;
});
Vue.filter("formatPlayCount", count => {
Vue.filter("formatPlayCount", (count) => {
if (!count) return "";
if (locale.locale === "zh-CN") {
if (count > 100000000) {
@ -83,7 +82,7 @@ Vue.filter("formatPlayCount", count => {
}
});
Vue.filter("toHttps", url => {
Vue.filter("toHttps", (url) => {
if (!url) return "";
return url.replace(/^http:/, "https:");
});

@ -2,16 +2,16 @@ import store from "@/store";
export function initMediaSession() {
if ("mediaSession" in navigator) {
navigator.mediaSession.setActionHandler("play", function() {
navigator.mediaSession.setActionHandler("play", function () {
store.state.howler.play();
});
navigator.mediaSession.setActionHandler("pause", function() {
navigator.mediaSession.setActionHandler("pause", function () {
store.state.howler.pause();
});
navigator.mediaSession.setActionHandler("previoustrack", function() {
navigator.mediaSession.setActionHandler("previoustrack", function () {
store.dispatch("previousTrack");
});
navigator.mediaSession.setActionHandler("nexttrack", function() {
navigator.mediaSession.setActionHandler("nexttrack", function () {
store.dispatch("nextTrack");
});
navigator.mediaSession.setActionHandler("stop", () => {

@ -3,18 +3,18 @@ import axios from "axios";
const service = axios.create({
baseURL: process.env.VUE_APP_NETEASE_API_URL,
withCredentials: true,
timeout: 15000
timeout: 15000,
});
const errors = new Map([
[401, "The token you are using has expired."],
[502, null],
[301, "You must login to use this feature."],
[-1, "An unexpected error has occurred: "]
[-1, "An unexpected error has occurred: "],
]);
service.interceptors.response.use(
response => {
(response) => {
const res = response.data;
if (res.code !== 200) {
@ -30,7 +30,7 @@ service.interceptors.response.use(
return res;
}
},
error => {
(error) => {
const errMsg = `error: ${error}`;
console.log(errMsg);

@ -27,13 +27,14 @@
<span :title="album.publishTime | formatDate">{{
new Date(album.publishTime).getFullYear()
}}</span>
<span> · {{ album.size }} {{ $t("common.songs") }}</span>,
<span> · {{ album.size }} {{ $t("common.songs") }}</span
>,
{{ albumTime | formatTime("Human") }}
</div>
<div class="description" @click="showFullDescription = true">
{{ album.description }}
</div>
<div class="buttons" style="margin-top:32px">
<div class="buttons" style="margin-top: 32px">
<ButtonTwoTone
@click.native="playAlbumByID(album.id)"
:iconClass="`play`"
@ -63,7 +64,7 @@
<div class="description-full" @click.stop>
<span>{{ album.description }}</span>
<span class="close" @click="showFullDescription = false">
{{ $t('modal.close') }}
{{ $t("modal.close") }}
</span>
</div>
</div>
@ -89,7 +90,7 @@ export default {
Cover,
ButtonTwoTone,
TrackList,
ExplicitSymbol
ExplicitSymbol,
},
data() {
return {
@ -97,27 +98,27 @@ export default {
id: 0,
picUrl: "",
artist: {
id: 0
}
id: 0,
},
},
tracks: [],
showFullDescription: false,
show: false
show: false,
};
},
created() {
getAlbum(this.$route.params.id)
.then(data => {
.then((data) => {
this.album = data.album;
this.tracks = data.songs;
NProgress.done();
this.show = true;
return this.tracks;
})
.then(tracks => {
.then((tracks) => {
// to get explicit mark
let trackIDs = tracks.map(t => t.id);
getTrackDetail(trackIDs.join(",")).then(data => {
let trackIDs = tracks.map((t) => t.id);
getTrackDetail(trackIDs.join(",")).then((data) => {
this.tracks = data.songs;
});
});
@ -126,20 +127,20 @@ export default {
...mapState(["player"]),
albumTime() {
let time = 0;
this.tracks.map(t => (time = time + t.dt));
this.tracks.map((t) => (time = time + t.dt));
return time;
}
},
},
methods: {
...mapMutations(["appendTrackToPlayerList"]),
...mapActions(["playFirstTrackOnList", "playTrackOnListByID"]),
playAlbumByID(id, trackID = "first") {
if (this.tracks.find(t => t.playable !== false) === undefined) {
if (this.tracks.find((t) => t.playable !== false) === undefined) {
return;
}
playAlbumByID(id, trackID);
}
}
},
},
};
</script>

@ -8,7 +8,7 @@
<div class="name">{{ artist.name }}</div>
<div class="artist">{{ $t("artist.artist") }}</div>
<div class="statistics">
{{ artist.musicSize }} {{ $t("common.songs") }} ·
{{ artist.musicSize }} {{ $t("common.songs") }} ·
{{ artist.albumSize }} {{ $t("artist.withAlbums") }} ·
{{ artist.mvSize }} {{ $t("artist.videos") }}
</div>
@ -112,7 +112,7 @@ export default {
show: false,
artist: {
img1v1Url:
"https://p1.music.126.net/VnZiScyynLG7atLIZ2YPkw==/18686200114669622.jpg"
"https://p1.music.126.net/VnZiScyynLG7atLIZ2YPkw==/18686200114669622.jpg",
},
popularTracks: [],
albumsData: [],
@ -122,52 +122,52 @@ export default {
id: 0,
name: "",
type: "",
size: ""
size: "",
},
showMorePopTracks: false,
mvs: []
mvs: [],
};
},
computed: {
...mapState(["player"]),
albums() {
return this.albumsData.filter(a => a.type === "专辑");
return this.albumsData.filter((a) => a.type === "专辑");
},
eps() {
return this.albumsData.filter(a =>
return this.albumsData.filter((a) =>
["EP/Single", "EP", "Single"].includes(a.type)
);
}
},
},
methods: {
...mapMutations(["appendTrackToPlayerList"]),
...mapActions(["playFirstTrackOnList", "playTrackOnListByID"]),
loadData(id, next = undefined) {
getArtist(id).then(data => {
getArtist(id).then((data) => {
this.artist = data.artist;
this.popularTracks = data.hotSongs;
if (next !== undefined) next();
NProgress.done();
this.show = true;
});
getArtistAlbum({ id: id, limit: 200 }).then(data => {
getArtistAlbum({ id: id, limit: 200 }).then((data) => {
this.albumsData = data.hotAlbums;
this.latestRelease = data.hotAlbums[0];
});
artistMv(id).then(data => {
artistMv(id).then((data) => {
this.mvs = data.mvs;
});
},
goToAlbum(id) {
this.$router.push({
name: "album",
params: { id }
params: { id },
});
},
playPopularSongs(trackID = "first") {
let trackIDs = this.popularTracks.map(t => t.id);
let trackIDs = this.popularTracks.map((t) => t.id);
playAList(trackIDs, this.artist.id, "artist", trackID);
}
},
},
created() {
this.loadData(this.$route.params.id);
@ -186,7 +186,7 @@ export default {
this.artist.img1v1Url =
"https://p1.music.126.net/VnZiScyynLG7atLIZ2YPkw==/18686200114669622.jpg";
this.loadData(to.params.id, next);
}
},
};
</script>

@ -1,6 +1,6 @@
<template>
<div class="explore">
<h1>{{ $t('explore.explore') }}</h1>
<h1>{{ $t("explore.explore") }}</h1>
<div class="buttons">
<div
class="button"
@ -35,7 +35,7 @@
@click.native="getPlaylist"
color="grey"
:loading="loadingMore"
>{{ $t('explore.loadMore') }}</ButtonTwoTone
>{{ $t("explore.loadMore") }}</ButtonTwoTone
>
</div>
</div>
@ -102,28 +102,26 @@ export default {
getPlaylist() {
this.loadingMore = true;
if (this.activeCategory === "推荐歌单") {
return this.getRecommendPlayList()
return this.getRecommendPlayList();
}
if (this.activeCategory === "精品歌单") {
return this.getHighQualityPlaylist()
return this.getHighQualityPlaylist();
}
if (this.activeCategory === "排行榜") {
return this.getTopLists()
return this.getTopLists();
}
return this.getTopPlayList()
return this.getTopPlayList();
},
getRecommendPlayList() {
recommendPlaylist({ limit: 100 }).then(data => {
this.playlists = []
recommendPlaylist({ limit: 100 }).then((data) => {
this.playlists = [];
this.updatePlaylist(data.result);
});
},
getHighQualityPlaylist() {
let playlists = this.playlists;
let before =
playlists.length !== 0
? playlists[playlists.length - 1].updateTime
: 0;
playlists.length !== 0 ? playlists[playlists.length - 1].updateTime : 0;
highQualityPlaylist({ limit: 50, before }).then((data) => {
this.updatePlaylist(data.playlists);
this.hasMore = data.more;
@ -131,7 +129,7 @@ export default {
},
getTopLists() {
toplists().then((data) => {
this.playlists = []
this.playlists = [];
this.updatePlaylist(data.list);
});
},
@ -139,11 +137,11 @@ export default {
topPlaylist({
cat: this.activeCategory,
offset: this.playlists.length,
}).then(data => {
}).then((data) => {
this.updatePlaylist(data.playlists);
this.hasMore = data.more;
});
}
},
},
activated() {
this.loadData();

@ -1,9 +1,7 @@
<template>
<div class="home" v-show="show">
<div class="index-row">
<div class="title">
by Apple Music
</div>
<div class="title"> by Apple Music </div>
<CoverRow
:type="'playlist'"
:items="byAppleMusic"
@ -70,36 +68,36 @@ export default {
newReleasesAlbum: { items: [] },
topList: {
items: [],
ids: [19723756, 180106, 60198, 3812895, 60131]
ids: [19723756, 180106, 60198, 3812895, 60131],
},
recommendArtists: {
items: [],
indexs: []
}
indexs: [],
},
};
},
computed: {
byAppleMusic() {
return byAppleMusic;
}
},
},
methods: {
loadData() {
if (!this.show) NProgress.start();
recommendPlaylist({
limit: 10
}).then(data => {
limit: 10,
}).then((data) => {
this.recommendPlaylist.items = data.result;
NProgress.done();
this.show = true;
});
newAlbums({
area: "EA",
limit: 10
}).then(data => {
limit: 10,
}).then((data) => {
this.newReleasesAlbum.items = data.albums;
});
toplistOfArtists(2).then(data => {
toplistOfArtists(2).then((data) => {
let indexs = [];
while (indexs.length < 5) {
let tmp = ~~(Math.random() * 100);
@ -110,16 +108,16 @@ export default {
indexs.includes(index)
);
});
toplists().then(data => {
this.topList.items = data.list.filter(l =>
toplists().then((data) => {
this.topList.items = data.list.filter((l) =>
this.topList.ids.includes(l.id)
);
});
}
},
},
activated() {
this.loadData();
}
},
};
</script>

@ -76,23 +76,23 @@ export default {
user: {
profile: {
avatarUrl: "",
nickname: ""
}
nickname: "",
},
},
playlists: [],
hasMorePlaylists: true,
likedSongsPlaylist: {
id: 0,
trackCount: 0
trackCount: 0,
},
likedSongs: [],
likedSongIDs: [],
lyric: undefined
lyric: undefined,
};
},
created() {
NProgress.start();
userDetail(this.settings.user.userId).then(data => {
userDetail(this.settings.user.userId).then((data) => {
this.user = data;
});
},
@ -107,7 +107,7 @@ export default {
pickedLyric() {
if (this.lyric === undefined) return "";
let lyric = this.lyric.split("\n");
lyric = lyric.filter(l => {
lyric = lyric.filter((l) => {
if (l.includes("作词") || l.includes("作曲")) {
return false;
}
@ -120,9 +120,9 @@ export default {
return [
lyric[lineIndex].split("]")[1],
lyric[lineIndex + 1].split("]")[1],
lyric[lineIndex + 2].split("]")[1]
lyric[lineIndex + 2].split("]")[1],
];
}
},
},
methods: {
playLikedSongs() {
@ -141,8 +141,8 @@ export default {
userPlaylist({
uid: this.settings.user.userId,
offset: this.playlists.length === 0 ? 0 : this.playlists.length - 1,
timestamp: new Date().getTime()
}).then(data => {
timestamp: new Date().getTime(),
}).then((data) => {
if (replace) {
this.playlists = data.playlist;
} else {
@ -153,11 +153,11 @@ export default {
},
getLikedSongs(getLyric = true) {
getPlaylistDetail(this.settings.user.likedSongPlaylistID, true).then(
data => {
(data) => {
this.likedSongsPlaylist = data.playlist;
let TrackIDs = data.playlist.trackIds.slice(0, 20).map(t => t.id);
let TrackIDs = data.playlist.trackIds.slice(0, 20).map((t) => t.id);
this.likedSongIDs = TrackIDs;
getTrackDetail(this.likedSongIDs.join(",")).then(data => {
getTrackDetail(this.likedSongIDs.join(",")).then((data) => {
this.likedSongs = data.songs;
NProgress.done();
this.show = true;
@ -169,16 +169,16 @@ export default {
getRandomLyric() {
getLyric(
this.likedSongIDs[randomNum(0, this.likedSongIDs.length - 1)]
).then(data => {
).then((data) => {
if (data.lrc !== undefined) this.lyric = data.lrc.lyric;
});
}
},
},
watch: {
likedSongsInState() {
this.getLikedSongs(false);
}
}
},
},
};
</script>

@ -14,8 +14,8 @@
>
<div class="container" :class="{ active: activeCard === 1 }">
<div class="title-info">
<div class="title">{{ $t('login.loginText') }}</div>
<div class="info">{{ $t('login.accessToAll') }}</div>
<div class="title">{{ $t("login.loginText") }}</div>
<div class="info">{{ $t("login.accessToAll") }}</div>
</div>
<svg-icon icon-class="arrow-right"></svg-icon>
</div>
@ -28,8 +28,8 @@
>
<div class="container" :class="{ active: activeCard === 2 }">
<div class="title-info">
<div class="title">{{ $t('login.search') }}</div>
<div class="info">{{ $t('login.readonly') }}</div>
<div class="title">{{ $t("login.search") }}</div>
<div class="info">{{ $t("login.readonly") }}</div>
</div>
<svg-icon icon-class="arrow-right"></svg-icon>
</div>

@ -102,7 +102,7 @@ export default {
email: "",
password: "",
smsCode: "",
inputFocus: ""
inputFocus: "",
};
},
created() {
@ -118,11 +118,11 @@ export default {
Cookies.set("loginMode", "account", { expires: 3650 });
userPlaylist({
uid: this.$store.state.settings.user.userId,
limit: 1
}).then(data => {
limit: 1,
}).then((data) => {
this.updateUserInfo({
key: "likedSongPlaylistID",
value: data.playlist[0].id
value: data.playlist[0].id,
});
this.$router.push({ path: "/library" });
});
@ -143,15 +143,15 @@ export default {
countrycode: this.countryCode.replace("+", "").replace(/\s/g, ""),
phone: this.phoneNumber.replace(/\s/g, ""),
password: "fakePassword",
md5_password: md5(this.password).toString()
md5_password: md5(this.password).toString(),
})
.then(data => {
.then((data) => {
if (data.code !== 502) {
this.updateUser(data.profile);
this.afterLogin();
}
})
.catch(error => {
.catch((error) => {
this.processing = false;
alert(error);
});
@ -169,21 +169,21 @@ export default {
loginWithEmail({
email: this.email.replace(/\s/g, ""),
password: "fakePassword",
md5_password: md5(this.password).toString()
md5_password: md5(this.password).toString(),
})
.then(data => {
.then((data) => {
if (data.code !== 502) {
this.updateUser(data.profile);
this.afterLogin();
}
})
.catch(error => {
.catch((error) => {
this.processing = false;
alert(error);
});
}
}
}
},
},
};
</script>

@ -1,7 +1,7 @@
<template>
<div class="login">
<div>
<div class="title">{{ $t('login.usernameLogin') }}</div>
<div class="title">{{ $t("login.usernameLogin") }}</div>
<div class="sestion">
<div class="search-box">
<div class="container">
@ -53,7 +53,7 @@ import NProgress from "nprogress";
import { search } from "@/api/others";
import Cookies from "js-cookie";
import { userPlaylist } from "@/api/user";
import { throttle } from '@/utils/common';
import { throttle } from "@/utils/common";
import ButtonTwoTone from "@/components/ButtonTwoTone.vue";
@ -97,7 +97,7 @@ export default {
},
throttleSearch: throttle(function () {
this.search();
}, 500)
}, 500),
},
};
</script>

@ -1,6 +1,6 @@
<template>
<div class="newAlbum">
<h1>{{ $t("home.newAlbum")}}</h1>
<h1>{{ $t("home.newAlbum") }}</h1>
<div class="playlist-row">
<div class="playlists">

@ -24,11 +24,11 @@ import TrackList from "@/components/TrackList.vue";
export default {
name: "Next",
components: {
TrackList
TrackList,
},
data() {
return {
tracks: []
tracks: [],
};
},
computed: {
@ -41,58 +41,60 @@ export default {
},
sortedTracks() {
function compare(property) {
return function(obj1, obj2) {
return function (obj1, obj2) {
var value1 = obj1[property];
var value2 = obj2[property];
return value1 - value2;
};
}
return this.tracks
.filter(t => this.player.list.find(t2 => t2.id === t.id) !== undefined)
.filter(t => t.sort > this.player.currentTrack.sort)
.filter(
(t) => this.player.list.find((t2) => t2.id === t.id) !== undefined
)
.filter((t) => t.sort > this.player.currentTrack.sort)
.sort(compare("sort"));
}
},
},
watch: {
currentTrack() {
this.loadTracks();
},
playerShuffle() {
this.tracks = this.tracks.map(t => {
t.sort = this.player.list.find(t2 => t.id === t2.id).sort;
this.tracks = this.tracks.map((t) => {
t.sort = this.player.list.find((t2) => t.id === t2.id).sort;
return t;
});
}
},
},
methods: {
...mapActions(["playTrackOnListByID"]),
loadTracks() {
console.time("loadTracks");
let loadedTrackIDs = this.tracks.map(t => t.id);
let loadedTrackIDs = this.tracks.map((t) => t.id);
let basicTracks = this.player.list
.filter(
t =>
(t) =>
t.sort > this.player.currentTrack.sort &&
t.sort <= this.player.currentTrack.sort + 100
)
.filter(t => loadedTrackIDs.includes(t.id) === false);
.filter((t) => loadedTrackIDs.includes(t.id) === false);
let trackIDs = basicTracks.map(t => t.id);
let trackIDs = basicTracks.map((t) => t.id);
if (trackIDs.length > 0) {
getTrackDetail(trackIDs.join(",")).then(data => {
let newTracks = data.songs.map(t => {
t.sort = this.player.list.find(t2 => t2.id == t.id).sort;
getTrackDetail(trackIDs.join(",")).then((data) => {
let newTracks = data.songs.map((t) => {
t.sort = this.player.list.find((t2) => t2.id == t.id).sort;
return t;
});
this.tracks.push(...newTracks);
});
}
console.timeEnd("loadTracks");
}
},
},
activated() {
this.loadTracks();
}
},
};
</script>

@ -13,31 +13,29 @@
<div class="artist">
Playlist by
<span
style="font-weight:600"
style="font-weight: 600"
v-if="
[
5277771961,
5277965913,
5277969451,
5277778542,
5278068783
5278068783,
].includes(playlist.id)
"
>Apple Music</span
>
<a
v-else
:href="
`https://music.163.com/#/user/home?id=${playlist.creator.userId}`
"
:href="`https://music.163.com/#/user/home?id=${playlist.creator.userId}`"
target="blank"
>{{ playlist.creator.nickname }}</a
>
</div>
<div class="date-and-count">
{{ $t("playlist.updatedAt") }}
{{ playlist.updateTime | formatDate }} ·
{{ playlist.trackCount }} {{ $t("common.songs") }}
{{ playlist.updateTime | formatDate }} · {{ playlist.trackCount }}
{{ $t("common.songs") }}
</div>
<div class="description" @click="showFullDescription = true">
{{ playlist.description }}
@ -80,7 +78,7 @@
<div class="description-full" @click.stop>
<span>{{ playlist.description }}</span>
<span class="close" @click="showFullDescription = false">
{{ $t('modal.close') }}
{{ $t("modal.close") }}
</span>
</div>
</div>
@ -105,7 +103,7 @@ export default {
components: {
Cover,
ButtonTwoTone,
TrackList
TrackList,
},
data() {
return {
@ -113,14 +111,14 @@ export default {
playlist: {
coverImgUrl: "",
creator: {
userId: ""
userId: "",
},
trackIds: []
trackIds: [],
},
showFullDescription: false,
tracks: [],
loadingMore: false,
lastLoadedTrackIndex: 9
lastLoadedTrackIndex: 9,
};
},
created() {
@ -140,23 +138,23 @@ export default {
},
isLikeSongsPage() {
return this.$route.name === "likedSongs";
}
},
},
methods: {
...mapMutations(["appendTrackToPlayerList"]),
...mapActions(["playFirstTrackOnList", "playTrackOnListByID"]),
playPlaylistByID(trackID = "first") {
let trackIDs = this.playlist.trackIds.map(t => t.id);
let trackIDs = this.playlist.trackIds.map((t) => t.id);
playAList(trackIDs, this.playlist.id, "playlist", trackID);
},
likePlaylist() {
subscribePlaylist({
id: this.playlist.id,
t: this.playlist.subscribed ? 2 : 1
}).then(data => {
t: this.playlist.subscribed ? 2 : 1,
}).then((data) => {
if (data.code === 200)
this.playlist.subscribed = !this.playlist.subscribed;
getPlaylistDetail(this.id, true).then(data => {
getPlaylistDetail(this.id, true).then((data) => {
this.playlist = data.playlist;
});
});
@ -164,7 +162,7 @@ export default {
loadData(id, next = undefined) {
this.id = id;
getPlaylistDetail(this.id, true)
.then(data => {
.then((data) => {
this.playlist = data.playlist;
this.tracks = data.playlist.tracks;
NProgress.done();
@ -191,8 +189,8 @@ export default {
)
return t;
});
trackIDs = trackIDs.map(t => t.id);
getTrackDetail(trackIDs.join(",")).then(data => {
trackIDs = trackIDs.map((t) => t.id);
getTrackDetail(trackIDs.join(",")).then((data) => {
this.tracks.push(...data.songs);
this.lastLoadedTrackIndex += trackIDs.length;
this.loadingMore = false;
@ -213,8 +211,8 @@ export default {
this.loadingMore = true;
this.loadMore();
}
}
}
},
},
};
</script>

@ -132,7 +132,7 @@ export default {
components: {
Cover,
TrackList,
MvRow
MvRow,
},
data() {
return {
@ -141,7 +141,7 @@ export default {
mvs: [],
type: 1,
limit: 30,
offset: 0
offset: 0,
};
},
computed: {
@ -155,26 +155,26 @@ export default {
},
isExistResult() {
return Object.keys(this.result).length;
}
},
},
methods: {
goToAlbum(id) {
this.$router.push({ name: "album", params: { id } });
},
playTrackInSearchResult(id) {
let track = this.tracks.find(t => t.id === id);
let track = this.tracks.find((t) => t.id === id);
appendTrackToPlayerList(track, true);
},
getData(keywords) {
search({ keywords: keywords, type: 1018 }).then(data => {
search({ keywords: keywords, type: 1018 }).then((data) => {
this.result = data.result;
NProgress.done();
this.show = true;
});
search({ keywords: keywords, type: 1004 }).then(data => {
search({ keywords: keywords, type: 1004 }).then((data) => {
this.mvs = data.result.mvs;
});
}
},
},
created() {
this.getData(this.$route.query.keywords);
@ -184,7 +184,7 @@ export default {
next();
NProgress.start();
this.getData(to.query.keywords);
}
},
};
</script>

@ -31,10 +31,7 @@ module.exports = {
},
chainWebpack(config) {
config.module.rules.delete("svg");
config.module
.rule("svg")
.exclude.add(resolve("src/assets/icons"))
.end();
config.module.rule("svg").exclude.add(resolve("src/assets/icons")).end();
config.module
.rule("icons")
.test(/\.svg$/)

@ -1115,6 +1115,11 @@
resolved "https://registry.npm.taobao.org/@types/normalize-package-data/download/@types/normalize-package-data-2.4.0.tgz?cache=0&sync_timestamp=1596839391651&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnormalize-package-data%2Fdownload%2F%40types%2Fnormalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha1-5IbQ2XOW15vu3QpuM/RTT/a0lz4=
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.npm.taobao.org/@types/parse-json/download/@types/parse-json-4.0.0.tgz?cache=0&sync_timestamp=1596839394119&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fparse-json%2Fdownload%2F%40types%2Fparse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha1-L4u0QUNNFjs1+4/9zNcTiSf/uMA=
"@types/q@^1.5.1":
version "1.5.4"
resolved "https://registry.npm.taobao.org/@types/q/download/@types/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
@ -2411,7 +2416,7 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.1.0:
chalk@^4.0.0, chalk@^4.1.0:
version "4.1.0"
resolved "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
integrity sha1-ThSHCmGNni7dl92DRf2dncMVZGo=
@ -2480,6 +2485,11 @@ ci-info@^1.5.0:
resolved "https://registry.npm.taobao.org/ci-info/download/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
integrity sha1-LKINu5zrMtRSSmgzAzE/AwSx5Jc=
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.npm.taobao.org/ci-info/download/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha1-Z6npZL4xpR4V5QENWObxKDQAL0Y=
cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
version "1.0.4"
resolved "https://registry.npm.taobao.org/cipher-base/download/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
@ -2677,6 +2687,11 @@ commondir@^1.0.1:
resolved "https://registry.npm.taobao.org/commondir/download/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
compare-versions@^3.6.0:
version "3.6.0"
resolved "https://registry.npm.taobao.org/compare-versions/download/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
integrity sha1-GlaJkTaF5ah2N7jT/8p1UU7EHWI=
component-emitter@^1.2.1:
version "1.3.0"
resolved "https://registry.npm.taobao.org/component-emitter/download/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@ -2836,6 +2851,17 @@ cosmiconfig@^5.0.0:
js-yaml "^3.13.1"
parse-json "^4.0.0"
cosmiconfig@^7.0.0:
version "7.0.0"
resolved "https://registry.npm.taobao.org/cosmiconfig/download/cosmiconfig-7.0.0.tgz?cache=0&sync_timestamp=1596310819353&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcosmiconfig%2Fdownload%2Fcosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3"
integrity sha1-75tE13OVnK5j3ezRIt4jhTtg+NM=
dependencies:
"@types/parse-json" "^4.0.0"
import-fresh "^3.2.1"
parse-json "^5.0.0"
path-type "^4.0.0"
yaml "^1.10.0"
create-ecdh@^4.0.0:
version "4.0.4"
resolved "https://registry.npm.taobao.org/create-ecdh/download/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@ -4034,6 +4060,13 @@ find-up@^4.0.0, find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
find-versions@^3.2.0:
version "3.2.0"
resolved "https://registry.npm.taobao.org/find-versions/download/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e"
integrity sha1-ECl/mAMKeGgpaBaQVF72We0dJU4=
dependencies:
semver-regex "^2.0.0"
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.npm.taobao.org/flat-cache/download/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
@ -4615,6 +4648,22 @@ human-signals@^1.1.1:
resolved "https://registry.npm.taobao.org/human-signals/download/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha1-xbHNFPUK6uCatsWf5jujOV/k36M=
husky@^4.3.0:
version "4.3.0"
resolved "https://registry.npm.taobao.org/husky/download/husky-4.3.0.tgz?cache=0&sync_timestamp=1602813564105&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhusky%2Fdownload%2Fhusky-4.3.0.tgz#0b2ec1d66424e9219d359e26a51c58ec5278f0de"
integrity sha1-Cy7B1mQk6SGdNZ4mpRxY7FJ48N4=
dependencies:
chalk "^4.0.0"
ci-info "^2.0.0"
compare-versions "^3.6.0"
cosmiconfig "^7.0.0"
find-versions "^3.2.0"
opencollective-postinstall "^2.0.2"
pkg-dir "^4.2.0"
please-upgrade-node "^3.2.0"
slash "^3.0.0"
which-pm-runs "^1.0.0"
iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -4674,7 +4723,7 @@ import-fresh@^2.0.0:
caller-path "^2.0.0"
resolve-from "^3.0.0"
import-fresh@^3.0.0:
import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.2.1"
resolved "https://registry.npm.taobao.org/import-fresh/download/import-fresh-3.2.1.tgz?cache=0&sync_timestamp=1589682760620&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fimport-fresh%2Fdownload%2Fimport-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
integrity sha1-Yz/2GFBueTr1rJG/SLcmd+FcvmY=
@ -6129,6 +6178,11 @@ open@^6.3.0:
dependencies:
is-wsl "^1.1.0"
opencollective-postinstall@^2.0.2:
version "2.0.3"
resolved "https://registry.npm.taobao.org/opencollective-postinstall/download/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
integrity sha1-eg//l49tv6TQBiOPusmO1BmMMlk=
opener@^1.5.1:
version "1.5.2"
resolved "https://registry.npm.taobao.org/opener/download/opener-1.5.2.tgz?cache=0&sync_timestamp=1598733244715&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fopener%2Fdownload%2Fopener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
@ -6380,6 +6434,11 @@ path-type@^3.0.0:
dependencies:
pify "^3.0.0"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.npm.taobao.org/path-type/download/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha1-hO0BwKe6OAr+CdkKjBgNzZ0DBDs=
pbkdf2@^3.0.3:
version "3.1.1"
resolved "https://registry.npm.taobao.org/pbkdf2/download/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94"
@ -6442,13 +6501,20 @@ pkg-dir@^3.0.0:
dependencies:
find-up "^3.0.0"
pkg-dir@^4.1.0:
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.npm.taobao.org/pkg-dir/download/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha1-8JkTPfft5CLoHR2ESCcO6z5CYfM=
dependencies:
find-up "^4.0.0"
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.npm.taobao.org/please-upgrade-node/download/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
integrity sha1-rt3T+ZTJM+StmLmdmlVu+g4v6UI=
dependencies:
semver-compare "^1.0.0"
plyr@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/plyr/-/plyr-3.6.2.tgz#5a55b608acd161262de1cc75ca843aa64355a051"
@ -6882,6 +6948,11 @@ prepend-http@^1.0.0:
resolved "https://registry.npm.taobao.org/prepend-http/download/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier@2.1.2:
version "2.1.2"
resolved "https://registry.npm.taobao.org/prettier/download/prettier-2.1.2.tgz?cache=0&sync_timestamp=1600215482255&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fprettier%2Fdownload%2Fprettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5"
integrity sha1-MFBwDa4uTItnxMP2Zs24r0BeHOU=
prettier@^1.18.2:
version "1.19.1"
resolved "https://registry.npm.taobao.org/prettier/download/prettier-1.19.1.tgz?cache=0&sync_timestamp=1600215482255&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fprettier%2Fdownload%2Fprettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
@ -7448,6 +7519,16 @@ selfsigned@^1.10.7:
dependencies:
node-forge "^0.10.0"
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.npm.taobao.org/semver-compare/download/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
semver-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.npm.taobao.org/semver-regex/download/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338"
integrity sha1-qTwsWERTmncCMzeRB7OMe0rJ0zg=
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.npm.taobao.org/semver/download/semver-5.7.1.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -7606,6 +7687,11 @@ slash@^2.0.0:
resolved "https://registry.npm.taobao.org/slash/download/slash-2.0.0.tgz?cache=0&sync_timestamp=1589682715547&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fslash%2Fdownload%2Fslash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
integrity sha1-3lUoUaF1nfOo8gZTVEL17E3eq0Q=
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.npm.taobao.org/slash/download/slash-3.0.0.tgz?cache=0&sync_timestamp=1589682715547&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fslash%2Fdownload%2Fslash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha1-ZTm+hwwWWtvVJAIg2+Nh8bxNRjQ=
slice-ansi@^2.1.0:
version "2.1.0"
resolved "https://registry.npm.taobao.org/slice-ansi/download/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
@ -8898,6 +8984,11 @@ which-module@^2.0.0:
resolved "https://registry.npm.taobao.org/which-module/download/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-pm-runs@^1.0.0:
version "1.0.0"
resolved "https://registry.npm.taobao.org/which-pm-runs/download/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
which@^1.2.9:
version "1.3.1"
resolved "https://registry.npm.taobao.org/which/download/which-1.3.1.tgz?cache=0&sync_timestamp=1589682812246&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwhich%2Fdownload%2Fwhich-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
@ -9121,6 +9212,11 @@ yallist@^4.0.0:
resolved "https://registry.npm.taobao.org/yallist/download/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI=
yaml@^1.10.0:
version "1.10.0"
resolved "https://registry.npm.taobao.org/yaml/download/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
integrity sha1-O1k63ZRIdgd9TWg/7gEIG9n/8x4=
yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-13.1.2.tgz?cache=0&sync_timestamp=1600655138204&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs-parser%2Fdownload%2Fyargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"

Loading…
Cancel
Save