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
+6
View File
@@ -21,6 +21,12 @@
<img alt="App Version" src="https://img.shields.io/github/package-json/v/simplePCBuilding/MusicPlayerV2.svg?label=Development Version"> <img alt="App Version" src="https://img.shields.io/github/package-json/v/simplePCBuilding/MusicPlayerV2.svg?label=Development Version">
</div> </div>
# Development Branch
This branch is for the upcoming version 4 of Music Player and is in early stages of development
# MusicPlayer
A music player, specifically created for displaying song information on multiple different displays that are connected to the same network, just from the browser. A music player, specifically created for displaying song information on multiple different displays that are connected to the same network, just from the browser.
The [hosted version](https://music.janishutz.com) of this MusicPlayer, which is fully set up for you is subscription-based and can be paid for on my [store](https://store.janishutz.com/product/com.janishutz). The [hosted version](https://music.janishutz.com) of this MusicPlayer, which is fully set up for you is subscription-based and can be paid for on my [store](https://store.janishutz.com/product/com.janishutz).
+744
View File
@@ -0,0 +1,744 @@
import eslint from '@eslint/js';
import globals from 'globals';
import stylistic from '@stylistic/eslint-plugin';
import tseslint from 'typescript-eslint';
import typescript from '@typescript-eslint/eslint-plugin';
import vue from 'eslint-plugin-vue';
const style = {
'plugins': {
'@stylistic': stylistic,
'@stylistic/js': stylistic,
'@stylistic/ts': stylistic
},
'files': [
'**/*.ts',
'**/*.js',
'**/*.mjs',
'**/*.cjs',
'**/*.tsx',
'**/*.jsx'
],
'rules': {
'sort-imports': [
'warn',
{
'ignoreCase': false,
'ignoreDeclarationSort': false,
'ignoreMemberSort': false,
'memberSyntaxSortOrder': [
'none',
'all',
'multiple',
'single'
],
'allowSeparatedGroups': false
}
],
// Formatting
'@stylistic/array-bracket-newline': [
'error',
{
'multiline': false,
'minItems': 2
}
],
'@stylistic/array-bracket-spacing': [
'error',
'always'
],
'@stylistic/array-element-newline': [
'error',
{
'consistent': false,
'multiline': false,
'minItems': 2
}
],
'@stylistic/arrow-parens': [
'error',
'as-needed'
],
'@stylistic/arrow-spacing': [
'error',
{
'before': true,
'after': true
}
],
'@stylistic/block-spacing': [
'error',
'always'
],
'@stylistic/brace-style': [
'error',
'1tbs',
{
'allowSingleLine': false
}
],
'@stylistic/comma-dangle': [
'error',
'never'
],
'@stylistic/comma-spacing': [
'error',
{
'before': false,
'after': true
}
],
'@stylistic/comma-style': [
'error',
'last'
],
'@stylistic/dot-location': [
'error',
'property'
],
'@stylistic/function-call-argument-newline': [
'error',
'consistent'
],
'@stylistic/function-call-spacing': [
'error',
'never'
],
'@stylistic/function-paren-newline': [
'error',
'multiline-arguments'
],
'@stylistic/implicit-arrow-linebreak': [
'error',
'beside'
],
'@stylistic/indent': [
'error',
4
],
'@stylistic/indent-binary-ops': [
'error',
4
],
'@stylistic/key-spacing': [
'error',
{
'beforeColon': false,
'afterColon': true
}
],
'@stylistic/keyword-spacing': [
'error',
{
'before': true,
'after': true
}
],
'@stylistic/lines-between-class-members': [
'error',
'always'
],
'@stylistic/max-len': [
'warn',
{
'code': 140,
'comments': 160,
'ignoreComments': false,
'ignoreUrls': true,
'ignoreStrings': true,
'ignoreTemplateLiterals': true,
'ignoreRegExpLiterals': true
}
],
'@stylistic/max-statements-per-line': [
'error',
{
'max': 1
}
],
'@stylistic/multiline-ternary': [
'error',
'always-multiline'
],
'@stylistic/new-parens': [
'error',
'always'
],
'@stylistic/newline-per-chained-call': 'error',
'@stylistic/no-confusing-arrow': 'error',
'@stylistic/no-extra-parens': [
'error',
'all',
{
'nestedBinaryExpressions': false,
'ternaryOperandBinaryExpressions': false,
'ignoreJSX': 'multi-line',
'nestedConditionalExpressions': false
}
],
'@stylistic/no-extra-semi': 'error',
'@stylistic/no-floating-decimal': 'error',
'@stylistic/no-mixed-operators': 'error',
'@stylistic/no-mixed-spaces-and-tabs': 'error',
'@stylistic/no-multi-spaces': 'error',
'@stylistic/no-multiple-empty-lines': [
'error',
{
'max': 3,
'maxEOF': 2
}
],
'@stylistic/no-tabs': 'error',
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/no-whitespace-before-property': 'error',
'@stylistic/object-curly-newline': [
'error',
{
'multiline': true,
'minProperties': 1
}
],
'@stylistic/object-curly-spacing': [
'error',
'always'
],
'@stylistic/object-property-newline': 'error',
'@stylistic/one-var-declaration-per-line': 'error',
'@stylistic/operator-linebreak': [
'error',
'before'
],
'@stylistic/padded-blocks': [
'error',
{
'blocks': 'never',
'classes': 'always',
'switches': 'never'
}
],
// Padding lines. The most in-depth part of this config
'@stylistic/padding-line-between-statements': [
'error',
// Variables, Constants
{
'blankLine': 'never',
'prev': 'var',
'next': 'var'
},
{
'blankLine': 'never',
'prev': 'let',
'next': 'let'
},
{
'blankLine': 'never',
'prev': 'const',
'next': 'const'
},
{
'blankLine': 'always',
'prev': 'var',
'next': [
'block',
'block-like',
'break',
'cjs-export',
'cjs-import',
'class',
'const',
'continue',
'debugger',
'directive',
'do',
'empty',
'export',
'expression',
'for',
'function',
'if',
'iife',
'import',
'let',
'return',
'switch',
'throw',
'try',
'var',
'with'
]
},
{
'blankLine': 'always',
'prev': 'let',
'next': [
'block',
'block-like',
'break',
'cjs-export',
'cjs-import',
'class',
'const',
'continue',
'debugger',
'directive',
'do',
'empty',
'export',
'expression',
'for',
'function',
'if',
'iife',
'import',
'return',
'switch',
'throw',
'try',
'var',
'while',
'with'
]
},
{
'blankLine': 'always',
'prev': 'const',
'next': [
'block',
'block-like',
'break',
'cjs-export',
'cjs-import',
'class',
'continue',
'debugger',
'directive',
'do',
'empty',
'export',
'expression',
'for',
'function',
'if',
'iife',
'import',
'let',
'return',
'switch',
'throw',
'try',
'var',
'while',
'with'
]
},
// Import
{
'blankLine': 'never',
'prev': 'import',
'next': 'import'
},
{
'blankLine': 'never',
'prev': 'cjs-import',
'next': 'cjs-import'
},
{
'blankLine': 'always',
'prev': [
'block',
'block-like',
'break',
'cjs-export',
'class',
'const',
'continue',
'debugger',
'directive',
'do',
'empty',
'export',
'expression',
'for',
'function',
'if',
'iife',
'let',
'return',
'switch',
'throw',
'try',
'var',
'while',
'with'
],
'next': 'cjs-import'
},
{
'blankLine': 'always',
'prev': 'cjs-import',
'next': [
'block',
'block-like',
'break',
'cjs-export',
'class',
'const',
'continue',
'debugger',
'directive',
'do',
'empty',
'export',
'expression',
'for',
'function',
'if',
'iife',
'let',
'return',
'switch',
'throw',
'try',
'var',
'while',
'with'
]
},
{
'blankLine': 'always',
'prev': [
'block',
'block-like',
'break',
'cjs-export',
'class',
'const',
'continue',
'debugger',
'directive',
'do',
'empty',
'export',
'expression',
'for',
'function',
'if',
'iife',
'let',
'return',
'switch',
'throw',
'try',
'var',
'while',
'with'
],
'next': 'import'
},
{
'blankLine': 'always',
'prev': 'import',
'next': [
'block',
'block-like',
'break',
'cjs-export',
'class',
'const',
'continue',
'debugger',
'directive',
'do',
'empty',
'export',
'expression',
'for',
'function',
'if',
'iife',
'let',
'return',
'switch',
'throw',
'try',
'var',
'while',
'with'
]
},
// If
{
'blankLine': 'always',
'prev': '*',
'next': 'if'
},
{
'blankLine': 'always',
'prev': 'if',
'next': '*'
},
// For
{
'blankLine': 'always',
'prev': '*',
'next': 'for'
},
{
'blankLine': 'always',
'prev': 'for',
'next': '*'
},
// While
{
'blankLine': 'always',
'prev': '*',
'next': 'while'
},
{
'blankLine': 'always',
'prev': 'while',
'next': '*'
},
// Functions
{
'blankLine': 'always',
'prev': '*',
'next': 'function'
},
{
'blankLine': 'always',
'prev': 'function',
'next': '*'
},
// Block Statements
{
'blankLine': 'always',
'prev': '*',
'next': 'block-like'
},
{
'blankLine': 'always',
'prev': 'block-like',
'next': '*'
},
// Switch
{
'blankLine': 'always',
'prev': '*',
'next': 'switch'
},
{
'blankLine': 'always',
'prev': 'switch',
'next': '*'
},
// Try-Catch
{
'blankLine': 'always',
'prev': '*',
'next': 'try'
},
{
'blankLine': 'always',
'prev': 'try',
'next': '*'
},
// Throw
{
'blankLine': 'always',
'prev': '*',
'next': 'throw'
},
{
'blankLine': 'always',
'prev': 'throw',
'next': '*'
},
// Return
{
'blankLine': 'never',
'prev': 'return',
'next': '*'
},
{
'blankLine': 'always',
'prev': '*',
'next': 'return'
},
// Export
{
'blankLine': 'always',
'prev': '*',
'next': 'export'
},
{
'blankLine': 'always',
'prev': 'export',
'next': '*'
},
{
'blankLine': 'always',
'prev': '*',
'next': 'cjs-export'
},
{
'blankLine': 'always',
'prev': 'cjs-export',
'next': '*'
},
// Classes
{
'blankLine': 'always',
'prev': '*',
'next': 'class'
},
{
'blankLine': 'always',
'prev': 'class',
'next': '*'
}
],
'@stylistic/quote-props': [
'error',
'always'
],
'@stylistic/quotes': [
'error',
'single'
],
'@stylistic/rest-spread-spacing': [
'error',
'never'
],
'@stylistic/semi': [
'error',
'always'
],
'@stylistic/semi-spacing': [
'error',
{
'before': false,
'after': true
}
],
'@stylistic/semi-style': [
'error',
'last'
],
'@stylistic/space-before-blocks': [
'error',
'always'
],
'@stylistic/space-before-function-paren': [
'error',
'always'
],
'@stylistic/space-in-parens': [
'error',
'always'
],
'@stylistic/space-infix-ops': [
'error',
{
'int32Hint': false
}
],
'@stylistic/space-unary-ops': 'error',
'@stylistic/spaced-comment': [
'error',
'always'
],
'@stylistic/switch-colon-spacing': 'error',
'@stylistic/template-curly-spacing': [
'error',
'always'
],
'@stylistic/template-tag-spacing': [
'error',
'always'
],
'@stylistic/type-generic-spacing': 'error',
'@stylistic/type-named-tuple-spacing': 'error',
'@stylistic/wrap-iife': [
'error',
'inside'
],
'@stylistic/wrap-regex': 'error',
'@stylistic/ts/type-annotation-spacing': 'error'
}
};
/** @type {import('eslint').Linter.Config} */
export default tseslint.config(
// Base JavaScript rules
eslint.configs.recommended,
tseslint.configs.recommended,
style,
// Vue support (including TS and JSX inside SFCs)
{
'files': [ '**/*.vue' ],
'languageOptions': {
'sourceType': 'module',
'ecmaVersion': 'latest',
'globals': globals.browser,
'parserOptions': {
'parser': tseslint.parser
}
},
'plugins': {
'vue': vue,
'@stylistic': stylistic,
'@stylistic/js': stylistic,
'@stylistic/ts': stylistic,
'@typescript-eslint': typescript
},
'extends': [
eslint.configs.recommended,
...vue.configs['flat/recommended']
],
'rules': {
...typescript.configs.recommended.rules,
...style.rules,
// Vue specific rules
'@stylistic/indent': 'off',
'vue/html-indent': [
'error',
4
],
'vue/html-comment-indent': [
'error',
4
],
'vue/script-indent': [
'error',
4,
{
'baseIndent': 1,
'switchCase': 1
}
],
'vue/html-self-closing': [
'error',
{
'html': {
'void': 'never',
'normal': 'never',
'component': 'always'
},
'svg': 'always',
'math': 'never'
}
],
'vue/max-attributes-per-line': [
'error',
{
'singleline': 3,
'multiline': 1
}
]
}
}
);
+5
View File
@@ -16,12 +16,17 @@
"vue-router": "^5.2.0" "vue-router": "^5.2.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "^5.10.0",
"@tsconfig/node24": "^24.0.4", "@tsconfig/node24": "^24.0.4",
"@types/node": "^24.13.3", "@types/node": "^24.13.3",
"@vitejs/plugin-vue": "^6.0.8", "@vitejs/plugin-vue": "^6.0.8",
"@vue/tsconfig": "^0.9.1", "@vue/tsconfig": "^0.9.1",
"eslint-plugin-vue": "^10.10.0",
"globals": "^17.11.0",
"npm-run-all2": "^9.0.2", "npm-run-all2": "^9.0.2",
"typescript": "~6.0.0", "typescript": "~6.0.0",
"typescript-eslint": "^8.67.0",
"vite": "^8.1.5", "vite": "^8.1.5",
"vite-plugin-vue-devtools": "^8.1.5", "vite-plugin-vue-devtools": "^8.1.5",
"vue-tsc": "^3.3.7" "vue-tsc": "^3.3.7"
+193
View File
@@ -0,0 +1,193 @@
{
"ages": {
"below": "Orange",
"16-18": "Yellow",
"18+": "Turquoise"
},
"offering": {
"big-bar": {
"offering": {
"softdrinks": {
"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"
},
"small-bar": {
"offering": {
"softdrinks": {
"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"
}
}
}
+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> <template>
<h1>You did it!</h1> <div>
<p> <button id="themeSelector" title="Toggle between light and dark mode" @click="changeTheme();">
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the <!-- eslint-disable-next-line vue/no-v-html -->
documentation <span class="material-symbols-outlined" v-html="theme"></span>
</p> </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> </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 App from './App.vue';
import { createPinia } from 'pinia' import {
createApp
} from 'vue';
import {
createPinia
} from 'pinia';
import router from './router';
import App from './App.vue' const app = createApp( App );
import router from './router'
const app = createApp(App) app.use( createPinia() );
app.use( router );
app.use(createPinia()) app.mount( '#app' );
app.use(router)
app.mount('#app')
+7 -5
View File
@@ -1,8 +1,10 @@
import { createRouter, createWebHistory } from 'vue-router' import {
createRouter, createWebHistory
} from 'vue-router';
const router = createRouter( { const router = createRouter( {
history: createWebHistory(import.meta.env.BASE_URL), 'history': createWebHistory( import.meta.env.BASE_URL ),
routes: [], '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
+18 -12
View File
@@ -1,18 +1,24 @@
import { fileURLToPath, URL } from 'node:url' import {
URL, fileURLToPath
import { defineConfig } from 'vite' } from 'node:url';
import vue from '@vitejs/plugin-vue' import {
import vueDevTools from 'vite-plugin-vue-devtools' defineConfig
} from 'vite';
import vue from '@vitejs/plugin-vue';
import vueDevTools from 'vite-plugin-vue-devtools';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig( { export default defineConfig( {
plugins: [ 'plugins': [
vue(), vue(),
vueDevTools(), vueDevTools()
], ],
resolve: { 'resolve': {
alias: { 'alias': {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath( new URL( './src', import.meta.url ) )
}
}, },
}, 'server': {
}) 'port': 8081
}
} );