From 867374fa4748db4da112d42e2805c98737cf7dd1 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Mon, 7 Sep 2026 16:15:31 +0200 Subject: [PATCH] feat: playlist loading --- web/src/App.vue | 25 +++- web/src/components/UserPlaylists.vue | 0 web/src/components/main/PlayerWrapper.vue | 8 +- .../components/main/PlaylistsComponent.vue | 6 + web/src/components/player/QueueViewer.vue | 5 +- web/src/components/playlists/AddPlaylist.vue | 86 ++++++++++++++ .../components/playlists/UserPlaylists.vue | 112 ++++++++++++++++++ web/src/composables/AssociationView.vue | 56 +++++++-- web/src/composables/associationManager.ts | 23 +++- web/src/main.ts | 2 + web/src/ts/dtype/playlist.d.ts | 5 + web/src/ts/player/playlists/add.ts | 4 + web/src/ts/player/playlists/loader.ts | 13 +- web/src/ts/player/state.ts | 4 +- web/src/ts/request.ts | 1 + web/src/ts/userPlaylists/index.ts | 19 +++ web/src/ts/userPlaylists/save.ts | 54 ++++++++- web/src/ts/userPlaylists/state.ts | 11 +- web/src/views/AppView.vue | 24 +--- 19 files changed, 411 insertions(+), 47 deletions(-) delete mode 100644 web/src/components/UserPlaylists.vue create mode 100644 web/src/components/playlists/AddPlaylist.vue create mode 100644 web/src/components/playlists/UserPlaylists.vue diff --git a/web/src/App.vue b/web/src/App.vue index 5fab23e..d4e33ff 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,4 +1,7 @@ + diff --git a/web/src/components/player/QueueViewer.vue b/web/src/components/player/QueueViewer.vue index 0e231af..f115488 100644 --- a/web/src/components/player/QueueViewer.vue +++ b/web/src/components/player/QueueViewer.vue @@ -18,6 +18,9 @@ import { queueIdx } from '@/ts/player/state'; + import { + savePlaylist + } from '@/ts/userPlaylists/save'; const queue: WritableComputedRef = computed( { get () { @@ -56,7 +59,7 @@ Clear - diff --git a/web/src/components/playlists/AddPlaylist.vue b/web/src/components/playlists/AddPlaylist.vue new file mode 100644 index 0000000..d91439f --- /dev/null +++ b/web/src/components/playlists/AddPlaylist.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/web/src/components/playlists/UserPlaylists.vue b/web/src/components/playlists/UserPlaylists.vue new file mode 100644 index 0000000..4eaf5db --- /dev/null +++ b/web/src/components/playlists/UserPlaylists.vue @@ -0,0 +1,112 @@ + + + diff --git a/web/src/composables/AssociationView.vue b/web/src/composables/AssociationView.vue index f4cf231..9193a3d 100644 --- a/web/src/composables/AssociationView.vue +++ b/web/src/composables/AssociationView.vue @@ -7,22 +7,23 @@ needsFiles, saveAssociations } from './associationManager'; + import { + fullPlayer, + queue + } from '@/ts/player/state'; import { onMounted, useTemplateRef } from 'vue'; import PopupElement from '@/components/popups/PopupElement.vue'; import player from '@/ts/player'; - import { - queue - } from '@/ts/player/state'; const fileinput = useTemplateRef( 'fileinput' ); onMounted( () => { fileinput.value?.addEventListener( 'change', async () => { if ( fileinput.value && fileinput.value.files ) { - associationResults.value.concat( await associationOpts.value?.get( fileinput.value.files ) ?? [] ); + await associationOpts.value?.get( fileinput.value.files ); } } ); } ); @@ -42,6 +43,12 @@ associationResults.value.splice( idx, 1 ); }; + + const cancel = () => { + player.clearQueue(); + isShowingAssociationManager.value = false; + fullPlayer.value = false; + }; + + diff --git a/web/src/composables/associationManager.ts b/web/src/composables/associationManager.ts index dea751f..2986fa7 100644 --- a/web/src/composables/associationManager.ts +++ b/web/src/composables/associationManager.ts @@ -5,6 +5,7 @@ import { import type { AssociationResult } from '@/ts/player/plugins/interface'; +import player from '@/ts/player'; import { queue } from '@/ts/player/state'; @@ -21,7 +22,7 @@ export const isAnalyzing = ref( false ); export const associationResults: Ref = ref( [] ); export const associationOpts: Ref<{ - 'get': ( files: FileList ) => Promise, + 'get': ( files: FileList ) => Promise, 'mime': string; } | null> = ref( null ); @@ -30,8 +31,21 @@ export const openAssociationManager = ( cb: ( files: FileList ) => Promise => { + const results = await cb( files ); + + + if ( results?.length ?? -1 > 0 ) { + associationResults.value = associationResults.value.concat( results! ); + } else { + isShowingAssociationManager.value = false; + player.playIndex( 0 ); + } + }; + associationOpts.value = { - 'get': cb, + 'get': callback, 'mime': mime }; }; @@ -45,4 +59,9 @@ export const saveAssociations = () => { } } } + + if ( associationResults.value.length === 0 ) { + isShowingAssociationManager.value = false; + player.playIndex( 0 ); + } }; diff --git a/web/src/main.ts b/web/src/main.ts index 1c95866..55d336a 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,5 +1,6 @@ import '@fortawesome/fontawesome-free/css/all.css'; import App from './App.vue'; +import Notifications from '@kyvg/vue3-notification'; import { createApp } from 'vue'; @@ -12,5 +13,6 @@ const app = createApp( App ); app.use( createPinia() ); app.use( router ); +app.use( Notifications ); app.mount( '#app' ); diff --git a/web/src/ts/dtype/playlist.d.ts b/web/src/ts/dtype/playlist.d.ts index 6713e78..0360ce9 100644 --- a/web/src/ts/dtype/playlist.d.ts +++ b/web/src/ts/dtype/playlist.d.ts @@ -1,5 +1,10 @@ // NOTE: For re-associating files with details here, can use dropdown in UI +export interface Playlist { + 'name': string; + 'songs': PlaylistSongs; +} + export type PlaylistSongs = Song[]; export interface UrlToFileMapping { diff --git a/web/src/ts/player/playlists/add.ts b/web/src/ts/player/playlists/add.ts index a985879..8ce8fa7 100644 --- a/web/src/ts/player/playlists/add.ts +++ b/web/src/ts/player/playlists/add.ts @@ -15,6 +15,9 @@ import { import type { Song } from '@/ts/dtype/playlist'; +import { + playlistIdx +} from '@/ts/userPlaylists/state'; export const addSongList = ( songs: Song[] ) => { rawQueue.value = rawQueue.value.concat( songs ); @@ -22,6 +25,7 @@ export const addSongList = ( songs: Song[] ) => { }; export const clearQueue = () => { + playlistIdx.value = -1; queue.value = []; rawQueue.value = []; sources[currentSource.value]?.stop(); diff --git a/web/src/ts/player/playlists/loader.ts b/web/src/ts/player/playlists/loader.ts index 0bdfc7e..069cd24 100644 --- a/web/src/ts/player/playlists/loader.ts +++ b/web/src/ts/player/playlists/loader.ts @@ -1,4 +1,5 @@ import { + fullPlayer, queue, rawQueue, sources @@ -42,6 +43,16 @@ export const load = ( playlist: PlaylistSongs ) => { if ( needToLoadLocalSongs ) { // FIXME: Combine mime types - openAssociationManager( fileLoader, '' ); + const mime = Object.values( sources ) + .map( src => { + return src.loading.requiresLocalFiles === true ? src.loading.mime : ''; + } ) + .reduce( ( prev, curr ) => { + return prev === '' ? curr : prev + ',' + curr; + } ); + + openAssociationManager( fileLoader, mime ); } + + fullPlayer.value = true; }; diff --git a/web/src/ts/player/state.ts b/web/src/ts/player/state.ts index 5b436a2..7e4fed1 100644 --- a/web/src/ts/player/state.ts +++ b/web/src/ts/player/state.ts @@ -26,7 +26,7 @@ export const currentSource = ref( '' ); export const rawQueue: Ref = ref( [] ); -export const queueIdx = ref( 0 ); +export const queueIdx = ref( -1 ); export const queue: Ref = ref( [] ); @@ -36,6 +36,8 @@ export const shuffle = ref( false ); export const repeat: Ref = ref( 'off' ); +export const fullPlayer = ref( false ); + const initSources = async () => { try { sources['applemusic'] = await useMusicKit(); diff --git a/web/src/ts/request.ts b/web/src/ts/request.ts index 95f0b2b..34fd3ac 100644 --- a/web/src/ts/request.ts +++ b/web/src/ts/request.ts @@ -13,6 +13,7 @@ const post = async ( url: string, payload: string, mime: string = 'application/j return await wrapper( url, { 'credentials': 'include', 'body': payload, + 'method': 'post', 'headers': { 'Content-Type': mime ?? 'application/json' } diff --git a/web/src/ts/userPlaylists/index.ts b/web/src/ts/userPlaylists/index.ts index e69de29..b8a6f84 100644 --- a/web/src/ts/userPlaylists/index.ts +++ b/web/src/ts/userPlaylists/index.ts @@ -0,0 +1,19 @@ +import { + editingPlaylists, + playlists +} from './state'; + +export const addPlaylist = ( name: string ) => { + playlists.value.push( { + 'name': name, + 'songs': [] + } ); + editingPlaylists.value.push( false ); +}; + +export const removePlaylist = ( idx: number ) => { + if ( confirm( 'Do you really want to delete this playlist?' ) ) { + playlists.value.splice( idx, 1 ); + editingPlaylists.value.splice( idx, 1 ); + } +}; diff --git a/web/src/ts/userPlaylists/save.ts b/web/src/ts/userPlaylists/save.ts index fa3beb7..2349a20 100644 --- a/web/src/ts/userPlaylists/save.ts +++ b/web/src/ts/userPlaylists/save.ts @@ -1,10 +1,62 @@ +import { + editingPlaylists, + playlistIdx, + playlists +} from './state'; import { queue, rawQueue, shuffle } from '../player/state'; +import { + addPlaylist +} from '.'; +import request from '../request'; +import { + useNotification +} from '@kyvg/vue3-notification'; -const savePlaylist = () => { +export const savePlaylist = async () => { rawQueue.value = queue.value; shuffle.value = false; + + if ( playlistIdx.value ) { + let name: null | string = ''; + + while ( !name || name.length === 0 ) { + name = prompt( 'You are trying to save a playlist that has not previously been created. Please enter a name for it' ); + } + + addPlaylist( name ); + playlistIdx.value = playlists.value.length - 1; + } + + playlists.value[ playlistIdx.value ]!.songs = rawQueue.value; + + savePlaylists(); +}; + +export const getPlaylists = async () => { + playlists.value = await ( await request.get( '/user/playlists' ) ).json(); + editingPlaylists.value = playlists.value.map( () => false ); +}; + +export const savePlaylists = async () => { + const notifications = useNotification(); + + try { + await request.post( '/user/playlists', JSON.stringify( playlists.value ) ); + notifications.notify( { + 'text': 'Playlists saved successfully', + 'type': 'success', + 'title': 'Playlists' + } ); + } catch ( e ) { + console.error( e ); + notifications.notify( { + 'text': 'Failed to save playlists', + 'type': 'error', + 'title': 'Playlists' + } ); + } }; diff --git a/web/src/ts/userPlaylists/state.ts b/web/src/ts/userPlaylists/state.ts index 1663291..04ed6d3 100644 --- a/web/src/ts/userPlaylists/state.ts +++ b/web/src/ts/userPlaylists/state.ts @@ -3,8 +3,11 @@ import { ref } from 'vue'; import type { - PlaylistSongs -} from '../dtype/playlist'; + Playlist +} from './file'; -const currentPlaylist = ref( '' ); -const playlists: Ref = ref( [] ); +export const playlistIdx = ref( -1 ); + +export const playlists: Ref = ref( [] ); + +export const editingPlaylists: Ref = ref( [] ); diff --git a/web/src/views/AppView.vue b/web/src/views/AppView.vue index 8ef9798..a74c5cf 100644 --- a/web/src/views/AppView.vue +++ b/web/src/views/AppView.vue @@ -1,33 +1,13 @@