forked from platypush/platypush
[media.jellyfin] Playlist track move implementation [UI].
This commit is contained in:
parent
1230236ca5
commit
9999025c0a
5 changed files with 145 additions and 28 deletions
|
@ -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>
|
||||||
|
|
||||||
|
<div class="artist-and-title">
|
||||||
<span class="artist" v-if="playlistView && item.artist">
|
<span class="artist" v-if="playlistView && item.artist">
|
||||||
{{ item.artist.name ?? item.artist }} —
|
{{ item.artist.name ?? item.artist }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span class="title">
|
||||||
{{item.title || item.name}}
|
{{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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
<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}"
|
||||||
|
:ref="'droppable-' + i"
|
||||||
|
v-if="playlistView && draggedIndex != null && i > draggedIndex" />
|
||||||
|
|
||||||
|
<Item :item="item"
|
||||||
:index="i"
|
:index="i"
|
||||||
:item="item"
|
|
||||||
:list-view="listView"
|
:list-view="listView"
|
||||||
:playlist="playlist"
|
:playlist="playlist"
|
||||||
:selected="selectedResult === i"
|
:selected="selectedResult === i"
|
||||||
|
@ -20,7 +23,33 @@
|
||||||
@view="$emit('view', item)"
|
@view="$emit('view', item)"
|
||||||
@download="$emit('download', item)"
|
@download="$emit('download', item)"
|
||||||
@download-audio="$emit('download-audio', 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>
|
||||||
|
|
Loading…
Reference in a new issue