mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 04:54:23 +00:00
Compare commits
8 Commits
d63df5898b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 576b6f9490 | |||
| e0dcfa6964 | |||
|
|
69d2db8c37 | ||
| 9a347c9206 | |||
| 60ea4669ab | |||
| 669cc620bf | |||
| 64d086dec4 | |||
| 1d714da494 |
@@ -1,19 +1,193 @@
|
|||||||
{
|
{
|
||||||
"ages": {
|
"ages": {
|
||||||
"below": "red",
|
"below": "Orange",
|
||||||
"16-18": "",
|
"16-18": "Yellow",
|
||||||
"18+": ""
|
"18+": "Turquoise"
|
||||||
},
|
},
|
||||||
"offering": {
|
"offering": {
|
||||||
"test": {
|
"big-bar": {
|
||||||
"name": "Test drink",
|
"offering": {
|
||||||
"price": 700,
|
"softdrinks": {
|
||||||
"id": "test"
|
"name": "Softdrinks",
|
||||||
|
"price": 400,
|
||||||
|
"id": "softdrinks"
|
||||||
|
},
|
||||||
|
"energy": {
|
||||||
|
"name": "Energy",
|
||||||
|
"price": 400,
|
||||||
|
"id": "energy"
|
||||||
|
},
|
||||||
|
"mate": {
|
||||||
|
"name": "Mate",
|
||||||
|
"price": 500,
|
||||||
|
"id": "mate",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"sparkly-water": {
|
||||||
|
"name": "Mineralwasser mit",
|
||||||
|
"price": 300,
|
||||||
|
"id": "sparkly-water",
|
||||||
|
"showLine": true
|
||||||
|
},
|
||||||
|
"rose": {
|
||||||
|
"name": "Rosé",
|
||||||
|
"price": 1500,
|
||||||
|
"id": "rose",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"name": "Rotwein",
|
||||||
|
"price": 2000,
|
||||||
|
"id": "red",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"wine-glasses": {
|
||||||
|
"name": "Weingläser",
|
||||||
|
"price": 0,
|
||||||
|
"id": "wine-glasses",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"appenzeller": {
|
||||||
|
"name": "Appenzeller Vollmond",
|
||||||
|
"price": 500,
|
||||||
|
"id": "appenzeller",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"feldschloesschen": {
|
||||||
|
"name": "Feldschlösschen",
|
||||||
|
"price": 500,
|
||||||
|
"id": "feldschloesschen",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"sommersby": {
|
||||||
|
"name": "Sommersby",
|
||||||
|
"price": 500,
|
||||||
|
"id": "sommersby",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"jever-fun": {
|
||||||
|
"name": "Jever Fun",
|
||||||
|
"price": 400,
|
||||||
|
"id": "jever-fun",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"trojka-ice": {
|
||||||
|
"name": "Trojka Ice",
|
||||||
|
"price": 600,
|
||||||
|
"id": "trojka-ice",
|
||||||
|
"depot": 200,
|
||||||
|
"showLine": true
|
||||||
|
},
|
||||||
|
"vodka-red-energy": {
|
||||||
|
"name": "Vodka Rot Energy",
|
||||||
|
"price": 800,
|
||||||
|
"id": "vodka-red-energy"
|
||||||
|
},
|
||||||
|
"vodka-green-citro": {
|
||||||
|
"name": "Vodka Grün Citro",
|
||||||
|
"price": 800,
|
||||||
|
"id": "vodka-green-citro"
|
||||||
|
},
|
||||||
|
"vodka-white-energy": {
|
||||||
|
"name": "Vodka White Energy",
|
||||||
|
"price": 900,
|
||||||
|
"id": "vodka-white-energy"
|
||||||
|
},
|
||||||
|
"gin-tonic": {
|
||||||
|
"name": "Gin Tonic",
|
||||||
|
"price": 900,
|
||||||
|
"id": "gin-tonic"
|
||||||
|
},
|
||||||
|
"rum-cola": {
|
||||||
|
"name": "Rum Cola",
|
||||||
|
"price": 900,
|
||||||
|
"id": "rum-cola"
|
||||||
|
},
|
||||||
|
"whiskey-cola": {
|
||||||
|
"name": "Whiskey Cola",
|
||||||
|
"price": 900,
|
||||||
|
"id": "whiskey-cola"
|
||||||
|
},
|
||||||
|
"mate-mit-schuss": {
|
||||||
|
"name": "Mate mit Schuss",
|
||||||
|
"price": 1200,
|
||||||
|
"id": "mate-mit-schuss",
|
||||||
|
"depot": 200,
|
||||||
|
"showLine": true
|
||||||
|
},
|
||||||
|
"poseidon": {
|
||||||
|
"name": "Poseidon",
|
||||||
|
"price": 900,
|
||||||
|
"id": "poseidon"
|
||||||
|
},
|
||||||
|
"arielle": {
|
||||||
|
"name": "Arielle",
|
||||||
|
"price": 900,
|
||||||
|
"id": "arielle"
|
||||||
|
},
|
||||||
|
"pearl-driver": {
|
||||||
|
"name": "Pearl Driver",
|
||||||
|
"price": 400,
|
||||||
|
"id": "pearl-driver"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Poseidon's Quelle",
|
||||||
|
"id": "big-bar"
|
||||||
},
|
},
|
||||||
"test-2": {
|
"small-bar": {
|
||||||
"name": "Test drink 2",
|
"offering": {
|
||||||
"price": 500,
|
"softdrinks": {
|
||||||
"id": "test-2"
|
"name": "Softdrinks (Alle)",
|
||||||
|
"price": 300,
|
||||||
|
"id": "softdrinks",
|
||||||
|
"showLine": true
|
||||||
|
},
|
||||||
|
"appenzeller": {
|
||||||
|
"name": "Appenzeller Vollmond",
|
||||||
|
"price": 500,
|
||||||
|
"id": "appenzeller",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"feldschloesschen": {
|
||||||
|
"name": "Feldschlösschen",
|
||||||
|
"price": 500,
|
||||||
|
"id": "feldschloesschen",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"sommersby": {
|
||||||
|
"name": "Sommersby",
|
||||||
|
"price": 500,
|
||||||
|
"id": "sommersby",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"jever-fun": {
|
||||||
|
"name": "Jever Fun",
|
||||||
|
"price": 400,
|
||||||
|
"id": "jever-fun",
|
||||||
|
"depot": 200,
|
||||||
|
"showLine": true
|
||||||
|
},
|
||||||
|
"rose": {
|
||||||
|
"name": "Rosé",
|
||||||
|
"price": 1500,
|
||||||
|
"id": "rose",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"name": "Rotwein",
|
||||||
|
"price": 2000,
|
||||||
|
"id": "red",
|
||||||
|
"depot": 200
|
||||||
|
},
|
||||||
|
"wine-glasses": {
|
||||||
|
"name": "Weingläser",
|
||||||
|
"price": 0,
|
||||||
|
"id": "wine-glasses",
|
||||||
|
"depot": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Seepferdchenbar",
|
||||||
|
"id": "small-bar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,23 +431,46 @@
|
|||||||
parseBlob( blob )
|
parseBlob( blob )
|
||||||
.then( data => {
|
.then( data => {
|
||||||
try {
|
try {
|
||||||
player.findSongOnAppleMusic( data.common.title
|
const searchTerm = data.common.title
|
||||||
?? songDetails.filename.split( '.' )[ 0 ] )
|
? data.common.title + ( data.common.artist ? ' ' + data.common.artist : '' )
|
||||||
|
: songDetails.filename.split( '.' )[ 0 ].replace( '_', ' ' );
|
||||||
|
|
||||||
|
|
||||||
|
player.findSongOnAppleMusic( searchTerm )
|
||||||
.then( d => {
|
.then( d => {
|
||||||
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
|
if ( d.data.results.songs ) {
|
||||||
|
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 ) );
|
console.debug(
|
||||||
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
|
'Result used for', searchTerm, 'is', d.data.results.songs.data[0]
|
||||||
const song: Song = {
|
);
|
||||||
'artist': d.data.results.songs.data[ 0 ].attributes.artistName,
|
|
||||||
'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 );
|
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': d.data.results.songs.data[ 0 ].attributes.artistName,
|
||||||
|
'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 );
|
||||||
|
} else {
|
||||||
|
const song: Song = {
|
||||||
|
'artist': data.common.artist ?? 'Unknown artist',
|
||||||
|
'title': data.common.title ?? 'Unknown song title',
|
||||||
|
'duration': data.format.duration ?? 1000,
|
||||||
|
'id': songDetails.url,
|
||||||
|
'origin': 'disk',
|
||||||
|
'cover': ''
|
||||||
|
};
|
||||||
|
|
||||||
|
console.warn( 'No results found for', searchTerm );
|
||||||
|
|
||||||
|
resolve( song );
|
||||||
|
}
|
||||||
} )
|
} )
|
||||||
.catch( e => {
|
.catch( e => {
|
||||||
console.error( e );
|
console.error( e );
|
||||||
@@ -560,7 +583,7 @@
|
|||||||
prepNiceDurationTime( playingSong );
|
prepNiceDurationTime( playingSong );
|
||||||
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
||||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
notificationHandler.emit( 'playback-update', isPlaying.value );
|
||||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
notificationHandler.emit( 'playback-start-update', new Date().getTime() - ( pos.value * 1000 ) );
|
||||||
hasStarted = true;
|
hasStarted = true;
|
||||||
}, 2000 );
|
}, 2000 );
|
||||||
}
|
}
|
||||||
@@ -574,7 +597,7 @@
|
|||||||
nicePlaybackPos.value = '0' + minuteCount + ':';
|
nicePlaybackPos.value = '0' + minuteCount + ':';
|
||||||
}
|
}
|
||||||
|
|
||||||
const secondCount = Math.floor( pos.value - minuteCount * 60 );
|
const secondCount = Math.floor( pos.value - ( minuteCount * 60 ) );
|
||||||
|
|
||||||
if ( ( '' + secondCount ).length === 1 ) {
|
if ( ( '' + secondCount ).length === 1 ) {
|
||||||
nicePlaybackPos.value += '0' + secondCount;
|
nicePlaybackPos.value += '0' + secondCount;
|
||||||
@@ -591,7 +614,7 @@
|
|||||||
niceDuration.value = '-0' + minuteCounts + ':';
|
niceDuration.value = '-0' + minuteCounts + ':';
|
||||||
}
|
}
|
||||||
|
|
||||||
const secondCounts = Math.floor( ( playingSong.duration - pos.value ) - minuteCounts * 60 );
|
const secondCounts = Math.floor( ( playingSong.duration - pos.value ) - ( minuteCounts * 60 ) );
|
||||||
|
|
||||||
if ( ( '' + secondCounts ).length === 1 ) {
|
if ( ( '' + secondCounts ).length === 1 ) {
|
||||||
niceDuration.value += '0' + secondCounts;
|
niceDuration.value += '0' + secondCounts;
|
||||||
|
|||||||
@@ -1,88 +1,153 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Queue</h1>
|
<h1>Queue</h1>
|
||||||
<input type="file" multiple accept="audio/*" id="more-songs" class="small-buttons">
|
<input
|
||||||
<button @click="addNewSongs()" class="small-buttons" title="Load selected files"><span class="material-symbols-outlined">upload</span></button>
|
id="more-songs"
|
||||||
<button @click="openSearch()" v-if="$props.isLoggedIntoAppleMusic" class="small-buttons" title="Search Apple Music for the song"><span class="material-symbols-outlined">search</span></button>
|
type="file"
|
||||||
<button @click="clearPlaylist()" class="small-buttons" title="Clear the playlist"><span class="material-symbols-outlined">delete</span></button>
|
multiple
|
||||||
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()"><span class="material-symbols-outlined">send</span></button>
|
accept="audio/*"
|
||||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
|
class="small-buttons"
|
||||||
<div class="playlist-box" id="pl-box">
|
>
|
||||||
|
<button class="small-buttons" title="Load selected files" @click="addNewSongs()">
|
||||||
|
<span class="material-symbols-outlined">upload</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="$props.isLoggedIntoAppleMusic"
|
||||||
|
class="small-buttons"
|
||||||
|
title="Search Apple Music for the song"
|
||||||
|
@click="openSearch()"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">search</span>
|
||||||
|
</button>
|
||||||
|
<button class="small-buttons" title="Clear the playlist" @click="clearPlaylist()">
|
||||||
|
<span class="material-symbols-outlined">delete</span>
|
||||||
|
</button>
|
||||||
|
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()">
|
||||||
|
<span class="material-symbols-outlined">send</span>
|
||||||
|
</button>
|
||||||
|
<p v-if="!hasSelectedSongs">
|
||||||
|
Please select at least one song to proceed
|
||||||
|
</p>
|
||||||
|
<div id="pl-box" class="playlist-box">
|
||||||
<!-- TODO: Allow editing additionalInfo. Think also how to make it persist over reloads... Export to JSON and then best-guess add them? Very easy for Apple Music 'cause ID, but how for local songs? Maybe using retrieved ID from Apple Music? -->
|
<!-- TODO: Allow editing additionalInfo. Think also how to make it persist over reloads... Export to JSON and then best-guess add them? Very easy for Apple Music 'cause ID, but how for local songs? Maybe using retrieved ID from Apple Music? -->
|
||||||
<!-- TODO: Handle long AppleMusic Playlists, as AppleMusic doesn't automatically load all songs of a playlist -->
|
<!-- TODO: Handle long AppleMusic Playlists, as AppleMusic doesn't automatically load all songs of a playlist -->
|
||||||
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
|
<div
|
||||||
|
v-for="song in computedPlaylist"
|
||||||
|
:key="song.id"
|
||||||
|
class="song"
|
||||||
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
|
: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' : '' )">
|
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )"
|
||||||
<img :src="song.cover" alt="Song cover" class="song-cover">
|
>
|
||||||
|
<img
|
||||||
|
v-if="song.cover"
|
||||||
|
:src="song.cover"
|
||||||
|
alt="Song cover"
|
||||||
|
class="song-cover"
|
||||||
|
>
|
||||||
|
<span v-else class="material-symbols-outlined song-cover">music_note</span>
|
||||||
<div v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && $props.isPlaying" class="playing-symbols">
|
<div v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && $props.isPlaying" class="playing-symbols">
|
||||||
<div class="playing-symbols-wrapper">
|
<div class="playing-symbols-wrapper">
|
||||||
<div class="playing-bar" id="bar-1"></div>
|
<div id="bar-1" class="playing-bar"></div>
|
||||||
<div class="playing-bar" id="bar-2"></div>
|
<div id="bar-2" class="playing-bar"></div>
|
||||||
<div class="playing-bar" id="bar-3"></div>
|
<div id="bar-3" class="playing-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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 v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )" class="material-symbols-outlined play-icon" @click="control( 'play' )">play_arrow</span>
|
||||||
<span class="material-symbols-outlined play-icon" @click="play( song.id )" v-else>play_arrow</span>
|
<span v-else class="material-symbols-outlined play-icon" @click="play( song.id )">play_arrow</span>
|
||||||
<span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</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
|
||||||
<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>
|
v-if="canBeMoved( 'up', song.id )"
|
||||||
<h3 class="song-title">{{ song.title }}</h3>
|
class="material-symbols-outlined move-icon"
|
||||||
|
title="Move song up"
|
||||||
|
@click="moveSong( song.id, 'up' )"
|
||||||
|
>arrow_upward</span>
|
||||||
|
<span
|
||||||
|
v-if="canBeMoved( 'down', song.id )"
|
||||||
|
class="material-symbols-outlined move-icon"
|
||||||
|
title="Move song down"
|
||||||
|
@click="moveSong( song.id, 'down' )"
|
||||||
|
>arrow_downward</span>
|
||||||
|
<h3 class="song-title">
|
||||||
|
{{ song.title }}
|
||||||
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<input type="text" placeholder="Additional information for remote display" title="Additional information for remote display" v-model="song.additionalInfo" @focusin="kbControl( 'on' )" @focusout="kbControl( 'off' )">
|
<input
|
||||||
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
|
v-model="song.additionalInfo"
|
||||||
|
type="text"
|
||||||
|
placeholder="Additional information for remote display"
|
||||||
|
title="Additional information for remote display"
|
||||||
|
@focusin="kbControl( 'on' )"
|
||||||
|
@focusout="kbControl( 'off' )"
|
||||||
|
>
|
||||||
|
<p class="playing-in">
|
||||||
|
{{ getTimeUntil( song ) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="deleteSong( song.id )" class="small-buttons" title="Remove this song from the queue" v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"><span class="material-symbols-outlined">delete</span></button>
|
<button
|
||||||
|
v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"
|
||||||
|
class="small-buttons"
|
||||||
|
title="Remove this song from the queue"
|
||||||
|
@click="deleteSong( song.id )"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">delete</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }"></searchView>
|
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// TODO: Add logout button
|
// TODO: Add logout button
|
||||||
import type { AppleMusicSongData, ReadFile, Song } from '@/scripts/song';
|
import type {
|
||||||
import { computed, ref } from 'vue';
|
AppleMusicSongData, ReadFile, Song
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
computed, ref
|
||||||
|
} from 'vue';
|
||||||
import searchView from './searchView.vue';
|
import searchView from './searchView.vue';
|
||||||
import { useUserStore } from '@/stores/userStore';
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/stores/userStore';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const search = ref( searchView );
|
const search = ref( searchView );
|
||||||
const props = defineProps( {
|
const props = defineProps( {
|
||||||
'playlist': {
|
'playlist': {
|
||||||
default: [],
|
'default': [],
|
||||||
required: true,
|
'required': true,
|
||||||
type: Array<Song>
|
'type': Array<Song>
|
||||||
},
|
},
|
||||||
'currentlyPlaying': {
|
'currentlyPlaying': {
|
||||||
default: 0,
|
'default': 0,
|
||||||
required: true,
|
'required': true,
|
||||||
type: Number,
|
'type': Number,
|
||||||
},
|
},
|
||||||
'isPlaying': {
|
'isPlaying': {
|
||||||
default: true,
|
'default': true,
|
||||||
required: true,
|
'required': true,
|
||||||
type: Boolean,
|
'type': Boolean,
|
||||||
},
|
},
|
||||||
'pos': {
|
'pos': {
|
||||||
default: 0,
|
'default': 0,
|
||||||
required: false,
|
'required': false,
|
||||||
type: Number,
|
'type': Number,
|
||||||
},
|
},
|
||||||
'isLoggedIntoAppleMusic': {
|
'isLoggedIntoAppleMusic': {
|
||||||
default: false,
|
'default': false,
|
||||||
required: true,
|
'required': true,
|
||||||
type: Boolean,
|
'type': Boolean,
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
const hasSelectedSongs = ref( true );
|
const hasSelectedSongs = ref( true );
|
||||||
|
|
||||||
const computedPlaylist = computed( () => {
|
const computedPlaylist = computed( () => {
|
||||||
let pl: Song[] = [];
|
let pl: Song[] = [];
|
||||||
|
|
||||||
// ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } );
|
// ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } );
|
||||||
for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) {
|
for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) {
|
||||||
pl.push( props.playlist[ i ] );
|
pl.push( props.playlist[ i ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
return pl;
|
return pl;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
@@ -92,60 +157,66 @@
|
|||||||
} else {
|
} else {
|
||||||
userStore.setKeyboardUsageStatus( true );
|
userStore.setKeyboardUsageStatus( true );
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const openSearch = () => {
|
const openSearch = () => {
|
||||||
if ( search.value ) {
|
if ( search.value ) {
|
||||||
search.value.controlSearch( 'show' );
|
search.value.controlSearch( 'show' );
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const canBeMoved = computed( () => {
|
const canBeMoved = computed( () => {
|
||||||
return ( direction: movementDirection, songID: string ): boolean => {
|
return ( direction: movementDirection, songID: string ): boolean => {
|
||||||
let id = 0;
|
let id = 0;
|
||||||
|
|
||||||
for ( let song in props.playlist ) {
|
for ( let song in props.playlist ) {
|
||||||
if ( props.playlist[ song ].id === songID ) {
|
if ( props.playlist[ song ].id === songID ) {
|
||||||
id = parseInt( song );
|
id = parseInt( song );
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( direction === 'up' ) {
|
if ( direction === 'up' ) {
|
||||||
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
|
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
|
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
} )
|
} );
|
||||||
|
|
||||||
const getTimeUntil = computed( () => {
|
const getTimeUntil = computed( () => {
|
||||||
return ( song: Song ) => {
|
return ( song: Song ) => {
|
||||||
let timeRemaining = 0;
|
let timeRemaining = 0;
|
||||||
|
|
||||||
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
|
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
|
||||||
if ( props.playlist[ i ] == song ) {
|
if ( props.playlist[ i ] == song ) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRemaining += props.playlist[ i ].duration;
|
timeRemaining += props.playlist[ i ].duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( props.isPlaying ) {
|
if ( props.isPlaying ) {
|
||||||
if ( timeRemaining === 0 ) {
|
if ( timeRemaining === 0 ) {
|
||||||
return 'Currently playing';
|
return 'Currently playing';
|
||||||
} else {
|
} else {
|
||||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
|
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ( timeRemaining === 0 ) {
|
if ( timeRemaining === 0 ) {
|
||||||
return 'Plays next';
|
return 'Plays next';
|
||||||
} else {
|
} else {
|
||||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
|
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const deleteSong = ( songID: string ) => {
|
const deleteSong = ( songID: string ) => {
|
||||||
@@ -154,69 +225,92 @@
|
|||||||
emits( 'delete-song', parseInt( song ) );
|
emits( 'delete-song', parseInt( song ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const clearPlaylist = () => {
|
const clearPlaylist = () => {
|
||||||
emits( 'clear-playlist', '' );
|
emits( 'clear-playlist', '' );
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const control = ( action: string ) => {
|
const control = ( action: string ) => {
|
||||||
emits( 'control', action );
|
emits( 'control', action );
|
||||||
}
|
};
|
||||||
|
|
||||||
const play = ( song: string ) => {
|
const play = ( song: string ) => {
|
||||||
emits( 'play-song', song );
|
emits( 'play-song', song );
|
||||||
}
|
};
|
||||||
|
|
||||||
const addNewSongs = () => {
|
const addNewSongs = () => {
|
||||||
const fileURLList: ReadFile[] = [];
|
const fileURLList: ReadFile[] = [];
|
||||||
const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
|
const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
|
||||||
|
|
||||||
if ( allFiles.length > 0 ) {
|
if ( allFiles.length > 0 ) {
|
||||||
hasSelectedSongs.value = true;
|
hasSelectedSongs.value = true;
|
||||||
|
|
||||||
for ( let file = 0; file < allFiles.length; file++ ) {
|
for ( let file = 0; file < allFiles.length; file++ ) {
|
||||||
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } );
|
fileURLList.push( {
|
||||||
|
'url': URL.createObjectURL( allFiles[ file ] ),
|
||||||
|
'filename': allFiles[ file ].name
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
emits( 'add-new-songs', fileURLList );
|
emits( 'add-new-songs', fileURLList );
|
||||||
} else {
|
} else {
|
||||||
hasSelectedSongs.value = false;
|
hasSelectedSongs.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
|
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
|
||||||
const song: Song = {
|
const song: Song = {
|
||||||
artist: songData.attributes.artistName,
|
'artist': songData.attributes.artistName,
|
||||||
cover: songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
'cover': songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
||||||
duration: songData.attributes.durationInMillis / 1000,
|
'duration': songData.attributes.durationInMillis / 1000,
|
||||||
id: songData.id,
|
'id': songData.id,
|
||||||
origin: 'apple-music',
|
'origin': 'apple-music',
|
||||||
title: songData.attributes.name
|
'title': songData.attributes.name
|
||||||
}
|
};
|
||||||
|
|
||||||
emits( 'add-new-songs-apple-music', song );
|
emits( 'add-new-songs-apple-music', song );
|
||||||
}
|
};
|
||||||
|
|
||||||
type movementDirection = 'up' | 'down';
|
type movementDirection = 'up' | 'down';
|
||||||
|
|
||||||
const moveSong = ( songID: string, direction: movementDirection ) => {
|
const moveSong = ( songID: string, direction: movementDirection ) => {
|
||||||
let newSongPos = 0;
|
let newSongPos = 0;
|
||||||
let hasFoundSongToMove = false;
|
let hasFoundSongToMove = false;
|
||||||
|
|
||||||
for ( let el in props.playlist ) {
|
for ( let el in props.playlist ) {
|
||||||
if ( props.playlist[ el ].id === songID ) {
|
if ( props.playlist[ el ].id === songID ) {
|
||||||
const currPos = parseInt( el );
|
const currPos = parseInt( el );
|
||||||
|
|
||||||
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
|
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
|
||||||
hasFoundSongToMove = true;
|
hasFoundSongToMove = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( hasFoundSongToMove ) {
|
if ( hasFoundSongToMove ) {
|
||||||
emits( 'playlist-reorder', { 'songID': songID, 'newPos': newSongPos } );
|
emits( 'playlist-reorder', {
|
||||||
|
'songID': songID,
|
||||||
|
'newPos': newSongPos
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const sendAdditionalInfo = () => {
|
const sendAdditionalInfo = () => {
|
||||||
emits( 'send-additional-info' );
|
emits( 'send-additional-info' );
|
||||||
}
|
};
|
||||||
|
|
||||||
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs', 'add-new-songs-apple-music', 'delete-song', 'clear-playlist', 'send-additional-info' ] );
|
const emits = defineEmits( [
|
||||||
|
'play-song',
|
||||||
|
'control',
|
||||||
|
'playlist-reorder',
|
||||||
|
'add-new-songs',
|
||||||
|
'add-new-songs-apple-music',
|
||||||
|
'delete-song',
|
||||||
|
'clear-playlist',
|
||||||
|
'send-additional-info'
|
||||||
|
] );
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -249,6 +343,10 @@
|
|||||||
font-size: 6rem;
|
font-size: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.song img.song-cover {
|
||||||
|
font-size: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.song-title {
|
.song-title {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
<!-- TODO: Make prettier -->
|
<!-- TODO: Make prettier -->
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!$props.isLoggedIn" class="not-logged-in">
|
<div v-else-if="!$props.isLoggedIn" class="not-logged-in">
|
||||||
<p>You are not logged into Apple Music. We therefore can't show you your playlists. <a href="" title="Refreshes the page, allowing you to log in">Change that</a></p>
|
<p>
|
||||||
|
You are not logged into Apple Music. We therefore can't show you your playlists.
|
||||||
|
<a href="" title="Refreshes the page, allowing you to log in">Change that</a>
|
||||||
|
</p>
|
||||||
<p>Use the button below to load songs from your local disk</p>
|
<p>Use the button below to load songs from your local disk</p>
|
||||||
<input
|
<input
|
||||||
id="pl-loader"
|
id="pl-loader"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// These functions handle connections to the backend with socket.io
|
// These functions handle connections to the backend with socket.io
|
||||||
|
|
||||||
import {
|
import {
|
||||||
io, type Socket
|
type Socket, io
|
||||||
} from 'socket.io-client';
|
} from 'socket.io-client';
|
||||||
import type {
|
import type {
|
||||||
SSEMap
|
SSEMap
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// These functions handle connections to the backend with socket.io
|
// These functions handle connections to the backend with socket.io
|
||||||
|
|
||||||
import {
|
import {
|
||||||
io, type Socket
|
type Socket, io
|
||||||
} from 'socket.io-client';
|
} from 'socket.io-client';
|
||||||
import type {
|
import type {
|
||||||
SSEMap
|
SSEMap
|
||||||
@@ -57,7 +57,8 @@ class NotificationHandler {
|
|||||||
*/
|
*/
|
||||||
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
|
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
|
||||||
return new Promise( ( resolve, reject ) => {
|
return new Promise( ( resolve, reject ) => {
|
||||||
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, {
|
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName='
|
||||||
|
+ roomName + '&useAntiTamper=' + useAntiTamper, {
|
||||||
'credentials': 'include'
|
'credentials': 'include'
|
||||||
} ).then( res => {
|
} ).then( res => {
|
||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
@@ -110,7 +111,8 @@ class NotificationHandler {
|
|||||||
'credentials': 'include'
|
'credentials': 'include'
|
||||||
} ).then( res => {
|
} ).then( res => {
|
||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, {
|
this.eventSource = new EventSource( localStorage.getItem( 'url' )
|
||||||
|
+ '/socket/connection?room=' + this.roomName, {
|
||||||
'withCredentials': true
|
'withCredentials': true
|
||||||
} );
|
} );
|
||||||
|
|
||||||
@@ -118,7 +120,8 @@ class NotificationHandler {
|
|||||||
this.isConnected = true;
|
this.isConnected = true;
|
||||||
this.connectionWasSuccessful = true;
|
this.connectionWasSuccessful = true;
|
||||||
this.reconnectRetryCount = 0;
|
this.reconnectRetryCount = 0;
|
||||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
console.log( '[ SSE Connection ] - '
|
||||||
|
+ new Date().toISOString() + ': Connection successfully established!' );
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,8 +138,10 @@ class NotificationHandler {
|
|||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
this.eventSource?.close();
|
this.eventSource?.close();
|
||||||
this.openConnectionsCount -= 1;
|
this.openConnectionsCount -= 1;
|
||||||
console.debug( e );
|
console.debug( '[ SSE Connection ] - Error encountered: ', e );
|
||||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
console.log( '[ SSE Connection ] - '
|
||||||
|
+ new Date().toISOString()
|
||||||
|
+ ': Reconnecting due to connection error!' );
|
||||||
|
|
||||||
this.eventSource = undefined;
|
this.eventSource = undefined;
|
||||||
|
|
||||||
@@ -146,7 +151,8 @@ class NotificationHandler {
|
|||||||
}, 1000 * this.reconnectRetryCount );
|
}, 1000 * this.reconnectRetryCount );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else if ( res.status === 403 || res.status === 401 || res.status === 404 || res.status === 402 ) {
|
} else if ( res.status === 403 || res.status === 401
|
||||||
|
|| res.status === 404 || res.status === 402 ) {
|
||||||
document.dispatchEvent( new Event( 'musicplayer:autherror' ) );
|
document.dispatchEvent( new Event( 'musicplayer:autherror' ) );
|
||||||
reject( 'ERR_UNAUTHORIZED' );
|
reject( 'ERR_UNAUTHORIZED' );
|
||||||
} else {
|
} else {
|
||||||
@@ -158,7 +164,8 @@ class NotificationHandler {
|
|||||||
reject( 'ERR_ROOM_CONNECTING' );
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
} else {
|
} else {
|
||||||
this.openConnectionsCount -= 1;
|
this.openConnectionsCount -= 1;
|
||||||
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to severe connection error!' );
|
console.log( '[ SSE Connection ] - ' + new Date().toISOString()
|
||||||
|
+ ': Reconnecting due to severe connection error!' );
|
||||||
|
|
||||||
this.eventSource = undefined;
|
this.eventSource = undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -5,22 +5,32 @@
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
interface FullConfig {
|
interface FullConfig {
|
||||||
'offering': BarConfig,
|
'offering': Bars;
|
||||||
'ages': Ages
|
'ages': Ages;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Bars {
|
||||||
|
[name: string]: {
|
||||||
|
'offering': BarConfig;
|
||||||
|
'name': string;
|
||||||
|
'id': string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Ages {
|
interface Ages {
|
||||||
'18+': string,
|
'18+': string;
|
||||||
'16-18': string
|
'16-18': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BarConfig {
|
interface BarConfig {
|
||||||
[id: string]: Offer;
|
[id: string]: Offer
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Offer {
|
interface Offer {
|
||||||
'name': string;
|
'name': string;
|
||||||
'price': number; // In cents
|
'price': number; // In cents
|
||||||
|
'depot'?: number; // In cents
|
||||||
|
'showLine'?: boolean;
|
||||||
'id': string;
|
'id': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,25 +43,38 @@
|
|||||||
'16-18': '',
|
'16-18': '',
|
||||||
'below': ''
|
'below': ''
|
||||||
} );
|
} );
|
||||||
const offering: Ref<BarConfig> = ref( {} );
|
const offering: Ref<Bars> = ref( {} );
|
||||||
const selection: Ref<Selection> = ref( {} );
|
const selection: Ref<Selection> = ref( {} );
|
||||||
|
const selectedBar: Ref<string> = ref( '' );
|
||||||
|
const enableDepotReminder = ref( true );
|
||||||
|
|
||||||
fetch( '/bar-config.json' ).then( res => {
|
let cashinInDepot = false;
|
||||||
|
|
||||||
|
fetch( '/bar-config.json', {
|
||||||
|
'cache': 'no-store'
|
||||||
|
} ).then( res => {
|
||||||
if ( res.status === 200 ) {
|
if ( res.status === 200 ) {
|
||||||
res.json().then( json => {
|
res.json().then( json => {
|
||||||
const data: FullConfig = json;
|
const data: FullConfig = json;
|
||||||
|
|
||||||
offering.value = data.offering;
|
offering.value = data.offering;
|
||||||
ages.value = data.ages;
|
ages.value = data.ages;
|
||||||
reset();
|
|
||||||
} );
|
} );
|
||||||
} else {
|
} else {
|
||||||
alert( 'Failed to load' );
|
alert( 'Failed to load' );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const reset = () => {
|
const reset = ( skipCheck = true ) => {
|
||||||
const keys = Object.keys( offering.value );
|
if ( !skipCheck && !Object.keys( offering.value ).includes( selectedBar.value ) ) return;
|
||||||
|
|
||||||
|
if ( cashinInDepot && enableDepotReminder.value ) alert( 'Hand out chips for depot' );
|
||||||
|
|
||||||
|
cashinInDepot = false;
|
||||||
|
|
||||||
|
const keys = Object.keys( offering.value[ selectedBar.value ].offering );
|
||||||
|
|
||||||
|
selection.value = {};
|
||||||
|
|
||||||
keys.forEach( val => {
|
keys.forEach( val => {
|
||||||
selection.value[ val ] = 0;
|
selection.value[ val ] = 0;
|
||||||
@@ -62,14 +85,22 @@
|
|||||||
const keys = Object.keys( selection.value );
|
const keys = Object.keys( selection.value );
|
||||||
|
|
||||||
let totalPrice = 0;
|
let totalPrice = 0;
|
||||||
|
let totalDepot = 0;
|
||||||
|
|
||||||
for ( let i = 0; i < keys.length; i++ ) {
|
for ( let i = 0; i < keys.length; i++ ) {
|
||||||
const o = selection.value[ keys[ i ] ];
|
const o = selection.value[ keys[ i ] ];
|
||||||
|
|
||||||
totalPrice += o * offering.value[ keys[ i ] ].price;
|
totalPrice += o * offering.value[ selectedBar.value ].offering[ keys[ i ] ].price;
|
||||||
|
totalDepot += o * ( offering.value[ selectedBar.value ].offering[ keys[ i ] ].depot ?? 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalPrice / 100;
|
if ( totalDepot > 0 ) {
|
||||||
|
cashinInDepot = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPrice += totalDepot;
|
||||||
|
|
||||||
|
return ( totalPrice / 100 ) + ( totalDepot ? ` (Depot = ${ totalDepot })` : '' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const changeValue = ( id: string, amount: number ) => {
|
const changeValue = ( id: string, amount: number ) => {
|
||||||
@@ -83,17 +114,40 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bar-utility">
|
<div class="bar-utility">
|
||||||
<h1>Bar utility</h1>
|
<div style="margin: 0">
|
||||||
|
<label> Depot chips reminder</label>
|
||||||
|
<input v-model="enableDepotReminder" type="checkbox">
|
||||||
|
</div>
|
||||||
|
<h1 style="margin: 15px;">
|
||||||
|
Bar utility
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
<label for="bar-select">Select bar </label>
|
||||||
|
<select id="bar-select" v-model="selectedBar" @change="reset()">
|
||||||
|
<option v-for="bar in Object.values( offering )" :key="bar.id" :value="bar.id">
|
||||||
|
{{ bar.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button @click="reset( false )">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p>Check ages! (18+: {{ ages[ '18+' ] }}, 16-18: {{ ages[ '16-18' ] }})</p>
|
<p>Check ages! (18+: {{ ages[ '18+' ] }}, 16-18: {{ ages[ '16-18' ] }})</p>
|
||||||
<button @click="reset()">
|
<p v-if="Object.keys( offering ).includes( selectedBar )">
|
||||||
Reset
|
Total: CHF {{ total }}
|
||||||
</button>
|
</p>
|
||||||
<p>Total: CHF {{ total }}</p>
|
<table v-if="Object.keys( offering ).includes( selectedBar )" class="offering-wrapper">
|
||||||
<table class="offering-wrapper">
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="offer in offering" :key="offer.id" class="offering">
|
<tr
|
||||||
|
v-for="offer in offering[ selectedBar ].offering"
|
||||||
|
:key="offer.id"
|
||||||
|
:class="[ 'offering', offer.showLine ? 'show-line' : '' ]"
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
<p>{{ offer.name }} (CHF {{ offer.price / 100 }})</p>
|
<p>
|
||||||
|
{{ offer.name }} (CHF {{ offer.price / 100 }}{{
|
||||||
|
offer.depot ? ' + ' + ( offer.depot / 100 ) : '' }})
|
||||||
|
</p>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>
|
<div>
|
||||||
@@ -109,6 +163,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<p v-if="Object.keys( offering ).includes( selectedBar )">
|
||||||
|
Total: CHF {{ total }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -120,7 +177,16 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
>.offering-wrapper {
|
>.offering-wrapper {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 5vh;
|
||||||
|
|
||||||
.offering {
|
.offering {
|
||||||
|
&.show-line {
|
||||||
|
>td {
|
||||||
|
border-bottom: solid 1px black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
>td {
|
>td {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
p {
|
p {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
||||||
<div class="current-song-wrapper">
|
<div class="current-song-wrapper">
|
||||||
<img
|
<img
|
||||||
v-if="playlist[ playingSong ]"
|
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
|
||||||
id="current-image"
|
id="current-image"
|
||||||
:src="playlist[ playingSong ].cover"
|
:src="playlist[ playingSong ].cover"
|
||||||
class="fancy-view-song-art"
|
class="fancy-view-song-art"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
||||||
<div class="current-song-wrapper">
|
<div class="current-song-wrapper">
|
||||||
<img
|
<img
|
||||||
v-if="playlist[ playingSong ]"
|
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
|
||||||
id="current-image"
|
id="current-image"
|
||||||
:src="playlist[ playingSong ].cover"
|
:src="playlist[ playingSong ].cover"
|
||||||
class="fancy-view-song-art"
|
class="fancy-view-song-art"
|
||||||
@@ -52,7 +52,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="song-list-wrapper">
|
<div class="song-list-wrapper">
|
||||||
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||||
<img :src="song.cover" class="song-image">
|
<img v-if="song.cover" :src="song.cover" class="song-image">
|
||||||
|
<span v-else class="material-symbols-outlined song-cover">music_note</span>
|
||||||
<div
|
<div
|
||||||
v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying"
|
v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying"
|
||||||
class="playing-symbols"
|
class="playing-symbols"
|
||||||
@@ -97,7 +98,7 @@
|
|||||||
Song
|
Song
|
||||||
} from '@/scripts/song';
|
} from '@/scripts/song';
|
||||||
import {
|
import {
|
||||||
computed, ref, type Ref
|
type Ref, computed, ref
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import bizualizer from '@/scripts/bizualizer';
|
import bizualizer from '@/scripts/bizualizer';
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import storeSDK from '@janishutz/store-sdk';
|
|||||||
import sdk from '@janishutz/login-sdk-server';
|
import sdk from '@janishutz/login-sdk-server';
|
||||||
import sse from './sse';
|
import sse from './sse';
|
||||||
import socket from './socket';
|
import socket from './socket';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
// const isFossVersion = true;
|
// const isFossVersion = true;
|
||||||
//
|
//
|
||||||
@@ -42,15 +43,13 @@ const run = () => {
|
|||||||
const httpServer = createServer( app );
|
const httpServer = createServer( app );
|
||||||
|
|
||||||
if ( !isFossVersion ) {
|
if ( !isFossVersion ) {
|
||||||
console.error( '[ APP ] Starting in non-FOSS version' );
|
logger.info( '[ APP ] Starting in non-FOSS version' );
|
||||||
|
|
||||||
const storeConfig = JSON.parse( fs.readFileSync( path.join(
|
const storeConfig = JSON.parse( fs.readFileSync( path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'/config/store-sdk.config.secret.json'
|
'/config/store-sdk.config.secret.json'
|
||||||
) ).toString() );
|
) ).toString() );
|
||||||
|
|
||||||
console.error( storeConfig );
|
|
||||||
|
|
||||||
storeSDK.configure( storeConfig );
|
storeSDK.configure( storeConfig );
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────
|
||||||
@@ -139,6 +138,7 @@ const run = () => {
|
|||||||
'useAntiTamper': request.query.useAntiTamper === 'true'
|
'useAntiTamper': request.query.useAntiTamper === 'true'
|
||||||
? true : false,
|
? true : false,
|
||||||
};
|
};
|
||||||
|
logger.debug( `Created room "${ roomName }"` );
|
||||||
response.send( roomToken );
|
response.send( roomToken );
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
@@ -211,7 +211,7 @@ const run = () => {
|
|||||||
} else {
|
} else {
|
||||||
storeSDK.getSubscriptions( uid )
|
storeSDK.getSubscriptions( uid )
|
||||||
.then( stat => {
|
.then( stat => {
|
||||||
console.error( 'Subscription check was successful' );
|
logger.log( 'Subscription check was successful' );
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
|
|
||||||
for ( const sub in stat ) {
|
for ( const sub in stat ) {
|
||||||
@@ -232,7 +232,7 @@ const run = () => {
|
|||||||
resolve( false );
|
resolve( false );
|
||||||
} )
|
} )
|
||||||
.catch( e => {
|
.catch( e => {
|
||||||
console.error( 'Subscription check unsuccessful with error', e );
|
logger.error( 'Subscription check unsuccessful with error', e );
|
||||||
reject( 'ERR_NOT_OWNED' );
|
reject( 'ERR_NOT_OWNED' );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|||||||
104
backend/src/logger.ts
Normal file
104
backend/src/logger.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
writeFile
|
||||||
|
} from 'node:fs';
|
||||||
|
|
||||||
|
const log = ( ...msg: string[] ) => {
|
||||||
|
output( 'log', log.caller.toString(), ...msg );
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = ( ...msg: string[] ) => {
|
||||||
|
output( 'info', log.caller.toString(), ...msg );
|
||||||
|
};
|
||||||
|
|
||||||
|
const debug = ( ...msg: string[] ) => {
|
||||||
|
output( 'debug', log.caller.toString(), ...msg );
|
||||||
|
};
|
||||||
|
|
||||||
|
const warn = ( ...msg: string[] ) => {
|
||||||
|
output( 'warn', log.caller.toString(), ...msg );
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = ( ...msg: string[] ) => {
|
||||||
|
output( 'error', log.caller.toString(), ...msg );
|
||||||
|
};
|
||||||
|
|
||||||
|
const fatal = ( ...msg: string[] ) => {
|
||||||
|
output( 'fatal', log.caller.toString(), ...msg );
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let loc = 'stderr';
|
||||||
|
let lev = 0;
|
||||||
|
type LogLevel = 'debug' | 'info' | 'log' | 'warn' | 'error' | 'fatal';
|
||||||
|
const levels = [
|
||||||
|
'debug',
|
||||||
|
'info',
|
||||||
|
'log',
|
||||||
|
'warn',
|
||||||
|
'error',
|
||||||
|
'fatal'
|
||||||
|
];
|
||||||
|
|
||||||
|
const configure = ( location: 'stderr' | 'file', minLevel: LogLevel, file?: string ) => {
|
||||||
|
if ( location === 'file' && !file ) {
|
||||||
|
throw new Error( 'File parameter required when location is "file"' );
|
||||||
|
}
|
||||||
|
|
||||||
|
loc = location === 'stderr' ? 'stderr' : file;
|
||||||
|
lev = levels.indexOf( minLevel );
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const logfile: string[] = [];
|
||||||
|
|
||||||
|
const output = ( level: LogLevel, caller: string, ...message: string[] ) => {
|
||||||
|
if ( levels.indexOf( level ) < lev ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = message.join( ' ' );
|
||||||
|
const out = `[${ level.toUpperCase() }] (${ new Date().toISOString() }) in ${ caller }: ${ msg }`;
|
||||||
|
|
||||||
|
if ( loc === 'stderr' ) {
|
||||||
|
console.error( out );
|
||||||
|
} else {
|
||||||
|
logfile.push( out );
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let isSaving = false;
|
||||||
|
let waitingOnSave = false;
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if ( isSaving ) {
|
||||||
|
waitingOnSave = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving = true;
|
||||||
|
writeFile( loc, JSON.stringify( logfile ), err => {
|
||||||
|
if ( err )
|
||||||
|
console.error( '[LOGGER] Failed to save with error ' + err );
|
||||||
|
|
||||||
|
if ( waitingOnSave ) {
|
||||||
|
waitingOnSave = false;
|
||||||
|
isSaving = false;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving = false;
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
log,
|
||||||
|
info,
|
||||||
|
debug,
|
||||||
|
warn,
|
||||||
|
error,
|
||||||
|
fatal,
|
||||||
|
configure
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import bodyParser from 'body-parser';
|
|||||||
import {
|
import {
|
||||||
SocketData
|
SocketData
|
||||||
} from './definitions';
|
} from './definitions';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
const useSSE = (
|
const useSSE = (
|
||||||
app: express.Application,
|
app: express.Application,
|
||||||
@@ -86,7 +87,11 @@ const useSSE = (
|
|||||||
|
|
||||||
for ( const c in cl ) {
|
for ( const c in cl ) {
|
||||||
if ( cl[ c ] === sid ) {
|
if ( cl[ c ] === sid ) {
|
||||||
cl.splice( parseInt( c ), 1 );
|
try {
|
||||||
|
cl.splice( parseInt( c ), 1 );
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch ( _ ) { /* empty */ }
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,12 +125,13 @@ const useSSE = (
|
|||||||
( request: express.Request, response: express.Response ) => {
|
( request: express.Request, response: express.Response ) => {
|
||||||
if ( request.query.room ) {
|
if ( request.query.room ) {
|
||||||
if ( socketData[ String( request.query.room ) ] ) {
|
if ( socketData[ String( request.query.room ) ] ) {
|
||||||
|
logger.debug( `Room "${ request.query.room }" was joined` );
|
||||||
response.send( 'ok' );
|
response.send( 'ok' );
|
||||||
} else {
|
} else {
|
||||||
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response.status( 404 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
response.status( 400 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -138,8 +144,17 @@ const useSSE = (
|
|||||||
( request: express.Request, response: express.Response ) => {
|
( request: express.Request, response: express.Response ) => {
|
||||||
if ( socketData[ request.body.roomName ] ) {
|
if ( socketData[ request.body.roomName ] ) {
|
||||||
if ( request.body.event === 'tampering' ) {
|
if ( request.body.event === 'tampering' ) {
|
||||||
|
logger.debug( `Room "${
|
||||||
|
request.query.roomName }" has new event: Tampering` );
|
||||||
|
|
||||||
const clients = clientReference[ request.body.roomName ];
|
const clients = clientReference[ request.body.roomName ];
|
||||||
|
|
||||||
|
if ( !clients ) {
|
||||||
|
response.send( 'ERR_CANNOT_SEND' );
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for ( const client in clients ) {
|
for ( const client in clients ) {
|
||||||
if ( importantClients[ clients[ client ] ] ) {
|
if ( importantClients[ clients[ client ] ] ) {
|
||||||
importantClients[ clients[ client ] ]
|
importantClients[ clients[ client ] ]
|
||||||
@@ -181,9 +196,18 @@ const useSSE = (
|
|||||||
.playlistIndex = request.body.data;
|
.playlistIndex = request.body.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug( `Room "${
|
||||||
|
request.query.roomName }" has new event: ${ update }` );
|
||||||
|
|
||||||
if ( send ) {
|
if ( send ) {
|
||||||
const clients = clientReference[ request.body.roomName ];
|
const clients = clientReference[ request.body.roomName ];
|
||||||
|
|
||||||
|
if ( !clients ) {
|
||||||
|
response.send( 'ERR_CANNOT_SEND' );
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for ( const client in clients ) {
|
for ( const client in clients ) {
|
||||||
if ( connectedClients[ clients[ client ] ] ) {
|
if ( connectedClients[ clients[ client ] ] ) {
|
||||||
connectedClients[ clients[ client ] ]
|
connectedClients[ clients[ client ] ]
|
||||||
@@ -222,9 +246,17 @@ const useSSE = (
|
|||||||
socketData[ request.body.roomName ].roomToken
|
socketData[ request.body.roomName ].roomToken
|
||||||
=== request.body.roomToken
|
=== request.body.roomToken
|
||||||
) {
|
) {
|
||||||
|
logger.debug( `Room "${
|
||||||
|
request.query.roomName }" was deleted` );
|
||||||
socketData[ request.body.roomName ] = undefined;
|
socketData[ request.body.roomName ] = undefined;
|
||||||
const clients = clientReference[ request.body.roomName ];
|
const clients = clientReference[ request.body.roomName ];
|
||||||
|
|
||||||
|
if ( !clients ) {
|
||||||
|
response.send( 'ok' );
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for ( const client in clients ) {
|
for ( const client in clients ) {
|
||||||
if ( connectedClients[ clients[ client ] ] ) {
|
if ( connectedClients[ clients[ client ] ] ) {
|
||||||
connectedClients[ clients[ client ] ]
|
connectedClients[ clients[ client ] ]
|
||||||
@@ -234,6 +266,8 @@ const useSSE = (
|
|||||||
} ) + '\n\n' );
|
} ) + '\n\n' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response.send( 'ok' );
|
||||||
} else {
|
} else {
|
||||||
response.send( 403 ).send( 'ERR_UNAUTHORIZED' );
|
response.send( 403 ).send( 'ERR_UNAUTHORIZED' );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as sqlDB from './mysqldb.js';
|
import * as sqlDB from './mysqldb.js';
|
||||||
|
import logger from '../logger.js';
|
||||||
|
|
||||||
declare let __dirname: string | undefined;
|
declare let __dirname: string | undefined;
|
||||||
|
|
||||||
@@ -33,9 +34,9 @@ dbh.connect();
|
|||||||
*/
|
*/
|
||||||
const initDB = (): undefined => {
|
const initDB = (): undefined => {
|
||||||
( async () => {
|
( async () => {
|
||||||
console.log( '[ DB ] Setting up...' );
|
logger.info( '[ DB ] Setting up...' );
|
||||||
dbh.setupDB();
|
dbh.setupDB();
|
||||||
console.log( '[ DB ] Setting up complete!' );
|
logger.info( '[ DB ] Setting up complete!' );
|
||||||
} )();
|
} )();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import mysql from 'mysql';
|
import mysql from 'mysql';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
declare let __dirname: string | undefined;
|
declare let __dirname: string | undefined;
|
||||||
|
|
||||||
@@ -63,20 +64,20 @@ class SQLDB {
|
|||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
if ( this.isRecovering ) {
|
if ( this.isRecovering ) {
|
||||||
console.log( '[ SQL ] Attempting to recover from critical error' );
|
logger.info( '[ SQL ] Attempting to recover from critical error' );
|
||||||
this.sqlConnection = mysql.createConnection( this.config );
|
this.sqlConnection = mysql.createConnection( this.config );
|
||||||
this.isRecovering = false;
|
this.isRecovering = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sqlConnection.connect( err => {
|
this.sqlConnection.connect( err => {
|
||||||
if ( err ) {
|
if ( err ) {
|
||||||
console.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack );
|
logger.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack );
|
||||||
reject( err );
|
reject( err );
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log( '[ SQL ] Connected to database successfully' );
|
logger.info( '[ SQL ] Connected to database successfully' );
|
||||||
self.sqlConnection.on( 'error', err => {
|
self.sqlConnection.on( 'error', err => {
|
||||||
if ( err.code === 'ECONNRESET' ) {
|
if ( err.code === 'ECONNRESET' ) {
|
||||||
self.isRecovering = true;
|
self.isRecovering = true;
|
||||||
@@ -85,7 +86,7 @@ class SQLDB {
|
|||||||
self.connect();
|
self.connect();
|
||||||
}, 1000 );
|
}, 1000 );
|
||||||
} else {
|
} else {
|
||||||
console.error( err );
|
logger.error( err );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
resolve( 'connection' );
|
resolve( 'connection' );
|
||||||
|
|||||||
Reference in New Issue
Block a user