Merge branch 'develop'

This commit is contained in:
child_duckling 2022-03-08 18:07:59 -08:00
commit df2f7b7216
195 changed files with 62285 additions and 12895 deletions

View file

@ -3,6 +3,7 @@
<div class="row">
<div class="col">
<img src="assets/banner.png" alt="Cider Logo" style="display:block;margin:0 auto;width: 500px;">
<p style="text-align: center" id="version">{{ $root.getLz("term.version") }} {{ $root.version }}</p>
<p style="text-align: center"> {{$root.getLz('about.thanks')}} </p>
<p style="text-align: center">"{{$root.getLz('term.appleMusic')}}" - {{$root.getLz('term.copyright')}} © 2022 <a href="https://www.apple.com/" class="dt-footer__link"
@ -11,9 +12,11 @@
{{$root.getLz('term.rightsReserved')}}</p>
<hr>
<h3>{{$root.getLz('term.sponsor')}}</h3>
<button onclick="window.open('https://github.com/sponsors/ciderapp')" class="md-btn sponsorBtn"><img src="./assets/github.svg"/>GitHub Sponsors</button>
<button onclick="window.open('https://ko-fi.com/cryptofyre')" class="md-btn sponsorBtn"><img src="./assets/ko_fi.svg"/>Ko-fi</button>
<button onclick="window.open('https://opencollective.com/ciderapp')" class="md-btn sponsorBtn"><img src="./assets/open_collective.svg"/>Open Collective</button>
<h3>{{$root.getLz('term.socials')}}</h3>
<button onclick="window.open('https://github.com/ciderapp/Cider')" class="md-btn sponsorBtn"><img style="width: 20.5px;" src="./assets/github.svg"/>{{$root.getLz('term.github')}}</button>
<button onclick="window.open('https://discord.gg/applemusic')" class="md-btn sponsorBtn"><img src="./assets/discord.svg"/>{{$root.getLz('term.discord')}}</button>
</div>
@ -55,6 +58,7 @@
data: function () {
return {
window: window,
version: app.version,
team: [
{
name: 'cryptofyre',
@ -85,6 +89,18 @@
link: 'https://github.com/vapormusic',
role: app.getLz('term.developer'),
avatar: 'https://avatars.githubusercontent.com/u/27716185?v=4'
},
{
name: 'crypticplank',
link: 'https://github.com/crypticplank',
role: app.getLz('term.developer'),
avatar: 'https://avatars.githubusercontent.com/u/52553007?v=4'
},
{
name: 'Maikiwi',
link: 'https://github.com/maikirakiwi',
role: app.getLz('term.developer'),
avatar: 'https://avatars.githubusercontent.com/u/74925636?v=4'
},
{
name: 'Void',

View file

@ -8,18 +8,16 @@
<h3>{{app.getLz('home.followedArtists')}}</h3>
</div>
</div>
<div class="well">
<mediaitem-scroller-horizontal>
<div v-for="artist in artists" style="margin: 6px;">
<mediaitem-square :item="artist" kind="small"></mediaitem-square>
<button @click="unfollow(artist.id)" class="md-btn md-btn-glyph" style="display:flex;">
<div class="sidebar-icon">
<div class="svg-icon" :style="{'--url': 'url(./assets/feather/x-circle.svg)'}"></div>
</div> {{app.getLz('action.unfollow')}}
</button>
</div>
</mediaitem-scroller-horizontal>
</div>
<vue-horizontal>
<div v-for="artist in artists" style="margin: 6px;">
<mediaitem-square :item="artist" kind="small"></mediaitem-square>
<button @click="unfollow(artist.id)" class="md-btn md-btn-glyph" style="display:flex;">
<div class="sidebar-icon">
<div class="svg-icon" :style="{'--url': 'url(./assets/feather/x-circle.svg)'}"></div>
</div> {{app.getLz('action.unfollow')}}
</button>
</div>
</vue-horizontal>
</div>
</div>
</div>
@ -80,21 +78,30 @@
let self = this
this.artists = []
this.artistFeed = []
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists?ids=${artists.toString()}&views=latest-release&include[songs]=albums&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount&limit[artists:top-songs]=2&art[url]=f`).then(artistData => {
artistData.data.data.forEach(item => {
self.artists.push(item)
if (item.views["latest-release"].data.length != 0) {
self.artistFeed.push(item.views["latest-release"].data[0])
}
})
// Apple limits the number of IDs we can provide in a single API call to 50.
// Divide it into groups of 50 and send parallel requests
let chunks = []
for (let artistIdx = 0; artistIdx < artists.length; artistIdx += 50) {
chunks.push(artists.slice(artistIdx, artistIdx + 50))
}
try {
const chunkArtistData = await Promise.all(chunks.map(chunk =>
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists?ids=${chunk.toString()}&views=latest-release&include[songs]=albums&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount&limit[artists:top-songs]=2&art[url]=f`)))
chunkArtistData.forEach(chunkResult =>
chunkResult.data.data.forEach(item => {
self.artists.push(item)
if (item.views["latest-release"].data.length != 0) {
self.artistFeed.push(item.views["latest-release"].data[0])
}
}))
// sort artistFeed by attributes.releaseDate descending, date is formatted as "YYYY-MM-DD"
this.artistFeed.sort((a, b) => {
let dateA = new Date(a.attributes.releaseDate)
let dateB = new Date(b.attributes.releaseDate)
return dateB - dateA
})
})
} catch (err) { }
}
}
});

View file

@ -57,35 +57,32 @@
</div>
<div class="artist-body">
<div class="row well">
<div class="col">
<div class="col-sm-3" v-if="data.views['latest-release'].data.length != 0">
<h3>{{app.getLz('term.latestReleases')}}</h3>
<div style="width: auto;margin: 0 auto;">
<mediaitem-square kind="card" v-for="song in data.views['latest-release'].data"
:item="song">
</mediaitem-square>
</div>
</div>
<div class="col-sm-12" v-if="data.views['top-songs']">
<div class="row">
<div class="col-auto" v-if="data.views['latest-release'].data.length != 0">
<h3>{{app.getLz('term.latestReleases')}}</h3>
<div style="width: auto;margin: 0 auto;">
<mediaitem-square kind="card" v-for="song in data.views['latest-release'].data"
:item="song">
</mediaitem-square>
</div>
<div class="col" style="padding:0;">
<h3>{{app.getLz('term.topSongs')}}</h3>
</div>
<div class="col" v-if="data.views['top-songs']">
<div class="row">
<div class="col" style="padding:0;">
<h3>{{app.getLz('term.topSongs')}}</h3>
</div>
<div class="col-auto flex-center" v-if="data.views['top-songs'].data.length >= 16" style="padding:0;">
<button class="cd-btn-seeall" @click="app.showArtistView(data.id, data.attributes.name + ' - Top Songs', 'top-songs')">{{app.getLz('term.seeAll')}}</button>
</div>
</div>
<div class="mediaitem-list-item__grid">
<div class="grid-body">
<mediaitem-list-item
v-for="(song, index) in data.views['top-songs'].data.limit(16)"
:index="index"
:item="song"></mediaitem-list-item>
</div>
</div>
<div class="col-auto flex-center" v-if="data.views['top-songs'].data.length >= 20" style="padding:0;">
<button class="cd-btn-seeall" @click="app.showArtistView(data.id, data.attributes.name + ' - Top Songs', 'top-songs')">{{app.getLz('term.seeAll')}}</button>
</div>
</div>
<div class="row">
<div class="col flex-center" style="padding:0;">
<div class="mediaitem-list-item__grid">
<listitem-horizontal :items="data.views['top-songs'].data.limit(20)">
</listitem-horizontal>
</div>
</div>
</div>
</div>
</div>
<div class="row well">

View file

