forked from platypush/platypush
[#414] [UI] Added support for generic Jellyfin media collections.
This commit is contained in:
parent
9e78a9a297
commit
c88a6aa3e6
13 changed files with 460 additions and 298 deletions
|
@ -97,6 +97,7 @@ export default {
|
||||||
border-top: $default-border-2;
|
border-top: $default-border-2;
|
||||||
background: $default-bg-2;
|
background: $default-bg-2;
|
||||||
box-shadow: $border-shadow-top;
|
box-shadow: $border-shadow-top;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -54,7 +54,7 @@ import Utils from "@/Utils"
|
||||||
export default {
|
export default {
|
||||||
name: "Install",
|
name: "Install",
|
||||||
mixins: [Utils],
|
mixins: [Utils],
|
||||||
emit: ['install-start', 'install-end'],
|
emits: ['install-start', 'install-end'],
|
||||||
components: {
|
components: {
|
||||||
CopyButton,
|
CopyButton,
|
||||||
Loading,
|
Loading,
|
||||||
|
|
|
@ -122,7 +122,7 @@ export default {
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
color: $default-media-img-fg;
|
color: $default-media-img-fg;
|
||||||
z-index: 2;
|
z-index: 3;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -5,33 +5,35 @@
|
||||||
<div class="media-jellyfin-browser">
|
<div class="media-jellyfin-browser">
|
||||||
<Loading v-if="isLoading" />
|
<Loading v-if="isLoading" />
|
||||||
|
|
||||||
<Index v-bind="componentData.props"
|
|
||||||
v-on="componentData.on"
|
|
||||||
@select="selectCollection"
|
|
||||||
v-else-if="currentView === 'index'" />
|
|
||||||
|
|
||||||
<Movies v-bind="componentData.props"
|
<Movies v-bind="componentData.props"
|
||||||
v-on="componentData.on"
|
v-on="componentData.on"
|
||||||
:collection="collection"
|
:collection="collection"
|
||||||
@select="select"
|
@select="select"
|
||||||
v-else-if="currentView === 'movies'" />
|
v-else-if="currentView === 'movies'" />
|
||||||
|
|
||||||
|
<Media v-bind="componentData.props"
|
||||||
|
v-on="componentData.on"
|
||||||
|
:collection="collection"
|
||||||
|
@select="select"
|
||||||
|
@select-collection="selectCollection"
|
||||||
|
v-else />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Index from "./Jellyfin/Index";
|
|
||||||
import Loading from "@/components/Loading";
|
import Loading from "@/components/Loading";
|
||||||
import MediaNav from "./Nav";
|
import MediaNav from "./Nav";
|
||||||
import MediaProvider from "./Mixin";
|
import MediaProvider from "./Mixin";
|
||||||
import Movies from "./Jellyfin/collections/Movies/Index";
|
import Media from "./Jellyfin/views/Media/Index";
|
||||||
|
import Movies from "./Jellyfin/views/Movies/Index";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [MediaProvider],
|
mixins: [MediaProvider],
|
||||||
components: {
|
components: {
|
||||||
Index,
|
|
||||||
Loading,
|
Loading,
|
||||||
MediaNav,
|
MediaNav,
|
||||||
|
Media,
|
||||||
Movies,
|
Movies,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -59,6 +61,7 @@ export default {
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
filter: this.filter,
|
filter: this.filter,
|
||||||
loading: this.isLoading,
|
loading: this.isLoading,
|
||||||
|
path: this.path,
|
||||||
},
|
},
|
||||||
|
|
||||||
on: {
|
on: {
|
||||||
|
@ -76,9 +79,11 @@ export default {
|
||||||
return 'index'
|
return 'index'
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.collection.type) {
|
switch (this.collection.collection_type) {
|
||||||
case 'movies':
|
case 'movies':
|
||||||
return 'movies'
|
return 'movies'
|
||||||
|
case 'homevideos':
|
||||||
|
return 'videos'
|
||||||
default:
|
default:
|
||||||
return 'index'
|
return 'index'
|
||||||
}
|
}
|
||||||
|
@ -117,10 +122,16 @@ export default {
|
||||||
if (item.type === 'index') {
|
if (item.type === 'index') {
|
||||||
this.path = [this.rootItem]
|
this.path = [this.rootItem]
|
||||||
} else {
|
} else {
|
||||||
this.path.push({
|
const itemIndex = this.path.findIndex((i) => i.id === item.id)
|
||||||
title: item.name,
|
if (itemIndex >= 0) {
|
||||||
...item,
|
this.path = this.path.slice(0, itemIndex + 1)
|
||||||
})
|
} else {
|
||||||
|
this.path.push({
|
||||||
|
title: item.name,
|
||||||
|
click: () => this.selectCollection(item),
|
||||||
|
...item,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.path = []
|
this.path = []
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<div class="collections index" :class="{ 'is-root': !parentId }">
|
||||||
|
<div class="collections items">
|
||||||
|
<div class="collection item"
|
||||||
|
v-for="collection in filteredItems"
|
||||||
|
:key="collection.id"
|
||||||
|
@click="$emit('select', collection)">
|
||||||
|
<div class="image">
|
||||||
|
<img :src="collection.image"
|
||||||
|
:alt="collection.name"
|
||||||
|
@error="onImageError(collection)"
|
||||||
|
v-if="!fallbackImageCollections[collection.id]">
|
||||||
|
<i :class="collectionsIcons[collection.type] ?? 'fas fa-folder'" v-else />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="name" v-if="fallbackImageCollections[collection.id] || parentId">
|
||||||
|
<h2>{{ collection.name }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
filter: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
|
||||||
|
parentId: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fallbackImageCollections: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
collectionsIcons() {
|
||||||
|
return {
|
||||||
|
books: "fas fa-book",
|
||||||
|
homevideos: "fas fa-video",
|
||||||
|
movies: "fas fa-film",
|
||||||
|
music: "fas fa-music",
|
||||||
|
playlists: "fas fa-list",
|
||||||
|
photos: "fas fa-image",
|
||||||
|
series: "fas fa-tv",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredItems() {
|
||||||
|
return Object.values(this.items).filter(
|
||||||
|
(item) => !this.filter || item.name.toLowerCase().includes(this.filter.toLowerCase())
|
||||||
|
).sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onImageError(collection) {
|
||||||
|
this.fallbackImageCollections[collection.id] = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "./common.scss";
|
||||||
|
|
||||||
|
.index {
|
||||||
|
.item {
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-weight: bold;
|
||||||
|
overflow: auto;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-root {
|
||||||
|
.item {
|
||||||
|
h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,131 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="collections index">
|
|
||||||
<Loading v-if="isLoading" />
|
|
||||||
|
|
||||||
<div class="collections items" v-else>
|
|
||||||
<div class="collection item"
|
|
||||||
v-for="collection in filteredCollections"
|
|
||||||
:key="collection.id"
|
|
||||||
@click="$emit('select', collection)">
|
|
||||||
<div class="image">
|
|
||||||
<img :src="collection.image"
|
|
||||||
:alt="collection.name"
|
|
||||||
@error="onImageError(collection)"
|
|
||||||
v-if="!fallbackImageCollections[collection.id]">
|
|
||||||
<i :class="collectionsIcons[collection.type] ?? 'fas fa-folder'" v-else />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="name" v-if="fallbackImageCollections[collection.id]">
|
|
||||||
<h2>{{ collection.name }}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Loading from "@/components/Loading";
|
|
||||||
import MediaProvider from "../Mixin";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mixins: [MediaProvider],
|
|
||||||
components: {
|
|
||||||
Loading,
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: [
|
|
||||||
'add-to-playlist',
|
|
||||||
'back',
|
|
||||||
'download',
|
|
||||||
'download-audio',
|
|
||||||
'play',
|
|
||||||
'play-with-opts',
|
|
||||||
'select',
|
|
||||||
],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
collections: {},
|
|
||||||
fallbackImageCollections: {},
|
|
||||||
loading_: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
collectionsIcons() {
|
|
||||||
return {
|
|
||||||
books: "fas fa-book",
|
|
||||||
homevideos: "fas fa-video",
|
|
||||||
movies: "fas fa-film",
|
|
||||||
music: "fas fa-music",
|
|
||||||
playlists: "fas fa-list",
|
|
||||||
photos: "fas fa-image",
|
|
||||||
series: "fas fa-tv",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
filteredCollections() {
|
|
||||||
return Object.values(this.collections).filter(
|
|
||||||
(collection) => !this.filter || collection.name.toLowerCase().includes(this.filter.toLowerCase())
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
isLoading() {
|
|
||||||
return this.loading_ || this.loading
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onImageError(collection) {
|
|
||||||
this.fallbackImageCollections[collection.id] = true
|
|
||||||
},
|
|
||||||
|
|
||||||
async refresh() {
|
|
||||||
this.loading_ = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.collections = (
|
|
||||||
await this.request('media.jellyfin.get_collections')
|
|
||||||
).reduce((acc, collection) => {
|
|
||||||
acc[collection.id] = collection
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
} finally {
|
|
||||||
this.loading_ = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
initCollection() {
|
|
||||||
const collectionId = this.getUrlArgs().collection
|
|
||||||
if (collectionId == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const collection = this.collections[collectionId]
|
|
||||||
if (!collection) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('select', collection)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
await this.refresh()
|
|
||||||
this.initCollection()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "./common.scss";
|
|
||||||
|
|
||||||
.index {
|
|
||||||
.item {
|
|
||||||
h2 {
|
|
||||||
font-size: 2em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
<script>
|
||||||
|
import MediaProvider from "@/components/panels/Media/Providers/Mixin";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [MediaProvider],
|
||||||
|
|
||||||
|
emits: [
|
||||||
|
'add-to-playlist',
|
||||||
|
'back',
|
||||||
|
'download',
|
||||||
|
'play',
|
||||||
|
'play-with-opts',
|
||||||
|
'select',
|
||||||
|
],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
collection: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
|
||||||
|
path: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
loading_: false,
|
||||||
|
selectedResult: null,
|
||||||
|
sort: {
|
||||||
|
attr: 'title',
|
||||||
|
desc: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isLoading() {
|
||||||
|
return this.loading_ || this.loading
|
||||||
|
},
|
||||||
|
|
||||||
|
sortedItems() {
|
||||||
|
if (!this.items) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...this.items].sort((a, b) => {
|
||||||
|
const attr = this.sort.attr
|
||||||
|
const desc = this.sort.desc
|
||||||
|
let aVal = a[attr]
|
||||||
|
let bVal = b[attr]
|
||||||
|
|
||||||
|
if (typeof aVal === 'number' || typeof bVal === 'number') {
|
||||||
|
aVal = aVal || 0
|
||||||
|
bVal = bVal || 0
|
||||||
|
return desc ? bVal - aVal : aVal - bVal
|
||||||
|
}
|
||||||
|
|
||||||
|
aVal = (aVal || '').toString().toLowerCase()
|
||||||
|
bVal = (bVal || '').toString().toLowerCase()
|
||||||
|
return desc ? bVal.localeCompare(aVal) : aVal.localeCompare(bVal)
|
||||||
|
}).map((item) => {
|
||||||
|
return {
|
||||||
|
item_type: item.type,
|
||||||
|
...item,
|
||||||
|
type: 'jellyfin',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async refresh() {
|
||||||
|
const collection = this.collection?.name
|
||||||
|
if (!collection?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading_ = true
|
||||||
|
try {
|
||||||
|
this.items = await this.request(
|
||||||
|
'media.jellyfin.search',
|
||||||
|
{ collection, limit: 1000 },
|
||||||
|
)
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
this.loading_ = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
collection() {
|
||||||
|
this.refresh()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,145 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="movies index">
|
|
||||||
<Loading v-if="isLoading" />
|
|
||||||
|
|
||||||
<NoItems :with-shadow="false"
|
|
||||||
v-else-if="sortedMovies.length === 0">
|
|
||||||
No movies found.
|
|
||||||
</NoItems>
|
|
||||||
|
|
||||||
<Results :results="sortedMovies"
|
|
||||||
:sources="{'jellyfin': true}"
|
|
||||||
:filter="filter"
|
|
||||||
:selected-result="selectedResult"
|
|
||||||
@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-else />
|
|
||||||
|
|
||||||
<SortButton :value="sort" @input="sort = $event" v-if="sortedMovies.length > 0" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Loading from "@/components/Loading";
|
|
||||||
import MediaProvider from "@/components/panels/Media/Providers/Mixin";
|
|
||||||
import NoItems from "@/components/elements/NoItems";
|
|
||||||
import Results from "@/components/panels/Media/Results";
|
|
||||||
import SortButton from "@/components/panels/Media/Providers/Jellyfin/components/SortButton";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mixins: [MediaProvider],
|
|
||||||
components: {
|
|
||||||
Loading,
|
|
||||||
NoItems,
|
|
||||||
Results,
|
|
||||||
SortButton,
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: [
|
|
||||||
'add-to-playlist',
|
|
||||||
'back',
|
|
||||||
'download',
|
|
||||||
'play',
|
|
||||||
'play-with-opts',
|
|
||||||
],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
collection: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
movies: [],
|
|
||||||
loading_: false,
|
|
||||||
selectedResult: null,
|
|
||||||
sort: {
|
|
||||||
attr: 'title',
|
|
||||||
desc: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
isLoading() {
|
|
||||||
return this.loading_ || this.loading
|
|
||||||
},
|
|
||||||
|
|
||||||
sortedMovies() {
|
|
||||||
if (!this.movies) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...this.movies].sort((a, b) => {
|
|
||||||
const attr = this.sort.attr
|
|
||||||
const desc = this.sort.desc
|
|
||||||
let aVal = a[attr]
|
|
||||||
let bVal = b[attr]
|
|
||||||
|
|
||||||
if (typeof aVal === 'number' || typeof bVal === 'number') {
|
|
||||||
aVal = aVal || 0
|
|
||||||
bVal = bVal || 0
|
|
||||||
return desc ? bVal - aVal : aVal - bVal
|
|
||||||
}
|
|
||||||
|
|
||||||
aVal = (aVal || '').toString().toLowerCase()
|
|
||||||
bVal = (bVal || '').toString().toLowerCase()
|
|
||||||
return desc ? bVal.localeCompare(aVal) : aVal.localeCompare(bVal)
|
|
||||||
}).map((movie) => {
|
|
||||||
return {
|
|
||||||
item_type: movie.type,
|
|
||||||
...movie,
|
|
||||||
type: 'jellyfin',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async refresh() {
|
|
||||||
const collection = this.collection?.name
|
|
||||||
if (!collection?.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading_ = true
|
|
||||||
try {
|
|
||||||
this.movies = await this.request(
|
|
||||||
'media.jellyfin.search',
|
|
||||||
{ collection, limit: 1000 },
|
|
||||||
)
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
this.loading_ = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
collection: {
|
|
||||||
immediate: true,
|
|
||||||
handler() {
|
|
||||||
this.refresh()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
await this.refresh()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "@/components/panels/Media/Providers/Jellyfin/common.scss";
|
|
||||||
|
|
||||||
.index {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -53,10 +53,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
background-color: rgba(0, 0, 0, 0.75);
|
background-color: rgba(0, 0, 0, 0.75);
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 1.75em;
|
font-size: 1.75em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="sort-buttons">
|
<div class="sort-buttons">
|
||||||
<Dropdown :icon-class="btnIconClass"
|
<Dropdown :icon-class="btnIconClass"
|
||||||
glow right
|
glow right
|
||||||
title="Sort Direction">
|
:title="title">
|
||||||
<div class="sort-buttons-dropdown-body">
|
<div class="sort-buttons-dropdown-body">
|
||||||
<div class="title">Sort Direction</div>
|
<div class="title">Sort Direction</div>
|
||||||
<DropdownItem text="Ascending"
|
<DropdownItem text="Ascending"
|
||||||
|
@ -22,15 +22,18 @@
|
||||||
<DropdownItem text="Release Date"
|
<DropdownItem text="Release Date"
|
||||||
icon-class="fa fa-calendar"
|
icon-class="fa fa-calendar"
|
||||||
:item-class="{ active: value?.attr === 'year' }"
|
:item-class="{ active: value?.attr === 'year' }"
|
||||||
@input="onAttrChange('year')" />
|
@input="onAttrChange('year')"
|
||||||
|
v-if="withReleaseDate" />
|
||||||
<DropdownItem text="Critics Rating"
|
<DropdownItem text="Critics Rating"
|
||||||
icon-class="fa fa-star"
|
icon-class="fa fa-star"
|
||||||
:item-class="{ active: value?.attr === 'critic_rating' }"
|
:item-class="{ active: value?.attr === 'critic_rating' }"
|
||||||
@input="onAttrChange('critic_rating')" />
|
@input="onAttrChange('critic_rating')"
|
||||||
|
v-if="withCriticRating" />
|
||||||
<DropdownItem text="Community Rating"
|
<DropdownItem text="Community Rating"
|
||||||
icon-class="fa fa-users"
|
icon-class="fa fa-users"
|
||||||
:item-class="{ active: value?.attr === 'community_rating' }"
|
:item-class="{ active: value?.attr === 'community_rating' }"
|
||||||
@input="onAttrChange('community_rating')" />
|
@input="onAttrChange('community_rating')"
|
||||||
|
v-if="withCommunityRating" />
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,12 +57,31 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
withReleaseDate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
withCriticRating: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
withCommunityRating: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
btnIconClass() {
|
btnIconClass() {
|
||||||
return this.value?.desc ? 'fa fa-arrow-down-wide-short' : 'fa fa-arrow-up-short-wide'
|
return this.value?.desc ? 'fa fa-arrow-down-wide-short' : 'fa fa-arrow-up-short-wide'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return 'Sort By: ' + (this.value?.attr ?? '[none]') + ' ' + (this.value?.desc ? 'descending' : 'ascending')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
<template>
|
||||||
|
<div class="videos index">
|
||||||
|
<Loading v-if="isLoading" />
|
||||||
|
|
||||||
|
<NoItems :with-shadow="false"
|
||||||
|
v-else-if="!items?.length">
|
||||||
|
No videos found.
|
||||||
|
</NoItems>
|
||||||
|
|
||||||
|
<div class="items-wrapper" v-else>
|
||||||
|
<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"
|
||||||
|
:selected-result="selectedResult"
|
||||||
|
@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" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SortButton :value="sort"
|
||||||
|
@input="sort = $event"
|
||||||
|
v-if="items.length > 0" />
|
||||||
|
</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";
|
||||||
|
import SortButton from "@/components/panels/Media/Providers/Jellyfin/components/SortButton";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [Mixin],
|
||||||
|
emits: ['select', 'select-collection'],
|
||||||
|
components: {
|
||||||
|
Collections,
|
||||||
|
Loading,
|
||||||
|
NoItems,
|
||||||
|
Results,
|
||||||
|
SortButton,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
collections() {
|
||||||
|
return this.sortedItems?.filter((item) => item.item_type === 'collection') ?? []
|
||||||
|
},
|
||||||
|
|
||||||
|
mediaItems() {
|
||||||
|
return this.sortedItems?.filter((item) => item.item_type !== 'collection') ?? []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
selectCollection(collection) {
|
||||||
|
this.$emit('select-collection', {
|
||||||
|
type: 'homevideos',
|
||||||
|
...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 {
|
||||||
|
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 {
|
||||||
|
this.loading_ = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.init()
|
||||||
|
await this.refresh()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/components/panels/Media/Providers/Jellyfin/common.scss";
|
||||||
|
|
||||||
|
.index {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
:deep(.items-wrapper) {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.index {
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div class="movies index">
|
||||||
|
<Loading v-if="isLoading" />
|
||||||
|
|
||||||
|
<NoItems :with-shadow="false"
|
||||||
|
v-else-if="movies.length === 0">
|
||||||
|
No movies found.
|
||||||
|
</NoItems>
|
||||||
|
|
||||||
|
<Results :results="movies"
|
||||||
|
:sources="{'jellyfin': true}"
|
||||||
|
:filter="filter"
|
||||||
|
:selected-result="selectedResult"
|
||||||
|
@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-else />
|
||||||
|
|
||||||
|
<SortButton :value="sort"
|
||||||
|
:with-release-date="true"
|
||||||
|
:with-critic-rating="true"
|
||||||
|
:with-community-rating="true"
|
||||||
|
@input="sort = $event"
|
||||||
|
v-if="movies.length > 0" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
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";
|
||||||
|
import SortButton from "@/components/panels/Media/Providers/Jellyfin/components/SortButton";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [Mixin],
|
||||||
|
components: {
|
||||||
|
Loading,
|
||||||
|
NoItems,
|
||||||
|
Results,
|
||||||
|
SortButton,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
movies() {
|
||||||
|
return this.sortedItems?.filter((item) => item.item_type === 'movie') ?? []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.refresh()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/components/panels/Media/Providers/Jellyfin/common.scss";
|
||||||
|
|
||||||
|
.index {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -26,8 +26,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
emit: ['back'],
|
emits: ['back', 'select'],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
path: {
|
path: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -37,8 +36,10 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onClick(token) {
|
onClick(token) {
|
||||||
if (token.click)
|
if (token.click) {
|
||||||
token.click()
|
token.click()
|
||||||
|
this.$emit('select', token)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue