mirror of
https://github.com/janishutz/MusicPlayer.git
synced 2026-09-10 14:25:24 +02:00
feat: playlist loading
This commit is contained in:
+24
-1
@@ -1,4 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Notifications
|
||||||
|
} from '@kyvg/vue3-notification';
|
||||||
import {
|
import {
|
||||||
RouterView
|
RouterView
|
||||||
} from 'vue-router';
|
} from 'vue-router';
|
||||||
@@ -35,6 +38,14 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<notifications
|
||||||
|
position="top center"
|
||||||
|
:duration="5000"
|
||||||
|
class="notifications"
|
||||||
|
style="top: 20px;"
|
||||||
|
width="400px"
|
||||||
|
:max="3"
|
||||||
|
/>
|
||||||
<button id="themeSelector" title="Toggle between light and dark mode" @click="changeTheme();">
|
<button id="themeSelector" title="Toggle between light and dark mode" @click="changeTheme();">
|
||||||
<i :class="['fa-solid', 'fa-' + theme]"></i>
|
<i :class="['fa-solid', 'fa-' + theme]"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -46,11 +57,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
body {
|
body {
|
||||||
background-color: var( --background-color );
|
background-color: var( --background-color );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notifications {
|
||||||
|
.vue-notification {
|
||||||
|
padding: 20px;
|
||||||
|
.notification-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.notification-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root, :root.light {
|
:root, :root.light {
|
||||||
--primary-color: #0a1520;
|
--primary-color: #0a1520;
|
||||||
--secondary-color: white;
|
--secondary-color: white;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import AssociationView from '@/composables/AssociationView.vue';
|
||||||
import PlayerComponent from './PlayerComponent.vue';
|
import PlayerComponent from './PlayerComponent.vue';
|
||||||
import SmallPlayerComponent from './SmallPlayerComponent.vue';
|
import SmallPlayerComponent from './SmallPlayerComponent.vue';
|
||||||
import {
|
import {
|
||||||
ref
|
fullPlayer
|
||||||
} from 'vue';
|
} from '@/ts/player/state';
|
||||||
|
|
||||||
const fullPlayer = ref( true );
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
fullPlayer.value = false;
|
fullPlayer.value = false;
|
||||||
@@ -14,6 +13,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="player-wrapper">
|
<div class="player-wrapper">
|
||||||
|
<AssociationView />
|
||||||
<SmallPlayerComponent v-model="fullPlayer" />
|
<SmallPlayerComponent v-model="fullPlayer" />
|
||||||
<div :class="['player-container', fullPlayer ? undefined : 'hidden']">
|
<div :class="['player-container', fullPlayer ? undefined : 'hidden']">
|
||||||
<i class="fa-solid fa-xmark" @click="close"></i>
|
<i class="fa-solid fa-xmark" @click="close"></i>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import UserPlaylists from '../playlists/UserPlaylists.vue';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Playlists</h1>
|
<h1>Playlists</h1>
|
||||||
|
<UserPlaylists />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
import {
|
import {
|
||||||
queueIdx
|
queueIdx
|
||||||
} from '@/ts/player/state';
|
} from '@/ts/player/state';
|
||||||
|
import {
|
||||||
|
savePlaylist
|
||||||
|
} from '@/ts/userPlaylists/save';
|
||||||
|
|
||||||
const queue: WritableComputedRef<Song[]> = computed( {
|
const queue: WritableComputedRef<Song[]> = computed( {
|
||||||
get () {
|
get () {
|
||||||
@@ -56,7 +59,7 @@
|
|||||||
<i class="fa-solid fa-xmark"></i>
|
<i class="fa-solid fa-xmark"></i>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
<button>
|
<button @click="savePlaylist">
|
||||||
<i class="fa-solid fa-save"></i>
|
<i class="fa-solid fa-save"></i>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import PopupElement from '../popups/PopupElement.vue';
|
||||||
|
import {
|
||||||
|
ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
const showPopup = defineModel<boolean>( {
|
||||||
|
'required': true
|
||||||
|
} );
|
||||||
|
|
||||||
|
const addPlaylist = () => {
|
||||||
|
showPopup.value = false;
|
||||||
|
emit( 'add-playlist', playlistName.value );
|
||||||
|
};
|
||||||
|
|
||||||
|
const playlistName = ref( '' );
|
||||||
|
const emit = defineEmits<{
|
||||||
|
( e: 'add-playlist', name: string ): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PopupElement v-model="showPopup" show-close>
|
||||||
|
<div class="title">
|
||||||
|
<h1>Add Playlist</h1>
|
||||||
|
</div>
|
||||||
|
<input v-model="playlistName" type="text" placeholder="Playlist name">
|
||||||
|
<br>
|
||||||
|
<button @click="addPlaylist">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
Add Playlist
|
||||||
|
</button>
|
||||||
|
</PopupElement>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.title {
|
||||||
|
>h1 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
>p {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.song-sources {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 50vw;
|
||||||
|
height: 40vh;
|
||||||
|
justify-content: center;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
>div {
|
||||||
|
width: 45%;
|
||||||
|
margin: 0.5%;
|
||||||
|
height: 60%;
|
||||||
|
background-color: var(--accent-background);
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
>div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
>.fa-solid {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
>.not-auth-notice {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
margin: 0;
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
addPlaylist,
|
||||||
|
removePlaylist
|
||||||
|
} from '@/ts/userPlaylists';
|
||||||
|
import {
|
||||||
|
editingPlaylists,
|
||||||
|
playlistIdx as playlistIdx,
|
||||||
|
playlists
|
||||||
|
} from '@/ts/userPlaylists/state';
|
||||||
|
import {
|
||||||
|
getPlaylists,
|
||||||
|
savePlaylists
|
||||||
|
} from '@/ts/userPlaylists/save';
|
||||||
|
import {
|
||||||
|
onMounted,
|
||||||
|
ref
|
||||||
|
} from 'vue';
|
||||||
|
import AddPlaylist from './AddPlaylist.vue';
|
||||||
|
import {
|
||||||
|
UnownedError
|
||||||
|
} from '@/ts/request';
|
||||||
|
import player from '@/ts/player';
|
||||||
|
import router from '@/router';
|
||||||
|
|
||||||
|
const checkingStatus = ref( true );
|
||||||
|
const dots = ref( 0 );
|
||||||
|
const showAddPlaylist = ref( false );
|
||||||
|
|
||||||
|
let interval = -1;
|
||||||
|
|
||||||
|
const loadPlaylists = async () => {
|
||||||
|
if ( interval < 0 ) {
|
||||||
|
interval = setInterval( () => {
|
||||||
|
dots.value = ( dots.value + 1 ) % 4;
|
||||||
|
}, 500 );
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getPlaylists();
|
||||||
|
} catch ( e ) {
|
||||||
|
if ( e instanceof UnownedError ) {
|
||||||
|
router.push( '/get' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkingStatus.value = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearInterval( interval );
|
||||||
|
interval = -1;
|
||||||
|
} catch { /* Empty */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted( () => {
|
||||||
|
loadPlaylists();
|
||||||
|
} );
|
||||||
|
|
||||||
|
onMounted( () => {} );
|
||||||
|
|
||||||
|
const openAddPlaylistPopup = () => {
|
||||||
|
showAddPlaylist.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePlaylistEditing = ( idx: number ) => {
|
||||||
|
editingPlaylists.value[ idx ] = !editingPlaylists.value[ idx ];
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPlaylist = ( idx: number ) => {
|
||||||
|
playlistIdx.value = idx;
|
||||||
|
player.clearQueue();
|
||||||
|
player.loadPlaylist( playlists.value[ idx ]!.songs );
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="playlists">
|
||||||
|
<AddPlaylist v-model="showAddPlaylist" @add-playlist="addPlaylist" />
|
||||||
|
<div v-if="checkingStatus">
|
||||||
|
Loading{{ '.'.repeat( dots ) }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="playlists.length === 0">
|
||||||
|
No playlists
|
||||||
|
<button @click="openAddPlaylistPopup">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
Add one
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button @click="openAddPlaylistPopup">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
Add Playlist
|
||||||
|
</button>
|
||||||
|
<button @click="savePlaylists">
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
<button @click="loadPlaylists">
|
||||||
|
<i class="fa-solid fa-rotate"></i>
|
||||||
|
Undo unsaved changes
|
||||||
|
</button>
|
||||||
|
<div v-for="(playlist, index) in playlists" :key="index">
|
||||||
|
<input v-if="editingPlaylists[ index ]" v-model="playlist.name" type="text">
|
||||||
|
<h2 v-else @click="() => selectPlaylist( index )">
|
||||||
|
{{ playlist.name }}
|
||||||
|
</h2>
|
||||||
|
<i class="fa-solid fa-pen-to-square" @click="() => togglePlaylistEditing( index )"></i>
|
||||||
|
<i class="fa-solid fa-trash" @click="() => removePlaylist( index )"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -7,22 +7,23 @@
|
|||||||
needsFiles,
|
needsFiles,
|
||||||
saveAssociations
|
saveAssociations
|
||||||
} from './associationManager';
|
} from './associationManager';
|
||||||
|
import {
|
||||||
|
fullPlayer,
|
||||||
|
queue
|
||||||
|
} from '@/ts/player/state';
|
||||||
import {
|
import {
|
||||||
onMounted,
|
onMounted,
|
||||||
useTemplateRef
|
useTemplateRef
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import PopupElement from '@/components/popups/PopupElement.vue';
|
import PopupElement from '@/components/popups/PopupElement.vue';
|
||||||
import player from '@/ts/player';
|
import player from '@/ts/player';
|
||||||
import {
|
|
||||||
queue
|
|
||||||
} from '@/ts/player/state';
|
|
||||||
|
|
||||||
const fileinput = useTemplateRef( 'fileinput' );
|
const fileinput = useTemplateRef( 'fileinput' );
|
||||||
|
|
||||||
onMounted( () => {
|
onMounted( () => {
|
||||||
fileinput.value?.addEventListener( 'change', async () => {
|
fileinput.value?.addEventListener( 'change', async () => {
|
||||||
if ( fileinput.value && fileinput.value.files ) {
|
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 );
|
associationResults.value.splice( idx, 1 );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
player.clearQueue();
|
||||||
|
isShowingAssociationManager.value = false;
|
||||||
|
fullPlayer.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -58,16 +65,15 @@
|
|||||||
:accept="associationOpts?.mime ?? '*'"
|
:accept="associationOpts?.mime ?? '*'"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!needsFiles && !isAnalyzing && associationResults.length === 0">
|
<div v-if="needsFiles && !isAnalyzing && associationResults.length > 0" class="association-wrapper">
|
||||||
All files have been associated correctly
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!needsFiles && !isAnalyzing && associationResults.length > 0">
|
|
||||||
<button @click="saveAssociations">
|
<button @click="saveAssociations">
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<div v-for="(result, index) in associationResults" :key="index">
|
<div v-for="(result, index) in associationResults" :key="index" class="association">
|
||||||
<p>
|
<p>
|
||||||
{{ result.song.name }} by {{ result.song.artist }}
|
<b>{{ result.song.name.length > 30 ? result.song.name.slice( 0, 30 ) + '...' : result.song.name }}</b>
|
||||||
|
by
|
||||||
|
<i>{{ result.song.artist.length > 20 ? result.song.artist.slice( 0, 20 ) + '...' : result.song.artist }}</i>
|
||||||
</p>
|
</p>
|
||||||
<select v-if="result.match === 'multiple'">
|
<select v-if="result.match === 'multiple'">
|
||||||
<option v-for="(file, idx) in result.possibleFiles" :key="idx" :value="idx">
|
<option v-for="(file, idx) in result.possibleFiles" :key="idx" :value="idx">
|
||||||
@@ -82,10 +88,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="associationResults.length === 0"></div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
An error occurred. Please try again
|
An error occurred. Please try again
|
||||||
<!-- FIXME: Button to restore -->
|
<!-- FIXME: Button to restore -->
|
||||||
</div>
|
</div>
|
||||||
|
<button @click="cancel">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</PopupElement>
|
</PopupElement>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.association-wrapper {
|
||||||
|
width: 60vw;
|
||||||
|
height: 50vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
.association {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
|
>div, select {
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
>p {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
AssociationResult
|
AssociationResult
|
||||||
} from '@/ts/player/plugins/interface';
|
} from '@/ts/player/plugins/interface';
|
||||||
|
import player from '@/ts/player';
|
||||||
import {
|
import {
|
||||||
queue
|
queue
|
||||||
} from '@/ts/player/state';
|
} from '@/ts/player/state';
|
||||||
@@ -21,7 +22,7 @@ export const isAnalyzing = ref( false );
|
|||||||
export const associationResults: Ref<AssociationResult[]> = ref( [] );
|
export const associationResults: Ref<AssociationResult[]> = ref( [] );
|
||||||
|
|
||||||
export const associationOpts: Ref<{
|
export const associationOpts: Ref<{
|
||||||
'get': ( files: FileList ) => Promise<AssociationResult[]>,
|
'get': ( files: FileList ) => Promise<void>,
|
||||||
'mime': string;
|
'mime': string;
|
||||||
} | null> = ref( null );
|
} | null> = ref( null );
|
||||||
|
|
||||||
@@ -30,8 +31,21 @@ export const openAssociationManager = ( cb: ( files: FileList ) => Promise<Assoc
|
|||||||
associationResults.value = [];
|
associationResults.value = [];
|
||||||
needsFiles.value = true;
|
needsFiles.value = true;
|
||||||
isAnalyzing.value = false;
|
isAnalyzing.value = false;
|
||||||
|
|
||||||
|
const callback = async ( files: FileList ): Promise<void> => {
|
||||||
|
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 = {
|
associationOpts.value = {
|
||||||
'get': cb,
|
'get': callback,
|
||||||
'mime': mime
|
'mime': mime
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -45,4 +59,9 @@ export const saveAssociations = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( associationResults.value.length === 0 ) {
|
||||||
|
isShowingAssociationManager.value = false;
|
||||||
|
player.playIndex( 0 );
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import '@fortawesome/fontawesome-free/css/all.css';
|
import '@fortawesome/fontawesome-free/css/all.css';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
import Notifications from '@kyvg/vue3-notification';
|
||||||
import {
|
import {
|
||||||
createApp
|
createApp
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
@@ -12,5 +13,6 @@ const app = createApp( App );
|
|||||||
|
|
||||||
app.use( createPinia() );
|
app.use( createPinia() );
|
||||||
app.use( router );
|
app.use( router );
|
||||||
|
app.use( Notifications );
|
||||||
|
|
||||||
app.mount( '#app' );
|
app.mount( '#app' );
|
||||||
|
|||||||
Vendored
+5
@@ -1,5 +1,10 @@
|
|||||||
// NOTE: For re-associating files with details here, can use dropdown in UI
|
// 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 type PlaylistSongs = Song[];
|
||||||
|
|
||||||
export interface UrlToFileMapping {
|
export interface UrlToFileMapping {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
Song
|
Song
|
||||||
} from '@/ts/dtype/playlist';
|
} from '@/ts/dtype/playlist';
|
||||||
|
import {
|
||||||
|
playlistIdx
|
||||||
|
} from '@/ts/userPlaylists/state';
|
||||||
|
|
||||||
export const addSongList = ( songs: Song[] ) => {
|
export const addSongList = ( songs: Song[] ) => {
|
||||||
rawQueue.value = rawQueue.value.concat( songs );
|
rawQueue.value = rawQueue.value.concat( songs );
|
||||||
@@ -22,6 +25,7 @@ export const addSongList = ( songs: Song[] ) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const clearQueue = () => {
|
export const clearQueue = () => {
|
||||||
|
playlistIdx.value = -1;
|
||||||
queue.value = [];
|
queue.value = [];
|
||||||
rawQueue.value = [];
|
rawQueue.value = [];
|
||||||
sources[currentSource.value]?.stop();
|
sources[currentSource.value]?.stop();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
fullPlayer,
|
||||||
queue,
|
queue,
|
||||||
rawQueue,
|
rawQueue,
|
||||||
sources
|
sources
|
||||||
@@ -42,6 +43,16 @@ export const load = ( playlist: PlaylistSongs ) => {
|
|||||||
|
|
||||||
if ( needToLoadLocalSongs ) {
|
if ( needToLoadLocalSongs ) {
|
||||||
// FIXME: Combine mime types
|
// 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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const currentSource = ref( '' );
|
|||||||
|
|
||||||
export const rawQueue: Ref<PlaylistSongs> = ref( [] );
|
export const rawQueue: Ref<PlaylistSongs> = ref( [] );
|
||||||
|
|
||||||
export const queueIdx = ref( 0 );
|
export const queueIdx = ref( -1 );
|
||||||
|
|
||||||
export const queue: Ref<PlaylistSongs> = ref( [] );
|
export const queue: Ref<PlaylistSongs> = ref( [] );
|
||||||
|
|
||||||
@@ -36,6 +36,8 @@ export const shuffle = ref( false );
|
|||||||
|
|
||||||
export const repeat: Ref<RepeatMode> = ref( 'off' );
|
export const repeat: Ref<RepeatMode> = ref( 'off' );
|
||||||
|
|
||||||
|
export const fullPlayer = ref( false );
|
||||||
|
|
||||||
const initSources = async () => {
|
const initSources = async () => {
|
||||||
try {
|
try {
|
||||||
sources['applemusic'] = await useMusicKit();
|
sources['applemusic'] = await useMusicKit();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const post = async ( url: string, payload: string, mime: string = 'application/j
|
|||||||
return await wrapper( url, {
|
return await wrapper( url, {
|
||||||
'credentials': 'include',
|
'credentials': 'include',
|
||||||
'body': payload,
|
'body': payload,
|
||||||
|
'method': 'post',
|
||||||
'headers': {
|
'headers': {
|
||||||
'Content-Type': mime ?? 'application/json'
|
'Content-Type': mime ?? 'application/json'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,62 @@
|
|||||||
|
import {
|
||||||
|
editingPlaylists,
|
||||||
|
playlistIdx,
|
||||||
|
playlists
|
||||||
|
} from './state';
|
||||||
import {
|
import {
|
||||||
queue,
|
queue,
|
||||||
rawQueue,
|
rawQueue,
|
||||||
shuffle
|
shuffle
|
||||||
} from '../player/state';
|
} 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;
|
rawQueue.value = queue.value;
|
||||||
shuffle.value = false;
|
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'
|
||||||
|
} );
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import {
|
|||||||
ref
|
ref
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import type {
|
import type {
|
||||||
PlaylistSongs
|
Playlist
|
||||||
} from '../dtype/playlist';
|
} from './file';
|
||||||
|
|
||||||
const currentPlaylist = ref( '' );
|
export const playlistIdx = ref( -1 );
|
||||||
const playlists: Ref<PlaylistSongs> = ref( [] );
|
|
||||||
|
export const playlists: Ref<Playlist[]> = ref( [] );
|
||||||
|
|
||||||
|
export const editingPlaylists: Ref<boolean[]> = ref( [] );
|
||||||
|
|||||||
@@ -1,34 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PlayerWrapper from '@/components/main/PlayerWrapper.vue';
|
import PlayerWrapper from '@/components/main/PlayerWrapper.vue';
|
||||||
import PlaylistsComponent from '@/components/main/PlaylistsComponent.vue';
|
import PlaylistsComponent from '@/components/main/PlaylistsComponent.vue';
|
||||||
import {
|
|
||||||
ref
|
|
||||||
} from 'vue';
|
|
||||||
import request from '@/ts/request';
|
|
||||||
import router from '@/router';
|
|
||||||
|
|
||||||
const checkingStatus = ref( true );
|
|
||||||
|
|
||||||
const ownershipCheck = async () => {
|
|
||||||
const data = await ( await request.get( '/user/owned' ) ).json();
|
|
||||||
|
|
||||||
if ( data[ 'status' ] )
|
|
||||||
router.push( '/get' );
|
|
||||||
};
|
|
||||||
|
|
||||||
ownershipCheck();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-app">
|
<div class="main-app">
|
||||||
<div v-if="checkingStatus">
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<PlaylistsComponent />
|
<PlaylistsComponent />
|
||||||
<PlayerWrapper />
|
<PlayerWrapper />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user