mirror of
https://github.com/janishutz/MusicPlayer.git
synced 2026-09-10 14:25:24 +02:00
feat: basic design of share page, various small fixes
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
<div
|
||||
class="panel"
|
||||
>
|
||||
<div style="margin-bottom: 20px; width: 100%;">
|
||||
<div class="current-song-wrapper">
|
||||
<CurrentSong
|
||||
v-model="queue[queueIdx]"
|
||||
/>
|
||||
@@ -44,6 +44,15 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
>.current-song-wrapper {
|
||||
margin-bottom: 1.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(100% - 15rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
background-color: var(--secondary-color);
|
||||
transition: bottom 1s ease;
|
||||
transition-delay: 0.25s;
|
||||
overflow: hidden;
|
||||
|
||||
&.hidden {
|
||||
transition-delay: 0s;
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type ComputedRef,
|
||||
computed
|
||||
} from 'vue';
|
||||
import {
|
||||
isPlaying,
|
||||
queue,
|
||||
queueIdx
|
||||
} from '@/ts/player/state';
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
import {
|
||||
beautifyTime
|
||||
} from '@/ts/util/time';
|
||||
import {
|
||||
playbackPercentage
|
||||
} from '@/ts/player/status-tracking';
|
||||
import player from '@/ts/player';
|
||||
|
||||
const closed = defineModel<boolean>( {
|
||||
'required': true
|
||||
} );
|
||||
@@ -6,30 +26,117 @@
|
||||
const open = () => {
|
||||
closed.value = true;
|
||||
};
|
||||
|
||||
const song: ComputedRef<Song> = computed( () => {
|
||||
if ( queueIdx.value >= 0 ) {
|
||||
return queue.value[ queueIdx.value ]!;
|
||||
} else {
|
||||
return {
|
||||
'artwork': '',
|
||||
'additional-info': '',
|
||||
'artist': 'No artist',
|
||||
'duration': -1,
|
||||
'identifier': 'nosong-ident',
|
||||
'name': 'Not Playing',
|
||||
'source': 'local'
|
||||
};
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['small-player', closed ? 'hidden' : undefined]" @click="open">
|
||||
Player
|
||||
<div :class="['small-player', closed ? 'hidden' : undefined]">
|
||||
<div>
|
||||
<img
|
||||
v-if="song.artwork"
|
||||
:src="song.artwork"
|
||||
alt="Song cover"
|
||||
class="song-cover"
|
||||
>
|
||||
<i v-else class="fa-solid fa-music song-cover" @click="open"></i>
|
||||
<div @click="open">
|
||||
<h3>{{ song.name }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
</div>
|
||||
<p @click="open">
|
||||
{{ beautifyTime( playbackPercentage * song.duration ) }} / {{ beautifyTime( song.duration ) }}
|
||||
</p>
|
||||
<i v-if="!isPlaying" class="fa-solid fa-play" @click="player.play"></i>
|
||||
<i v-else class="fa-solid fa-pause" @click="player.pause"></i>
|
||||
<i class="fa-solid fa-forward-step" @click="player.next"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.small-player {
|
||||
background-color: green;
|
||||
background-color: var(--secondary-color);
|
||||
width: 80vw;
|
||||
height: 7vh;
|
||||
height: 4.5rem;
|
||||
position: fixed;
|
||||
left: 10vw;
|
||||
bottom: 15px;
|
||||
border-radius: 4vh;
|
||||
border-radius: 2rem;
|
||||
overflow: hidden;
|
||||
transition: bottom 0.5s ease;
|
||||
transition-delay: 0.75s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.hidden {
|
||||
bottom: -8vh;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
>div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: calc(100% - 6rem);
|
||||
height: 100%;
|
||||
|
||||
>.song-cover {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
font-size: 2.5rem;
|
||||
cursor: pointer;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
>div {
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: calc(100% - 20rem);
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
|
||||
>h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
>p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
>p {
|
||||
cursor: pointer;
|
||||
margin-right: 15px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
>.fa-solid {
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
const song = defineModel<Song>( {
|
||||
'required': false
|
||||
} );
|
||||
const props = defineProps<{
|
||||
'showAdditionalInfo'?: boolean
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,6 +27,9 @@
|
||||
{{ song?.name ?? 'Not playing' }}
|
||||
</h1>
|
||||
<p>{{ song?.artist ?? 'No artist' }}</p>
|
||||
<p v-if="props.showAdditionalInfo">
|
||||
{{ song?.['additional-info'] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -31,14 +37,22 @@
|
||||
<style lang="scss" scoped>
|
||||
.current-song {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.artwork {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(100% - 9rem);
|
||||
|
||||
>img, .fa-solid {
|
||||
width: 60%;
|
||||
max-height: 50%;
|
||||
font-size: 15vw;
|
||||
max-width: 60%;
|
||||
height: 100%;
|
||||
font-size: 40vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
Clear
|
||||
</button>
|
||||
<button @click="editSong( -1 )">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
Edit Current
|
||||
</button>
|
||||
<button @click="savePlaylist">
|
||||
<i class="fa-solid fa-save"></i>
|
||||
Save
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
|
||||
const save = () => {
|
||||
model.value = false;
|
||||
document.dispatchEvent( new CustomEvent( 'musicplayer:update' ) );
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
total += currentQueue.value[ currentQueueIdx.value ]?.duration ?? 0;
|
||||
total -= playbackTime.value;
|
||||
|
||||
return Math.round( total / 60 );
|
||||
return Math.ceil( total / 60 );
|
||||
};
|
||||
} );
|
||||
</script>
|
||||
@@ -29,29 +29,42 @@
|
||||
<template>
|
||||
<div class="queue-viewer">
|
||||
<div class="queue-container">
|
||||
<div v-for="(song, index) in songs" :key="index" class="song-list-element">
|
||||
<div class="song-cover-wrapper">
|
||||
<img
|
||||
v-if="song.artwork && showArtworks"
|
||||
: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">
|
||||
<h3>{{ song.name }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
<p>{{ song['additional-info'] }}</p>
|
||||
</div>
|
||||
<div class="song-actions">
|
||||
<p>In {{ timeToPlay( index ) }}min</p>
|
||||
<div v-if="songs.length > 0" class="queue-scroll">
|
||||
<div v-for="(song, index) in songs" :key="index" class="song-list-element">
|
||||
<div class="song-cover-wrapper">
|
||||
<img
|
||||
v-if="song.artwork && showArtworks"
|
||||
: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">
|
||||
<h3>{{ song.name }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
<p>{{ song['additional-info'] }}</p>
|
||||
</div>
|
||||
<div class="song-actions">
|
||||
<p>In {{ '<' + timeToPlay( index ) }}min</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="queue-empty">
|
||||
No upcoming songs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/scss/components/queue.scss';
|
||||
|
||||
.queue-scroll {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
justify-content: flex-start !important;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
.shared-view {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
.panel {
|
||||
width: 45%;
|
||||
margin-left: 2.5%;
|
||||
margin-right: 2.5%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
>.current-song-wrapper {
|
||||
margin-bottom: 1.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(100% - 10rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
>.time {
|
||||
display: flex;
|
||||
width: 85%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
>p {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
>.duration {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -8,5 +8,8 @@ declare global {
|
||||
'musicplayer:playpause': CustomEvent<void>;
|
||||
'musicplayer:playindex': CustomEvent<void>;
|
||||
'musicplayer:seek': CustomEvent<void>;
|
||||
'musicplayer:update': CustomEvent<void>;
|
||||
'musicplayer:reauth': CustomEvent<void>;
|
||||
'musicplayer:autherror': CustomEvent<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
import {
|
||||
playbackPercentage
|
||||
} from '../player/status-tracking';
|
||||
import {
|
||||
reauth
|
||||
} from '../util/reauth';
|
||||
import request from '../request';
|
||||
|
||||
const RETRY_CAP = 10;
|
||||
@@ -43,6 +46,8 @@ const createRoom = async ( name: string, antiTamper: boolean ): Promise<boolean>
|
||||
useAntiTamper.value = antiTamper;
|
||||
room.value = name;
|
||||
|
||||
document.addEventListener( 'musicplayer:autherror', reauth );
|
||||
|
||||
return await connect();
|
||||
};
|
||||
|
||||
@@ -50,6 +55,11 @@ const closeRoom = async () => {
|
||||
// TODO: Trigger close on backend and disconnect.
|
||||
isConnected.value = false;
|
||||
localStorage.removeItem( 'room' );
|
||||
connection?.close();
|
||||
|
||||
try {
|
||||
document.removeEventListener( 'musicplayer:autherror', reauth );
|
||||
} catch { /* empty */ }
|
||||
};
|
||||
|
||||
const connect = (): Promise<boolean> => {
|
||||
@@ -102,9 +112,14 @@ let stateLock = false;
|
||||
const sendPlaylistData = () => {
|
||||
if ( isConnected.value && !playlistLock ) {
|
||||
playlistLock = true;
|
||||
request.post( `/room/${ room.value }/update/playlist`, JSON.stringify( {
|
||||
'playlist': queue.value
|
||||
} ) );
|
||||
|
||||
try {
|
||||
request.post( `/room/${ room.value }/update/playlist`, JSON.stringify( {
|
||||
'playlist': queue.value
|
||||
} ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
}
|
||||
|
||||
setTimeout( () => {
|
||||
playlistLock = false;
|
||||
@@ -112,15 +127,20 @@ const sendPlaylistData = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendStateData = () => {
|
||||
const sendStateData = async () => {
|
||||
if ( isConnected.value && !stateLock ) {
|
||||
stateLock = true;
|
||||
request.post( `/room/${ room.value }/update/state`, JSON.stringify( {
|
||||
'playing': isPlaying.value,
|
||||
'index': queueIdx.value,
|
||||
'start': new Date().getTime() - 100,
|
||||
'offset': playbackPercentage.value * ( queue.value[ queueIdx.value ]?.duration ?? 0 )
|
||||
} ) );
|
||||
|
||||
try {
|
||||
request.post( `/room/${ room.value }/update/state`, JSON.stringify( {
|
||||
'playing': isPlaying.value,
|
||||
'index': queueIdx.value,
|
||||
'start': new Date().getTime() - 100,
|
||||
'offset': playbackPercentage.value * ( queue.value[ queueIdx.value ]?.duration ?? 0 )
|
||||
} ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
}
|
||||
|
||||
setTimeout( () => {
|
||||
stateLock = false;
|
||||
@@ -135,6 +155,7 @@ const useRoomWatchers = () => {
|
||||
document.addEventListener( 'musicplayer:playindex', sendStateData );
|
||||
document.addEventListener( 'musicplayer:seek', sendStateData );
|
||||
document.addEventListener( 'musicplayer:playpause', sendStateData );
|
||||
document.addEventListener( 'musicplayer:update', sendPlaylistData );
|
||||
} );
|
||||
|
||||
onUnmounted( () => {
|
||||
@@ -149,6 +170,10 @@ const useRoomWatchers = () => {
|
||||
try {
|
||||
document.addEventListener( 'musicplayer:playpause', sendStateData );
|
||||
} catch { /* empty */ }
|
||||
|
||||
try {
|
||||
document.addEventListener( 'musicplayer:update', sendPlaylistData );
|
||||
} catch { /* empty */ }
|
||||
} );
|
||||
|
||||
if ( room.value ) connect();
|
||||
|
||||
@@ -28,7 +28,8 @@ const wrapper = async ( url: string, opts: RequestInit ): Promise<Response> => {
|
||||
if ( res.ok ) {
|
||||
return res;
|
||||
} else if ( res.status === 403 || res.status === 401 ) {
|
||||
// TODO: Handle these errors better (probably do something like in the old version, but better)
|
||||
document.dispatchEvent( new CustomEvent( 'musicplayer:autherror' ) );
|
||||
|
||||
throw new AuthError( 'ERR_USER_UNAUTHORIZED' );
|
||||
} else if ( res.status === 402 ) {
|
||||
throw new UnownedError( 'ERR_USER_UNOWNED' );
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
currentQueueIdx,
|
||||
isPlaying,
|
||||
playbackOffset,
|
||||
playbackProgress,
|
||||
playbackTime,
|
||||
startTime
|
||||
} from './state';
|
||||
@@ -44,7 +45,7 @@ const connect = (): Promise<void> => {
|
||||
startTime.value = data.state.start;
|
||||
playbackTime.value = data.state.offset;
|
||||
playbackOffset.value = data.state.offset;
|
||||
|
||||
playbackProgress.value = data.state.offset / ( data.playlist[ data.state.index ]?.duration ?? -1 );
|
||||
resolve();
|
||||
};
|
||||
|
||||
|
||||
@@ -19,3 +19,5 @@ export const showArtworks = ref( false );
|
||||
export const playbackTime = ref( 0 );
|
||||
|
||||
export const playbackOffset = ref( 0 );
|
||||
|
||||
export const playbackProgress = ref( 0 );
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const reauth = () => {
|
||||
localStorage.setItem( 'close-tab', 'true' );
|
||||
|
||||
const listener = () => {
|
||||
if ( localStorage.getItem( 'reauth-ok' ) === 'true' ) {
|
||||
try {
|
||||
window.removeEventListener( 'storage', listener );
|
||||
} catch { /* empty */ }
|
||||
|
||||
localStorage.removeItem( 'reauth-ok' );
|
||||
|
||||
document.dispatchEvent( new CustomEvent( 'musicplayer:reauth' ) );
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener( 'storage', listener );
|
||||
};
|
||||
@@ -2,14 +2,14 @@
|
||||
import {
|
||||
type ComputedRef,
|
||||
computed,
|
||||
onMounted,
|
||||
ref
|
||||
onMounted
|
||||
} from 'vue';
|
||||
import {
|
||||
currentQueue,
|
||||
currentQueueIdx,
|
||||
isPlaying,
|
||||
playbackOffset,
|
||||
playbackProgress,
|
||||
playbackTime,
|
||||
startTime
|
||||
} from '@/ts/shared/state';
|
||||
@@ -25,7 +25,6 @@
|
||||
import shared from '@/ts/shared';
|
||||
|
||||
shared.connect();
|
||||
const playbackProgress = ref( 0 );
|
||||
|
||||
onMounted( () => {
|
||||
setInterval( () => {
|
||||
@@ -62,22 +61,26 @@
|
||||
|
||||
<template>
|
||||
<div class="shared-view">
|
||||
<div>
|
||||
<div>
|
||||
<CurrentSong v-model="song" />
|
||||
<div class="time">
|
||||
<p class="current">
|
||||
{{ beautifyTime( playbackTime ) }}
|
||||
</p>
|
||||
<p class="duration">
|
||||
{{ beautifyTime( song.duration ) }}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar v-model="playbackProgress" :disallow-move="true" />
|
||||
<div class="panel">
|
||||
<div class="current-song-wrapper">
|
||||
<CurrentSong v-model="song" :show-additional-info="true" />
|
||||
</div>
|
||||
<div>
|
||||
<SharedQueue />
|
||||
<ProgressBar v-model="playbackProgress" :disallow-move="true" />
|
||||
<div class="time">
|
||||
<p class="current">
|
||||
{{ beautifyTime( playbackTime ) }}
|
||||
</p>
|
||||
<p class="duration">
|
||||
{{ beautifyTime( song.duration ) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<SharedQueue />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/scss/shared/main.scss';
|
||||
</style>
|
||||
|
||||
@@ -18,13 +18,24 @@
|
||||
store.isAuth = await sdk.verify();
|
||||
|
||||
if ( store.isAuth )
|
||||
router.push( '/app' );
|
||||
isAuthorizedHandler();
|
||||
|
||||
isLoggingIn.value = false;
|
||||
|
||||
// TODO: Logout button
|
||||
} );
|
||||
|
||||
const isAuthorizedHandler = () => {
|
||||
if ( localStorage.getItem( 'close-tab' ) === 'true' ) {
|
||||
localStorage.setItem( 'login-ok', 'true' );
|
||||
localStorage.removeItem( 'close-tab' );
|
||||
|
||||
return window.close();
|
||||
}
|
||||
|
||||
router.push( '/app' );
|
||||
};
|
||||
|
||||
const login = () => {
|
||||
if ( isLoggingIn.value ) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user