Skip to content

Commit

Permalink
feat(playlist) Got playlists working for the most part. (the skeleton…
Browse files Browse the repository at this point in the history
… display isn't quite right, though.)
  • Loading branch information
BrianCArnold committed Nov 25, 2022
1 parent 4cceda0 commit ef7e673
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 10 deletions.
7 changes: 5 additions & 2 deletions frontend/components/Item/CollectionTabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ export default Vue.extend({
computed: {
...mapStores(itemsStore),
children(): Record<string, BaseItemDto[]> | undefined {
if (this.items.getChildrenOfParent(this.item.Id)?.length) {
return groupBy(this.items.getChildrenOfParent(this.item.Id), 'Type');
if (this.items.getChildrenOfParentCollection(this.item.Id)?.length) {
return groupBy(
this.items.getChildrenOfParentCollection(this.item.Id),
'Type'
);
}
}
},
Expand Down
183 changes: 183 additions & 0 deletions frontend/components/Item/PlaylistItems.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<template>
<div>
<h1 v-if="!children && !loading" class="text-h5 text-center">
{{ $t('collectionEmpty') }}
</h1>
<skeleton-item-grid v-if="loading" :view-type="''" />

<v-list v-if="!!children" color="transparent" two-line>
<v-list-item-group class="list-group">
<draggable
class="list-draggable"
v-bind="dragOptions"
v-if="children.length > 0"
v-model="children"
:move="checkMove"
>
<v-hover
v-for="(item, index) in children"
:key="`${item.Id}-${index}`"
v-slot="{ hover }"
>
<v-list-item ripple @click="playQueueFrom(index)">
<v-list-item-action
v-if="!hover"
class="list-group-item d-flex justify-center d-flex transition"
:class="{ 'primary--text font-weight-bold': isPlaying(item) }"
>
{{ index + 1 }}
</v-list-item-action>
<v-list-item-action v-else class="justify-center d-flex">
<v-icon>mdi-drag-horizontal</v-icon>
</v-list-item-action>
<v-list-item-avatar tile class="list-group-item">
<blurhash-image :item="item" />
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title
class="text-truncate ml-2 list-group-item transition"
:class="{
'primary--text font-weight-bold': isPlaying(item)
}"
>
{{ item.Name }}
</v-list-item-title>
<v-list-item-subtitle
v-if="getArtists(item)"
class="ml-2 list-group-item transition"
:class="{
'primary--text font-weight-bold': isPlaying(item)
}"
>
{{ getArtists(item) }}
</v-list-item-subtitle>
</v-list-item-content>

<v-list-item-action>
<like-button :item="item" />
</v-list-item-action>
<v-list-item-action class="mr-2">
<item-menu :item="item" queue />
</v-list-item-action>
</v-list-item>
</v-hover>
</draggable>
<div
v-for="index in skeletonLength"
v-else
:key="index"
class="d-flex align-center mt-5 mb-5"
>
<v-skeleton-loader type="avatar" class="ml-3 mr-3" />
<v-skeleton-loader type="sentences" width="10em" class="pr-5" />
</div>
</v-list-item-group>
</v-list>
</div>
</template>

<script lang="ts">
import { BaseItemDto } from '@jellyfin/client-axios';
import Vue from 'vue';
import { mapStores } from 'pinia';
import { itemsStore, playbackManagerStore } from '~/store';
export default Vue.extend({
props: {
item: {
type: Object as () => BaseItemDto,
required: true
}
},
methods: {
checkMove(evt: any) {
console.log(evt.draggedContext);
this.newIndex = evt.draggedContext.futureIndex;
this.oldIndex = evt.draggedContext.index;
},
getArtists(item: BaseItemDto): string | null {
if (item.Artists) {
return item.Artists.join(', ');
} else {
return null;
}
},
isPlaying(item: BaseItemDto): boolean {
if (this.playbackManager.getCurrentItem == undefined) {
return false;
}
return item.Id == (this.playbackManager.getCurrentItem as BaseItemDto).Id;
},
playQueueFrom(playFromIndex: number): void {
this.playbackManager
.play({
item: this.item,
startFromIndex: playFromIndex,
initiator: this.item
})
.then(() => this.items.fetchAndAddPlaylist(this.item.Id as string));
}
},
data() {
return {
currentTab: 0,
loading: false,
newIndex: null as number | null,
oldIndex: null as number | null,
dragOptions: {
animation: 500,
delay: 0,
group: false,
dragoverBubble: true,
ghostClass: 'ghost'
}
};
},
computed: {
...mapStores(itemsStore, playbackManagerStore),
children: {
get(): BaseItemDto[] {
return this.items.getChildrenOfParentPlaylist(
this.item.Id
) as BaseItemDto[];
},
set(newValue: BaseItemDto[]): void {
if (this.oldIndex != null && this.newIndex != null) {
this.loading = true;
console.log(this.item);
console.log(this.children[this.oldIndex]);
console.log(this.newIndex);
this.items
.movePlaylistItem(
this.item,
this.children[this.oldIndex],
this.newIndex
)
.then(() => (this.loading = false));
}
// this.items.addCollection(this.item, newValue);
// setTimeout(() => (this.loading = false), 1000);
}
}
},
watch: {
item: {
immediate: true,
async handler(item: BaseItemDto): Promise<void> {
if (!this.children) {
this.loading = true;
await this.items.fetchAndAddPlaylist(item.Id as string);
this.loading = false;
}
}
}
}
});
</script>

<style lang="scss" scoped>
.list-draggable {
user-select: none;
min-height: 20px;
}
</style>
8 changes: 4 additions & 4 deletions frontend/pages/item/_itemId/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,12 @@
</v-col>
</v-row>
<v-row>
<v-col
v-if="item.Type === 'BoxSet' || item.Type === 'Playlist'"
cols="12"
>
<v-col v-if="item.Type === 'BoxSet'" cols="12">
<collection-tabs :item="item" />
</v-col>
<v-col v-if="item.Type === 'Playlist'" cols="12">
<playlist-items :item="item" />
</v-col>
<v-col cols="12">
<related-items :id="$route.params.itemId" :item="item" />
</v-col>
Expand Down
135 changes: 131 additions & 4 deletions frontend/store/items.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import Vue from 'vue';
import { BaseItemDto, ItemFields } from '@jellyfin/client-axios';
import { BaseItemDto, ImageType, ItemFields } from '@jellyfin/client-axios';
import { defineStore } from 'pinia';
import { authStore } from '.';

export interface ItemsState {
byId: Record<string, BaseItemDto>;
collectionById: Record<string, string[]>;
playlistById: Record<string, string[]>;
}

export const itemsStore = defineStore('items', {
state: () => {
return {
byId: {},
collectionById: {}
collectionById: {},
playlistById: {}
} as ItemsState;
},
actions: {
Expand Down Expand Up @@ -69,6 +71,113 @@ export const itemsStore = defineStore('items', {
* @param children
* @returns - The children of the item
*/
async movePlaylistItem(
parent: BaseItemDto,
localChild: BaseItemDto,
index: number
): Promise<BaseItemDto[]> {
const auth = authStore();

// You're probably asking "... but why?"

// Because when the Playback manager is playing these tracks,
// it seems to erase the PlaylistItemId from each of the items.
// So... I just get a new bunch of them to move things.

// Probably a better way to do it, but...
// ... I didn't feel like figuring that out right now.

// If you try to fix this, make sure that you can click "Play"
// on the playlist, then move tracks.

const children = await this.$nuxt.$api.playlists.getPlaylistItems({
userId: auth.currentUserId,
playlistId: parent.Id as string,
fields: [ItemFields.PrimaryImageAspectRatio],
enableImageTypes: [
ImageType.Primary,
ImageType.Backdrop,
ImageType.Banner,
ImageType.Thumb
]
});
const child = children.data.Items?.find(
(i) => i.Id == localChild.Id
) as BaseItemDto;
await this.$nuxt.$api.playlists.moveItem({
playlistId: parent.Id as string,
itemId: child.PlaylistItemId as string,
newIndex: index
});
return (await this.fetchAndAddPlaylist(
parent.Id as string
)) as BaseItemDto[];
},
addPlaylist(parent: BaseItemDto, children: BaseItemDto[]): BaseItemDto[] {
if (!parent.Id) {
throw new Error("Parent item doesn't have an Id");
}

const childIds = [];

for (const child of children) {
if (child.Id) {
if (!this.getItemById(child.Id)) {
this.add(child);
}

childIds.push(child.Id);
}
}

Vue.set(this.playlistById, parent.Id, childIds);

return this.getChildrenOfParentPlaylist(parent.Id) as BaseItemDto[];
},
async fetchAndAddPlaylist(
parentId: string | undefined
): Promise<BaseItemDto[]> {
const auth = authStore();

if (parentId && !this.getItemById(parentId)) {
const parentItem = (
await this.$nuxt.$api.items.getItems({
userId: auth.currentUserId,
ids: [parentId],
fields: Object.values(ItemFields)
})
).data;

if (!parentItem.Items?.[0]) {
throw new Error("This parent doesn't exist");
}

this.add(parentItem.Items[0]);
}

const childItems = (
await this.$nuxt.$api.playlists.getPlaylistItems({
userId: auth.currentUserId,
playlistId: parentId as string,
fields: [ItemFields.PrimaryImageAspectRatio],
enableImageTypes: [
ImageType.Primary,
ImageType.Backdrop,
ImageType.Banner,
ImageType.Thumb
]
})
).data;

if (childItems.Items) {
const parent = this.getItemById(parentId);

return this.addPlaylist(parent as BaseItemDto, childItems.Items);
} else {
// I think this just means it's an empty playlist...?
return this.addPlaylist(parent as BaseItemDto, []);
}
},
addCollection(parent: BaseItemDto, children: BaseItemDto[]): BaseItemDto[] {
if (!parent.Id) {
throw new Error("Parent item doesn't have an Id");
Expand All @@ -88,7 +197,7 @@ export const itemsStore = defineStore('items', {

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

return this.getChildrenOfParent(parent.Id) as BaseItemDto[];
return this.getChildrenOfParentCollection(parent.Id) as BaseItemDto[];
},
/**
* Fetches a parent and its children and adds thecollection to the store
Expand Down Expand Up @@ -156,7 +265,7 @@ export const itemsStore = defineStore('items', {
return res;
};
},
getChildrenOfParent: (state) => {
getChildrenOfParentCollection: (state) => {
return (id: string | undefined): BaseItemDto[] | undefined => {
if (!id) {
throw new Error('No itemId provided');
Expand All @@ -165,6 +274,24 @@ export const itemsStore = defineStore('items', {
const res = [] as BaseItemDto[];
const ids = state.collectionById[id];

if (ids?.length) {
for (const _id of ids) {
res.push(state.byId[_id]);
}

return res;
}
};
},
getChildrenOfParentPlaylist: (state) => {
return (id: string | undefined): BaseItemDto[] | undefined => {
if (!id) {
throw new Error('No itemId provided');
}

const res = [] as BaseItemDto[];
const ids = state.playlistById[id];

if (ids?.length) {
for (const _id of ids) {
res.push(state.byId[_id]);
Expand Down

0 comments on commit ef7e673

Please sign in to comment.