@ -0,0 +1,222 @@
<script type="text/x-template" id="audiolabs-page">
<div class="content-inner settings-page">
<div class="md-option-container">
<div class="md-option-header">
<span>{{$root.getLz('settings.option.audio.audioLab')}}</span>
</div>
<div class="settings-option-body">
<div class="md-option-line" v-show="!app.cfg.advanced.AudioContext">
<div class="md-option-segment">
{{$root.getLz('settings.warn.audioLab.withoutAF')}}
</div>
<button class="md-btn" style="margin-top: 5px;" onclick="app.appRoute('settings')">
{{$root.getLz('term.settings')}}
</button>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.ciderPPE')}}
<br>
<small>{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.ciderPPE.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.audio.maikiwiAudio.ciderPPE" v-on:change="CiderAudio.hierarchical_loading();" switch/>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext && app.cfg.audio.maikiwiAudio.ciderPPE === true">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.ciderPPEStrength')}}
<br>
<small>{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.ciderPPEStrength.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" :disabled="app.cfg.audio.maikiwiAudio.ciderPPE_value === 0.5" v-model="app.cfg.audio.maikiwiAudio.ciderPPE_value" v-on:click="ciderPPEStandard">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.ciderPPEStrength.standard')}}
</button>
<button class="md-btn" style="margin-top: 5px;" :disabled="app.cfg.audio.maikiwiAudio.ciderPPE_value === 0.55" v-model="app.cfg.audio.maikiwiAudio.ciderPPE_value" v-on:click="ciderPPEClarity">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.ciderPPEStrength.aggressive')}}
</button>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.analogWarmth')}}
<br>
<small>{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.analogWarmth.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.audio.maikiwiAudio.analogWarmth" v-on:change="CiderAudio.hierarchical_loading();" switch/>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext === true && app.cfg.audio.maikiwiAudio.analogWarmth === true">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.analogWarmthIntensity')}}
<br>
<small>{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.analogWarmthIntensity.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" :disabled="app.cfg.audio.maikiwiAudio.analogWarmth_value === 1.25" v-model="app.cfg.audio.maikiwiAudio.analogWarmth_value" v-on:click="analogWarmthSmooth">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.analogWarmthIntensity.smooth')}}
</button>
<button class="md-btn" :disabled="app.cfg.audio.maikiwiAudio.analogWarmth_value === 1.75" v-model="app.cfg.audio.maikiwiAudio.analogWarmth_value" v-on:click="analogWarmthWarm">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.analogWarmthIntensity.warm')}}
</button>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext === true">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.audioSpatialization')}}
<br>
<small>{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.audio.spatial" v-on:change="toggleSpatial" switch/>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext === true">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.tunedAudioSpatialization')}}
<br>
<small>{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.tunedAudioSpatialization.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.audio.maikiwiAudio.spatial" :disabled="app.cfg.audio.spatial === false" v-on:change="toggleSpatial" switch/>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.audio.maikiwiAudio.spatial === true && app.cfg.audio.spatial === true">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.tunedAudioSpatialization.profile')}}
<br>
<small>{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.tunedAudioSpatialization.profile.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" :disabled="app.cfg.audio.maikiwiAudio.spatialType === 0" v-model="app.cfg.audio.maikiwiAudio.spatialType" onclick="app.cfg.audio.maikiwiAudio.spatialType = 0; CiderAudio.hierarchical_loading();">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.tunedAudioSpatialization.profile.standard')}}
</button>
<button class="md-btn" :disabled="app.cfg.audio.maikiwiAudio.spatialType === 1" v-model="app.cfg.audio.maikiwiAudio.spatialType" onclick="app.cfg.audio.maikiwiAudio.spatialType = 1; CiderAudio.hierarchical_loading();">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.tunedAudioSpatialization.profile.audiophile')}}
</button>
</div>
</div>
<div style="opacity: 0.5; pointer-events: none">
<div class="md-option-header">
<span>{{$root.getLz('settings.header.unfinished')}}</span>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext === true">
<div class="md-option-segment">
Cider Atmosphere Realizer™
<br>
<small>Realizes an entirely different musical atmosphere only to be found on state of the art audio setups.</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn">
Signature
</button>
<button class="md-btn">
Signature+
</button>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext === true">
<div class="md-option-segment">
Cider Origami™ Vocal Enhancer/Remasterer
<br>
<small>Re-textures the vocals by carving out the frequencies and adjusts them to the selected profile.<br>
<b>Modern:</b>
Embracing 21st Century Equipment, this revives old recordings while preserving the Master's original intent.<br>
<b>Intimate:</b>
Bringing the vocals closer to your heart, communicating only the most personal connection between you and the artist.<br>
<b>Breathy:</b>
Giving the perfectionists a new voice, this adds naturality to the vocals by making them more breathy and more natural. <br>
<b>Articulate:</b>
Wrapping every detail of the vocal to your ear, resulting in a more expressive voice.
</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<select class="md-select">
<option value="none">{{$root.getLz('settings.header.visual.windowBackgroundStyle.none')}}
</option>
<option value="modern">
Modern
</option>
<option value="intimate">
Intimate
</option>
<option value="breathy">
Breathy
</option>
<option value="articulate">
Articulate
</option>
</select>
</div>
</div>
</div>
</div>
</script>
<script>
Vue.component('audiolabs-page', {
template: "#audiolabs-page",
props: [],
data: function () {
return {
app: this.$root,
}
},
mounted: function () {
},
methods: {
toggleSpatial: function () {
if (app.cfg.audio.spatial) {
if (app.cfg.audio.maikiwiAudio.spatial === false) {
if (app.mk.volume - 0.2512 > 0) {app.mk.volume -= 0.2512}
else {app.mk.volume = 0.0001}
}
CiderAudio.spatialOn()
CiderAudio.hierarchical_loading();
if (app.cfg.audio.maikiwiAudio.spatial === true) {
if (app.mk.volume + 0.2512 < 1) {app.mk.volume += 0.2512}
else {app.mk.volume = 1}
}
}
else {
if (app.cfg.audio.maikiwiAudio.spatial === true) {
if (app.mk.volume - 0.2512 > 0) {app.mk.volume -= 0.2512}
else {app.mk.volume = 0.0001}
}
app.cfg.audio.maikiwiAudio.spatial = false;
CiderAudio.spatialOff()
}
},
ciderPPEStandard: function () {
app.cfg.audio.maikiwiAudio.ciderPPE_value = 0.5;
let LLPW_GAIN = [0.38, -1.81, -0.23, -0.51, 0.4, 0.84, 0.36, -0.34, 0.27, -1.2, -0.42, -0.67, 0.81, 1.31, -0.71, 0.68, -1.04, 0.79, -0.73, -1.33, 1.17, 0.57, 0.35, 6.33];
for (let i = 0; i < 24; i++) {
CiderAudio.audioNodes.llpw[i].gain.value = LLPW_GAIN[i];
}
},
ciderPPEClarity: function () {
app.cfg.audio.maikiwiAudio.ciderPPE_value = 0.55;
let c_LLPW_GAIN = [-0.11, 0.27, -0.8, 0.57, 1.84, -0.38, 0.47, -1.56, 0.83, 1.58, -1.79, -0.45, 0.48, 1.22, -1.58, -1.59, -2.03, 2.56, -2.2, -2.48, 4.75, 10.5, 1.43, 3.76];
for (let i = 0; i < 24; i++) {
CiderAudio.audioNodes.llpw[i].gain.value = LLPW_GAIN[i];
}
},
analogWarmthSmooth: function () {
app.cfg.audio.maikiwiAudio.analogWarmth_value = 1.25
let WARMTH_GAIN = [-4.81, 0.74, 0.55, -0.84, -1.52, 0.84, 0.66, -0.29, 0.29, 0.94, 1.67, 1.62, -0.53, -0.81, -4.98, 1.43, 0.86, 1.13, -1.06, -0.95, -1.13, 1.78, -3.86];
for (let i = 0; i < 23; i++) {
CiderAudio.audioNodes.analogWarmth[i].gain.value = WARMTH_GAIN[i] * 1.25;
}
},
analogWarmthWarm: function () {
app.cfg.audio.maikiwiAudio.analogWarmth_value = 1.75
let WARMTH_GAIN = [-4.81, 0.74, 0.55, -0.84, -1.52, 0.84, 0.66, -0.29, 0.29, 0.94, 1.67, 1.62, -0.53, -0.81, -4.98, 1.43, 0.86, 1.13, -1.06, -0.95, -1.13, 1.78, -3.86];
for (let i = 0; i < 23; i++) {
CiderAudio.audioNodes.analogWarmth[i].gain.value = WARMTH_GAIN[i] * 1.75;
}
}
}})
</script>

View file

@ -21,7 +21,7 @@
:video-priority="true"
:url="(data.attributes != null && data.attributes.artwork != null) ? data.attributes.artwork.url : ((data.relationships != null && data.relationships.tracks.data.length > 0 && data.relationships.tracks.data[0].attributes != null) ? ((data.relationships.tracks.data[0].attributes.artwork != null)? data.relationships.tracks.data[0].attributes.artwork.url : ''):'')"
:video="(data.attributes != null && data.attributes.editorialVideo != null) ? (data.attributes.editorialVideo.motionDetailSquare ? data.attributes.editorialVideo.motionDetailSquare.video : (data.attributes.editorialVideo.motionSquareVideo1x1 ? data.attributes.editorialVideo.motionSquareVideo1x1.video : '')) : '' "
size="260"
size="500"
></mediaitem-artwork>
</div>
</div>
@ -32,23 +32,32 @@
{{data.attributes ? (data.attributes.name ??
(data.attributes.title ?? '') ?? '') : ''}}
</div>
<div class="playlist-name" v-show="nameEditing"><input type="text" spellcheck="false"
<div class="playlist-name" v-show="nameEditing"><input type="text"
spellcheck="false"
class="nameEdit"
v-model="data.attributes.name"
@blur="editPlaylist"
@change="editPlaylist"
@keydown.enter="editPlaylist"/></div>
@keydown.enter="editPlaylist"/>
</div>
<div class="playlist-artist item-navigate"
v-if="getArtistName(data) != ''"
v-if="getArtistName(data) != '' && !useArtistChip"
@click="data.attributes && data.attributes.artistName ? app.searchAndNavigate(data,'artist') : ''">
{{getArtistName(data)}}
</div>
<div class="playlist-desc" v-if="data.attributes.description && (data.attributes.description.standard || data.attributes.description.short)">
<div v-if="data.attributes.description.short" class="content" v-html="data.attributes.description.short"></div>
<div v-else-if="data.attributes.description.standard" class="content" v-html="data.attributes.description.standard"></div>
<template v-if="useArtistChip">
<artist-chip v-for="artist in data.relationships.artists.data"
:item="artist"></artist-chip>
</template>
<div class="playlist-desc"
v-if="data.attributes.description && (data.attributes.description.standard || data.attributes.description.short)">
<div v-if="data.attributes.description.short" class="content"
v-html="data.attributes.description.short"></div>
<div v-else-if="data.attributes.description.standard" class="content"
v-html="data.attributes.description.standard"></div>
<button v-if="data.attributes.description.short" class="more-btn"
@click="editorialNotesExpanded = !editorialNotesExpanded">
{{app.getLz('term.showMore')}}
{{app.getLz('term.showMore')}}
</button>
</div>
</div>
@ -57,26 +66,31 @@
<div class="playlist-desc-expanded">
<div class="content"
v-html="((data.attributes.editorialNotes) ? (data.attributes.editorialNotes.standard ?? (data.attributes.editorialNotes.short ?? '') ) : (data.attributes.description ? (data.attributes.description.standard ?? (data.attributes.description.short ?? '')) : ''))"></div>
<button class="more-btn" @click="editorialNotesExpanded = !editorialNotesExpanded">{{app.getLz('term.showLess')}}
<button class="more-btn" @click="editorialNotesExpanded = !editorialNotesExpanded">
{{app.getLz('term.showLess')}}
</button>
</div>
</template>
<div class="playlist-controls" v-observe-visibility="{callback: isHeaderVisible}">
<button class="md-btn md-btn-primary md-btn-icon" style="min-width: 100px;"
@click="app.mk.shuffleMode = 0; play()"> <img class="md-ico-play">
{{app.getLz('term.play')}}
@click="app.mk.shuffleMode = 0; play()"><img class="md-ico-play">
{{app.getLz('term.play')}}
</button>
<button class="md-btn md-btn-primary md-btn-icon" style="min-width: 100px;"
@click="app.mk.shuffleMode = 1;play()"> <img class="md-ico-shuffle">
{{app.getLz('term.shuffle')}}
@click="app.mk.shuffleMode = 1;play()"><img class="md-ico-shuffle">
{{app.getLz('term.shuffle')}}
</button>
<button class="md-btn md-btn-icon" style="min-width: 180px;" v-if="inLibrary!=null && confirm!=true"
@click="confirmButton()"> <img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{ (!inLibrary) ? app.getLz('action.addToLibrary') : app.getLz("action.removeFromLibrary") }}
<button class="md-btn md-btn-icon" style="min-width: 180px;"
v-if="inLibrary!=null && confirm!=true"
@click="confirmButton()"><img
:class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{ (!inLibrary) ? app.getLz('action.addToLibrary') :
app.getLz("action.removeFromLibrary") }}
</button>
<button class="md-btn md-btn-icon" style="min-width: 180px;" v-if="confirm==true"
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) "> <img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{app.getLz('term.confirm')}}
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
<img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{app.getLz('term.confirm')}}
</button>
<button class="more-btn-round" style="float:right;" @click="menu">
<div class="svg-icon"></div>
@ -90,7 +104,8 @@
</div>
</div>
<div class="floating-header" :style="{opacity: (headerVisible ? 0 : 1),'pointer-events': (headerVisible ? 'none' : '')}">
<div class="floating-header"
:style="{opacity: (headerVisible ? 0 : 1),'pointer-events': (headerVisible ? 'none' : '')}">
<div class="row">
<div class="col">
<h3>{{data.attributes ? (data.attributes.name ??
@ -99,20 +114,24 @@
<div class="col-auto flex-center">
<div>
<button class="md-btn md-btn-primary md-btn-icon" style="min-width: 100px;"
@click="app.mk.shuffleMode = 0; play()"> <img class="md-ico-play">
{{app.getLz('term.play')}}
@click="app.mk.shuffleMode = 0; play()"><img class="md-ico-play">
{{app.getLz('term.play')}}
</button>
<button class="md-btn md-btn-primary md-btn-icon" style="min-width: 100px;"
@click="app.mk.shuffleMode = 1;play()"> <img class="md-ico-shuffle">
{{app.getLz('term.shuffle')}}
@click="app.mk.shuffleMode = 1;play()"><img class="md-ico-shuffle">
{{app.getLz('term.shuffle')}}
</button>
<button class="md-btn md-btn-icon" style="min-width: 180px;" v-if="inLibrary!=null && confirm!=true"
@click="confirmButton()"> <img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{ (!inLibrary) ? app.getLz('action.addToLibrary') : app.getLz("action.removeFromLibrary") }}
<button class="md-btn md-btn-icon" style="min-width: 180px;"
v-if="inLibrary!=null && confirm!=true"
@click="confirmButton()"><img
:class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{ (!inLibrary) ? app.getLz('action.addToLibrary') :
app.getLz("action.removeFromLibrary") }}
</button>
<button class="md-btn md-btn-icon" style="min-width: 180px;" v-if="confirm==true"
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) "> <img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{app.getLz('term.confirm')}}
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
<img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{app.getLz('term.confirm')}}
</button>
</div>
</div>
@ -128,7 +147,9 @@
<div style="width:100%">
<draggable :sort="data.attributes.canEdit && data.type == 'library-playlists'"
v-model="data.relationships.tracks.data" @start="drag=true" @end="drag=false;put()">
<mediaitem-list-item :item="item" :parent="getItemParent(data)" :index="index" :showIndex="true" :showIndexPlaylist="(data.attributes.playParams.kind ?? data.type ?? '').includes('playlist')"
<mediaitem-list-item :item="item" :parent="getItemParent(data)" :index="index"
:showIndex="true"
:showIndexPlaylist="(data.attributes.playParams.kind ?? data.type ?? '').includes('playlist')"
:context-ext="buildContextMenu()"
v-for="(item,index) in data.relationships.tracks.data"></mediaitem-list-item>
</draggable>
@ -156,21 +177,26 @@
style="width: 50%;">
{{data.attributes.copyright}}
</div>
<template v-if="(data.attributes?.playParams?.kind ?? data.type ?? '').includes('album') && data.relationships.catalog != null && data.relationships.catalog != null && data.relationships.catalog.data.length > 0">
<div class="playlist-time showExtended item-navigate" style="color:#fa586a; font-weight: bold" @click="app.routeView(data.relationships.catalog.data[0])">
<template
v-if="(data.attributes?.playParams?.kind ?? data.type ?? '').includes('album') && data.relationships.catalog != null && data.relationships.catalog != null && data.relationships.catalog.data.length > 0">
<div class="playlist-time showExtended item-navigate" style="color:#fa586a; font-weight: bold"
@click="app.routeView(data.relationships.catalog.data[0])">
{{$root.getLz("action.showAlbum")}}
</div>
</template>
</div>
</template>
<hr>
<template v-if="typeof data.meta != 'undefined'">
<div v-for="view in data.meta.views.order" v-if="data.views[view].data.length != 0">
<div class="row" >
<div class="row">
<div class="col">
<h3>{{ data.views[view].attributes.title }}</h3>
</div>
</div>
<div>
<mediaitem-scroller-horizontal :items="data.views[view].data"></mediaitem-scroller-horizontal>
<div class="row">
<div class="col">
<mediaitem-scroller-horizontal
:items="data.views[view].data"></mediaitem-scroller-horizontal>
</div>
</div>
</div>
</template>
@ -196,7 +222,8 @@
app: this.$root,
itemBadges: [],
badgesRequested: false,
headerVisible: true
headerVisible: true,
useArtistChip: false,
}
},
mounted: function () {
@ -246,10 +273,11 @@
setTimeout(() => this.confirm = false, 3000);
},
getArtistName(data) {
console.log(data.attributes)
if (data.attributes.artistName) {
this.useArtistChip = true
return data.attributes.artistName
} else if (data.attributes.artist) {
this.useArtistChip = true
return data.attributes.artist.attributes.name
} else if (data.attributes.curatorName) {
return data.attributes.curatorName
@ -287,7 +315,7 @@
this.confirm = false
},
async removeFromLibrary(id) {
const params = {"fields[somgs]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library"};
const params = {"fields[songs]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library"};
var id = this.data.id ?? this.data.attributes.playParams.id
const res = await app.mkapi(this.data.attributes.playParams.kind ?? this.data.type, this.data.attributes.playParams.isLibrary ?? false, this.data.attributes.playParams.id ?? this.data.id, params);
if (res.data.data[0] && res.data.data[0].relationships && res.data.data[0].relationships.library && res.data.data[0].relationships.library.data && res.data.data[0].relationships.library.data.length > 0) {
@ -295,11 +323,12 @@
}
let kind = this.data.attributes.playParams.kind ?? this.data.type ?? '';
const truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`, {},
{
fetchOptions: {
method: "DELETE"
}})
fetchOptions: {
method: "DELETE"
}
})
this.inLibrary = false
this.confirm = false
},
@ -339,18 +368,18 @@
if (!this.data.attributes.canEdit) {
return
}
console.log('sds',this.convert())
console.log('sds', this.convert())
await app.mk.api.v3.music(
`/v1/me/library/playlists/${this.data.attributes.playParams.id}/tracks`,
{},
{
fetchOptions: {
method: "PUT",
body: JSON.stringify({
data: this.convert()
})
`/v1/me/library/playlists/${this.data.attributes.playParams.id}/tracks`,
{},
{
fetchOptions: {
method: "PUT",
body: JSON.stringify({
data: this.convert()
})
}
}
}
)
},
async remove() {
@ -386,7 +415,17 @@
})
},
menu(event) {
app.showMenuPanel({
let self = this
let artistId = null
if(typeof this.data.relationships.artists != "undefined") {
artistId = this.data.relationships.artists.data[0].id
if(this.data.relationships.artists.data[0].type == "library-artists") {
artistId = this.data.relationships.artists.data[0].relationships.catalog.data[0].id
}
}
let menuItems = {
items: {
"share": {
name: app.getLz('term.share'),
@ -415,9 +454,42 @@
app.copyToClipboard(res.data.data[0].attributes.url)
})
}
}
},
"follow": {
name: app.getLz('action.follow'),
icon: "./assets/feather/plus-circle.svg",
hidden: false,
action: () => {
app.followArtistById(artistId, true)
}
},
"unfollow": {
name: app.getLz('action.unfollow'),
icon: "./assets/feather/x-circle.svg",
hidden: true,
action: () => {
app.followArtistById(artistId, false)
}
},
}
}, event)
}
if(artistId != null) {
if(app.followingArtist(artistId)){
menuItems.items.follow.hidden = true
menuItems.items.unfollow.hidden = false
} else {
menuItems.items.follow.hidden = false
menuItems.items.unfollow.hidden = true
}
}else{
menuItems.items.follow.hidden = true
menuItems.items.unfollow.hidden = true
}
app.showMenuPanel(menuItems, event)
},
getItemParent: function (data) {
kind = data.attributes.playParams.kind;
@ -430,13 +502,13 @@
if (date == null || date === "") return "";
switch (date) {
case this.data.attributes.releaseDate:
prefix = this.app.getLz('term.time.released')+ ' '
prefix = this.app.getLz('term.time.released') + ' '
break;
case this.data.attributes.lastModifiedDate:
prefix = this.app.getLz('term.time.updated')+ ' '
prefix = this.app.getLz('term.time.updated') + ' '
break;
case this.data.attributes.dateAdded:
prefix = this.app.getLz('term.time.added')+ ' '
prefix = this.app.getLz('term.time.added') + ' '
break;
}
let month, year;
@ -446,13 +518,22 @@
// date = releaseDate.getDate();
// year = releaseDate.getFullYear();
let formatted = ''
try {formatted = new Intl.DateTimeFormat(this.app.cfg.general.language?.replace('_','-') ?? 'en-US', {day:'numeric',month: 'long', year: 'numeric'}).format(releaseDate);}
catch(e){
try {
formatted = new Intl.DateTimeFormat(this.app.cfg.general.language?.replace('_', '-') ?? 'en-US', {
day: 'numeric',
month: 'long',
year: 'numeric'
}).format(releaseDate);
} catch (e) {
// use the format in json instead
if (this.app.getLz('date.format') != null){
if (this.app.getLz('date.format') != null) {
formatted = new this.app.getLz('date.format').replace("${d}", releaseDate.getDate()).replace("${m}", releaseDate.getMonth()).replace("${y}", releaseDate.getFullYear());
} else {
formatted = new Intl.DateTimeFormat('en-US', {day:'numeric',month: 'long', year: 'numeric'}).format(releaseDate);
formatted = new Intl.DateTimeFormat('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric'
}).format(releaseDate);
}
}
return prefix + formatted
@ -478,7 +559,7 @@
let query = (this.data ?? app.showingPlaylist).relationships.tracks.data.map(item => new MusicKit.MediaItem(item));
app.mk.stop().then(function () {
app.mk.setQueue({[truekind]: [id]}).then(function () {
app.mk.setQueue({[truekind]: [id], parameters: {l: app.mklang}}).then(function () {
app.mk.play().then(function () {
if (query.length > 100) {
let u = query.slice(100);

View file

@ -1,12 +1,12 @@
<script type="text/x-template" id="cider-collection-list">
<div class="content-inner collection-page">
<h3 class="header-text" v-observe-visibility="{callback: headerVisibility}">{{ title }}</h3>
<div v-if="data['data'] != 'null'" class="well">
<div v-if="data['data'] != 'null'" class="well itemContainer">
<template v-for="(item, key) in data.data">
<template v-if="item.type == 'artists'">
<mediaitem-square :item="item"></mediaitem-square>
</template>
<template v-else>
<template v-else>
<mediaitem-list-item
v-if="getKind(item) == 'song'"
:index="key"
@ -14,8 +14,7 @@
<mediaitem-square v-else :item="item" :type="getKind(item)"></mediaitem-square>
</template>
</template>
<button v-if="triggerEnabled" style="opacity:0;height: 32px;"
v-observe-visibility="{callback: visibilityChanged}">{{this.app.getLz('term.showMore')}}
<button v-if="triggerEnabled" style="opacity:0;height: 32px;" v-observe-visibility="{callback: visibilityChanged}">{{this.app.getLz('term.showMore')}}
</button>
</div>
<transition name="fabfade">
@ -23,7 +22,9 @@
<%- include("../svg/arrow-up.svg") %>
</button>
</transition>
<div class="well" v-show="loading"><div class="spinner"></div></div>
<div class="well itemContainer" v-show="loading">
<div class="spinner"></div>
</div>
</div>
</script>
<script>
@ -44,7 +45,7 @@
default: "artists"
}
},
data: function () {
data: function() {
return {
triggerEnabled: true,
canSeeTrigger: false,
@ -92,8 +93,8 @@
this.triggerEnabled = true;
}
this.loading = false
}else{
if(!response.data.results[app.collectionList.response.groups]) {
} else {
if (!response.data.results[app.collectionList.response.groups]) {
this.loading = false
return
}
@ -106,14 +107,14 @@
}
})
},
headerVisibility: function (isVisible, entry) {
headerVisibility: function(isVisible, entry) {
if (isVisible) {
this.showFab = false;
} else {
this.showFab = true;
}
},
visibilityChanged: function (isVisible, entry) {
visibilityChanged: function(isVisible, entry) {
if (isVisible) {
this.canSeeTrigger = true;
this.getNext();

View file

@ -3,7 +3,14 @@
<div v-if="page == 'main'">
<div class="row">
<div class="col">
<h3>{{app.getLz('home.recentlyPlayed')}}</h3>
<div class="row">
<div class="col nopadding">
<h3>{{app.getLz('home.recentlyPlayed')}}</h3>
</div>
<div class="col-auto nopadding flex-center">
<button class="cd-btn-seeall" @click="seeAllHistory()">{{app.getLz('term.history')}}</button>
</div>
</div>
<div class="well artistfeed-well">
<template v-if="isSectionReady('recentlyPlayed')">
<mediaitem-list-item v-for="item in recentlyPlayed.limit(6)"
@ -13,7 +20,7 @@
</div>
</div>
<div class="col">
<div class="row nopadding">
<div class="row">
<div class="col nopadding">
<h3>{{app.getLz('home.artistsFeed')}}</h3>
</div>
@ -42,13 +49,24 @@
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<div class="row" v-if="!seenReplay">
<div class="col">
<button class="md-btn md-btn-block md-btn-replay--hero" @click="$root.appRoute('replay')">{{$root.getLz('term.replay')}} {{ year }}</button>
</div>
</div>
<div class="row">
<div class="col">
<h3>{{app.getLz('home.madeForYou')}}</h3>
<div class="row">
<div class="col nopadding">
<h3>{{app.getLz('home.madeForYou')}}</h3>
</div>
<div class="col-auto nopadding flex-center">
<button class="md-btn md-btn-replay" v-if="seenReplay" @click="$root.appRoute('replay')">{{$root.getLz('term.replay')}} {{ year }}</button>
</div>
</div>
<div class="well">
<template v-if="isSectionReady('madeForYou')">
<mediaitem-square kind="small" v-for="item in madeForYou" :item="item"></mediaitem-square>
</template>
<mediaitem-scroller-horizontal :items="madeForYou" v-if="isSectionReady('madeForYou')">
</mediaitem-scroller-horizontal>
<div class="spinner" v-else></div>
</div>
</div>
@ -64,10 +82,8 @@
</div>
</div>
<div class="well">
<template v-if="isSectionReady('friendsListeningTo')">
<mediaitem-square kind="small" v-for="item in friendsListeningTo"
:item="item"></mediaitem-square>
</template>
<mediaitem-scroller-horizontal :items="friendsListeningTo" v-if="isSectionReady('friendsListeningTo')">
</mediaitem-scroller-horizontal>
<div class="spinner" v-else></div>
</div>
</div>
@ -94,7 +110,9 @@
artistFeed: [],
showingArtistFeed: false,
page: "main",
sectionsReady: []
sectionsReady: [],
year: new Date().getFullYear(),
seenReplay: localStorage.getItem('seenReplay')
}
},
async mounted() {
@ -102,8 +120,18 @@
this.getListenNowData()
await this.getArtistFeed()
await this.getFavorites()
if (new Date().getMonth() == 11) {
this.seenReplay = false
localStorage.setItem('seenReplay', false)
}
},
methods: {
async seeAllHistory() {
let hist = await app.mk.api.v3.music(`/v1/me/recent/played/tracks`, {
l: this.$root.mklang
})
app.showCollection(hist.data, app.getLz('term.history'))
},
isSectionReady(section) {
return this.sectionsReady.includes(section)
},
@ -131,13 +159,17 @@
playlists.push(item.id)
}
}
if (playlists.length != 0) {
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/playlists/${playlists.toString()}`).then(playlistsData => {
if (playlists.length != 0) {
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/playlists/${playlists.toString()}`, {
l: this.$root.mklang
}).then(playlistsData => {
self.favorites.push(...playlistsData.data)
})
}
if (libraryPlaylists.length != 0) {
this.app.mk.api.v3.music(`v1/me/library/playlists/${playlists.toString()}`).then(playlistsData => {
if (libraryPlaylists.length != 0) {
this.app.mk.api.v3.music(`v1/me/library/playlists/${playlists.toString()}`, {
l: this.$root.mklang
}).then(playlistsData => {
self.favorites.push(...playlistsData.data)
})
}
@ -145,28 +177,34 @@
async getArtistFeed() {
let artists = this.followedArtists
let self = this
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists?ids=${artists.toString()}&views=latest-release&include[songs]=albums&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount&limit[artists:top-songs]=2&art[url]=f`).then(artistData => {
artistData.data.data.forEach(item => {
let chunks = []
for (let artistIdx = 0; artistIdx < artists.length; artistIdx += 50) {
chunks.push(artists.slice(artistIdx, artistIdx + 50));
}
try {
const chunkArtistData = await Promise.all(chunks.map(chunk =>
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists?ids=${chunk.toString()}&views=latest-release&include[songs]=albums&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount&limit[artists:top-songs]=2&art[url]=f`)))
chunkArtistData.forEach(chunkResult =>
chunkResult.data.data.forEach(item => {
if (item.views["latest-release"].data.length != 0) {
self.artistFeed.push(item.views["latest-release"].data[0])
}
})
// sort artistFeed by attributes.releaseDate descending, date is formatted as "YYYY-MM-DD"
}))
// sort artistFeed by attributes.releaseDate descending, date is formatted as "YYYY-MM-DD"
this.artistFeed.sort((a, b) => {
let dateA = new Date(a.attributes.releaseDate)
let dateB = new Date(b.attributes.releaseDate)
return dateB - dateA
})
})
} catch (error) { }
},
getRecentlyPlayed() {
},
async getListenNowData() {
let self = this
this.app.mk.api.v3.music(`/v1/me/recommendations?timezone=${encodeURIComponent(app.formatTimezoneOffset())}&name=listen-now&with=friendsMix,library,social&art[social-profiles:url]=c&art[url]=c,f&omit[resource]=autos&relate[editorial-items]=contents&extend=editorialCard,editorialVideo&extend[albums]=artistUrl&extend[library-albums]=artistUrl,editorialVideo&extend[playlists]=artistNames,editorialArtwork,editorialVideo&extend[library-playlists]=artistNames,editorialArtwork,editorialVideo&extend[social-profiles]=topGenreNames&include[albums]=artists&include[songs]=artists&include[music-videos]=artists&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url&fields[artists]=name,url&extend[stations]=airDate,supportsAirTimeUpdates&meta[stations]=inflectionPoints&types=artists,albums,editorial-items,library-albums,library-playlists,music-movies,music-videos,playlists,stations,uploaded-audios,uploaded-videos,activities,apple-curators,curators,tv-shows,social-upsells&platform=web`).then((data) => {
this.app.mk.api.v3.music(`/v1/me/recommendations?timezone=${encodeURIComponent(app.formatTimezoneOffset())}&name=listen-now&with=friendsMix,library,social&art[social-profiles:url]=c&art[url]=c,f&omit[resource]=autos&relate[editorial-items]=contents&extend=editorialCard,editorialVideo&extend[albums]=artistUrl&extend[library-albums]=artistUrl,editorialVideo&extend[playlists]=artistNames,editorialArtwork,editorialVideo&extend[library-playlists]=artistNames,editorialArtwork,editorialVideo&extend[social-profiles]=topGenreNames&include[albums]=artists&include[songs]=artists&include[music-videos]=artists&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url&fields[artists]=name,url&extend[stations]=airDate,supportsAirTimeUpdates&meta[stations]=inflectionPoints&types=artists,albums,editorial-items,library-albums,library-playlists,music-movies,music-videos,playlists,stations,uploaded-audios,uploaded-videos,activities,apple-curators,curators,tv-shows,social-upsells&platform=web&l=${this.$root.mklang}`).then((data) => {
console.log(data.data.data[1])
try {
self.madeForYou = data.data.data.filter(section => {
@ -193,7 +231,7 @@
self.profile = response.data.data[0]
})
}
}
}
});
</script>

View file

@ -1,11 +1,11 @@
<template v-if="page == 'library-albums'">
<script type="text/x-template" id="cider-library-albums">
<div class="content-inner">
<div class="row">
<div class="col" style="padding:0;">
<h1 class="header-text">{{$root.getLz('term.albums')}}</h1>
</div>
<div class="col-auto">
<button v-if="library.albums.downloadState == 2" @click="getLibraryAlbumsFull(true, 1)" class="reload-btn"><%- include('../svg/redo.svg') %></button>
<button v-if="library.albums.downloadState == 2" @click="$root.getLibraryAlbumsFull(true, 1)" class="reload-btn"><%- include('../svg/redo.svg') %></button>
</div>
</div>
<div class="row">
@ -16,21 +16,21 @@
style="width:100%;"
spellcheck="false"
:placeholder="$root.getLz('term.search') + '...'"
@input="searchLibraryAlbums"
@input="$root.searchLibraryAlbums"
v-model="library.albums.search" class="search-input">
</div>
</div>
<div class="col-auto flex-center">
<div class="row">
<div class="col">
<select class="md-select" v-model="library.albums.sorting[1]" @change="searchLibraryAlbums(1)">
<select class="md-select" v-model="prefs.sort" @change="library.albums.sorting[1] = prefs.sort; $root.searchLibraryAlbums(1)">
<optgroup :label="$root.getLz('term.sortBy')">
<option v-for="(sort, index) in library.albums.sortingOptions" :value="index">{{ sort }}</option>
</optgroup>
</select>
</div>
<div class="col">
<select class="md-select" v-model="library.albums.sortOrder[1]" @change="searchLibraryAlbums(1)">
<select class="md-select" v-model="prefs.sortOrder" @change="library.albums.sortOrder[1] = prefs.sortOrder; $root.searchLibraryAlbums(1)">
<optgroup :label="$root.getLz('term.sortOrder')">
<option value="asc">{{$root.getLz('term.sortOrder.ascending')}}</option>
<option value="desc">{{$root.getLz('term.sortOrder.descending')}}</option>
@ -38,7 +38,7 @@
</select>
</div>
<div class="col">
<select class="md-select" v-model="library.albums.viewAs">
<select class="md-select" v-model="prefs.viewAs">
<optgroup :label="$root.getLz('term.viewAs')">
<option value="covers">{{$root.getLz('term.viewAs.coverArt')}}</option>
<option value="list">{{$root.getLz('term.viewAs.list')}}</option>
@ -51,12 +51,27 @@
<div class="well">
<div class="albums-square-container">
<div>
<mediaitem-square v-if="library.albums.viewAs == 'covers'" :size="'300'" :item="item" v-for="item in library.albums.displayListing">
<mediaitem-square v-if="prefs.viewAs == 'covers'" :size="'300'" :item="item" v-for="item in library.albums.displayListing">
</mediaitem-square>
</div>
</div>
<mediaitem-list-item v-if="library.albums.viewAs == 'list'" :show-duration="false" :show-meta-data="true" :show-library-status="false" :item="item" v-for="item in library.albums.displayListing">
<mediaitem-list-item v-if="prefs.viewAs == 'list'" :show-duration="false" :show-meta-data="true" :show-library-status="false" :item="item" v-for="item in library.albums.displayListing">
</mediaitem-list-item>
</div>
</div>
</template>
</script>
<script>
Vue.component('cider-library-albums', {
template: '#cider-library-albums',
data: function () {
return {
library: this.$root.library,
mediaItemSize: "compact",
prefs: this.$root.cfg.libraryPrefs.albums,
app : this.$root
}
},
methods: {
}
});
</script>

View file

@ -1,40 +1,35 @@
<div class="content-inner">
<div class="row">
<div class="col" style="padding:0;">
<h1 class="header-text">{{$root.getLz('term.artists')}}</h1>
</div>
<div class="content-inner">
<div class="row">
<div class="col" style="padding:0;">
<h1 class="header-text">{{$root.getLz('term.artists')}}</h1>
</div>
<div class="row">
<div class="col" style="padding:0;">
<div class="search-input-container" style="width:100%;margin: 16px 0;">
<div class="search-input--icon"></div>
<input type="search"
style="width:100%;"
spellcheck="false"
:placeholder="$root.getLz('term.search') + '...'"
@input="searchLibraryArtists"
v-model="library.artists.search" class="search-input">
</div>
</div>
<div class="row">
<div class="col" style="padding:0;">
<div class="search-input-container" style="width:100%;margin: 16px 0;">
<div class="search-input--icon"></div>
<input type="search" style="width:100%;" spellcheck="false" :placeholder="$root.getLz('term.search') + '...'" @input="searchLibraryArtists" v-model="library.artists.search" class="search-input">
</div>
<div class="col-auto flex-center">
<div class="row">
<!-- <div class="col">
</div>
<div class="col-auto flex-center">
<div class="row">
<!-- <div class="col">
<select class="md-select" v-model="library.artists.sorting[1]" @change="searchLibraryArtists(1)">
<optgroup label="Sort By">
<option v-for="(sort, index) in library.artists.sortingOptions" :value="index">{{ sort }}</option>
</optgroup>
</select>
</div> -->
<div class="col">
<select class="md-select" v-model="library.artists.sortOrder[1]" @change="searchLibraryArtists(1)">
<div class="col">
<select class="md-select" v-model="library.artists.sortOrder[1]" @change="searchLibraryArtists(1)">
<optgroup :label="$root.getLz('term.sortOrder')">
<option value="asc">{{$root.getLz('term.sortOrder.ascending')}}</option>
<option value="desc">{{$root.getLz('term.sortOrder.descending')}}</option>
</optgroup>
</select>
</div>
<!-- <div class="col">
</div>
<!-- <div class="col">
<select class="md-select" v-model="library.artists.viewAs">
<optgroup label="View As">
<option value="covers">Cover Art</option>
@ -42,13 +37,13 @@
</optgroup>
</select>
</div> -->
</div>
</div>
</div>
<div class="well">
<!-- <mediaitem-square v-if="library.artists.viewAs == 'covers'" :item="item" v-for="item in library.artists.displayListing">
</div>
<div class="well">
<!-- <mediaitem-square v-if="library.artists.viewAs == 'covers'" :item="item" v-for="item in library.artists.displayListing">
</mediaitem-square> -->
<libraryartist-item :show-duration="false" :show-meta-data="true" :show-library-status="false" :item="item" v-for="item in library.artists.displayListing">
</libraryartist-item>
</div>
</div>
<libraryartist-item :show-duration="false" :show-meta-data="true" :show-library-status="false" :item="item" v-for="item in library.artists.displayListing">
</libraryartist-item>
</div>
</div>

View file

@ -15,8 +15,16 @@
v-model="library.songs.search" class="search-input">
</div>
</div>
<div class="col-auto flex-center">
<div class="col-auto flex-center">
<div class="row">
<button class="col md-btn md-btn-primary md-btn-icon" style="min-width: 100px;margin-right: 3px;"
@click="app.mk.shuffleMode = 0; play()"> <img class="md-ico-play">
{{app.getLz('term.play')}}
</button>
<button class="col md-btn md-btn-primary md-btn-icon" style="min-width: 100px;margin-right: 3px;"
@click="app.mk.shuffleMode = 1;play()"> <img class="md-ico-shuffle">
{{app.getLz('term.shuffle')}}
</button>
<div class="col">
<select class="md-select" v-model="prefs.sort" @change="$root.searchLibrarySongs()">
<optgroup :label="app.getLz('term.sortBy')">
@ -75,6 +83,28 @@
methods: {
sayHello: function () {
alert('Hello world!');
},
play: function () {
function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
let query = this.app.library.songs.displayListing.map(item => new MusicKit.MediaItem(item));
if (!app.mk.queue.isEmpty)
app.mk.queue.splice(0, app.mk.queue._itemIDs.length);
app.mk.stop().then(() => {
if (app.mk.shuffleMode == 1) {
shuffleArray(query)
}
app.mk.queue.append(query)
app.mk.changeToMediaAtIndex(0)
});
}
}
});

View file

@ -6,8 +6,13 @@
</div>
</div>
<div class="madeforyou-body">
<mediaitem-square :size="300" :item="item" v-for="item in videos">
</mediaitem-square>
<template v-if="videos.length > 0">
<mediaitem-square :size="300" :item="item" v-for="item in videos">
</mediaitem-square>
</template>
<template v-else-if="loaded == true">
<div>{{$root.getLz('term.noVideos')}}</div>
</template>
</div>
</div>
</script>
@ -19,12 +24,14 @@
data: function(){
return {
videos: [],
loaded: false
}
},
mounted() {
this.$nextTick(async function () {
if (this.$data.videos == null || this.$data.videos.length == 0)
this.$data.videos = (await this.$root.mk.api.v3.music('/v1/me/library/music-videos')).data?.data ?? []
this.$data.loaded = true
})
}
})

View file

@ -0,0 +1,604 @@
<script type="text/x-template" id="playlist-inline">
<div class="content-inner playlist-page inline-playlist" @click.self="$root.resetState()">
<div class="playlist-inner" v-if="data != [] && data.attributes != null">
<div class="close-btn" title="Close" @click="$root.resetState()">
<svg fill="white" xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21"
aria-role="presentation" focusable="false">
<path
d="M10.5 21C4.724 21 0 16.275 0 10.5S4.724 0 10.5 0 21 4.725 21 10.5 16.276 21 10.5 21zm-3.543-5.967a.96.96 0 00.693-.295l2.837-2.842 2.85 2.842c.167.167.41.295.693.295.552 0 1.001-.461 1.001-1.012 0-.281-.115-.512-.295-.704L11.899 10.5l2.85-2.855a.875.875 0 00.295-.68c0-.55-.45-.998-1.001-.998a.871.871 0 00-.668.295l-2.888 2.855-2.862-2.843a.891.891 0 00-.668-.281.99.99 0 00-1.001.986c0 .269.116.512.295.678L9.088 10.5l-2.837 2.843a.926.926 0 00-.295.678c0 .551.45 1.012 1.001 1.012z"
fill-rule="nonzero"/>
</svg>
</div>
<template v-if="app.playlists.loadingState == 0">
<div class="content-inner centered">
<div class="spinner"></div>
</div>
</template>
<template v-if="app.playlists.loadingState == 1">
<div class="playlist-display"
:style="{
'--bgColor': (data.attributes.artwork != null && data.attributes.artwork['bgColor'] != null) ? ('#' + data.attributes.artwork.bgColor) : '',
'--textColor': (data.attributes.artwork != null && data.attributes.artwork['textColor1'] != null) ? ('#' + data.attributes.artwork.textColor1) : ''
}">
<div class="playlistInfo">
<div class="row">
<div class="col-auto flex-center">
<div style="width: 260px;height:260px;">
<mediaitem-artwork
shadow="large"
:video-priority="true"
:url="(data.attributes != null && data.attributes.artwork != null) ? data.attributes.artwork.url : ((data.relationships != null && data.relationships.tracks.data.length > 0 && data.relationships.tracks.data[0].attributes != null) ? ((data.relationships.tracks.data[0].attributes.artwork != null)? data.relationships.tracks.data[0].attributes.artwork.url : ''):'')"
:video="(data.attributes != null && data.attributes.editorialVideo != null) ? (data.attributes.editorialVideo.motionDetailSquare ? data.attributes.editorialVideo.motionDetailSquare.video : (data.attributes.editorialVideo.motionSquareVideo1x1 ? data.attributes.editorialVideo.motionSquareVideo1x1.video : '')) : '' "
size="260"
></mediaitem-artwork>
</div>
</div>
<div class="col playlist-info">
<template v-if="!editorialNotesExpanded">
<div>
<div class="playlist-name" @click="editPlaylistName()" v-show="!nameEditing">
{{data.attributes ? (data.attributes.name ??
(data.attributes.title ?? '') ?? '') : ''}}
</div>
<div class="playlist-name" v-show="nameEditing"><input type="text"
spellcheck="false"
class="nameEdit"
v-model="data.attributes.name"
@blur="editPlaylist"
@change="editPlaylist"
@keydown.enter="editPlaylist"/>
</div>
<div class="playlist-artist item-navigate"
v-if="getArtistName(data) != '' && !useArtistChip"
@click="data.attributes && data.attributes.artistName ? app.searchAndNavigate(data,'artist') : ''">
{{getArtistName(data)}}
</div>
<template v-if="useArtistChip">
<artist-chip v-for="artist in data.relationships.artists.data"
:item="artist"></artist-chip>
</template>
<div class="playlist-desc"
v-if="data.attributes.description && (data.attributes.description.standard || data.attributes.description.short)">
<div v-if="data.attributes.description.short" class="content"
v-html="data.attributes.description.short"></div>
<div v-else-if="data.attributes.description.standard" class="content"
v-html="data.attributes.description.standard"></div>
<button v-if="data.attributes.description.short" class="more-btn"
@click="editorialNotesExpanded = !editorialNotesExpanded">
{{app.getLz('term.showMore')}}
</button>
</div>
</div>
</template>
<template v-if="editorialNotesExpanded">
<div class="playlist-desc-expanded">
<div class="content"
v-html="((data.attributes.editorialNotes) ? (data.attributes.editorialNotes.standard ?? (data.attributes.editorialNotes.short ?? '') ) : (data.attributes.description ? (data.attributes.description.standard ?? (data.attributes.description.short ?? '')) : ''))"></div>
<button class="more-btn"
@click="editorialNotesExpanded = !editorialNotesExpanded">
{{app.getLz('term.showLess')}}
</button>
</div>
</template>
<div class="playlist-controls" v-observe-visibility="{callback: isHeaderVisible}">
<button class="md-btn md-btn-primary md-btn-icon" style="min-width: 100px;"
@click="app.mk.shuffleMode = 0; play()"><img class="md-ico-play">
{{app.getLz('term.play')}}
</button>
<button class="md-btn md-btn-primary md-btn-icon" style="min-width: 100px;"
@click="app.mk.shuffleMode = 1;play()"><img class="md-ico-shuffle">
{{app.getLz('term.shuffle')}}
</button>
<button class="md-btn md-btn-icon" style="min-width: 180px;"
v-if="inLibrary!=null && confirm!=true"
@click="confirmButton()"><img
:class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{ (!inLibrary) ? app.getLz('action.addToLibrary') :
app.getLz("action.removeFromLibrary") }}
</button>
<button class="md-btn md-btn-icon" style="min-width: 180px;" v-if="confirm==true"
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
<img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{app.getLz('term.confirm')}}
</button>
<button class="more-btn-round" style="float:right;" @click="menu">
<div class="svg-icon"></div>
</button>
</div>
</div>
</div>
</div>
<div class="artworkContainer" v-if="data.attributes.artwork != null">
<artwork-material :url="data.attributes.artwork.url" size="260" images="1"></artwork-material>
</div>
</div>
<div class="floating-header"
:style="{opacity: (headerVisible ? 0 : 1),'pointer-events': (headerVisible ? 'none' : '')}">
<div class="row">
<div class="col">
<h3>{{data.attributes ? (data.attributes.name ??
(data.attributes.title ?? '') ?? '') : ''}}</h3>
</div>
<div class="col-auto flex-center">
<div>
<button class="md-btn md-btn-primary md-btn-icon" style="min-width: 100px;"
@click="app.mk.shuffleMode = 0; play()"><img class="md-ico-play">
{{app.getLz('term.play')}}
</button>
<button class="md-btn md-btn-primary md-btn-icon" style="min-width: 100px;"
@click="app.mk.shuffleMode = 1;play()"><img class="md-ico-shuffle">
{{app.getLz('term.shuffle')}}
</button>
<button class="md-btn md-btn-icon" style="min-width: 180px;"
v-if="inLibrary!=null && confirm!=true"
@click="confirmButton()"><img
:class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{ (!inLibrary) ? app.getLz('action.addToLibrary') :
app.getLz("action.removeFromLibrary") }}
</button>
<button class="md-btn md-btn-icon" style="min-width: 180px;" v-if="confirm==true"
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
<img :class="(!inLibrary) ? 'md-ico-add' : 'md-ico-remove'">
{{app.getLz('term.confirm')}}
</button>
</div>
</div>
<div class="col-auto flex-center">
<button class="more-btn-round" style="float:right;" @click="menu">
<div class="svg-icon"></div>
</button>
</div>
</div>
</div>
<div class="playlist-body">
<div class="well">
<div style="width:100%">
<draggable :sort="data.attributes.canEdit && data.type == 'library-playlists'"
v-model="data.relationships.tracks.data" @start="drag=true"
@end="drag=false;put()">
<mediaitem-list-item :item="item" :parent="getItemParent(data)" :index="index"
:showIndex="true"
:showIndexPlaylist="(data.attributes.playParams.kind ?? data.type ?? '').includes('playlist')"
:context-ext="buildContextMenu()"
v-for="(item,index) in data.relationships.tracks.data"></mediaitem-list-item>
</draggable>
</div>
</div>
<div class="friends-info" v-if="itemBadges.length != 0">
<div class="well">
<div class="badge-container">
<div class="socialBadge"
:title="`${badge.attributes.name} - @${badge.attributes.handle}`"
v-for="badge in itemBadges">
<mediaitem-artwork
:url="badge.attributes.artwork.url"
:size="60"></mediaitem-artwork>
</div>
</div>
</div>
</div>
<div class="playlist-time">
{{getFormattedDate()}}
</div>
<div class="playlist-time total">{{app.getTotalTime()}}</div>
<div class="playlist-time item-navigate" @click="app.searchAndNavigate(data,'recordLabel') "
style="width: 50%;">
{{data.attributes.copyright}}
</div>
<template
v-if="(data.attributes?.playParams?.kind ?? data.type ?? '').includes('album') && data.relationships.catalog != null && data.relationships.catalog != null && data.relationships.catalog.data.length > 0">
<div class="playlist-time showExtended item-navigate" style="color:#fa586a; font-weight: bold"
@click="app.routeView(data.relationships.catalog.data[0])">
{{$root.getLz("action.showAlbum")}}
</div>
</template>
<hr>
<template v-if="typeof data.meta != 'undefined'">
<div v-for="view in data.meta.views.order" v-if="data.views[view].data.length != 0">
<div class="row">
<div class="col">
<h3>{{ data.views[view].attributes.title }}</h3>
</div>
</div>
<div>
<mediaitem-scroller-horizontal
:items="data.views[view].data"></mediaitem-scroller-horizontal>
</div>
</div>
</template>
</div>
</template>
</div>
<div class="playlist-inner" v-else>
<div class="close-btn" title="Close" @click="$root.resetState()">
<svg fill="white" xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21"
aria-role="presentation" focusable="false">
<path
d="M10.5 21C4.724 21 0 16.275 0 10.5S4.724 0 10.5 0 21 4.725 21 10.5 16.276 21 10.5 21zm-3.543-5.967a.96.96 0 00.693-.295l2.837-2.842 2.85 2.842c.167.167.41.295.693.295.552 0 1.001-.461 1.001-1.012 0-.281-.115-.512-.295-.704L11.899 10.5l2.85-2.855a.875.875 0 00.295-.68c0-.55-.45-.998-1.001-.998a.871.871 0 00-.668.295l-2.888 2.855-2.862-2.843a.891.891 0 00-.668-.281.99.99 0 00-1.001.986c0 .269.116.512.295.678L9.088 10.5l-2.837 2.843a.926.926 0 00-.295.678c0 .551.45 1.012 1.001 1.012z"
fill-rule="nonzero"/>
</svg>
</div>
<template v-if="app.playlists.loadingState == 0">
<div class="content-inner centered">
<div class="spinner"></div>
</div>
</template>
</div>
</div>
</script>
<script>
Vue.component('playlist-inline', {
template: "#playlist-inline",
props: ["data"],
data: function () {
return {
editorialNotesExpanded: false,
drag: false,
nameEditing: false,
inLibrary: null,
confirm: false,
app: this.$root,
itemBadges: [],
badgesRequested: false,
headerVisible: true,
useArtistChip: false,
}
},
mounted: function () {
this.$nextTick(function () {
this.isInLibrary()
})
},
watch: {
data: function () {
this.isInLibrary()
this.getBadges()
}
},
methods: {
isHeaderVisible(visible) {
this.headerVisible = visible
},
getBadges() {
return
if (this.badgesRequested) {
return
}
this.badgesRequested = true
this.itemBadges = []
let self = this
var id = 0
try {
id = this.data.attributes.playParams.id
} catch (e) {
id = this.data.id
}
this.$root.getSocialBadges((badges) => {
let friends = 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])
})
})
}
})
},
confirmButton() {
// Return button to normal state after 3 seconds
this.confirm = true
setTimeout(() => this.confirm = false, 3000);
},
getArtistName(data) {
if (data.attributes.artistName) {
this.useArtistChip = true
return data.attributes.artistName
} else if (data.attributes.artist) {
this.useArtistChip = true
return data.attributes.artist.attributes.name
} else if (data.attributes.curatorName) {
return data.attributes.curatorName
} else {
return ""
}
},
async isInLibrary() {
if (this.data.type && !this.data.type.includes("library")) {
// please keep using vars here
const params = {
"fields[playlists]": "inLibrary",
"fields[albums]": "inLibrary",
"relate": "library"
};
const res = await app.mkapi(this.data.attributes.playParams.kind ?? this.data.type, this.data.attributes.playParams.isLibrary ?? false, this.data.attributes.playParams.id ?? this.data.id, params);
this.inLibrary = (res.data.data[0] && res.data.data[0].attributes && res.data.data[0].attributes.inLibrary) ? res.data.data[0].attributes.inLibrary : false
console.log(res)
} else {
this.inLibrary = true
}
},
editPlaylist() {
this.app.editPlaylist(this.data.id, this.data.attributes.name);
this.app.playlists.listing.forEach(playlist => {
if (playlist.id === this.data.id) {
playlist.attributes.name = this.data.attributes.name
}
})
this.nameEditing = false
},
addToLibrary(id) {
app.mk.addToLibrary(id)
this.inLibrary = true
this.confirm = false
},
async removeFromLibrary(id) {
const params = {"fields[songs]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library"};
var id = this.data.id ?? this.data.attributes.playParams.id
const res = await app.mkapi(this.data.attributes.playParams.kind ?? this.data.type, this.data.attributes.playParams.isLibrary ?? false, this.data.attributes.playParams.id ?? this.data.id, params);
if (res.data.data[0] && res.data.data[0].relationships && res.data.data[0].relationships.library && res.data.data[0].relationships.library.data && res.data.data[0].relationships.library.data.length > 0) {
id = res.data.data[0].relationships.library.data[0].id
}
let kind = this.data.attributes.playParams.kind ?? this.data.type ?? '';
const truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`, {},
{
fetchOptions: {
method: "DELETE"
}
})
this.inLibrary = false
this.confirm = false
},
editPlaylistName() {
if (this.data.attributes.canEdit && this.data.type === "library-playlists") {
this.nameEditing = true
setTimeout(() => {
document.querySelector(".nameEdit").focus()
}, 100)
}
},
buildContextMenu(index) {
let self = this
if (!this.data.attributes.canEdit) {
return
}
return {
normal: [
{
name: app.getLz('action.removeFromPlaylist'),
action: () => {
self.remove()
}
},
],
multiple: [
{
name: app.getLz('action.removeFromPlaylist'),
action: () => {
self.remove()
}
},
]
}
},
async put() {
if (!this.data.attributes.canEdit) {
return
}
console.log('sds', this.convert())
await app.mk.api.v3.music(
`/v1/me/library/playlists/${this.data.attributes.playParams.id}/tracks`,
{},
{
fetchOptions: {
method: "PUT",
body: JSON.stringify({
data: this.convert()
})
}
}
)
},
async remove() {
if (!this.data.attributes.canEdit) {
return
}
// for each app.selectedMediaItems splice the items from the playlist
for (let i = 0; i < app.selectedMediaItems.length; i++) {
let item = app.selectedMediaItems[i]
let index = this.data.relationships.tracks.data.findIndex(x => x.id == item.id)
if (index > -1) {
this.data.relationships.tracks.data.splice(index, 1)
}
}
await this.put()
},
convert() {
let pl_tracks = []
for (let i = 0; i < this.data.relationships.tracks.data.length; i++) {
pl_tracks.push({
id: this.data.relationships.tracks.data[i].id,
type: this.data.relationships.tracks.data[i].type
})
}
return pl_tracks
},
getRecursive(url) {
app.apiCall(app.musicBaseUrl + "/v1/me/library/playlists/p.V7VYlrDso6kkYY/tracks?offset=100", res => {
this.data.relationships.tracks.data = this.data.relationships.tracks.data.concat(res.data.data)
if (res.data.next) {
this.getRecursive(res.data.next)
}
})
},
menu(event) {
let self = this
let artistId = null
if (typeof this.data.relationships.artists != "undefined") {
artistId = this.data.relationships.artists.data[0].id
if (this.data.relationships.artists.data[0].type == "library-artists") {
artistId = this.data.relationships.artists.data[0].relationships.catalog.data[0].id
}
}
let menuItems = {
items: {
"share": {
name: app.getLz('term.share'),
icon: "./assets/feather/share.svg",
action: () => {
let route = ""
switch (this.data.type) {
case 'albums':
route = `/v1/catalog/${app.mk.storefrontId}/albums/${this.data.id}`
break;
case 'playlists':
route = `/v1/catalog/${app.mk.storefrontId}/playlists/${this.data.id}`
break;
case "library-playlists":
route = `/v1/me/library/playlists/${this.data.id}/catalog`
break
case "library-albums":
route = `/v1/me/library/albums/${this.data.id}/catalog`
break
}
if (route === '') {
return
}
app.mk.api.v3.music(route).then(res => {
console.log(res.data.data[0].attributes.url)
app.copyToClipboard(res.data.data[0].attributes.url)
})
}
},
"follow": {
name: app.getLz('action.follow'),
icon: "./assets/feather/plus-circle.svg",
hidden: false,
action: () => {
app.followArtistById(artistId, true)
}
},
"unfollow": {
name: app.getLz('action.unfollow'),
icon: "./assets/feather/x-circle.svg",
hidden: true,
action: () => {
app.followArtistById(artistId, false)
}
},
}
}
if (artistId != null) {
if (app.followingArtist(artistId)) {
menuItems.items.follow.hidden = true
menuItems.items.unfollow.hidden = false
} else {
menuItems.items.follow.hidden = false
menuItems.items.unfollow.hidden = true
}
} else {
menuItems.items.follow.hidden = true
menuItems.items.unfollow.hidden = true
}
app.showMenuPanel(menuItems, event)
},
getItemParent: function (data) {
kind = data.attributes.playParams.kind;
id = data.attributes.playParams.id;
return `${kind}:${id}`
},
getFormattedDate: function () {
let date = (this.data.attributes.releaseDate ?? (this.data.attributes.lastModifiedDate ?? (this.data.attributes.dateAdded ?? '')))
let prefix = '';
if (date == null || date === "") return "";
switch (date) {
case this.data.attributes.releaseDate:
prefix = this.app.getLz('term.time.released') + ' '
break;
case this.data.attributes.lastModifiedDate:
prefix = this.app.getLz('term.time.updated') + ' '
break;
case this.data.attributes.dateAdded:
prefix = this.app.getLz('term.time.added') + ' '
break;
}
let month, year;
try {
const releaseDate = new Date(date);
// month = new Intl.DateTimeFormat(this.app.cfg.general.language.replace('_','-'), {month: 'long'}).format(releaseDate);
// date = releaseDate.getDate();
// year = releaseDate.getFullYear();
let formatted = ''
try {
formatted = new Intl.DateTimeFormat(this.app.cfg.general.language?.replace('_', '-') ?? 'en-US', {
day: 'numeric',
month: 'long',
year: 'numeric'
}).format(releaseDate);
} catch (e) {
// use the format in json instead
if (this.app.getLz('date.format') != null) {
formatted = new this.app.getLz('date.format').replace("${d}", releaseDate.getDate()).replace("${m}", releaseDate.getMonth()).replace("${y}", releaseDate.getFullYear());
} else {
formatted = new Intl.DateTimeFormat('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric'
}).format(releaseDate);
}
}
return prefix + formatted
} catch (e) {
return ""
}
},
play() {
function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
const id = this.data.attributes.playParams.id ?? this.data.id;
//console.log("1")
const kind = this.data.attributes.playParams.kind ?? this.data.type ?? '';
//console.log("1")
const truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
let query = (this.data ?? app.showingPlaylist).relationships.tracks.data.map(item => new MusicKit.MediaItem(item));
app.mk.stop().then(function () {
app.mk.setQueue({[truekind]: [id], parameters: {l: app.mklang}}).then(function () {
app.mk.play().then(function () {
if (query.length > 100) {
let u = query.slice(100);
if (app.mk.shuffleMode == 1) {
shuffleArray(u)
}
app.mk.queue.append(u)
}
})
})
})
}
}
})
</script>

View file

@ -0,0 +1,22 @@
<script>
Vue.component('plugin-renderer', {
/*html*/
template: `
<component :is="getPage()"></component>
`,
data: function () {
return {
app: this.$root,
}
},
props: {},
mounted() {
},
methods: {
getPage() {
return this.$root.pluginPages.page
}
},
}
);
</script>

View file

@ -0,0 +1,190 @@
<script type="text/x-template" id="plugins-github">
<div class="content-inner github-themes-page">
<div class="gh-header">
<div class="row">
<div class="col nopadding">
<h1 class="header-text">{{$root.getLz('settings.header.visual.plugin.github.page')}}</h1>
</div>
<div class="col-auto nopadding flex-center">
<button class="md-btn md-btn-small md-btn-block" @click="installThemeURL()">
{{$root.getLz('settings.option.visual.plugin.github.download')}}
</button>
</div>
</div>
</div>
<div class="gh-content">
<div class="repos-list">
<ul class="list-group list-group-flush">
<li @click="showRepo(repo)" class="list-group-item list-group-item-dark"
:style="{'background': (repo.id == openRepo.id) ? 'var(--keyColor)' : ''}"
v-for="repo in repos">
<div class="row">
<div class="col flex-center">
<div>
<h4 class="repo-name">{{ (repo.description != null) ? repo.description : repo.full_name }}</h4>
<div>⭐ {{ repo.stargazers_count }}</div>
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="github-preview" v-if="openRepo.full_name">
<div class="gh-preview-header">
<div class="row nopadding">
<div class="col nopadding flex-center">
<div>
<h3 class="repo-preview-name">{{ openRepo.description }}</h3>
<div>
<div class="svg-icon inline" :style="{'--url': 'url(\'./assets/github.svg\')'}"></div>
<a class="repo-url" target="_blank" :href="openRepo.html_url">{{ openRepo.full_name
}}</a></div>
<div>⭐ {{ openRepo.stargazers_count }}</div>
</div>
</div>
<div class="col-auto nopadding flex-center">
<button class="md-btn md-btn-primary" @click="installThemeRepo(openRepo)">
<span v-if="!themesInstalled.includes(openRepo.full_name)">{{$root.getLz('action.install')}}</span>
<span v-else>{{$root.getLz('action.update')}}</span>
</button>
</div>
</div>
</div>
<hr>
<div v-html="openRepo.readme" class="github-content"></div>
</div>
<div class="github-preview" v-else>
</div>
</transition>
</div>
</div>
</script>
<script>
Vue.component('plugins-github', {
template: "#plugins-github",
props: [],
data: function () {
return {
repos: [],
openRepo: {
id: -1,
name: '',
description: '',
html_url: '',
stargazers_count: 0,
owner: {
avatar_url: ''
},
readme: ""
},
themesInstalled: []
}
},
mounted() {
this.getRepos();
// this.getInstalledThemes();
},
methods: {
getInstalledThemes() {
let self = this
const themes = ipcRenderer.sendSync("get-themes")
// for each theme, get the github_repo property and push it to the themesInstalled array, if not blank
themes.forEach(theme => {
if (theme.github_repo !== "") {
self.themesInstalled.push(theme.github_repo)
}
})
},
showRepo(repo) {
const self = this
const readmeUrl = `https://raw.githubusercontent.com/${repo.full_name}/main/README.md`;
var requestOptions = {
method: 'GET',
redirect: 'follow'
};
fetch(readmeUrl, requestOptions)
.then(response => response.text())
.then(result => {
self.openRepo = repo
self.openRepo.readme = self.convertReadMe(result);
})
.catch(error => {
self.openRepo = repo
self.openRepo.readme = `This repository doesn't have a README.md file.`;
console.log('error', error)
});
},
convertReadMe(text) {
var converter = new showdown.Converter(),
html = converter.makeHtml(text);
return html
},
installThemeRepo(repo) {
let self = this
let msg = app.stringTemplateParser(app.getLz('settings.option.visual.plugin.github.install.confirm'), {
repo: repo.full_name
});
bootbox.confirm(msg, (res) => {
if (res) {
ipcRenderer.once("plugin-installed", (event, arg) => {
if (arg.success) {
self.themes = []
notyf.success(app.getLz('settings.notyf.visual.plugin.install.success'));
bootbox.confirm(app.getLz("settings.prompt.visual.plugin.github.success"), (ok)=>{
if(ok) {
ipcRenderer.invoke("relaunchApp")
}else{
return
}
})
} else {
notyf.error(app.getLz('settings.notyf.visual.plugin.install.error'));
}
});
ipcRenderer.invoke("get-github-plugin", repo.html_url)
}
})
},
installThemeURL() {
let self = this
bootbox.prompt(app.getLz('settings.prompt.visual.plugin.github.URL'), (result) => {
if (result) {
ipcRenderer.once("plugin-installed", (event, arg) => {
if (arg.success) {
self.themes = ipcRenderer.sendSync("get-themes")
bootbox.confirm(app.getLz("settings.prompt.visual.plugin.github.success"), (ok)=>{
if(ok) {
ipcRenderer.invoke("relaunchApp")
}else{
return
}
})
notyf.success(app.getLz('settings.notyf.visual.plugin.install.success'));
} else {
notyf.error(app.getLz('settings.notyf.visual.plugin.install.error'));
}
});
ipcRenderer.invoke("get-github-plugin", result)
}
});
},
getRepos() {
let self = this
var requestOptions = {
method: 'GET',
redirect: 'follow'
};
fetch("https://api.github.com/search/repositories?q=topic:cidermusicplugin fork:true", requestOptions)
.then(response => response.text())
.then(result => {
self.repos = JSON.parse(result).items
})
.catch(error => console.log('error', error));
}
}
})
</script>

View file

@ -204,7 +204,7 @@
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
},
playEpisode(episode) {
app.mk.setQueue({'episode': episode.id}).then(() => {app.mk.play()})
app.mk.setQueue({'episode': episode.id, parameters : {l : app.mklang}}).then(() => {app.mk.play()})
},
selectPodcast(podcast) {
this.podcastSelected = podcast

View file

@ -0,0 +1,21 @@
<script type="text/x-template" id="remote-pair">
<div style="display:flex;width:100%;height:100%;padding-top: var(--navigationBarHeight);position:absolute;top:0;left:0;">
<webview id="foo" src="https://cider.sh" style="display:inline-flex; width:100%;"></webview>
</div>
</script>
<script>
Vue.component('remote-pair', {
template: '#remote-pair',
async mounted() {
ipcRenderer.send('get-remote-pair-url')
ipcRenderer.on('send-remote-pair-url', (event, url) => {
this.url = url
app.webview.src = url
document.getElementById('foo').src = url;
})
},
methods: {
}
});
</script>

View file

@ -0,0 +1,157 @@
<script type="text/x-template" id="replay-page">
<div class="content-inner replay-page">
<vue-horizontal style="height: 300px;">
<div class="replay-period" v-for="year in years" @click="getReplayYear(year.attributes.year)">
<div class="artwork-container">
<mediaitem-artwork :size="200" :url="year.relationships.playlist.data[0].attributes.artwork.url"></mediaitem-artwork>
</div>
{{ year.attributes.year }}
</div>
</vue-horizontal>
<hr>
<transition name="replaycard">
<div class="replay-viewport" v-if="loaded.id != -1">
<!-- Stats -->
<div class="replay-video" v-if="false">
<mediaitem-artwork
:url="loaded.playlist.attributes.editorialVideo.motionWideVideo21x9.previewFrame.url"
:video="loaded.playlist.attributes.editorialVideo.motionWideVideo21x9.video"
:video-priority="true"></mediaitem-artwork>
</div>
<h1 class="replay-header">{{ loaded.attributes.year }} {{$root.getLz('term.replay')}}</h1>
<hr>
<div class="row">
<div class="col">
<h4>{{ loaded.attributes.listenTimeInMinutes }} {{$root.getLz('term.time.minutes')}}</h4>
<h4>{{ loaded.attributes.uniqueAlbumCount }} {{$root.getLz('term.uniqueAlbums')}}</h4>
<h4>{{ loaded.attributes.uniqueArtistCount }} {{$root.getLz('term.uniqueArtists')}}</h4>
<h4>{{ loaded.attributes.uniqueSongCount }} {{$root.getLz('term.uniqueSongs')}}</h4>
</div>
<div class="col-auto replay-playlist-container">
<mediaitem-square kind="card" :force-video="true" :item="loaded.playlist"></mediaitem-square>
</div>
</div>
<!-- Top Artists-->
<h3>{{$root.getLz('term.topArtists')}}</h3>
<div class="well">
<mediaitem-scroller-horizontal>
<div class="card replay-card" v-for="artistData in loaded.views['top-artists'].data">
<div class="card-body">
<mediaitem-square :item="artistData.relationships.artist.data[0]"></mediaitem-square>
</div>
<div class="card-footer">
{{ artistData.attributes.listenTimeInMinutes }} {{$root.getLz('term.time.minutes')}}<br>
{{$root.getLz('term.listenedTo')}} {{ artistData.attributes.playCount }} {{$root.getLz('term.times')}}
</div>
</div>
</mediaitem-scroller-horizontal>
</div>
<!-- Top Albums-->
<h3>{{$root.getLz('term.topAlbums')}}</h3>
<div class="well">
<mediaitem-scroller-horizontal>
<div class="card replay-card" v-for="albumData in loaded.views['top-albums'].data">
<div class="card-body">
<mediaitem-square :item="albumData.relationships.album.data[0]"></mediaitem-square>
</div>
<div class="card-footer">
{{ albumData.attributes.listenTimeInMinutes }} {{$root.getLz('term.time.minutes')}}<br>
{{ albumData.attributes.playCount }} {{$root.getLz('term.plays')}}
</div>
</div>
</mediaitem-scroller-horizontal>
</div>
<!-- Top Songs-->
<h3>{{$root.getLz('term.topSongs')}}</h3>
<div class="well">
<listitem-horizontal :show-library-status="false" :items="songsToArray(loaded.views['top-songs'].data)"></listitem-horizontal>
</div>
<h3>{{$root.getLz('term.topGenres')}}</h3>
<div class="top-genres-container">
<div v-for="genre in loaded.topGenres" class="replay-genre-display">
<div class="genre-name">
{{ genre.genre }}
</div>
<div class="genre-count">
<div class="genre-count-bar" :style="{'width': genre.count + '%'}">
{{ genre.count }}%
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
</script>
<script>
Vue.component('replay-page', {
template: '#replay-page',
data: function () {
return {
years: [],
loaded: {
id: -1
},
musicTypeGenre: ""
}
},
async mounted() {
// Get available years
let year = await app.mk.api.v3.music("/v1/me/music-summaries/search?extend=inLibrary&period=year&fields[music-summaries]=period%2Cyear&include[music-summaries]=playlist")
this.years = year.data.data
this.years.reverse()
localStorage.setItem("seenReplay", true)
this.getReplayYear();
const musicGenre = await app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/genres/34`)
this.musicTypeGenre = musicGenre.data.data[0].attributes.name
},
methods: {
songsToArray(songsData) {
let self = this
let songs = []
let topGenres = {}
let genrePlayCount = 0;
songsData.forEach(function (songData) {
let song = songData.relationships.song.data[0]
song.attributes.playCount = songData.attributes.playCount
songs.push(song)
genrePlayCount += song.attributes.playCount
song.attributes.genreNames.forEach(function (genre) {
if (genre != self.musicTypeGenre) {
if (topGenres[genre] == undefined) {
topGenres[genre] = song.attributes.playCount
} else {
topGenres[genre] += song.attributes.playCount
}
}
})
})
let topGenresArray = []
for (let genre in topGenres) {
topGenresArray.push({
genre: genre,
count: topGenres[genre]
})
}
topGenresArray.sort(function (a, b) {
return b.count - a.count
})
topGenresArray.forEach(function (genre) {
genre.count = Math.round(genre.count / genrePlayCount * 100)
})
this.loaded.topGenres = topGenresArray
return songs
},
async getReplayYear(year = new Date().getFullYear()) {
this.loaded.id = -1
let response = await app.mk.api.v3.music(`/v1/me/music-summaries/year-${year}?extend=inLibrary&views=top-artists%2Ctop-albums%2Ctop-songs&include[music-summaries]=playlist&include[playlists]=tracks&includeOnly=playlist%2Ctracks%2Csong%2Cartist%2Calbum`)
let replayData = response.data.data[0]
// extended playlist
let playlist = await app.mk.api.v3.music(replayData.relationships.playlist.data[0].href, {extend: "editorialArtwork,editorialVideo"})
replayData.playlist = playlist.data.data[0]
this.loaded = replayData
}
}
});
</script>

View file

@ -1,13 +1,10 @@
<script type="text/x-template" id="cider-search">
<div class="content-inner">
<div v-if="search != null && search != [] && search.term != ''">
<h3>{{app.getLz('term.topResult')}}</h3>
<mediaitem-scroller-horizontal
:items="search.results[search.results.meta.results.order[0]]['data']"></mediaitem-scroller-horizontal>
<div class="row">
<div class="col-sm" style="width: auto;" v-if="getTopResult()">
<template>
<h3>Top Result</h3>
<mediaitem-square :item="getTopResult()"></mediaitem-square>
</template>
</div>
<div v-else style="text-align: center">
<h3>{{app.getLz('error.noResults')}}</h3>
<p>{{app.getLz('error.noResults.description')}}</p>
@ -15,7 +12,7 @@
<div class="col" v-if="search.results.song">
<div class="row">
<div class="col">
<h3>Songs</h3>
<h3>{{app.getLz('term.songs')}}</h3>
</div>
<div class="col-auto flex-center"
@click="app.showSearchView(app.search.term, 'song', app.friendlyTypes('song'))"
@ -24,10 +21,8 @@
</div>
</div>
<div class="mediaitem-list-item__grid">
<div class="grid-body">
<mediaitem-list-item :item="item" :index="index"
v-for="(item, index) in search.results.song.data.limit(12)"></mediaitem-list-item>
</div>
<listitem-horizontal :items="search.results.song.data.limit(12)">
</listitem-horizontal>
</div>
</div>
</div>
@ -41,8 +36,7 @@
</div>
<div class="col-auto flex-center" v-if="search.results[section].data.length >= 10">
<button class="cd-btn-seeall"
@click="app.showSearchView(app.search.term, section, app.friendlyTypes(section))">See
All
@click="app.showSearchView(app.search.term, section, app.friendlyTypes(section))">{{app.getLz('term.seeAll')}}
</button>
</div>
</div>
@ -59,7 +53,7 @@
<template v-if="search.resultsSocial.playlist">
<div class="row">
<div class="col">
<h3>Shared Playlists</h3>
<h3>{{app.getLz('term.sharedPlaylists')}}</h3>
</div>
<div class="col-auto flex-center" v-if="search.resultsSocial.playlist.data.length >= 10">
<button class="cd-btn-seeall"
@ -73,7 +67,7 @@
<template v-if="search.resultsSocial.profile">
<div class="row">
<div class="col">
<h3>People</h3>
<h3>{{app.getLz('term.people')}}</h3>
</div>
<div class="col-auto flex-center" v-if="search.resultsSocial.profile.data.length >= 10">
<button class="cd-btn-seeall"
@ -94,7 +88,7 @@
<mediaitem-square :kind="'385'" size="600"
:item="item ? (item.attributes.kind ? item : ((item.relationships && item.relationships.contents ) ? item.relationships.contents.data[0] : item)) : []"
:imagesize="800"
v-for="item in categoriesView[1].relationships.contents.data.filter(item => item.type != 'editorial-items')">
v-for="item of getFlattenedCategories()">
</div>
</div>
</div>
@ -122,11 +116,24 @@
},
async getCategories() {
if (this.categoriesView != [] && this.categoriesView.length > 0) { this.categoriesReady = true; return await true; } else {
let response = await this.app.mk.api.v3.music(`/v1/recommendations/${this.app.mk.storefrontId}?timezone=${encodeURIComponent(this.app.formatTimezoneOffset())}&name=search-landing&platform=web&extend=editorialArtwork&art%5Burl%5D=f%2Cc&types=editorial-items%2Capple-curators%2Cactivities`);
let response = await this.app.mk.api.v3.music(`/v1/recommendations/${this.app.mk.storefrontId}?timezone=${encodeURIComponent(this.app.formatTimezoneOffset())}&name=search-landing&platform=web&extend=editorialArtwork&art%5Burl%5D=f%2Cc&types=editorial-items%2Capple-curators%2Cactivities&l=${this.$root.mklang}`);
this.categoriesView = response.data.data;
console.log(this.categoriesView)
this.categoriesReady = true;
return await true;
}
},
getFlattenedCategories() {
let flattened = [];
for (let i = 0; i < this.categoriesView.length; i++) {
if (this.categoriesView[i].relationships && this.categoriesView[i].relationships.contents && this.categoriesView[i].relationships.contents.data) {
for (let j = 0; j < this.categoriesView[i].relationships.contents.data.length; j++) {
if (this.categoriesView[i].relationships.contents.data[j].type != 'editorial-items')
flattened.push(this.categoriesView[i].relationships.contents.data[j])
}
}
}
return flattened;
}
}
})

View file

@ -6,6 +6,9 @@
</div>
<div class="settings-option-body">
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('term.accountSettings')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" @click="app.appRoute('apple-account-settings')">
{{$root.getLz('term.accountSettings')}}
@ -17,7 +20,26 @@
{{$root.getLz('term.privateSession')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.mk.privateEnabled" switch/>
<input type="checkbox" v-model="$root.cfg.general.privateEnabled" v-on:change="$root.mk.privateEnabled = $root.cfg.general.privateEnabled" switch/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.general.resumebehavior')}}<br>
<small>
{{$root.getLz('settings.option.general.resumebehavior.description')}}<br>
<b>{{$root.getLz('settings.option.general.resumebehavior.locally')}}: </b>
{{$root.getLz('settings.option.general.resumebehavior.locally.description')}}<br>
<b>{{$root.getLz('settings.option.general.resumebehavior.history')}}: </b>
{{$root.getLz('settings.option.general.resumebehavior.history.description')}}
</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<select class="md-select" style="width:180px;" v-model="$root.cfg.general.resumeOnStartupBehavior">
<option value="disabled">{{$root.getLz('term.disabled')}}</option>
<option value="local">{{$root.getLz('settings.option.general.resumebehavior.locally')}}</option>
<option value="history">{{$root.getLz('settings.option.general.resumebehavior.history')}}</option>
</select>
</div>
</div>
</div>
@ -32,19 +54,29 @@
<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">
<!-- // <option value="990">Extreme</option> -->
<option value="256">{{$root.getLz('settings.header.audio.quality.high')}}</option>
<option value="64">{{$root.getLz('settings.header.audio.quality.low')}}</option>
<option value="auto">{{$root.getLz('settings.header.audio.quality.auto')}}</option>
<!-- // <option value="">{{$root.getLz('settings.header.audio.quality.hireslossless')}}</option> -->
<!-- <option value="">{{$root.getLz('settings.header.audio.quality.lossless')}}</option> -->
<option value="HIGH">{{$root.getLz('settings.header.audio.quality.high')}} ({{$root.getLz('settings.header.audio.quality.high.description')}})</option>
<option value="STANDARD">{{$root.getLz('settings.header.audio.quality.standard')}} ({{$root.getLz('settings.header.audio.quality.standard.description')}})</option>
</select>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('term.audioControls')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" onclick="app.modals.audioControls = true">
{{$root.getLz('term.audioControls')}}
</button>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.seamlessTransition')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.audio.seamless_audio" switch/>
<input type="checkbox" v-model="app.cfg.audio.seamless_audio" v-on:change="app.mk._bag.features['seamless-audio-transitions'] = app.cfg.audio.seamless_audio" switch/>
</div>
</div>
<div class="md-option-line">
@ -58,6 +90,18 @@
switch/>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.audioLab')}}
<br>
<small>{{$root.getLz('settings.option.audio.audioLab.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" onclick="app.appRoute('audiolabs')">
{{$root.getLz('term.browse')}}
</button>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext">
<div class="md-option-segment">
{{$root.getLz('term.equalizer')}}
@ -75,18 +119,7 @@
<small>{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.audioNormalization.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.audio.normalization" v-on:change="toggleNormalization"
switch/>
</div>
</div>
<div class="md-option-line" v-show="app.cfg.advanced.AudioContext">
<div class="md-option-segment">
{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.audioSpatialization')}}
<br>
<small>{{$root.getLz('settings.option.audio.enableAdvancedFunctionality.audioSpatialization.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.audio.spatial" v-on:change="toggleSpatial" switch/>
<input type="checkbox" v-model="app.cfg.audio.normalization" v-on:change="toggleNormalization" :disabled="app.cfg.audio.equalizer.vibrantBass !== 0 || app.cfg.audio.maikiwiAudio.spatial === true || app.cfg.audio.maikiwiAudio.ciderPPE === true" switch/>
</div>
</div>
</div>
@ -94,6 +127,33 @@
<span>{{$root.getLz('settings.header.visual')}}</span>
</div>
<div class="settings-option-body">
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.header.visual.theme')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<select class="md-select" @change="$root.setTheme($root.cfg.visual.theme)" v-model="$root.cfg.visual.theme">
<option value="default.less">{{$root.getLz('settings.option.visual.theme.default')}}</option>
<option value="dark.less">{{$root.getLz('settings.option.visual.theme.dark')}}</option>
<option value="sweetener.less">Sweetener</option>
<option v-for="theme in themes" :value="theme.file">{{ theme.name }}</option>
</select>
<button class="md-btn md-btn-small md-btn-block" @click="gitHubExplore()" style="margin-top: 8px">
{{$root.getLz('settings.option.visual.theme.github.explore')}}
</button>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
Window Style
</div>
<div class="md-option-segment md-option-segment_auto">
<select class="md-select" v-model="$root.cfg.visual.directives.windowLayout">
<option value="default">Cupertino</option>
<option value="twopanel">Redmond</option>
</select>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.visual.windowBackgroundStyle')}}
@ -567,11 +627,73 @@
</div>
</div>
</div>
<div class="md-option-header">
<span>{{$root.getLz('settings.header.debug')}}</span>
</div>
<div class="settings-option-body">
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.debug.copy_log')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" @click="copyLogs">
{{$root.getLz('action.copy')}}
</button>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.debug.openAppData')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" @click="openAppData">
{{$root.getLz('action.open')}}
</button>
</div>
</div>
</div>
<div class="md-option-header">
<span>{{$root.getLz('settings.header.experimental')}}</span>
</div>
<div class="settings-option-body">
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.visual.plugin.github.explore')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" @click="$root.appRoute('plugins-github')">{{ $root.getLz("settings.option.visual.plugin.github.explore") }}</button>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.experimental.unknownPlugin')}}
<br>
<small>{{$root.getLz('settings.option.experimental.unknownPlugin.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.advanced.experiments.includes('unknown-sources')" @click="app.cfg.advanced.experiments.includes('unknown-sources') ? removeExperiment('unknown-sources') : addExperiment('unknown-sources')" switch/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.advanced.playlistTrackMapping')}}
<br>
<small>{{$root.getLz('settings.option.advanced.playlistTrackMapping.description')}}</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.advanced.playlistTrackMapping" switch/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.experimental.compactUI')}}
@ -581,6 +703,15 @@
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('settings.option.experimental.inline_playlists')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.advanced.experiments.includes('inline-playlists')" @click="app.cfg.advanced.experiments.includes('inline-playlists') ? removeExperiment('inline-playlists') : addExperiment('inline-playlists')" switch/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
{{$root.getLz('term.language')}}
@ -604,32 +735,48 @@
<input type="checkbox" v-model="app.cfg.general.close_button_hide" switch/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-line update-check">
<div class="md-option-segment">
{{$root.getLz('term.updateCider')}}
{{$root.getLz('settings.option.general.updateCider')}}
</div>
<div class="md-option-segment md-option-segment_auto">
<button class="md-btn" @click="app.checkForUpdate()">
<button class="md-btn" id='settings.option.general.updateCider.check' @click="app.checkForUpdate()">
{{$root.getLz('term.check')}}
</button>
</div>
</div>
<div class="md-option-line update-check">
<div class="md-option-segment">
{{$root.getLz('settings.option.general.updateCider.branch')}}<br>
<small>({{$root.getLz('settings.option.general.updateCider.branch.description')}})</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<select class="md-select" style="width:180px;" v-model="app.cfg.general.update_branch">
<option value="main">
{{$root.getLz('settings.option.general.updateCider.branch.main')}}
</option>
<option value="develop">
{{$root.getLz('settings.option.general.updateCider.branch.develop')}}
</option>
</select>
</div>
</div>
<div class="md-option-line update-check" v-if="app.platform === 'win32'">
<div class="md-option-segment">
{{$root.getLz('settings.option.visual.transparent')}}<br>
<small>({{$root.getLz('settings.option.visual.transparent.description')}})</small>
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" v-model="app.cfg.visual.transparent" switch @change="promptForRelaunch()"/>
</div>
</div>
</div>
<div style="opacity: 0.5; pointer-events: none">
<div class="md-option-header">
<span>{{$root.getLz('settings.header.unfinished')}}</span>
</div>
<div class="settings-option-body">
<div class="md-option-line">
<div class="md-option-segment">
Theme
</div>
<div class="md-option-segment md-option-segment_auto">
<select class="md-select">
<option value="0">Cider</option>
</select>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
Theme Options
@ -686,86 +833,108 @@
</script>
<script>
Vue.component('cider-settings', {
template: "#cider-settings",
props: [],
data: function () {
return {
app: this.$root
}
},
mounted: function () {
if (app.cfg.lastfm.enabled) {
const element = document.getElementById('lfmConnect');
if (element) {
element.innerHTML = `Disconnect\n<p style="font-size: 8px"><i>(Authed: ${app.cfg.lastfm.auth_token})</i></p>`;
element.onclick = app.LastFMDeauthorize;
}
}
},
methods: {
getLanguages: function () {
let langs = this.$root.lzListing
let categories = {
"main": [],
"fun": [],
"unsorted": []
}
// sort by category if category is undefined or empty put it in "unsorted"
for (let i = 0; i < langs.length; i++) {
if (langs[i].category === undefined || langs[i].category === "") {
categories.unsorted.push(langs[i])
} else {
categories[langs[i].category].push(langs[i])
}
}
// return
return categories
},
addExperiment(flag) {
app.cfg.advanced.experiments.push(flag);
},
removeExperiment(flag) {
app.cfg.advanced.experiments.splice(app.cfg.advanced.experiments.indexOf(flag), 1);
},
toggleAudioContext: function () {
if (app.cfg.advanced.AudioContext) {
CiderAudio.init();
if (app.cfg.audio.normalization) {
CiderAudio.normalizerOn()
}
if (app.cfg.audio.spatial) {
CiderAudio.spatialOn()
}
} else {
CiderAudio.off();
}
},
toggleNormalization: function () {
if (app.cfg.audio.normalization) {
CiderAudio.normalizerOn()
} else {
CiderAudio.normalizerOff()
}
},
toggleSpatial: function () {
if (app.cfg.audio.spatial) {
CiderAudio.spatialOn()
} else {
CiderAudio.spatialOff()
}
},
changeAudioQuality: function () {
app.mk.bitrate = app.cfg.audio.quality
},
toggleUserInfo: function () {
app.chrome.hideUserInfo = !app.cfg.visual.showuserinfo
},
sendDataToMTT: function () {
ipcRenderer.invoke('setStoreValue', 'general.close_behavior', app.cfg.general.close_behavior);
Vue.component('cider-settings', {
template: "#cider-settings",
props: [],
data: function () {
return {
app: this.$root,
themes: ipcRenderer.sendSync("get-themes")
}
},
mounted: function () {
if (app.cfg.lastfm.enabled) {
const element = document.getElementById('lfmConnect');
if (element) {
element.innerHTML = `Disconnect\n<p style="font-size: 8px"><i>(Authed: ${app.cfg.lastfm.auth_token})</i></p>`;
element.onclick = app.LastFMDeauthorize;
}
}
},
methods: {
gitHubExplore() {
app.appRoute("themes-github")
},
copyLogs() {
ipcRenderer.send('fetch-log')
notyf.success(app.getLz('term.share.success'));
},
openAppData() {
ipcRenderer.send('open-appdata')
},
getLanguages: function () {
let langs = this.$root.lzListing
let categories = {
"main": [],
"fun": [],
"unsorted": []
}
// sort by category if category is undefined or empty put it in "unsorted"
for (let i = 0; i < langs.length; i++) {
if (langs[i].category === undefined || langs[i].category === "") {
categories.unsorted.push(langs[i])
} else {
categories[langs[i].category].push(langs[i])
}
}
// return
return categories
},
addExperiment(flag) {
app.cfg.advanced.experiments.push(flag);
},
removeExperiment(flag) {
app.cfg.advanced.experiments.splice(app.cfg.advanced.experiments.indexOf(flag), 1);
},
toggleAudioContext: function () {
if (app.cfg.advanced.AudioContext === true) {
CiderAudio.init();
if (app.cfg.audio.normalization === true) {
CiderAudio.normalizerOn()
}
if (app.cfg.audio.spatial === true) {
CiderAudio.spatialOn()
CiderAudio.hierarchical_loading();
}
} else {
CiderAudio.off();
}
},
toggleNormalization: function () {
if (app.cfg.audio.normalization) {
CiderAudio.normalizerOn()
} else {
CiderAudio.normalizerOff()
}
},
changeAudioQuality: function () {
1
app.mk.bitrate = MusicKit.PlaybackBitrate[app.cfg.audio.quality];
},
toggleUserInfo: function () {
app.chrome.hideUserInfo = !app.cfg.visual.showuserinfo
},
sendDataToMTT: function () {
ipcRenderer.invoke('setStoreValue', 'general.close_behavior', app.cfg.general.close_behavior);
// setStoreValue does not change plugin store values somehow
ipcRenderer.invoke('update-store-mtt', app.cfg.general.close_behavior);
}
}
})
ipcRenderer.invoke('update-store-mtt', app.cfg.general.close_behavior);
},
checkIfUpdateDisabled() {
if (ipcRenderer.sendSync('disable-update')) {
let updateFields = document.getElementsByClassName('update-check');
for (var i=0; i < updateFields.length; i++) {
updateFields[i].style = "opacity: 0.5; pointer-events: none;";
updateFields[i].title = "Not available on this type of build";
}
}
},
promptForRelaunch(){
bootbox.confirm(app.getLz('action.relaunch.confirm'), function (result) {
if (result) {
ipcRenderer.send('relaunchApp','');
}
});
}
}
})
</script>

View file

@ -0,0 +1,204 @@
<script type="text/x-template" id="themes-github">
<div class="content-inner github-themes-page">
<div class="gh-header">
<div class="row">
<div class="col nopadding">
<h1 class="header-text">{{$root.getLz('settings.header.visual.theme.github.page')}}</h1>
</div>
<div class="col-auto flex-center">
<select class="md-select" @change="$root.setTheme($root.cfg.visual.theme)"
v-model="$root.cfg.visual.theme">
<option value="default.less">{{$root.getLz('settings.option.visual.theme.default')}}</option>
<option value="dark.less">{{$root.getLz('settings.option.visual.theme.dark')}}</option>
<option value="sweetener.less">Sweetener</option>
<option v-for="theme in themes" :value="theme.file">{{ theme.name }}</option>
</select>
</div>
<div class="col-auto flex-center">
<button class="md-btn md-btn-small md-btn-block" @click="openThemesFolder()">
Open Themes Folder
</button>
</div>
<div class="col-auto nopadding flex-center">
<button class="md-btn md-btn-small md-btn-block" @click="installThemeURL()">
{{$root.getLz('settings.option.visual.theme.github.download')}}
</button>
</div>
</div>
</div>
<div class="gh-content">
<div class="repos-list">
<ul class="list-group list-group-flush">
<li @click="showRepo(repo)" class="list-group-item list-group-item-dark"
:style="{'background': (repo.id == openRepo.id) ? 'var(--keyColor)' : ''}"
v-for="repo in repos">
<div class="row">
<div class="col flex-center">
<div>
<h4 class="repo-name">{{ (repo.description != null) ? repo.description :
repo.full_name }}</h4>
<div>⭐ {{ repo.stargazers_count }}</div>
</div>
</div>
<div class="col-auto">
<span v-if="themesInstalled.includes(repo.full_name.toLowerCase())"
class="codicon codicon-cloud-download"></span>
</div>
</div>
</li>
</ul>
</div>
<div class="github-preview" v-if="openRepo.full_name">
<div class="gh-preview-header">
<div class="row nopadding">
<div class="col nopadding flex-center">
<div>
<h3 class="repo-preview-name">{{ openRepo.description }}</h3>
<div>
<div class="svg-icon inline"
:style="{'--url': 'url(\'./assets/github.svg\')'}"></div>
<a class="repo-url" target="_blank" :href="openRepo.html_url">{{ openRepo.full_name
}}</a></div>
<div>⭐ {{ openRepo.stargazers_count }}</div>
</div>
</div>
<div class="col-auto nopadding flex-center">
<button class="md-btn md-btn-primary" @click="installThemeRepo(openRepo)">
<span v-if="!themesInstalled.includes(openRepo.full_name.toLowerCase())">{{$root.getLz('action.install')}}</span>
<span v-else>{{$root.getLz('action.update')}}</span>
</button>
</div>
</div>
</div>
<hr>
<div v-html="openRepo.readme" class="github-content"></div>
</div>
<div class="github-preview" v-else>
</div>
</transition>
</div>
</div>
</script>
<script>
Vue.component('themes-github', {
template: "#themes-github",
props: [],
data: function () {
return {
repos: [],
openRepo: {
id: -1,
name: '',
description: '',
html_url: '',
stargazers_count: 0,
owner: {
avatar_url: ''
},
readme: ""
},
themesInstalled: [],
themes: []
}
},
mounted() {
this.themes = ipcRenderer.sendSync("get-themes")
this.getRepos();
this.getInstalledThemes();
app.checkForThemeUpdates()
},
methods: {
openThemesFolder() {
ipcRenderer.invoke("open-path", "themes")
},
getInstalledThemes() {
let self = this
const themes = ipcRenderer.sendSync("get-themes")
// for each theme, get the github_repo property and push it to the themesInstalled array, if not blank
themes.forEach(theme => {
if (theme.github_repo !== "" && typeof theme.commit != "") {
self.themesInstalled.push(theme.github_repo.toLowerCase())
}
})
},
showRepo(repo) {
const self = this
const readmeUrl = `https://raw.githubusercontent.com/${repo.full_name}/main/README.md`;
var requestOptions = {
method: 'GET',
redirect: 'follow'
};
fetch(readmeUrl, requestOptions)
.then(response => response.text())
.then(result => {
self.openRepo = repo
self.openRepo.readme = self.convertReadMe(result);
})
.catch(error => {
self.openRepo = repo
self.openRepo.readme = `This repository doesn't have a README.md file.`;
console.log('error', error)
});
},
convertReadMe(text) {
var converter = new showdown.Converter(),
html = converter.makeHtml(text);
return html
},
installThemeRepo(repo) {
let self = this
let msg = app.stringTemplateParser(app.getLz('settings.option.visual.theme.github.install.confirm'), {
repo: repo.full_name
});
bootbox.confirm(msg, (res) => {
if (res) {
ipcRenderer.once("theme-installed", (event, arg) => {
if (arg.success) {
self.themes = ipcRenderer.sendSync("get-themes")
self.getInstalledThemes()
notyf.success(app.getLz('settings.notyf.visual.theme.install.success'));
} else {
notyf.error(app.getLz('settings.notyf.visual.theme.install.error'));
}
});
ipcRenderer.invoke("get-github-theme", repo.html_url)
}
})
},
installThemeURL() {
let self = this
bootbox.prompt(app.getLz('settings.prompt.visual.theme.github.URL'), (result) => {
if (result) {
ipcRenderer.once("theme-installed", (event, arg) => {
if (arg.success) {
self.themes = ipcRenderer.sendSync("get-themes")
notyf.success(app.getLz('settings.notyf.visual.theme.install.success'));
} else {
notyf.error(app.getLz('settings.notyf.visual.theme.install.error'));
}
});
ipcRenderer.invoke("get-github-theme", result)
}
});
},
getRepos() {
let self = this
var requestOptions = {
method: 'GET',
redirect: 'follow'
};
fetch("https://api.github.com/search/repositories?q=topic:cidermusictheme fork:true", requestOptions)
.then(response => response.text())
.then(result => {
let items = JSON.parse(result).items
self.repos = items
})
.catch(error => console.log('error', error));
}
}
})
</script>

View file

@ -1,5 +1,6 @@
<template v-if="page == 'webview'">
<div style="display:flex;width:100%;height:100%">
<webview id="foo" :src="webview.url" style="display:inline-flex; width:100%;"></webview>
<<webview id="foo" :src="webview.url" nodeintegration="true" style="display:inline-flex; width:100%;"></webview>
</div>
</template>

View file

@ -1,12 +1,33 @@
<template v-if="page == 'zoo'">
<script type="text/x-template" id="cider-zoo">
<div class="content-inner">
<h3>Welcome to element park. *BERR NERR NERR NERR NERRRRR BERR NER NER NER NERRR BERRR NR NR NRRRR*</h3>
<button @click="app.playMediaItemById('1592151778', 'album')">Play Test Album</button>
{{ $store.state.test }}
<div class="spinner"></div>
<button class="md-btn">Cider Button</button>
<div style="position: relative;width: 300px;height: 300px;">
<artwork-material url="https://is3-ssl.mzstatic.com/image/thumb/Music126/v4/13/41/13/1341133b-560f-1aee-461f-c4b32ec049b4/cover.jpg/{w}x{h}bb.jpg"></artwork-material>
</div>
<artist-chip
v-if="artistLoaded"
:item="artist"
></artist-chip>
</div>
</template>
</script>
<script>
Vue.component('cider-zoo', {
template: '#cider-zoo',
data: function () {
return {
artistLoaded: false,
artist: {}
}
},
async mounted() {
app.mk.api.v3.music("/v1/catalog/us/artists/669831761").then(response => {
this.artist = response.data.data[0];
this.artistLoaded = true;
});
},
methods: {
}
});
</script>