Skip to content

Commit 9d1247a

Browse files
committed
Got playlists working for the most part. (the skeleton display isn't quite right, though.)
1 parent 4cceda0 commit 9d1247a

File tree

4 files changed

+323
-10
lines changed

4 files changed

+323
-10
lines changed

frontend/components/Item/CollectionTabs.vue

+5-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ export default Vue.extend({
4242
computed: {
4343
...mapStores(itemsStore),
4444
children(): Record<string, BaseItemDto[]> | undefined {
45-
if (this.items.getChildrenOfParent(this.item.Id)?.length) {
46-
return groupBy(this.items.getChildrenOfParent(this.item.Id), 'Type');
45+
if (this.items.getChildrenOfParentCollection(this.item.Id)?.length) {
46+
return groupBy(
47+
this.items.getChildrenOfParentCollection(this.item.Id),
48+
'Type'
49+
);
4750
}
4851
}
4952
},
+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<template>
2+
<div>
3+
<h1 v-if="!children && !loading" class="text-h5 text-center">
4+
{{ $t('collectionEmpty') }}
5+
</h1>
6+
<skeleton-item-grid v-if="loading" :view-type="''" />
7+
8+
<v-list v-if="!!children" color="transparent" two-line>
9+
<v-list-item-group class="list-group">
10+
<draggable
11+
class="list-draggable"
12+
v-bind="dragOptions"
13+
v-if="children.length > 0"
14+
v-model="children"
15+
:move="checkMove"
16+
>
17+
<v-hover
18+
v-for="(item, index) in children"
19+
:key="`${item.Id}-${index}`"
20+
v-slot="{ hover }"
21+
>
22+
<v-list-item ripple @click="playQueueFrom(index)">
23+
<v-list-item-action
24+
v-if="!hover"
25+
class="list-group-item d-flex justify-center d-flex transition"
26+
:class="{ 'primary--text font-weight-bold': isPlaying(item) }"
27+
>
28+
{{ index + 1 }}
29+
</v-list-item-action>
30+
<v-list-item-action v-else class="justify-center d-flex">
31+
<v-icon>mdi-drag-horizontal</v-icon>
32+
</v-list-item-action>
33+
<v-list-item-avatar tile class="list-group-item">
34+
<blurhash-image :item="item" />
35+
</v-list-item-avatar>
36+
<v-list-item-content>
37+
<v-list-item-title
38+
class="text-truncate ml-2 list-group-item transition"
39+
:class="{
40+
'primary--text font-weight-bold': isPlaying(item)
41+
}"
42+
>
43+
{{ item.Name }}
44+
</v-list-item-title>
45+
<v-list-item-subtitle
46+
v-if="getArtists(item)"
47+
class="ml-2 list-group-item transition"
48+
:class="{
49+
'primary--text font-weight-bold': isPlaying(item)
50+
}"
51+
>
52+
{{ getArtists(item) }}
53+
</v-list-item-subtitle>
54+
</v-list-item-content>
55+
56+
<v-list-item-action>
57+
<like-button :item="item" />
58+
</v-list-item-action>
59+
<v-list-item-action class="mr-2">
60+
<item-menu :item="item" queue />
61+
</v-list-item-action>
62+
</v-list-item>
63+
</v-hover>
64+
</draggable>
65+
<div
66+
v-for="index in skeletonLength"
67+
v-else
68+
:key="index"
69+
class="d-flex align-center mt-5 mb-5"
70+
>
71+
<v-skeleton-loader type="avatar" class="ml-3 mr-3" />
72+
<v-skeleton-loader type="sentences" width="10em" class="pr-5" />
73+
</div>
74+
</v-list-item-group>
75+
</v-list>
76+
</div>
77+
</template>
78+
79+
<script lang="ts">
80+
import { BaseItemDto } from '@jellyfin/client-axios';
81+
import Vue from 'vue';
82+
import { mapStores } from 'pinia';
83+
import { itemsStore, playbackManagerStore } from '~/store';
84+
85+
export default Vue.extend({
86+
props: {
87+
item: {
88+
type: Object as () => BaseItemDto,
89+
required: true
90+
}
91+
},
92+
methods: {
93+
checkMove(evt: any) {
94+
console.log(evt.draggedContext);
95+
this.newIndex = evt.draggedContext.futureIndex;
96+
this.oldIndex = evt.draggedContext.index;
97+
},
98+
getArtists(item: BaseItemDto): string | null {
99+
if (item.Artists) {
100+
return item.Artists.join(', ');
101+
} else {
102+
return null;
103+
}
104+
},
105+
isPlaying(item: BaseItemDto): boolean {
106+
if (this.playbackManager.getCurrentItem == undefined) {
107+
return false;
108+
}
109+
return item.Id == (this.playbackManager.getCurrentItem as BaseItemDto).Id;
110+
},
111+
playQueueFrom(playFromIndex: number): void {
112+
this.playbackManager
113+
.play({
114+
item: this.item,
115+
startFromIndex: playFromIndex,
116+
initiator: this.item
117+
})
118+
.then(() => this.items.fetchAndAddPlaylist(this.item.Id as string));
119+
}
120+
},
121+
data() {
122+
return {
123+
currentTab: 0,
124+
loading: false,
125+
newIndex: null as number | null,
126+
oldIndex: null as number | null,
127+
dragOptions: {
128+
animation: 500,
129+
delay: 0,
130+
group: false,
131+
dragoverBubble: true,
132+
ghostClass: 'ghost'
133+
}
134+
};
135+
},
136+
computed: {
137+
...mapStores(itemsStore, playbackManagerStore),
138+
children: {
139+
get(): BaseItemDto[] {
140+
return this.items.getChildrenOfParentPlaylist(
141+
this.item.Id
142+
) as BaseItemDto[];
143+
},
144+
set(newValue: BaseItemDto[]): void {
145+
if (this.oldIndex != null && this.newIndex != null) {
146+
this.loading = true;
147+
console.log(this.item);
148+
console.log(this.children[this.oldIndex]);
149+
console.log(this.newIndex);
150+
this.items
151+
.movePlaylistItem(
152+
this.item,
153+
this.children[this.oldIndex],
154+
this.newIndex
155+
)
156+
.then(() => (this.loading = false));
157+
}
158+
// this.items.addCollection(this.item, newValue);
159+
// setTimeout(() => (this.loading = false), 1000);
160+
}
161+
}
162+
},
163+
watch: {
164+
item: {
165+
immediate: true,
166+
async handler(item: BaseItemDto): Promise<void> {
167+
if (!this.children) {
168+
this.loading = true;
169+
await this.items.fetchAndAddPlaylist(item.Id as string);
170+
this.loading = false;
171+
}
172+
}
173+
}
174+
}
175+
});
176+
</script>
177+
178+
<style lang="scss" scoped>
179+
.list-draggable {
180+
user-select: none;
181+
min-height: 20px;
182+
}
183+
</style>

frontend/pages/item/_itemId/index.vue

+4-4
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,12 @@
241241
</v-col>
242242
</v-row>
243243
<v-row>
244-
<v-col
245-
v-if="item.Type === 'BoxSet' || item.Type === 'Playlist'"
246-
cols="12"
247-
>
244+
<v-col v-if="item.Type === 'BoxSet'" cols="12">
248245
<collection-tabs :item="item" />
249246
</v-col>
247+
<v-col v-if="item.Type === 'Playlist'" cols="12">
248+
<playlist-items :item="item" />
249+
</v-col>
250250
<v-col cols="12">
251251
<related-items :id="$route.params.itemId" :item="item" />
252252
</v-col>

frontend/store/items.ts

+131-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import Vue from 'vue';
2-
import { BaseItemDto, ItemFields } from '@jellyfin/client-axios';
2+
import { BaseItemDto, ImageType, ItemFields } from '@jellyfin/client-axios';
33
import { defineStore } from 'pinia';
44
import { authStore } from '.';
55

66
export interface ItemsState {
77
byId: Record<string, BaseItemDto>;
88
collectionById: Record<string, string[]>;
9+
playlistById: Record<string, string[]>;
910
}
1011

1112
export const itemsStore = defineStore('items', {
1213
state: () => {
1314
return {
1415
byId: {},
15-
collectionById: {}
16+
collectionById: {},
17+
playlistById: {}
1618
} as ItemsState;
1719
},
1820
actions: {
@@ -69,6 +71,113 @@ export const itemsStore = defineStore('items', {
6971
* @param children
7072
* @returns - The children of the item
7173
*/
74+
async movePlaylistItem(
75+
parent: BaseItemDto,
76+
localChild: BaseItemDto,
77+
index: number
78+
): Promise<BaseItemDto[]> {
79+
const auth = authStore();
80+
81+
// You're probably asking "... but why?"
82+
83+
// Because when the Playback manager is playing these tracks,
84+
// it seems to erase the PlaylistItemId from each of the items.
85+
// So... I just get a new bunch of them to move things.
86+
87+
// Probably a better way to do it, but...
88+
// ... I didn't feel like figuring that out right now.
89+
90+
// If you try to fix this, make sure that you can click "Play"
91+
// on the playlist, then move tracks.
92+
93+
const children = await this.$nuxt.$api.playlists.getPlaylistItems({
94+
userId: auth.currentUserId,
95+
playlistId: parent.Id as string,
96+
fields: [ItemFields.PrimaryImageAspectRatio],
97+
enableImageTypes: [
98+
ImageType.Primary,
99+
ImageType.Backdrop,
100+
ImageType.Banner,
101+
ImageType.Thumb
102+
]
103+
});
104+
const child = children.data.Items?.find(
105+
(i) => i.Id == localChild.Id
106+
) as BaseItemDto;
107+
await this.$nuxt.$api.playlists.moveItem({
108+
playlistId: parent.Id as string,
109+
itemId: child.PlaylistItemId as string,
110+
newIndex: index
111+
});
112+
return (await this.fetchAndAddPlaylist(
113+
parent.Id as string
114+
)) as BaseItemDto[];
115+
},
116+
addPlaylist(parent: BaseItemDto, children: BaseItemDto[]): BaseItemDto[] {
117+
if (!parent.Id) {
118+
throw new Error("Parent item doesn't have an Id");
119+
}
120+
121+
const childIds = [];
122+
123+
for (const child of children) {
124+
if (child.Id) {
125+
if (!this.getItemById(child.Id)) {
126+
this.add(child);
127+
}
128+
129+
childIds.push(child.Id);
130+
}
131+
}
132+
133+
Vue.set(this.playlistById, parent.Id, childIds);
134+
135+
return this.getChildrenOfParentPlaylist(parent.Id) as BaseItemDto[];
136+
},
137+
async fetchAndAddPlaylist(
138+
parentId: string | undefined
139+
): Promise<BaseItemDto[]> {
140+
const auth = authStore();
141+
142+
if (parentId && !this.getItemById(parentId)) {
143+
const parentItem = (
144+
await this.$nuxt.$api.items.getItems({
145+
userId: auth.currentUserId,
146+
ids: [parentId],
147+
fields: Object.values(ItemFields)
148+
})
149+
).data;
150+
151+
if (!parentItem.Items?.[0]) {
152+
throw new Error("This parent doesn't exist");
153+
}
154+
155+
this.add(parentItem.Items[0]);
156+
}
157+
158+
const childItems = (
159+
await this.$nuxt.$api.playlists.getPlaylistItems({
160+
userId: auth.currentUserId,
161+
playlistId: parentId as string,
162+
fields: [ItemFields.PrimaryImageAspectRatio],
163+
enableImageTypes: [
164+
ImageType.Primary,
165+
ImageType.Backdrop,
166+
ImageType.Banner,
167+
ImageType.Thumb
168+
]
169+
})
170+
).data;
171+
172+
if (childItems.Items) {
173+
const parent = this.getItemById(parentId);
174+
175+
return this.addPlaylist(parent as BaseItemDto, childItems.Items);
176+
} else {
177+
// I think this just means it's an empty playlist...?
178+
return this.addPlaylist(parent as BaseItemDto, []);
179+
}
180+
},
72181
addCollection(parent: BaseItemDto, children: BaseItemDto[]): BaseItemDto[] {
73182
if (!parent.Id) {
74183
throw new Error("Parent item doesn't have an Id");
@@ -88,7 +197,7 @@ export const itemsStore = defineStore('items', {
88197

89198
Vue.set(this.collectionById, parent.Id, childIds);
90199

91-
return this.getChildrenOfParent(parent.Id) as BaseItemDto[];
200+
return this.getChildrenOfParentCollection(parent.Id) as BaseItemDto[];
92201
},
93202
/**
94203
* Fetches a parent and its children and adds thecollection to the store
@@ -156,7 +265,7 @@ export const itemsStore = defineStore('items', {
156265
return res;
157266
};
158267
},
159-
getChildrenOfParent: (state) => {
268+
getChildrenOfParentCollection: (state) => {
160269
return (id: string | undefined): BaseItemDto[] | undefined => {
161270
if (!id) {
162271
throw new Error('No itemId provided');
@@ -165,6 +274,24 @@ export const itemsStore = defineStore('items', {
165274
const res = [] as BaseItemDto[];
166275
const ids = state.collectionById[id];
167276

277+
if (ids?.length) {
278+
for (const _id of ids) {
279+
res.push(state.byId[_id]);
280+
}
281+
282+
return res;
283+
}
284+
};
285+
},
286+
getChildrenOfParentPlaylist: (state) => {
287+
return (id: string | undefined): BaseItemDto[] | undefined => {
288+
if (!id) {
289+
throw new Error('No itemId provided');
290+
}
291+
292+
const res = [] as BaseItemDto[];
293+
const ids = state.playlistById[id];
294+
168295
if (ids?.length) {
169296
for (const _id of ids) {
170297
res.push(state.byId[_id]);

0 commit comments

Comments
 (0)