mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 13:04:23 +00:00
mostly complete base spec player
This commit is contained in:
@@ -1,24 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Library</h1>
|
||||
<playlistsView :playlists="$props.playlists" @selected-playlist="( id ) => selectPlaylist( id )"></playlistsView>
|
||||
<playlistsView :playlists="$props.playlists" @selected-playlist="( id ) => selectPlaylist( id )" :is-logged-in="$props.isLoggedIn"
|
||||
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"></playlistsView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import playlistsView from '@/components/playlistsView.vue';
|
||||
import type { ReadFile } from '@/scripts/song';
|
||||
|
||||
const emits = defineEmits( [ 'selected-playlist' ] );
|
||||
const emits = defineEmits( [ 'selected-playlist', 'custom-playlist' ] );
|
||||
|
||||
const selectPlaylist = ( id: string ) => {
|
||||
emits( 'selected-playlist', id );
|
||||
}
|
||||
|
||||
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
||||
emits( 'custom-playlist', playlist );
|
||||
}
|
||||
|
||||
defineProps( {
|
||||
'playlists': {
|
||||
'default': [],
|
||||
'type': Array<any>,
|
||||
'required': true,
|
||||
}
|
||||
},
|
||||
'isLoggedIn': {
|
||||
'default': false,
|
||||
'type': Boolean,
|
||||
'required': true,
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
383
MusicPlayerV2-GUI/src/components/notificationsModule.vue
Normal file
383
MusicPlayerV2-GUI/src/components/notificationsModule.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<!-- eslint-disable no-undef -->
|
||||
<template>
|
||||
<div id="notifications">
|
||||
<div class="message-box" :class="[ location, size ]">
|
||||
<div class="message-container" :class="messageType">
|
||||
<button @click="handleNotifications();" class="close-notification"><span class="material-symbols-outlined close-notification-icon">close</span></button>
|
||||
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
|
||||
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
|
||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
|
||||
<p class="message" @click="notificationAction()">{{ notifications[ currentDID ] ? notifications[ currentDID ].message : '' }}</p>
|
||||
<div :class="'countdown countdown-' + messageType" :style="'width: ' + ( 100 - ( currentTime - notificationDisplayStartTime ) / ( notifications[ currentDID ] ? notifications[ currentDID ].showDuration : 1 ) / 10 ) + '%'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import router from '@/router';
|
||||
import { onUnmounted, ref, type Ref } from 'vue';
|
||||
|
||||
defineProps( {
|
||||
location: {
|
||||
type: String,
|
||||
'default': 'topleft',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
'default': 'default',
|
||||
}
|
||||
// Size options: small, default (default option), big, bigger, huge
|
||||
} );
|
||||
|
||||
interface Notification {
|
||||
message: string;
|
||||
showDuration: number;
|
||||
messageType: string;
|
||||
priority: string;
|
||||
id: number;
|
||||
redirect?: string;
|
||||
}
|
||||
|
||||
interface NotificationList {
|
||||
[ key: string ]: Notification
|
||||
}
|
||||
|
||||
const notifications: Ref<NotificationList> = ref( {} );
|
||||
const queue: Ref<number[]> = ref( [] );
|
||||
const currentDID: Ref<number> = ref( 0 );
|
||||
const messageType: Ref<string> = ref( 'hide' );
|
||||
const currentID = ref( { 'critical': 0, 'medium': 1000, 'low': 10000 } );
|
||||
const notificationDisplayStartTime: Ref<number> = ref( 0 );
|
||||
const currentTime: Ref<number> = ref( 0 );
|
||||
let progressBar = 0;
|
||||
let notificationTimeout = 0;
|
||||
const notificationAction = () => {
|
||||
if ( notifications.value[ currentDID.value ] ) {
|
||||
if ( notifications.value[ currentDID.value ].redirect ) {
|
||||
router.push( notifications.value[ currentDID.value ].redirect ?? '' );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a notification that will be displayed using the internal notification scheduler
|
||||
* @param {string} message The message to show. Can only be plain text (no HTML)
|
||||
* @param {number} showDuration The duration in seconds for which to show the notification
|
||||
* @param {string} messageType Type of notification to show. Will dictate how it looks: 'ok', 'error', 'info', 'warn', 'progress'
|
||||
* @param {string} priority The priority of the message: 'low', 'normal', 'critical'
|
||||
* @returns {number}
|
||||
*/
|
||||
const createNotification = ( message: string, showDuration: number, messageType: string, priority: string, redirect?: string ): number => {
|
||||
/*
|
||||
Takes a notification options array that contains: message, showDuration (in seconds), messageType (ok, error, progress, info) and priority (low, normal, critical).
|
||||
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
|
||||
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
|
||||
*/
|
||||
let id = 0;
|
||||
|
||||
if ( priority === 'critical' ) {
|
||||
currentID.value[ 'critical' ] += 1;
|
||||
id = currentID.value[ 'critical' ];
|
||||
} else if ( priority === 'normal' ) {
|
||||
currentID.value[ 'medium' ] += 1;
|
||||
id = currentID.value[ 'medium' ];
|
||||
} else if ( priority === 'low' ) {
|
||||
currentID.value[ 'low' ] += 1;
|
||||
id = currentID.value[ 'low' ];
|
||||
}
|
||||
notifications.value[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': messageType, 'priority': priority, 'id': id, redirect: redirect };
|
||||
queue.value.push( id );
|
||||
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
|
||||
if ( ( new Date().getTime() - notificationDisplayStartTime.value ) / 1000 >= ( notifications.value[ currentDID.value ] ? notifications.value[ currentDID.value ].showDuration : 0 ) ) {
|
||||
handleNotifications();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a notification's message after creating it
|
||||
* @param {number} id The notification ID returned by createNotification
|
||||
* @param {string} message The new message
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateNotification = ( id: number, message: string ): void => {
|
||||
if ( notifications.value[ id ] ) {
|
||||
notifications.value[ id ].message = message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a previously created notification
|
||||
* @param {string} id The notification ID returned by createNotification
|
||||
* @returns {undefined}
|
||||
*/
|
||||
const cancelNotification = ( id: number ): undefined => {
|
||||
try {
|
||||
delete notifications.value[ id ];
|
||||
} catch ( error ) {
|
||||
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
|
||||
}
|
||||
try {
|
||||
queue.value.splice( queue.value.indexOf( id ), 1 );
|
||||
} catch {
|
||||
console.debug( 'queue empty' );
|
||||
}
|
||||
if ( currentDID.value == id ) {
|
||||
try {
|
||||
clearTimeout( notificationTimeout );
|
||||
} catch (err) { /* empty */ }
|
||||
handleNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
const handleNotifications = () => {
|
||||
notificationDisplayStartTime.value = new Date().getTime();
|
||||
queue.value.sort();
|
||||
if ( queue.value.length > 0 ) {
|
||||
if ( currentDID.value !== 0 ) {
|
||||
delete notifications.value[ currentDID.value ];
|
||||
}
|
||||
currentDID.value = notifications.value[ queue.value[ 0 ] ][ 'id' ];
|
||||
messageType.value = notifications.value[ queue.value[ 0 ] ].messageType;
|
||||
queue.value.reverse();
|
||||
queue.value.pop();
|
||||
progressBar = setInterval( progressBarHandler, 25 );
|
||||
notificationTimeout = setTimeout( () => {
|
||||
handleNotifications();
|
||||
}, notifications.value[ currentDID.value ].showDuration * 1000 );
|
||||
// eslint-disable-next-line no-undef
|
||||
$( '.message-box' ).css( 'z-index', 1000 );
|
||||
} else {
|
||||
try {
|
||||
clearInterval( progressBar );
|
||||
} catch (err) { /* empty */ }
|
||||
messageType.value = 'hide';
|
||||
// eslint-disable-next-line no-undef
|
||||
$( '.message-box' ).css( 'z-index', -1 );
|
||||
}
|
||||
}
|
||||
|
||||
const progressBarHandler = () => {
|
||||
currentTime.value = new Date().getTime();
|
||||
}
|
||||
|
||||
onUnmounted( () => {
|
||||
try {
|
||||
clearInterval( progressBar );
|
||||
} catch (err) { /* empty */ }
|
||||
try {
|
||||
clearInterval( notificationTimeout );
|
||||
} catch (err) { /* empty */ }
|
||||
} );
|
||||
|
||||
defineExpose( {
|
||||
createNotification,
|
||||
cancelNotification,
|
||||
updateNotification
|
||||
} );
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-box {
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
color: white;
|
||||
transition: all 0.5s;
|
||||
width: 95vw;
|
||||
right: 2.5vw;
|
||||
top: 1vh;
|
||||
height: 10vh;
|
||||
}
|
||||
|
||||
.close-notification {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background: none;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-notification-icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: all 0.5s;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.types {
|
||||
color: white;
|
||||
border-radius: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: 5%;
|
||||
padding: 1.5%;
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-right: calc( 5% + 30px );
|
||||
text-align: end;
|
||||
height: 90%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ok {
|
||||
background-color: rgb(1, 71, 1);
|
||||
}
|
||||
|
||||
.countdown-ok {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: rgb(114, 1, 1);
|
||||
}
|
||||
|
||||
.countdown-error {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: rgb(44, 112, 151);
|
||||
}
|
||||
|
||||
.countdown-info {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.countdown-warning {
|
||||
background-color: orangered;
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
z-index: 100;
|
||||
background-color: rgb(0, 0, 99);
|
||||
}
|
||||
|
||||
.countdown-ok {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.progress-spinner {
|
||||
animation: spin 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate( 0deg );
|
||||
}
|
||||
to {
|
||||
transform: rotate( 720deg );
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 750px) {
|
||||
|
||||
.default {
|
||||
height: 10vh;
|
||||
width: 32vw;
|
||||
}
|
||||
|
||||
.small {
|
||||
height: 7vh;
|
||||
width: 27vw;
|
||||
}
|
||||
|
||||
.big {
|
||||
height: 12vh;
|
||||
width: 38vw;
|
||||
}
|
||||
|
||||
.bigger {
|
||||
height: 15vh;
|
||||
width: 43vw;
|
||||
}
|
||||
|
||||
.huge {
|
||||
height: 20vh;
|
||||
width: 50vw;
|
||||
}
|
||||
|
||||
.topleft {
|
||||
top: 3vh;
|
||||
left: 0.5vw;
|
||||
}
|
||||
|
||||
.topright {
|
||||
top: 3vh;
|
||||
right: 0.5vw;
|
||||
}
|
||||
|
||||
.bottomright {
|
||||
bottom: 3vh;
|
||||
right: 0.5vw;
|
||||
}
|
||||
|
||||
.bottomleft {
|
||||
bottom: 3vh;
|
||||
right: 0.5vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (min-width: 1500px) {
|
||||
.default {
|
||||
height: 10vh;
|
||||
width: 15vw;
|
||||
}
|
||||
|
||||
.small {
|
||||
height: 7vh;
|
||||
width: 11vw;
|
||||
}
|
||||
|
||||
.big {
|
||||
height: 12vh;
|
||||
width: 17vw;
|
||||
}
|
||||
|
||||
.bigger {
|
||||
height: 15vh;
|
||||
width: 20vw;
|
||||
}
|
||||
|
||||
.huge {
|
||||
height: 20vh;
|
||||
width: 25vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="'player' + ( isShowingFullScreenPlayer ? '' : ' player-hidden' )">
|
||||
<div class="main-player">
|
||||
<!-- TODO: Make cover art of song or otherwise MusicPlayer Logo -->
|
||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo-player" @click="controlUI( 'show' )" v-if="coverArt === ''">
|
||||
<img :src="coverArt" alt="MusicPlayer Logo" class="logo-player" @click="controlUI( 'show' )" v-else>
|
||||
<div class="song-name-wrapper">
|
||||
<p class="song-name" @click="controlUI( 'show' )">{{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i></p>
|
||||
<div :class="'playback' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
|
||||
<sliderView :position="pos" :active="true" :duration="duration" name="main" @pos="( pos ) => player.goToPos( pos )"
|
||||
v-if="isShowingFullScreenPlayer"></sliderView>
|
||||
<div :class="'playback-pos-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
|
||||
<p class="playback-pos">{{ nicePlaybackPos }}</p>
|
||||
<p v-if="!isShowingFullScreenPlayer"> / </p>
|
||||
<p class="playback-duration">{{ niceDuration }}</p>
|
||||
<div class="player">
|
||||
<div :class="'main-player' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
|
||||
<div :class="'song-name-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" @click="controlUI( 'show' )">
|
||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo-player" v-if="coverArt === ''">
|
||||
<img :src="coverArt" alt="MusicPlayer Logo" class="logo-player" v-else>
|
||||
<div class="name-time">
|
||||
<p class="song-name">{{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i></p>
|
||||
<div class="playback" v-if="!isShowingFullScreenPlayer">
|
||||
<div class="playback-pos-wrapper">
|
||||
<p class="playback-pos">{{ nicePlaybackPos }}</p>
|
||||
<p> / </p>
|
||||
<p class="playback-duration">{{ niceDuration }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-wrapper">
|
||||
<span class="material-symbols-outlined controls next-previous" @click="control( 'previous' )" id="previous">skip_previous</span>
|
||||
<span class="material-symbols-outlined controls forward-back" @click="control( 'back' )" :style="'rotate: -' + 360 * clickCountBack + 'deg;'">replay_10</span>
|
||||
<span class="material-symbols-outlined controls" v-if="isPlaying" @click="playPause()" id="play-pause">pause</span>
|
||||
<span class="material-symbols-outlined controls" v-else @click="playPause()" id="play-pause">play_arrow</span>
|
||||
<span class="material-symbols-outlined controls forward-back" @click="control( 'forward' )" :style="'rotate: ' + 360 * clickCountForward + 'deg;'">forward_10</span>
|
||||
<span class="material-symbols-outlined controls next-previous" @click="control( 'next' )" id="next">skip_next</span>
|
||||
<div :class="'controls-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
|
||||
<div class="main-controls">
|
||||
<span class="material-symbols-outlined controls next-previous" @click="control( 'previous' )" id="previous" v-if="isShowingFullScreenPlayer">skip_previous</span>
|
||||
<span class="material-symbols-outlined controls forward-back" @click="control( 'back' )" :style="'rotate: -' + 360 * clickCountBack + 'deg;'" v-if="isShowingFullScreenPlayer">replay_10</span>
|
||||
<span class="material-symbols-outlined controls" v-if="isPlaying" @click="playPause()" id="play-pause">pause</span>
|
||||
<span class="material-symbols-outlined controls" v-else @click="playPause()" id="play-pause">play_arrow</span>
|
||||
<span class="material-symbols-outlined controls forward-back" @click="control( 'forward' )" :style="'rotate: ' + 360 * clickCountForward + 'deg;'" v-if="isShowingFullScreenPlayer">forward_10</span>
|
||||
<span class="material-symbols-outlined controls next-previous" @click="control( 'next' )" id="next">skip_next</span>
|
||||
</div>
|
||||
|
||||
<span class="material-symbols-outlined controls" @click="control( 'repeat' )" style="margin-left: 20px;">repeat{{ repeatMode }}</span>
|
||||
<span class="material-symbols-outlined controls" @click="control( 'shuffle' )">shuffle{{ shuffleMode }}</span>
|
||||
<div class="slider-wrapper" v-if="isShowingFullScreenPlayer">
|
||||
<div class="slider-pb-pos">
|
||||
<p class="playback-pos">{{ nicePlaybackPos }}</p>
|
||||
<p class="playback-duration" @click="toggleRemaining()">{{ niceDuration }}</p>
|
||||
</div>
|
||||
<sliderView :position="pos" :active="true" :duration="duration" name="main" @pos="( pos ) => player.goToPos( pos )"></sliderView>
|
||||
</div>
|
||||
|
||||
<div class="shuffle-repeat" v-if="isShowingFullScreenPlayer">
|
||||
<span class="material-symbols-outlined controls" @click="control( 'repeat' )" style="margin-right: auto;">repeat{{ repeatMode }}</span>
|
||||
<span class="material-symbols-outlined controls" @click="control( 'shuffle' )">shuffle{{ shuffleMode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="'playlist-view' + ( isShowingFullScreenPlayer ? '' : ' hidden' )">
|
||||
<span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span>
|
||||
<playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying"
|
||||
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }"></playlistView>
|
||||
<playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying" :pos="pos"
|
||||
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }"
|
||||
@add-new-songs="( songs ) => addNewSongs( songs )" @playlist-reorder="( move ) => moveSong( move )"></playlistView>
|
||||
</div>
|
||||
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule>
|
||||
<audio src="" id="local-audio" controls="false"></audio>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -46,7 +60,9 @@
|
||||
import playlistView from '@/components/playlistView.vue';
|
||||
import MusicKitJSWrapper from '@/scripts/music-player';
|
||||
import sliderView from './sliderView.vue';
|
||||
import type { Song } from '@/scripts/song';
|
||||
import type { ReadFile, Song, SongMove } from '@/scripts/song';
|
||||
import { parseBlob } from 'music-metadata-browser';
|
||||
import notificationsModule from './notificationsModule.vue';
|
||||
|
||||
const isPlaying = ref( false );
|
||||
const repeatMode = ref( '' );
|
||||
@@ -65,6 +81,7 @@
|
||||
const currentlyPlayingSongArtist = ref( '' );
|
||||
const pos = ref( 0 );
|
||||
const duration = ref( 0 );
|
||||
const notifications = ref( notificationsModule );
|
||||
|
||||
const emits = defineEmits( [ 'playerStateChange' ] );
|
||||
|
||||
@@ -79,6 +96,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
const toggleRemaining = () => {
|
||||
isShowingRemainingTime.value = !isShowingRemainingTime.value;
|
||||
}
|
||||
|
||||
const control = ( action: string ) => {
|
||||
if ( action === 'pause' ) {
|
||||
isPlaying.value = false;
|
||||
@@ -103,9 +124,11 @@
|
||||
if ( shuffleMode.value === '' ) {
|
||||
shuffleMode.value = '_on';
|
||||
player.setShuffle( true );
|
||||
getDetails();
|
||||
} else {
|
||||
shuffleMode.value = '';
|
||||
player.setShuffle( false );
|
||||
getDetails();
|
||||
}
|
||||
getDetails();
|
||||
} else if ( action === 'forward' ) {
|
||||
@@ -177,13 +200,71 @@
|
||||
} );
|
||||
}
|
||||
|
||||
const selectCustomPlaylist = async ( pl: ReadFile[] ) => {
|
||||
let n = notifications.value.createNotification( 'Analyzing playlist', 200, 'progress', 'normal' );
|
||||
playlist.value = [];
|
||||
let plLoad: Song[] = [];
|
||||
for ( let element in pl ) {
|
||||
try {
|
||||
plLoad.push( await fetchSongData( pl[ element ] ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
}
|
||||
notifications.value.updateNotification( n, `Analyzing playlist (${element}/${pl.length})` );
|
||||
}
|
||||
playlist.value = plLoad;
|
||||
player.setPlaylist( playlist.value );
|
||||
player.prepare( 0 );
|
||||
isPlaying.value = true;
|
||||
setTimeout( () => {
|
||||
startProgressTracker();
|
||||
getDetails();
|
||||
}, 2000 );
|
||||
notifications.value.cancelNotification( n );
|
||||
notifications.value.createNotification( 'Playlist loaded', 10, 'ok', 'normal' );
|
||||
}
|
||||
|
||||
const fetchSongData = ( songDetails: ReadFile ): Promise<Song> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
fetch( songDetails.url ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.blob().then( blob => {
|
||||
parseBlob( blob ).then( data => {
|
||||
player.findSongOnAppleMusic( data.common.title ?? songDetails.filename.split( '.' )[ 0 ] ).then( d => {
|
||||
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
|
||||
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) );
|
||||
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
|
||||
const song: Song = {
|
||||
artist: data.common.artist ?? d.data.results.songs.data[ 0 ].attributes.artistName,
|
||||
title: data.common.title ?? d.data.results.songs.data[ 0 ].attributes.name,
|
||||
duration: data.format.duration ?? ( d.data.results.songs.data[ 0 ].attributes.durationInMillis / 1000 ),
|
||||
id: songDetails.url,
|
||||
origin: 'disk',
|
||||
cover: url
|
||||
}
|
||||
resolve( song );
|
||||
} ).catch( e => {
|
||||
reject( e );
|
||||
} );
|
||||
} ).catch( e => {
|
||||
reject( e );
|
||||
} );
|
||||
} ).catch( e => {
|
||||
reject( e );
|
||||
} );
|
||||
}
|
||||
} ).catch( e => {
|
||||
reject( e );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
const getDetails = () => {
|
||||
const details = player.getPlayingSong();
|
||||
currentlyPlayingSongName.value = details.title;
|
||||
coverArt.value = details.cover;
|
||||
currentlyPlayingSongIndex.value = player.getPlayingSongID();
|
||||
currentlyPlayingSongIndex.value = player.getQueueID();
|
||||
playlist.value = player.getQueue();
|
||||
console.log( playlist.value );
|
||||
currentlyPlayingSongArtist.value = details.artist;
|
||||
}
|
||||
|
||||
@@ -207,7 +288,10 @@
|
||||
|
||||
|
||||
let progressTracker = 0;
|
||||
let hasReachedEnd = false;
|
||||
let hasStarted = false;
|
||||
const startProgressTracker = () => {
|
||||
hasReachedEnd = false;
|
||||
isPlaying.value = true;
|
||||
const playingSong = player.getPlayingSong();
|
||||
duration.value = playingSong.duration;
|
||||
@@ -224,9 +308,18 @@
|
||||
}
|
||||
progressTracker = setInterval( () => {
|
||||
pos.value = player.getPlaybackPos();
|
||||
if ( pos.value > playingSong.duration - 1 ) {
|
||||
// TODO: repeat
|
||||
control( 'next' );
|
||||
if ( pos.value > playingSong.duration - 1 && !hasReachedEnd ) {
|
||||
stopProgressTracker();
|
||||
hasReachedEnd = true;
|
||||
if ( repeatMode.value === '_one_on' ) {
|
||||
player.goToPos( 0 );
|
||||
} else {
|
||||
control( 'next' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( pos.value > 0 && !hasStarted ) {
|
||||
hasStarted = true;
|
||||
}
|
||||
|
||||
const minuteCount = Math.floor( pos.value / 60 );
|
||||
@@ -264,6 +357,49 @@
|
||||
isPlaying.value = false;
|
||||
}
|
||||
|
||||
const moveSong = ( move: SongMove ) => {
|
||||
player.moveSong( move );
|
||||
getDetails();
|
||||
}
|
||||
|
||||
const addNewSongs = async ( songs: ReadFile[] ) => {
|
||||
let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' );
|
||||
playlist.value = player.getPlaylist();
|
||||
for ( let element in songs ) {
|
||||
try {
|
||||
playlist.value.push( await fetchSongData( songs[ element ] ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
}
|
||||
notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` );
|
||||
}
|
||||
player.setPlaylist( playlist.value );
|
||||
player.prepare( 0 );
|
||||
isPlaying.value = true;
|
||||
setTimeout( () => {
|
||||
startProgressTracker();
|
||||
getDetails();
|
||||
}, 2000 );
|
||||
notifications.value.cancelNotification( n );
|
||||
notifications.value.createNotification( 'New songs added', 10, 'ok', 'normal' );
|
||||
}
|
||||
|
||||
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
|
||||
|
||||
document.addEventListener( 'keydown', ( e ) => {
|
||||
if ( e.key === ' ' ) {
|
||||
// TODO: fix
|
||||
e.preventDefault();
|
||||
playPause();
|
||||
} else if ( e.key === 'ArrowRight' ) {
|
||||
e.preventDefault();
|
||||
control( 'next' );
|
||||
} else if ( e.key === 'ArrowLeft' ) {
|
||||
e.preventDefault();
|
||||
control( 'previous' );
|
||||
}
|
||||
} );
|
||||
|
||||
defineExpose( {
|
||||
logIntoAppleMusic,
|
||||
getPlaylists,
|
||||
@@ -271,12 +407,12 @@
|
||||
getAuth,
|
||||
skipLogin,
|
||||
selectPlaylist,
|
||||
selectCustomPlaylist,
|
||||
} );
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.player {
|
||||
height: 15%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -296,7 +432,14 @@
|
||||
position: relative
|
||||
}
|
||||
|
||||
.main-player.full-screen {
|
||||
flex-direction: column;
|
||||
height: 30vh;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.song-name-wrapper {
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
width: 100%;
|
||||
@@ -305,22 +448,66 @@
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.song-name-wrapper.full-screen {
|
||||
flex-direction: row;
|
||||
max-height: 50%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name-time {
|
||||
margin-right: auto;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.song-name {
|
||||
margin: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.slider-wrapper {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.shuffle-repeat {
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
width: 80%;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.slider-pb-pos {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slider-pb-pos .playback-duration {
|
||||
margin-top: 5px;
|
||||
margin-left: auto;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider-pb-pos .playback-pos {
|
||||
margin-top: 5px;
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.logo-player {
|
||||
cursor: pointer;
|
||||
height: 80%;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.player-hidden {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
@@ -342,6 +529,17 @@
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.controls-wrapper.full-screen {
|
||||
flex-direction: column;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.main-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#play-pause {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
@@ -390,7 +588,7 @@
|
||||
}
|
||||
|
||||
.pl-wrapper {
|
||||
height: 80vh;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.playback {
|
||||
@@ -412,6 +610,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.playback-pos-wrapper p {
|
||||
@@ -425,4 +624,27 @@
|
||||
.playback-pos-wrapper.full-screen .playback-duration {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 800px) {
|
||||
.slider-wrapper {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.shuffle-repeat {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.main-controls .controls {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
#play-pause {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
#local-audio {
|
||||
position: fixed;
|
||||
bottom: -50%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Playlist</h1>
|
||||
<input type="file" multiple accept="audio/*" id="more-songs">
|
||||
<button @click="addNewSongs()">Load local songs</button>
|
||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
|
||||
<div class="playlist-box" id="pl-box">
|
||||
<!-- TODO: Allow sorting -->
|
||||
<!-- TODO: Allow adding more songs with search on Apple Music or loading from local disk -->
|
||||
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
|
||||
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
|
||||
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )">
|
||||
@@ -16,16 +21,18 @@
|
||||
<span class="material-symbols-outlined play-icon" @click="control( 'play' )" v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )">play_arrow</span>
|
||||
<span class="material-symbols-outlined play-icon" @click="play( song.id )" v-else>play_arrow</span>
|
||||
<span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</span>
|
||||
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'up' )" title="Move song up" v-if="canBeMoved( 'up', song.id )">arrow_upward</span>
|
||||
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'down' )" title="Move song down" v-if="canBeMoved( 'down', song.id )">arrow_downward</span>
|
||||
<h3 class="song-title">{{ song.title }}</h3>
|
||||
<p class="playing-in">playing in</p>
|
||||
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Song } from '@/scripts/song';
|
||||
import { computed } from 'vue';
|
||||
import type { ReadFile, Song } from '@/scripts/song';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps( {
|
||||
'playlist': {
|
||||
@@ -42,8 +49,14 @@
|
||||
default: true,
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
'pos': {
|
||||
default: 0,
|
||||
required: false,
|
||||
type: Number,
|
||||
}
|
||||
} );
|
||||
const hasSelectedSongs = ref( true );
|
||||
|
||||
const computedPlaylist = computed( () => {
|
||||
let pl: Song[] = [];
|
||||
@@ -54,31 +67,53 @@
|
||||
return pl;
|
||||
} );
|
||||
|
||||
// TODO: Implement
|
||||
// const getTimeUntil = computed( () => {
|
||||
// return ( song ) => {
|
||||
// let timeRemaining = 0;
|
||||
// for ( let i = this.queuePos; i < Object.keys( this.songs ).length; i++ ) {
|
||||
// if ( this.songs[ i ] == song ) {
|
||||
// break;
|
||||
// }
|
||||
// timeRemaining += parseInt( this.songs[ i ].duration );
|
||||
// }
|
||||
// if ( this.isPlaying ) {
|
||||
// if ( timeRemaining === 0 ) {
|
||||
// return 'Currently playing';
|
||||
// } else {
|
||||
// return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
|
||||
// }
|
||||
// } else {
|
||||
// if ( timeRemaining === 0 ) {
|
||||
// return 'Plays next';
|
||||
// } else {
|
||||
// return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } );
|
||||
const canBeMoved = computed( () => {
|
||||
return ( direction: movementDirection, songID: string ): boolean => {
|
||||
let id = 0;
|
||||
for ( let song in props.playlist ) {
|
||||
if ( props.playlist[ song ].id === songID ) {
|
||||
id = parseInt( song );
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( direction === 'up' ) {
|
||||
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} )
|
||||
|
||||
const getTimeUntil = computed( () => {
|
||||
return ( song: Song ) => {
|
||||
let timeRemaining = 0;
|
||||
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
|
||||
if ( props.playlist[ i ] == song ) {
|
||||
break;
|
||||
}
|
||||
timeRemaining += props.playlist[ i ].duration;
|
||||
}
|
||||
if ( props.isPlaying ) {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Currently playing';
|
||||
} else {
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
|
||||
}
|
||||
} else {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Plays next';
|
||||
} else {
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
const control = ( action: string ) => {
|
||||
@@ -89,12 +124,44 @@
|
||||
emits( 'play-song', song );
|
||||
}
|
||||
|
||||
const emits = defineEmits( [ 'play-song', 'control' ] );
|
||||
const addNewSongs = () => {
|
||||
// TODO: Also allow loading Apple Music songs
|
||||
const fileURLList: ReadFile[] = [];
|
||||
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? [];
|
||||
if ( allFiles.length > 0 ) {
|
||||
hasSelectedSongs.value = true;
|
||||
for ( let file = 0; file < allFiles.length; file++ ) {
|
||||
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } );
|
||||
}
|
||||
emits( 'add-new-songs', fileURLList );
|
||||
} else {
|
||||
hasSelectedSongs.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
type movementDirection = 'up' | 'down';
|
||||
const moveSong = ( songID: string, direction: movementDirection ) => {
|
||||
let newSongPos = 0;
|
||||
let hasFoundSongToMove = false;
|
||||
for ( let el in props.playlist ) {
|
||||
if ( props.playlist[ el ].id === songID ) {
|
||||
const currPos = parseInt( el );
|
||||
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
|
||||
hasFoundSongToMove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( hasFoundSongToMove ) {
|
||||
emits( 'playlist-reorder', { 'songID': songID, 'newPos': newSongPos } );
|
||||
}
|
||||
}
|
||||
|
||||
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs' ] );
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.playlist-box {
|
||||
height: 80vh !important;
|
||||
height: calc( 100% - 100px );
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
@@ -222,4 +289,9 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.move-icon {
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,16 @@
|
||||
<template>
|
||||
<div class="playlists">
|
||||
<h3 style="width: fit-content;">Your playlists</h3>
|
||||
<div v-if="$props.playlists ? $props.playlists.length < 1 : true">
|
||||
loading...
|
||||
<div v-if="( $props.playlists ? $props.playlists.length < 1 : true ) && $props.isLoggedIn">
|
||||
Loading...
|
||||
<!-- TODO: Make prettier -->
|
||||
</div>
|
||||
<div v-else-if="!$props.isLoggedIn">
|
||||
<p>You are not logged into Apple Music.</p>
|
||||
<input class="fancy-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br>
|
||||
<button @click="loadPlaylistFromDisk()" class="fancy-button">Load custom playlist from disk</button>
|
||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed!</p>
|
||||
</div>
|
||||
<div class="playlist-wrapper">
|
||||
<div v-for="pl in $props.playlists" v-bind:key="pl.id" class="playlist" @click="selectPlaylist( pl.id )">
|
||||
{{ pl.attributes.name }}
|
||||
@@ -14,15 +20,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ReadFile } from '@/scripts/song';
|
||||
import { ref } from 'vue';
|
||||
const hasSelectedSongs = ref( true );
|
||||
|
||||
defineProps( {
|
||||
'playlists': {
|
||||
'default': [],
|
||||
'type': Array<any>,
|
||||
'required': true,
|
||||
},
|
||||
'isLoggedIn': {
|
||||
'default': false,
|
||||
'type': Boolean,
|
||||
'required': true,
|
||||
}
|
||||
} );
|
||||
|
||||
const emits = defineEmits( [ 'selected-playlist' ] );
|
||||
const loadPlaylistFromDisk = () => {
|
||||
const fileURLList: ReadFile[] = [];
|
||||
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? [];
|
||||
if ( allFiles.length > 0 ) {
|
||||
hasSelectedSongs.value = true;
|
||||
for ( let file = 0; file < allFiles.length; file++ ) {
|
||||
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } );
|
||||
}
|
||||
emits( 'custom-playlist', fileURLList );
|
||||
} else {
|
||||
hasSelectedSongs.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const emits = defineEmits( [ 'selected-playlist', 'custom-playlist' ] );
|
||||
|
||||
const selectPlaylist = ( id: string ) => {
|
||||
emits( 'selected-playlist', id );
|
||||
|
||||
481
MusicPlayerV2-GUI/src/components/popupModule.vue
Normal file
481
MusicPlayerV2-GUI/src/components/popupModule.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<!-- eslint-disable no-undef -->
|
||||
<template>
|
||||
<div id="popup-backdrop">
|
||||
<div class="popup-container">
|
||||
<div class="popup" :class="size">
|
||||
<div class="close-wrapper"><span class="material-symbols-outlined close-button" @click="closePopup( 'cancel' );" title="Close this popup">close</span></div>
|
||||
<div class="message-container">
|
||||
<div v-if="contentType === 'string'" class="options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<div class="button-wrapper">
|
||||
<button @click="closePopup( 'ok' )" title="Close popup" class="buttons fancy-button">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'html'" v-html="data.message" class="options"></div>
|
||||
<div v-else-if="contentType === 'code'" class="options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<button @click="copy()" id="code-copy" class="buttons fancy-button">Copy</button>
|
||||
<pre>
|
||||
<code>
|
||||
{{ data.options.code }}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'long-text'" class="options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<p>{{ data.options.note }}</p>
|
||||
<textarea cols="80" rows="10" v-model="data.selected" id="text-input"></textarea>
|
||||
<div class="button-wrapper">
|
||||
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.display.save ?? 'Save' }}</button>
|
||||
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'text'" class="options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<input type="text" v-model="data.selected">
|
||||
<p>{{ info }}</p>
|
||||
<div class="button-wrapper">
|
||||
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.display.save ?? 'Save' }}</button>
|
||||
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'number'" class="options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<input type="number" v-model="data.selected">
|
||||
<div class="button-wrapper">
|
||||
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.display.save ?? 'Save' }}</button>
|
||||
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'settings'" class="options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<settings v-model:settings="data.options"></settings>
|
||||
<div class="button-wrapper">
|
||||
<button @click="submitSettings( 'ok' )" title="Save changes" class="buttons fancy-button">Save</button>
|
||||
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'confirm'" class="confirm options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<div class="button-wrapper">
|
||||
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.ok ?? 'Ok' }}</button>
|
||||
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'dropdown'" class="options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<select id="select" v-model="data.selected">
|
||||
<option v-for="selectOption in data.options" :key="selectOption.value" :value="selectOption.value">{{ selectOption.displayName }}</option>
|
||||
</select>
|
||||
<div class="button-wrapper">
|
||||
<button @click="closePopup( 'ok' )" title="Save changes" class="buttons fancy-button">{{ data.options.display.save ?? 'Save' }}</button>
|
||||
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">{{ data.options.display.cancel ?? 'Cancel' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'selection'" class="options selection">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<div v-for="selectOption in data.options.selections" :key="selectOption.value" class="select-button-wrapper">
|
||||
<button class="select-button" @click="closePopupAdvanced( 'ok', selectOption.value )">{{ selectOption.displayName }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'iframe'" class="options iframe-wrapper">
|
||||
<iframe :src="data.options.link" frameborder="0" class="popup-iframe"></iframe>
|
||||
</div>
|
||||
<div v-else-if="contentType === 'editor'" class="options">
|
||||
<!-- Create the toolbar container -->
|
||||
<h3>{{ data.message }}</h3>
|
||||
<p v-if="data.options.note" v-html="data.options.note"></p>
|
||||
<!-- Optional toggles (added via options object) -->
|
||||
<table class="editor-options">
|
||||
<tr v-for="element in data.options.settings" :key="element.id">
|
||||
<td>
|
||||
{{ element.displayName }}
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" v-if="element.type === 'text'" v-model="data.selected[ element.id ]">
|
||||
<input type="number" v-else-if="element.type === 'number'" v-model="data.selected[ element.id ]">
|
||||
<input type="email" v-else-if="element.type === 'email'" v-model="data.selected[ element.id ]">
|
||||
<select v-else-if="element.type === 'dropdown'" v-model="data.selected[ element.id ]">
|
||||
<option v-for="el in element.options" :key="el.value" :value="el.value">{{ el.displayName }}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div id="quill-toolbar">
|
||||
<span class="ql-formats">
|
||||
<select class="ql-font" title="Fonts">
|
||||
<option selected="" title="Default"></option>
|
||||
<option value="serif" title="Serif"></option>
|
||||
<option value="monospace" title="Monospace"></option>
|
||||
</select>
|
||||
<select class="ql-size" title="Font size">
|
||||
<option value="small" title="Small"></option>
|
||||
<option selected="" title="Default"></option>
|
||||
<option value="large" title="Large"></option>
|
||||
<option value="huge" title="Huge"></option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-bold" title="Bold"></button>
|
||||
<button class="ql-italic" title="Italic"></button>
|
||||
<button class="ql-underline" title="Underlined"></button>
|
||||
<button class="ql-strike" title="Strikethrough"></button>
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<select class="ql-color" title="Text colour"></select>
|
||||
<select class="ql-background" title="Background colour"></select>
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-list" value="ordered" title="Ordered list"></button>
|
||||
<button class="ql-list" value="bullet" title="Bullet points"></button>
|
||||
<select class="ql-align" title="Alignment">
|
||||
<option selected="" title="left"></option>
|
||||
<option value="center" title="center"></option>
|
||||
<option value="right" title="right"></option>
|
||||
<option value="justify" title="block"></option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-link" title="Insert link"></button>
|
||||
<button class="ql-image" title="Insert image"></button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Create the editor container -->
|
||||
<div id="quill-editor">
|
||||
</div>
|
||||
|
||||
<div class="message-iframe" v-if="data.selected.oldMsg" style="height: 60vh;">
|
||||
<p>Attached message: </p>
|
||||
<iframe :srcdoc="data.selected.oldMsg" frameborder="0" class="message-iframe"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="button-wrapper">
|
||||
<button @click="closePopupEditor()" :title="data.options.saveButtonHint" class="buttons fancy-button">{{ data.options.saveButtonDisplay }}</button>
|
||||
<button @click="closePopup( 'cancel' )" title="Cancel changes" class="buttons fancy-button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Options to be passed in: html, settings (for settings component), strings, confirm, dropdowns, selection -->
|
||||
|
||||
|
||||
<script>
|
||||
import settings from '@/components/settingsOptions.vue';
|
||||
import hljs from 'highlight.js';
|
||||
import beautify from 'json-beautify';
|
||||
import Quill from 'quill';
|
||||
import( 'quill/dist/quill.snow.css' );
|
||||
|
||||
export default {
|
||||
name: 'popupsHandler',
|
||||
components: {
|
||||
settings,
|
||||
},
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
'default': 'normal',
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
contentType: 'dropdown',
|
||||
data: {
|
||||
options: {
|
||||
display: '',
|
||||
},
|
||||
},
|
||||
info: '',
|
||||
editor: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
closePopup( message ) {
|
||||
if ( this.data.options.disallowedCharacters ) {
|
||||
for ( let letter in this.data.selected ) {
|
||||
if ( this.data.options.disallowedCharacters.includes( this.data.selected[ letter ] ) ) {
|
||||
this.info = `Illegal character "${ this.data.selected[ letter ] }"`;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
$( '#popup-backdrop' ).fadeOut( 300 );
|
||||
if ( message ) {
|
||||
this.$emit( 'data', { 'data': this.data.selected, 'status': message } );
|
||||
}
|
||||
},
|
||||
closePopupEditor () {
|
||||
this.data.selected;
|
||||
this.data.selected.mail = document.getElementsByClassName( 'ql-editor' )[ 0 ].innerHTML + ( this.data.selected.oldMsg ?? '' );
|
||||
this.closePopup( 'editor' );
|
||||
},
|
||||
selectTicket ( option ) {
|
||||
let total = 0;
|
||||
for ( let i in this.data.options.count ) {
|
||||
total += this.data.options.count[ i ];
|
||||
}
|
||||
|
||||
if ( total < this.data.options.max ) {
|
||||
this.data.options.count[ option ] += 1;
|
||||
}
|
||||
},
|
||||
deselectTicket ( option ) {
|
||||
if ( this.data.options.count[ option ] > 0 ) {
|
||||
this.data.options.count[ option ] -= 1;
|
||||
}
|
||||
},
|
||||
submitTicket () {
|
||||
// eslint-disable-next-line no-undef
|
||||
$( '#popup-backdrop' ).fadeOut( 300 );
|
||||
this.$emit( 'ticket', { 'data': this.data.options.count, 'component': this.data.options.id } );
|
||||
},
|
||||
closePopupAdvanced ( message, data ) {
|
||||
this.data[ 'selected' ] = data;
|
||||
this.closePopup( message );
|
||||
},
|
||||
submitSettings () {
|
||||
// eslint-disable-next-line no-undef
|
||||
$( '#popup-backdrop' ).fadeOut( 300 );
|
||||
const dat = this.data.options;
|
||||
let ret = {};
|
||||
for ( let setting in dat ) {
|
||||
if ( dat[ setting ][ 'type' ] !== 'link' ) {
|
||||
ret[ setting ] = dat[ setting ][ 'value' ];
|
||||
}
|
||||
}
|
||||
this.$emit( 'data', { 'data': ret, 'status': 'settings' } );
|
||||
},
|
||||
openPopup ( message, options, dataType, selected ) {
|
||||
this.data = {
|
||||
'message': message ?? 'No message defined on method call!!',
|
||||
'options': options ?? { '1': { 'value': 'undefined', 'displayName': 'No options specified in call' } },
|
||||
'selected': selected ?? ''
|
||||
};
|
||||
this.contentType = dataType ?? 'string';
|
||||
// eslint-disable-next-line no-undef
|
||||
$( '#popup-backdrop' ).fadeIn( 300 );
|
||||
if ( dataType === 'code' ) {
|
||||
if ( options.lang === 'json' ) {
|
||||
this.data.options.code = beautify( options.code, null, 2, 50 );
|
||||
}
|
||||
setTimeout( () => {
|
||||
hljs.highlightAll();
|
||||
}, 200 );
|
||||
} else if ( dataType === 'editor' ) {
|
||||
setTimeout( () => {
|
||||
if ( !document.getElementById( 'quill-editor' ).classList.contains( 'ql-container' ) ) {
|
||||
this.editor = new Quill( '#quill-editor', {
|
||||
modules: { toolbar: '#quill-toolbar' },
|
||||
theme: 'snow',
|
||||
} );
|
||||
if ( this.data.selected === '' ) {
|
||||
this.data.selected = {};
|
||||
}
|
||||
setTimeout( () => {
|
||||
if ( selected.message ) {
|
||||
console.log( selected.message );
|
||||
document.getElementsByClassName( 'ql-editor' )[ 0 ].innerHTML = selected.message;
|
||||
}
|
||||
}, 500 );
|
||||
}
|
||||
}, 200 );
|
||||
}
|
||||
},
|
||||
copy() {
|
||||
const codeCopy = document.getElementById( 'code-copy' )
|
||||
codeCopy.innerHTML = 'Copied!';
|
||||
navigator.clipboard.writeText( this.data.options.code );
|
||||
setTimeout( () => {
|
||||
codeCopy.innerHTML = 'Copy';
|
||||
}, 2000 );
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-iframe {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
margin-top: 1%;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
code {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#popup-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba( 0, 0, 0, 0.6 );
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
width: 90%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
width: 100%;
|
||||
margin-top: 3%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.close-wrapper {
|
||||
width: 100%;
|
||||
height: 5%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
margin-right: 1vw;
|
||||
margin-top: 2vw;
|
||||
font-size: 200%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popup {
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
background-color: white;
|
||||
width: 90vw;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.popup-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
height: 90%;
|
||||
width: 90%;
|
||||
margin-left: 5%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.options .buttons {
|
||||
padding: 1% 2%;
|
||||
margin: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.options .buttons:hover {
|
||||
background-color: darkgreen;
|
||||
}
|
||||
|
||||
.select-button-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select-button {
|
||||
background-color: rgba( 0, 0, 0, 0 ) !important;
|
||||
color: black !important;
|
||||
padding: 3vh 2vw !important;
|
||||
border: solid black 1px;
|
||||
border-radius: 5px;
|
||||
transition: all 0.5s ease-in-out;
|
||||
margin-bottom: 1vh;
|
||||
width: 90%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select-button:hover {
|
||||
background-color: gray !important;
|
||||
}
|
||||
|
||||
.controls {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
font-size: 100%;
|
||||
font-weight: bold;
|
||||
border: solid var( --primary-color ) 1px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 999px) {
|
||||
.small {
|
||||
width: 40%;
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
.normal {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.big {
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
}
|
||||
|
||||
.bigger {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.huge {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
#quill-editor, #quill-toolbar {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#quill-editor {
|
||||
min-height: 20vh;
|
||||
}
|
||||
|
||||
.editor-options {
|
||||
width: 80%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Song } from "./song";
|
||||
import type { SearchResult, Song, SongMove } from "./song";
|
||||
|
||||
interface Config {
|
||||
devToken: string;
|
||||
@@ -20,6 +20,7 @@ class MusicKitJSWrapper {
|
||||
isShuffleEnabled: boolean;
|
||||
hasEncounteredAuthError: boolean;
|
||||
queuePos: number;
|
||||
audioPlayer: HTMLAudioElement;
|
||||
|
||||
constructor () {
|
||||
this.playingSongID = 0;
|
||||
@@ -35,6 +36,7 @@ class MusicKitJSWrapper {
|
||||
this.isLoggedIn = false;
|
||||
this.hasEncounteredAuthError = false;
|
||||
this.queuePos = 0;
|
||||
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||
|
||||
const self = this;
|
||||
|
||||
@@ -77,6 +79,7 @@ class MusicKitJSWrapper {
|
||||
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', { credentials: 'include' } ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.text().then( token => {
|
||||
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||
// MusicKit global is now defined
|
||||
MusicKit.configure( {
|
||||
developerToken: token,
|
||||
@@ -204,7 +207,11 @@ class MusicKitJSWrapper {
|
||||
console.log( err );
|
||||
} );
|
||||
} else {
|
||||
// TODO: Implement
|
||||
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||
this.audioPlayer.src = this.playlist[ this.playingSongID ].id;
|
||||
setTimeout( () => {
|
||||
this.control( 'play' );
|
||||
}, 500 );
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
@@ -225,8 +232,8 @@ class MusicKitJSWrapper {
|
||||
this.musicKit.play();
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.play();
|
||||
return false;
|
||||
// TODO: Implement
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
@@ -237,8 +244,8 @@ class MusicKitJSWrapper {
|
||||
this.musicKit.pause();
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.pause();
|
||||
return false;
|
||||
// TODO: Implement
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
@@ -248,8 +255,8 @@ class MusicKitJSWrapper {
|
||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
|
||||
return false;
|
||||
} else {
|
||||
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
|
||||
return false;
|
||||
// TODO: Implement
|
||||
}
|
||||
case "skip-10":
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
@@ -266,30 +273,30 @@ class MusicKitJSWrapper {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: Finish
|
||||
// if ( this.audioPlayer.currentTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
|
||||
// this.audioPlayer.currentTime = this.audioPlayer.currentTime + 10;
|
||||
// this.pos = this.audioPlayer.currentTime;
|
||||
// this.sendUpdate( 'pos' );
|
||||
// } else {
|
||||
// if ( this.repeatMode !== 'one' ) {
|
||||
// this.control( 'next' );
|
||||
// } else {
|
||||
// this.audioPlayer.currentTime = 0;
|
||||
// this.pos = this.audioPlayer.currentTime;
|
||||
// this.sendUpdate( 'pos' );
|
||||
// }
|
||||
// }
|
||||
if ( this.audioPlayer.currentTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
|
||||
this.audioPlayer.currentTime = this.audioPlayer.currentTime + 10;
|
||||
} else {
|
||||
if ( this.repeatMode !== 'once' ) {
|
||||
this.control( 'next' );
|
||||
} else {
|
||||
this.audioPlayer.currentTime = 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "next":
|
||||
if ( this.queuePos < this.queue.length ) {
|
||||
if ( this.queuePos < this.queue.length - 1 ) {
|
||||
this.queuePos += 1;
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
return true;
|
||||
} else {
|
||||
this.queuePos = 0;
|
||||
this.control( 'pause' );
|
||||
if ( this.repeatMode !== 'all' ) {
|
||||
this.control( 'pause' );
|
||||
} else {
|
||||
this.playingSongID = this.queue[ this.queuePos ];
|
||||
this.prepare( this.queue[ this.queuePos ] );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "previous":
|
||||
@@ -324,6 +331,13 @@ class MusicKitJSWrapper {
|
||||
this.queue.push( parseInt( song ) );
|
||||
}
|
||||
}
|
||||
// Find current song ID in queue
|
||||
for ( const el in this.queue ) {
|
||||
if ( this.queue[ el ] === this.playingSongID ) {
|
||||
this.queuePos = parseInt( el );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRepeatMode ( mode: RepeatMode ) {
|
||||
@@ -334,10 +348,40 @@ class MusicKitJSWrapper {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
this.musicKit.seekToTime( pos );
|
||||
} else {
|
||||
// TODO: Implement
|
||||
this.audioPlayer.currentTime = pos;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
moveSong ( move: SongMove ) {
|
||||
const newQueue = [];
|
||||
const finishedQueue = [];
|
||||
let songID = 0;
|
||||
for ( const song in this.playlist ) {
|
||||
if ( this.playlist[ song ].id === move.songID ) {
|
||||
songID = parseInt( song );
|
||||
break;
|
||||
}
|
||||
}
|
||||
for ( const el in this.queue ) {
|
||||
if ( this.queue[ el ] !== songID ) {
|
||||
newQueue.push( this.queue[ el ] );
|
||||
}
|
||||
}
|
||||
let hasBeenAdded = false;
|
||||
for ( const el in newQueue ) {
|
||||
if ( parseInt( el ) === move.newPos ) {
|
||||
finishedQueue.push( songID );
|
||||
hasBeenAdded = true;
|
||||
}
|
||||
finishedQueue.push( newQueue[ el ] );
|
||||
}
|
||||
if ( !hasBeenAdded ) {
|
||||
finishedQueue.push( songID );
|
||||
}
|
||||
this.queue = finishedQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current position of the play heed. Will return in ms since start of the song
|
||||
* @returns {number}
|
||||
@@ -346,8 +390,7 @@ class MusicKitJSWrapper {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
return this.musicKit.currentPlaybackTime;
|
||||
} else {
|
||||
return 0;
|
||||
// TODO: Implement
|
||||
return this.audioPlayer.currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +410,14 @@ class MusicKitJSWrapper {
|
||||
return this.playingSongID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the queue index of the currently playing song
|
||||
* @returns {number}
|
||||
*/
|
||||
getQueueID (): number {
|
||||
return this.queuePos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full playlist, as it is set currently, not ordered by queue settings, but as passed in originally
|
||||
* @returns {Song[]}
|
||||
@@ -405,14 +456,25 @@ class MusicKitJSWrapper {
|
||||
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||
return this.musicKit.isPlaying;
|
||||
} else {
|
||||
// TODO: Implement
|
||||
return false;
|
||||
return !this.audioPlayer.paused;
|
||||
}
|
||||
}
|
||||
|
||||
// findSongOnAppleMusic ( searchTerm: string ): Song => {
|
||||
// TODO: Implement
|
||||
// }
|
||||
findSongOnAppleMusic ( searchTerm: string ): Promise<SearchResult> {
|
||||
// TODO: Make storefront adjustable
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const queryParameters = {
|
||||
term: ( searchTerm ),
|
||||
types: [ 'songs' ],
|
||||
};
|
||||
this.musicKit.api.music( `v1/catalog/ch/search`, queryParameters ).then( results => {
|
||||
resolve( results );
|
||||
} ).catch( e => {
|
||||
console.error( e );
|
||||
reject( e );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
export default MusicKitJSWrapper;
|
||||
@@ -1,7 +1,44 @@
|
||||
const subscribe = ( handler: ( data: any ) => {} ): string => {
|
||||
return '';
|
||||
/*
|
||||
* MusicPlayerV2 - notificationHandler.ts
|
||||
*
|
||||
* Created by Janis Hutz 06/26/2024, Licensed under the GPL V3 License
|
||||
* https://janishutz.com, development@janishutz.com
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
// These functions handle connections to the backend with socket.io
|
||||
|
||||
import { io, type Socket } from "socket.io-client"
|
||||
|
||||
class NotificationHandler {
|
||||
socket: Socket;
|
||||
|
||||
constructor () {
|
||||
this.socket = io();
|
||||
}
|
||||
|
||||
/**
|
||||
* Description
|
||||
* @param {string} event The event to emit
|
||||
* @param {any} data
|
||||
* @returns {void}
|
||||
*/
|
||||
emit ( event: string, data: any ): void {
|
||||
this.socket.emit( event, data );
|
||||
}
|
||||
|
||||
registerListener ( event: string, cb: ( data: any ) => void ): void {
|
||||
this.socket.on( event, cb );
|
||||
}
|
||||
|
||||
disconnect (): void {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
|
||||
joinRoom ( roomName: string ): void {
|
||||
// this.socket.
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = ( id: string ) => {
|
||||
|
||||
}
|
||||
export default NotificationHandler;
|
||||
40
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
40
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
@@ -40,4 +40,44 @@ export interface Song {
|
||||
* (OPTIONAL) This will be displayed in brackets on the showcase screens
|
||||
*/
|
||||
additionalInfo?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ReadFile {
|
||||
url: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
data: {
|
||||
results: {
|
||||
songs: {
|
||||
data: AppleMusicSongData[],
|
||||
href: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface AppleMusicSongData {
|
||||
id: string,
|
||||
type: string;
|
||||
href: string;
|
||||
attributes: {
|
||||
albumName: string;
|
||||
artistName: string;
|
||||
artwork: {
|
||||
width: number,
|
||||
height: number,
|
||||
url: string
|
||||
},
|
||||
name: string;
|
||||
genreNames: string[];
|
||||
durationInMillis: number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SongMove {
|
||||
songID: string;
|
||||
newPos: number;
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div class="app-view">
|
||||
<div class="home-view" v-if="isLoggedIntoAppleMusic">
|
||||
<libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }"></libraryView>
|
||||
<div class="home-view" v-if="isReady">
|
||||
<libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }"
|
||||
:is-logged-in="isLoggedIntoAppleMusic" @custom-playlist="( pl ) => selectCustomPlaylist( pl )"></libraryView>
|
||||
</div>
|
||||
<div v-else class="login-view">
|
||||
<img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon">
|
||||
<button class="fancy-button" style="margin-top: 20px;" @click="logIntoAppleMusic()">Log into Apple Music</button>
|
||||
<button class="fancy-button" title="This allows you to use local playlists only. Cover images for your songs will be fetched from the apple music api as good as possible" @click="skipLogin()">Continue without logging in</button>
|
||||
</div>
|
||||
<playerView :class="'player-view' + ( isLoggedIntoAppleMusic ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
||||
<playerView :class="'player-view' + ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
||||
ref="player"></playerView>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,8 +18,10 @@
|
||||
import playerView from '@/components/playerView.vue';
|
||||
import libraryView from '@/components/libraryView.vue';
|
||||
import { ref } from 'vue';
|
||||
import type { ReadFile } from '@/scripts/song';
|
||||
|
||||
const isLoggedIntoAppleMusic = ref( false );
|
||||
const isReady = ref( false );
|
||||
const isShowingFullScreenPlayer = ref( false );
|
||||
const player = ref( playerView );
|
||||
const playlists = ref( [] );
|
||||
@@ -37,6 +40,7 @@
|
||||
loginChecker = setInterval( () => {
|
||||
if ( player.value.getAuth()[ 0 ] ) {
|
||||
isLoggedIntoAppleMusic.value = true;
|
||||
isReady.value = true;
|
||||
player.value.getPlaylists( ( data ) => {
|
||||
playlists.value = data.data.data;
|
||||
} );
|
||||
@@ -49,13 +53,18 @@
|
||||
}
|
||||
|
||||
const skipLogin = () => {
|
||||
isLoggedIntoAppleMusic.value = true;
|
||||
isReady.value = true;
|
||||
isLoggedIntoAppleMusic.value = false;
|
||||
player.value.skipLogin();
|
||||
}
|
||||
|
||||
const selectPlaylist = ( id: string ) => {
|
||||
player.value.selectPlaylist( id );
|
||||
}
|
||||
|
||||
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
||||
player.value.selectCustomPlaylist( playlist );
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||
<h1>MusicPlayer</h1>
|
||||
<button @click="login()" class="fancy-button">Log in</button>
|
||||
<button @click="login()" class="fancy-button">Log in / Sign up</button>
|
||||
<p>MusicPlayer is a browser based Music Player, that allows you to connect other devices, simply with another web-browser, where you can see the current playlist with sleek animations. You can log in using your Apple Music account or load a playlist from your local disk, simply by selecting the songs using a file picker.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -28,5 +28,6 @@ import router from '@/router';
|
||||
|
||||
.logo {
|
||||
height: 50vh;
|
||||
border-radius: 50px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user