feat: redesign main page, get player working (mostly) with Apple Music source

This commit is contained in:
2026-09-01 18:16:11 +02:00
parent faa271edd6
commit 01526034ce
29 changed files with 425 additions and 219 deletions
+39 -9
View File
@@ -10,9 +10,10 @@
'required': true
} );
const addSong = ( source: string ) => {
player.addSongFromSource( source );
showPopup.value = false;
const addSong = async ( source: string ) => {
if ( await player.addSongFromSource( source ) ) {
showPopup.value = false;
}
};
</script>
@@ -20,11 +21,19 @@
<div>
<ImportTypePicker />
<PopupElement v-model="showPopup" show-close>
<h1>Add Song</h1>
<div class="title">
<h1>Add Song(s)</h1>
<p>Pick the source of your songs</p>
</div>
<div class="song-sources">
<div v-for="(source, index) in sources" :key="index" @click="() => addSong( source.id )">
<i v-if="source.icon" :class="['fa-solid', 'fa-' + source.icon]"></i>
<p>{{ source.name }}</p>
<div>
<i v-if="source.icon" :class="['fa-solid', 'fa-' + source.icon]"></i>
<p>{{ source.name }}</p>
</div>
<p v-if="!source.authorized.value" class="not-auth-notice">
You have not yet logged into this source. Clicking this will start login
</p>
</div>
</div>
</PopupElement>
@@ -32,11 +41,20 @@
</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: 35vh;
height: 40vh;
justify-content: center;
overflow-y: scroll;
overflow-x: hidden;
@@ -51,9 +69,21 @@
justify-content: center;
align-items: center;
cursor: pointer;
flex-direction: column;
>.fa-solid {
font-size: 1.5rem;
>div {
display: flex;
justify-content: center;
align-items: center;
>.fa-solid {
font-size: 1.5rem;
}
}
>.not-auth-notice {
font-size: 0.6rem;
margin: 0;
}
}
}
+51
View File
@@ -0,0 +1,51 @@
<script setup lang="ts">
import type {
Song
} from '@/ts/dtype/playlist';
const song = defineModel<Song>( {
'required': false
} );
</script>
<template>
<div class="current-song">
<div class="artwork">
<img
v-if="song?.artwork"
:src="song.artwork"
alt="Song cover"
class="song-cover"
>
<i v-else class="fa-solid fa-music song-cover"></i>
</div>
<div class="song-details">
<h1>
{{ song?.name ?? 'Not playing' }}
</h1>
<p>{{ song?.artist ?? 'No artist' }}</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.current-song {
width: 100%;
.artwork {
width: 100%;
>img, .fa-solid {
width: 60%;
max-height: 50%;
font-size: 15vw;
}
}
.song-details {
>* {
margin: 10px;
}
}
}
</style>
+14 -5
View File
@@ -3,10 +3,12 @@
import {
beautifyTime
} from '@/ts/util/time';
import {
computed
} from 'vue';
import player from '@/ts/player';
const playbackPercentage = player.playbackPercentage;
const duration = player.duration;
const repeatMode = player.repeat;
const shuffleMode = player.shuffle;
@@ -31,6 +33,14 @@
alert( 'Share menu not yet implemented' );
};
const current = computed( () => {
return beautifyTime( player.playbackPercentage.value * player.duration.value );
} );
const duration = computed( () => {
return beautifyTime( player.duration.value );
} );
// TODO: Button availability
</script>
@@ -39,7 +49,7 @@
<div class="controls">
<i class="fa-solid fa-backward-step" @click="player.prev"></i>
<i class="fa-solid fa-arrow-rotate-left quick-seek" @click="player.back10"></i>
<div :class="['play-pause', 'paused']">
<div :class="['play-pause', player.isPlaying.value ? undefined : 'paused']">
<i class="fa-solid fa-play" @click="player.play"></i>
<i class="fa-solid fa-pause" @click="player.pause"></i>
</div>
@@ -48,12 +58,11 @@
</div>
<div class="time">
<!-- TODO: Probably needs to be a computed -->
<p class="current">
{{ beautifyTime( playbackPercentage * duration ) }}
{{ current }}
</p>
<p class="duration">
{{ beautifyTime( duration ) }}
{{ duration }}
</p>
</div>
<ProgressBar v-model="playbackPercentage" @move-end="seek" />
+4 -3
View File
@@ -33,6 +33,7 @@
align-items: center;
z-index: 1000;
transition-delay: 0;
overflow: hidden;
.popup-backdrop {
width: 100vw;
@@ -42,7 +43,7 @@
top: 0;
left: 0;
overflow: hidden;
transition: all 0.5s;
transition: all 0.25s;
}
.popup-main {
@@ -53,7 +54,7 @@
border-radius: 20px;
transform: scale(1);
position: relative;
overflow-y: scroll;
overflow: hidden;
display: block;
transition: transform 0.25s ease, opacity 0.25s ease;
@@ -69,7 +70,7 @@
&.hidden {
top: -100vh;
transition-delay: 0.5s;
transition-delay: 0.25s;
.popup-main {
transform: scale(1.25);
+9 -4
View File
@@ -9,22 +9,27 @@
'default': 0.5
} );
const offset = ref( -1 );
const isMoving = ref( false );
const bar = useTemplateRef( 'bar' );
const start = ( ev: MouseEvent ) => {
offset.value = ev.x - bar.value!.offsetLeft;
val.value = offset.value / bar.value!.clientWidth;
offset.value = bar.value!.getBoundingClientRect().x;
isMoving.value = true;
val.value = ( ev.x - offset.value ) / bar.value!.clientWidth;
emit( 'move-start' );
};
const move = ( ev: MouseEvent ) => {
if ( offset.value > -1 ) {
val.value = Math.max( 0, Math.min( ev.x / bar.value!.clientWidth, 1 ) );
if ( isMoving.value ) {
val.value = Math.max( 0, Math.min( ( ev.x - offset.value ) / bar.value!.clientWidth, 1 ) );
}
};
const end = () => {
if ( !isMoving.value ) return;
offset.value = -1;
isMoving.value = false;
emit( 'move-end' );
};
+22 -20
View File
@@ -15,16 +15,18 @@
beautifyTime
} from '@/ts/util/time';
import player from '@/ts/player';
import {
queueIdx
} from '@/ts/player/state';
const queue: WritableComputedRef<Song[]> = computed( {
get () {
return player.queue.value.slice( player.queueIdx.value );
return player.queue.value.slice( player.queueIdx.value + 1 );
},
set ( val ) {
player.queue.value = player.queue.value.slice( 0, player.queueIdx.value ).concat( val );
player.queue.value = player.queue.value.slice( 0, player.queueIdx.value + 1 ).concat( val );
}
} );
const isPlaying = player.isPlaying;
const showAddSong = ref( false );
const showEditSong = ref( false );
const editingSong: Ref<null | Song> = ref( null );
@@ -35,16 +37,16 @@
const editSong = ( idx: number ) => {
showEditSong.value = true;
editingSong.value = player.queue.value[ idx ]!;
editingSong.value = player.queue.value[ idx + queueIdx.value + 1 ]!;
};
const deleteSong = ( idx: number ) => {
player.removeSong( idx );
player.removeSong( idx + queueIdx.value + 1 );
};
</script>
<template>
<div>
<div class="queue-viewer">
<div>
<button @click="addSong">
<i class="fa-solid fa-plus"></i>
@@ -54,6 +56,7 @@
<i class="fa-solid fa-xmark"></i>
Clear
</button>
<!-- TODO: On save, copy the current queue into rawQueue! -->
<button>
<i class="fa-solid fa-save"></i>
Save
@@ -66,7 +69,7 @@
</div>
<SongEditor v-model="showEditSong" editing-song="" />
<AddSong v-model="showAddSong" />
<div>
<div v-if="queue.length > 0" class="queue-container">
<SortableList v-slot="{ item: song, index }" v-model="queue">
<div class="song-list-element">
<div class="song-cover-wrapper">
@@ -77,19 +80,8 @@
class="song-cover"
>
<i v-else class="fa-solid fa-music song-cover"></i>
<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>
<div id="bar-3" class="playing-bar"></div>
</div>
<i
v-else
class="fa-solid fa-pause"
></i>
</div>
<div v-else class="play-overlay hover">
<i class="fa-solid fa-play" @click="() => player.playIndex( index )"></i>
<div class="play-overlay hover">
<i class="fa-solid fa-play" @click="() => player.playIndex( index + queueIdx + 1 )"></i>
</div>
</div>
<div class="song-details">
@@ -105,6 +97,16 @@
</div>
</SortableList>
</div>
<div v-else class="queue-container">
<p>
No more songs in queue
</p>
<button @click="addSong">
<i class="fa-solid fa-plus"></i>
Add
</button>
</div>
</div>
</template>