mirror of
https://github.com/janishutz/MusicPlayer.git
synced 2026-09-10 14:25:24 +02:00
chore: skeleton for MusicKitJS abstractions
This commit is contained in:
+205
-7
@@ -1,11 +1,209 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
RouterView
|
||||
} from 'vue-router';
|
||||
import {
|
||||
ref
|
||||
} from 'vue';
|
||||
|
||||
const theme = ref( '☼' );
|
||||
|
||||
const changeTheme = () => {
|
||||
if ( theme.value === '☽' ) {
|
||||
document.documentElement.classList.remove( 'dark' );
|
||||
document.documentElement.classList.add( 'light' );
|
||||
localStorage.setItem( 'theme', '☼' );
|
||||
theme.value = '☼';
|
||||
} else if ( theme.value === '☼' ) {
|
||||
document.documentElement.classList.remove( 'light' );
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
localStorage.setItem( 'theme', '☽' );
|
||||
theme.value = '☽';
|
||||
}
|
||||
};
|
||||
|
||||
theme.value = localStorage.getItem( 'theme' ) ?? '';
|
||||
|
||||
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === '☽' ) {
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
theme.value = '☽';
|
||||
} else {
|
||||
document.documentElement.classList.add( 'light' );
|
||||
theme.value = '☼';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>You did it!</h1>
|
||||
<p>
|
||||
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
|
||||
documentation
|
||||
</p>
|
||||
<div>
|
||||
<button id="themeSelector" title="Toggle between light and dark mode" @click="changeTheme();">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="material-symbols-outlined" v-html="theme"></span>
|
||||
</button>
|
||||
<router-view id="main-view" v-slot="{ Component, route }">
|
||||
<transition :name="route.meta.transition ? String( route.meta.transition ) : 'fade'" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style>
|
||||
body {
|
||||
background-color: var( --background-color );
|
||||
}
|
||||
|
||||
:root, :root.light {
|
||||
--primary-color: #0a1520;
|
||||
--secondary-color: white;
|
||||
--background-color: rgb(221, 221, 221);
|
||||
--nav-background: white;
|
||||
--hover-color: #00457a;
|
||||
--popup-color: rgb(224, 224, 224);
|
||||
--overlay-color: rgba(0, 0, 0, 0.7);
|
||||
--PI: 3.14159265358979;
|
||||
--gray-color: rgb(53, 53, 53);
|
||||
--footer-background: rgb(233, 233, 233);
|
||||
--accent-background: rgb(195, 235, 243);
|
||||
--loading-color: rgb(167, 167, 167);
|
||||
--slider-color: rgb(119, 132, 255);
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--primary-color: white;
|
||||
--secondary-color: black;
|
||||
--background-color: rgb(32, 32, 32);
|
||||
--nav-background: rgb(54, 54, 54);
|
||||
--popup-color: rgb(58, 58, 58);
|
||||
--hover-color: #007ddd;
|
||||
--overlay-color: rgba(104, 104, 104, 0.575);
|
||||
--gray-color: rgb(207, 207, 207);
|
||||
--footer-background: rgb(53, 53, 53);
|
||||
--accent-background: rgb(24, 12, 58);
|
||||
--loading-color: rgb(65, 65, 65);
|
||||
--slider-color: rgb(119, 132, 255);
|
||||
}
|
||||
|
||||
@media ( prefers-color-scheme: dark ) {
|
||||
:root {
|
||||
--primary-color: white;
|
||||
--secondary-color: black;
|
||||
--background-color: rgb(32, 32, 32);
|
||||
--nav-background: rgb(54, 54, 54);
|
||||
--popup-color: rgb(58, 58, 58);
|
||||
--hover-color: #007ddd;
|
||||
--overlay-color: rgba(104, 104, 104, 0.575);
|
||||
--gray-color: rgb(207, 207, 207);
|
||||
--footer-background: rgb(53, 53, 53);
|
||||
--accent-background: rgb(24, 12, 58);
|
||||
--loading-color: rgb(65, 65, 65);
|
||||
--slider-color: rgb(119, 132, 255);
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #7c8cec;
|
||||
color: white;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
#app {
|
||||
transition: 0.5s;
|
||||
background-color: var( --background-color );
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
/* font-family: Avenir, Helvetica, Arial, sans-serif; */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: var( --primary-color );
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#main-view {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.scale-enter-active,
|
||||
.scale-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.scale-enter-from,
|
||||
.scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 48
|
||||
}
|
||||
|
||||
.clr-open {
|
||||
border: black solid 1px !important;
|
||||
}
|
||||
|
||||
#themeSelector {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var( --primary-color );
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.fancy-button {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: linear-gradient( 45deg, rgb(0, 33, 139), rgb(151, 0, 0) );
|
||||
font-size: larger;
|
||||
transition: all 0.5s;
|
||||
background-size: 150%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fancy-button:hover {
|
||||
border-radius: 5px;
|
||||
background-position: 50%;
|
||||
}
|
||||
|
||||
.fancy-button-inactive {
|
||||
background: linear-gradient( 45deg, rgba(0, 33, 139, 0.6), rgba(151, 0, 0, 0.6) );
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.fancy-button-inactive:hover {
|
||||
border-radius: 20px;
|
||||
background-position: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="'popup-backdrop' + ( isOpen ? '' : ' hidden' )">
|
||||
<div class="popup-main">
|
||||
<span v-if="props.showClose" class="material-symbols-outlined close-icon" @click="closePopup()">close</span>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
const isOpen = defineModel<boolean>();
|
||||
const props = defineProps<{
|
||||
'showClose'?: boolean
|
||||
}>();
|
||||
|
||||
const closePopup = () => {
|
||||
isOpen.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.popup-backdrop {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
background-color: var( --overlay-color );
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
transition: all 0.5s;
|
||||
transform: scale(1);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.popup-main {
|
||||
width: 40%;
|
||||
height: 50%;
|
||||
background-color: var( --secondary-color );
|
||||
padding: 2.5%;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
overflow-y: scroll;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
+12
-9
@@ -1,12 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue';
|
||||
import {
|
||||
createApp
|
||||
} from 'vue';
|
||||
import {
|
||||
createPinia
|
||||
} from 'pinia';
|
||||
import router from './router';
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
const app = createApp( App );
|
||||
|
||||
const app = createApp(App)
|
||||
app.use( createPinia() );
|
||||
app.use( router );
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
app.mount( '#app' );
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import {
|
||||
createRouter, createWebHistory
|
||||
} from 'vue-router';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [],
|
||||
})
|
||||
const router = createRouter( {
|
||||
'history': createWebHistory( import.meta.env.BASE_URL ),
|
||||
'routes': []
|
||||
} );
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
// NOTE: For re-associating files with details here, can use dropdown in UI
|
||||
|
||||
export type Playlist = Song[];
|
||||
|
||||
export interface UrlToFileMapping {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export interface Song {
|
||||
/**
|
||||
* Where the song can be found
|
||||
*/
|
||||
'source': 'local' | 'applemusic';
|
||||
|
||||
/**
|
||||
* The identifier for the song so it can be found again (such as Apple Music ID or filename)
|
||||
*/
|
||||
'identifier': string;
|
||||
|
||||
/**
|
||||
* Additional info, such as dance type to be displayed on remote screens
|
||||
*/
|
||||
'additional-info': string;
|
||||
|
||||
/**
|
||||
* The artist of the song
|
||||
*/
|
||||
'artist': string;
|
||||
|
||||
/**
|
||||
* The name of the song
|
||||
*/
|
||||
'name': string;
|
||||
|
||||
/**
|
||||
* The cover image as a URL
|
||||
*/
|
||||
'cover': string;
|
||||
|
||||
/**
|
||||
* Song duration
|
||||
*/
|
||||
'duration': string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const useMusicKit = () => {
|
||||
const init = () => {
|
||||
isInit = true;
|
||||
};
|
||||
|
||||
let isInit = false;
|
||||
|
||||
if ( !window.MusicKit ) {
|
||||
document.addEventListener( 'musickitloaded', () => {
|
||||
init();
|
||||
} );
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export const play = () => {};
|
||||
|
||||
export const pause = () => {};
|
||||
|
||||
export const skip10 = () => {};
|
||||
|
||||
export const back10 = () => {};
|
||||
|
||||
export const playSong = ( id: string ) => {};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const login = async () => {
|
||||
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
type Ref,
|
||||
ref
|
||||
} from 'vue';
|
||||
import type {
|
||||
Playlist
|
||||
} from '../dtype/playlist';
|
||||
|
||||
export const queueIdx = ref( 0 );
|
||||
|
||||
export const queue: Ref<Playlist> = ref( [] );
|
||||
|
||||
export const isPlaying = ref( false );
|
||||
|
||||
export const shuffle = ref( false );
|
||||
|
||||
export const repeat: Ref<'off' | 'one' | 'all'> = ref( 'off' );
|
||||
|
||||
export const playbackPercentage = ref( 0 );
|
||||
|
||||
export const duration = ref( 0 );
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||
<h1>404</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-view {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 50vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,229 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type Ref,
|
||||
computed, ref
|
||||
} from 'vue';
|
||||
|
||||
interface FullConfig {
|
||||
'offering': Bars;
|
||||
'ages': Ages;
|
||||
}
|
||||
|
||||
interface Bars {
|
||||
[name: string]: {
|
||||
'offering': BarConfig;
|
||||
'name': string;
|
||||
'id': string;
|
||||
}
|
||||
}
|
||||
|
||||
interface Ages {
|
||||
'18+': string;
|
||||
'16-18': string;
|
||||
}
|
||||
|
||||
interface BarConfig {
|
||||
[id: string]: Offer
|
||||
}
|
||||
|
||||
interface Offer {
|
||||
'name': string;
|
||||
'price': number; // In cents
|
||||
'depot'?: number; // In cents
|
||||
'showLine'?: boolean;
|
||||
'id': string;
|
||||
}
|
||||
|
||||
interface Selection {
|
||||
[id: string]: number;
|
||||
}
|
||||
|
||||
const ages: Ref<Ages> = ref( {
|
||||
'18+': '',
|
||||
'16-18': '',
|
||||
'below': ''
|
||||
} );
|
||||
const offering: Ref<Bars> = ref( {} );
|
||||
const selection: Ref<Selection> = ref( {} );
|
||||
const selectedBar: Ref<string> = ref( '' );
|
||||
const enableDepotReminder = ref( true );
|
||||
|
||||
let cashinInDepot = false;
|
||||
|
||||
fetch( '/bar-config.json', {
|
||||
'cache': 'no-store'
|
||||
} ).then( res => {
|
||||
if ( res.status === 200 ) {
|
||||
res.json().then( json => {
|
||||
const data: FullConfig = json;
|
||||
|
||||
offering.value = data.offering;
|
||||
ages.value = data.ages;
|
||||
} );
|
||||
} else {
|
||||
alert( 'Failed to load' );
|
||||
}
|
||||
} );
|
||||
|
||||
const reset = ( skipCheck = true ) => {
|
||||
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 => {
|
||||
selection.value[ val ] = 0;
|
||||
} );
|
||||
};
|
||||
|
||||
const total = computed( () => {
|
||||
const keys = Object.keys( selection.value );
|
||||
|
||||
let totalPrice = 0;
|
||||
let totalDepot = 0;
|
||||
|
||||
for ( let i = 0; i < keys.length; i++ ) {
|
||||
const o = selection.value[ keys[ i ]! ]!;
|
||||
|
||||
totalPrice += o * offering.value[ selectedBar.value ]!.offering[ keys[ i ]! ]!.price;
|
||||
totalDepot += o * ( offering.value[ selectedBar.value ]!.offering[ keys[ i ]! ]!.depot ?? 0 );
|
||||
}
|
||||
|
||||
if ( totalDepot > 0 ) {
|
||||
cashinInDepot = true;
|
||||
}
|
||||
|
||||
totalPrice += totalDepot;
|
||||
|
||||
return ( totalPrice / 100 ) + ( totalDepot ? ` (Depot = ${ totalDepot })` : '' );
|
||||
} );
|
||||
|
||||
const changeValue = ( id: string, amount: number ) => {
|
||||
selection.value[ id ]! += amount;
|
||||
|
||||
if ( selection.value[ id ]! < 0 ) {
|
||||
selection.value[ id ] = 0;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bar-utility">
|
||||
<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 v-if="Object.keys( offering ).includes( selectedBar )">
|
||||
Total: CHF {{ total }}
|
||||
</p>
|
||||
<table v-if="Object.keys( offering ).includes( selectedBar )" class="offering-wrapper">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="offer in offering[ selectedBar ]!.offering"
|
||||
:key="offer.id"
|
||||
:class="[ 'offering', offer.showLine ? 'show-line' : '' ]"
|
||||
>
|
||||
<td>
|
||||
<p>
|
||||
{{ offer.name }} (CHF {{ offer.price / 100 }}{{
|
||||
offer.depot ? ' + ' + ( offer.depot / 100 ) : '' }})
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<button class="inc-dec" @click="changeValue( offer.id, 1 )">
|
||||
+
|
||||
</button>
|
||||
<p>{{ selection[ offer.id ] }}</p>
|
||||
<button class="inc-dec" @click="changeValue( offer.id, -1 )">
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="Object.keys( offering ).includes( selectedBar )">
|
||||
Total: CHF {{ total }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bar-utility {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
>.offering-wrapper {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 5vh;
|
||||
|
||||
.offering {
|
||||
&.show-line {
|
||||
>td {
|
||||
border-bottom: solid 1px black;
|
||||
}
|
||||
}
|
||||
|
||||
>td {
|
||||
padding: 5px;
|
||||
p {
|
||||
margin: 0;
|
||||
margin-right: 15px;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
>div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
p {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
>.inc-dec {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: solid var( --primary-color ) 1px;
|
||||
border-radius: 20px;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
color: var( --primary-color );
|
||||
touch-action: manipulation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user