[media.jellyfin] Playlist track move implementation [UI].

This commit is contained in:
Fabio Manganiello 2024-11-09 16:24:53 +01:00
parent 1230236ca5
commit 9999025c0a
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
5 changed files with 145 additions and 28 deletions

View file

@ -1,5 +1,5 @@
<template> <template>
<div <div ref="item"
class="item media-item" class="item media-item"
:class="{selected: selected, 'list': listView}" :class="{selected: selected, 'list': listView}"
@click.right="onContextClick" @click.right="onContextClick"
@ -22,11 +22,15 @@
{{ item.track_number }} {{ item.track_number }}
</span> </span>
<span class="artist" v-if="playlistView && item.artist"> <div class="artist-and-title">
{{ item.artist.name ?? item.artist }} &nbsp;&mdash;&nbsp; <span class="artist" v-if="playlistView && item.artist">
</span> {{ item.artist.name ?? item.artist }}
</span>
{{item.title || item.name}} <span class="title">
{{item.title || item.name}}
</span>
</div>
</div> </div>
<div class="right side" :class="{'col-1': !listView, 'col-2': listView }"> <div class="right side" :class="{'col-1': !listView, 'col-2': listView }">
@ -129,7 +133,7 @@ export default {
}, },
playlist: { playlist: {
type: String, type: [Object, String],
}, },
selected: { selected: {
@ -326,10 +330,22 @@ export default {
} }
.artist { .artist {
font-size: 0.9em;
font-weight: 300; font-weight: 300;
opacity: .75; opacity: .75;
letter-spacing: 0.065em; letter-spacing: 0.065em;
} }
.artist-and-title {
width: 100%;
display: flex;
flex-direction: column;
.title {
font-size: 1em;
padding: 0;
}
}
} }
.side { .side {

View file

@ -47,6 +47,11 @@ export default {
return [] return []
} }
if (this.collection?.item_type === 'playlist') {
// Don't sort playlists
return this.items
}
return [...this.items].sort((a, b) => { return [...this.items].sort((a, b) => {
const attr = this.sort.attr const attr = this.sort.attr
const desc = this.sort.desc const desc = this.sort.desc

View file

@ -11,6 +11,7 @@
@delete="$emit('delete', $event)" @delete="$emit('delete', $event)"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-with-opts="$emit('play-with-opts', $event)" @play-with-opts="$emit('play-with-opts', $event)"
@playlist-move="playlistMove"
@remove-from-playlist="$emit('remove-from-playlist', $event)" @remove-from-playlist="$emit('remove-from-playlist', $event)"
@select="selectedResult = $event; $emit('select', $event)" @select="selectedResult = $event; $emit('select', $event)"
@select-collection="selectCollection" @select-collection="selectCollection"
@ -124,6 +125,23 @@ export default {
this.selectedResult = index this.selectedResult = index
}, },
async playlistMove(event) {
const { item, to } = event
this.loading_ = true
try {
await this.request('media.jellyfin.playlist_move', {
playlist: this.collection.id,
playlist_item_id: item.playlist_item_id,
to_pos: to,
})
await this.refresh()
} finally {
this.loading_ = false
}
},
async init() { async init() {
const args = this.getUrlArgs() const args = this.getUrlArgs()
let collection = args?.collection let collection = args?.collection

View file

@ -89,6 +89,7 @@
:show-date="false" :show-date="false"
@add-to-playlist="$emit('add-to-playlist', $event)" @add-to-playlist="$emit('add-to-playlist', $event)"
@download="$emit('download', $event)" @download="$emit('download', $event)"
@move="$emit('playlist-move', $event)"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-with-opts="$emit('play-with-opts', $event)" @play-with-opts="$emit('play-with-opts', $event)"
@remove-from-playlist="$emit('remove-from-playlist', $event)" @remove-from-playlist="$emit('remove-from-playlist', $event)"
@ -129,6 +130,7 @@ export default {
'download', 'download',
'play', 'play',
'play-with-opts', 'play-with-opts',
'playlist-move',
'remove-from-playlist', 'remove-from-playlist',
'select', 'select',
'select-collection', 'select-collection',
@ -177,6 +179,11 @@ export default {
return ( return (
this.sortedItems?.filter((item) => !['collection', 'artist', 'album'].includes(item.item_type)) ?? [] this.sortedItems?.filter((item) => !['collection', 'artist', 'album'].includes(item.item_type)) ?? []
).sort((a, b) => { ).sort((a, b) => {
if (this.view === 'playlist') {
// Skip sorting if this is a playlist
return 0
}
if (this.view === 'album') { if (this.view === 'album') {
if (a.track_number && b.track_number) { if (a.track_number && b.track_number) {
if (a.track_number !== b.track_number) { if (a.track_number !== b.track_number) {
@ -313,9 +320,9 @@ export default {
case 'playlist': case 'playlist':
this.items = await this.request( this.items = await this.request(
'media.jellyfin.get_items', 'media.jellyfin.get_playlist_items',
{ {
parent_id: this.collection.id, playlist: this.collection.id,
limit: 25000, limit: 25000,
} }
) )

View file

@ -2,25 +2,54 @@
<div class="media-results" :class="{'list': listView}"> <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" <div class="item-container" v-for="(item, i) in visibleResults" :key="i" ref="item">
:key="i" <div class="droppable-container"
:hidden="!!Object.keys(sources || {}).length && !sources[item.type]" :class="{'dragover': dragOverIndex === i}"
:index="i" :ref="'droppable-' + i"
:item="item" v-if="playlistView && draggedIndex != null && i > draggedIndex" />
:list-view="listView"
:playlist="playlist" <Item :item="item"
:selected="selectedResult === i" :index="i"
:show-date="showDate" :list-view="listView"
@add-to-playlist="$emit('add-to-playlist', item)" :playlist="playlist"
@open-channel="$emit('open-channel', item)" :selected="selectedResult === i"
@remove-from-playlist="$emit('remove-from-playlist', item)" :show-date="showDate"
@select="$emit('select', i)" @add-to-playlist="$emit('add-to-playlist', item)"
@play="$emit('play', item)" @open-channel="$emit('open-channel', item)"
@play-with-opts="$emit('play-with-opts', $event)" @remove-from-playlist="$emit('remove-from-playlist', item)"
@view="$emit('view', item)" @select="$emit('select', i)"
@download="$emit('download', item)" @play="$emit('play', item)"
@download-audio="$emit('download-audio', item)" @play-with-opts="$emit('play-with-opts', $event)"
/> @view="$emit('view', item)"
@download="$emit('download', item)"
@download-audio="$emit('download-audio', item)"
@vue:mounted="itemsRef[i] = $event.el"
@vue:unmounted="delete itemsRef[i]"
/>
<Draggable :element="itemsRef[i]"
@drag="draggedIndex = i"
v-if="playlistView" />
<Droppable :element="itemsRef[i]"
@dragenter="dragOverIndex = i"
@dragleave="dragOverIndex = null"
@dragover="dragOverIndex = i"
@drop="onMove(i)" />
<div class="droppable-container"
:class="{'dragover': dragOverIndex === i}"
:ref="'droppable-' + i"
v-if="playlistView && draggedIndex != null && i < draggedIndex" />
<Droppable :element="$refs['droppable-' + i]?.[0]"
@dragenter="dragOverIndex = i"
@dragleave="dragOverIndex = null"
@dragover="dragOverIndex = i"
@drop="onMove(i)"
v-if="playlistView && draggedIndex != null && i !== draggedIndex" />
</div>
</div> </div>
<Modal ref="infoModal" title="Media info" @close="$emit('select', null)"> <Modal ref="infoModal" title="Media info" @close="$emit('select', null)">
@ -38,17 +67,28 @@
</template> </template>
<script> <script>
import Draggable from "@/components/elements/Draggable"
import Droppable from "@/components/elements/Droppable"
import Info from "@/components/panels/Media/Info"; import Info from "@/components/panels/Media/Info";
import Item from "./Item"; import Item from "./Item";
import Loading from "@/components/Loading"; import Loading from "@/components/Loading";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
export default { export default {
components: {Info, Item, Loading, Modal}, components: {
Draggable,
Droppable,
Info,
Item,
Loading,
Modal,
},
emits: [ emits: [
'add-to-playlist', 'add-to-playlist',
'download', 'download',
'download-audio', 'download-audio',
'move',
'open-channel', 'open-channel',
'play', 'play',
'play-with-opts', 'play-with-opts',
@ -109,11 +149,18 @@ export default {
data() { data() {
return { return {
draggedIndex: null,
dragOverIndex: null,
itemsRef: {},
maxResultIndex: this.resultIndexStep, maxResultIndex: this.resultIndexStep,
} }
}, },
computed: { computed: {
playlistView() {
return this.playlist != null && this.listView
},
visibleResults() { visibleResults() {
let results = this.results let results = this.results
.filter((item) => { .filter((item) => {
@ -131,6 +178,20 @@ export default {
}, },
methods: { methods: {
onMove(toPos) {
if (this.draggedIndex == null)
return
const item = this.results[this.draggedIndex]
this.$emit('move', {
from: this.draggedIndex,
to: toPos,
item: item,
})
this.draggedIndex = null
},
onScroll(e) { onScroll(e) {
const el = e.target const el = e.target
if (!el) if (!el)
@ -197,5 +258,15 @@ export default {
} }
} }
} }
.droppable-container {
background: $selected-fg;
box-shadow: $scrollbar-track-shadow;
&.dragover {
height: 0.5em;
background: $active-glow-bg-2;
}
}
} }
</style> </style>