[#414] Added support for photo collections in Jellyfin UI.

This commit is contained in:
Fabio Manganiello 2024-10-15 22:15:49 +02:00
parent 3ffb061e2a
commit 7b8d92b120
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
4 changed files with 116 additions and 12 deletions

View file

@ -2,11 +2,11 @@
<div <div
class="item media-item" class="item media-item"
:class="{selected: selected, 'list': listView}" :class="{selected: selected, 'list': listView}"
@click.right.prevent="$refs.dropdown.toggle()" @click.right="onContextClick"
v-if="!hidden"> v-if="!hidden">
<div class="thumbnail" v-if="!listView"> <div class="thumbnail" v-if="!listView">
<MediaImage :item="item" @play="$emit('play')" @select="$emit('select')" /> <MediaImage :item="item" @play="$emit('play')" @select="onMediaSelect" />
</div> </div>
<div class="body"> <div class="body">
@ -29,10 +29,12 @@
<span class="actions"> <span class="actions">
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h" ref="dropdown"> <Dropdown title="Actions" icon-class="fa fa-ellipsis-h" ref="dropdown">
<DropdownItem icon-class="fa fa-play" text="Play" @input="$emit('play')" <DropdownItem icon-class="fa fa-play" text="Play" @input="$emit('play')"
v-if="item.type !== 'torrent'" /> v-if="item.type !== 'torrent' && item.item_type !== 'photo'" />
<DropdownItem icon-class="fa fa-play" text="Play (With Cache)" <DropdownItem icon-class="fa fa-play" text="Play (With Cache)"
@input="$emit('play-with-opts', {item: item, opts: {cache: true}})" @input="$emit('play-with-opts', {item: item, opts: {cache: true}})"
v-if="item.type === 'youtube'" /> v-if="item.type === 'youtube'" />
<DropdownItem icon-class="fa fa-eye" text="View" @input="showPhoto = true"
v-if="item.item_type === 'photo'" />
<DropdownItem icon-class="fa fa-download" text="Download" @input="$emit('download')" <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'" /> 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')" <DropdownItem icon-class="fa fa-volume-high" text="Download Audio" @input="$emit('download-audio')"
@ -74,6 +76,12 @@
</span> </span>
</div> </div>
</div> </div>
<div class="photo-container" v-if="item.item_type === 'photo' && showPhoto">
<Modal :title="item.title || item.name" :visible="true" @close="showPhoto = false">
<img :src="item.url" ref="image" @load="onImgLoad" />
</Modal>
</div>
</div> </div>
</template> </template>
@ -82,11 +90,18 @@ import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem"; import DropdownItem from "@/components/elements/DropdownItem";
import Icons from "./icons.json"; import Icons from "./icons.json";
import MediaImage from "./MediaImage"; import MediaImage from "./MediaImage";
import Modal from "@/components/Modal";
import Utils from "@/Utils"; import Utils from "@/Utils";
export default { export default {
components: {Dropdown, DropdownItem, MediaImage},
mixins: [Utils], mixins: [Utils],
components: {
Dropdown,
DropdownItem,
MediaImage,
Modal,
},
emits: [ emits: [
'add-to-playlist', 'add-to-playlist',
'download', 'download',
@ -130,8 +145,39 @@ export default {
}, },
}, },
methods: {
onContextClick(e) {
if (this.item?.item_type === 'photo') {
return
}
e.preventDefault()
this.$refs.dropdown.toggle()
},
onImgLoad() {
const width = this.$refs.image.naturalWidth
const height = this.$refs.image.naturalHeight
if (width > height) {
this.$refs.image.classList.add('horizontal')
} else {
this.$refs.image.classList.add('vertical')
}
},
onMediaSelect() {
if (this.item?.item_type === 'photo') {
this.showPhoto = true
} else {
this.$emit('select')
}
},
},
data() { data() {
return { return {
showPhoto: false,
typeIcons: Icons, typeIcons: Icons,
} }
}, },
@ -329,5 +375,31 @@ export default {
} }
} }
} }
.photo-container {
:deep(.modal) {
.body {
max-width: 95vh;
max-height: 90vh;
padding: 0;
}
img {
&.horizontal {
width: 100%;
height: auto;
max-width: 95vh;
max-height: 100%;
}
&.vertical {
width: auto;
height: 100%;
max-width: 100%;
max-height: 90vh;
}
}
}
}
} }
</style> </style>

