forked from platypush/platypush
[#433] Added music UI to Jellyfin.
This commit is contained in:
parent
4f81a73fb9
commit
86559a623a
9 changed files with 861 additions and 169 deletions
|
@ -4,7 +4,7 @@
|
||||||
@click.prevent="searchAlbum"
|
@click.prevent="searchAlbum"
|
||||||
v-if="status?.state !== 'stop'">
|
v-if="status?.state !== 'stop'">
|
||||||
<div class="remote-image-container" v-if="trackImage">
|
<div class="remote-image-container" v-if="trackImage">
|
||||||
<img class="image" :src="trackImage" :alt="track.title">
|
<img class="image" :src="trackImage" :alt="trackTitle">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="icon-container" v-else>
|
<div class="icon-container" v-else>
|
||||||
|
@ -59,18 +59,18 @@
|
||||||
<div class="track-container col-s-9 col-m-9 col-l-3">
|
<div class="track-container col-s-9 col-m-9 col-l-3">
|
||||||
<div class="track-info" @click="$emit('info', track)" v-if="track && status?.state !== 'stop'">
|
<div class="track-info" @click="$emit('info', track)" v-if="track && status?.state !== 'stop'">
|
||||||
<div class="img-container" v-if="trackImage">
|
<div class="img-container" v-if="trackImage">
|
||||||
<img class="image from desktop" :src="trackImage" :alt="track.title">
|
<img class="image from desktop" :src="trackImage" :alt="trackTitle">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="title-container">
|
<div class="title-container">
|
||||||
<div class="title" v-if="status.state === 'play' || status.state === 'pause'">
|
<div class="title" v-if="status.state === 'play' || status.state === 'pause'">
|
||||||
<a :href="$route.fullPath" v-text="track.title?.length ? track.title : '[No Title]'"
|
<a :href="$route.fullPath" v-text="trackTitle"
|
||||||
@click.prevent="searchAlbum" v-if="track.album"></a>
|
@click.prevent="searchAlbum" v-if="track.album"></a>
|
||||||
<a v-text="track.title?.length ? track.title : '[No Title]'" v-else-if="track.url"></a>
|
<a v-text="trackTitle" v-else-if="track.url"></a>
|
||||||
<span v-text="track.title?.length ? track.title : '[No Title]' " v-else></span>
|
<span v-text="trackTitle" v-else></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="artist" v-if="track.artist?.length && (status.state === 'play' || status.state === 'pause')">
|
<div class="artist" v-if="trackArtistName?.length && (status.state === 'play' || status.state === 'pause')">
|
||||||
<a v-text="track.artist" @click.prevent="searchArtist"></a>
|
<a v-text="trackArtistName" @click.prevent="searchArtist"></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -206,12 +206,27 @@ export default {
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
trackArtistId() {
|
||||||
|
return typeof this.track?.artist === 'object' ? this.track.artist.id : null
|
||||||
|
},
|
||||||
|
|
||||||
|
trackArtistName() {
|
||||||
|
if (typeof this.track?.artist === 'string')
|
||||||
|
return this.track.artist
|
||||||
|
|
||||||
|
return this.track?.artist?.name || this.track?.artist?.title
|
||||||
|
},
|
||||||
|
|
||||||
trackImage() {
|
trackImage() {
|
||||||
if (this.track?.images?.length)
|
if (this.track?.images?.length)
|
||||||
return this.track.images[0].url
|
return this.track.images[0].url
|
||||||
|
|
||||||
return this.track?.image || this.image
|
return this.track?.image || this.image
|
||||||
},
|
},
|
||||||
|
|
||||||
|
trackTitle() {
|
||||||
|
return this.track?.title || this.track?.name || '[No Title]'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -235,11 +250,11 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
searchArtist() {
|
searchArtist() {
|
||||||
if (!this.track?.artist)
|
if (!this.trackArtistName?.length)
|
||||||
return
|
return
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
artist: this.track.artist,
|
artist: this.trackArtistName,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.track.artist_uri)
|
if (this.track.artist_uri)
|
||||||
|
|
|
@ -95,6 +95,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="media-loading-indicator" v-if="opening">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</MediaView>
|
</MediaView>
|
||||||
|
|
||||||
|
@ -149,6 +153,7 @@ import Utils from "@/Utils";
|
||||||
import Browser from "@/components/panels/Media/Browser";
|
import Browser from "@/components/panels/Media/Browser";
|
||||||
import Header from "@/components/panels/Media/Header";
|
import Header from "@/components/panels/Media/Header";
|
||||||
import Info from "@/components/panels/Media/Info";
|
import Info from "@/components/panels/Media/Info";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
import MediaDownloads from "@/components/panels/Media/Downloads";
|
import MediaDownloads from "@/components/panels/Media/Downloads";
|
||||||
import MediaUtils from "@/components/Media/Utils";
|
import MediaUtils from "@/components/Media/Utils";
|
||||||
import MediaView from "@/components/Media/View";
|
import MediaView from "@/components/Media/View";
|
||||||
|
@ -166,6 +171,7 @@ export default {
|
||||||
Browser,
|
Browser,
|
||||||
Header,
|
Header,
|
||||||
Info,
|
Info,
|
||||||
|
Loading,
|
||||||
MediaDownloads,
|
MediaDownloads,
|
||||||
MediaView,
|
MediaView,
|
||||||
Modal,
|
Modal,
|
||||||
|
@ -205,6 +211,7 @@ export default {
|
||||||
forceShowNav: false,
|
forceShowNav: false,
|
||||||
infoTrack: null,
|
infoTrack: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
opening: false,
|
||||||
prevSelectedView: null,
|
prevSelectedView: null,
|
||||||
results: [],
|
results: [],
|
||||||
selectedPlayer: null,
|
selectedPlayer: null,
|
||||||
|
@ -324,7 +331,7 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true
|
this.opening = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.selectedPlayer.component.supports(item))
|
if (!this.selectedPlayer.component.supports(item))
|
||||||
|
@ -334,9 +341,9 @@ export default {
|
||||||
item, this.selectedSubtitles, this.selectedPlayer, opts
|
item, this.selectedSubtitles, this.selectedPlayer, opts
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.refresh()
|
await this.refresh(item)
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.opening = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -383,15 +390,36 @@ export default {
|
||||||
await this.download(item, {onlyAudio: true})
|
await this.download(item, {onlyAudio: true})
|
||||||
},
|
},
|
||||||
|
|
||||||
async refresh() {
|
async refresh(item) {
|
||||||
this.selectedPlayer.status = await this.selectedPlayer.component.status(this.selectedPlayer)
|
let newStatus = {
|
||||||
|
...(await this.selectedPlayer.component.status(this.selectedPlayer)),
|
||||||
|
...(item || {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStatus(newStatus)
|
||||||
|
},
|
||||||
|
|
||||||
|
setStatus(status) {
|
||||||
|
const curStatus = this.selectedPlayer?.status || {}
|
||||||
|
let newStatus = {}
|
||||||
|
|
||||||
|
if (curStatus.resource === status.resource) {
|
||||||
|
newStatus = {
|
||||||
|
...curStatus,
|
||||||
|
...status,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newStatus = status
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedPlayer.status = newStatus
|
||||||
},
|
},
|
||||||
|
|
||||||
onStatusUpdate(status) {
|
onStatusUpdate(status) {
|
||||||
if (!this.selectedPlayer)
|
if (!this.selectedPlayer)
|
||||||
return
|
return
|
||||||
|
|
||||||
this.selectedPlayer.status = status
|
this.setStatus(status)
|
||||||
},
|
},
|
||||||
|
|
||||||
onPlayUrlModalOpen() {
|
onPlayUrlModalOpen() {
|
||||||
|
@ -558,17 +586,7 @@ export default {
|
||||||
|
|
||||||
async playUrl(url) {
|
async playUrl(url) {
|
||||||
this.urlPlay = url
|
this.urlPlay = url
|
||||||
this.loading = true
|
await this.play({ url: url })
|
||||||
|
|
||||||
try {
|
|
||||||
await this.play({
|
|
||||||
url: url,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$refs.playUrlModal.close()
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshDownloads() {
|
async refreshDownloads() {
|
||||||
|
@ -788,6 +806,7 @@ export default {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.view-container {
|
.view-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -806,6 +825,24 @@ export default {
|
||||||
height: calc(100% - #{$media-header-height} - #{$filter-header-height} - #{$media-ctrl-panel-height});
|
height: calc(100% - #{$media-header-height} - #{$filter-header-height} - #{$media-ctrl-panel-height});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.media-loading-indicator) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 5em;
|
||||||
|
height: 5em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media-info">
|
<div class="media-info">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
|
||||||
<div class="row header">
|
<div class="row header">
|
||||||
<div class="item-container">
|
<div class="item-container">
|
||||||
<Item :item="item"
|
<Item :item="item"
|
||||||
|
@ -25,91 +27,113 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.series">
|
<div class="row direct-url" v-if="computedItem?.imdb_url">
|
||||||
<div class="left side">TV Series</div>
|
<div class="left side">ImDB URL</div>
|
||||||
<div class="right side" v-text="item.series" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.season">
|
|
||||||
<div class="left side">Season</div>
|
|
||||||
<div class="right side" v-text="item.season" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.episode">
|
|
||||||
<div class="left side">Episode</div>
|
|
||||||
<div class="right side" v-text="item.episode" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.num_seasons">
|
|
||||||
<div class="left side">Number of seasons</div>
|
|
||||||
<div class="right side" v-text="item.num_seasons" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.description">
|
|
||||||
<div class="left side">Description</div>
|
|
||||||
<div class="right side" v-text="item.description" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.summary">
|
|
||||||
<div class="left side">Summary</div>
|
|
||||||
<div class="right side" v-text="item.summary" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.overview">
|
|
||||||
<div class="left side">Overview</div>
|
|
||||||
<div class="right side" v-text="item.overview" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.country">
|
|
||||||
<div class="left side">Country</div>
|
|
||||||
<div class="right side" v-text="item.country" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.network">
|
|
||||||
<div class="left side">Network</div>
|
|
||||||
<div class="right side" v-text="item.network" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.status">
|
|
||||||
<div class="left side">Status</div>
|
|
||||||
<div class="right side" v-text="item.status" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-if="item?.width && item?.height">
|
|
||||||
<div class="left side">Resolution</div>
|
|
||||||
<div class="right side">
|
<div class="right side">
|
||||||
{{ item.width }}x{{ item.height }}
|
<a :href="computedItem.imdb_url" title="ImDB URL" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt" />
|
||||||
|
</a>
|
||||||
|
<button @click="copyToClipboard(computedItem.imdb_url)" title="Copy URL to clipboard">
|
||||||
|
<i class="fas fa-clipboard" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.view_count != null">
|
<div class="row" v-if="computedItem?.artist?.name">
|
||||||
|
<div class="left side">Artist</div>
|
||||||
|
<div class="right side" v-text="computedItem.artist.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.album?.name">
|
||||||
|
<div class="left side">Album</div>
|
||||||
|
<div class="right side" v-text="computedItem.album.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.series">
|
||||||
|
<div class="left side">TV Series</div>
|
||||||
|
<div class="right side" v-text="computedItem.series" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.season">
|
||||||
|
<div class="left side">Season</div>
|
||||||
|
<div class="right side" v-text="computedItem.season" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.episode">
|
||||||
|
<div class="left side">Episode</div>
|
||||||
|
<div class="right side" v-text="computedItem.episode" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.num_seasons">
|
||||||
|
<div class="left side">Number of seasons</div>
|
||||||
|
<div class="right side" v-text="computedItem.num_seasons" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.description">
|
||||||
|
<div class="left side">Description</div>
|
||||||
|
<div class="right side" v-text="computedItem.description" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.summary">
|
||||||
|
<div class="left side">Summary</div>
|
||||||
|
<div class="right side" v-text="computedItem.summary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.overview">
|
||||||
|
<div class="left side">Overview</div>
|
||||||
|
<div class="right side" v-text="computedItem.overview" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.country">
|
||||||
|
<div class="left side">Country</div>
|
||||||
|
<div class="right side" v-text="computedItem.country" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.network">
|
||||||
|
<div class="left side">Network</div>
|
||||||
|
<div class="right side" v-text="computedItem.network" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.status">
|
||||||
|
<div class="left side">Status</div>
|
||||||
|
<div class="right side" v-text="computedItem.status" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.width && computedItem?.height">
|
||||||
|
<div class="left side">Resolution</div>
|
||||||
|
<div class="right side">
|
||||||
|
{{ computedItem.width }}x{{ computedItem.height }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.view_count != null">
|
||||||
<div class="left side">Views</div>
|
<div class="left side">Views</div>
|
||||||
<div class="right side">{{ formatNumber(item.view_count) }}</div>
|
<div class="right side">{{ formatNumber(computedItem.view_count) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.rating">
|
<div class="row" v-if="computedItem?.rating">
|
||||||
<div class="left side">Rating</div>
|
<div class="left side">Rating</div>
|
||||||
<div class="right side">{{ item.rating }}%</div>
|
<div class="right side">{{ Math.round(computedItem.rating) }}%</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.critic_rating">
|
<div class="row" v-if="computedItem?.critic_rating">
|
||||||
<div class="left side">Critic Rating</div>
|
<div class="left side">Critic Rating</div>
|
||||||
<div class="right side">{{ item.critic_rating }}%</div>
|
<div class="right side">{{ Math.round(computedItem.critic_rating) }}%</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.community_rating">
|
<div class="row" v-if="computedItem?.community_rating">
|
||||||
<div class="left side">Community Rating</div>
|
<div class="left side">Community Rating</div>
|
||||||
<div class="right side">{{ item.community_rating }}%</div>
|
<div class="right side">{{ Math.round(computedItem.community_rating) }}%</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.votes">
|
<div class="row" v-if="computedItem?.votes">
|
||||||
<div class="left side">Votes</div>
|
<div class="left side">Votes</div>
|
||||||
<div class="right side" v-text="item.votes" />
|
<div class="right side" v-text="computedItem.votes" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.genres">
|
<div class="row" v-if="computedItem?.genres">
|
||||||
<div class="left side">Genres</div>
|
<div class="left side">Genres</div>
|
||||||
<div class="right side" v-text="item.genres.join(', ')" />
|
<div class="right side" v-text="computedItem.genres.join(', ')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="channel">
|
<div class="row" v-if="channel">
|
||||||
|
@ -119,9 +143,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.year">
|
<div class="row" v-if="computedItem?.year">
|
||||||
<div class="left side">Year</div>
|
<div class="left side">Year</div>
|
||||||
<div class="right side" v-text="item.year" />
|
<div class="right side" v-text="computedItem.year" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="publishedDate">
|
<div class="row" v-if="publishedDate">
|
||||||
|
@ -129,46 +153,56 @@
|
||||||
<div class="right side" v-text="publishedDate" />
|
<div class="right side" v-text="publishedDate" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.file">
|
<div class="row" v-if="computedItem?.file">
|
||||||
<div class="left side">File</div>
|
<div class="left side">File</div>
|
||||||
<div class="right side" v-text="item.file" />
|
<div class="right side" v-text="computedItem.file" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.trailer">
|
<div class="row" v-if="computedItem?.track_number != null">
|
||||||
|
<div class="left side">Track</div>
|
||||||
|
<div class="right side" v-text="computedItem.track_number" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.trailer">
|
||||||
<div class="left side">Trailer</div>
|
<div class="left side">Trailer</div>
|
||||||
<div class="right side url">
|
<div class="right side url">
|
||||||
<a :href="item.trailer" target="_blank" v-text="item.trailer" />
|
<a :href="computedItem.trailer" target="_blank" v-text="computedItem.trailer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.size">
|
<div class="row" v-if="computedItem?.size">
|
||||||
<div class="left side">Size</div>
|
<div class="left side">Size</div>
|
||||||
<div class="right side" v-text="convertSize(item.size)" />
|
<div class="right side" v-text="convertSize(computedItem.size)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.quality">
|
<div class="row" v-if="computedItem?.quality">
|
||||||
<div class="left side">Quality</div>
|
<div class="left side">Quality</div>
|
||||||
<div class="right side" v-text="item.quality" />
|
<div class="right side" v-text="computedItem.quality" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.seeds">
|
<div class="row" v-if="computedItem?.seeds">
|
||||||
<div class="left side">Seeds</div>
|
<div class="left side">Seeds</div>
|
||||||
<div class="right side" v-text="item.seeds" />
|
<div class="right side" v-text="computedItem.seeds" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.peers">
|
<div class="row" v-if="computedItem?.peers">
|
||||||
<div class="left side">Peers</div>
|
<div class="left side">Peers</div>
|
||||||
<div class="right side" v-text="item.peers" />
|
<div class="right side" v-text="computedItem.peers" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.language">
|
<div class="row" v-if="computedItem?.tags">
|
||||||
|
<div class="left side">Tags</div>
|
||||||
|
<div class="right side" v-text="computedItem.tags.join(', ')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="computedItem?.language">
|
||||||
<div class="left side">Language</div>
|
<div class="left side">Language</div>
|
||||||
<div class="right side" v-text="item.language" />
|
<div class="right side" v-text="computedItem.language" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.audio_channels">
|
<div class="row" v-if="computedItem?.audio_channels">
|
||||||
<div class="left side">Audio Channels</div>
|
<div class="left side">Audio Channels</div>
|
||||||
<div class="right side" v-text="item.audio_channels" />
|
<div class="right side" v-text="computedItem.audio_channels" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -176,6 +210,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Icons from "./icons.json";
|
import Icons from "./icons.json";
|
||||||
import Item from "./Item";
|
import Item from "./Item";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
import MediaUtils from "@/components/Media/Utils";
|
import MediaUtils from "@/components/Media/Utils";
|
||||||
import Utils from "@/Utils";
|
import Utils from "@/Utils";
|
||||||
|
|
||||||
|
@ -183,6 +218,7 @@ export default {
|
||||||
name: "Info",
|
name: "Info",
|
||||||
components: {
|
components: {
|
||||||
Item,
|
Item,
|
||||||
|
Loading,
|
||||||
},
|
},
|
||||||
mixins: [Utils, MediaUtils],
|
mixins: [Utils, MediaUtils],
|
||||||
emits: [
|
emits: [
|
||||||
|
@ -207,8 +243,10 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
typeIcons: Icons,
|
typeIcons: Icons,
|
||||||
|
loading: false,
|
||||||
loadingUrl: false,
|
loadingUrl: false,
|
||||||
youtubeUrl: null,
|
youtubeUrl: null,
|
||||||
|
metadata: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -235,6 +273,13 @@ export default {
|
||||||
return ret
|
return ret
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computedItem() {
|
||||||
|
return {
|
||||||
|
...(this.item || {}),
|
||||||
|
...(this.metadata || {}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
publishedDate() {
|
publishedDate() {
|
||||||
if (this.item?.publishedAt)
|
if (this.item?.publishedAt)
|
||||||
return this.formatDate(this.item.publishedAt, true)
|
return this.formatDate(this.item.publishedAt, true)
|
||||||
|
@ -263,6 +308,35 @@ export default {
|
||||||
return this.item?.url
|
return this.item?.url
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async updateMetadata() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.item?.type === 'jellyfin' && this.item?.id) {
|
||||||
|
this.metadata = await this.request('media.jellyfin.info', {
|
||||||
|
item_id: this.item.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
item: {
|
||||||
|
handler() {
|
||||||
|
this.updateMetadata()
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.updateMetadata()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -271,6 +345,7 @@ export default {
|
||||||
|
|
||||||
.media-info {
|
.media-info {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 60em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
|
|
@ -1,35 +1,51 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="item media-item"
|
class="item media-item"
|
||||||
:class="{selected: selected}"
|
:class="{selected: selected, 'list': listView}"
|
||||||
@click.right.prevent="$refs.dropdown.toggle()"
|
@click.right.prevent="$refs.dropdown.toggle()"
|
||||||
v-if="!hidden">
|
v-if="!hidden">
|
||||||
<div class="thumbnail">
|
|
||||||
|
<div class="thumbnail" v-if="!listView">
|
||||||
<MediaImage :item="item" @play="$emit('play')" @select="$emit('select')" />
|
<MediaImage :item="item" @play="$emit('play')" @select="$emit('select')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="row title">
|
<div class="row title">
|
||||||
<div class="col-11 left side" v-text="item.title || item.name" @click="$emit('select')" />
|
<div class="left side"
|
||||||
<div class="col-1 right side">
|
:class="{'col-11': !listView, 'col-10': listView }"
|
||||||
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h" ref="dropdown">
|
@click.stop="$emit('select')">
|
||||||
<DropdownItem icon-class="fa fa-play" text="Play" @input="$emit('play')"
|
<span class="track-number" v-if="listView && item.track_number">
|
||||||
v-if="item.type !== 'torrent'" />
|
{{ item.track_number }}
|
||||||
<DropdownItem icon-class="fa fa-play" text="Play (With Cache)"
|
</span>
|
||||||
@input="$emit('play-with-opts', {item: item, opts: {cache: true}})"
|
|
||||||
v-if="item.type === 'youtube'" />
|
{{item.title || item.name}}
|
||||||
<DropdownItem icon-class="fa fa-download" text="Download" @input="$emit('download')"
|
</div>
|
||||||
v-if="(item.type === 'torrent' || item.type === 'youtube') && item.item_type !== 'channel' && item.item_type !== 'playlist'" />
|
|
||||||
<DropdownItem icon-class="fa fa-volume-high" text="Download Audio" @input="$emit('download-audio')"
|
<div class="right side" :class="{'col-1': !listView, 'col-2': listView }">
|
||||||
v-if="item.type === 'youtube' && item.item_type !== 'channel' && item.item_type !== 'playlist'" />
|
<span class="duration" v-if="item.duration && listView">
|
||||||
<DropdownItem icon-class="fa fa-list" text="Add to playlist" @input="$emit('add-to-playlist')"
|
<span v-text="formatDuration(item.duration, true)" />
|
||||||
v-if="item.type === 'youtube'" />
|
</span>
|
||||||
<DropdownItem icon-class="fa fa-trash" text="Remove from playlist" @input="$emit('remove-from-playlist')"
|
|
||||||
v-if="item.type === 'youtube' && playlist?.length" />
|
<span class="actions">
|
||||||
<DropdownItem icon-class="fa fa-window-maximize" text="View in browser" @input="$emit('view')"
|
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h" ref="dropdown">
|
||||||
v-if="item.type === 'file'" />
|
<DropdownItem icon-class="fa fa-play" text="Play" @input="$emit('play')"
|
||||||
<DropdownItem icon-class="fa fa-info-circle" text="Info" @input="$emit('select')" />
|
v-if="item.type !== 'torrent'" />
|
||||||
</Dropdown>
|
<DropdownItem icon-class="fa fa-play" text="Play (With Cache)"
|
||||||
|
@input="$emit('play-with-opts', {item: item, opts: {cache: true}})"
|
||||||
|
v-if="item.type === 'youtube'" />
|
||||||
|
<DropdownItem icon-class="fa fa-download" text="Download" @input="$emit('download')"
|
||||||
|
v-if="(item.type === 'torrent' || item.type === 'youtube') && item.item_type !== 'channel' && item.item_type !== 'playlist'" />
|
||||||
|
<DropdownItem icon-class="fa fa-volume-high" text="Download Audio" @input="$emit('download-audio')"
|
||||||
|
v-if="item.type === 'youtube' && item.item_type !== 'channel' && item.item_type !== 'playlist'" />
|
||||||
|
<DropdownItem icon-class="fa fa-list" text="Add to playlist" @input="$emit('add-to-playlist')"
|
||||||
|
v-if="item.type === 'youtube'" />
|
||||||
|
<DropdownItem icon-class="fa fa-trash" text="Remove from playlist" @input="$emit('remove-from-playlist')"
|
||||||
|
v-if="item.type === 'youtube' && playlist?.length" />
|
||||||
|
<DropdownItem icon-class="fa fa-window-maximize" text="View in browser" @input="$emit('view')"
|
||||||
|
v-if="item.type === 'file'" />
|
||||||
|
<DropdownItem icon-class="fa fa-info-circle" text="Info" @input="$emit('select')" />
|
||||||
|
</Dropdown>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -40,11 +56,11 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row creation-date" v-if="item.created_at">
|
<div class="row creation-date" v-if="item.created_at && showDate">
|
||||||
{{ formatDateTime(item.created_at, true) }}
|
{{ formatDateTime(item.created_at, true) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row creation-date" v-text="item.year" v-else-if="item.year" />
|
<div class="row creation-date" v-text="item.year" v-else-if="item.year && showDate" />
|
||||||
|
|
||||||
<div class="row ratings" v-if="item.critic_rating != null || item.community_rating != null">
|
<div class="row ratings" v-if="item.critic_rating != null || item.community_rating != null">
|
||||||
<span class="rating" title="Critic rating" v-if="item.critic_rating != null">
|
<span class="rating" title="Critic rating" v-if="item.critic_rating != null">
|
||||||
|
@ -94,7 +110,7 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
selected: {
|
listView: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -102,6 +118,16 @@ export default {
|
||||||
playlist: {
|
playlist: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
selected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
showDate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -261,5 +287,47 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.list {
|
||||||
|
max-height: none;
|
||||||
|
border-bottom: 1px solid $default-shadow-color !important;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
max-height: none;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
font-size: .9em;
|
||||||
|
opacity: .75;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-number {
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: .9em;
|
||||||
|
margin-right: 1em;
|
||||||
|
color: $default-fg-2;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -44,6 +44,7 @@ export default {
|
||||||
'download-audio',
|
'download-audio',
|
||||||
'play',
|
'play',
|
||||||
'play-with-opts',
|
'play-with-opts',
|
||||||
|
'select',
|
||||||
],
|
],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -84,6 +85,8 @@ export default {
|
||||||
return 'movies'
|
return 'movies'
|
||||||
case 'homevideos':
|
case 'homevideos':
|
||||||
return 'videos'
|
return 'videos'
|
||||||
|
case 'music':
|
||||||
|
return 'music'
|
||||||
default:
|
default:
|
||||||
return 'index'
|
return 'index'
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
<div class="name" v-if="fallbackImageCollections[collection.id] || parentId">
|
<div class="name" v-if="fallbackImageCollections[collection.id] || parentId">
|
||||||
<h2>{{ collection.name }}</h2>
|
<h2>{{ collection.name }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="float bottom-right" v-if="collection.year">
|
||||||
|
<span>{{ collection.year }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,7 +64,17 @@ export default {
|
||||||
filteredItems() {
|
filteredItems() {
|
||||||
return Object.values(this.items).filter(
|
return Object.values(this.items).filter(
|
||||||
(item) => !this.filter || item.name.toLowerCase().includes(this.filter.toLowerCase())
|
(item) => !this.filter || item.name.toLowerCase().includes(this.filter.toLowerCase())
|
||||||
).sort((a, b) => a.name.localeCompare(b.name))
|
).sort((a, b) => {
|
||||||
|
if (a.item_type === 'album' && b.item_type === 'album') {
|
||||||
|
if (a.year && b.year) {
|
||||||
|
if (a.year !== b.year) {
|
||||||
|
return b.year - a.year
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -77,12 +91,29 @@ export default {
|
||||||
|
|
||||||
.index {
|
.index {
|
||||||
.item {
|
.item {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.float {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0.25em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
|
||||||
|
&.bottom-right {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-root {
|
&.is-root {
|
||||||
|
|
|
@ -2,12 +2,23 @@
|
||||||
<div class="videos index">
|
<div class="videos index">
|
||||||
<Loading v-if="isLoading" />
|
<Loading v-if="isLoading" />
|
||||||
|
|
||||||
|
<div class="wrapper music-wrapper" v-else-if="collection?.collection_type === 'music'">
|
||||||
|
<Music :collection="collection"
|
||||||
|
:filter="filter"
|
||||||
|
:loading="isLoading"
|
||||||
|
:path="path"
|
||||||
|
@play="$emit('play', $event)"
|
||||||
|
@play-with-opts="$emit('play-with-opts', $event)"
|
||||||
|
@select="selectedResult = $event; $emit('select', $event)"
|
||||||
|
@select-collection="selectCollection" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<NoItems :with-shadow="false"
|
<NoItems :with-shadow="false"
|
||||||
v-else-if="!items?.length">
|
v-else-if="!items?.length">
|
||||||
No videos found.
|
No videos found.
|
||||||
</NoItems>
|
</NoItems>
|
||||||
|
|
||||||
<div class="items-wrapper" v-else>
|
<div class="wrapper items-wrapper" v-else>
|
||||||
<Collections :collection="collection"
|
<Collections :collection="collection"
|
||||||
:filter="filter"
|
:filter="filter"
|
||||||
:items="collections"
|
:items="collections"
|
||||||
|
@ -28,10 +39,6 @@
|
||||||
@select="selectedResult = $event"
|
@select="selectedResult = $event"
|
||||||
v-if="mediaItems.length > 0" />
|
v-if="mediaItems.length > 0" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SortButton :value="sort"
|
|
||||||
@input="sort = $event"
|
|
||||||
v-if="items.length > 0" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -39,9 +46,9 @@
|
||||||
import Collections from "@/components/panels/Media/Providers/Jellyfin/Collections";
|
import Collections from "@/components/panels/Media/Providers/Jellyfin/Collections";
|
||||||
import Loading from "@/components/Loading";
|
import Loading from "@/components/Loading";
|
||||||
import Mixin from "@/components/panels/Media/Providers/Jellyfin/Mixin";
|
import Mixin from "@/components/panels/Media/Providers/Jellyfin/Mixin";
|
||||||
|
import Music from "../Music/Index";
|
||||||
import NoItems from "@/components/elements/NoItems";
|
import NoItems from "@/components/elements/NoItems";
|
||||||
import Results from "@/components/panels/Media/Results";
|
import Results from "@/components/panels/Media/Results";
|
||||||
import SortButton from "@/components/panels/Media/Providers/Jellyfin/components/SortButton";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [Mixin],
|
mixins: [Mixin],
|
||||||
|
@ -49,9 +56,9 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
Collections,
|
Collections,
|
||||||
Loading,
|
Loading,
|
||||||
|
Music,
|
||||||
NoItems,
|
NoItems,
|
||||||
Results,
|
Results,
|
||||||
SortButton,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -92,18 +99,34 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
|
// Don't fetch items if we're in the music view -
|
||||||
|
// we'll fetch them in the Music component
|
||||||
|
if (this.collection?.collection_type === 'music')
|
||||||
|
return
|
||||||
|
|
||||||
this.loading_ = true
|
this.loading_ = true
|
||||||
try {
|
try {
|
||||||
this.items = this.collection?.id ?
|
if (this.collection?.collection_type === 'tvshows') {
|
||||||
(
|
this.items = (
|
||||||
await this.request('media.jellyfin.get_items', {
|
await this.request('media.jellyfin.get_collections', {
|
||||||
parent_id: this.collection.id,
|
parent_id: this.collection.id,
|
||||||
limit: 5000,
|
|
||||||
})
|
})
|
||||||
) : (await this.request('media.jellyfin.get_collections')).map((collection) => ({
|
).map((collection) => ({
|
||||||
...collection,
|
...collection,
|
||||||
item_type: 'collection',
|
item_type: 'collection',
|
||||||
}))
|
}))
|
||||||
|
} else {
|
||||||
|
this.items = this.collection?.id ?
|
||||||
|
(
|
||||||
|
await this.request('media.jellyfin.get_items', {
|
||||||
|
parent_id: this.collection.id,
|
||||||
|
limit: 5000,
|
||||||
|
})
|
||||||
|
) : (await this.request('media.jellyfin.get_collections')).map((collection) => ({
|
||||||
|
...collection,
|
||||||
|
item_type: 'collection',
|
||||||
|
}))
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.loading_ = false
|
this.loading_ = false
|
||||||
}
|
}
|
||||||
|
@ -136,5 +159,9 @@ export default {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.music-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,411 @@
|
||||||
|
<template>
|
||||||
|
<div class="music index">
|
||||||
|
<Loading v-if="isLoading" />
|
||||||
|
|
||||||
|
<NoItems :with-shadow="false" v-else-if="!items?.length">
|
||||||
|
No music found.
|
||||||
|
</NoItems>
|
||||||
|
|
||||||
|
<main :class="{ album: view === 'album', artist: view === 'artist' }" v-else>
|
||||||
|
<div class="artist header" v-if="view === 'artist'">
|
||||||
|
<div class="image" v-if="collection.image">
|
||||||
|
<img :src="collection.image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<h1 v-text="collection.name" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="album header" v-if="view === 'album'">
|
||||||
|
<div class="image" v-if="collection.image">
|
||||||
|
<img :src="collection.image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<h1 v-text="collection.name" />
|
||||||
|
<div class="artist" v-if="displayedArtist?.id">
|
||||||
|
<a href="#" v-text="displayedArtist.name"
|
||||||
|
@click.prevent.stop="selectArtist"
|
||||||
|
v-if="displayedArtist" />
|
||||||
|
<span v-text="displayedArtist.name" v-else />
|
||||||
|
</div>
|
||||||
|
<div class="details">
|
||||||
|
<div class="row" v-if="collection.year">
|
||||||
|
<span class="label">Year:</span>
|
||||||
|
<span class="value" v-text="collection.year" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="collection.duration">
|
||||||
|
<span class="label">Duration:</span>
|
||||||
|
<span class="value" v-text="formatDuration(collection.duration, true)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Collections :collection="collection"
|
||||||
|
:filter="filter"
|
||||||
|
:items="collections"
|
||||||
|
:loading="isLoading"
|
||||||
|
:parent-id="collection?.id"
|
||||||
|
@select="selectCollection"
|
||||||
|
v-if="collections?.length > 0" />
|
||||||
|
|
||||||
|
<Results :results="mediaItems"
|
||||||
|
:sources="{'jellyfin': true}"
|
||||||
|
:filter="filter"
|
||||||
|
:list-view="true"
|
||||||
|
:selected-result="selectedResult"
|
||||||
|
:show-date="false"
|
||||||
|
@add-to-playlist="$emit('add-to-playlist', $event)"
|
||||||
|
@download="$emit('download', $event)"
|
||||||
|
@play="$emit('play', $event)"
|
||||||
|
@play-with-opts="$emit('play-with-opts', $event)"
|
||||||
|
@remove-from-playlist="$emit('remove-from-playlist', $event)"
|
||||||
|
@select="selectedResult = $event"
|
||||||
|
v-if="mediaItems?.length > 0" />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Collections from "@/components/panels/Media/Providers/Jellyfin/Collections";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import Mixin from "@/components/panels/Media/Providers/Jellyfin/Mixin";
|
||||||
|
import NoItems from "@/components/elements/NoItems";
|
||||||
|
import Results from "@/components/panels/Media/Results";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [Mixin],
|
||||||
|
emits: ['select', 'select-collection'],
|
||||||
|
components: {
|
||||||
|
Collections,
|
||||||
|
Loading,
|
||||||
|
NoItems,
|
||||||
|
Results,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
artist: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
collections() {
|
||||||
|
return (
|
||||||
|
this.sortedItems?.filter((item) => ['collection', 'artist', 'album'].includes(item.item_type)) ?? []
|
||||||
|
).sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
},
|
||||||
|
|
||||||
|
displayedArtist() {
|
||||||
|
return this.artist || this.collection?.artist
|
||||||
|
},
|
||||||
|
|
||||||
|
mediaItems() {
|
||||||
|
return (
|
||||||
|
this.sortedItems?.filter((item) => !['collection', 'artist', 'album'].includes(item.item_type)) ?? []
|
||||||
|
).sort((a, b) => {
|
||||||
|
if (this.view === 'album') {
|
||||||
|
if (a.track_number && b.track_number) {
|
||||||
|
if (a.track_number !== b.track_number) {
|
||||||
|
return a.track_number - b.track_number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}).map((item) => {
|
||||||
|
if (this.view === 'album') {
|
||||||
|
item.artist = this.artist || this.collection.artist
|
||||||
|
item.album = this.collection
|
||||||
|
item.image = this.collection.image
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
view() {
|
||||||
|
switch (this.collection?.item_type) {
|
||||||
|
case 'artist':
|
||||||
|
return 'artist'
|
||||||
|
case 'album':
|
||||||
|
return 'album'
|
||||||
|
default:
|
||||||
|
return 'index'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async selectArtist() {
|
||||||
|
const artistId = this.displayedArtist?.id || this.getUrlArgs().artist
|
||||||
|
if (!artistId?.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.loading_ = true
|
||||||
|
try {
|
||||||
|
const artist = this.displayedArtist || (await this.request('media.jellyfin.info', { item_id: artistId }))
|
||||||
|
if (artist) {
|
||||||
|
this.selectCollection(artist)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.setUrlArgs({ artist: artist.id, collection: artist.id })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading_ = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectCollection(collection) {
|
||||||
|
if (!collection || collection?.id === this.collection?.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (collection.item_type === 'artist') {
|
||||||
|
this.setUrlArgs({ artist: collection.id })
|
||||||
|
} else if (collection.item_type === 'album') {
|
||||||
|
this.setUrlArgs({ collection: collection.id })
|
||||||
|
} else {
|
||||||
|
this.setUrlArgs({ collection: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('select-collection', {
|
||||||
|
collection_type: 'music',
|
||||||
|
...collection,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const args = this.getUrlArgs()
|
||||||
|
let collection = args?.collection
|
||||||
|
if (!collection)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.loading_ = true
|
||||||
|
try {
|
||||||
|
collection = await this.request('media.jellyfin.info', {
|
||||||
|
item_id: collection,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (collection)
|
||||||
|
this.selectCollection(collection)
|
||||||
|
} finally {
|
||||||
|
this.loading_ = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
this.loading_ = true
|
||||||
|
try {
|
||||||
|
switch (this.view) {
|
||||||
|
case 'artist':
|
||||||
|
this.artist = {...this.collection}
|
||||||
|
this.setUrlArgs({
|
||||||
|
artist: this.collection.id,
|
||||||
|
collection: this.collection.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.items = (
|
||||||
|
await this.request(
|
||||||
|
'media.jellyfin.get_items',
|
||||||
|
{
|
||||||
|
parent_id: this.collection.id,
|
||||||
|
limit: 5000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).map((item) => {
|
||||||
|
if (this.collection?.item_type === 'album') {
|
||||||
|
item.image = this.collection.image
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'album':
|
||||||
|
this.setUrlArgs({
|
||||||
|
collection: this.collection.id,
|
||||||
|
artist: this.collection.artist?.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.items = await this.request(
|
||||||
|
'media.jellyfin.get_items',
|
||||||
|
{
|
||||||
|
parent_id: this.collection.id,
|
||||||
|
limit: 5000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.artist = null
|
||||||
|
this.items = await this.request(
|
||||||
|
'media.jellyfin.get_artists',
|
||||||
|
{ limit: 5000 }
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading_ = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.init()
|
||||||
|
await this.refresh()
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
this.setUrlArgs({
|
||||||
|
collection: null,
|
||||||
|
artist: null,
|
||||||
|
album: null,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/components/panels/Media/Providers/Jellyfin/common.scss";
|
||||||
|
|
||||||
|
$artist-header-height: 5em;
|
||||||
|
$album-header-height: 10em;
|
||||||
|
|
||||||
|
.index {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
:deep(main) {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
&.artist, &.album {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.media-results {
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
height: fit-content;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.artist {
|
||||||
|
.index {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.items {
|
||||||
|
height: calc(100% - #{$artist-header-height} - 0.5em);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.album {
|
||||||
|
.media-results {
|
||||||
|
height: calc(100% - #{$album-header-height} - 0.5em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.index {
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: $info-header-bg;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1em;
|
||||||
|
box-shadow: $border-shadow-bottom;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.artist {
|
||||||
|
height: $artist-header-height;
|
||||||
|
padding: 0;
|
||||||
|
background: linear-gradient(rgba(0, 20, 25, 0.85), rgba(0, 0, 0, 0.85));
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: $artist-header-height;
|
||||||
|
height: $artist-header-height;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 95%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.album {
|
||||||
|
height: $album-header-height;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
img {
|
||||||
|
width: $album-header-height;
|
||||||
|
height: $album-header-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
margin-right: 1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: cover;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist {
|
||||||
|
a {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
font-size: 0.7em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.media-results.list ) {
|
||||||
|
.grid {
|
||||||
|
margin-top: -0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,13 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media-results">
|
<div class="media-results" :class="{'list': listView}">
|
||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
<div class="grid" ref="grid" v-if="results?.length" @scroll="onScroll">
|
<div class="grid" ref="grid" v-if="results?.length" @scroll="onScroll">
|
||||||
<Item v-for="(item, i) in visibleResults"
|
<Item v-for="(item, i) in visibleResults"
|
||||||
:key="i"
|
:key="i"
|
||||||
:hidden="!!Object.keys(sources || {}).length && !sources[item.type]"
|
:hidden="!!Object.keys(sources || {}).length && !sources[item.type]"
|
||||||
:item="item"
|
:item="item"
|
||||||
|
:list-view="listView"
|
||||||
:playlist="playlist"
|
:playlist="playlist"
|
||||||
:selected="selectedResult === i"
|
:selected="selectedResult === i"
|
||||||
|
:show-date="showDate"
|
||||||
@add-to-playlist="$emit('add-to-playlist', item)"
|
@add-to-playlist="$emit('add-to-playlist', item)"
|
||||||
@open-channel="$emit('open-channel', item)"
|
@open-channel="$emit('open-channel', item)"
|
||||||
@remove-from-playlist="$emit('remove-from-playlist', item)"
|
@remove-from-playlist="$emit('remove-from-playlist', item)"
|
||||||
|
@ -56,11 +58,25 @@ export default {
|
||||||
],
|
],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
filter: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
listView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
playlist: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
pluginName: {
|
pluginName: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
@ -70,27 +86,23 @@ export default {
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
|
||||||
selectedResult: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
|
|
||||||
sources: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
|
|
||||||
filter: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
resultIndexStep: {
|
resultIndexStep: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 25,
|
default: 25,
|
||||||
},
|
},
|
||||||
|
|
||||||
playlist: {
|
selectedResult: {
|
||||||
default: null,
|
type: Number,
|
||||||
|
},
|
||||||
|
|
||||||
|
showDate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
sources: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -107,7 +119,7 @@ export default {
|
||||||
if (!this.filter?.length)
|
if (!this.filter?.length)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
return item.title.toLowerCase().includes(this.filter.toLowerCase())
|
return (item.title || item.name).toLowerCase().includes(this.filter.toLowerCase())
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.maxResultIndex != null)
|
if (this.maxResultIndex != null)
|
||||||
|
@ -134,18 +146,18 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
watch: {
|
||||||
this.$watch('selectedResult', (value) => {
|
selectedResult(value) {
|
||||||
if (value?.item_type === 'playlist' || value?.item_type === 'channel') {
|
if (value?.item_type === 'playlist' || value?.item_type === 'channel') {
|
||||||
this.$emit('select', null)
|
this.$emit('select', null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value == null)
|
if (this.selectedResult == null)
|
||||||
this.$refs.infoModal?.close()
|
this.$refs.infoModal?.close()
|
||||||
else
|
else
|
||||||
this.$refs.infoModal?.show()
|
this.$refs.infoModal?.show()
|
||||||
})
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -169,5 +181,18 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: initial;
|
cursor: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.list {
|
||||||
|
:deep(.grid) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
row-gap: 0;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue