mirror of
https://github.com/janishutz/MusicPlayer.git
synced 2026-09-10 14:25:24 +02:00
feat: design for queue, add songs, search and more
This commit is contained in:
@@ -50,6 +50,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
>.fa-solid {
|
||||
font-size: 1.5rem;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import PopupElement from './PopupElement.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PopupElement>
|
||||
<h2></h2>
|
||||
<button>Ok</button>
|
||||
<button>Cancel</button>
|
||||
</PopupElement>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,22 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type Ref,
|
||||
type WritableComputedRef,
|
||||
computed,
|
||||
ref
|
||||
} from 'vue';
|
||||
import AddSong from './AddSong.vue';
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
import SongEditor from './SongEditor.vue';
|
||||
import SortableList from './SortableList.vue';
|
||||
import {
|
||||
beautifyTime
|
||||
} from '@/ts/util/time';
|
||||
import player from '@/ts/player';
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
|
||||
const queue = player.queue;
|
||||
const queueIndex = player.queueIdx;
|
||||
const queue: WritableComputedRef<Song[]> = computed( {
|
||||
get () {
|
||||
return player.queue.value.slice( player.queueIdx.value );
|
||||
},
|
||||
set ( val ) {
|
||||
player.queue.value = player.queue.value.slice( 0, player.queueIdx.value ).concat( val );
|
||||
}
|
||||
} );
|
||||
const isPlaying = player.isPlaying;
|
||||
const showAddSong = ref( false );
|
||||
const showEditSong = ref( false );
|
||||
const editingSong: Ref<null | Song> = ref( null );
|
||||
|
||||
const addSong = () => {
|
||||
showAddSong.value = true;
|
||||
};
|
||||
|
||||
const editSong = ( idx: number ) => {
|
||||
showEditSong.value = true;
|
||||
editingSong.value = player.queue.value[ idx ]!;
|
||||
};
|
||||
|
||||
const deleteSong = ( idx: number ) => {
|
||||
player.removeSong( idx );
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -40,6 +64,7 @@
|
||||
Transmit
|
||||
</button>
|
||||
</div>
|
||||
<SongEditor v-model="showEditSong" editing-song="" />
|
||||
<AddSong v-model="showAddSong" />
|
||||
<div>
|
||||
<SortableList v-slot="{ item: song, index }" v-model="queue">
|
||||
@@ -52,7 +77,7 @@
|
||||
class="song-cover"
|
||||
>
|
||||
<i v-else class="fa-solid fa-music song-cover"></i>
|
||||
<div v-if="index === queueIndex" class="play-overlay">
|
||||
<div v-if="index === 0" class="play-overlay">
|
||||
<div v-if="isPlaying" class="playing-symbols">
|
||||
<div id="bar-1" class="playing-bar"></div>
|
||||
<div id="bar-2" class="playing-bar"></div>
|
||||
@@ -67,15 +92,15 @@
|
||||
<i class="fa-solid fa-play" @click="() => player.playIndex( index )"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="song-details">
|
||||
<h3>{{ song.name }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
<p>{{ beautifyTime( song.duration ) }}</p>
|
||||
<p>{{ song['additional-info'] }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<i v-if="index !== queueIndex" class="fa-solid fa-trash-can"></i>
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
<div class="song-actions">
|
||||
<p>{{ beautifyTime( song.duration ) }}</p>
|
||||
<i v-if="index !== 0" class="fa-solid fa-trash-can" @click="() => deleteSong( index )"></i>
|
||||
<i class="fa-solid fa-pen-to-square" @click="() => editSong( index )"></i>
|
||||
</div>
|
||||
</div>
|
||||
</SortableList>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import PopupElement from './PopupElement.vue';
|
||||
|
||||
const model = defineModel<boolean>( {
|
||||
'required': true
|
||||
} );
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PopupElement v-model="model" show-close>
|
||||
<h2>Edit Song</h2>
|
||||
<!-- TODO: Need a way to use the search feature to replace the song details, as of course as well text editors -->
|
||||
</PopupElement>
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,6 +29,8 @@
|
||||
let moveSpeed = 0;
|
||||
|
||||
const start = ( ev: MouseEvent, idx: number ) => {
|
||||
if ( array.value.length < 2 ) return;
|
||||
|
||||
movableSize.value = document.getElementById( 'movable-' + idx )!.scrollHeight;
|
||||
offset.value = ev.y - ( movableSize.value / 2 );
|
||||
movingIdx.value = idx;
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<SearchView />
|
||||
<PopupElement v-model="isShowingImportTypePicker">
|
||||
<h3>What would you like to add?</h3>
|
||||
<PopupElement v-model="isShowingImportTypePicker" show-close>
|
||||
<h2>What would you like to add?</h2>
|
||||
<div class="import-type-list">
|
||||
<div v-for="(source, index) in importers" :key="index" @click="openPicker( source )">
|
||||
{{ source.name }}
|
||||
@@ -38,3 +38,31 @@
|
||||
</PopupElement>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.import-type-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 50vw;
|
||||
height: 35vh;
|
||||
justify-content: center;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
>div {
|
||||
width: 32%;
|
||||
margin: 0.5%;
|
||||
height: 50%;
|
||||
background-color: var(--accent-background);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
>.fa-solid {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
const results: Ref<Song[]> = ref( [] );
|
||||
const query = ref( '' );
|
||||
const isSearching = ref( false );
|
||||
const addedIndex = ref( -1 );
|
||||
|
||||
let addedTimeout = -1;
|
||||
let timeout = -1;
|
||||
let searchedForQuery = '';
|
||||
|
||||
@@ -46,7 +48,15 @@
|
||||
};
|
||||
|
||||
const add = ( idx: number ) => {
|
||||
try {
|
||||
clearTimeout( addedTimeout );
|
||||
} catch { /* empty */ }
|
||||
|
||||
addedIndex.value = idx;
|
||||
searchOpts.value?.addSelected( idx );
|
||||
addedTimeout = setTimeout( () => {
|
||||
addedIndex.value = -1;
|
||||
}, 2000 );
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -63,11 +73,14 @@
|
||||
<div v-if="!isSearching" class="search-results-wrapper">
|
||||
<div v-for="(result, index) in results" :key="index" @click="() => add(index)">
|
||||
<img :src="result.artwork" :alt="'Album artwork of ' + result.name + ' by ' + result.artist">
|
||||
<h4>{{ result.name }}</h4>
|
||||
<p>{{ result.artist }}</p>
|
||||
<div>
|
||||
<h4>{{ result.name }}</h4>
|
||||
<p>{{ result.artist }}</p>
|
||||
</div>
|
||||
<i v-if="addedIndex === index" class="fa-solid fa-check"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="search-results-wrapper">
|
||||
<div v-else class="search-results-wrapper placeholder">
|
||||
Searching...
|
||||
</div>
|
||||
</PopupElement>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
height: 10rem;
|
||||
width: 10rem;
|
||||
position: relative;
|
||||
margin-right: 20px;
|
||||
|
||||
.song-cover,
|
||||
.fa-solid {
|
||||
@@ -25,6 +26,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
&.hover {
|
||||
display: none;
|
||||
@@ -37,6 +39,39 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>.song-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
>* {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
>h3 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
>.song-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-right: 10px;
|
||||
|
||||
>* {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
>.fa-solid {
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playing-symbols {
|
||||
|
||||
@@ -3,10 +3,42 @@
|
||||
height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
&.placeholder {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
>div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
width: 75%;
|
||||
cursor: pointer;
|
||||
|
||||
>div {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
>* {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
>img {
|
||||
width: 10%;
|
||||
height: 7rem;
|
||||
width: 7rem;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
>.fa-solid {
|
||||
margin-left: auto;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&.moving {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
addSongList,
|
||||
clearQueue,
|
||||
removeSong,
|
||||
shuffleList
|
||||
} from './playlists/add';
|
||||
import {
|
||||
@@ -27,12 +28,12 @@ import {
|
||||
} from './playlists';
|
||||
|
||||
const next = () => {
|
||||
playIndex( queueIdx.value + 1 );
|
||||
playIndex( ( queueIdx.value + 1 ) % queue.value.length );
|
||||
};
|
||||
|
||||
const prev = () => {
|
||||
// TODO: Back to beginning if more than some seconds have passed?
|
||||
playIndex( queueIdx.value - 1 );
|
||||
playIndex( ( queueIdx.value - 1 + queue.value.length ) % queue.value.length );
|
||||
};
|
||||
|
||||
const play = () => {
|
||||
@@ -117,6 +118,10 @@ const getSources = (): string[] => {
|
||||
return Object.keys( sources );
|
||||
};
|
||||
|
||||
/**
|
||||
* Add songs to the playlist from given source
|
||||
* @param source - The ID of the source to add from
|
||||
*/
|
||||
const addSongFromSource = async ( source: string ) => {
|
||||
sources[source]!.addSongsFromThisSource( addSongList );
|
||||
};
|
||||
@@ -135,6 +140,7 @@ export default {
|
||||
next,
|
||||
prev,
|
||||
clearQueue,
|
||||
removeSong,
|
||||
queue,
|
||||
queueIdx,
|
||||
duration,
|
||||
|
||||
@@ -35,7 +35,6 @@ export const removeSong = ( idx: number ) => {
|
||||
|
||||
const removed = queue.value.splice( idx, 1 )[0];
|
||||
|
||||
// TODO: Check that this works
|
||||
for ( let i = 0; i < rawQueue.value.length; i++ ) {
|
||||
if ( rawQueue.value[ i ] === removed ) {
|
||||
rawQueue.value.splice( i, 1 );
|
||||
|
||||
+5
@@ -90,6 +90,11 @@ export interface PlayerSourcePlugin {
|
||||
*/
|
||||
'getDuration': () => number;
|
||||
|
||||
/**
|
||||
* Check if source is available for playback
|
||||
*/
|
||||
'available': () => boolean;
|
||||
|
||||
/**
|
||||
* Called when user adds another song to the playlist via this source.
|
||||
* You may use the provided interface elements (search bar and popups) to e.g. ask if user wants to use a playlist, album, etc
|
||||
|
||||
@@ -12,6 +12,7 @@ export const useLocalPlayer: PlayerSourcePluginInitializer = async (): Promise<P
|
||||
};
|
||||
|
||||
return {
|
||||
'available': () => true,
|
||||
'id': 'local',
|
||||
'name': 'Local Disk',
|
||||
'play': player.play,
|
||||
|
||||
@@ -43,6 +43,7 @@ export const useMusicKit: PlayerSourcePluginInitializer = ( storefront: string =
|
||||
};
|
||||
|
||||
return {
|
||||
'available': controls.getLoggedIn,
|
||||
'play': controls.play,
|
||||
'pause': controls.pause,
|
||||
'stop': controls.stop,
|
||||
|
||||
@@ -34,6 +34,10 @@ export const musicKitPlayback = ( musickitInstance: MusicKitInstance ) => {
|
||||
return musickitInstance.currentPlaybackDuration;
|
||||
};
|
||||
|
||||
const getLoggedIn = (): boolean => {
|
||||
return musickitInstance.isAuthorized;
|
||||
};
|
||||
|
||||
return {
|
||||
play,
|
||||
pause,
|
||||
@@ -41,6 +45,7 @@ export const musicKitPlayback = ( musickitInstance: MusicKitInstance ) => {
|
||||
playSong,
|
||||
stop,
|
||||
getPlaybackPos,
|
||||
getDuration
|
||||
getDuration,
|
||||
getLoggedIn
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,12 +7,16 @@ import type {
|
||||
import {
|
||||
openImportTypePicker
|
||||
} from '@/composables/importTypePicker';
|
||||
import {
|
||||
searchPlaylists
|
||||
} from './playlists';
|
||||
import {
|
||||
searchSongs
|
||||
} from './songs';
|
||||
|
||||
export const addFromAppleMusic = async ( instance: MusicKitInstance, cb: ( songs: Song[] ) => void ): Promise<void> => {
|
||||
const songs = await searchSongs( instance, cb );
|
||||
const playlists = await searchPlaylists( instance, cb );
|
||||
|
||||
openImportTypePicker( [
|
||||
{
|
||||
@@ -24,8 +28,8 @@ export const addFromAppleMusic = async ( instance: MusicKitInstance, cb: ( songs
|
||||
{
|
||||
'name': 'Playlists',
|
||||
'type': 'cloud',
|
||||
'addSelected': songs.addSelected,
|
||||
'search': songs.search
|
||||
'addSelected': playlists.addSelected,
|
||||
'search': playlists.search
|
||||
}
|
||||
] );
|
||||
};
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
import type {
|
||||
MusicKitInstance
|
||||
} from 'musickitjs-v3-types/MusicKitInstance';
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
|
||||
export const searchPlaylists = async ( instance: MusicKitInstance ) => {
|
||||
export const searchPlaylists = async ( instance: MusicKitInstance, cb: ( songs: Song[] ) => void ) => {
|
||||
// TODO: Get all playlists
|
||||
|
||||
const search = async () => {};
|
||||
|
||||
const addSelected = async ( idx: number ): Promise<Song[]> => {
|
||||
// TODO: Load the playlist content and return songs
|
||||
const search = async ( term: string ): Promise<Song[]> => {
|
||||
return [];
|
||||
};
|
||||
|
||||
const addSelected = async ( idx: number ) => {
|
||||
// TODO: Implement
|
||||
cb( [] );
|
||||
};
|
||||
|
||||
return {
|
||||
search,
|
||||
addSelected
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import PlayerControls from '@/components/PlayerControls.vue';
|
||||
import QueueViewer from '@/components/QueueViewer.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Player</h1>
|
||||
<QueueViewer />
|
||||
<div class="main-app">
|
||||
<div class="player-controls">
|
||||
<PlayerControls />
|
||||
</div>
|
||||
<div class="song-queue">
|
||||
<QueueViewer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-app {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.player-controls {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.song-queue {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user