Merge remote-tracking branch 'upstream/upcoming' into upcoming

This commit is contained in:
Onur Gümüş 2022-01-22 18:53:09 +03:00
commit 8ed82cc5e8
8 changed files with 1598 additions and 12 deletions

View file

@ -201,6 +201,8 @@ export class Win {
*/
const remote = express();
remote.use(express.static(path.join(this.paths.srcPath, "./web-remote/")))
remote.set("views", path.join(this.paths.srcPath, "./web-remote/views"));
remote.set("view engine", "ejs");
getPort({port: 6942}).then((port) => {
this.remotePort = port;
remote.listen(this.remotePort, () => {
@ -218,6 +220,9 @@ export class Win {
}
firstRequest = false;
})
remote.get("/", (req, res) => {
res.render("index", this.EnvironmentVariables);
});
})
}

View file

@ -186,7 +186,7 @@ export class wsapi {
response.message = "Previous";
break;
case "musickit-api":
this._win.webContents.executeJavaScript(`wsapi.musickitApi(\`${data.method}\`, \`${data.id}\`, ${JSON.stringify(data.params)})`);
this._win.webContents.executeJavaScript(`wsapi.musickitApi(\`${data.method}\`, \`${data.id}\`, ${JSON.stringify(data.params)} , ${data.library})`);
break;
case "musickit-library-api":
break;
@ -218,7 +218,7 @@ export class wsapi {
this._win.hide()
break;
case "play-mediaitem":
this._win.webContents.executeJavaScript(`wsapi.playTrackById(${data.id}, \`${data.kind}\`)`);
this._win.webContents.executeJavaScript(`wsapi.playTrackById("${data.id}", \`${data.kind}\`)`);
response.message = "Playing track";
break;
case "get-status":

View file

@ -2,12 +2,12 @@ const wsapi = {
cache: {playParams: {id: 0}, status: null, remainingTime: 0},
playbackCache: {status: null, time: Date.now()},
search(term, limit) {
MusicKit.getInstance().api.search(term, {limit: limit, types: 'songs,artists,albums'}).then((results)=>{
MusicKit.getInstance().api.search(term, {limit: limit, types: 'songs,artists,albums,playlists'}).then((results)=>{
ipcRenderer.send('wsapi-returnSearch', JSON.stringify(results))
})
},
searchLibrary(term, limit) {
MusicKit.getInstance().api.library.search(term, {limit: limit, types: 'library-songs,library-artists,library-albums'}).then((results)=>{
MusicKit.getInstance().api.library.search(term, {limit: limit, types: 'library-songs,library-artists,library-albums,library-playlists'}).then((results)=>{
ipcRenderer.send('wsapi-returnSearchLibrary', JSON.stringify(results))
})
},
@ -47,10 +47,16 @@ const wsapi = {
returnDynamic(data, type) {
ipcRenderer.send('wsapi-returnDynamic', JSON.stringify(data), type)
},
musickitApi(method, id, params) {
MusicKit.getInstance().api[method](id, params).then((results)=>{
musickitApi(method, id, params, library = false) {
if (library) {
MusicKit.getInstance().api.library[method](id, params).then((results)=>{
ipcRenderer.send('wsapi-returnMusicKitApi', JSON.stringify(results), method)
})
} else {
MusicKit.getInstance().api[method](id, params).then((results)=>{
ipcRenderer.send('wsapi-returnMusicKitApi', JSON.stringify(results), method)
})
}
},
getPlaybackState () {
ipcRenderer.send('wsapi-updatePlaybackState', MusicKitInterop.getAttributes());

View file

@ -1,3 +1,4 @@
var socket;
Vue.component('footer-player', {
@ -56,13 +57,14 @@ var app = new Vue({
searchScroll(e) {
this.search.lastY = e.target.scrollTop;
},
musicKitAPI(method, id, params) {
musicKitAPI(method, id, params, library = false) {
socket.send(
JSON.stringify({
action: "musickit-api",
method: method,
id: id,
params: params
params: params,
library : library
})
)
},
@ -361,15 +363,20 @@ var app = new Vue({
showArtistByName(name) {
this.musicKitAPI("search", name, { types: "artists" })
},
showAlbum(id) {
showAlbum(id,library = false) {
this.search.lastPage = "album"
this.screen = "album-page"
this.musicKitAPI("album", id, {})
this.musicKitAPI("album", id, {}, library)
},
showArtist(id) {
showPlaylist(id, library = false) {
this.search.lastPage = "album"
this.screen = "album-page"
this.musicKitAPI("playlist", id, {}, library)
},
showArtist(id, library = false) {
this.search.lastPage = "artist"
this.screen = "artist-page"
this.musicKitAPI("artist", id, { include: "songs,playlists,albums" })
this.musicKitAPI("artist", id, { include: "songs,playlists,albums" }, library)
},
showQueue() {
this.queue.temp = this.player["queue"]["_queueItems"]
@ -425,6 +432,14 @@ var app = new Vue({
}
this.playMediaItemById(id, 'album');
},
playCustom(id, kind, shuffle = false) {
if (shuffle) {
this.setShuffle(true)
} else {
this.setShuffle(false)
}
this.playMediaItemById(id, kind);
},
getLyrics() {
socket.send(JSON.stringify({
action: "get-lyrics",
@ -512,6 +527,7 @@ var app = new Vue({
case "musickitapi.search":
self.showArtist(response.data["artists"][0]["id"]);
break;
case "musickitapi.playlist":
case "musickitapi.album":
if (self.screen == "album-page") {
self.albumPage.data = response.data

View file

@ -0,0 +1,74 @@
<script type="text/x-template" id="animatedartwork-view">
<div class="animated" v-bind:vid="app.hashCode(video).toString()" v-if="video">
<video ref="video" class="animated-artwork-video" loop id="animated-artwork"></video>
</div>
</script>
<script>
Vue.component('animatedartwork-view', {
template: '#animatedartwork-view',
data: function () {
return {
app: this.$root,
hls: null,
}
},
props: {
video: {
type: String,
required: true
},
priority: {
type: Boolean,
default: false
}
},
async mounted() {
if (!this.priority && app.cfg.visual.animated_artwork == "limited") {
return
} else if (app.cfg.visual.animated_artwork == "disabled") {
return
}
if (this.video) {
this.$nextTick(function () {
var config = {
backBufferLength: 0,
enableWebVTT: false,
enableCEA708Captions: false,
emeEnabled: false,
abrEwmaDefaultEstimate: 10000,
testBandwidth: false,
};
if (this.hls) {
this.hls.detachMedia();
} else {
this.hls = new CiderHls(config);
}
// bind them together
if (this.$refs.video) {
let d = "WIDEVINE_SOFTWARE"
let h = {
initDataTypes: ["cenc", "keyids"],
distinctiveIdentifier: "optional",
persistentState: "required"
}
let p = {
platformInfo: {requiresCDMAttachOnStart: !0, maxSecurityLevel: d, keySystemConfig: h},
appData: {serviceName: "Apple Music"}
}
this.hls.attachMedia(this.$refs.video);
this.hls.loadSource(this.video);
this.hls.loadLevel = parseInt(app.cfg.visual.animated_artwork_qualityLevel || 1);
}
})
}
},
async beforeDestroy() {
if (this.hls) {
await this.hls.destroy();
this.hls = null
}
}
});
</script>

View file

@ -0,0 +1,101 @@
<script type="text/x-template" id="mediaitem-artwork">
<div class="mediaitem-artwork" :class="[{'rounded': (type == 'artists')}, classes]" :key="url"
v-observe-visibility="{callback: visibilityChanged}">
<img :src="getMediaItemArtwork(url, size, width)"
decoding="async" loading="lazy"
class="mediaitem-artwork--img">
<!-- <div v-if="video && isVisible && getVideoPriority()" class="animatedartwork-view-box">
<animatedartwork-view :priority="getVideoPriority()" :video="video"></animatedartwork-view>
</div> -->
</div>
</script>
<script>
Vue.component('mediaitem-artwork', {
template: '#mediaitem-artwork',
props: {
size: {
type: [String, Number],
default: '120'
},
width: {
type: [String, Number],
required: false
},
url: {
type: String,
default: ''
},
type: {
type: String,
default: ''
},
video: {
type: String,
required: false
},
videoPriority: {
type: Boolean,
required: false
},
shadow: {
type: String,
default: ''
}
},
data: function () {
return {
app:this.$root,
isVisible: false,
style: {
"box-shadow": ""
},
classes: []
}
},
mounted() {
this.getClasses()
},
methods: {
getVideoPriority() {
return false
},
getClasses() {
switch (this.shadow) {
case "none":
this.classes.push("no-shadow")
break;
case "large":
this.classes.push("shadow")
break;
case "subtle":
this.classes.push("subtle-shadow")
break;
default:
break;
}
return this.classes;
},
getArtworkStyle() {
return {
width: this.size + 'px',
height: this.size + 'px'
};
},
visibilityChanged: function (isVisible, entry) {
this.isVisible = isVisible
},
getMediaItemArtwork(url, height = 64, width) {
if (typeof url == "undefined" || url == "") {
return "https://beta.music.apple.com/assets/product/MissingArtworkMusic.svg"
}
let newurl = `${url.replace('{w}', width ?? height).replace('{h}', height).replace('{f}', "webp").replace('{c}', ((width === 900) ? "sr" : "cc"))}`;
if (newurl.includes("900x516")) {
newurl = newurl.replace("900x516cc", "900x516sr").replace("900x516bb", "900x516sr");
}
return newurl
},
}
});
</script>

View file

@ -0,0 +1,469 @@
<script type="text/x-template" id="mediaitem-square">
<div tabindex="0"
class="cd-mediaitem-square" :class="getClasses()" @contextmenu="contextMenu"
v-observe-visibility="{callback: visibilityChanged}"
:style="{'--spcolor': getBgColor()}">
<template v-if="isVisible">
<div class="artwork-container">
<div class="artwork" @click='app.routeView(item)'>
<mediaitem-artwork
:url="getArtworkUrl()"
:video="(item.attributes != null && item.attributes.editorialVideo != null) ? (item.attributes.editorialVideo.motionDetailSquare ? item.attributes.editorialVideo.motionDetailSquare.video : (item.attributes.editorialVideo.motionSquareVideo1x1 ? item.attributes.editorialVideo.motionSquareVideo1x1.video : '')) : '' "
:size="size"
shadow="subtle"
:type="item.type"></mediaitem-artwork>
</div>
<button class="menu-btn" v-if="!nomenu.includes(item.type)"
@click="contextMenu"><%- include("../svg/more.svg") %></button>
<button class="play-btn" v-if="!noplay.includes(item.type)"
@click="app.playMediaItem(item)"><%- include("../svg/play.svg") %></button>
<div class="badge-container" v-if="itemBadges.length != 0">
<div class="socialBadge" v-for="badge in itemBadges.limit(1)">
<mediaitem-artwork
:url="(badge.attributes.artwork ? badge.attributes.artwork.url : '')"
:size="32"></mediaitem-artwork>
</div>
</div>
</div>
<div class="info-rect" :class="{'info-rect-card': kind == 'card'}" :style="{'--bgartwork': getArtworkUrl(size, true)}">
<div class="title" v-if="item.attributes.artistNames == null || kind!= 'card'" @click='app.routeView(item)'>
<div class="item-navigate text-overflow-elipsis">{{ item.attributes.name }}</div>
<div class="explicit-icon" v-if="item.attributes && item.attributes.contentRating == 'explicit'" style= "background-image: url(./assets/explicit.svg);height: 12px;width: 12px;filter: contrast(0);background-repeat: no-repeat;margin-top: 2.63px;margin-left: 4px;"></div>
</div>
<div class="subtitle item-navigate text-overflow-elipsis" @click="getSubtitleNavigation()"
v-if="getSubtitle() != ''">
{{ getSubtitle() }}
</div>
<div class="subtitle" v-if="getSubtitle() == '' && kind != 'card'">&nbsp;</div>
</div>
</template>
</div>
</script>
<script>
Vue.component('mediaitem-square', {
template: '#mediaitem-square',
props: {
item: {
type: Object,
required: true
},
kind: {
type: String,
default: ''
},
size: {
type: String,
default: '190'
},
'contextExt': {type: Object, required: false},
},
data: function () {
return {
isVisible: false,
addedToLibrary: false,
guid: this.uuidv4(),
noplay: ["apple-curators"],
nomenu: ["artists", "stations", "apple-curators"],
app: this.$root,
badges: this.$root.socialBadges.badgeMap,
itemBadges: []
}
},
async mounted() {
await this.getBadges()
},
methods: {
getBgColor() {
let color = `#${(this.item.attributes.artwork != null && this.item.attributes.artwork.bgColor != null) ? (this.item.attributes.artwork.bgColor) : `333333`}`
let c = color.substring(1); // strip #
var rgb = parseInt(c, 16); // convert rrggbb to decimal
var r = (rgb >> 16) & 0xff; // extract red
var g = (rgb >> 8) & 0xff; // extract green
var b = (rgb >> 0) & 0xff; // extract blue
var luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
if (luma > 140) {
return "#aaaaaa"
}else{
return color
}
},
getSubtitle() {
if(this.kind == 'card') {
try {
if (typeof this.item.attributes.artistNames != "undefined") {
return this.item.attributes.artistNames
} else if (typeof this.item.attributes.editorialNotes != "undefined") {
return this.item.attributes.editorialNotes.short
} else if (typeof this.item.attributes.artistName != "undefined") {
return this.item.attributes.artistName
} else {
return ''
}
}catch(e) {
return ''
}
}else {
if (typeof this.item.attributes.artistName != "undefined") {
return this.item.attributes.artistName
} else {
return ''
}
}
},
getSubtitleNavigation() {
if(this.kind == 'card') {
try {
if (typeof this.item.attributes.artistNames != "undefined") {
return app.routeView(this.item)
} else if (typeof this.item.attributes.editorialNotes != "undefined") {
return app.routeView(this.item)
} else if (typeof this.item.attributes.artistName != "undefined") {
return app.searchAndNavigate(this.item,'artist')
} else {
return app.routeView(this.item)
}
}catch(e) {
return app.routeView(this.item)
}
}else {
if (typeof this.item.attributes.artistName != "undefined") {
return app.searchAndNavigate(this.item,'artist')
} else {
return app.routeView(this.item)
}
}
},
async getBadges() {
const self = this
const id = (this.item.attributes.playParams ? this.item.attributes.playParams.id : null) || this.item.id
if (id && this.badges[id]) {
let friends = this.badges[id]
if (friends) {
friends.forEach(function (friend) {
self.app.mk.api.v3.music(`/v1/social/${app.mk.storefrontId}/social-profiles/${friend}`).then(data => {
self.itemBadges.push(data.data.data[0])
})
})
}
}
},
revisedRandId() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10);
},
async isInLibrary() {
if (this.item.type && !this.item.type.includes("library")) {
var params = {
"fields[playlists]": "inLibrary",
"fields[albums]": "inLibrary",
"relate": "library",
"extend": this.revisedRandId()
}
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
res = res.data.data[0]
this.addedToLibrary = (res && res.attributes && res.attributes.inLibrary) ? res.attributes.inLibrary : false
} else {
this.addedToLibrary = true
}
},
async removeFromLibrary(id) {
var params = {
"fields[playlists]": "inLibrary",
"fields[songs]": "inLibrary",
"fields[albums]": "inLibrary",
"relate": "library",
"extend": this.revisedRandId()
}
var id = this.item.id ?? this.item.attributes.playParams.id
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
res= res.data.data[0]
if (res && res.relationships && res.relationships.library && res.relationships.library.data && res.relationships.library.data.length > 0) {
id = res.relationships.library.data[0].id
}
let kind = this.item.attributes.playParams.kind ?? this.item.type ?? '';
var truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
{
fetchOptions: {
method: "DELETE"
}})
this.addedToLibrary = true
},
uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
},
getArtworkUrl(size = -1, includeUrl = false) {
let artwork = this.item.attributes.artwork ? this.item.attributes.artwork.url : ''
if(size != -1) {
artwork = artwork.replace('{w}', size).replace('{h}', size).replace('{f}', "webp").replace('{c}', ((size === 900) ? "sr" : "cc"))
}
switch (this.kind) {
case "385":
artwork = this.item.attributes.editorialArtwork.subscriptionHero.url
break;
}
if(!includeUrl) {
return artwork
}else{
return `url("${artwork}")`
}
},
getClasses() {
let type = this.item.type
if (this.kind != "") {
type = this.kind
}
switch (type) {
default:
return []
break;
case "card":
return ["mediaitem-card"]
break;
case "385": // editorial
return ["mediaitem-brick"]
break;
case "small":
return ["mediaitem-small"]
break;
case "music-videos":
case "uploadedVideo":
case "uploaded-videos":
return "mediaitem-video";
break;
}
},
visibilityChanged: function (isVisible, entry) {
this.isVisible = isVisible
},
async contextMenu(event) {
if (this.nomenu.includes(this.item.type)) {
return
}
if (!event) {
event = this.$refs.main
} else {
console.log(event)
}
let self = this
let useMenu = "normal"
if (app.selectedMediaItems.length <= 1) {
app.selectedMediaItems = []
app.select_selectMediaItem(this.item.attributes.playParams.id ?? this.item.id, this.item.attributes.playParams.kind ?? this.item.type, this.index, this.guid, this.item.attributes.playParams.isLibrary ?? false)
} else {
useMenu = "multiple"
}
let menus = {
multiple: {
items: [
{
name: `Play ${app.selectedMediaItems.length} tracks next`,
"icon": "./assets/arrow-bend-up.svg",
action: () => {
let itemsToPlay = {}
app.selectedMediaItems.forEach(item => {
if (!itemsToPlay[item.kind]) {
itemsToPlay[item.kind] = []
}
itemsToPlay[item.kind].push(item.id)
})
// loop through itemsToPlay
for (let kind in itemsToPlay) {
let ids = itemsToPlay[kind]
if (ids.length > 0) {
app.mk.playNext({[kind + "s"]: itemsToPlay[kind]})
}
}
console.log(itemsToPlay)
app.selectedMediaItems = []
}
},
{
name: `Play ${app.selectedMediaItems.length} tracks later`,
"icon": "./assets/arrow-bend-down.svg",
action: () => {
let itemsToPlay = {}
app.selectedMediaItems.forEach(item => {
if (!itemsToPlay[item.kind]) {
itemsToPlay[item.kind] = []
}
itemsToPlay[item.kind].push(item.id)
})
// loop through itemsToPlay
for (let kind in itemsToPlay) {
let ids = itemsToPlay[kind]
if (ids.length > 0) {
app.mk.playLater({[kind + "s"]: itemsToPlay[kind]})
}
}
app.selectedMediaItems = []
}
},
]
},
normal: {
headerItems: [
{
"icon": "./assets/feather/heart.svg",
"id": "love",
"name": "Love",
"hidden": false,
"disabled": true,
"action": function () {
app.love(self.item)
}
},
{
"icon": "./assets/feather/heart.svg",
"id": "unlove",
"active": true,
"name": "Unlove",
"hidden": true,
"action": function () {
app.unlove(self.item)
}
},
{
"icon": "./assets/feather/thumbs-down.svg",
"id": "dislike",
"name": "Dislike",
"hidden": false,
"disabled": true,
"action": function () {
app.dislike(self.item)
}
},
{
"icon": "./assets/feather/thumbs-down.svg",
"id": "undo_dislike",
"name": "Undo dislike",
"active": true,
"hidden": true,
"action": function () {
app.unlove(self.item)
}
},
],
items: [
{
"icon": "./assets/feather/list.svg",
"id": "addToPlaylist",
"name": "Add to Playlist...",
"action": function () {
app.promptAddToPlaylist()
}
},
{
"id": "addToLibrary",
"icon": "./assets/feather/plus.svg",
"name": "Add to library",
"hidden": false,
"disabled": true,
"action": function () {
let item_id = self.item.attributes.playParams.id ?? self.item.id;
let data_type = self.item.attributes.playParams.kind ?? self.item.type;
app.addToLibrary(item_id);
self.addedToLibrary = true;
}
},
{
"id": "removeFromLibrary",
"icon": "./assets/feather/x-circle.svg",
"name": "Remove from library",
"hidden": true,
"action": async function () {
console.log("remove");
let item_id = self.item.attributes.playParams.id ?? self.item.id;
let data_type = self.item.attributes.playParams.kind ?? self.item.type;
await self.removeFromLibrary(item_id);
self.addedToLibrary = false
}
},
{
"name": "Play Next",
"icon": "./assets/arrow-bend-up.svg",
"action": function () {
app.mk.playNext({[self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id})
app.mk.queue._reindex()
app.selectedMediaItems = []
}
},
{
"name": "Play Later",
"icon": "./assets/arrow-bend-down.svg",
"action": function () {
app.mk.playLater({[self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id})
app.mk.queue._reindex()
app.selectedMediaItems = []
}
},
{
"icon": "./assets/feather/share.svg",
"name": "Share",
"action": function () {
self.app.copyToClipboard(self.item.attributes.url)
}
}
]
}
}
if ((self.item.attributes.playParams.kind ?? self.item.type).includes("playlist")) {
// remove the add to playlist option by id "addToPlaylist" using the .filter() method
menus.normal.items = menus.normal.items.filter(function (item) {
return item.id != "addToPlaylist"
})
}
app.showMenuPanel(menus[useMenu], event)
try {
await this.isInLibrary().then((_) => {
if(self.addedToLibrary) {
menus.normal.items.find(x => x.id == 'addToLibrary').hidden = true
menus.normal.items.find(x => x.id == 'removeFromLibrary').hidden = false
}else{
menus.normal.items.find(x => x.id == 'addToLibrary').disabled = false
}
})
}catch(e) {
}
try{
let rating = await app.getRating(self.item)
if (rating == 0) {
menus.normal.headerItems.find(x => x.id == 'love').disabled = false
menus.normal.headerItems.find(x => x.id == 'dislike').disabled = false
} else if (rating == 1) {
menus.normal.headerItems.find(x => x.id == 'unlove').hidden = false
menus.normal.headerItems.find(x => x.id == 'love').hidden = true
} else if (rating == -1) {
menus.normal.headerItems.find(x => x.id == 'undo_dislike').hidden = false
menus.normal.headerItems.find(x => x.id == 'dislike').hidden = true
}
} catch(err) {
}
if (this.contextExt) {
if (this.contextExt.normal) {
menus.normal.items = menus.normal.items.concat(this.contextExt.normal)
}
if (this.contextExt.multiple) {
menus.multiple.items = menus.multiple.items.concat(this.contextExt.multiple)
}
}
},
},
beforeDestroy: function () {
// this.item = null;
// this.kind = null;
// this.size = null;
}
});
</script>

View file

@ -0,0 +1,915 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Web Remote</title>
<link rel="stylesheet" href="style.css?v=2">
<script src="vue.js"></script>
<script src="sortable.min.js"></script>
<script src="vuedraggable.umd.min.js"></script>
<link rel="manifest" href="./manifest.json?v=2">
</head>
<body oncontextmenu="return false;">
<div id="app" :style="{'--artwork': getAlbumArtUrl()}">
<!-- App view when connected -->
<template v-if="connectedState == 1">
<!-- Streamer Overlay -->
<template></template>
<!-- Mini Player -->
<template v-if="screen == 'miniplayer'">
<div class="miniplayer-main">
<div class="media-artwork--miniplayer" :class="artworkPlaying()"
:style="{'--artwork': getAlbumArtUrl()}">
</div>
<div class="miniplayer-draggable">
</div>
<div class="miniplayer-controls">
<button class="md-btn playback-button--small repeat" @click="repeat()"
v-if="player.currentMediaItem.repeatMode == 0"></button>
<button class="md-btn playback-button--small repeat active" @click="repeat()"
v-else-if="player.currentMediaItem.repeatMode == 2"></button>
<button class="md-btn playback-button--small repeat repeatOne" @click="repeat()"
v-else-if="player.currentMediaItem.repeatMode == 1"></button>
<button class="md-btn playback-button previous" @click="previous()"></button>
<button class="md-btn playback-button pause" @click="pause()"
v-if="player.currentMediaItem.status"></button>
<button class="md-btn playback-button play" @click="play()" v-else></button>
<button class="md-btn playback-button next" @click="next()"></button>
<button class="md-btn playback-button--small shuffle" @click="shuffle()"
v-if="player.currentMediaItem.shuffleMode == 0"></button>
<button class="md-btn playback-button--small shuffle active" @click="shuffle()" v-else></button>
</div>
</div>
</template>
<!-- Player -->
<transition name="wpfade">
<div class="md-container md-container_panel player-panel" v-if="screen == 'player'">
<div class="player_top">
<div class="md-body player-artwork-container">
<div class="media-artwork" :class="artworkPlaying()"
:style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
</div>
<div class="player_bottom" v-if="player.lowerPanelState == 'lyrics'">
<div class="md-header" style="width:100%;">
<div class="list-entry" v-if="false">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ player.currentMediaItem.name }}
</div>
<div class="list-entry-artist">
{{ player.currentMediaItem.artistName }}
</div>
</div>
</div>
</div>
</div>
<div class="md-body lyric-body" style="width:100%;">
<template v-if="player.lyrics">
<template v-for="lyric in player.lyrics" v-if="lyric.line != 'lrcInstrumental'">
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
{{ lyric.line }}
</h3>
</template>
<template v-else>
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
<div class="lyricWaiting">
<div></div>
<div></div>
<div></div>
</div>
</h3>
</template>
</template>
<template v-else>
No Lyrics Available
</template>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics active"
@click="player.lowerPanelState = 'controls'"></button>
</div>
</div>
<div class="player_bottom" v-if="player.lowerPanelState == 'controls'">
<div class="md-footer">
<div class="row player-track-info">
<div class="col nopadding text-overflow-elipsis">
<div class="player-song-title text-overflow-elipsis">
{{ player.currentMediaItem.name }}
</div>
<div class="player-song-artist text-overflow-elipsis" @click="searchArtist()">
{{ player.currentMediaItem.artistName }}
</div>
</div>
<div class="col-auto nopadding player-more-container" v-if="false" style="">
<button @click="player.songActions = true;" class="player-more-button">...</button>
</div>
</div>
</div>
<div class="md-footer">
<input type="range" min="0"
:value="player.currentMediaItem.durationInMillis - player.currentMediaItem.remainingTime"
:max="player.currentMediaItem.durationInMillis" class="web-slider playback-slider"
@input="seekTo($event.target.value)">
<div class="row nopadding player-duration-container" style="width: 90%;margin: 0 auto;">
<div class="col nopadding player-duration-time" style="text-align:left">
{{ parseTime(player.currentMediaItem.durationInMillis -
player.currentMediaItem.remainingTime) }}
</div>
<div class="col nopadding player-duration-time" style="text-align:right">
-{{ parseTime(player.currentMediaItem.remainingTime) }}
</div>
</div>
</div>
<div class="md-footer playback-buttons">
<button class="md-btn playback-button--small repeat" @click="repeat()"
v-if="player.currentMediaItem.repeatMode == 0"></button>
<button class="md-btn playback-button--small repeat active" @click="repeat()"
v-else-if="player.currentMediaItem.repeatMode == 2"></button>
<button class="md-btn playback-button--small repeat repeatOne" @click="repeat()"
v-else-if="player.currentMediaItem.repeatMode == 1"></button>
<button class="md-btn playback-button previous" @click="previous()"></button>
<button class="md-btn playback-button pause" @click="pause()"
v-if="player.currentMediaItem.status"></button>
<button class="md-btn playback-button play" @click="play()" v-else></button>
<button class="md-btn playback-button next" @click="next()"></button>
<button class="md-btn playback-button--small shuffle" @click="shuffle()"
v-if="player.currentMediaItem.shuffleMode == 0"></button>
<button class="md-btn playback-button--small shuffle active" @click="shuffle()"
v-else></button>
</div>
<div class="md-footer">
<div class="row volume-slider-container">
<div class="col-auto">
<div class="player-volume-glyph decrease"></div>
</div>
<div class="col">
<input type="range" class="web-slider volume-slider" max="1" min="0" step="0.01"
@input="setVolume($event.target.value)" :value="player.currentMediaItem.volume">
</div>
<div class="col-auto">
<div class="player-volume-glyph increase"></div>
</div>
</div>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'portrait'"
@click="showLyrics()"></button>
<button class="md-btn playback-button--small lyrics"
v-if="checkOrientation() == 'landscape'" @click="showLyricsInline()"></button>
<button class="md-btn playback-button--small queue" @click="showQueue()"></button>
<button class="md-btn playback-button--small search" @click="showSearch()"></button>
</div>
</div>
</div>
</transition>
<!-- Search -->
<transition name="wpfade">
<div class="md-container md-container_panel search-panel" v-if="screen == 'search'">
<div class="search-header">
<div class="md-header">
<div class="row">
<div class="col-auto">
<button class="back-button" @click="screen = 'player'"></button>
</div>
<div class="col" style="display: flex;align-items: center;">
<div class="col">
<input type="text" placeholder="Artists, Songs, Lyrics, and More"
spellcheck="false" v-model="search.query" @change="searchQuery()"
v-on:keyup.enter="searchQuery()" class="search-input">
</div>
</div>
</div>
</div>
<div class="md-header search-type-container">
<button class="search-type-button" @click="search.searchType = 'applemusic';searchQuery()"
:class="searchTypeClass('applemusic')" style="width:100%;">Apple Music
</button>
<button class="search-type-button" @click="search.searchType = 'library';searchQuery()"
:class="searchTypeClass('library')" style="width:100%;">Library
</button>
</div>
<div class="md-header search-tab-container" v-if="search.state == 2">
<button class="search-tab" @click="search.tab = 'all'" :class="searchTabClass('all')">All
Results
</button>
<button class="search-tab" @click="search.tab = 'songs'"
:class="searchTabClass('songs')">Songs
</button>
<button class="search-tab" @click="search.tab = 'albums'"
:class="searchTabClass('albums')">Albums
</button>
<button class="search-tab" @click="search.tab = 'artists'"
:class="searchTabClass('artists')">Artists
</button>
<button class="search-tab" @click="search.tab = 'playlists'"
:class="searchTabClass('playlists')">Playlists
</button>
</div>
</div>
<div class="search-body-container">
<transition name="wpfade">
<div class="md-body search-body" v-if="search.state == 0">
<div
style="font-size: 17px;display:flex;flex-direction: column;justify-content: center;align-items: center;">
<img src="./assets/search.svg" style="width: 40px;margin: 32px;opacity: 0.85">
Search by song, album, artist, or lyrics.
</div>
</div>
</transition>
<transition name="wpfade">
<div class="md-body search-body" v-if="search.state == 1">
<!-- loading state -->
</div>
</transition>
<transition name="wpfade">
<div class="md-body search-body" ref="searchBody" @scroll="searchScroll"
style="overflow-y:auto;" v-if="search.state == 2">
<template v-if="canShowSearchTab('songs')">
<div class="list-entry-header">Songs</div>
<template v-if="search.results.songs != null">
<div class="list-entry" v-for="song in search.results.songs.data"
@click="trackSelect(song)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.attributes.name }}
</div>
<div class="list-entry-artist">
{{ song.attributes.artistName }}
<!-- <span class="lossless-badge" v-if="song.audioTraits.includes('lossless')">Lossless</span> -->
</div>
</div>
</div>
</div>
</template>
<template v-if="search.results['library-songs'] != null">
<div class="list-entry" v-for="song in search.results['library-songs'].data"
@click="trackSelect(song)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.attributes.name }}
</div>
<div class="list-entry-artist">
{{ song.attributes.artistName }}
</div>
</div>
</div>
</div>
</template>
</template>
<template v-if="canShowSearchTab('albums')">
<div class="list-entry-header">Albums</div>
<template v-if="search.results.albums != null">
<div class="list-entry" v-for="album in search.results.albums.data"
@click="showAlbum(album.id)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="album.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(album.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ album.attributes.name }}
</div>
<div class="list-entry-artist">
{{ album.attributes.artistName }}
<!-- <span class="lossless-badge" v-if="album.audioTraits.includes('lossless')">Lossless</span> -->
</div>
</div>
</div>
</div>
</template>
<template v-if="search.results['library-albums'] != null">
<div class="list-entry" v-for="album in search.results['library-albums'].data"
@click="showAlbum(album.id, true)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="album.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(album.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ album.attributes.name }}
</div>
<div class="list-entry-artist">
{{ album.attributes.artistName }}
</div>
</div>
</div>
</div>
</template>
</template>
<template v-if="canShowSearchTab('artists')">
<div class="list-entry-header">Artists</div>
<template v-if="search.results.artists != null">
<div class="list-entry" @click="showArtist(artist.id)"
v-for="artist in search.results.artists.data">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image artist"
v-if="artist.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(artist.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ artist.attributes.name }}
</div>
<div class="list-entry-artist">
</div>
</div>
</div>
</div>
</template>
<template v-if="search.results['library-artists'] != null">
<div class="list-entry"
v-for="artist in search.results['library-artists'].data">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image artist"
v-if="artist.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(artist.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ artist.attributes.name }}
</div>
<div class="list-entry-artist">
</div>
</div>
</div>
</div>
</template>
</template>
<template v-if="canShowSearchTab('playlists')">
<div class="list-entry-header">Playlists</div>
<template v-if="search.results.playlists != null">
<div class="list-entry" v-for="playlist in search.results.playlists.data"
@click="showPlaylist(playlist.attributes.playParams.id)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="playlist.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(playlist.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ playlist.attributes.name }}
</div>
<div class="list-entry-artist">
{{ playlist.attributes.artistName }}
<!-- <span class="lossless-badge" v-if="album.audioTraits.includes('lossless')">Lossless</span> -->
</div>
</div>
</div>
</div>
</template>
<template v-if="search.results['library-playlists'] != null">
<div class="list-entry" v-for="playlist in search.results['library-playlists'].data"
@click="showPlaylist(playlist.attributes.playParams.id, true)"
>
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="playlist.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(playlist.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ playlist.attributes.name }}
</div>
<div class="list-entry-artist">
{{ playlist.attributes.artistName }}
</div>
</div>
</div>
</div>
</template>
</template>
</div>
</transition>
</div>
<footer-player></footer-player>
</div>
</transition>
<!-- Track Select Actions -->
<transition name="wpfade">
<div class="md-container md-container_panel context-menu" style="overflow-y:auto;"
v-if="search.trackSelect">
<div class="md-body context-menu-body">
<button class="context-menu-item context-menu-item--left"
@click="playMediaItemById(search.selected.attributes.playParams?.id ?? search.selected.id );clearSelectedTrack()">
<div class="row">
<div class="col-auto flex-center" v-if="search.selected.attributes.artwork"
style="display:flex;align-items: center;">
<div class="list-entry-image"
:style="{'--artwork': getAlbumArtUrlList(search.selected.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center" style="display:flex;align-items: center;">
<div style="width:100%;font-size: 18px;">
{{ search.selected.attributes.name }}
</div>
<div style="width:100%;font-size: 16px;">
{{ search.selected.attributes.artistName }}
</div>
<div style="width:100%;font-size: 14px;">
{{ parseTime(search.selected.attributes.durationInMillis) }}
</div>
</div>
</div>
</button>
</div>
<div class="md-body context-menu-body" style="height: auto;">
<button class="context-menu-item context-menu-item--left"
@click="playMediaItemById(search.selected.attributes.playParams?.id ?? search.selected.id );clearSelectedTrack()">
<div class="row">
<div class="col">
Play
</div>
<div class="col-auto">
▶️
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left"
@click="playNext('song',search.selected.attributes.playParams?.id ?? search.selected.id );clearSelectedTrack()">
<div class="row">
<div class="col">
Play Next
</div>
<div class="col-auto">
⏭️
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left"
@click="playLater('song', search.selected.attributes.playParams?.id ?? search.selected.id );clearSelectedTrack()">
<div class="row">
<div class="col">
Play Later
</div>
<div class="col-auto">
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left" v-if="false">
<div class="row">
<div class="col">
Open in {{ musicAppVariant() }}
</div>
<div class="col-auto">
🎵
</div>
</div>
</button>
</div>
<div class="md-footer">
<button class="context-menu-item" @click="clearSelectedTrack()">Cancel</button>
</div>
</div>
</transition>
<!-- Song Actions -->
<transition name="wpfade">
<div class="md-container md-container_panel context-menu" v-if="player.songActions">
<div class="md-header">
</div>
<div class="md-body context-menu-body">
<button class="context-menu-item context-menu-item--left" v-if="false">
<div class="row">
<div class="col">
Add To Library
</div>
<div class="col-auto">
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left" v-if="false">
<div class="row">
<div class="col">
Love
</div>
<div class="col-auto">
❤️
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left">
<div class="row">
<div class="col">
Share
</div>
<div class="col-auto">
🌐
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left">
<div class="row">
<div class="col">
Open in {{ musicAppVariant() }}
</div>
<div class="col-auto">
🎵
</div>
</div>
</button>
</div>
<div class="md-footer">
<button class="context-menu-item" @click="player.songActions = false">Cancel</button>
</div>
</div>
</transition>
<!-- Artist Page -->
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="screen == 'artist-page'"
v-if="artistPage.data.attributes['name']">
<div class="md-header">
<div class="row">
<div class="col-auto">
<button class="back-button" @click="showSearch(true)"></button>
</div>
<div class="col flex-center">
{{ artistPage.data.attributes["name"] }}
</div>
</div>
</div>
<div class="album-body-container" :style="getMediaPalette(artistPage.data.attributes)">
<div class="artist-header" v-if="artistPage.data.attributes['artwork']"
:style="getMediaPalette(artistPage.data.attributes)">
<div class="artist-header-portrait"
:style="{'--artwork': getAlbumArtUrlList(artistPage.data.attributes['artwork']['url'], 600)}">
</div>
<h2>{{ artistPage.data.attributes["name"] }}</h2>
</div>
<div class="md-body artist-body">
<h2>Songs</h2>
<div class="song-scroller-horizontal">
<button v-for="song in artistPage.data.relationships['songs'].data"
class="song-placeholder" @click="trackSelect(song)">
{{ song.attributes.name }}
</button>
</div>
<h2>Albums</h2>
<div class="mediaitem-scroller-horizontal">
<div v-for="album in artistPage.data.relationships['albums'].data" @click="showAlbum(album.attributes.playParams.id)">
<template v-if="album.attributes.artwork">
<mediaitem-artwork :url="album.attributes.artwork.url" :size="200"></mediaitem-artwork>
</template>
<div class="text-overflow-elipsis" style="width: 213px">
{{ album.attributes["name"] }}
</div>
<div class="text-overflow-elipsis" style="width: 213px">
{{ album.attributes["artistName"] }}
</div>
</div>
</div>
<h2>Playlists</h2>
<div class="mediaitem-scroller-horizontal">
<div v-for="playlist in artistPage.data.relationships['playlists'].data" @click="showPlaylist(playlist.attributes.playParams.id)">
<template v-if="playlist.attributes.artwork">
<mediaitem-artwork :url="playlist.attributes.artwork.url" :size="200"></mediaitem-artwork>
</template>
<div class="text-overflow-elipsis" style="width: 213px">
{{ playlist.attributes["name"] }}
</div>
</div>
</div>
</div>
</div>
<footer-player></footer-player>
</div>
</transition>
<!-- Queue -->
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="screen == 'queue'">
<div class="md-header">
<div class="list-entry" @click="screen = 'player'">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ player.currentMediaItem.name }}
</div>
<div class="list-entry-artist">
{{ player.currentMediaItem.artistName }}
</div>
</div>
</div>
</div>
</div>
<div class="md-header" style="text-align: right;padding: 5px 16px;">
<button class="md-btn playback-button--small autoplay"
v-if="!player.currentMediaItem.autoplayEnabled" @click="setAutoplay(true)"></button>
<button class="md-btn playback-button--small autoplay activeColor" v-else
@click="setAutoplay(false)"></button>
</div>
<div class="md-body queue-body" v-if="!player.queue['_queueItems']">
Empty
</div>
<div class="md-body queue-body" style="overflow-y:auto;" id="list-queue" v-else>
<draggable v-model="queue.temp" handle=".handle" filter=".passed" @change="queueMove">
<template v-for="(song, position) in queue.temp"
v-if="position > player.queue['_position']">
<div class="list-entry" :class="getQueuePositionClass(position)">
<div class="row" style="width:100%;">
<div class="col-auto">
<div class="handle">
</div>
</div>
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.item.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.item.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.item.attributes.name }}
</div>
<div class="list-entry-artist">
{{ song.item.attributes.artistName }}
</div>
</div>
<div class="col-auto flex-center" style="text-align:right;">
<div v-if="position == player.queue['_position']">▶️</div>
</div>
</div>
</div>
</template>
</draggable>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'portrait'"
@click="showLyrics()"></button>
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'landscape'"
@click="screen = 'player';showLyricsInline()"></button>
<button class="md-btn playback-button--small queue active" @click="screen = 'player'"></button>
<button class="md-btn playback-button--small search" @click="showSearch()"></button>
</div>
</div>
</transition>
<!-- Lyrics -->
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="screen == 'lyrics'">
<div class="md-header">
<div class="list-entry" @click="screen = 'player'">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ player.currentMediaItem.name }}
</div>
<div class="list-entry-artist">
{{ player.currentMediaItem.artistName }}
</div>
</div>
</div>
</div>
</div>
<div class="md-body lyric-body">
<template v-if="player.lyrics">
<template v-for="lyric in player.lyrics" v-if="lyric.line != 'lrcInstrumental'">
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
{{ lyric.line }}
</h3>
</template>
<template v-else>
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
<div class="lyricWaiting">
<div></div>
<div></div>
<div></div>
</div>
</h3>
</template>
</template>
<template v-else>
No Lyrics Available
</template>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics active" @click="screen = 'player'"></button>
<button class="md-btn playback-button--small queue" @click="showQueue()"></button>
<button class="md-btn playback-button--small search" @click="showSearch()"></button>
</div>
</div>
</transition>
<!-- Album Page -->
<transition name="wpfade">
<div class="md-container md-container_panel md-container_album"
v-if="screen == 'album-page' && albumPage.data.attributes['name']">
<div class="md-header">
<div class="row">
<div class="col-auto">
<button class="back-button" @click="showSearch(true)"></button>
</div>
</div>
</div>
<div class="album-body-container">
<div class="md-header">
<div class="albumpage-artwork"
:style="{'--artwork': getAlbumArtUrlList(albumPage.data.attributes['artwork'] ? albumPage.data.attributes['artwork']['url'] : '', 300)}">
</div>
<div class="albumpage-album-name">
{{ albumPage.data.attributes["name"] }}
</div>
<template v-if="albumPage.data.attributes['artists']">
<div class="albumpage-artist-name"
@click="showArtist(albumPage.data.attributes['artists'][0]['id'])">
{{ albumPage.data.attributes["artistName"] }}
</div>
</template>
<template>
<div class="albumpage-misc-info">
{{ albumPage.data.attributes.genreNames ? albumPage.data.attributes.genreNames[0] :
'' }} ∙ {{
albumPage.data.attributes.releaseDate ? new
Date(albumPage.data.attributes.releaseDate).getFullYear() : '' }}
</div>
</template>
<div class="row" style="margin-top: 20px;">
<div class="col">
<button class="wr-btn" @click="playCustom(albumPage.data.attributes.playParams.id, albumPage.data.attributes.playParams.kind, false)"
style="width:100%;">Play
</button>
</div>
<div class="col">
<button class="wr-btn" style="width:100%;"
@click="playCustom(albumPage.data.attributes.playParams.id, albumPage.data.attributes.playParams.kind, true)">Shuffle
</button>
</div>
</div>
<div class="albumpage-album-notes" v-if="albumPage.data.attributes['editorialNotes']">
<div class="notes-preview"
v-html="albumPage.data.attributes['editorialNotes']['standard']">
</div>
<button @click="albumPage.editorsNotes = true" class="notes-more">More</button>
</div>
</div>
<div class="md-body artist-body">
<div class="list-entry-header">Tracks</div>
<div class="list-entry" v-for="song in albumPage.data.relationships['tracks'].data"
@click="trackSelect(song)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.attributes.name }}
</div>
<div class="list-entry-artist">
{{ song.attributes.artistName }}
</div>
</div>
</div>
</div>
<div class="md-footer">
<div>{{ albumPage.data.relationships['tracks'].data.length }} Tracks</div>
<div>
{{ albumPage.data.attributes['copyright'] }}
</div>
</div>
</div>
</div>
<footer-player></footer-player>
</div>
</transition>
<!-- Album Page - Editorial Notes -->
<transition name="wpfade">
<div class="md-container md-container_panel context-menu" v-if="albumPage.editorsNotes"
style="padding-top: 42px;">
<div class="md-header" :style="getMediaPalette(albumPage.data)"
style="font-size: 18px;background:var(--bgColor);color:var(--textColor1);text-align: center;border-radius: 10px 10px 0 0;border-top: 1px solid #ffffff1f;">
{{ albumPage.data.attributes["name"] }}
</div>
<div class="md-body album-page-fullnotes-body" :style="getMediaPalette(albumPage.data.attributes)"
style="background:var(--bgColor);color:var(--textColor1);"
v-html="albumPage.data.attributes['editorialNotes']['standard']">
</div>
<div class="md-footer" :style="getMediaPalette(albumPage.data.attributes)"
style="background:var(--bgColor);color:var(--textColor1);">
<button class="context-menu-item" @click="albumPage.editorsNotes = false">Close</button>
</div>
</div>
</transition>
</template>
<!-- Loading -->
<transition name="wpfade">
<div class="md-container md-container_panel connection-error-panel" v-if="connectedState != 1">
<div class="md-header">
</div>
<div class="md-body" style="display:flex;justify-content: center;align-items: center;">
<div v-if="connectedState == 0">
Loading...
</div>
<div v-else>
<h3 style="text-align:center;">Connection Interrupted</h3>
<!--<button class="md-btn md-btn-primary" style="font-weight:500;width: 120px;border-radius: 50px;display:block;margin: 0 auto;" @click="connect()">Retry-->
<button class="md-btn md-btn-primary"
style="font-weight:500;width: 120px;border-radius: 50px;display:block;margin: 0 auto;"
onclick="document.location = document.location">Retry
</button>
</div>
</div>
<div class="md-footer">
</div>
</div>
</transition>
<!-- Template -->
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="false">
<div class="md-header">
</div>
<div class="md-body">
</div>
<div class="md-footer">
</div>
</div>
</transition>
</div>
<script type="text/x-template" id="footer-player">
<div class="footer-player" v-show="$parent.player.currentMediaItem['name']">
<div class="row" style="width:100%;margin:0px;">
<div class="col-auto flex-center" style="padding:0 6px;" @click="$parent.screen = 'player'">
<div class="list-entry-image" :style="{'--artwork': $parent.getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center text-overflow-elipsis" @click="$parent.screen = 'player'">
<div class="list-entry-name text-overflow-elipsis">
{{ $parent.player.currentMediaItem.name }}
</div>
<div class="list-entry-artist text-overflow-elipsis">
{{ $parent.player.currentMediaItem.artistName }}
</div>
</div>
<div class="col-auto">
<button class="md-btn playback-button pause" @click="$parent.pause()" v-if="$parent.player.currentMediaItem.status"></button>
<button class="md-btn playback-button play" @click="$parent.play()" v-else></button>
</div>
</div>
</div>
</script>
<script src="./index.js?v=1"></script>
</body>
<!-- Horizontal MediaItem Scroller (MV) -->
<%- include('components/mediaitem-artwork')
%>
</html>