View file

@ -1,12 +1,12 @@
<template> <template>
<div class="image-container" <div class="image-container"
:class="{ 'with-image': !!item?.image }"> :class="{ 'with-image': !!item?.image, 'photo': item?.item_type === 'photo' }">
<div class="play-overlay" @click="$emit(clickEvent, item)" v-if="hasPlay"> <div class="play-overlay" @click.stop="onItemClick" v-if="hasPlay || item?.item_type === 'photo'">
<i :class="overlayIconClass" /> <i :class="overlayIconClass" />
</div> </div>
<div class="backdrop" v-if="item?.image" <div class="backdrop" v-if="item?.image || item?.preview_url"
:style="{ backgroundImage: `url(${item.image})` }" /> :style="{ backgroundImage: `url(${item.image || item.preview_url})` }" />
<span class="icon type-icon" v-if="typeIcons[item?.type]"> <span class="icon type-icon" v-if="typeIcons[item?.type]">
<a :href="item.url" target="_blank" v-if="item.url"> <a :href="item.url" target="_blank" v-if="item.url">
@ -17,6 +17,7 @@
</span> </span>
<img class="image" :src="imgUrl" :alt="item.title" v-if="imgUrl" /> <img class="image" :src="imgUrl" :alt="item.title" v-if="imgUrl" />
<div class="image" v-else> <div class="image" v-else>
<div class="inner"> <div class="inner">
<i :class="iconClass" /> <i :class="iconClass" />
@ -68,6 +69,7 @@ export default {
case 'channel': case 'channel':
case 'playlist': case 'playlist':
case 'folder': case 'folder':
case 'photo':
return 'select' return 'select'
default: default:
return 'play' return 'play'
@ -88,6 +90,10 @@ export default {
}, },
imgUrl() { imgUrl() {
if (this.item?.item_type === 'photo') {
return this.item?.preview_url || this.item?.url
}
let img = this.item?.image let img = this.item?.image
if (!img) { if (!img) {
img = this.item?.images?.[0]?.url img = this.item?.images?.[0]?.url
@ -103,11 +109,19 @@ export default {
this.item?.item_type === 'folder' this.item?.item_type === 'folder'
) { ) {
return 'fas fa-folder-open' return 'fas fa-folder-open'
} else if (this.item?.item_type === 'photo') {
return 'fas fa-eye'
} }
return 'fas fa-play' return 'fas fa-play'
}, },
}, },
methods: {
onItemClick() {
this.$emit(this.clickEvent, this.item)
},
},
} }
</script> </script>

View file

@ -67,7 +67,25 @@ export default {
}, },
mediaItems() { mediaItems() {
return this.sortedItems?.filter((item) => item.item_type !== 'collection') ?? [] const items = this.sortedItems?.filter((item) => item.item_type !== 'collection') ?? []
if (this.collection && !this.collection.collection_type) {
return items.sort((a, b) => {
if (a.created_at && b.created_at)
return (new Date(a.created_at)) < (new Date(b.created_at))
if (a.created_at)
return -1
if (b.created_at)
return 1
let names = [a.name || a.title || '', b.name || b.title || '']
return names[0].localeCompare(names[1])
})
}
return items
}, },
}, },
@ -120,7 +138,7 @@ export default {
( (
await this.request('media.jellyfin.get_items', { await this.request('media.jellyfin.get_items', {
parent_id: this.collection.id, parent_id: this.collection.id,
limit: 5000, limit: 25000,
}) })
) : (await this.request('media.jellyfin.get_collections')).map((collection) => ({ ) : (await this.request('media.jellyfin.get_collections')).map((collection) => ({
...collection, ...collection,

View file

@ -212,7 +212,7 @@ export default {
'media.jellyfin.get_items', 'media.jellyfin.get_items',
{ {
parent_id: this.collection.id, parent_id: this.collection.id,
limit: 5000, limit: 25000,
} }
) )
).map((item) => { ).map((item) => {
@ -234,7 +234,7 @@ export default {
'media.jellyfin.get_items', 'media.jellyfin.get_items',
{ {
parent_id: this.collection.id, parent_id: this.collection.id,
limit: 5000, limit: 25000,
} }
) )
break break