Merge branch 'upcoming' into upcoming
This commit is contained in:
commit
e0814e3c95
14 changed files with 432 additions and 138 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -309,3 +309,4 @@ GitHub.sublime-settings
|
||||||
#Service Worker mappings
|
#Service Worker mappings
|
||||||
src/renderer/sw.js.map
|
src/renderer/sw.js.map
|
||||||
src/renderer/workbox-962786f2.js.map
|
src/renderer/workbox-962786f2.js.map
|
||||||
|
/src/renderer/musickit-dev.js
|
||||||
|
|
|
@ -57,7 +57,8 @@ export class ConfigStore {
|
||||||
"animated_artwork": "limited", // 0 = always, 1 = limited, 2 = never
|
"animated_artwork": "limited", // 0 = always, 1 = limited, 2 = never
|
||||||
"animated_artwork_qualityLevel": 1,
|
"animated_artwork_qualityLevel": 1,
|
||||||
"bg_artwork_rotation": false,
|
"bg_artwork_rotation": false,
|
||||||
"hw_acceleration": "default" // default, webgpu, disabled
|
"hw_acceleration": "default", // default, webgpu, disabled
|
||||||
|
"showuserinfo": true
|
||||||
},
|
},
|
||||||
"lyrics": {
|
"lyrics": {
|
||||||
"enable_mxm": false,
|
"enable_mxm": false,
|
||||||
|
|
1
src/renderer/assets/feather/mic.svg
Normal file
1
src/renderer/assets/feather/mic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mic"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>
|
After Width: | Height: | Size: 418 B |
1
src/renderer/assets/feather/plus-circle-white.svg
Normal file
1
src/renderer/assets/feather/plus-circle-white.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
|
After Width: | Height: | Size: 344 B |
BIN
src/renderer/assets/feather/x-circlePng.png
Normal file
BIN
src/renderer/assets/feather/x-circlePng.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
src/renderer/assets/playPng.png
Normal file
BIN
src/renderer/assets/playPng.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
BIN
src/renderer/assets/shufflePng.png
Normal file
BIN
src/renderer/assets/shufflePng.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3 KiB |
|
@ -257,7 +257,7 @@ const app = new Vue({
|
||||||
tmpVar: [],
|
tmpVar: [],
|
||||||
notification: false,
|
notification: false,
|
||||||
chrome: {
|
chrome: {
|
||||||
hideUserInfo: ipcRenderer.sendSync("is-dev"),
|
hideUserInfo: ipcRenderer.sendSync("is-dev") || false,
|
||||||
artworkReady: false,
|
artworkReady: false,
|
||||||
userinfo: {
|
userinfo: {
|
||||||
"id": "",
|
"id": "",
|
||||||
|
@ -486,13 +486,21 @@ const app = new Vue({
|
||||||
let self = this
|
let self = this
|
||||||
clearTimeout(this.hangtimer)
|
clearTimeout(this.hangtimer)
|
||||||
this.mk = MusicKit.getInstance()
|
this.mk = MusicKit.getInstance()
|
||||||
|
let needsReload = (typeof localStorage["music.ampwebplay.media-user-token"] == "undefined")
|
||||||
this.mk.authorize().then(() => {
|
this.mk.authorize().then(() => {
|
||||||
self.mkIsReady = true
|
self.mkIsReady = true
|
||||||
//document.location.reload()
|
if(needsReload) {
|
||||||
|
document.location.reload()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.$forceUpdate()
|
this.$forceUpdate()
|
||||||
if (this.isDev) {
|
if (this.isDev) {
|
||||||
this.mk.privateEnabled = true
|
this.mk.privateEnabled = true
|
||||||
|
// Hide UserInfo if Dev mode
|
||||||
|
this.chrome.hideUserInfo = true
|
||||||
|
} else {
|
||||||
|
// Get Hide User from Settings
|
||||||
|
this.chrome.hideUserInfo = !this.cfg.visual.showuserinfo
|
||||||
}
|
}
|
||||||
if (this.cfg.visual.hw_acceleration == "disabled") {
|
if (this.cfg.visual.hw_acceleration == "disabled") {
|
||||||
document.body.classList.add("no-gpu")
|
document.body.classList.add("no-gpu")
|
||||||
|
@ -560,7 +568,8 @@ const app = new Vue({
|
||||||
app.mk.bitrate = app.cfg.audio.quality = 64
|
app.mk.bitrate = app.cfg.audio.quality = 64
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
app.mk.bitrate = app.cfg.audio.quality
|
// app.mk.bitrate = app.cfg.audio.quality
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -717,6 +726,10 @@ const app = new Vue({
|
||||||
this.$forceUpdate()
|
this.$forceUpdate()
|
||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
|
unauthorize() {
|
||||||
|
this.mk.unauthorize()
|
||||||
|
document.location.reload()
|
||||||
|
},
|
||||||
getAppClasses() {
|
getAppClasses() {
|
||||||
if (this.cfg.advanced.experiments.includes('compactui')) {
|
if (this.cfg.advanced.experiments.includes('compactui')) {
|
||||||
return { compact: true }
|
return { compact: true }
|
||||||
|
@ -3254,6 +3267,15 @@ const app = new Vue({
|
||||||
return 0 !== s && (h = s > 0 ? "-" : "+"),
|
return 0 !== s && (h = s > 0 ? "-" : "+"),
|
||||||
`${h}${leadingZeros(n, 2)}:${leadingZeros(d, 2)}`
|
`${h}${leadingZeros(n, 2)}:${leadingZeros(d, 2)}`
|
||||||
},
|
},
|
||||||
|
toggleHideUserInfo() {
|
||||||
|
if(this.chrome.hideUserInfo) {
|
||||||
|
this.cfg.visual.showuserinfo = true
|
||||||
|
this.chrome.hideUserInfo = false
|
||||||
|
} else {
|
||||||
|
this.cfg.visual.showuserinfo = false
|
||||||
|
this.chrome.hideUserInfo = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1056,7 +1056,7 @@ input[type=range].web-slider::-webkit-slider-runnable-track {
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 0 0.5em;
|
margin: 0 0 0 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-chrome .app-chrome-item > .app-playback-controls .song-artist {
|
.app-chrome .app-chrome-item > .app-playback-controls .song-artist {
|
||||||
|
@ -2058,9 +2058,9 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
||||||
|
|
||||||
.md-btn {
|
.md-btn {
|
||||||
background: rgba(100, 100, 100, 0.5);
|
background: rgba(100, 100, 100, 0.5);
|
||||||
padding: 4px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 13px;
|
font-size: 16px;
|
||||||
border: 1px solid rgb(100 100 100 / 35%);
|
border: 1px solid rgb(100 100 100 / 35%);
|
||||||
color: #eee;
|
color: #eee;
|
||||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.3), 0px 1px 1px rgba(0, 0, 0, 0.4);
|
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.3), 0px 1px 1px rgba(0, 0, 0, 0.4);
|
||||||
|
@ -2094,6 +2094,40 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-ico-play {
|
||||||
|
content:url("./assets/playPng.png");
|
||||||
|
width: 10px;
|
||||||
|
height: 12px;
|
||||||
|
margin-right: 1px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-ico-shuffle {
|
||||||
|
content:url("./assets/shufflePng.png");
|
||||||
|
width: 15px;
|
||||||
|
height: 13px;
|
||||||
|
margin-right: 1px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-ico-remove {
|
||||||
|
content:url("./assets/feather/x-circlePng.png");
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 1px;
|
||||||
|
margin-bottom: -1.5px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-ico-add {
|
||||||
|
content:url("./assets/feather/plus-circle-white.svg");
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 1px;
|
||||||
|
margin-bottom: -1.5px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
.md-select {
|
.md-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
@ -2519,6 +2553,107 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Podcast Page
|
||||||
|
.content-inner.podcasts-page {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100% - var(--navigationBarHeight));
|
||||||
|
padding: 0px;
|
||||||
|
|
||||||
|
.podcast-artwork {
|
||||||
|
width: 200px;
|
||||||
|
margin: 16px auto;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcasts-list {
|
||||||
|
height: 100%;
|
||||||
|
width: 280px;
|
||||||
|
background: rgb(200 200 200 / 10%);
|
||||||
|
overflow-y: overlay;
|
||||||
|
border-right: 1px solid var(--color2);
|
||||||
|
flex: none;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episodes-list {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: rgb(200 200 200 / 6%);
|
||||||
|
overflow-y: overlay;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.episodes-inline-info {
|
||||||
|
padding: 14px 14px 0px 14px;
|
||||||
|
|
||||||
|
.podcast-show-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcast-show-description {
|
||||||
|
margin: 32px 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcast-artwork {
|
||||||
|
width: 120px;
|
||||||
|
margin: 0px auto;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcasts-details {
|
||||||
|
height: 100%;
|
||||||
|
width: 400px;
|
||||||
|
flex: none;
|
||||||
|
background: rgba(200, 200, 200, 0.1);
|
||||||
|
overflow-y: overlay;
|
||||||
|
border-left: 1px solid var(--color2);
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.podcast-genre {
|
||||||
|
text-align: center;
|
||||||
|
margin: 6px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcast-metainfo {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.7em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcast-header {
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcast-play-btn {
|
||||||
|
width: 50%;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcast-description {
|
||||||
|
margin: 12px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Album / Playlist Page */
|
/* Album / Playlist Page */
|
||||||
.playlist-page {
|
.playlist-page {
|
||||||
--bgColor: transparent;
|
--bgColor: transparent;
|
||||||
|
@ -4372,7 +4507,7 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 0 0.5em;
|
margin: 0 0 0 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -2,28 +2,20 @@
|
||||||
<div v-observe-visibility="{callback: visibilityChanged}"
|
<div v-observe-visibility="{callback: visibilityChanged}"
|
||||||
@click="select"
|
@click="select"
|
||||||
class="cd-mediaitem-list-item"
|
class="cd-mediaitem-list-item"
|
||||||
:class="{'mediaitem-selected': app.select_hasMediaItem(guid)}">
|
:class="{'mediaitem-selected': app.select_hasMediaItem(guid)}"
|
||||||
|
@contextmenu="contextMenu">
|
||||||
<template v-if="isVisible">
|
<template v-if="isVisible">
|
||||||
<div class="artwork" v-if="showArtwork == true">
|
<div class="artwork" v-if="showArtwork == true">
|
||||||
<mediaitem-artwork
|
<mediaitem-artwork
|
||||||
:url="getArtwork()"
|
:url="getArtwork()"
|
||||||
size="50"
|
size="50"
|
||||||
:type="item.type"></mediaitem-artwork>
|
:type="item.type"></mediaitem-artwork>
|
||||||
<button class="overlay-play" @click="select"></button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="info-rect" :style="{'padding-left': (showArtwork ? '' : '16px')}"
|
<div class="info-rect" :style="{'padding-left': (showArtwork ? '' : '16px')}"
|
||||||
@dblclick="app.routeView(item)">
|
@dblclick="app.routeView(item)">
|
||||||
<div class="title text-overflow-elipsis">
|
<div class="title text-overflow-elipsis">
|
||||||
{{ item.attributes.name }}
|
{{ item.attributes.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="subtitle text-overflow-elipsis" style="-webkit-box-orient: horizontal;">
|
|
||||||
<template v-if="item.attributes.name">
|
|
||||||
<div class="artist item-navigate text-overflow-elipsis"
|
|
||||||
@click="select">
|
|
||||||
{{ item.attributes.artistName }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,22 +54,19 @@
|
||||||
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
|
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
|
||||||
},
|
},
|
||||||
getDataType() {
|
getDataType() {
|
||||||
if (this.item.attributes.playParams.isLibrary) {
|
|
||||||
return this.item.type
|
return this.item.type
|
||||||
} else {
|
|
||||||
return this.item.attributes.playParams.kind
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async select(e) {
|
async select(e) {
|
||||||
let u = this.item
|
let u = this.item
|
||||||
let u1 = await app.mk.api.v3.music(`/v1/me/library/artists/${u.id}/albums`,
|
let u1 = await app.mk.api.v3.music(`/v1/me/library/artists/${u.id}/albums`, {
|
||||||
{platform: "web",
|
"platform": "web",
|
||||||
"include[library-albums]": "artists,tracks",
|
"include[library-albums]": "artists,tracks",
|
||||||
"include[library-artists]": "catalog",
|
"include[library-artists]": "catalog",
|
||||||
"fields[artists]": "url",
|
"fields[artists]": "url",
|
||||||
"includeOnly": "catalog,artists"}
|
"includeOnly": "catalog,artists"
|
||||||
)
|
})
|
||||||
app.showCollection({data : Object.assign({},u1.data.data)}, u.attributes.name?? '', '');
|
app.showCollection({data : Object.assign({},u1.data.data)}, u.attributes.name?? '', '');
|
||||||
|
app.select_selectMediaItem(u.id, this.getDataType(), this.index, this.guid, true)
|
||||||
},
|
},
|
||||||
getArtwork(){
|
getArtwork(){
|
||||||
let u = ""
|
let u = ""
|
||||||
|
@ -87,79 +76,37 @@
|
||||||
return u;
|
return u;
|
||||||
},
|
},
|
||||||
contextMenu(event) {
|
contextMenu(event) {
|
||||||
|
|
||||||
let self = this
|
let self = this
|
||||||
let data_type = this.getDataType()
|
let data_type = this.getDataType()
|
||||||
let item_id = this.item.attributes.playParams.id ?? this.item.id
|
|
||||||
let isLibrary = this.item.attributes.playParams.isLibrary ?? false
|
let item = self.item
|
||||||
|
item.attributes.artistName = item.attributes.name;
|
||||||
|
|
||||||
let useMenu = "normal"
|
let useMenu = "normal"
|
||||||
if (app.selectedMediaItems.length <= 1) {
|
if (app.selectedMediaItems.length <= 1) {
|
||||||
app.selectedMediaItems = []
|
app.selectedMediaItems = []
|
||||||
app.select_selectMediaItem(item_id, data_type, this.index, this.guid, isLibrary)
|
app.select_selectMediaItem(this.item.id, data_type, this.index, this.guid, true)
|
||||||
} else {
|
} else {
|
||||||
useMenu = "multiple"
|
useMenu = "multiple"
|
||||||
}
|
}
|
||||||
|
|
||||||
let menus = {
|
let menus = {
|
||||||
multiple: {
|
multiple: {
|
||||||
items: [
|
items: [] //
|
||||||
{
|
|
||||||
"name": "Add to Playlist...",
|
|
||||||
"action": function () {
|
|
||||||
app.promptAddToPlaylist()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `Play ${app.selectedMediaItems.length} tracks next`,
|
|
||||||
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`,
|
|
||||||
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: {
|
normal: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
"name": "Add to Playlist...",
|
"name": "Go to Artist",
|
||||||
|
"icon": "./assets/feather/user.svg",
|
||||||
"action": function () {
|
"action": function () {
|
||||||
app.promptAddToPlaylist()
|
app.searchAndNavigate(self.item, 'artist')
|
||||||
|
console.log(self.item)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "./assets/feather/radio.svg",
|
||||||
"name": "Start Radio",
|
"name": "Start Radio",
|
||||||
"action": function () {
|
"action": function () {
|
||||||
app.mk.setStationQueue({song: self.item.attributes.playParams.id ?? self.item.id}).then(() => {
|
app.mk.setStationQueue({song: self.item.attributes.playParams.id ?? self.item.id}).then(() => {
|
||||||
|
@ -169,31 +116,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Play Next",
|
"icon": "./assets/feather/share.svg",
|
||||||
|
"name": "Share",
|
||||||
"action": function () {
|
"action": function () {
|
||||||
app.mk.playNext({[self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id})
|
if (!self.item.attributes.url && self.item.relationships){
|
||||||
app.mk.queue._reindex()
|
if (self.item.relationships.catalog){
|
||||||
app.selectedMediaItems = []
|
app.mkapi(self.item.attributes.playParams.kind, false, self.item.relationships.catalog.data[0].id).then(u => {self.app.copyToClipboard((u.length && u.length > 0)? u[0].attributes.url : u.attributes.url)})
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
{
|
self.app.copyToClipboard(self.item.attributes.url)}
|
||||||
"name": "Play Later",
|
|
||||||
"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 = []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Go to Artist",
|
|
||||||
"action": function () {
|
|
||||||
app.searchAndNavigate(self.item, 'artist')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Go to Album",
|
|
||||||
"action": function () {
|
|
||||||
app.searchAndNavigate(self.item, 'album')
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -208,7 +139,9 @@
|
||||||
menus.multiple.items = menus.multiple.items.concat(this.contextExt.multiple)
|
menus.multiple.items = menus.multiple.items.concat(this.contextExt.multiple)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CiderContextMenu.Create(event, menus[useMenu])
|
//CiderContextMenu.Create(event, menus[useMenu]); // Depreciated Context Menu
|
||||||
|
app.showMenuPanel(menus[useMenu], event);
|
||||||
|
|
||||||
},
|
},
|
||||||
visibilityChanged: function (isVisible, entry) {
|
visibilityChanged: function (isVisible, entry) {
|
||||||
this.isVisible = isVisible
|
this.isVisible = isVisible
|
||||||
|
|
|
@ -85,7 +85,9 @@
|
||||||
<div class="song-name" style="-webkit-box-orient: horizontal;"
|
<div class="song-name" style="-webkit-box-orient: horizontal;"
|
||||||
:style="[mk.nowPlayingItem['attributes']['contentRating'] == 'explicit' ? {'margin-left' : '23px'} : {'margin-left' : '0px'} ]">
|
:style="[mk.nowPlayingItem['attributes']['contentRating'] == 'explicit' ? {'margin-left' : '23px'} : {'margin-left' : '0px'} ]">
|
||||||
{{ mk.nowPlayingItem["attributes"]["name"] }}
|
{{ mk.nowPlayingItem["attributes"]["name"] }}
|
||||||
<div class="explicit-icon" v-if="mk.nowPlayingItem['attributes']['contentRating'] == 'explicit'" style="display: inline-block"></div>
|
<div class="explicit-icon"
|
||||||
|
v-if="mk.nowPlayingItem['attributes']['contentRating'] == 'explicit'"
|
||||||
|
style="display: inline-block"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="song-artist"
|
<div class="song-artist"
|
||||||
style="display: inline-block; -webkit-box-orient: horizontal; white-space: nowrap;">
|
style="display: inline-block; -webkit-box-orient: horizontal; white-space: nowrap;">
|
||||||
|
@ -198,6 +200,8 @@
|
||||||
page="browse"></sidebar-library-item>
|
page="browse"></sidebar-library-item>
|
||||||
<sidebar-library-item name="Radio" svg-icon="./assets/feather/radio.svg"
|
<sidebar-library-item name="Radio" svg-icon="./assets/feather/radio.svg"
|
||||||
page="radio"></sidebar-library-item>
|
page="radio"></sidebar-library-item>
|
||||||
|
<sidebar-library-item name="Podcasts" svg-icon="./assets/feather/mic.svg"
|
||||||
|
page="podcasts"></sidebar-library-item>
|
||||||
<div class="app-sidebar-header-text">
|
<div class="app-sidebar-header-text">
|
||||||
Library
|
Library
|
||||||
</div>
|
</div>
|
||||||
|
@ -218,7 +222,7 @@
|
||||||
<transition name="wpfade">
|
<transition name="wpfade">
|
||||||
<div class="usermenu-container" v-if="chrome.menuOpened">
|
<div class="usermenu-container" v-if="chrome.menuOpened">
|
||||||
<div class="usermenu-body">
|
<div class="usermenu-body">
|
||||||
<button class="usermenu-item" @click="chrome.hideUserInfo = !chrome.hideUserInfo">
|
<button class="usermenu-item" @click="toggleHideUserInfo()">
|
||||||
<div class="row nopadding">
|
<div class="row nopadding">
|
||||||
<div class="col nopadding">
|
<div class="col nopadding">
|
||||||
Show Personal Info
|
Show Personal Info
|
||||||
|
@ -254,7 +258,7 @@
|
||||||
<button class="usermenu-item" @click="appRoute('settings')">
|
<button class="usermenu-item" @click="appRoute('settings')">
|
||||||
Settings
|
Settings
|
||||||
</button>
|
</button>
|
||||||
<button class="usermenu-item" @click="mk.unauthorize()">
|
<button class="usermenu-item" @click="unauthorize()">
|
||||||
Sign Out
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -312,7 +316,7 @@
|
||||||
<template v-if="chrome.userinfo.id">
|
<template v-if="chrome.userinfo.id">
|
||||||
<div class="fullname text-overflow-elipsis">{{ chrome.userinfo.attributes.name }}
|
<div class="fullname text-overflow-elipsis">{{ chrome.userinfo.attributes.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="handle-text text-overflow-elipsis">@{{ chrome.userinfo.attributes.handle
|
<div class="handle-text text-overflow-elipsis">{{ chrome.userinfo.attributes.handle
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -339,6 +343,14 @@
|
||||||
<button class="nav-item"
|
<button class="nav-item"
|
||||||
@click="navigateForward()"><%- include('svg/chevron-right.svg') %></button>
|
@click="navigateForward()"><%- include('svg/chevron-right.svg') %></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Podcasts -->
|
||||||
|
<transition name="wpfade">
|
||||||
|
<template v-if="page == 'podcasts'">
|
||||||
|
<apple-podcasts></apple-podcasts>
|
||||||
|
</template>
|
||||||
|
</transition>
|
||||||
|
|
||||||
<!-- Apple Setings Page -->
|
<!-- Apple Setings Page -->
|
||||||
<transition name="wpfade">
|
<transition name="wpfade">
|
||||||
<template v-if="page == 'apple-account-settings'">
|
<template v-if="page == 'apple-account-settings'">
|
||||||
|
@ -524,7 +536,8 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<transition name="drawertransition">
|
<transition name="drawertransition">
|
||||||
<div class="app-drawer" v-if="drawer.open && drawer.panel == 'lyrics' && lyrics && lyrics != [] && lyrics.length > 0">
|
<div class="app-drawer"
|
||||||
|
v-if="drawer.open && drawer.panel == 'lyrics' && lyrics && lyrics != [] && lyrics.length > 0">
|
||||||
<div class="bgArtworkMaterial">
|
<div class="bgArtworkMaterial">
|
||||||
<div class="bg-artwork-container">
|
<div class="bg-artwork-container">
|
||||||
<img class="bg-artwork a" :src="$store.state.artwork.playerLCD">
|
<img class="bg-artwork a" :src="$store.state.artwork.playerLCD">
|
||||||
|
@ -607,6 +620,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Apple Settings Page -->
|
||||||
|
<%- include('pages/podcasts') %>
|
||||||
<!-- Apple Settings Page -->
|
<!-- Apple Settings Page -->
|
||||||
<%- include('pages/apple-account-settings') %>
|
<%- include('pages/apple-account-settings') %>
|
||||||
<!-- Library - Songs -->
|
<!-- Library - Songs -->
|
||||||
|
|
|
@ -62,20 +62,20 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="playlist-controls" v-observe-visibility="{callback: isHeaderVisible}">
|
<div class="playlist-controls" v-observe-visibility="{callback: isHeaderVisible}">
|
||||||
<button class="md-btn" style="min-width: 120px;"
|
<button class="md-btn" style="min-width: 100px;"
|
||||||
@click="app.mk.shuffleMode = 0; play()">
|
@click="app.mk.shuffleMode = 0; play()"> <img class="md-ico-play">
|
||||||
Play
|
Play
|
||||||
</button>
|
</button>
|
||||||
<button class="md-btn" style="min-width: 120px;"
|
<button class="md-btn" style="min-width: 100px;"
|
||||||
@click="app.mk.shuffleMode = 1;play()">
|
@click="app.mk.shuffleMode = 1;play()"> <img class="md-ico-shuffle">
|
||||||
Shuffle
|
Shuffle
|
||||||
</button>
|
</button>
|
||||||
<button class="md-btn" style="min-width: 120px;" v-if="inLibrary!=null && confirm!=true"
|
<button class="md-btn" style="min-width: 180px;" v-if="inLibrary!=null && confirm!=true"
|
||||||
@click="confirmButton()">
|
@click="confirmButton()"> <img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
|
||||||
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
|
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="md-btn" style="min-width: 120px;" v-if="confirm==true"
|
<button class="md-btn" style="min-width: 180px;" v-if="confirm==true"
|
||||||
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
|
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) "> <img class="md-ico-remove">
|
||||||
Confirm?
|
Confirm?
|
||||||
</button>
|
</button>
|
||||||
<button class="more-btn-round" style="float:right;" @click="menu">
|
<button class="more-btn-round" style="float:right;" @click="menu">
|
||||||
|
@ -98,20 +98,20 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto flex-center">
|
<div class="col-auto flex-center">
|
||||||
<div>
|
<div>
|
||||||
<button class="md-btn" style="min-width: 120px;"
|
<button class="md-btn" style="min-width: 100px;"
|
||||||
@click="app.mk.shuffleMode = 0; play()">
|
@click="app.mk.shuffleMode = 0; play()"> <img class="md-ico-play">
|
||||||
Play
|
Play
|
||||||
</button>
|
</button>
|
||||||
<button class="md-btn" style="min-width: 120px;"
|
<button class="md-btn" style="min-width: 100px;"
|
||||||
@click="app.mk.shuffleMode = 1;play()">
|
@click="app.mk.shuffleMode = 1;play()"> <img class="md-ico-shuffle">
|
||||||
Shuffle
|
Shuffle
|
||||||
</button>
|
</button>
|
||||||
<button class="md-btn" style="min-width: 120px;" v-if="inLibrary!=null && confirm!=true"
|
<button class="md-btn" style="min-width: 180px;" v-if="inLibrary!=null && confirm!=true"
|
||||||
@click="confirmButton()">
|
@click="confirmButton()"> <img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
|
||||||
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
|
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="md-btn" style="min-width: 120px;" v-if="confirm==true"
|
<button class="md-btn" style="min-width: 180px;" v-if="confirm==true"
|
||||||
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
|
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) "> <img class="md-ico-remove">
|
||||||
Confirm?
|
Confirm?
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
173
src/renderer/views/pages/podcasts.ejs
Normal file
173
src/renderer/views/pages/podcasts.ejs
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
<script type="text/x-template" id="apple-podcasts">
|
||||||
|
<div class="content-inner podcasts-page">
|
||||||
|
<div class="podcasts-list">
|
||||||
|
<podcast-tab :isselected="podcastSelected.id == podcast.id" @click.native="selectPodcast(podcast)" v-for="podcast in podcasts" :item="podcast"></podcast-tab>
|
||||||
|
</div>
|
||||||
|
<div class="episodes-list">
|
||||||
|
<div v-if="podcastSelected.id != -1" class="episodes-inline-info">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto flex-center">
|
||||||
|
<div class="podcast-artwork">
|
||||||
|
<mediaitem-artwork shadow="large" :url="podcastSelected.attributes.artwork.url" size="300"></mediaitem-artwork>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col podcast-show-info">
|
||||||
|
<h1>{{ podcastSelected.attributes.name }}</h1>
|
||||||
|
<small>{{ podcastSelected.attributes.releaseFrequency }}</small>
|
||||||
|
<small>Created: {{ new Date(podcastSelected.attributes.createdDate).toLocaleDateString() }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="well podcast-show-description">{{ podcastSelected.attributes.description.standard }}</div>
|
||||||
|
|
||||||
|
<h3>Episodes</h3>
|
||||||
|
</div>
|
||||||
|
<podcast-episode :isselected="selected.id == episode.id" @dblclick.native="playEpisode(episode)" @click.native="selectEpisode(episode)" :item="episode"
|
||||||
|
v-for="episode in episodes"></podcast-episode>
|
||||||
|
</div>
|
||||||
|
<div class="podcasts-details" v-if="selected.id != -1">
|
||||||
|
<div class="podcast-artwork">
|
||||||
|
<mediaitem-artwork shadow="large" :url="selected.attributes.artwork.url" size="300"></mediaitem-artwork>
|
||||||
|
</div>
|
||||||
|
<h3 class="podcast-header">{{ selected.attributes.name }}</h3>
|
||||||
|
<button @click="playEpisode(selected)" class="md-btn podcast-play-btn">Play Episode</button>
|
||||||
|
<div class="podcast-genre">
|
||||||
|
{{ selected.attributes.genreNames[0] }}
|
||||||
|
</div>
|
||||||
|
<div class="podcast-metainfo">
|
||||||
|
{{ msToMinSec(selected.attributes.durationInMilliseconds) }} • {{ new Date(selected.attributes.releaseDateTime).toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="well podcast-description" v-if="selected.attributes.description.standard">{{ selected.attributes.description.standard }}</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<button class="md-btn md-btn-block" @click="openUrl(selected.attributes.websiteUrl)">Podcast Website</button>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button class="md-btn md-btn-block">Share</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
<script type="text/x-template" id="podcast-tab">
|
||||||
|
<div class="cd-mediaitem-list-item" :class="{'mediaitem-selected': isselected}">
|
||||||
|
<div class="artwork">
|
||||||
|
<mediaitem-artwork
|
||||||
|
:url="item.attributes.artwork.url"
|
||||||
|
size="50"
|
||||||
|
type="podcast"></mediaitem-artwork>
|
||||||
|
</div>
|
||||||
|
<div class="info-rect">
|
||||||
|
<div class="title text-overflow-elipsis">
|
||||||
|
{{ item.attributes.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
<script type="text/x-template" id="podcast-episode">
|
||||||
|
<div class="cd-mediaitem-list-item" :class="{'mediaitem-selected': isselected}">
|
||||||
|
<div class="info-rect" :style="{'padding-left':'16px'}">
|
||||||
|
<div class="title text-overflow-elipsis">
|
||||||
|
{{ item.attributes.name }}
|
||||||
|
</div>
|
||||||
|
<div class="subtitle text-overflow-elipsis">
|
||||||
|
{{ item.attributes.description.standard }}
|
||||||
|
</div>
|
||||||
|
<div class="subtitle text-overflow-elipsis">
|
||||||
|
{{ msToMinSec(item.attributes.durationInMilliseconds) }} • {{ new Date(item.attributes.releaseDateTime).toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
Vue.component('podcast-episode', {
|
||||||
|
template: '#podcast-episode',
|
||||||
|
props: ['item', 'isselected'],
|
||||||
|
methods: {
|
||||||
|
msToMinSec(ms) {
|
||||||
|
var minutes = Math.floor(ms / 60000);
|
||||||
|
var seconds = ((ms % 60000) / 1000).toFixed(0);
|
||||||
|
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Vue.component('podcast-tab', {
|
||||||
|
template: '#podcast-tab',
|
||||||
|
props: ['item', 'isselected'],
|
||||||
|
methods: {}
|
||||||
|
});
|
||||||
|
Vue.component('apple-podcasts', {
|
||||||
|
template: '#apple-podcasts',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
podcasts: [],
|
||||||
|
episodes: [],
|
||||||
|
podcastSelected: {
|
||||||
|
id: -1
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
id: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
let podcastShow = await app.mk.api.v3.podcasts(`/v1/me/library/podcasts?include=episodes`)
|
||||||
|
this.podcasts = podcastShow.data.data
|
||||||
|
if (podcastShow.data.next) {
|
||||||
|
await this.getNext(podcastShow.data.next)
|
||||||
|
}
|
||||||
|
// this.episodes = podcastShow.data.data[0].relationships.episodes.data
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openUrl(url) {
|
||||||
|
window.open(url)
|
||||||
|
},
|
||||||
|
msToMinSec(ms) {
|
||||||
|
var minutes = Math.floor(ms / 60000);
|
||||||
|
var seconds = ((ms % 60000) / 1000).toFixed(0);
|
||||||
|
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
|
||||||
|
},
|
||||||
|
playEpisode(episode) {
|
||||||
|
app.mk.setQueue({'episode': episode.id}).then(() => {app.mk.play()})
|
||||||
|
},
|
||||||
|
selectPodcast(podcast) {
|
||||||
|
this.podcastSelected = podcast
|
||||||
|
this.getEpisodes(podcast)
|
||||||
|
},
|
||||||
|
selectEpisode(episode) {
|
||||||
|
this.selected = episode
|
||||||
|
},
|
||||||
|
async getEpisodes(podcast) {
|
||||||
|
this.episodes = []
|
||||||
|
let eps = await app.mk.api.v3.podcasts(`/v1/catalog/${app.mk.storefrontId}/podcasts/${podcast.id}?include=episodes`)
|
||||||
|
|
||||||
|
eps.data.data[0].relationships.episodes.data.forEach(ep => {
|
||||||
|
this.episodes.push(ep)
|
||||||
|
})
|
||||||
|
if (eps.data.data[0].relationships.episodes.next) {
|
||||||
|
await this.getNextEpisodes(eps.data.data[0].relationships.episodes.next, podcast.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getNextEpisodes(next, podcastId) {
|
||||||
|
|
||||||
|
let podcastShow = await app.mk.api.v3.podcasts(next)
|
||||||
|
if(podcastId != this.podcastSelected.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
podcastShow.data.data.forEach(ep => {
|
||||||
|
this.episodes.push(ep)
|
||||||
|
})
|
||||||
|
if (podcastShow.data.next) {
|
||||||
|
await this.getNextEpisodes(podcastShow.data.next, podcastId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getNext(next) {
|
||||||
|
let podcastShow = await app.mk.api.v3.podcasts(next)
|
||||||
|
this.podcasts = this.podcasts.concat(podcastShow.data.data)
|
||||||
|
if (podcastShow.data.next) {
|
||||||
|
await this.getNext(podcastShow.data.next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="md-option-segment md-option-segment_auto">
|
<div class="md-option-segment md-option-segment_auto">
|
||||||
<select class="md-select" style="width:180px;" v-model="app.cfg.audio.quality" v-on:change="changeAudioQuality">
|
<select class="md-select" style="width:180px;" v-model="app.cfg.audio.quality" v-on:change="changeAudioQuality">
|
||||||
<option value="990">Extreme</option>
|
<!-- // <option value="990">Extreme</option> -->
|
||||||
<option value="256">High</option>
|
<option value="256">High</option>
|
||||||
<option value="64">Low</option>
|
<option value="64">Low</option>
|
||||||
<option value="auto">Auto</option>
|
<option value="auto">Auto</option>
|
||||||
|
@ -103,6 +103,14 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="md-option-line">
|
||||||
|
<div class="md-option-segment">
|
||||||
|
Show Personal Info
|
||||||
|
</div>
|
||||||
|
<div class="md-option-segment md-option-segment_auto">
|
||||||
|
<input type="checkbox" v-model="app.cfg.visual.showuserinfo" v-on:change="toggleUserInfo" switch/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="md-option-header">
|
<div class="md-option-header">
|
||||||
<span>Lyrics</span>
|
<span>Lyrics</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -605,6 +613,9 @@
|
||||||
},
|
},
|
||||||
changeAudioQuality : function(){
|
changeAudioQuality : function(){
|
||||||
app.mk.bitrate = app.cfg.audio.quality
|
app.mk.bitrate = app.cfg.audio.quality
|
||||||
|
},
|
||||||
|
toggleUserInfo : function(){
|
||||||
|
app.chrome.hideUserInfo = !app.cfg.visual.showuserinfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue