chore: skeleton for MusicKitJS abstractions

This commit is contained in:
2026-08-19 16:02:45 +02:00
parent cb0db033ea
commit 494eef6cd2
28 changed files with 1606 additions and 37 deletions
+205 -7
View File
@@ -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( '&#9788;' );
const changeTheme = () => {
if ( theme.value === '&#9789;' ) {
document.documentElement.classList.remove( 'dark' );
document.documentElement.classList.add( 'light' );
localStorage.setItem( 'theme', '&#9788;' );
theme.value = '&#9788;';
} else if ( theme.value === '&#9788;' ) {
document.documentElement.classList.remove( 'light' );
document.documentElement.classList.add( 'dark' );
localStorage.setItem( 'theme', '&#9789;' );
theme.value = '&#9789;';
}
};
theme.value = localStorage.getItem( 'theme' ) ?? '';
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === '&#9789;' ) {
document.documentElement.classList.add( 'dark' );
theme.value = '&#9789;';
} else {
document.documentElement.classList.add( 'light' );
theme.value = '&#9788;';
}
</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>
+65
View File
@@ -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
View File
@@ -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' );
+8 -6
View File
@@ -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;
View File
View File
+44
View File
@@ -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;
}
View File
View File
+17
View File
@@ -0,0 +1,17 @@
export const useMusicKit = () => {
const init = () => {
isInit = true;
};
let isInit = false;
if ( !window.MusicKit ) {
document.addEventListener( 'musickitloaded', () => {
init();
} );
} else {
init();
}
return {};
};
+9
View File
@@ -0,0 +1,9 @@
export const play = () => {};
export const pause = () => {};
export const skip10 = () => {};
export const back10 = () => {};
export const playSong = ( id: string ) => {};
+3
View File
@@ -0,0 +1,3 @@
export const login = async () => {
};
View File
+21
View File
@@ -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 );
View File
View File
+24
View File
@@ -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>
View File
+229
View File
@@ -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>
View File
View File