Compare commits
77 Commits
V1.0.0
...
669cc620bf
| Author | SHA1 | Date | |
|---|---|---|---|
| 669cc620bf | |||
| 64d086dec4 | |||
| 1d714da494 | |||
| d63df5898b | |||
| bd18636141 | |||
| 51f0b5639a | |||
| 6a6b06a994 | |||
| e2e27e640f | |||
| ac575adedb | |||
| d15366e572 | |||
| 25cfc80925 | |||
| 952ef0ee85 | |||
| 80c497b80a | |||
| 8778740454 | |||
| 74075c2919 | |||
| 3e13a4ebf5 | |||
| 84b640ee5e | |||
| 0315241d76 | |||
| 6e93cfdf2c | |||
| 6b9d556e57 | |||
| 025c7ed111 | |||
| 057ea67a95 | |||
| 16543fb577 | |||
| f0c538126d | |||
| b0a1f9a538 | |||
|
|
9fac37203d | ||
| b10aeed1f8 | |||
| b8faeef189 | |||
| 4ef98f80cf | |||
| c9f487b981 | |||
| dc2327af33 | |||
|
|
360cc7c206 | ||
|
|
7e8170c794 | ||
|
|
83b800ad96 | ||
| 52d7fa7de7 | |||
| a2f6757d84 | |||
| da01326777 | |||
|
|
c77bfc8336 | ||
| 372ebf1057 | |||
| 6cf9e72263 | |||
| b989111849 | |||
| bd2e4cdac4 | |||
| 4ae05bd060 | |||
| e3e4bfc8bd | |||
| f8f0b27d52 | |||
| 1567640634 | |||
| 070e9d158b | |||
| 8d9607dbc6 | |||
| 95170194af | |||
| e54f5178a1 | |||
| a2711b76dd | |||
| d7f36b8e07 | |||
| a5e2520d28 | |||
| 221ae67ec2 | |||
| 361523172d | |||
| 88ecea1761 | |||
| c3bff192bb | |||
| 1c4aace806 | |||
| 429bb53f36 | |||
| f314732f3f | |||
| 7a42ab8b4e | |||
| 1e11f1dc2e | |||
| 76f543eb2f | |||
| 4ecf93d31b | |||
| ed63ca77d6 | |||
| b2d8180bb9 | |||
| 1ffdc873a7 | |||
| 56a714ab9e | |||
| 8f5fce8b97 | |||
| ce82014826 | |||
| 17225d07bc | |||
| 15d59d9cee | |||
| 28ae628f4d | |||
|
|
c903481739 | ||
| eb15e1fc3f | |||
| cc09bb87f8 | |||
| eae13bd107 |
4
.gitignore
vendored
@@ -2,4 +2,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
*.secret.json
|
*.secret.json
|
||||||
apple_private_key.p8
|
apple_private_key.p8
|
||||||
musicplayerv2-server.zip
|
musicplayerv2-server.zip
|
||||||
|
dist
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
.DS_Store
|
# Logs
|
||||||
node_modules
|
logs
|
||||||
/dist
|
*.log
|
||||||
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Log files
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
#Electron-builder output
|
*.tsbuildinfo
|
||||||
/dist_electron
|
|
||||||
test
|
|
||||||
7
MusicPlayerV2-GUI/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"Vue.vscode-typescript-vue-plugin",
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
]
|
||||||
|
}
|
||||||
46
MusicPlayerV2-GUI/README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# MusicPlayerV2-GUI
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||||
|
|
||||||
|
## Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||||
|
|
||||||
|
1. Disable the built-in TypeScript Extension
|
||||||
|
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||||
|
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||||
|
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
1
MusicPlayerV2-GUI/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
714
MusicPlayerV2-GUI/eslint.config.mjs
Normal file
@@ -0,0 +1,714 @@
|
|||||||
|
import vue from 'eslint-plugin-vue';
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import typescript from '@typescript-eslint/eslint-plugin';
|
||||||
|
import stylistic from '@stylistic/eslint-plugin';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
'plugins': {
|
||||||
|
'@stylistic': stylistic,
|
||||||
|
'@stylistic/js': stylistic,
|
||||||
|
'@stylistic/ts': stylistic,
|
||||||
|
},
|
||||||
|
'files': [
|
||||||
|
'**/*.ts',
|
||||||
|
'**/*.js',
|
||||||
|
'**/*.mjs',
|
||||||
|
'**/*.cjs',
|
||||||
|
'**/*.tsx',
|
||||||
|
'**/*.jsx'
|
||||||
|
],
|
||||||
|
'rules': {
|
||||||
|
'sort-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
"ignoreCase": false,
|
||||||
|
"ignoreDeclarationSort": false,
|
||||||
|
"ignoreMemberSort": false,
|
||||||
|
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"],
|
||||||
|
"allowSeparatedGroups": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// Formatting
|
||||||
|
'@stylistic/array-bracket-newline': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'multiline': true,
|
||||||
|
'minItems': 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@stylistic/array-bracket-spacing': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'@stylistic/array-element-newline': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'multiline': true,
|
||||||
|
'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'
|
||||||
|
],
|
||||||
|
'@stylistic/comma-spacing': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'before': false,
|
||||||
|
'after': true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@stylistic/comma-style': [
|
||||||
|
'error',
|
||||||
|
'last'
|
||||||
|
],
|
||||||
|
'@stylistic/dot-location': [
|
||||||
|
'error',
|
||||||
|
'property'
|
||||||
|
],
|
||||||
|
'@stylistic/eol-last': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'@stylistic/function-call-spacing': [
|
||||||
|
'error',
|
||||||
|
'never'
|
||||||
|
],
|
||||||
|
'@stylistic/function-paren-newline': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'minItems': 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@stylistic/function-call-argument-newline': [
|
||||||
|
'error',
|
||||||
|
'consistent'
|
||||||
|
],
|
||||||
|
'@stylistic/implicit-arrow-linebreak': [
|
||||||
|
'error',
|
||||||
|
'beside'
|
||||||
|
],
|
||||||
|
'@stylistic/indent': [
|
||||||
|
'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': 120,
|
||||||
|
'comments': 140,
|
||||||
|
'ignoreComments': false,
|
||||||
|
'ignoreUrls': true,
|
||||||
|
'ignoreStrings': false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@stylistic/new-parens': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'@stylistic/newline-per-chained-call': [ '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/operator-linebreak': [
|
||||||
|
'error',
|
||||||
|
'before'
|
||||||
|
],
|
||||||
|
'@stylistic/one-var-declaration-per-line': 'error',
|
||||||
|
'@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/template-curly-spacing': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'@stylistic/switch-colon-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,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
25
MusicPlayerV2-GUI/index.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
|
||||||
|
<!-- TODO: Update URL -->
|
||||||
|
<script src="https://js-cdn.music.apple.com/musickit/v3/musickit.js"></script>
|
||||||
|
<script src="https://static.janishutz.com/libs/jquery/jquery.min.js"></script>
|
||||||
|
<title>MusicPlayer</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>This application requires JavaScript to work!</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script>
|
||||||
|
localStorage.setItem('music-player-config', 'sse');// Or 'ws'
|
||||||
|
</script>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
46
MusicPlayerV2-GUI/package.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "musicplayerv2-gui",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build --force",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||||
|
"@janishutz/login-sdk-browser": "^1.1.0",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
|
"@melloware/coloris": "^0.24.0",
|
||||||
|
"@rollup/plugin-inject": "^5.0.5",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"colorthief": "^2.6.0",
|
||||||
|
"music-metadata": "^11.9.0",
|
||||||
|
"musickit-typescript": "^1.2.4",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"socket.io-client": "^4.7.5",
|
||||||
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
|
"vue": "^3.4.15",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@stylistic/eslint-plugin": "^5.4.0",
|
||||||
|
"@tsconfig/node20": "^20.1.2",
|
||||||
|
"@types/jquery": "^3.5.33",
|
||||||
|
"@types/node": "^20.11.10",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
|
"@vue/tsconfig": "^0.5.1",
|
||||||
|
"eslint-plugin-vue": "^10.5.0",
|
||||||
|
"npm-run-all2": "^6.1.1",
|
||||||
|
"sass-embedded": "^1.92.0",
|
||||||
|
"typescript": "~5.3.0",
|
||||||
|
"typescript-eslint": "^8.44.1",
|
||||||
|
"vite": "^7.1.4",
|
||||||
|
"vue-tsc": "^2.0.29"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
MusicPlayerV2-GUI/public/bar-config.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"ages": {
|
||||||
|
"below": "red",
|
||||||
|
"16-18": "",
|
||||||
|
"18+": ""
|
||||||
|
},
|
||||||
|
"offering": {
|
||||||
|
"test": {
|
||||||
|
"name": "Test drink",
|
||||||
|
"price": 700,
|
||||||
|
"id": "test"
|
||||||
|
},
|
||||||
|
"test-2": {
|
||||||
|
"name": "Test drink 2",
|
||||||
|
"price": 500,
|
||||||
|
"id": "test-2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
MusicPlayerV2-GUI/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
209
MusicPlayerV2-GUI/src/App.vue
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
1
MusicPlayerV2-GUI/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
44
MusicPlayerV2-GUI/src/components/libraryView.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Library</h1>
|
||||||
|
<playlistsView
|
||||||
|
:playlists="$props.playlists"
|
||||||
|
:is-logged-in="$props.isLoggedIn"
|
||||||
|
@selected-playlist="( id ) => selectPlaylist( id )"
|
||||||
|
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import playlistsView from '@/components/playlistsView.vue';
|
||||||
|
import type {
|
||||||
|
ReadFile
|
||||||
|
} from '@/scripts/song';
|
||||||
|
|
||||||
|
const emits = defineEmits( [
|
||||||
|
'selected-playlist',
|
||||||
|
'custom-playlist'
|
||||||
|
] );
|
||||||
|
|
||||||
|
const selectPlaylist = ( id: string ) => {
|
||||||
|
emits( 'selected-playlist', id );
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
||||||
|
emits( 'custom-playlist', playlist );
|
||||||
|
};
|
||||||
|
|
||||||
|
defineProps( {
|
||||||
|
'playlists': {
|
||||||
|
'default': [],
|
||||||
|
'type': Array<any>,
|
||||||
|
'required': true,
|
||||||
|
},
|
||||||
|
'isLoggedIn': {
|
||||||
|
'default': false,
|
||||||
|
'type': Boolean,
|
||||||
|
'required': true,
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
416
MusicPlayerV2-GUI/src/components/notificationsModule.vue
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
<!-- eslint-disable no-undef -->
|
||||||
|
<template>
|
||||||
|
<div id="notifications">
|
||||||
|
<div class="message-box" :class="[ location, size ]" :style="'z-index: ' + ( messageType === 'hide' ? '-1' : '1000' )">
|
||||||
|
<div class="message-container" :class="messageType">
|
||||||
|
<button class="close-notification" @click="handleNotifications();">
|
||||||
|
<span class="material-symbols-outlined close-notification-icon">close</span>
|
||||||
|
</button>
|
||||||
|
<span v-if="messageType == 'hide'" class="material-symbols-outlined types hide">question_mark</span>
|
||||||
|
<span v-else-if="messageType == 'ok'" class="material-symbols-outlined types" style="background-color: green;">done</span>
|
||||||
|
<span v-else-if="messageType == 'error'" class="material-symbols-outlined types" style="background-color: red;">close</span>
|
||||||
|
<span v-else-if="messageType == 'progress'" class="material-symbols-outlined types progress-spinner" style="background-color: blue;">progress_activity</span>
|
||||||
|
<span v-else-if="messageType == 'info'" class="material-symbols-outlined types" style="background-color: lightblue;">info</span>
|
||||||
|
<span v-else-if="messageType == 'warning'" class="material-symbols-outlined types" style="background-color: orangered;">warning</span>
|
||||||
|
<p class="message" @click="notificationAction()">
|
||||||
|
{{ notifications[ currentDID ] ? notifications[ currentDID ].message : '' }}
|
||||||
|
</p>
|
||||||
|
<div :class="'countdown countdown-' + messageType" :style="'width: ' + ( 100 - ( currentTime - notificationDisplayStartTime ) / ( notifications[ currentDID ] ? notifications[ currentDID ].showDuration : 1 ) / 10 ) + '%'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import router from '@/router';
|
||||||
|
import {
|
||||||
|
type Ref, onUnmounted, ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
defineProps( {
|
||||||
|
'location': {
|
||||||
|
'type': String,
|
||||||
|
'default': 'topleft',
|
||||||
|
},
|
||||||
|
'size': {
|
||||||
|
'type': String,
|
||||||
|
'default': 'default',
|
||||||
|
}
|
||||||
|
// Size options: small, default (default option), big, bigger, huge
|
||||||
|
} );
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
'message': string;
|
||||||
|
'showDuration': number;
|
||||||
|
'messageType': string;
|
||||||
|
'priority': string;
|
||||||
|
'id': number;
|
||||||
|
'redirect'?: string;
|
||||||
|
'openInNewTab'?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationList {
|
||||||
|
[ key: string ]: Notification
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifications: Ref<NotificationList> = ref( {} );
|
||||||
|
const queue: Ref<number[]> = ref( [] );
|
||||||
|
const currentDID: Ref<number> = ref( 0 );
|
||||||
|
const messageType: Ref<string> = ref( 'hide' );
|
||||||
|
const currentID = ref( {
|
||||||
|
'critical': 0,
|
||||||
|
'medium': 1000,
|
||||||
|
'low': 10000
|
||||||
|
} );
|
||||||
|
const notificationDisplayStartTime: Ref<number> = ref( 0 );
|
||||||
|
const currentTime: Ref<number> = ref( 0 );
|
||||||
|
|
||||||
|
let progressBar = 0;
|
||||||
|
let notificationTimeout = 0;
|
||||||
|
|
||||||
|
const notificationAction = () => {
|
||||||
|
if ( notifications.value[ currentDID.value ] ) {
|
||||||
|
if ( notifications.value[ currentDID.value ].redirect ) {
|
||||||
|
if ( notifications.value[ currentDID.value ].openInNewTab ) {
|
||||||
|
window.open( notifications.value[ currentDID.value ].redirect ?? '' );
|
||||||
|
} else {
|
||||||
|
router.push( notifications.value[ currentDID.value ].redirect ?? '' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a notification that will be displayed using the internal notification scheduler
|
||||||
|
* @param {string} message The message to show. Can only be plain text (no HTML)
|
||||||
|
* @param {number} showDuration The duration in seconds for which to show the notification
|
||||||
|
* @param {string} msgType Type of notification to show. Will dictate how it looks: 'ok', 'error', 'info', 'warn', 'progress'
|
||||||
|
* @param {string} priority The priority of the message: 'low', 'normal', 'critical'
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const createNotification = (
|
||||||
|
message: string, showDuration: number, msgType: string, priority: string, redirect?: string, openInNewTab?: boolean
|
||||||
|
): number => {
|
||||||
|
/*
|
||||||
|
Takes a notification options array that contains: message, showDuration (in seconds), msgType (ok, error, progress, info) and priority (low, normal, critical).
|
||||||
|
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
|
||||||
|
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
|
||||||
|
*/
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
if ( priority === 'critical' ) {
|
||||||
|
currentID.value[ 'critical' ] += 1;
|
||||||
|
id = currentID.value[ 'critical' ];
|
||||||
|
} else if ( priority === 'normal' ) {
|
||||||
|
currentID.value[ 'medium' ] += 1;
|
||||||
|
id = currentID.value[ 'medium' ];
|
||||||
|
} else if ( priority === 'low' ) {
|
||||||
|
currentID.value[ 'low' ] += 1;
|
||||||
|
id = currentID.value[ 'low' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.value[ id ] = {
|
||||||
|
'message': message,
|
||||||
|
'showDuration': showDuration,
|
||||||
|
'messageType': msgType,
|
||||||
|
'priority': priority,
|
||||||
|
'id': id,
|
||||||
|
'redirect': redirect,
|
||||||
|
'openInNewTab': openInNewTab
|
||||||
|
};
|
||||||
|
queue.value.push( id );
|
||||||
|
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
|
||||||
|
|
||||||
|
if ( ( new Date().getTime() - notificationDisplayStartTime.value ) / 1000 >= ( notifications.value[ currentDID.value ] ? notifications.value[ currentDID.value ].showDuration : 0 ) || messageType.value === 'hide' ) {
|
||||||
|
handleNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a notification's message after creating it
|
||||||
|
* @param {number} id The notification ID returned by createNotification
|
||||||
|
* @param {string} message The new message
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const updateNotification = ( id: number, message: string ): void => {
|
||||||
|
if ( notifications.value[ id ] ) {
|
||||||
|
notifications.value[ id ].message = message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a previously created notification
|
||||||
|
* @param {string} id The notification ID returned by createNotification
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
const cancelNotification = ( id: number ): undefined => {
|
||||||
|
try {
|
||||||
|
delete notifications.value[ id ];
|
||||||
|
} catch ( error ) {
|
||||||
|
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
queue.value.splice( queue.value.indexOf( id ), 1 );
|
||||||
|
} catch {
|
||||||
|
console.debug( 'queue empty' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( currentDID.value == id ) {
|
||||||
|
try {
|
||||||
|
clearTimeout( notificationTimeout );
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
|
handleNotifications();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNotifications = () => {
|
||||||
|
notificationDisplayStartTime.value = new Date().getTime();
|
||||||
|
queue.value.sort();
|
||||||
|
|
||||||
|
if ( queue.value.length > 0 ) {
|
||||||
|
if ( currentDID.value !== 0 ) {
|
||||||
|
delete notifications.value[ currentDID.value ];
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDID.value = notifications.value[ queue.value[ 0 ] ][ 'id' ];
|
||||||
|
messageType.value = notifications.value[ queue.value[ 0 ] ].messageType;
|
||||||
|
queue.value.reverse();
|
||||||
|
queue.value.pop();
|
||||||
|
progressBar = setInterval( progressBarHandler, 25 );
|
||||||
|
notificationTimeout = setTimeout( () => {
|
||||||
|
handleNotifications();
|
||||||
|
}, notifications.value[ currentDID.value ].showDuration * 1000 );
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
clearInterval( progressBar );
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
|
messageType.value = 'hide';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressBarHandler = () => {
|
||||||
|
currentTime.value = new Date().getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted( () => {
|
||||||
|
try {
|
||||||
|
clearInterval( progressBar );
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearInterval( notificationTimeout );
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
|
} );
|
||||||
|
|
||||||
|
defineExpose( {
|
||||||
|
createNotification,
|
||||||
|
cancelNotification,
|
||||||
|
updateNotification
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.message-box {
|
||||||
|
position: fixed;
|
||||||
|
z-index: -100;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.5s;
|
||||||
|
width: 95vw;
|
||||||
|
right: 2.5vw;
|
||||||
|
top: 1vh;
|
||||||
|
height: 10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-notification {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
background: none;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-notification-icon {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.5s;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.types {
|
||||||
|
color: white;
|
||||||
|
border-radius: 100%;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: 5%;
|
||||||
|
padding: 1.5%;
|
||||||
|
font-size: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-right: calc( 5% + 30px );
|
||||||
|
text-align: end;
|
||||||
|
height: 90%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ok {
|
||||||
|
background-color: rgb(1, 71, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-ok {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: rgb(114, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-error {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background-color: rgb(44, 112, 151);
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-info {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-warning {
|
||||||
|
background-color: orangered;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
z-index: 100;
|
||||||
|
background-color: rgb(0, 0, 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-ok {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-spinner {
|
||||||
|
animation: spin 2s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate( 0deg );
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate( 720deg );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 750px) {
|
||||||
|
|
||||||
|
.default {
|
||||||
|
height: 10vh;
|
||||||
|
width: 32vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
height: 7vh;
|
||||||
|
width: 27vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big {
|
||||||
|
height: 12vh;
|
||||||
|
width: 38vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigger {
|
||||||
|
height: 15vh;
|
||||||
|
width: 43vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.huge {
|
||||||
|
height: 20vh;
|
||||||
|
width: 50vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topleft {
|
||||||
|
top: 3vh;
|
||||||
|
left: 0.5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topright {
|
||||||
|
top: 3vh;
|
||||||
|
right: 0.5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomright {
|
||||||
|
bottom: 3vh;
|
||||||
|
right: 0.5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomleft {
|
||||||
|
bottom: 3vh;
|
||||||
|
right: 0.5vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (min-width: 1500px) {
|
||||||
|
.default {
|
||||||
|
height: 10vh;
|
||||||
|
width: 15vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
height: 7vh;
|
||||||
|
width: 11vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big {
|
||||||
|
height: 12vh;
|
||||||
|
width: 17vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigger {
|
||||||
|
height: 15vh;
|
||||||
|
width: 20vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.huge {
|
||||||
|
height: 20vh;
|
||||||
|
width: 25vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1115
MusicPlayerV2-GUI/src/components/playerView.vue
Normal file
473
MusicPlayerV2-GUI/src/components/playlistView.vue
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Queue</h1>
|
||||||
|
<input
|
||||||
|
id="more-songs"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="audio/*"
|
||||||
|
class="small-buttons"
|
||||||
|
>
|
||||||
|
<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: Handle long AppleMusic Playlists, as AppleMusic doesn't automatically load all songs of a playlist -->
|
||||||
|
<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' )
|
||||||
|
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )"
|
||||||
|
>
|
||||||
|
<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 class="playing-symbols-wrapper">
|
||||||
|
<div id="bar-1" class="playing-bar"></div>
|
||||||
|
<div id="bar-2" class="playing-bar"></div>
|
||||||
|
<div id="bar-3" class="playing-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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 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
|
||||||
|
v-if="canBeMoved( 'up', song.id )"
|
||||||
|
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>
|
||||||
|
<input
|
||||||
|
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>
|
||||||
|
<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>
|
||||||
|
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// TODO: Add logout button
|
||||||
|
import type {
|
||||||
|
AppleMusicSongData, ReadFile, Song
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
computed, ref
|
||||||
|
} from 'vue';
|
||||||
|
import searchView from './searchView.vue';
|
||||||
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/stores/userStore';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const search = ref( searchView );
|
||||||
|
const props = defineProps( {
|
||||||
|
'playlist': {
|
||||||
|
'default': [],
|
||||||
|
'required': true,
|
||||||
|
'type': Array<Song>
|
||||||
|
},
|
||||||
|
'currentlyPlaying': {
|
||||||
|
'default': 0,
|
||||||
|
'required': true,
|
||||||
|
'type': Number,
|
||||||
|
},
|
||||||
|
'isPlaying': {
|
||||||
|
'default': true,
|
||||||
|
'required': true,
|
||||||
|
'type': Boolean,
|
||||||
|
},
|
||||||
|
'pos': {
|
||||||
|
'default': 0,
|
||||||
|
'required': false,
|
||||||
|
'type': Number,
|
||||||
|
},
|
||||||
|
'isLoggedIntoAppleMusic': {
|
||||||
|
'default': false,
|
||||||
|
'required': true,
|
||||||
|
'type': Boolean,
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
const hasSelectedSongs = ref( true );
|
||||||
|
const computedPlaylist = computed( () => {
|
||||||
|
let pl: Song[] = [];
|
||||||
|
|
||||||
|
// ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } );
|
||||||
|
for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) {
|
||||||
|
pl.push( props.playlist[ i ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return pl;
|
||||||
|
} );
|
||||||
|
|
||||||
|
const kbControl = ( action: string ) => {
|
||||||
|
if ( action === 'off' ) {
|
||||||
|
userStore.setKeyboardUsageStatus( false );
|
||||||
|
} else {
|
||||||
|
userStore.setKeyboardUsageStatus( true );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openSearch = () => {
|
||||||
|
if ( search.value ) {
|
||||||
|
search.value.controlSearch( 'show' );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canBeMoved = computed( () => {
|
||||||
|
return ( direction: movementDirection, songID: string ): boolean => {
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
for ( let song in props.playlist ) {
|
||||||
|
if ( props.playlist[ song ].id === songID ) {
|
||||||
|
id = parseInt( song );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( direction === 'up' ) {
|
||||||
|
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
const getTimeUntil = computed( () => {
|
||||||
|
return ( song: Song ) => {
|
||||||
|
let timeRemaining = 0;
|
||||||
|
|
||||||
|
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
|
||||||
|
if ( props.playlist[ i ] == song ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeRemaining += props.playlist[ i ].duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( props.isPlaying ) {
|
||||||
|
if ( timeRemaining === 0 ) {
|
||||||
|
return 'Currently playing';
|
||||||
|
} else {
|
||||||
|
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ( timeRemaining === 0 ) {
|
||||||
|
return 'Plays next';
|
||||||
|
} else {
|
||||||
|
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
|
const deleteSong = ( songID: string ) => {
|
||||||
|
for ( const song in props.playlist ) {
|
||||||
|
if ( props.playlist[ song ].id === songID ) {
|
||||||
|
emits( 'delete-song', parseInt( song ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearPlaylist = () => {
|
||||||
|
emits( 'clear-playlist', '' );
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const control = ( action: string ) => {
|
||||||
|
emits( 'control', action );
|
||||||
|
};
|
||||||
|
|
||||||
|
const play = ( song: string ) => {
|
||||||
|
emits( 'play-song', song );
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNewSongs = () => {
|
||||||
|
const fileURLList: ReadFile[] = [];
|
||||||
|
const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
|
||||||
|
|
||||||
|
if ( allFiles.length > 0 ) {
|
||||||
|
hasSelectedSongs.value = true;
|
||||||
|
|
||||||
|
for ( let file = 0; file < allFiles.length; file++ ) {
|
||||||
|
fileURLList.push( {
|
||||||
|
'url': URL.createObjectURL( allFiles[ file ] ),
|
||||||
|
'filename': allFiles[ file ].name
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
emits( 'add-new-songs', fileURLList );
|
||||||
|
} else {
|
||||||
|
hasSelectedSongs.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
|
||||||
|
const song: Song = {
|
||||||
|
'artist': songData.attributes.artistName,
|
||||||
|
'cover': songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
||||||
|
'duration': songData.attributes.durationInMillis / 1000,
|
||||||
|
'id': songData.id,
|
||||||
|
'origin': 'apple-music',
|
||||||
|
'title': songData.attributes.name
|
||||||
|
};
|
||||||
|
|
||||||
|
emits( 'add-new-songs-apple-music', song );
|
||||||
|
};
|
||||||
|
|
||||||
|
type movementDirection = 'up' | 'down';
|
||||||
|
|
||||||
|
const moveSong = ( songID: string, direction: movementDirection ) => {
|
||||||
|
let newSongPos = 0;
|
||||||
|
let hasFoundSongToMove = false;
|
||||||
|
|
||||||
|
for ( let el in props.playlist ) {
|
||||||
|
if ( props.playlist[ el ].id === songID ) {
|
||||||
|
const currPos = parseInt( el );
|
||||||
|
|
||||||
|
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
|
||||||
|
hasFoundSongToMove = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( hasFoundSongToMove ) {
|
||||||
|
emits( 'playlist-reorder', {
|
||||||
|
'songID': songID,
|
||||||
|
'newPos': newSongPos
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendAdditionalInfo = () => {
|
||||||
|
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'
|
||||||
|
] );
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.playlist-box {
|
||||||
|
height: calc( 100% - 150px );
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song {
|
||||||
|
border: solid var( --primary-color ) 1px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 80%;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 1vh;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song .song-cover {
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
font-size: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song img.song-cover {
|
||||||
|
font-size: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-title {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing-symbols {
|
||||||
|
position: absolute;
|
||||||
|
left: 1vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 0;
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
background-color: rgba( 0, 0, 0, 0.6 );
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing-symbols-wrapper {
|
||||||
|
width: 5rem;
|
||||||
|
height: 6rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing-bar {
|
||||||
|
height: 60%;
|
||||||
|
background-color: white;
|
||||||
|
width: 10%;
|
||||||
|
border-radius: 50px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bar-1 {
|
||||||
|
animation: music-playing 0.9s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bar-2 {
|
||||||
|
animation: music-playing 0.9s infinite ease-in-out;
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bar-3 {
|
||||||
|
animation: music-playing 0.9s infinite ease-in-out;
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes music-playing {
|
||||||
|
0% {
|
||||||
|
transform: scaleY( 1 );
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scaleY( 0.5 );
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleY( 1 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-icon, .pause-icon {
|
||||||
|
display: none;
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
font-size: 6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing:hover .pause-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing:hover .playing-symbols {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song:hover .song-cover {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-playing:hover .play-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-song .pause-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-song.not-playing .song-cover {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-song .song-image, .active-song:hover .pause-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-buttons {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-buttons .material-symbols-outlined {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var( --primary-color );
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-buttons:hover .material-symbols-outlined {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
137
MusicPlayerV2-GUI/src/components/playlistsView.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<div class="playlists">
|
||||||
|
<h3 style="width: fit-content;">
|
||||||
|
Your playlists
|
||||||
|
</h3>
|
||||||
|
<div v-if="( $props.playlists ? $props.playlists.length < 1 : true ) && $props.isLoggedIn">
|
||||||
|
Loading...
|
||||||
|
<!-- TODO: Make prettier -->
|
||||||
|
</div>
|
||||||
|
<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>Use the button below to load songs from your local disk</p>
|
||||||
|
<input
|
||||||
|
id="pl-loader"
|
||||||
|
class="pl-loader-button"
|
||||||
|
type="file"
|
||||||
|
multiple="true"
|
||||||
|
accept="audio/*"
|
||||||
|
><br>
|
||||||
|
<button id="load-button" class="pl-loader-button" @click="loadPlaylistFromDisk()">
|
||||||
|
Load
|
||||||
|
</button>
|
||||||
|
<p v-if="!hasSelectedSongs">
|
||||||
|
Please select at least one song to proceed!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="playlist-wrapper">
|
||||||
|
<div
|
||||||
|
v-for="pl in $props.playlists"
|
||||||
|
:key="pl.id"
|
||||||
|
class="playlist"
|
||||||
|
@click="selectPlaylist( pl.id )"
|
||||||
|
>
|
||||||
|
{{ pl.attributes.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {
|
||||||
|
ReadFile
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
const hasSelectedSongs = ref( true );
|
||||||
|
|
||||||
|
defineProps( {
|
||||||
|
'playlists': {
|
||||||
|
'default': [],
|
||||||
|
'type': Array<any>,
|
||||||
|
'required': true,
|
||||||
|
},
|
||||||
|
'isLoggedIn': {
|
||||||
|
'default': false,
|
||||||
|
'type': Boolean,
|
||||||
|
'required': true,
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
const loadPlaylistFromDisk = () => {
|
||||||
|
const fileURLList: ReadFile[] = [];
|
||||||
|
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? [];
|
||||||
|
|
||||||
|
if ( allFiles.length > 0 ) {
|
||||||
|
hasSelectedSongs.value = true;
|
||||||
|
|
||||||
|
for ( let file = 0; file < allFiles.length; file++ ) {
|
||||||
|
fileURLList.push( {
|
||||||
|
'url': URL.createObjectURL( allFiles[ file ] ),
|
||||||
|
'filename': allFiles[ file ].name
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
emits( 'custom-playlist', fileURLList );
|
||||||
|
} else {
|
||||||
|
hasSelectedSongs.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const emits = defineEmits( [
|
||||||
|
'selected-playlist',
|
||||||
|
'custom-playlist'
|
||||||
|
] );
|
||||||
|
|
||||||
|
const selectPlaylist = ( id: string ) => {
|
||||||
|
emits( 'selected-playlist', id );
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.playlists {
|
||||||
|
width: 100%;
|
||||||
|
height: 75vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-wrapper {
|
||||||
|
width: 85%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
border: solid var( --primary-color ) 1px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-loader-button {
|
||||||
|
background-color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#load-button {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-logged-in {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
304
MusicPlayerV2-GUI/src/components/popupModule.vue
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div :class="'popup-backdrop' + ( isShowingPopup ? '' : ' hidden' )" :style="'transform-origin: ' + transformOriginVertical + ';'">
|
||||||
|
<div class="popup-main">
|
||||||
|
<span class="material-symbols-outlined close-icon" @click="closePopup()">close</span>
|
||||||
|
<h2>{{ popupContent.title }}</h2>
|
||||||
|
<div v-if="popupContent.popupType === 'information' || popupContent.popupType === 'confirmation'" class="popup-content">
|
||||||
|
<p v-html="popupContent.subtitle"></p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="popup-content">
|
||||||
|
<p v-if="isShowingIncomplete" class="incomplete-message">{{ popupContent.incompleteMessage ? popupContent.incompleteMessage : 'Some entries are not filled out. Please fill them out to proceed.' }}</p>
|
||||||
|
<p v-html="popupContent.subtitle"></p>
|
||||||
|
<div v-for="content in popupContent.data" v-bind:key="content.id" class="popup-content-wrapper">
|
||||||
|
<div v-if="content.dataType === 'text'">
|
||||||
|
<label :for="'text-' + content.id">{{ content.name }}</label><br>
|
||||||
|
<input type="text" :id="'text-' + content.id" v-model="data[ content.id ]" class="input">
|
||||||
|
</div>
|
||||||
|
<div v-else-if="content.dataType === 'number'">
|
||||||
|
<label :for="'number-' + content.id">{{ content.name }}</label><br>
|
||||||
|
<input type="number" :id="'number-' + content.id" v-model="data[ content.id ]" class="input">
|
||||||
|
</div>
|
||||||
|
<div v-else-if="content.dataType === 'checkbox'">
|
||||||
|
<label :for="'checkbox-' + content.id">{{ content.name }}</label><br>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" v-model="data[ content.id ]">
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="content.dataType === 'textbox'">
|
||||||
|
<label :for="'textarea-' + content.id">{{ content.name }}</label><br>
|
||||||
|
<textarea :id="'textarea-' + content.id" v-model="data[ content.id ]" class="textarea"></textarea>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="content.dataType === 'colour'">
|
||||||
|
<label :for="'colour-' + content.id">{{ content.name }}</label><br>
|
||||||
|
<input type="text" :id="'colour-' + content.id" v-model="data[ content.id ]" class="input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<button @click="closePopup()" style="margin-right: 10px;" class="fancy-button" v-if="popupContent.popupType === 'confirmation'">Cancel</button>
|
||||||
|
<button @click="closePopupReturn()" class="fancy-button">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type PopupType = 'confirmation' | 'information' | 'input';
|
||||||
|
|
||||||
|
interface PopupData {
|
||||||
|
/**
|
||||||
|
* What to display to the user in front of the input field
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of data to display
|
||||||
|
*/
|
||||||
|
dataType: 'text' | 'number' | 'checkbox' | 'textbox' | 'colour';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID that is used for internal usage only. May only contain alphanumerical characters, as well as dashes and underscores
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PopupContent {
|
||||||
|
/**
|
||||||
|
* The title shown in big letters at the top of the popup
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (OPTIONAL) The subtitle shown to the user in normal sized letters below title
|
||||||
|
*/
|
||||||
|
subtitle?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (OPTIONAL) The message to show, if the user has not filled out all fields
|
||||||
|
*/
|
||||||
|
incompleteMessage?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of popup (i.e. what it is for)
|
||||||
|
*/
|
||||||
|
popupType: PopupType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (REQUIRED ONLY when popupType === 'input') The input fields to show
|
||||||
|
*/
|
||||||
|
data?: PopupData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
import "@melloware/coloris/dist/coloris.css";
|
||||||
|
import { ref, type Ref } from 'vue';
|
||||||
|
import Coloris from '@melloware/coloris';
|
||||||
|
|
||||||
|
Coloris.init();
|
||||||
|
|
||||||
|
const isShowingPopup = ref( false );
|
||||||
|
const transformOriginVertical = ref( '50% 50%' );
|
||||||
|
const data: Ref<Data> = ref( {} );
|
||||||
|
const isShowingIncomplete = ref( false );
|
||||||
|
|
||||||
|
const popupContent: Ref<PopupContent> = ref( {
|
||||||
|
title: 'Undefined popup title',
|
||||||
|
popupType: 'information',
|
||||||
|
subtitle: 'This popup was not configured correctly during development. Please send a bug-report at <a href="https://support.janishutz.com/index.php?a=add&category=7" target="_blank">support.janishutz.com</a> and inform us about where exactly you encountered this popup! We are sorry for the inconvenience'
|
||||||
|
} );
|
||||||
|
|
||||||
|
const closePopup = () => {
|
||||||
|
isShowingPopup.value = false;
|
||||||
|
Coloris.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePopupReturn = () => {
|
||||||
|
for ( let el in popupContent.value.data ) {
|
||||||
|
if ( !data.value[ popupContent.value.data[ parseInt( el ) ].id ] && popupContent.value.data[ parseInt( el ) ].dataType !== 'checkbox' ) {
|
||||||
|
isShowingIncomplete.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closePopup();
|
||||||
|
if ( popupContent.value.popupType === 'confirmation' ) {
|
||||||
|
emit( 'update', true );
|
||||||
|
} else {
|
||||||
|
emit( 'update', data.value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPopup = ( popupConfig: PopupContent, transformOrigin?: string ) => {
|
||||||
|
if ( transformOrigin ) {
|
||||||
|
transformOriginVertical.value = transformOrigin;
|
||||||
|
} else {
|
||||||
|
transformOriginVertical.value = '50% 50%';
|
||||||
|
}
|
||||||
|
data.value = {};
|
||||||
|
for ( let el in popupConfig.data ) {
|
||||||
|
if ( !popupConfig.data[ parseInt( el ) ].id ) {
|
||||||
|
console.warn( '[ popup ] Missing ID for input with name "' + popupConfig.data[ parseInt( el ) ].name + '"!' );
|
||||||
|
} else if ( popupConfig.data[ parseInt( el ) ].dataType === 'colour' ) {
|
||||||
|
Coloris( { el: '#colour-' + popupConfig.data[ parseInt( el ) ].id } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isShowingPopup.value = true;
|
||||||
|
popupContent.value = popupConfig;
|
||||||
|
for ( const el in popupContent.value.data ) {
|
||||||
|
if ( popupContent.value.data[ parseInt( el ) ].dataType === 'checkbox' ) {
|
||||||
|
data.value[ popupContent.value.data[ parseInt( el ) ].id ] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose( {
|
||||||
|
openPopup,
|
||||||
|
} );
|
||||||
|
|
||||||
|
const emit = defineEmits( [ 'update' ] );
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incomplete-message {
|
||||||
|
color: red;
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
position: unset;
|
||||||
|
height: 60%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
width: 80%;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: solid var( --primary-color ) 1px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: solid var( --primary-color ) 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content-wrapper {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_switch */
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + .slider {
|
||||||
|
box-shadow: 0 0 1px #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
-webkit-transform: translateX(26px);
|
||||||
|
-ms-transform: translateX(26px);
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
265
MusicPlayerV2-GUI/src/components/searchView.vue
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div id="search-bar" :class="showsSearch ? 'search-shown' : ''">
|
||||||
|
<div id="search-box-wrapper">
|
||||||
|
<input type="text" v-model="searchText" id="search-box" placeholder="Type to search..." @keyup="e => { keyHandler( e ) }">
|
||||||
|
<div class="symbol-wrapper" id="search-symbol-wrapper">
|
||||||
|
<span class="material-symbols-outlined search-symbol" @click="search()">search</span>
|
||||||
|
</div>
|
||||||
|
<div :class="'search-result-wrapper' + ( searchText.length > 0 ? ' show-search-results' : '' )">
|
||||||
|
<div v-for="result in searchResults" v-bind:key="result.id"
|
||||||
|
:class="'search-result' + ( selectedProduct === result.id ? ' prod-selected' : '' )"
|
||||||
|
@mouseenter="removeSelection()" @click="select( result )">
|
||||||
|
<div :style="'background-image: url(' + result.attributes.artwork.url.replace( '{w}', '500' ).replace( '{h}', '500' ) + ');'" class="search-product-image"></div>
|
||||||
|
<div class="search-product-name"><p><b>{{ result.attributes.name }}</b> <i>by {{ result.attributes.artistName }}</i></p></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="searchResults.length === 0 && searchText.length < 3">
|
||||||
|
<p>No results to show</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="searchText.length < 3">
|
||||||
|
<p>Enter at least three characters to start searching</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="symbol-wrapper">
|
||||||
|
<span class="material-symbols-outlined search-symbol" @click="controlSearch( 'hide' )" id="close-icon">close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MusicKitJSWrapper from '@/scripts/music-player';
|
||||||
|
import type { AppleMusicSongData } from '@/scripts/song';
|
||||||
|
import { useUserStore } from '@/stores/userStore';
|
||||||
|
import { ref, type Ref } from 'vue';
|
||||||
|
|
||||||
|
const showsSearch = ref( false );
|
||||||
|
const searchText = ref( '' );
|
||||||
|
const selectedProduct = ref( '' );
|
||||||
|
let selectedProductIndex = -1;
|
||||||
|
const player = new MusicKitJSWrapper();
|
||||||
|
|
||||||
|
const updateSearchResults = () => {
|
||||||
|
if ( searchText.value.length > 2 ) {
|
||||||
|
player.findSongOnAppleMusic( searchText.value ).then( data => {
|
||||||
|
searchResults.value = data.data.results.songs.data ?? [];
|
||||||
|
selectedProductIndex = -1;
|
||||||
|
selectedProduct.value = '';
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchResults: Ref<AppleMusicSongData[]> = ref( [] );
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const controlSearch = ( action: string ) => {
|
||||||
|
if ( action === 'show' ) {
|
||||||
|
userStore.setKeyboardUsageStatus( true );
|
||||||
|
showsSearch.value = true;
|
||||||
|
setTimeout( () => {
|
||||||
|
const searchBox = document.getElementById( 'search-box' ) as HTMLInputElement;
|
||||||
|
searchBox.focus();
|
||||||
|
}, 500 );
|
||||||
|
} else if ( action === 'hide' ) {
|
||||||
|
userStore.setKeyboardUsageStatus( false );
|
||||||
|
showsSearch.value = false;
|
||||||
|
}
|
||||||
|
searchText.value = '';
|
||||||
|
removeSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSelection = () => {
|
||||||
|
selectedProduct.value = '';
|
||||||
|
selectedProductIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyHandler = ( e: KeyboardEvent ) => {
|
||||||
|
if ( e.key === 'Escape' ) {
|
||||||
|
controlSearch( 'hide' );
|
||||||
|
} else if ( e.key === 'Enter' ) {
|
||||||
|
e.preventDefault();
|
||||||
|
if ( selectedProductIndex >= 0 ) {
|
||||||
|
select( searchResults.value[ selectedProductIndex ] );
|
||||||
|
controlSearch( 'hide' );
|
||||||
|
} else {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
} else if ( e.key === 'ArrowDown' ) {
|
||||||
|
e.preventDefault();
|
||||||
|
if ( selectedProductIndex < searchResults.value.length - 1 ) {
|
||||||
|
selectedProductIndex += 1;
|
||||||
|
selectedProduct.value = searchResults.value[ selectedProductIndex ].id;
|
||||||
|
}
|
||||||
|
} else if ( e.key === 'ArrowUp' ) {
|
||||||
|
e.preventDefault();
|
||||||
|
if ( selectedProductIndex > 0 ) {
|
||||||
|
selectedProductIndex -= 1;
|
||||||
|
selectedProduct.value = searchResults.value[ selectedProductIndex ].id;
|
||||||
|
} else {
|
||||||
|
removeSelection();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateSearchResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = ( song: AppleMusicSongData ) => {
|
||||||
|
emits( 'selected-song', song );
|
||||||
|
controlSearch( 'hide' );
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = () => {
|
||||||
|
emits( 'selected-song', searchResults.value[ 0 ] );
|
||||||
|
controlSearch( 'hide' );
|
||||||
|
}
|
||||||
|
|
||||||
|
const emits = defineEmits( [ 'selected-song' ] );
|
||||||
|
|
||||||
|
|
||||||
|
defineExpose( {
|
||||||
|
controlSearch
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#search-bar {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
top: -15vh;
|
||||||
|
background-color: var( --accent-background );
|
||||||
|
height: 15vh;
|
||||||
|
width: 100vw;
|
||||||
|
left: 0;
|
||||||
|
transition: all 1s;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-bar.search-shown {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-box {
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-box-wrapper {
|
||||||
|
width: 60vw;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 1000px) {
|
||||||
|
#search-box-wrapper {
|
||||||
|
width: 45vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 1500px) {
|
||||||
|
#search-box-wrapper {
|
||||||
|
width: 30vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-symbol-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol-wrapper {
|
||||||
|
display: flex;
|
||||||
|
height: 3rem;
|
||||||
|
width: 3rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-symbol {
|
||||||
|
color: black;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 200%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-symbol:hover {
|
||||||
|
font-size: 250%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#close-icon {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: var( --primary-color );
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-result-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
left: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: white;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
transform-origin: top;
|
||||||
|
transform: scaleY( 0 );
|
||||||
|
transition: all 0.5s;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-search-results {
|
||||||
|
transform: scaleY( 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result {
|
||||||
|
padding: 3px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 98%;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.5s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result:hover, .prod-selected {
|
||||||
|
text-decoration: underline black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-product-image {
|
||||||
|
height: 4rem;
|
||||||
|
width: 4rem;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
margin-right: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-product-name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: calc( 100% - 4rem - 20px );
|
||||||
|
}
|
||||||
|
</style>
|
||||||
145
MusicPlayerV2-GUI/src/components/sliderView.vue
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<div style="width: 100%; height: 100%;">
|
||||||
|
<progress :id="'progress-slider-' + name" class="progress-slider" :value="sliderProgress" max="1000" @mousedown="( e ) => { setPos( e ) }"
|
||||||
|
:class="active ? '' : 'slider-inactive'"></progress>
|
||||||
|
<div v-if="active" id="slider-knob" @mousedown="( e ) => { startMove( e ) }"
|
||||||
|
:style="'left: ' + ( originalPos + sliderPos ) + 'px;'">
|
||||||
|
<div id="slider-knob-style"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else id="slider-knob" class="slider-inactive" style="left: 0;">
|
||||||
|
<div id="slider-knob-style"></div>
|
||||||
|
</div>
|
||||||
|
<div id="drag-support" @mousemove="e => { handleDrag( e ) }" @mouseup="() => { stopMove(); }"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.progress-slider {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var( --slider-color );
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-slider::-webkit-progress-value {
|
||||||
|
background-color: var( --slider-color );
|
||||||
|
}
|
||||||
|
|
||||||
|
#slider-knob {
|
||||||
|
height: 20px;
|
||||||
|
width: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-end;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
#slider-knob-style {
|
||||||
|
background-color: var( --slider-color );
|
||||||
|
height: 15px;
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#drag-support {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-support-active {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-inactive {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps( {
|
||||||
|
style: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
type: Number,
|
||||||
|
default: 100
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '1',
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
const offset = ref( 0 );
|
||||||
|
const isDragging = ref( false );
|
||||||
|
const sliderPos = ref( 0 );
|
||||||
|
const originalPos= ref( 0 );
|
||||||
|
const sliderProgress = ref( 0 );
|
||||||
|
|
||||||
|
const handleDrag = ( e: MouseEvent ) => {
|
||||||
|
if ( isDragging.value ) {
|
||||||
|
if ( 0 < originalPos.value + e.screenX - offset.value && originalPos.value + e.screenX - offset.value < ( document.getElementById( 'progress-slider-' + props.name ) as HTMLProgressElement ).clientWidth - 5 ) {
|
||||||
|
sliderPos.value = e.screenX - offset.value;
|
||||||
|
calcProgressPos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const startMove = ( e: MouseEvent ) => {
|
||||||
|
offset.value = e.screenX;
|
||||||
|
isDragging.value = true;
|
||||||
|
( document.getElementById( 'drag-support' ) as HTMLDivElement ).classList.add( 'drag-support-active' );
|
||||||
|
}
|
||||||
|
const stopMove = () => {
|
||||||
|
originalPos.value += sliderPos.value;
|
||||||
|
isDragging.value = false;
|
||||||
|
offset.value = 0;
|
||||||
|
sliderPos.value = 0;
|
||||||
|
( document.getElementById( 'drag-support' ) as HTMLDivElement ).classList.remove( 'drag-support-active' );
|
||||||
|
calcPlaybackPos();
|
||||||
|
}
|
||||||
|
const setPos = ( e: MouseEvent ) => {
|
||||||
|
if ( props.active ) {
|
||||||
|
originalPos.value = e.offsetX;
|
||||||
|
calcProgressPos();
|
||||||
|
calcPlaybackPos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const calcProgressPos = () => {
|
||||||
|
sliderProgress.value = Math.ceil( ( originalPos.value + sliderPos.value ) / ( ( document.getElementById( 'progress-slider-' + props.name ) as HTMLProgressElement ).clientWidth - 5 ) * 1000 );
|
||||||
|
}
|
||||||
|
const calcPlaybackPos = () => {
|
||||||
|
emits( 'pos', Math.round( ( originalPos.value + sliderPos.value ) / ( ( document.getElementById( 'progress-slider-' + props.name ) as HTMLProgressElement ).clientWidth - 5 ) * props.duration ) );
|
||||||
|
}
|
||||||
|
watch( () => props.position, () => {
|
||||||
|
if ( !isDragging.value ) {
|
||||||
|
sliderProgress.value = Math.ceil( props.position / props.duration * 1000 + 2 );
|
||||||
|
originalPos.value = Math.ceil( props.position / props.duration * ( ( document.getElementById( 'progress-slider-' + props.name ) as HTMLProgressElement ).scrollWidth - 5 ) );
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
|
||||||
|
const emits = defineEmits( [ 'pos' ] );
|
||||||
|
</script>
|
||||||
26
MusicPlayerV2-GUI/src/main.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import App from './App.vue';
|
||||||
|
import {
|
||||||
|
createApp
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
createPinia
|
||||||
|
} from 'pinia';
|
||||||
|
import router from './router';
|
||||||
|
import sdk from '@janishutz/login-sdk-browser';
|
||||||
|
|
||||||
|
|
||||||
|
const app = createApp( App );
|
||||||
|
|
||||||
|
app.use( createPinia() );
|
||||||
|
app.use( router );
|
||||||
|
|
||||||
|
// localStorage.setItem( 'url', 'http://localhost:8082' );
|
||||||
|
localStorage.setItem( 'url', 'https://music-api.janishutz.com' );
|
||||||
|
sdk.setUp(
|
||||||
|
'jh-music',
|
||||||
|
String( localStorage.getItem( 'url' ) ),
|
||||||
|
'/app',
|
||||||
|
false // Set to false for deploy to actual backend
|
||||||
|
);
|
||||||
|
|
||||||
|
app.mount( '#app' );
|
||||||
96
MusicPlayerV2-GUI/src/router/index.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
import HomeView from '@/views/HomeView.vue';
|
||||||
|
import { useUserStore } from '@/stores/userStore';
|
||||||
|
|
||||||
|
const router = createRouter( {
|
||||||
|
history: createWebHistory( import.meta.env.BASE_URL ),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView,
|
||||||
|
meta: {
|
||||||
|
'authRequired': false,
|
||||||
|
'title': 'Login'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/app',
|
||||||
|
name: 'app',
|
||||||
|
component: () => import( '../views/AppView.vue' ),
|
||||||
|
meta: {
|
||||||
|
'authRequired': true,
|
||||||
|
'title': 'App'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/get',
|
||||||
|
name: 'get',
|
||||||
|
component: () => import( '../views/GetView.vue' ),
|
||||||
|
meta: {
|
||||||
|
'authRequired': false,
|
||||||
|
'title': 'Get'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/share/:name',
|
||||||
|
name: 'share',
|
||||||
|
component: () => import( '../views/RemoteView.vue' ),
|
||||||
|
meta: {
|
||||||
|
'authRequired': false,
|
||||||
|
'title': 'Share'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/fancy/:name',
|
||||||
|
name: 'fancy',
|
||||||
|
component: () => import( '../views/ShowcaseView.vue' ),
|
||||||
|
meta: {
|
||||||
|
'authRequired': false,
|
||||||
|
'title': 'Fancy View'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/tools/bar',
|
||||||
|
name: 'tool-bar',
|
||||||
|
component: () => import( '../views/BarView.vue' ),
|
||||||
|
meta: {
|
||||||
|
'authRequired': false,
|
||||||
|
'title': 'Bar utility'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: () => import( '../views/404View.vue' ),
|
||||||
|
meta: {
|
||||||
|
title: '404 :: Page not found',
|
||||||
|
transition: 'scale',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} );
|
||||||
|
|
||||||
|
// router.beforeResolve( ( to, _from, next ) => {
|
||||||
|
// if ( to.name ) {
|
||||||
|
// NProgress.start();
|
||||||
|
// }
|
||||||
|
// next();
|
||||||
|
// } );
|
||||||
|
|
||||||
|
router.beforeEach( ( to ) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const isUserAuthenticated = userStore.getUserAuthenticated;
|
||||||
|
if ( !isUserAuthenticated && to.meta.authRequired ) {
|
||||||
|
localStorage.setItem( 'redirect', to.fullPath );
|
||||||
|
return { name: 'home' };
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
router.afterEach( ( to ) => {
|
||||||
|
window.scrollTo( { top: 0, behavior: 'smooth' } );
|
||||||
|
document.title = to.meta.title ? to.meta.title + ' - MusicPlayer' : 'MusicPlayer';
|
||||||
|
// NProgress.done();
|
||||||
|
} );
|
||||||
|
|
||||||
|
export default router;
|
||||||
161
MusicPlayerV2-GUI/src/scripts/bizualizer.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import ColorThief from 'colorthief';
|
||||||
|
|
||||||
|
const colorThief = new ColorThief();
|
||||||
|
|
||||||
|
const getImageData = (): Promise<number[][]> => {
|
||||||
|
return new Promise( resolve => {
|
||||||
|
const img = document.getElementById( 'current-image' ) as HTMLImageElement;
|
||||||
|
|
||||||
|
if ( img.complete ) {
|
||||||
|
resolve( colorThief.getPalette( img ) );
|
||||||
|
} else {
|
||||||
|
img.addEventListener( 'load', () => {
|
||||||
|
resolve( colorThief.getPalette( img ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBackground = () => {
|
||||||
|
return new Promise( resolve => {
|
||||||
|
getImageData().then( palette => {
|
||||||
|
const colourDetails: number[][] = [];
|
||||||
|
const colours: string[] = [];
|
||||||
|
|
||||||
|
let differentEnough = true;
|
||||||
|
|
||||||
|
if ( palette[ 0 ] ) {
|
||||||
|
for ( const i in palette ) {
|
||||||
|
for ( const colour in colourDetails ) {
|
||||||
|
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - palette[ i ][ 0 ] ) / 255
|
||||||
|
+ Math.abs( colourDetails[ colour ][ 1 ] - palette[ i ][ 1 ] ) / 255
|
||||||
|
+ Math.abs( colourDetails[ colour ][ 2 ] - palette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
||||||
|
|
||||||
|
if ( colourDiff > 15 ) {
|
||||||
|
differentEnough = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( differentEnough ) {
|
||||||
|
colourDetails.push( palette[ i ] );
|
||||||
|
colours.push( 'rgb(' + palette[ i ][ 0 ] + ',' + palette[ i ][ 1 ] + ',' + palette[ i ][ 2 ] + ')' );
|
||||||
|
}
|
||||||
|
|
||||||
|
differentEnough = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let outColours = 'conic-gradient(';
|
||||||
|
|
||||||
|
if ( colours.length < 3 ) {
|
||||||
|
for ( let i = 0; i < 3; i++ ) {
|
||||||
|
if ( colours[ i ] ) {
|
||||||
|
outColours += colours[ i ] + ',';
|
||||||
|
} else {
|
||||||
|
if ( i === 0 ) {
|
||||||
|
outColours += 'blue,';
|
||||||
|
} else if ( i === 1 ) {
|
||||||
|
outColours += 'green,';
|
||||||
|
} else if ( i === 2 ) {
|
||||||
|
outColours += 'red,';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ( colours.length < 11 ) {
|
||||||
|
for ( const i in colours ) {
|
||||||
|
outColours += colours[ i ] + ',';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for ( let i = 0; i < 10; i++ ) {
|
||||||
|
outColours += colours[ i ] + ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outColours += colours[ 0 ] ?? 'blue' + ')';
|
||||||
|
resolve( outColours );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
let callbackFun = () => {};
|
||||||
|
|
||||||
|
const subscribeToBeatUpdate = ( cb: () => void ) => {
|
||||||
|
callbackFun = cb;
|
||||||
|
micAudioHandler();
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribeFromBeatUpdate = () => {
|
||||||
|
callbackFun = () => {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearInterval( micAnalyzer );
|
||||||
|
} catch ( e ) { /* empty */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
const coolDown = () => {
|
||||||
|
beatDetected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let micAnalyzer = 0;
|
||||||
|
let beatDetected = false;
|
||||||
|
|
||||||
|
const micAudioHandler = () => {
|
||||||
|
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
||||||
|
const analyser = audioContext.createAnalyser();
|
||||||
|
|
||||||
|
analyser.fftSize = 256;
|
||||||
|
const bufferLength = analyser.frequencyBinCount;
|
||||||
|
const dataArray = new Uint8Array( bufferLength );
|
||||||
|
|
||||||
|
beatDetected = false;
|
||||||
|
|
||||||
|
navigator.mediaDevices.getUserMedia( {
|
||||||
|
'audio': true
|
||||||
|
} ).then( stream => {
|
||||||
|
const mic = audioContext.createMediaStreamSource( stream );
|
||||||
|
|
||||||
|
mic.connect( analyser );
|
||||||
|
analyser.getByteFrequencyData( dataArray );
|
||||||
|
let prevSpectrum: number[] = [];
|
||||||
|
|
||||||
|
const threshold = 10; // Adjust as needed
|
||||||
|
|
||||||
|
micAnalyzer = setInterval( () => {
|
||||||
|
analyser.getByteFrequencyData( dataArray );
|
||||||
|
// Convert the frequency data to a numeric array
|
||||||
|
const currentSpectrum = Array.from( dataArray );
|
||||||
|
|
||||||
|
if ( prevSpectrum ) {
|
||||||
|
// Calculate the spectral flux
|
||||||
|
const flux = calculateSpectralFlux( prevSpectrum, currentSpectrum );
|
||||||
|
|
||||||
|
if ( flux > threshold && !beatDetected ) {
|
||||||
|
// Beat detected
|
||||||
|
beatDetected = true;
|
||||||
|
callbackFun();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSpectrum = currentSpectrum;
|
||||||
|
}, 60 / 180 * 250 );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSpectralFlux = ( prevSpectrum: number[], currentSpectrum: number[] ) => {
|
||||||
|
let flux = 0;
|
||||||
|
|
||||||
|
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
||||||
|
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
||||||
|
|
||||||
|
flux += Math.max( 0, diff );
|
||||||
|
}
|
||||||
|
|
||||||
|
return flux;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createBackground,
|
||||||
|
subscribeToBeatUpdate,
|
||||||
|
unsubscribeFromBeatUpdate,
|
||||||
|
coolDown,
|
||||||
|
};
|
||||||
212
MusicPlayerV2-GUI/src/scripts/connection.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// These functions handle connections to the backend with socket.io
|
||||||
|
|
||||||
|
import {
|
||||||
|
io, type Socket
|
||||||
|
} from 'socket.io-client';
|
||||||
|
import type {
|
||||||
|
SSEMap
|
||||||
|
} from './song';
|
||||||
|
|
||||||
|
class SocketConnection {
|
||||||
|
|
||||||
|
socket: Socket;
|
||||||
|
|
||||||
|
roomName: string;
|
||||||
|
|
||||||
|
isConnected: boolean;
|
||||||
|
|
||||||
|
useSocket: boolean;
|
||||||
|
|
||||||
|
eventSource?: EventSource;
|
||||||
|
|
||||||
|
toBeListenedForItems: SSEMap;
|
||||||
|
|
||||||
|
reconnectRetryCount: number;
|
||||||
|
|
||||||
|
openConnectionsCount: number;
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||||
|
'autoConnect': false,
|
||||||
|
} );
|
||||||
|
this.roomName = location.pathname.split( '/' )[ 2 ];
|
||||||
|
this.isConnected = false;
|
||||||
|
this.useSocket = localStorage.getItem( 'music-player-config' ) === 'ws';
|
||||||
|
this.toBeListenedForItems = {};
|
||||||
|
this.reconnectRetryCount = 0;
|
||||||
|
this.openConnectionsCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a room token and connect to
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
connect (): Promise<unknown> {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
if ( this.reconnectRetryCount < 5 ) {
|
||||||
|
if ( this.useSocket ) {
|
||||||
|
this.socket.connect();
|
||||||
|
this.socket.emit(
|
||||||
|
'join-room', this.roomName, ( res: {
|
||||||
|
'status': boolean,
|
||||||
|
'msg': string,
|
||||||
|
'data': unknown
|
||||||
|
} ) => {
|
||||||
|
if ( res.status === true ) {
|
||||||
|
this.isConnected = true;
|
||||||
|
resolve( res.data );
|
||||||
|
} else {
|
||||||
|
console.debug( res.msg );
|
||||||
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||||
|
this.openConnectionsCount += 1;
|
||||||
|
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
|
if ( res.status === 200 ) {
|
||||||
|
this.eventSource
|
||||||
|
= new EventSource( localStorage.getItem( 'url' )
|
||||||
|
+ '/socket/connection?room=' + this.roomName, {
|
||||||
|
'withCredentials': true
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.eventSource.onopen = () => {
|
||||||
|
this.isConnected = true;
|
||||||
|
this.reconnectRetryCount = 0;
|
||||||
|
console.log( '[ SSE Connection ] - '
|
||||||
|
+ new Date().toISOString() + ': Connection successfully established!' );
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventSource.onmessage = e => {
|
||||||
|
const d = JSON.parse( e.data );
|
||||||
|
|
||||||
|
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||||
|
this.toBeListenedForItems[ d.type ]( d.data );
|
||||||
|
} else if ( d.type === 'basics' ) {
|
||||||
|
resolve( d.data );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventSource.onerror = () => {
|
||||||
|
if ( this.isConnected ) {
|
||||||
|
this.isConnected = false;
|
||||||
|
this.openConnectionsCount -= 1;
|
||||||
|
this.eventSource?.close();
|
||||||
|
console.log( '[ SSE Connection ] - '
|
||||||
|
+ new Date().toISOString()
|
||||||
|
+ ': Reconnecting due to connection error!' );
|
||||||
|
// console.debug( e );
|
||||||
|
|
||||||
|
this.eventSource = undefined;
|
||||||
|
|
||||||
|
this.reconnectRetryCount += 1;
|
||||||
|
setTimeout( () => {
|
||||||
|
this.connect();
|
||||||
|
}, 1000 * this.reconnectRetryCount );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log( '[ SSE Connection ] - '
|
||||||
|
+ new Date().toISOString()
|
||||||
|
+ ': Could not connect due to error ' + res.status );
|
||||||
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
console.log( '[ SSE Connection ] - '
|
||||||
|
+ new Date().toISOString()
|
||||||
|
+ ': Could not connect due to error.' );
|
||||||
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
console.log( '[ SSE Connection ]: Trimmed connections' );
|
||||||
|
reject( 'ERR_TOO_MANY_CONNECTIONS' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert( 'Could not reconnect to the share. Please reload the page to retry!' );
|
||||||
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit an event
|
||||||
|
* @param {string} event The event to emit
|
||||||
|
* @param {any} data
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
emit ( event: string, data: unknown ): void {
|
||||||
|
if ( this.isConnected ) {
|
||||||
|
if ( this.useSocket ) {
|
||||||
|
this.socket.emit( event, {
|
||||||
|
'roomName': this.roomName,
|
||||||
|
'data': data
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||||
|
'method': 'post',
|
||||||
|
'body': JSON.stringify( {
|
||||||
|
'event': event,
|
||||||
|
'roomName': this.roomName,
|
||||||
|
'data': data
|
||||||
|
} ),
|
||||||
|
'credentials': 'include',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'charset': 'utf-8'
|
||||||
|
}
|
||||||
|
} ).catch( () => {} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a listener function for an event
|
||||||
|
* @param {string} event The event to listen for
|
||||||
|
* @param {( data: any ) => void} cb The callback function / listener function
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
registerListener ( event: string, cb: ( data: unknown ) => void ): void {
|
||||||
|
if ( this.useSocket ) {
|
||||||
|
if ( this.isConnected ) {
|
||||||
|
this.socket.on( event, cb );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toBeListenedForItems[ event ] = cb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the server
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
disconnect (): void {
|
||||||
|
if ( this.isConnected ) {
|
||||||
|
if ( this.useSocket ) {
|
||||||
|
this.socket.disconnect();
|
||||||
|
} else {
|
||||||
|
this.eventSource!.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus (): boolean {
|
||||||
|
if ( this.useSocket ) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if ( this.eventSource ) {
|
||||||
|
return this.eventSource!.OPEN && this.isConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SocketConnection;
|
||||||
571
MusicPlayerV2-GUI/src/scripts/music-player.ts
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
import type {
|
||||||
|
SearchResult, Song, SongMove
|
||||||
|
} from './song';
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
'devToken': string;
|
||||||
|
'userToken': string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlAction = 'play' | 'pause' | 'next' | 'previous' | 'skip-10' | 'back-10';
|
||||||
|
type RepeatMode = 'off' | 'once' | 'all';
|
||||||
|
|
||||||
|
class MusicKitJSWrapper {
|
||||||
|
|
||||||
|
playingSongID: number;
|
||||||
|
|
||||||
|
playlist: Song[];
|
||||||
|
|
||||||
|
queue: number[];
|
||||||
|
|
||||||
|
config: Config;
|
||||||
|
|
||||||
|
musicKit: any;
|
||||||
|
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
|
||||||
|
isPreparedToPlay: boolean;
|
||||||
|
|
||||||
|
repeatMode: RepeatMode;
|
||||||
|
|
||||||
|
isShuffleEnabled: boolean;
|
||||||
|
|
||||||
|
hasEncounteredAuthError: boolean;
|
||||||
|
|
||||||
|
queuePos: number;
|
||||||
|
|
||||||
|
audioPlayer: HTMLAudioElement;
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.playingSongID = 0;
|
||||||
|
this.playlist = [];
|
||||||
|
this.queue = [];
|
||||||
|
this.config = {
|
||||||
|
'devToken': '',
|
||||||
|
'userToken': '',
|
||||||
|
};
|
||||||
|
this.isShuffleEnabled = false;
|
||||||
|
this.repeatMode = 'off';
|
||||||
|
this.isPreparedToPlay = false;
|
||||||
|
this.isLoggedIn = false;
|
||||||
|
this.hasEncounteredAuthError = false;
|
||||||
|
this.queuePos = 0;
|
||||||
|
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
if ( !window.MusicKit ) {
|
||||||
|
document.addEventListener( 'musickitloaded', () => {
|
||||||
|
self.init();
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a user into Apple Music. Will automatically initialize MusicKitJS, once user is logged in
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
logIn (): void {
|
||||||
|
if ( !this.musicKit.isAuthorized ) {
|
||||||
|
this.musicKit.authorize().then( () => {
|
||||||
|
this.isLoggedIn = true;
|
||||||
|
this.init();
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
this.hasEncounteredAuthError = true;
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
this.musicKit.authorize().then( () => {
|
||||||
|
this.isLoggedIn = true;
|
||||||
|
this.init();
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
this.hasEncounteredAuthError = true;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize MusicKitJS. Should not be called. Use logIn instead, which first tries to log the user in, then calls this method.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
init (): void {
|
||||||
|
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
|
if ( res.status === 200 ) {
|
||||||
|
res.text().then( token => {
|
||||||
|
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||||
|
// MusicKit global is now defined
|
||||||
|
MusicKit.configure( {
|
||||||
|
'developerToken': token,
|
||||||
|
'app': {
|
||||||
|
'name': 'MusicPlayer',
|
||||||
|
'build': '3'
|
||||||
|
},
|
||||||
|
'storefrontId': 'CH',
|
||||||
|
} ).then( () => {
|
||||||
|
this.config.devToken = token;
|
||||||
|
this.musicKit = MusicKit.getInstance();
|
||||||
|
|
||||||
|
if ( this.musicKit.isAuthorized ) {
|
||||||
|
this.isLoggedIn = true;
|
||||||
|
this.config.userToken = this.musicKit.musicUserToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.musicKit.shuffleMode = MusicKit.PlayerShuffleMode.off;
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the authentication status of the user
|
||||||
|
* @returns {boolean[]} Returns an array, where the first element indicates login status, the second one, if an error was encountered
|
||||||
|
*/
|
||||||
|
getAuth (): boolean[] {
|
||||||
|
return [
|
||||||
|
this.isLoggedIn,
|
||||||
|
this.hasEncounteredAuthError
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request data from the Apple Music API
|
||||||
|
* @param {string} url The URL at the Apple Music API to call (including protocol and url)
|
||||||
|
* @param {( data: object ) => void} callback A callback function that takes the data and returns nothing
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
apiGetRequest ( url: string, callback: ( data: object ) => void ): void {
|
||||||
|
if ( this.config.devToken != '' && this.config.userToken != '' ) {
|
||||||
|
fetch( url, {
|
||||||
|
'method': 'GET',
|
||||||
|
'headers': {
|
||||||
|
'Authorization': `Bearer ${ this.config.devToken }`,
|
||||||
|
'Music-User-Token': this.config.userToken
|
||||||
|
}
|
||||||
|
} ).then( res => {
|
||||||
|
if ( res.status === 200 ) {
|
||||||
|
res.json().then( json => {
|
||||||
|
try {
|
||||||
|
callback( {
|
||||||
|
'status': 'ok',
|
||||||
|
'data': json
|
||||||
|
} );
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
callback( {
|
||||||
|
'status': 'error',
|
||||||
|
'error': res.status
|
||||||
|
} );
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} else return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the playlist to play
|
||||||
|
* @param {Song[]} playlist The playlist as an array of songs
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
setPlaylist ( playlist: Song[] ): void {
|
||||||
|
this.playlist = playlist;
|
||||||
|
this.setShuffle( this.isShuffleEnabled );
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlaylistByID ( id: string ): Promise<void> {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
this.musicKit.setQueue( {
|
||||||
|
'playlist': id
|
||||||
|
} ).then( () => {
|
||||||
|
const pl = this.musicKit.queue.items;
|
||||||
|
const songs: Song[] = [];
|
||||||
|
|
||||||
|
for ( const item in pl ) {
|
||||||
|
let url = pl[ item ].attributes.artwork.url;
|
||||||
|
|
||||||
|
url = url.replace( '{w}', pl[ item ].attributes.artwork.width );
|
||||||
|
url = url.replace( '{h}', pl[ item ].attributes.artwork.height );
|
||||||
|
const song: Song = {
|
||||||
|
'artist': pl[ item ].attributes.artistName,
|
||||||
|
'cover': url,
|
||||||
|
'duration': pl[ item ].attributes.durationInMillis / 1000,
|
||||||
|
'id': pl[ item ].id,
|
||||||
|
'origin': 'apple-music',
|
||||||
|
'title': pl[ item ].attributes.name,
|
||||||
|
'genres': pl[ item ].attributes.genreNames
|
||||||
|
};
|
||||||
|
|
||||||
|
songs.push( song );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playlist = songs;
|
||||||
|
this.setShuffle( this.isShuffleEnabled );
|
||||||
|
this.queuePos = 0;
|
||||||
|
this.playingSongID = this.queue[ 0 ];
|
||||||
|
this.prepare( this.playingSongID );
|
||||||
|
resolve();
|
||||||
|
} )
|
||||||
|
.catch( err => {
|
||||||
|
console.error( err );
|
||||||
|
reject( err );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare a specific song in the queue for playing and start playing
|
||||||
|
* @param {number} playlistID The ID of the song in the playlist to prepare to play
|
||||||
|
* @returns {boolean} Returns true, if successful, false, if playlist is missing / empty. Set that first
|
||||||
|
*/
|
||||||
|
prepare ( playlistID: number ): boolean {
|
||||||
|
if ( this.playlist.length > 0 ) {
|
||||||
|
this.playingSongID = playlistID;
|
||||||
|
this.isPreparedToPlay = true;
|
||||||
|
|
||||||
|
for ( const el in this.queue ) {
|
||||||
|
if ( this.queue[ el ] === playlistID ) {
|
||||||
|
this.queuePos = parseInt( el );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
|
this.musicKit.setQueue( {
|
||||||
|
'song': this.playlist[ this.playingSongID ].id
|
||||||
|
} ).then( () => {
|
||||||
|
setTimeout( () => {
|
||||||
|
this.control( 'play' );
|
||||||
|
}, 500 );
|
||||||
|
} )
|
||||||
|
.catch( err => {
|
||||||
|
console.log( err );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
this.audioPlayer = document.getElementById( 'local-audio' ) as HTMLAudioElement;
|
||||||
|
this.audioPlayer.src = this.playlist[ this.playingSongID ].id;
|
||||||
|
setTimeout( () => {
|
||||||
|
this.control( 'play' );
|
||||||
|
}, 500 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control the player
|
||||||
|
* @param {ControlAction} action Action to take on the player
|
||||||
|
* @returns {boolean} returns a boolean indicating if there was a change in song.
|
||||||
|
*/
|
||||||
|
control ( action: ControlAction ): boolean {
|
||||||
|
switch ( action ) {
|
||||||
|
case 'play':
|
||||||
|
if ( this.isPreparedToPlay ) {
|
||||||
|
this.control( 'pause' );
|
||||||
|
|
||||||
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
|
this.musicKit.play();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.audioPlayer.play();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'pause':
|
||||||
|
if ( this.isPreparedToPlay ) {
|
||||||
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
|
this.musicKit.pause();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.audioPlayer.pause();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'back-10':
|
||||||
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
|
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'skip-10':
|
||||||
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
|
if ( this.musicKit.currentPlaybackTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
|
||||||
|
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if ( this.repeatMode !== 'once' ) {
|
||||||
|
this.control( 'next' );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.musicKit.seekToTime( 0 );
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ( this.audioPlayer.currentTime < ( this.playlist[ this.playingSongID ].duration - 10 ) ) {
|
||||||
|
this.audioPlayer.currentTime = this.audioPlayer.currentTime + 10;
|
||||||
|
} else {
|
||||||
|
if ( this.repeatMode !== 'once' ) {
|
||||||
|
this.control( 'next' );
|
||||||
|
} else {
|
||||||
|
this.audioPlayer.currentTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'next':
|
||||||
|
this.control( 'pause' );
|
||||||
|
|
||||||
|
if ( this.queuePos < this.queue.length - 1 ) {
|
||||||
|
this.queuePos += 1;
|
||||||
|
this.prepare( this.queue[ this.queuePos ] );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.queuePos = 0;
|
||||||
|
|
||||||
|
if ( this.repeatMode !== 'all' ) {
|
||||||
|
this.control( 'pause' );
|
||||||
|
} else {
|
||||||
|
this.playingSongID = this.queue[ this.queuePos ];
|
||||||
|
this.prepare( this.queue[ this.queuePos ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'previous':
|
||||||
|
this.control( 'pause' );
|
||||||
|
|
||||||
|
if ( this.queuePos > 0 ) {
|
||||||
|
this.queuePos -= 1;
|
||||||
|
this.prepare( this.queue[ this.queuePos ] );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.queuePos = this.queue.length - 1;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setShuffle ( enabled: boolean ) {
|
||||||
|
this.isShuffleEnabled = enabled;
|
||||||
|
this.queue = [];
|
||||||
|
|
||||||
|
if ( enabled ) {
|
||||||
|
const d = [];
|
||||||
|
|
||||||
|
for ( const el in this.playlist ) {
|
||||||
|
d.push( parseInt( el ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queue = d.map( value => ( {
|
||||||
|
value,
|
||||||
|
'sort': Math.random()
|
||||||
|
} ) )
|
||||||
|
.sort( ( a, b ) => a.sort - b.sort )
|
||||||
|
.map( ( {
|
||||||
|
value
|
||||||
|
} ) => value );
|
||||||
|
this.queue.splice( this.queue.indexOf( this.playingSongID ), 1 );
|
||||||
|
this.queue.push( this.playingSongID );
|
||||||
|
this.queue.reverse();
|
||||||
|
} else {
|
||||||
|
for ( const song in this.playlist ) {
|
||||||
|
this.queue.push( parseInt( song ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find current song ID in queue
|
||||||
|
for ( const el in this.queue ) {
|
||||||
|
if ( this.queue[ el ] === this.playingSongID ) {
|
||||||
|
this.queuePos = parseInt( el );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRepeatMode ( mode: RepeatMode ) {
|
||||||
|
this.repeatMode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
goToPos ( pos: number ) {
|
||||||
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
|
this.musicKit.seekToTime( pos );
|
||||||
|
} else {
|
||||||
|
this.audioPlayer.currentTime = pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
moveSong ( move: SongMove ) {
|
||||||
|
const newQueue = [];
|
||||||
|
const finishedQueue = [];
|
||||||
|
|
||||||
|
let songID = 0;
|
||||||
|
|
||||||
|
for ( const song in this.playlist ) {
|
||||||
|
if ( this.playlist[ song ].id === move.songID ) {
|
||||||
|
songID = parseInt( song );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( const el in this.queue ) {
|
||||||
|
if ( this.queue[ el ] !== songID ) {
|
||||||
|
newQueue.push( this.queue[ el ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasBeenAdded = false;
|
||||||
|
|
||||||
|
for ( const el in newQueue ) {
|
||||||
|
if ( parseInt( el ) === move.newPos ) {
|
||||||
|
finishedQueue.push( songID );
|
||||||
|
hasBeenAdded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedQueue.push( newQueue[ el ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !hasBeenAdded ) {
|
||||||
|
finishedQueue.push( songID );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queue = finishedQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current position of the play heed. Will return in ms since start of the song
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getPlaybackPos (): number {
|
||||||
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
|
return this.musicKit.currentPlaybackTime;
|
||||||
|
} else {
|
||||||
|
return this.audioPlayer.currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get details on the currently playing song
|
||||||
|
* @returns {Song}
|
||||||
|
*/
|
||||||
|
getPlayingSong (): Song {
|
||||||
|
return this.playlist[ this.playingSongID ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the playlist index of the currently playing song
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getPlayingSongID (): number {
|
||||||
|
return this.playingSongID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the queue index of the currently playing song
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getQueueID (): number {
|
||||||
|
return this.queuePos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full playlist, as it is set currently, not ordered by queue settings, but as passed in originally
|
||||||
|
* @returns {Song[]}
|
||||||
|
*/
|
||||||
|
getPlaylist (): Song[] {
|
||||||
|
return this.playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as getPlaylist, but returns a ordered playlist, by how it will play according to the queue.
|
||||||
|
* @returns {Song[]}
|
||||||
|
*/
|
||||||
|
getQueue (): Song[] {
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
for ( const el in this.queue ) {
|
||||||
|
data.push( this.playlist[ this.queue[ el ] ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all playlists the authenticated user has on Apple Music. Only available once the user has authenticated!
|
||||||
|
* @param {( data: object ) => void} cb The callback function called with the results from the API
|
||||||
|
* @returns {boolean} Returns true, if user is authenticated and request was started, false if not.
|
||||||
|
*/
|
||||||
|
getUserPlaylists ( cb: ( data: object ) => void ): boolean {
|
||||||
|
if ( this.isLoggedIn ) {
|
||||||
|
this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', cb );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaying ( ): boolean {
|
||||||
|
if ( this.playlist[ this.playingSongID ].origin === 'apple-music' ) {
|
||||||
|
return this.musicKit.isPlaying;
|
||||||
|
} else {
|
||||||
|
return !this.audioPlayer.paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findSongOnAppleMusic ( searchTerm: string ): Promise<SearchResult> {
|
||||||
|
// TODO: Make storefront adjustable
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
const queryParameters = {
|
||||||
|
'term': searchTerm,
|
||||||
|
'types': [ 'songs' ],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.musicKit.api.music( 'v1/catalog/ch/search', queryParameters )
|
||||||
|
.then( results => {
|
||||||
|
resolve( results );
|
||||||
|
} )
|
||||||
|
.catch( e => {
|
||||||
|
console.error( e );
|
||||||
|
reject( e );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MusicKitJSWrapper;
|
||||||
308
MusicPlayerV2-GUI/src/scripts/notificationHandler.ts
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// These functions handle connections to the backend with socket.io
|
||||||
|
|
||||||
|
import {
|
||||||
|
io, type Socket
|
||||||
|
} from 'socket.io-client';
|
||||||
|
import type {
|
||||||
|
SSEMap
|
||||||
|
} from './song';
|
||||||
|
|
||||||
|
class NotificationHandler {
|
||||||
|
|
||||||
|
socket: Socket;
|
||||||
|
|
||||||
|
roomName: string;
|
||||||
|
|
||||||
|
roomToken: string;
|
||||||
|
|
||||||
|
isConnected: boolean;
|
||||||
|
|
||||||
|
useSocket: boolean;
|
||||||
|
|
||||||
|
eventSource?: EventSource;
|
||||||
|
|
||||||
|
toBeListenedForItems: SSEMap;
|
||||||
|
|
||||||
|
reconnectRetryCount: number;
|
||||||
|
|
||||||
|
lastEmitTimestamp: number;
|
||||||
|
|
||||||
|
openConnectionsCount: number;
|
||||||
|
|
||||||
|
pendingRequestCount: number;
|
||||||
|
|
||||||
|
connectionWasSuccessful: boolean;
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.socket = io( localStorage.getItem( 'url' ) ?? '', {
|
||||||
|
'autoConnect': false,
|
||||||
|
} );
|
||||||
|
this.roomName = '';
|
||||||
|
this.roomToken = '';
|
||||||
|
this.isConnected = false;
|
||||||
|
this.useSocket = localStorage.getItem( 'music-player-config' ) === 'ws';
|
||||||
|
this.toBeListenedForItems = {};
|
||||||
|
this.reconnectRetryCount = 0;
|
||||||
|
this.lastEmitTimestamp = 0;
|
||||||
|
this.pendingRequestCount = 0;
|
||||||
|
this.openConnectionsCount = 0;
|
||||||
|
this.connectionWasSuccessful = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a room token and connect to
|
||||||
|
* @param {string} roomName
|
||||||
|
* @param {boolean} useAntiTamper
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
connect ( roomName: string, useAntiTamper: boolean ): Promise<void> {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
fetch( localStorage.getItem( 'url' ) + '/createRoomToken?roomName=' + roomName + '&useAntiTamper=' + useAntiTamper, {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
|
if ( res.status === 200 ) {
|
||||||
|
res.text().then( text => {
|
||||||
|
this.roomToken = text;
|
||||||
|
this.roomName = roomName;
|
||||||
|
|
||||||
|
if ( this.useSocket ) {
|
||||||
|
this.socket.connect();
|
||||||
|
this.socket.emit(
|
||||||
|
'create-room', {
|
||||||
|
'name': this.roomName,
|
||||||
|
'token': this.roomToken
|
||||||
|
}, ( res: {
|
||||||
|
'status': boolean,
|
||||||
|
'msg': string
|
||||||
|
} ) => {
|
||||||
|
if ( res.status === true ) {
|
||||||
|
this.isConnected = true;
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.sseConnect().then( () => {
|
||||||
|
resolve();
|
||||||
|
} )
|
||||||
|
.catch( );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} else if ( res.status === 409 ) {
|
||||||
|
reject( 'ERR_CONFLICT' );
|
||||||
|
} else if ( res.status === 403 || res.status === 401 ) {
|
||||||
|
reject( 'ERR_UNAUTHORIZED' );
|
||||||
|
} else {
|
||||||
|
reject( 'ERR_ROOM_CREATING' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
sseConnect (): Promise<void> {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
if ( this.reconnectRetryCount < 5 ) {
|
||||||
|
if ( this.openConnectionsCount < 1 && !this.isConnected ) {
|
||||||
|
this.openConnectionsCount += 1;
|
||||||
|
fetch( localStorage.getItem( 'url' ) + '/socket/joinRoom?room=' + this.roomName, {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
|
if ( res.status === 200 ) {
|
||||||
|
this.eventSource = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, {
|
||||||
|
'withCredentials': true
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.eventSource.onopen = () => {
|
||||||
|
this.isConnected = true;
|
||||||
|
this.connectionWasSuccessful = true;
|
||||||
|
this.reconnectRetryCount = 0;
|
||||||
|
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventSource.onmessage = e => {
|
||||||
|
const d = JSON.parse( e.data );
|
||||||
|
|
||||||
|
if ( this.toBeListenedForItems[ d.type ] ) {
|
||||||
|
this.toBeListenedForItems[ d.type ]( d.data );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventSource.onerror = e => {
|
||||||
|
if ( this.isConnected ) {
|
||||||
|
this.isConnected = false;
|
||||||
|
this.eventSource?.close();
|
||||||
|
this.openConnectionsCount -= 1;
|
||||||
|
console.debug( e );
|
||||||
|
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to connection error!' );
|
||||||
|
|
||||||
|
this.eventSource = undefined;
|
||||||
|
|
||||||
|
this.reconnectRetryCount += 1;
|
||||||
|
setTimeout( () => {
|
||||||
|
this.sseConnect();
|
||||||
|
}, 1000 * this.reconnectRetryCount );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if ( res.status === 403 || res.status === 401 || res.status === 404 || res.status === 402 ) {
|
||||||
|
document.dispatchEvent( new Event( 'musicplayer:autherror' ) );
|
||||||
|
reject( 'ERR_UNAUTHORIZED' );
|
||||||
|
} else {
|
||||||
|
reject( 'ERR_ROOM_CONNECTING_STATUS_CODE' );
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
if ( !this.connectionWasSuccessful ) {
|
||||||
|
reject( 'ERR_ROOM_CONNECTING' );
|
||||||
|
} else {
|
||||||
|
this.openConnectionsCount -= 1;
|
||||||
|
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Reconnecting due to severe connection error!' );
|
||||||
|
|
||||||
|
this.eventSource = undefined;
|
||||||
|
|
||||||
|
this.reconnectRetryCount += 1;
|
||||||
|
setTimeout( () => {
|
||||||
|
this.sseConnect();
|
||||||
|
}, 1000 * this.reconnectRetryCount );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ( confirm( 'Connection lost and it could not be reestablished. Please click ok to retry or press cancel to stop retrying. Your share will be deleted as a result thereof.' ) ) {
|
||||||
|
this.reconnectRetryCount = 0;
|
||||||
|
this.sseConnect();
|
||||||
|
} else {
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit an event
|
||||||
|
* @param {string} event The event to emit
|
||||||
|
* @param {any} data
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
emit ( event: string, data: any ): void {
|
||||||
|
if ( this.isConnected ) {
|
||||||
|
if ( this.useSocket ) {
|
||||||
|
this.socket.emit( event, {
|
||||||
|
'roomToken': this.roomToken,
|
||||||
|
'roomName': this.roomName,
|
||||||
|
'data': data
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
|
||||||
|
if ( this.lastEmitTimestamp < now - 250 ) {
|
||||||
|
this.lastEmitTimestamp = now;
|
||||||
|
this.sendEmitConventionally( event, data );
|
||||||
|
} else {
|
||||||
|
this.pendingRequestCount += 1;
|
||||||
|
setTimeout( () => {
|
||||||
|
this.pendingRequestCount = 0;
|
||||||
|
this.lastEmitTimestamp = now;
|
||||||
|
this.sendEmitConventionally( event, data );
|
||||||
|
}, 250 * this.pendingRequestCount );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEmitConventionally ( event: string, data: any ): void {
|
||||||
|
fetch( localStorage.getItem( 'url' ) + '/socket/update', {
|
||||||
|
'method': 'post',
|
||||||
|
'body': JSON.stringify( {
|
||||||
|
'event': event,
|
||||||
|
'roomName': this.roomName,
|
||||||
|
'roomToken': this.roomToken,
|
||||||
|
'data': data
|
||||||
|
} ),
|
||||||
|
'credentials': 'include',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'charset': 'utf-8'
|
||||||
|
}
|
||||||
|
} ).catch( () => {} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a listener function for an event
|
||||||
|
* @param {string} event The event to listen for
|
||||||
|
* @param {( data: any ) => void} cb The callback function / listener function
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
registerListener ( event: string, cb: ( data: any ) => void ): void {
|
||||||
|
if ( this.useSocket ) {
|
||||||
|
if ( this.isConnected ) {
|
||||||
|
this.socket.on( event, cb );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toBeListenedForItems[ event ] = cb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the server
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
async disconnect (): Promise<void> {
|
||||||
|
if ( this.isConnected ) {
|
||||||
|
if ( this.useSocket ) {
|
||||||
|
this.socket.emit(
|
||||||
|
'delete-room', {
|
||||||
|
'name': this.roomName,
|
||||||
|
'token': this.roomToken
|
||||||
|
}, ( res: {
|
||||||
|
'status': boolean,
|
||||||
|
'msg': string
|
||||||
|
} ) => {
|
||||||
|
this.socket.disconnect();
|
||||||
|
|
||||||
|
if ( !res.status ) {
|
||||||
|
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
fetch( localStorage.getItem( 'url' ) + '/socket/deleteRoom', {
|
||||||
|
'method': 'post',
|
||||||
|
'body': JSON.stringify( {
|
||||||
|
'roomName': this.roomName,
|
||||||
|
'roomToken': this.roomToken
|
||||||
|
} ),
|
||||||
|
'credentials': 'include',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'charset': 'utf-8'
|
||||||
|
}
|
||||||
|
} ).then( res => {
|
||||||
|
if ( res.status === 200 ) {
|
||||||
|
this.eventSource!.close();
|
||||||
|
} else {
|
||||||
|
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
return;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoomName (): string {
|
||||||
|
return this.roomName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotificationHandler;
|
||||||
95
MusicPlayerV2-GUI/src/scripts/song.d.ts
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
export type Origin = 'apple-music' | 'disk';
|
||||||
|
|
||||||
|
export interface Song {
|
||||||
|
/**
|
||||||
|
* The ID. Either the apple music ID, or if from local disk, an ID starting in local_
|
||||||
|
*/
|
||||||
|
'id': string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Origin of the song
|
||||||
|
*/
|
||||||
|
'origin': Origin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cover image as a URL
|
||||||
|
*/
|
||||||
|
'cover': string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The artist of the song
|
||||||
|
*/
|
||||||
|
'artist': string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the song
|
||||||
|
*/
|
||||||
|
'title': string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of the song in milliseconds
|
||||||
|
*/
|
||||||
|
'duration': number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (OPTIONAL) The genres this song belongs to. Can be displayed on the showcase screen, but requires settings there
|
||||||
|
*/
|
||||||
|
'genres'?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (OPTIONAL) This will be displayed in brackets on the showcase screens
|
||||||
|
*/
|
||||||
|
'additionalInfo'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SongTransmitted {
|
||||||
|
'title': string;
|
||||||
|
'artist': string;
|
||||||
|
'duration': number;
|
||||||
|
'cover': string;
|
||||||
|
'additionalInfo'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ReadFile {
|
||||||
|
'url': string;
|
||||||
|
'filename': string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
'data': {
|
||||||
|
'results': {
|
||||||
|
'songs': {
|
||||||
|
'data': AppleMusicSongData[],
|
||||||
|
'href': string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppleMusicSongData {
|
||||||
|
'id': string,
|
||||||
|
'type': string;
|
||||||
|
'href': string;
|
||||||
|
'attributes': {
|
||||||
|
'albumName': string;
|
||||||
|
'artistName': string;
|
||||||
|
'artwork': {
|
||||||
|
'width': number,
|
||||||
|
'height': number,
|
||||||
|
'url': string
|
||||||
|
},
|
||||||
|
'name': string;
|
||||||
|
'genreNames': string[];
|
||||||
|
'durationInMillis': number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SongMove {
|
||||||
|
'songID': string;
|
||||||
|
'newPos': number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SSEMap {
|
||||||
|
[key: string]: ( data: any ) => void;
|
||||||
|
}
|
||||||
29
MusicPlayerV2-GUI/src/stores/userStore.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
defineStore
|
||||||
|
} from 'pinia';
|
||||||
|
|
||||||
|
|
||||||
|
// FOSS-VERSION: To enable the UI to be used with the FOSS version, change "isUserAuth" to true, you will be "logged in"
|
||||||
|
export const useUserStore = defineStore( 'user', {
|
||||||
|
'state': () => ( {
|
||||||
|
'isUserAuth': false,
|
||||||
|
'hasSubscribed': false,
|
||||||
|
'isUsingKeyboard': false,
|
||||||
|
'isFOSSVersion': false
|
||||||
|
} ),
|
||||||
|
'getters': {
|
||||||
|
'getUserAuthenticated': state => state.isUserAuth,
|
||||||
|
'getSubscriptionStatus': state => state.hasSubscribed,
|
||||||
|
},
|
||||||
|
'actions': {
|
||||||
|
setUserAuth ( auth: boolean ) {
|
||||||
|
this.isUserAuth = auth;
|
||||||
|
},
|
||||||
|
setSubscriptionStatus ( status: boolean ) {
|
||||||
|
this.hasSubscribed = status;
|
||||||
|
},
|
||||||
|
setKeyboardUsageStatus ( status: boolean ) {
|
||||||
|
this.isUsingKeyboard = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
24
MusicPlayerV2-GUI/src/views/404View.vue
Normal 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>
|
||||||
197
MusicPlayerV2-GUI/src/views/AppView.vue
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-view">
|
||||||
|
<button id="logout" @click="logout()">
|
||||||
|
<span class="material-symbols-outlined">logout</span>
|
||||||
|
</button>
|
||||||
|
<div v-if="!hasFinishedLoading" class="loading-view">
|
||||||
|
<h1>Loading...</h1>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="hasFinishedLoading && isReady" class="home-view">
|
||||||
|
<libraryView
|
||||||
|
class="library-view"
|
||||||
|
:playlists="playlists"
|
||||||
|
:is-logged-in="isLoggedIntoAppleMusic"
|
||||||
|
@selected-playlist="( id ) => { selectPlaylist( id ) }"
|
||||||
|
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="login-view">
|
||||||
|
<img src="@/assets/appleMusicIcon.svg" alt="Apple Music Icon">
|
||||||
|
<button class="fancy-button" style="margin-top: 20px;" @click="logIntoAppleMusic()">
|
||||||
|
Log into Apple Music
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="fancy-button"
|
||||||
|
title="This allows you to use local playlists only.
|
||||||
|
Cover images for your songs will be fetched from the apple music api as good as possible"
|
||||||
|
@click="skipLogin()"
|
||||||
|
>
|
||||||
|
Continue without logging in
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<playerView
|
||||||
|
ref="player"
|
||||||
|
:class="'player-view'
|
||||||
|
+ ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )"
|
||||||
|
@player-state-change="( state ) => { handlePlayerStateChange( state ) }"
|
||||||
|
/>
|
||||||
|
<!-- TODO: Call to backend to check if user has access -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import playerView from '@/components/playerView.vue';
|
||||||
|
import libraryView from '@/components/libraryView.vue';
|
||||||
|
import {
|
||||||
|
ref
|
||||||
|
} from 'vue';
|
||||||
|
import type {
|
||||||
|
ReadFile
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import router from '@/router';
|
||||||
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/stores/userStore';
|
||||||
|
|
||||||
|
const isLoggedIntoAppleMusic = ref( false );
|
||||||
|
const isReady = ref( false );
|
||||||
|
const isShowingFullScreenPlayer = ref( false );
|
||||||
|
const player = ref( playerView );
|
||||||
|
const playlists = ref( [] );
|
||||||
|
const hasFinishedLoading = ref( false );
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const handlePlayerStateChange = ( newState: string ) => {
|
||||||
|
if ( newState === 'hide' ) {
|
||||||
|
isShowingFullScreenPlayer.value = false;
|
||||||
|
} else {
|
||||||
|
isShowingFullScreenPlayer.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let loginChecker = 0;
|
||||||
|
|
||||||
|
const logIntoAppleMusic = () => {
|
||||||
|
player.value.logIntoAppleMusic();
|
||||||
|
loginChecker = setInterval( () => {
|
||||||
|
if ( player.value.getAuth()[ 0 ] ) {
|
||||||
|
isLoggedIntoAppleMusic.value = true;
|
||||||
|
isReady.value = true;
|
||||||
|
player.value.getPlaylists( data => {
|
||||||
|
playlists.value = data.data.data;
|
||||||
|
} );
|
||||||
|
clearInterval( loginChecker );
|
||||||
|
} else if ( player.value.getAuth()[ 1 ] ) {
|
||||||
|
clearInterval( loginChecker );
|
||||||
|
alert( 'An error occurred when logging you in. Please try again!' );
|
||||||
|
}
|
||||||
|
}, 500 );
|
||||||
|
};
|
||||||
|
|
||||||
|
const skipLogin = () => {
|
||||||
|
isReady.value = true;
|
||||||
|
isLoggedIntoAppleMusic.value = false;
|
||||||
|
player.value.skipLogin();
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPlaylist = ( id: string ) => {
|
||||||
|
player.value.selectPlaylist( id );
|
||||||
|
player.value.controlUI( 'show' );
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
|
||||||
|
player.value.selectCustomPlaylist( playlist );
|
||||||
|
player.value.controlUI( 'show' );
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', {
|
||||||
|
'credentials': 'include'
|
||||||
|
} ).then( res => {
|
||||||
|
if ( res.status === 200 ) {
|
||||||
|
res.text().then( text => {
|
||||||
|
if ( text === 'ok' ) {
|
||||||
|
hasFinishedLoading.value = true;
|
||||||
|
userStore.setSubscriptionStatus( true );
|
||||||
|
} else {
|
||||||
|
userStore.setSubscriptionStatus( false );
|
||||||
|
sessionStorage.setItem( 'getRedirectionReason', 'notOwned' );
|
||||||
|
router.push( '/get' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} else if ( res.status === 402 ) {
|
||||||
|
userStore.setSubscriptionStatus( false );
|
||||||
|
router.push( '/get' );
|
||||||
|
sessionStorage.setItem( 'getRedirectionReason', 'notOwned' );
|
||||||
|
} else {
|
||||||
|
console.log( res.status );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
||||||
|
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#logout {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
position: fixed;
|
||||||
|
left: calc( 10px + 2rem );
|
||||||
|
top: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logout .material-symbols-outlined {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var( --primary-color );
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-view {
|
||||||
|
height: calc( 90vh - 10px );
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-view {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-view {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-view {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-view {
|
||||||
|
height: 13vh;
|
||||||
|
width: calc( 100vw - 20px );
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
background-color: var( --secondary-color );
|
||||||
|
transition: all 0.75s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-screen-player {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
163
MusicPlayerV2-GUI/src/views/BarView.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
type Ref,
|
||||||
|
computed, ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
interface FullConfig {
|
||||||
|
'offering': BarConfig,
|
||||||
|
'ages': Ages
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Ages {
|
||||||
|
'18+': string,
|
||||||
|
'16-18': string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BarConfig {
|
||||||
|
[id: string]: Offer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Offer {
|
||||||
|
'name': string;
|
||||||
|
'price': number; // In cents
|
||||||
|
'id': string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Selection {
|
||||||
|
[id: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ages: Ref<Ages> = ref( {
|
||||||
|
'18+': '',
|
||||||
|
'16-18': '',
|
||||||
|
'below': ''
|
||||||
|
} );
|
||||||
|
const offering: Ref<BarConfig> = ref( {} );
|
||||||
|
const selection: Ref<Selection> = ref( {} );
|
||||||
|
|
||||||
|
fetch( '/bar-config.json' ).then( res => {
|
||||||
|
if ( res.status === 200 ) {
|
||||||
|
res.json().then( json => {
|
||||||
|
const data: FullConfig = json;
|
||||||
|
|
||||||
|
offering.value = data.offering;
|
||||||
|
ages.value = data.ages;
|
||||||
|
reset();
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
alert( 'Failed to load' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
const keys = Object.keys( offering.value );
|
||||||
|
|
||||||
|
keys.forEach( val => {
|
||||||
|
selection.value[ val ] = 0;
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
const total = computed( () => {
|
||||||
|
const keys = Object.keys( selection.value );
|
||||||
|
|
||||||
|
let totalPrice = 0;
|
||||||
|
|
||||||
|
for ( let i = 0; i < keys.length; i++ ) {
|
||||||
|
const o = selection.value[ keys[ i ] ];
|
||||||
|
|
||||||
|
totalPrice += o * offering.value[ keys[ i ] ].price;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalPrice / 100;
|
||||||
|
} );
|
||||||
|
|
||||||
|
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">
|
||||||
|
<h1>Bar utility</h1>
|
||||||
|
<p>Check ages! (18+: {{ ages[ '18+' ] }}, 16-18: {{ ages[ '16-18' ] }})</p>
|
||||||
|
<button @click="reset()">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<p>Total: CHF {{ total }}</p>
|
||||||
|
<table class="offering-wrapper">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="offer in offering" :key="offer.id" class="offering">
|
||||||
|
<td>
|
||||||
|
<p>{{ offer.name }} (CHF {{ offer.price / 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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bar-utility {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
>.offering-wrapper {
|
||||||
|
.offering {
|
||||||
|
>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>
|
||||||
106
MusicPlayerV2-GUI/src/views/GetView.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="top-view">
|
||||||
|
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||||
|
<h1>MusicPlayer</h1>
|
||||||
|
<p v-if="reasonForRedirectHere" style="color: red;">
|
||||||
|
{{ reasons[ reasonForRedirectHere ] }}
|
||||||
|
</p>
|
||||||
|
<p v-if="!reasonForRedirectHere">
|
||||||
|
<i>An Open Source, browser-based MusicPlayer with beautiful graphics</i>
|
||||||
|
</p>
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<a href="https://store.janishutz.com/product/com.janishutz.MusicPlayer" class="fancy-button" target="_blank">Subscribe</a>
|
||||||
|
<a
|
||||||
|
v-if="!reasonForRedirectHere"
|
||||||
|
href="/"
|
||||||
|
class="fancy-button"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
>Log in</a>
|
||||||
|
<button
|
||||||
|
v-if="reasonForRedirectHere"
|
||||||
|
href="/"
|
||||||
|
class="fancy-button"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
@click="logout()"
|
||||||
|
>
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://github.com/simplePCBuilding/MusicPlayerV2"
|
||||||
|
class="fancy-button"
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
target="_blank"
|
||||||
|
>GitHub</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Fully featured Music Player</h2>
|
||||||
|
<p>All the features you'd expect a Music Player to have are also present here</p>
|
||||||
|
|
||||||
|
<h2>Apple Music integration</h2>
|
||||||
|
<p>Use MusicPlayer in conjunction with Apple Music</p>
|
||||||
|
|
||||||
|
<h2>Share your playlist</h2>
|
||||||
|
<p>
|
||||||
|
You can share your playlist on a beautifully animated public page,
|
||||||
|
so that other people can join in and view your playlist
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Fully browser based</h2>
|
||||||
|
<p>No installation required when using MusicPlayer on <a href="https://music.janishutz.com">music.janishutz.com</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ref, type Ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
interface Reasons {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reasons: Ref<Reasons> = ref( {
|
||||||
|
'notOwned': 'Please subscribe to use MusicPlayer here, or download and install it manually from GitHub!',
|
||||||
|
} );
|
||||||
|
const reasonForRedirectHere = ref( sessionStorage.getItem( 'getRedirectionReason' ) );
|
||||||
|
|
||||||
|
sessionStorage.removeItem( 'getRedirectionReason' );
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
// location.href = 'http://localhost:8080/logout?return=' + location.href;
|
||||||
|
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logo {
|
||||||
|
height: 60vh;
|
||||||
|
max-height: 90vw;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-view {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-height {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-img {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
MusicPlayerV2-GUI/src/views/HomeView.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home-view">
|
||||||
|
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo">
|
||||||
|
<button
|
||||||
|
:class="'fancy-button' + ( isTryingToSignIn ? ' fancy-button-inactive' : '' )"
|
||||||
|
style="margin-top: 5vh;"
|
||||||
|
title="Sign in or sign up with janishutz.com ID"
|
||||||
|
@click="login()"
|
||||||
|
>
|
||||||
|
{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}
|
||||||
|
</button>
|
||||||
|
<p style="width: 80%;">
|
||||||
|
MusicPlayer is a browser based Music Player, that allows you to connect other devices,
|
||||||
|
simply with another web-browser, where you can see the current playlist with sleek animations.
|
||||||
|
You can log in using your Apple Music account or load a playlist from your local disk,
|
||||||
|
simply by selecting the songs using a file picker.
|
||||||
|
</p>
|
||||||
|
<router-link to="/get" class="fancy-button">
|
||||||
|
More information
|
||||||
|
</router-link>
|
||||||
|
<notificationsModule ref="notifications" location="bottomleft" size="bigger" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// TODO: Make possible to install and use without account, if using FOSS version
|
||||||
|
import {
|
||||||
|
RouterLink
|
||||||
|
} from 'vue-router';
|
||||||
|
import notificationsModule from '@/components/notificationsModule.vue';
|
||||||
|
import {
|
||||||
|
ref
|
||||||
|
} from 'vue';
|
||||||
|
import router from '@/router';
|
||||||
|
import sdk from '@janishutz/login-sdk-browser';
|
||||||
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/stores/userStore';
|
||||||
|
|
||||||
|
const notifications = ref( notificationsModule );
|
||||||
|
const isTryingToSignIn = ref( true );
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
sdk.login();
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = useUserStore();
|
||||||
|
|
||||||
|
if ( store.isUserAuth ) {
|
||||||
|
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
||||||
|
localStorage.removeItem( 'redirect' );
|
||||||
|
} else {
|
||||||
|
sdk.verifyFull()
|
||||||
|
.then( res => {
|
||||||
|
if ( res ) {
|
||||||
|
store.isUserAuth = true;
|
||||||
|
|
||||||
|
if ( localStorage.getItem( 'close-tab' ) ) {
|
||||||
|
localStorage.removeItem( 'close-tab' );
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem( 'login-ok', 'true' );
|
||||||
|
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
|
||||||
|
localStorage.removeItem( 'redirect' );
|
||||||
|
} else {
|
||||||
|
isTryingToSignIn.value = false;
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.catch( e => {
|
||||||
|
console.debug( e );
|
||||||
|
isTryingToSignIn.value = false;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-view {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 50vh;
|
||||||
|
border-radius: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
389
MusicPlayerV2-GUI/src/views/RemoteView.vue
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="info">
|
||||||
|
Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a>
|
||||||
|
</div>
|
||||||
|
<div class="remote-view">
|
||||||
|
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
||||||
|
<div class="current-song-wrapper">
|
||||||
|
<img
|
||||||
|
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
|
||||||
|
id="current-image"
|
||||||
|
:src="playlist[ playingSong ].cover"
|
||||||
|
class="fancy-view-song-art"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
>
|
||||||
|
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||||
|
<div class="current-song">
|
||||||
|
<h1 style="margin-bottom: 5px;">
|
||||||
|
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
|
||||||
|
</h1>
|
||||||
|
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
||||||
|
<p
|
||||||
|
v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false"
|
||||||
|
class="additional-info"
|
||||||
|
>
|
||||||
|
{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}
|
||||||
|
</p>
|
||||||
|
<progress id="progress" max="1000" :value="progressBar"></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="song-list-wrapper">
|
||||||
|
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||||
|
<div class="song-details-wrapper">
|
||||||
|
<h3>{{ song.title }}</h3>
|
||||||
|
<p>{{ song.artist }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="time-until">
|
||||||
|
{{ getTimeUntil( song.id ) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <img :src="" alt=""> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!hasLoaded && !showCouldNotFindRoom">
|
||||||
|
<h1>Loading...</h1>
|
||||||
|
</div>
|
||||||
|
<div v-else style="max-width: 80%;">
|
||||||
|
<span class="material-symbols-outlined" style="font-size: 4rem;">wifi_off</span>
|
||||||
|
<h1>Couldn't connect!</h1>
|
||||||
|
<p>
|
||||||
|
There does not appear to be a share with the specified name, or an error occurred when connecting.
|
||||||
|
</p>
|
||||||
|
<p>You may <a href="">reload</a> the page to try again!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SocketConnection from '@/scripts/connection';
|
||||||
|
import type {
|
||||||
|
Song
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
computed, ref, type Ref
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
const isPlaying = ref( false );
|
||||||
|
const playlist: Ref<Song[]> = ref( [] );
|
||||||
|
const pos = ref( 0 );
|
||||||
|
const playingSong = ref( 0 );
|
||||||
|
const progressBar = ref( 0 );
|
||||||
|
const hasLoaded = ref( false );
|
||||||
|
const showCouldNotFindRoom = ref( false );
|
||||||
|
const playbackStart = ref( 0 );
|
||||||
|
|
||||||
|
let timeTracker = 0;
|
||||||
|
|
||||||
|
const conn = new SocketConnection();
|
||||||
|
|
||||||
|
conn.connect().then( d => {
|
||||||
|
playlist.value = d.playlist;
|
||||||
|
isPlaying.value = d.playbackStatus;
|
||||||
|
playingSong.value = d.playlistIndex;
|
||||||
|
playbackStart.value = d.playbackStart;
|
||||||
|
|
||||||
|
if ( isPlaying.value ) {
|
||||||
|
startTimeTracker();
|
||||||
|
}
|
||||||
|
|
||||||
|
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||||
|
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ]
|
||||||
|
? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||||
|
hasLoaded.value = true;
|
||||||
|
conn.registerListener( 'playlist', data => {
|
||||||
|
playlist.value = data;
|
||||||
|
} );
|
||||||
|
|
||||||
|
conn.registerListener( 'playback', data => {
|
||||||
|
isPlaying.value = data;
|
||||||
|
|
||||||
|
if ( isPlaying.value ) {
|
||||||
|
startTimeTracker();
|
||||||
|
} else {
|
||||||
|
stopTimeTracker();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
conn.registerListener( 'playback-start', data => {
|
||||||
|
playbackStart.value = data;
|
||||||
|
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
||||||
|
} );
|
||||||
|
|
||||||
|
conn.registerListener( 'playlist-index', data => {
|
||||||
|
playingSong.value = parseInt( data );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
conn.registerListener( 'delete-share', _ => {
|
||||||
|
alert( `This share was just deleted. It is no longer available.
|
||||||
|
The page will reload automatically to try and re-establish connection!` );
|
||||||
|
conn.disconnect();
|
||||||
|
location.reload();
|
||||||
|
} );
|
||||||
|
} )
|
||||||
|
.catch( e => {
|
||||||
|
console.error( e );
|
||||||
|
showCouldNotFindRoom.value = true;
|
||||||
|
} );
|
||||||
|
|
||||||
|
const songQueue = computed( () => {
|
||||||
|
let ret: Song[] = [];
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
for ( let song in playlist.value ) {
|
||||||
|
if ( pos >= playingSong.value ) {
|
||||||
|
ret.push( playlist.value[ song ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
} );
|
||||||
|
const getTimeUntil = computed( () => {
|
||||||
|
return ( song: string ) => {
|
||||||
|
let timeRemaining = 0;
|
||||||
|
|
||||||
|
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
||||||
|
if ( playlist.value[ i ].id == song ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeRemaining += playlist.value[ i ].duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isPlaying.value ) {
|
||||||
|
if ( timeRemaining === 0 ) {
|
||||||
|
return 'Currently playing';
|
||||||
|
} else {
|
||||||
|
return 'Playing in less than ' + Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ( timeRemaining === 0 ) {
|
||||||
|
return 'Plays next';
|
||||||
|
} else {
|
||||||
|
return 'Playing less than '
|
||||||
|
+ Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min after starting to play';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
|
const startTimeTracker = () => {
|
||||||
|
try {
|
||||||
|
clearInterval( timeTracker );
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
|
timeTracker = setInterval( () => {
|
||||||
|
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
||||||
|
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
||||||
|
|
||||||
|
if ( isNaN( progressBar.value ) ) {
|
||||||
|
progressBar.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( playlist.value[ playingSong.value ].duration + 10 - pos.value < 0 ) {
|
||||||
|
stopTimeTracker();
|
||||||
|
alert( 'It looks like if you have been disconnected! We are trying to reconnect you now!' );
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}, 100 );
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopTimeTracker = () => {
|
||||||
|
clearInterval( timeTracker );
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener( 'visibilitychange', () => {
|
||||||
|
if ( !document.hidden ) {
|
||||||
|
if ( !conn.getStatus ) {
|
||||||
|
stopTimeTracker();
|
||||||
|
alert( 'It looks like if you have been disconnected! We are trying to reconnect you now!' );
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#themeSelector {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.remote-view {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: justify;
|
||||||
|
background-color: rgb(2, 16, 61);
|
||||||
|
color: white;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loaded {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing-symbols {
|
||||||
|
position: absolute;
|
||||||
|
left: 10vw;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 5vw;
|
||||||
|
height: 5vw;
|
||||||
|
background-color: rgba( 0, 0, 0, 0.6 );
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing-symbols-wrapper {
|
||||||
|
width: 4vw;
|
||||||
|
height: 5vw;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-list-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 80%;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 1vh;
|
||||||
|
border: 1px white solid;
|
||||||
|
background-color: rgba( 0, 0, 0, 0.4 );
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-details-wrapper {
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 65%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-icon {
|
||||||
|
width: 5vw;
|
||||||
|
height: 5vw;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
font-size: 5vw !important;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-song-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 2%;
|
||||||
|
margin-top: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-song {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 1vh;
|
||||||
|
padding: 1vh;
|
||||||
|
max-width: 80%;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba( 0, 0, 0, 0.4 );
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancy-view-song-art {
|
||||||
|
height: 30vh;
|
||||||
|
width: 30vh;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 30vh !important;
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
background-color: rgba( 0, 0, 0, 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress, #progress::-webkit-progress-bar {
|
||||||
|
background-color: rgb(82, 82, 82);
|
||||||
|
color: rgb(82, 82, 82);
|
||||||
|
width: 30vw;
|
||||||
|
height: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
accent-color: white;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress::-moz-progress-bar {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress::-webkit-progress-value {
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-wrapper {
|
||||||
|
opacity: 0;
|
||||||
|
position: fixed;
|
||||||
|
right: 0.5%;
|
||||||
|
top: 0.5%;
|
||||||
|
padding: 0.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-wrapper:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-info {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
position: fixed;
|
||||||
|
font-size: 12px;
|
||||||
|
transform: rotate(270deg);
|
||||||
|
left: -150px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
top: 50%;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-until {
|
||||||
|
width: 30%;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
595
MusicPlayerV2-GUI/src/views/ShowcaseView.vue
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
v-if="isAntiTamperEnabled"
|
||||||
|
class="anti-tamper material-symbols-outlined"
|
||||||
|
@click="secureModeInfo( 'toggle' )"
|
||||||
|
>lock</span>
|
||||||
|
<div
|
||||||
|
v-if="isShowingSecureModeInfo && isAntiTamperEnabled"
|
||||||
|
class="anti-tamper-info"
|
||||||
|
@click="secureModeInfo( 'hide' )"
|
||||||
|
>
|
||||||
|
Anti-Tamper is enabled. Leaving this window will cause a notification to be dispatched to the player!
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a>
|
||||||
|
</div>
|
||||||
|
<div class="remote-view">
|
||||||
|
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
||||||
|
<div class="current-song-wrapper">
|
||||||
|
<img
|
||||||
|
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
|
||||||
|
id="current-image"
|
||||||
|
:src="playlist[ playingSong ].cover"
|
||||||
|
class="fancy-view-song-art"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
>
|
||||||
|
<span v-else class="material-symbols-outlined fancy-view-song-art">music_note</span>
|
||||||
|
<div class="current-song">
|
||||||
|
<h1 style="margin-bottom: 5px;">
|
||||||
|
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
|
||||||
|
</h1>
|
||||||
|
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
|
||||||
|
<p
|
||||||
|
v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false"
|
||||||
|
class="additional-info"
|
||||||
|
>
|
||||||
|
{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}
|
||||||
|
</p>
|
||||||
|
<progress id="progress" max="1000" :value="progressBar"></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mode-selector-wrapper">
|
||||||
|
<select v-model="visualizationSettings" @change="handleAnimationChange()">
|
||||||
|
<option value="mic">
|
||||||
|
Microphone (Mic access required)
|
||||||
|
</option>
|
||||||
|
<option value="off">
|
||||||
|
No visualization except background
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="song-list-wrapper">
|
||||||
|
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||||
|
<img v-if="song.cover" :src="song.cover" class="song-image">
|
||||||
|
<span v-else class="material-symbols-outlined song-cover">music_note</span>
|
||||||
|
<div
|
||||||
|
v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying"
|
||||||
|
class="playing-symbols"
|
||||||
|
>
|
||||||
|
<div class="playing-symbols-wrapper">
|
||||||
|
<div id="bar-1" class="playing-bar"></div>
|
||||||
|
<div id="bar-2" class="playing-bar"></div>
|
||||||
|
<div id="bar-3" class="playing-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="song-details-wrapper">
|
||||||
|
<h3>{{ song.title }}</h3>
|
||||||
|
<p>{{ song.artist }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="time-until">
|
||||||
|
{{ getTimeUntil( song.id ) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <img :src="" alt=""> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
||||||
|
<h1>Loading...</h1>
|
||||||
|
</div>
|
||||||
|
<div v-else class="showcase-wrapper">
|
||||||
|
<h1>Couldn't connect!</h1>
|
||||||
|
<p>
|
||||||
|
There does not appear to be a share with the specified name, or an error occurred when connecting.
|
||||||
|
</p>
|
||||||
|
<p>You may reload the page to try again!</p>
|
||||||
|
</div>
|
||||||
|
<div id="background" class="background">
|
||||||
|
<div class="beat-manual"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SocketConnection from '@/scripts/connection';
|
||||||
|
import type {
|
||||||
|
Song
|
||||||
|
} from '@/scripts/song';
|
||||||
|
import {
|
||||||
|
type Ref, computed, ref
|
||||||
|
} from 'vue';
|
||||||
|
import bizualizer from '@/scripts/bizualizer';
|
||||||
|
|
||||||
|
const isPlaying = ref( false );
|
||||||
|
const playlist: Ref<Song[]> = ref( [] );
|
||||||
|
const pos = ref( 0 );
|
||||||
|
const playingSong = ref( 0 );
|
||||||
|
const progressBar = ref( 0 );
|
||||||
|
const hasLoaded = ref( false );
|
||||||
|
const showCouldNotFindRoom = ref( false );
|
||||||
|
const playbackStart = ref( 0 );
|
||||||
|
|
||||||
|
let timeTracker = 0;
|
||||||
|
|
||||||
|
const visualizationSettings = ref( 'mic' );
|
||||||
|
const isAntiTamperEnabled = ref( false );
|
||||||
|
const conn = new SocketConnection();
|
||||||
|
|
||||||
|
conn.connect().then( d => {
|
||||||
|
playlist.value = d.playlist;
|
||||||
|
isPlaying.value = d.playbackStatus;
|
||||||
|
playingSong.value = d.playlistIndex;
|
||||||
|
playbackStart.value = d.playbackStart;
|
||||||
|
|
||||||
|
if ( isPlaying.value ) {
|
||||||
|
startTimeTracker();
|
||||||
|
}
|
||||||
|
|
||||||
|
pos.value = ( new Date().getTime() - parseInt( d.playbackStart ) ) / 1000;
|
||||||
|
progressBar.value
|
||||||
|
= ( pos.value / ( playlist.value[ playingSong.value ]
|
||||||
|
? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
|
||||||
|
hasLoaded.value = true;
|
||||||
|
|
||||||
|
if ( d.useAntiTamper ) {
|
||||||
|
isAntiTamperEnabled.value = true;
|
||||||
|
notifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.registerListener( 'playlist', data => {
|
||||||
|
playlist.value = data;
|
||||||
|
} );
|
||||||
|
|
||||||
|
conn.registerListener( 'playback', data => {
|
||||||
|
isPlaying.value = data;
|
||||||
|
|
||||||
|
if ( isPlaying.value ) {
|
||||||
|
startTimeTracker();
|
||||||
|
} else {
|
||||||
|
stopTimeTracker();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
conn.registerListener( 'playback-start', data => {
|
||||||
|
playbackStart.value = data;
|
||||||
|
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
|
||||||
|
} );
|
||||||
|
|
||||||
|
conn.registerListener( 'playlist-index', data => {
|
||||||
|
playingSong.value = parseInt( data );
|
||||||
|
setTimeout( () => {
|
||||||
|
setBackground();
|
||||||
|
}, 1000 );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
conn.registerListener( 'delete-share', _ => {
|
||||||
|
alert( 'This share was just deleted. It is no longer available. This page will reload automatically!' );
|
||||||
|
conn.disconnect();
|
||||||
|
location.reload();
|
||||||
|
} );
|
||||||
|
} )
|
||||||
|
.catch( e => {
|
||||||
|
console.error( e );
|
||||||
|
showCouldNotFindRoom.value = true;
|
||||||
|
} );
|
||||||
|
|
||||||
|
const songQueue = computed( () => {
|
||||||
|
let ret: Song[] = [];
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
for ( let song in playlist.value ) {
|
||||||
|
if ( pos >= playingSong.value ) {
|
||||||
|
ret.push( playlist.value[ song ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
} );
|
||||||
|
const getTimeUntil = computed( () => {
|
||||||
|
return ( song: string ) => {
|
||||||
|
let timeRemaining = 0;
|
||||||
|
|
||||||
|
for ( let i = playingSong.value; i < Object.keys( playlist.value ).length - 1; i++ ) {
|
||||||
|
if ( playlist.value[ i ].id == song ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeRemaining += playlist.value[ i ].duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isPlaying.value ) {
|
||||||
|
if ( timeRemaining === 0 ) {
|
||||||
|
return 'Currently playing';
|
||||||
|
} else {
|
||||||
|
return 'Playing in less than ' + Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ( timeRemaining === 0 ) {
|
||||||
|
return 'Plays next';
|
||||||
|
} else {
|
||||||
|
return 'Playing less than '
|
||||||
|
+ Math.ceil( ( timeRemaining / 60 ) - ( pos.value / 60 ) ) + 'min after starting to play';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
|
const startTimeTracker = () => {
|
||||||
|
try {
|
||||||
|
clearInterval( timeTracker );
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch ( err ) { /* empty */ }
|
||||||
|
|
||||||
|
setTimeout( () => {
|
||||||
|
handleAnimationChange();
|
||||||
|
setBackground();
|
||||||
|
}, 1000 );
|
||||||
|
timeTracker = setInterval( () => {
|
||||||
|
pos.value = ( new Date().getTime() - playbackStart.value ) / 1000;
|
||||||
|
progressBar.value = ( pos.value / playlist.value[ playingSong.value ].duration ) * 1000;
|
||||||
|
|
||||||
|
if ( isNaN( progressBar.value ) ) {
|
||||||
|
progressBar.value = 0;
|
||||||
|
}
|
||||||
|
}, 100 );
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopTimeTracker = () => {
|
||||||
|
clearInterval( timeTracker );
|
||||||
|
|
||||||
|
handleAnimationChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateBeat = () => {
|
||||||
|
$( '.beat-manual' ).stop();
|
||||||
|
const duration = Math.ceil( 60 / 180 * 500 ) - 50;
|
||||||
|
|
||||||
|
$( '.beat-manual' ).fadeIn( 50 );
|
||||||
|
setTimeout( () => {
|
||||||
|
$( '.beat-manual' ).fadeOut( duration );
|
||||||
|
setTimeout( () => {
|
||||||
|
bizualizer.coolDown();
|
||||||
|
$( '.beat-manual' ).stop();
|
||||||
|
}, duration );
|
||||||
|
}, 50 );
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAnimationChange = () => {
|
||||||
|
if ( visualizationSettings.value === 'mic' && isPlaying.value ) {
|
||||||
|
bizualizer.subscribeToBeatUpdate( animateBeat );
|
||||||
|
} else {
|
||||||
|
bizualizer.unsubscribeFromBeatUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setBackground = () => {
|
||||||
|
bizualizer.createBackground().then( bg => {
|
||||||
|
$( '#background' ).css( 'background', bg );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
const notifier = () => {
|
||||||
|
Notification.requestPermission();
|
||||||
|
|
||||||
|
console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
|
||||||
|
|
||||||
|
// Detect if window is currently in focus
|
||||||
|
window.onblur = () => {
|
||||||
|
sendNotification();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Detect if browser window becomes hidden (also with blur event)
|
||||||
|
document.onvisibilitychange = () => {
|
||||||
|
if ( document.visibilityState === 'hidden' ) {
|
||||||
|
sendNotification();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendNotification = () => {
|
||||||
|
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
|
||||||
|
'body': 'Please return to the original webpage immediately!',
|
||||||
|
'requireInteraction': true,
|
||||||
|
} );
|
||||||
|
|
||||||
|
conn.emit( 'tampering', '' );
|
||||||
|
};
|
||||||
|
|
||||||
|
const isShowingSecureModeInfo = ref( false );
|
||||||
|
|
||||||
|
const secureModeInfo = ( action: string ) => {
|
||||||
|
if ( action === 'toggle' ) {
|
||||||
|
isShowingSecureModeInfo.value = !isShowingSecureModeInfo.value;
|
||||||
|
} else if ( action === 'show' ) {
|
||||||
|
isShowingSecureModeInfo.value = true;
|
||||||
|
} else {
|
||||||
|
isShowingSecureModeInfo.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.anti-tamper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
bottom: 5px;
|
||||||
|
right: 5px;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anti-tamper-info {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
bottom: calc( 10px + 2rem );
|
||||||
|
right: 5px;
|
||||||
|
max-width: 20rem;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-view {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing-symbols {
|
||||||
|
position: absolute;
|
||||||
|
left: 10vw;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 5vw;
|
||||||
|
height: 5vw;
|
||||||
|
background-color: rgba( 0, 0, 0, 0.6 );
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing-symbols-wrapper {
|
||||||
|
width: 4vw;
|
||||||
|
height: 5vw;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playing-bar {
|
||||||
|
height: 60%;
|
||||||
|
background-color: white;
|
||||||
|
width: 10%;
|
||||||
|
border-radius: 50px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bar-1 {
|
||||||
|
animation: music-playing 0.9s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bar-2 {
|
||||||
|
animation: music-playing 0.9s infinite ease-in-out;
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bar-3 {
|
||||||
|
animation: music-playing 0.9s infinite ease-in-out;
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes music-playing {
|
||||||
|
0% {
|
||||||
|
transform: scaleY( 1 );
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scaleY( 0.5 );
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleY( 1 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-list-wrapper {
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 80%;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 1vh;
|
||||||
|
border: 1px white solid;
|
||||||
|
background-color: rgba( 0, 0, 0, 0.4 );
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-details-wrapper {
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-list .song-image {
|
||||||
|
width: 5vw;
|
||||||
|
height: 5vw;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
font-size: 5vw;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-icon {
|
||||||
|
width: 5vw;
|
||||||
|
height: 5vw;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
font-size: 5vw !important;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-song-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 55vh;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5%;
|
||||||
|
margin-top: 0.25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-song {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 1vh;
|
||||||
|
padding: 1vh;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba( 0, 0, 0, 0.4 );
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancy-view-song-art {
|
||||||
|
height: 30vh;
|
||||||
|
width: 30vh;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 30vh !important;
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
background-color: rgba( 0, 0, 0, 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress, #progress::-webkit-progress-bar {
|
||||||
|
background-color: rgb(82, 82, 82);
|
||||||
|
color: rgb(82, 82, 82);
|
||||||
|
width: 30vw;
|
||||||
|
height: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
accent-color: white;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress::-moz-progress-bar {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress::-webkit-progress-value {
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-wrapper {
|
||||||
|
opacity: 0;
|
||||||
|
position: fixed;
|
||||||
|
right: 0.5%;
|
||||||
|
top: 0.5%;
|
||||||
|
padding: 0.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-wrapper:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dancing-style {
|
||||||
|
font-size: 250%;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
position: fixed;
|
||||||
|
font-size: 12px;
|
||||||
|
transform: rotate(270deg);
|
||||||
|
left: -150px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 100;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.background {
|
||||||
|
position: fixed;
|
||||||
|
left: -50vw;
|
||||||
|
width: 200vw;
|
||||||
|
height: 200vw;
|
||||||
|
top: -50vw;
|
||||||
|
z-index: 1;
|
||||||
|
filter: blur(10px);
|
||||||
|
background: conic-gradient( blue, green, red, blue );
|
||||||
|
animation: gradientAnim 10s infinite linear;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beat, .beat-manual {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgba( 0, 0, 0, 0.1 );
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beat {
|
||||||
|
animation: beatAnim 0.6s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes beatAnim {
|
||||||
|
0% {
|
||||||
|
background-color: rgba( 0, 0, 0, 0.2 );
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
background-color: rgba( 0, 0, 0, 0 );
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgba( 0, 0, 0, 0.2 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientAnim {
|
||||||
|
from {
|
||||||
|
transform: rotate( 0deg );
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate( 360deg );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
MusicPlayerV2-GUI/tsconfig.app.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "public/musickit.js"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
MusicPlayerV2-GUI/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
MusicPlayerV2-GUI/tsconfig.node.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node20/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node", "jquery"]
|
||||||
|
}
|
||||||
|
}
|
||||||
21
MusicPlayerV2-GUI/vite.config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
nodePolyfills(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 8081
|
||||||
|
}
|
||||||
|
})
|
||||||
21
README.md
@@ -1,12 +1,11 @@
|
|||||||
<div id="title" align="center">
|
<div id="title" align="center">
|
||||||
<img src="./assets/logo.png" width="300">
|
<img src="./assets/logo.png" width="300">
|
||||||
<h1>MusicPlayerV2</h1>
|
<h1>MusicPlayer</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="badges" align="center">
|
<div id="badges" align="center">
|
||||||
<img alt="Project License" src="https://img.shields.io/github/license/simplePCBuilding/MusicPlayerV2.svg">
|
<img alt="Project License" src="https://img.shields.io/github/license/simplePCBuilding/MusicPlayerV2.svg">
|
||||||
<img alt="GitHub Repo size" src="https://img.shields.io/github/repo-size/simplePCBuilding/MusicPlayerV2.svg">
|
<img alt="GitHub Repo size" src="https://img.shields.io/github/repo-size/simplePCBuilding/MusicPlayerV2.svg">
|
||||||
<img alt="Project code lines count" src="https://img.shields.io/tokei/lines/github/simplePCBuilding/MusicPlayerV2">
|
|
||||||
<img alt="GitHub Repo issues" src="https://img.shields.io/github/issues-pr-raw/simplePCBuilding/MusicPlayerV2">
|
<img alt="GitHub Repo issues" src="https://img.shields.io/github/issues-pr-raw/simplePCBuilding/MusicPlayerV2">
|
||||||
<img alt="Top Languages" src="https://img.shields.io/github/languages/top/simplePCBuilding/MusicPlayerV2">
|
<img alt="Top Languages" src="https://img.shields.io/github/languages/top/simplePCBuilding/MusicPlayerV2">
|
||||||
<img alt="GitHub Repo filecount" src="https://img.shields.io/github/directory-file-count/simplePCBuilding/MusicPlayerV2.svg">
|
<img alt="GitHub Repo filecount" src="https://img.shields.io/github/directory-file-count/simplePCBuilding/MusicPlayerV2.svg">
|
||||||
@@ -22,12 +21,18 @@
|
|||||||
<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>
|
||||||
|
|
||||||
A music player, specifically created for displaying song information from a CSV or JSON file on multiple different displays that are connected
|
A music player, specifically created for displaying song information on multiple different displays that are connected to the same network, just from the browser.
|
||||||
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).
|
||||||
|
|
||||||
|
<div id="donate" align="center">
|
||||||
|
<a href="https://store.janishutz.com/donate" target="_blank"><img src="https://store-cdn.janishutz.com/static/support-me.jpg" width="150px"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
- Electron App that runs on all major Desktop OS (Linux, MacOS & Windows)
|
- Browser based App that runs on all OS (Linux, MacOS, Windows, iOS, Android, iPadOS, ...)
|
||||||
- Show all song information over the local network on any amount of client displays
|
- Fully featured Music Player
|
||||||
- Client displays show the playback position and all information from the metadata and CSV / JSON file that contains all song information
|
- Show all song information over the Internet on any amount of client displays
|
||||||
|
- Client displays show the playback position and all information from song metadata fetched from the Apple Music API
|
||||||
- Play most common music files
|
- Play most common music files
|
||||||
- Backend to allow users to connect over the internet
|
- No setup required when using the hosted version at [music.janishutz.com](https://music.janishutz.com)
|
||||||
|
|||||||
184
backend/app.js
@@ -1,184 +0,0 @@
|
|||||||
const express = require( 'express' );
|
|
||||||
let app = express();
|
|
||||||
const path = require( 'path' );
|
|
||||||
const expressSession = require( 'express-session' );
|
|
||||||
const fs = require( 'fs' );
|
|
||||||
const bodyParser = require( 'body-parser' );
|
|
||||||
// const favicon = require( 'serve-favicon' );
|
|
||||||
|
|
||||||
const authKey = '' + fs.readFileSync( path.join( __dirname + '/authorizationKey.txt' ) );
|
|
||||||
|
|
||||||
app.use( expressSession ( {
|
|
||||||
secret: 'akgfsdkgfösdolfgslöodfvolwseifvoiwefö',
|
|
||||||
resave: true,
|
|
||||||
saveUninitialized: true
|
|
||||||
} ) );
|
|
||||||
app.use( bodyParser.urlencoded( { extended: false } ) );
|
|
||||||
app.use( bodyParser.json() );
|
|
||||||
// app.use( favicon( path.join( __dirname + '' ) ) );
|
|
||||||
|
|
||||||
let connectedClients = {};
|
|
||||||
let currentDetails = {
|
|
||||||
'songQueue': [],
|
|
||||||
'playingSong': {},
|
|
||||||
'pos': 0,
|
|
||||||
'isPlaying': false,
|
|
||||||
'queuePos': 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
app.get( '/', ( request, response ) => {
|
|
||||||
response.sendFile( path.join( __dirname + '/ui/index.html' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/showcase.js', ( request, response ) => {
|
|
||||||
response.sendFile( path.join( __dirname + '/ui/showcase.js' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/showcase.css', ( request, response ) => {
|
|
||||||
response.sendFile( path.join( __dirname + '/ui/showcase.css' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.post( '/authSSE', ( req, res ) => {
|
|
||||||
if ( req.body.authKey === authKey ) {
|
|
||||||
req.session.isAuth = true;
|
|
||||||
res.send( 'ok' );
|
|
||||||
} else {
|
|
||||||
res.send( 'hello' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.post( '/fancy/auth', ( req, res ) => {
|
|
||||||
if ( req.body.key === authKey ) {
|
|
||||||
req.session.isAuth = true;
|
|
||||||
res.redirect( '/fancy' );
|
|
||||||
} else {
|
|
||||||
res.send( 'wrong' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/fancy', ( req, res ) => {
|
|
||||||
if ( req.session.isAuth ) {
|
|
||||||
res.sendFile( path.join( __dirname + '/ui/fancy/showcase.html' ) );
|
|
||||||
} else {
|
|
||||||
res.sendFile( path.join( __dirname + '/ui/fancy/auth.html' ) );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/fancy/showcase.js', ( req, res ) => {
|
|
||||||
if ( req.session.isAuth ) {
|
|
||||||
res.sendFile( path.join( __dirname + '/ui/fancy/showcase.js' ) );
|
|
||||||
} else {
|
|
||||||
res.redirect( '/' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/fancy/showcase.css', ( req, res ) => {
|
|
||||||
if ( req.session.isAuth ) {
|
|
||||||
res.sendFile( path.join( __dirname + '/ui/fancy/showcase.css' ) );
|
|
||||||
} else {
|
|
||||||
res.redirect( '/' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/fancy/backgroundAnim.css', ( req, res ) => {
|
|
||||||
if ( req.session.isAuth ) {
|
|
||||||
res.sendFile( path.join( __dirname + '/ui/fancy/backgroundAnim.css' ) );
|
|
||||||
} else {
|
|
||||||
res.redirect( '/' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
let connectedMain = {};
|
|
||||||
|
|
||||||
app.get( '/mainNotifier', ( req, res ) => {
|
|
||||||
const ipRetrieved = req.headers[ 'x-forwarded-for' ];
|
|
||||||
const ip = ipRetrieved ? ipRetrieved.split( /, / )[ 0 ] : req.connection.remoteAddress;
|
|
||||||
if ( req.session.isAuth ) {
|
|
||||||
res.writeHead( 200, {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
} );
|
|
||||||
res.status( 200 );
|
|
||||||
res.flushHeaders();
|
|
||||||
let det = { 'type': 'basics' };
|
|
||||||
res.write( `data: ${ JSON.stringify( det ) }\n\n` );
|
|
||||||
connectedMain = res;
|
|
||||||
} else {
|
|
||||||
res.send( 'wrong' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// STATUS UPDATE from the client display to send to main ui
|
|
||||||
// Send update if page is closed
|
|
||||||
const allowedMainUpdates = [ 'blur', 'visibility' ];
|
|
||||||
app.post( '/clientStatusUpdate', ( req, res ) => {
|
|
||||||
if ( allowedMainUpdates.includes( req.body.type ) ) {
|
|
||||||
const ipRetrieved = req.headers[ 'x-forwarded-for' ];
|
|
||||||
const ip = ipRetrieved ? ipRetrieved.split( /, / )[ 0 ] : req.connection.remoteAddress;
|
|
||||||
sendClientUpdate( req.body.type, ip );
|
|
||||||
res.send( 'ok' );
|
|
||||||
} else {
|
|
||||||
res.status( 400 ).send( 'ERR_UNKNOWN_TYPE' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
const sendClientUpdate = ( update, ip ) => {
|
|
||||||
try {
|
|
||||||
connectedMain.write( 'data: ' + JSON.stringify( { 'type': update, 'ip': ip } ) + '\n\n' );
|
|
||||||
} catch ( err ) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
app.post( '/connect', ( request, response ) => {
|
|
||||||
if ( request.body.authKey === authKey ) {
|
|
||||||
request.session.authorized = true;
|
|
||||||
response.send( 'Handshake OK' );
|
|
||||||
} else {
|
|
||||||
response.status( 403 ).send( 'AuthKey wrong' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/clientDisplayNotifier', ( req, res ) => {
|
|
||||||
res.writeHead( 200, {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
} );
|
|
||||||
res.status( 200 );
|
|
||||||
res.flushHeaders();
|
|
||||||
let det = { 'type': 'basics', 'data': currentDetails };
|
|
||||||
res.write( `data: ${ JSON.stringify( det ) }\n\n` );
|
|
||||||
connectedClients[ req.session.id ] = res;
|
|
||||||
req.on( 'close', () => {
|
|
||||||
connectedClients.splice( Object.keys( connectedClients ).indexOf( req.session.id ), 1 );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
const sendUpdate = ( update ) => {
|
|
||||||
for ( let client in connectedClients ) {
|
|
||||||
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': update, 'data': currentDetails[ update ] } ) + '\n\n' );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const allowedTypes = [ 'playingSong', 'isPlaying', 'songQueue', 'pos', 'queuePos' ];
|
|
||||||
app.post( '/statusUpdate', ( req, res ) => {
|
|
||||||
if ( req.body.authKey === authKey ) {
|
|
||||||
if ( allowedTypes.includes( req.body.type ) ) {
|
|
||||||
currentDetails[ req.body.type ] = req.body.data
|
|
||||||
res.send( 'thanks' );
|
|
||||||
sendUpdate( req.body.type );
|
|
||||||
} else {
|
|
||||||
res.status( 400 ).send( 'incorrect request' );
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.status( 403 ).send( 'Unauthorized' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.use( ( request, response, next ) => {
|
|
||||||
response.sendFile( path.join( __dirname + '' ) )
|
|
||||||
} );
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
|
||||||
app.listen( PORT );
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
gaöwovwef89voawö8p9 odövefw8öoaewpf89wec
|
|
||||||
7
backend/config/sdk.config.testing.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"token": "phafowegoväbwpb$weapvbpvfwcvfäawef39'ü0wtäqgpt5^ü62q'ẗ9wäa3g",
|
||||||
|
"name": "localhost:8082",
|
||||||
|
"client": "localhost:8081",
|
||||||
|
"backendURL": "http://localhost:8080",
|
||||||
|
"failReturnURL": "http://localhost:8081"
|
||||||
|
}
|
||||||
5
backend/config/store-sdk.config.testing.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"backendURL": "http://localhost:8083",
|
||||||
|
"name": "testing",
|
||||||
|
"signingSecret": "test"
|
||||||
|
}
|
||||||
702
backend/eslint.config.mjs
Normal file
@@ -0,0 +1,702 @@
|
|||||||
|
import vue from 'eslint-plugin-vue';
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import typescript from '@typescript-eslint/eslint-plugin';
|
||||||
|
import stylistic from '@stylistic/eslint-plugin';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
'plugins': {
|
||||||
|
'@stylistic': stylistic,
|
||||||
|
'@stylistic/js': stylistic,
|
||||||
|
'@stylistic/ts': stylistic,
|
||||||
|
},
|
||||||
|
'files': [
|
||||||
|
'**/*.ts',
|
||||||
|
'**/*.js',
|
||||||
|
'**/*.mjs',
|
||||||
|
'**/*.cjs',
|
||||||
|
'**/*.tsx',
|
||||||
|
'**/*.jsx'
|
||||||
|
],
|
||||||
|
'rules': {
|
||||||
|
// Formatting
|
||||||
|
'@stylistic/array-bracket-newline': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'multiline': true,
|
||||||
|
'minItems': 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@stylistic/array-bracket-spacing': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'@stylistic/array-element-newline': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'multiline': true,
|
||||||
|
'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'
|
||||||
|
],
|
||||||
|
'@stylistic/comma-spacing': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'before': false,
|
||||||
|
'after': true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@stylistic/comma-style': [
|
||||||
|
'error',
|
||||||
|
'last'
|
||||||
|
],
|
||||||
|
'@stylistic/dot-location': [
|
||||||
|
'error',
|
||||||
|
'property'
|
||||||
|
],
|
||||||
|
'@stylistic/eol-last': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'@stylistic/function-call-spacing': [
|
||||||
|
'error',
|
||||||
|
'never'
|
||||||
|
],
|
||||||
|
'@stylistic/function-paren-newline': [
|
||||||
|
'error',
|
||||||
|
'multiline'
|
||||||
|
],
|
||||||
|
'@stylistic/function-call-argument-newline': [
|
||||||
|
'error',
|
||||||
|
'consistent'
|
||||||
|
],
|
||||||
|
'@stylistic/implicit-arrow-linebreak': [
|
||||||
|
'error',
|
||||||
|
'beside'
|
||||||
|
],
|
||||||
|
'@stylistic/indent': [
|
||||||
|
'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': 90,
|
||||||
|
'comments': 100,
|
||||||
|
'ignoreComments': false,
|
||||||
|
'ignoreUrls': true,
|
||||||
|
'ignoreStrings': false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@stylistic/new-parens': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'@stylistic/newline-per-chained-call': [ '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/operator-linebreak': [
|
||||||
|
'error',
|
||||||
|
'before'
|
||||||
|
],
|
||||||
|
'@stylistic/one-var-declaration-per-line': 'error',
|
||||||
|
'@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/template-curly-spacing': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
'@stylistic/switch-colon-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,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
2
backend/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
const app = require( './dist/app.js' ).default;
|
||||||
|
app.run();
|
||||||
788
backend/package-lock.json
generated
@@ -1,788 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "backend",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"dependencies": {
|
|
||||||
"body-parser": "^1.20.2",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"express-session": "^1.17.3",
|
|
||||||
"express-static": "^1.2.6",
|
|
||||||
"serve-favicon": "^2.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/accepts": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-types": "~2.1.34",
|
|
||||||
"negotiator": "0.6.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/array-flatten": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
|
||||||
},
|
|
||||||
"node_modules/body-parser": {
|
|
||||||
"version": "1.20.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
|
||||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
|
||||||
"dependencies": {
|
|
||||||
"bytes": "3.1.2",
|
|
||||||
"content-type": "~1.0.5",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"destroy": "1.2.0",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"iconv-lite": "0.4.24",
|
|
||||||
"on-finished": "2.4.1",
|
|
||||||
"qs": "6.11.0",
|
|
||||||
"raw-body": "2.5.2",
|
|
||||||
"type-is": "~1.6.18",
|
|
||||||
"unpipe": "1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8",
|
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bytes": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/call-bind": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"function-bind": "^1.1.2",
|
|
||||||
"get-intrinsic": "^1.2.1",
|
|
||||||
"set-function-length": "^1.1.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/content-disposition": {
|
|
||||||
"version": "0.5.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
|
||||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "5.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/content-type": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cookie": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cookie-signature": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
|
||||||
"version": "2.6.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
|
||||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/define-data-property": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"get-intrinsic": "^1.2.1",
|
|
||||||
"gopd": "^1.0.1",
|
|
||||||
"has-property-descriptors": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/depd": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/destroy": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8",
|
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ee-first": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
|
||||||
},
|
|
||||||
"node_modules/encodeurl": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/escape-html": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
|
||||||
},
|
|
||||||
"node_modules/etag": {
|
|
||||||
"version": "1.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
|
||||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/express": {
|
|
||||||
"version": "4.18.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
|
||||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"accepts": "~1.3.8",
|
|
||||||
"array-flatten": "1.1.1",
|
|
||||||
"body-parser": "1.20.1",
|
|
||||||
"content-disposition": "0.5.4",
|
|
||||||
"content-type": "~1.0.4",
|
|
||||||
"cookie": "0.5.0",
|
|
||||||
"cookie-signature": "1.0.6",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"etag": "~1.8.1",
|
|
||||||
"finalhandler": "1.2.0",
|
|
||||||
"fresh": "0.5.2",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"merge-descriptors": "1.0.1",
|
|
||||||
"methods": "~1.1.2",
|
|
||||||
"on-finished": "2.4.1",
|
|
||||||
"parseurl": "~1.3.3",
|
|
||||||
"path-to-regexp": "0.1.7",
|
|
||||||
"proxy-addr": "~2.0.7",
|
|
||||||
"qs": "6.11.0",
|
|
||||||
"range-parser": "~1.2.1",
|
|
||||||
"safe-buffer": "5.2.1",
|
|
||||||
"send": "0.18.0",
|
|
||||||
"serve-static": "1.15.0",
|
|
||||||
"setprototypeof": "1.2.0",
|
|
||||||
"statuses": "2.0.1",
|
|
||||||
"type-is": "~1.6.18",
|
|
||||||
"utils-merge": "1.0.1",
|
|
||||||
"vary": "~1.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/express-session": {
|
|
||||||
"version": "1.17.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
|
|
||||||
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
|
|
||||||
"dependencies": {
|
|
||||||
"cookie": "0.4.2",
|
|
||||||
"cookie-signature": "1.0.6",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "~2.0.0",
|
|
||||||
"on-headers": "~1.0.2",
|
|
||||||
"parseurl": "~1.3.3",
|
|
||||||
"safe-buffer": "5.2.1",
|
|
||||||
"uid-safe": "~2.1.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/express-session/node_modules/cookie": {
|
|
||||||
"version": "0.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
|
||||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/express-static": {
|
|
||||||
"version": "1.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/express-static/-/express-static-1.2.6.tgz",
|
|
||||||
"integrity": "sha512-pmp8fSe+bCxGCTk5+X6HzIF2APYMvrAq3Y7sT/WOELc0JZZxJhMTVgCbxcoUgMRBSqnQ9EfTw/Uv3J1ONoIAuA==",
|
|
||||||
"dependencies": {
|
|
||||||
"mime2": "latest"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"express-static": "bin/server.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/express/node_modules/body-parser": {
|
|
||||||
"version": "1.20.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
|
||||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
|
||||||
"dependencies": {
|
|
||||||
"bytes": "3.1.2",
|
|
||||||
"content-type": "~1.0.4",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"destroy": "1.2.0",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"iconv-lite": "0.4.24",
|
|
||||||
"on-finished": "2.4.1",
|
|
||||||
"qs": "6.11.0",
|
|
||||||
"raw-body": "2.5.1",
|
|
||||||
"type-is": "~1.6.18",
|
|
||||||
"unpipe": "1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8",
|
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/express/node_modules/raw-body": {
|
|
||||||
"version": "2.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
|
||||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
|
||||||
"dependencies": {
|
|
||||||
"bytes": "3.1.2",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"iconv-lite": "0.4.24",
|
|
||||||
"unpipe": "1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/finalhandler": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"on-finished": "2.4.1",
|
|
||||||
"parseurl": "~1.3.3",
|
|
||||||
"statuses": "2.0.1",
|
|
||||||
"unpipe": "~1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/forwarded": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fresh": {
|
|
||||||
"version": "0.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
|
||||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/function-bind": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/get-intrinsic": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
|
|
||||||
"dependencies": {
|
|
||||||
"function-bind": "^1.1.2",
|
|
||||||
"has-proto": "^1.0.1",
|
|
||||||
"has-symbols": "^1.0.3",
|
|
||||||
"hasown": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gopd": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
|
||||||
"dependencies": {
|
|
||||||
"get-intrinsic": "^1.1.3"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-property-descriptors": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
|
|
||||||
"dependencies": {
|
|
||||||
"get-intrinsic": "^1.2.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-proto": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-symbols": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hasown": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
|
|
||||||
"dependencies": {
|
|
||||||
"function-bind": "^1.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/http-errors": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"inherits": "2.0.4",
|
|
||||||
"setprototypeof": "1.2.0",
|
|
||||||
"statuses": "2.0.1",
|
|
||||||
"toidentifier": "1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/iconv-lite": {
|
|
||||||
"version": "0.4.24",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
|
||||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/inherits": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
|
||||||
},
|
|
||||||
"node_modules/ipaddr.js": {
|
|
||||||
"version": "1.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
|
||||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/media-typer": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
|
||||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/merge-descriptors": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
|
||||||
},
|
|
||||||
"node_modules/methods": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
|
||||||
"bin": {
|
|
||||||
"mime": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime2": {
|
|
||||||
"version": "0.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime2/-/mime2-0.0.11.tgz",
|
|
||||||
"integrity": "sha512-Ch599Y8U4vUgG9AaQgLEnIdXRRLoZPfjAdWFDLYePBEJS1nsS43H2pzbZn0u8lkbYx+yInuKxLsUEgM3fa9ZLQ=="
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
|
||||||
},
|
|
||||||
"node_modules/negotiator": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/object-inspect": {
|
|
||||||
"version": "1.13.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
|
||||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/on-finished": {
|
|
||||||
"version": "2.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
|
||||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
|
||||||
"dependencies": {
|
|
||||||
"ee-first": "1.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/on-headers": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/parseurl": {
|
|
||||||
"version": "1.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
|
||||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-to-regexp": {
|
|
||||||
"version": "0.1.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
|
||||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
|
||||||
},
|
|
||||||
"node_modules/proxy-addr": {
|
|
||||||
"version": "2.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
|
||||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
|
||||||
"dependencies": {
|
|
||||||
"forwarded": "0.2.0",
|
|
||||||
"ipaddr.js": "1.9.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/qs": {
|
|
||||||
"version": "6.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"side-channel": "^1.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/random-bytes": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/range-parser": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/raw-body": {
|
|
||||||
"version": "2.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
|
||||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
|
||||||
"dependencies": {
|
|
||||||
"bytes": "3.1.2",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"iconv-lite": "0.4.24",
|
|
||||||
"unpipe": "1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/safe-buffer": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/safer-buffer": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
|
||||||
},
|
|
||||||
"node_modules/send": {
|
|
||||||
"version": "0.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
|
||||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"destroy": "1.2.0",
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"etag": "~1.8.1",
|
|
||||||
"fresh": "0.5.2",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"mime": "1.6.0",
|
|
||||||
"ms": "2.1.3",
|
|
||||||
"on-finished": "2.4.1",
|
|
||||||
"range-parser": "~1.2.1",
|
|
||||||
"statuses": "2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/send/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
|
||||||
},
|
|
||||||
"node_modules/serve-favicon": {
|
|
||||||
"version": "2.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz",
|
|
||||||
"integrity": "sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==",
|
|
||||||
"dependencies": {
|
|
||||||
"etag": "~1.8.1",
|
|
||||||
"fresh": "0.5.2",
|
|
||||||
"ms": "2.1.1",
|
|
||||||
"parseurl": "~1.3.2",
|
|
||||||
"safe-buffer": "5.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/serve-favicon/node_modules/ms": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
|
||||||
},
|
|
||||||
"node_modules/serve-favicon/node_modules/safe-buffer": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
|
||||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
|
||||||
},
|
|
||||||
"node_modules/serve-static": {
|
|
||||||
"version": "1.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
|
||||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
|
||||||
"dependencies": {
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"parseurl": "~1.3.3",
|
|
||||||
"send": "0.18.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/set-function-length": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"define-data-property": "^1.1.1",
|
|
||||||
"get-intrinsic": "^1.2.1",
|
|
||||||
"gopd": "^1.0.1",
|
|
||||||
"has-property-descriptors": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/setprototypeof": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
|
||||||
},
|
|
||||||
"node_modules/side-channel": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
|
||||||
"dependencies": {
|
|
||||||
"call-bind": "^1.0.0",
|
|
||||||
"get-intrinsic": "^1.0.2",
|
|
||||||
"object-inspect": "^1.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/statuses": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/toidentifier": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/type-is": {
|
|
||||||
"version": "1.6.18",
|
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
|
||||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
|
||||||
"dependencies": {
|
|
||||||
"media-typer": "0.3.0",
|
|
||||||
"mime-types": "~2.1.24"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/uid-safe": {
|
|
||||||
"version": "2.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
|
||||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
|
||||||
"dependencies": {
|
|
||||||
"random-bytes": "~1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unpipe": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/utils-merge": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vary": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,43 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"name": "musicplayer-v2-backend",
|
||||||
"body-parser": "^1.20.2",
|
"version": "1.0.0",
|
||||||
"express": "^4.18.2",
|
"description": "The backend for MusicPlayerV2",
|
||||||
"express-session": "^1.17.3",
|
"main": "index.js",
|
||||||
"express-static": "^1.2.6",
|
"scripts": {
|
||||||
"serve-favicon": "^2.5.0"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
}
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/simplePCBuilding/MusicPlayerV2.git"
|
||||||
|
},
|
||||||
|
"author": "Janis Hutz",
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/simplePCBuilding/MusicPlayerV2/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/simplePCBuilding/MusicPlayerV2#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.29.0",
|
||||||
|
"@stylistic/eslint-plugin": "^5.0.0",
|
||||||
|
"@types/express-session": "^1.18.0",
|
||||||
|
"eslint-plugin-vue": "^10.2.0",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"typescript-eslint": "^8.35.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@janishutz/login-sdk-server": "^1.2.0",
|
||||||
|
"@janishutz/login-sdk-server-stubs": "^1.0.0",
|
||||||
|
"@janishutz/store-sdk": "^1.1.0",
|
||||||
|
"@types/body-parser": "^1.19.5",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"express-session": "^1.18.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"node-mysql": "^0.4.2",
|
||||||
|
"socket.io": "^4.7.5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
279
backend/src/app.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import cors from 'cors';
|
||||||
|
import {
|
||||||
|
createServer
|
||||||
|
} from 'node:http';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import {
|
||||||
|
SocketData
|
||||||
|
} from './definitions';
|
||||||
|
|
||||||
|
|
||||||
|
// ┌ ┐
|
||||||
|
// │ Handle FOSS vs paid version │
|
||||||
|
// └ ┘
|
||||||
|
const isFossVersion = false;
|
||||||
|
|
||||||
|
import storeSDK from '@janishutz/store-sdk';
|
||||||
|
import sdk from '@janishutz/login-sdk-server';
|
||||||
|
import sse from './sse';
|
||||||
|
import socket from './socket';
|
||||||
|
|
||||||
|
// const isFossVersion = true;
|
||||||
|
//
|
||||||
|
// import storeSDK from './sdk/store-sdk-stub';
|
||||||
|
// import sdk from '@janishutz/login-sdk-server-stubs';
|
||||||
|
|
||||||
|
|
||||||
|
const corsOpts: cors.CorsOptions = {
|
||||||
|
'credentials': true,
|
||||||
|
'origin': ( origin, cb ) => {
|
||||||
|
if ( isFossVersion ) cb( null, true );
|
||||||
|
else cb( null, origin === 'https://music.janishutz.com' );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
const app = express();
|
||||||
|
const httpServer = createServer( app );
|
||||||
|
|
||||||
|
if ( !isFossVersion ) {
|
||||||
|
console.error( '[ APP ] Starting in non-FOSS version' );
|
||||||
|
|
||||||
|
const storeConfig = JSON.parse( fs.readFileSync( path.join(
|
||||||
|
__dirname,
|
||||||
|
'/config/store-sdk.config.secret.json'
|
||||||
|
) ).toString() );
|
||||||
|
|
||||||
|
console.error( storeConfig );
|
||||||
|
|
||||||
|
storeSDK.configure( storeConfig );
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
const sdkConfig = JSON.parse( fs.readFileSync( path.join(
|
||||||
|
__dirname,
|
||||||
|
'/config/sdk.config.secret.json'
|
||||||
|
) ).toString() );
|
||||||
|
|
||||||
|
// Load id.janishutz.com SDK and allow signing in
|
||||||
|
sdk.setUp(
|
||||||
|
{
|
||||||
|
'prod': false,
|
||||||
|
'service': {
|
||||||
|
'serviceID': 'jh-music',
|
||||||
|
'serviceToken': sdkConfig[ 'token' ]
|
||||||
|
},
|
||||||
|
'user-agent': sdkConfig[ 'ua' ],
|
||||||
|
'sessionType': 'memory',
|
||||||
|
'frontendURL': 'https://music.janishutz.com',
|
||||||
|
'corsWhitelist': [ 'https://music.janishutz.com' ],
|
||||||
|
'recheckTimeout': 300 * 1000,
|
||||||
|
'advancedVerification': 'sdk',
|
||||||
|
},
|
||||||
|
app,
|
||||||
|
() => {
|
||||||
|
return new Promise( resolve => {
|
||||||
|
resolve( true );
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return new Promise( resolve => {
|
||||||
|
resolve( true );
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return new Promise( resolve => {
|
||||||
|
resolve( true );
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return new Promise( resolve => {
|
||||||
|
resolve( true );
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Configuration of SSE or WebSocket
|
||||||
|
*/
|
||||||
|
const socketData: SocketData = {};
|
||||||
|
|
||||||
|
sse.useSSE( app, socketData, corsOpts, sdk.getSessionID, sdk.getSignedIn );
|
||||||
|
socket.useWebSocket( httpServer, socketData );
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
GENERAL ROUTES
|
||||||
|
*/
|
||||||
|
app.get( '/', ( _request: express.Request, response: express.Response ) => {
|
||||||
|
response.send( 'Please visit <a href="https://music.janishutz.com">https://music.janishutz.com</a> to use this service' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/createRoomToken',
|
||||||
|
cors( corsOpts ),
|
||||||
|
sdk.loginCheck(),
|
||||||
|
( request: express.Request, response: express.Response ) => {
|
||||||
|
// eslint-disable-next-line no-constant-binary-expression
|
||||||
|
const roomName = String( request.query.roomName ) ?? '';
|
||||||
|
|
||||||
|
if ( !socketData[ roomName ] ) {
|
||||||
|
const roomToken = crypto.randomUUID();
|
||||||
|
|
||||||
|
socketData[ roomName ] = {
|
||||||
|
'playbackStart': 0,
|
||||||
|
'playbackStatus': false,
|
||||||
|
'playlist': [],
|
||||||
|
'playlistIndex': 0,
|
||||||
|
'roomName': roomName,
|
||||||
|
'roomToken': roomToken,
|
||||||
|
'ownerUID': sdk.getUID( request ),
|
||||||
|
'useAntiTamper': request.query.useAntiTamper === 'true'
|
||||||
|
? true : false,
|
||||||
|
};
|
||||||
|
response.send( roomToken );
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
socketData[ roomName ].ownerUID
|
||||||
|
=== sdk.getUID( request )
|
||||||
|
) {
|
||||||
|
response.send( socketData[ roomName ].roomToken );
|
||||||
|
} else {
|
||||||
|
response.status( 409 ).send( 'ERR_CONFLICT' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/getAppleMusicDevToken',
|
||||||
|
cors( corsOpts ),
|
||||||
|
sdk.loginCheck(),
|
||||||
|
( req, res ) => {
|
||||||
|
checkIfOwned( req )
|
||||||
|
.then( owned => {
|
||||||
|
if ( owned ) {
|
||||||
|
// sign dev token
|
||||||
|
const privateKey = fs.readFileSync( path.join(
|
||||||
|
__dirname,
|
||||||
|
'/config/apple_private_key.p8'
|
||||||
|
) ).toString();
|
||||||
|
const config = JSON.parse( fs.readFileSync( path.join(
|
||||||
|
__dirname,
|
||||||
|
'/config/apple-music-api.config.secret.json'
|
||||||
|
) ).toString() );
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const tomorrow = now + ( 24 * 3600 * 1000 );
|
||||||
|
const jwtToken = jwt.sign( {
|
||||||
|
'iss': config.teamID,
|
||||||
|
'iat': Math.floor( now / 1000 ),
|
||||||
|
'exp': Math.floor( tomorrow / 1000 ),
|
||||||
|
}, privateKey, {
|
||||||
|
'algorithm': 'ES256',
|
||||||
|
'keyid': config.keyID
|
||||||
|
} );
|
||||||
|
|
||||||
|
res.send( jwtToken );
|
||||||
|
} else {
|
||||||
|
res.status( 402 ).send( 'ERR_NOT_OWNED' );
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.catch( e => {
|
||||||
|
if ( e === 'ERR_NOT_OWNED' ) {
|
||||||
|
res.status( 402 ).send( e );
|
||||||
|
} else if ( e === 'ERR_AUTH_REQUIRED' ) {
|
||||||
|
res.status( 401 ).send( e );
|
||||||
|
} else {
|
||||||
|
res.send( 500 ).send( e );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const ownedCache = {};
|
||||||
|
|
||||||
|
const checkIfOwned = ( request: express.Request ): Promise<boolean> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
const uid = sdk.getUID( request );
|
||||||
|
|
||||||
|
if ( ownedCache[ uid ] ) {
|
||||||
|
resolve( ownedCache[ uid ] );
|
||||||
|
} else {
|
||||||
|
storeSDK.getSubscriptions( uid )
|
||||||
|
.then( stat => {
|
||||||
|
console.error( 'Subscription check was successful' );
|
||||||
|
const now = new Date().getTime();
|
||||||
|
|
||||||
|
for ( const sub in stat ) {
|
||||||
|
if ( stat[ sub ].expires - now > 0
|
||||||
|
&& (
|
||||||
|
stat[ sub ].id
|
||||||
|
=== 'com.janishutz.MusicPlayer.subscription'
|
||||||
|
|| stat[ sub ].id
|
||||||
|
=== 'com.janishutz.MusicPlayer.subscription-month'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ownedCache[ uid ] = true;
|
||||||
|
resolve( true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ownedCache[ uid ] = false;
|
||||||
|
resolve( false );
|
||||||
|
} )
|
||||||
|
.catch( e => {
|
||||||
|
console.error( 'Subscription check unsuccessful with error', e );
|
||||||
|
reject( 'ERR_NOT_OWNED' );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/checkUserStatus',
|
||||||
|
cors( corsOpts ),
|
||||||
|
sdk.loginCheck(),
|
||||||
|
( request: express.Request, response: express.Response ) => {
|
||||||
|
checkIfOwned( request )
|
||||||
|
.then( owned => {
|
||||||
|
if ( owned ) {
|
||||||
|
response.send( 'ok' );
|
||||||
|
} else {
|
||||||
|
response.status( 402 ).send( 'ERR_NOT_OWNED' );
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.catch( e => {
|
||||||
|
if ( e === 'ERR_NOT_OWNED' ) {
|
||||||
|
response.status( 402 ).send( e );
|
||||||
|
} else if ( e === 'ERR_AUTH_REQUIRED' ) {
|
||||||
|
response.status( 401 ).send( e );
|
||||||
|
} else {
|
||||||
|
response.send( 500 ).send( e );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use( ( request: express.Request, response: express.Response ) => {
|
||||||
|
response.status( 404 ).send( 'ERR_NOT_FOUND: ' + request.path );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 8082;
|
||||||
|
|
||||||
|
httpServer.listen( PORT );
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
run
|
||||||
|
};
|
||||||
23
backend/src/definitions.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export interface Room {
|
||||||
|
'playbackStatus': boolean;
|
||||||
|
'playbackStart': number;
|
||||||
|
'playlist': Song[];
|
||||||
|
'playlistIndex': number;
|
||||||
|
'roomName': string;
|
||||||
|
'roomToken': string;
|
||||||
|
'ownerUID': string;
|
||||||
|
'useAntiTamper': boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Song {
|
||||||
|
'title': string;
|
||||||
|
'artist': string;
|
||||||
|
'duration': number;
|
||||||
|
'cover': string;
|
||||||
|
'additionalInfo'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocketData {
|
||||||
|
[key: string]: Room;
|
||||||
|
}
|
||||||
|
|
||||||
15
backend/src/sdk/store-sdk-stub.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const getSubscriptions = ( _uid: string ) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': 'com.janishutz.MusicPlayer.subscription',
|
||||||
|
'expires': new Date().getTime() + 200000,
|
||||||
|
'status': true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getSubscriptions,
|
||||||
|
};
|
||||||
|
|
||||||
178
backend/src/socket.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import http from 'node:http';
|
||||||
|
import {
|
||||||
|
Server
|
||||||
|
} from 'socket.io';
|
||||||
|
import type {
|
||||||
|
SocketData,
|
||||||
|
Song
|
||||||
|
} from './definitions';
|
||||||
|
|
||||||
|
const useWebSocket = ( httpServer: http.Server, socketData: SocketData ) => {
|
||||||
|
// Websocket for events
|
||||||
|
const io = new Server( httpServer, {
|
||||||
|
'cors': {
|
||||||
|
'origin': true,
|
||||||
|
'credentials': true,
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
io.on( 'connection', socket => {
|
||||||
|
socket.on( 'create-room', ( room: {
|
||||||
|
'name': string,
|
||||||
|
'token': string
|
||||||
|
}, cb: ( res: {
|
||||||
|
'status': boolean,
|
||||||
|
'msg': string
|
||||||
|
} ) => void ) => {
|
||||||
|
if ( socketData[ room.name ] ) {
|
||||||
|
if ( room.token === socketData[ room.name ].roomToken ) {
|
||||||
|
socket.join( room.name );
|
||||||
|
cb( {
|
||||||
|
'status': true,
|
||||||
|
'msg': 'ADDED_TO_ROOM'
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
cb( {
|
||||||
|
'status': false,
|
||||||
|
'msg': 'ERR_TOKEN_INVALID'
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb( {
|
||||||
|
'status': false,
|
||||||
|
'msg': 'ERR_NAME_INVALID'
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'delete-room', ( room: {
|
||||||
|
'name': string,
|
||||||
|
'token': string
|
||||||
|
}, cb: ( res: {
|
||||||
|
'status': boolean,
|
||||||
|
'msg': string
|
||||||
|
} ) => void ) => {
|
||||||
|
if ( socketData[ room.name ] ) {
|
||||||
|
if ( room.token === socketData[ room.name ].roomToken ) {
|
||||||
|
socket.leave( room.name );
|
||||||
|
socket.to( room.name ).emit( 'delete-share', room.name );
|
||||||
|
socketData[ room.name ] = undefined;
|
||||||
|
cb( {
|
||||||
|
'status': true,
|
||||||
|
'msg': 'ROOM_DELETED'
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
cb( {
|
||||||
|
'status': false,
|
||||||
|
'msg': 'ERR_TOKEN_INVALID'
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb( {
|
||||||
|
'status': false,
|
||||||
|
'msg': 'ERR_NAME_INVALID'
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'join-room', ( room: string, cb: ( res: {
|
||||||
|
'status': boolean,
|
||||||
|
'msg': string,
|
||||||
|
'data'?: {
|
||||||
|
'playbackStatus': boolean,
|
||||||
|
'playbackStart': number,
|
||||||
|
'playlist': Song[],
|
||||||
|
'playlistIndex': number,
|
||||||
|
'useAntiTamper': boolean
|
||||||
|
}
|
||||||
|
} ) => void ) => {
|
||||||
|
if ( socketData[ room ] ) {
|
||||||
|
socket.join( room );
|
||||||
|
cb( {
|
||||||
|
'data': {
|
||||||
|
'playbackStart': socketData[ room ].playbackStart,
|
||||||
|
'playbackStatus': socketData[ room ].playbackStatus,
|
||||||
|
'playlist': socketData[ room ].playlist,
|
||||||
|
'playlistIndex': socketData[ room ].playlistIndex,
|
||||||
|
'useAntiTamper': socketData[ room ].useAntiTamper,
|
||||||
|
},
|
||||||
|
'msg': 'STATUS_OK',
|
||||||
|
'status': true,
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
cb( {
|
||||||
|
'msg': 'ERR_NO_ROOM_WITH_THIS_ID',
|
||||||
|
'status': false,
|
||||||
|
} );
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'tampering', ( data: {
|
||||||
|
'msg': string,
|
||||||
|
'roomName': string
|
||||||
|
} ) => {
|
||||||
|
if ( data.roomName ) {
|
||||||
|
socket.to( data.roomName ).emit( 'tampering-msg', data.msg );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'playlist-update', ( data: {
|
||||||
|
'roomName': string,
|
||||||
|
'roomToken': string,
|
||||||
|
'data': Song[]
|
||||||
|
} ) => {
|
||||||
|
if ( socketData[ data.roomName ] ) {
|
||||||
|
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||||
|
if ( socketData[ data.roomName ].playlist !== data.data ) {
|
||||||
|
socketData[ data.roomName ].playlist = data.data;
|
||||||
|
io.to( data.roomName ).emit( 'playlist', data.data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'playback-update', ( data: {
|
||||||
|
'roomName': string,
|
||||||
|
'roomToken': string,
|
||||||
|
'data': boolean
|
||||||
|
} ) => {
|
||||||
|
if ( socketData[ data.roomName ] ) {
|
||||||
|
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||||
|
socketData[ data.roomName ].playbackStatus = data.data;
|
||||||
|
io.to( data.roomName ).emit( 'playback', data.data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'playlist-index-update', ( data: {
|
||||||
|
'roomName': string,
|
||||||
|
'roomToken': string,
|
||||||
|
'data': number
|
||||||
|
} ) => {
|
||||||
|
if ( socketData[ data.roomName ] ) {
|
||||||
|
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||||
|
socketData[ data.roomName ].playlistIndex = data.data;
|
||||||
|
io.to( data.roomName ).emit( 'playlist-index', data.data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
socket.on( 'playback-start-update', ( data: {
|
||||||
|
'roomName': string,
|
||||||
|
'roomToken': string,
|
||||||
|
'data': number
|
||||||
|
} ) => {
|
||||||
|
if ( socketData[ data.roomName ] ) {
|
||||||
|
if ( socketData[ data.roomName ].roomToken === data.roomToken ) {
|
||||||
|
socketData[ data.roomName ].playbackStart = data.data;
|
||||||
|
io.to( data.roomName ).emit( 'playback-start', data.data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
useWebSocket
|
||||||
|
};
|
||||||
252
backend/src/sse.ts
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import cors from 'cors';
|
||||||
|
import express from 'express';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import {
|
||||||
|
SocketData
|
||||||
|
} from './definitions';
|
||||||
|
|
||||||
|
const useSSE = (
|
||||||
|
app: express.Application,
|
||||||
|
socketData: SocketData,
|
||||||
|
corsOpts: cors.CorsOptions,
|
||||||
|
getSessionID: ( request: express.Request ) => string,
|
||||||
|
getSignedIn: ( request: express.Request ) => boolean
|
||||||
|
) => {
|
||||||
|
/*
|
||||||
|
ROUTES FOR SERVER SENT EVENTS VERSION
|
||||||
|
*/
|
||||||
|
// Connected clients have their session ID as key
|
||||||
|
interface SocketClientList {
|
||||||
|
[key: string]: SocketClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocketClient {
|
||||||
|
'response': express.Response;
|
||||||
|
'room': string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientReferenceList {
|
||||||
|
/**
|
||||||
|
* Find all clients connected to one room
|
||||||
|
*/
|
||||||
|
[key: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const importantClients: SocketClientList = {};
|
||||||
|
const connectedClients: SocketClientList = {};
|
||||||
|
const clientReference: ClientReferenceList = {};
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/socket/connection',
|
||||||
|
cors( corsOpts ),
|
||||||
|
( request: express.Request, response: express.Response ) => {
|
||||||
|
if ( request.query.room ) {
|
||||||
|
if ( socketData[ String( request.query.room ) ] ) {
|
||||||
|
response.writeHead( 200, {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
} );
|
||||||
|
response.status( 200 );
|
||||||
|
response.flushHeaders();
|
||||||
|
response.write( `data: ${ JSON.stringify( {
|
||||||
|
'type': 'basics',
|
||||||
|
'data': socketData[ String( request.query.room ) ]
|
||||||
|
} ) }\n\n` );
|
||||||
|
const sid = getSessionID( request );
|
||||||
|
|
||||||
|
if ( getSignedIn( request ) ) {
|
||||||
|
importantClients[ sid ] = {
|
||||||
|
'response': response,
|
||||||
|
'room': String( request.query.room )
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedClients[ sid ] = {
|
||||||
|
'response': response,
|
||||||
|
'room': String( request.query.room )
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( !clientReference[ String( request.query.room ) ] ) {
|
||||||
|
clientReference[ String( request.query.room ) ] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !clientReference[ String( request.query.room ) ]
|
||||||
|
.includes( sid ) ) {
|
||||||
|
clientReference[ String( request.query.room ) ].push( sid );
|
||||||
|
}
|
||||||
|
|
||||||
|
request.on( 'close', () => {
|
||||||
|
try {
|
||||||
|
importantClients[ sid ] = undefined;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch ( e ) { /* empty */ }
|
||||||
|
|
||||||
|
const cl = clientReference[ String( request.query.room ) ];
|
||||||
|
|
||||||
|
for ( const c in cl ) {
|
||||||
|
if ( cl[ c ] === sid ) {
|
||||||
|
cl.splice( parseInt( c ), 1 );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedClients[ sid ] = undefined;
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.status( 404 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/socket/getData',
|
||||||
|
cors( corsOpts ),
|
||||||
|
( request: express.Request, response: express.Response ) => {
|
||||||
|
if ( request.query.room ) {
|
||||||
|
response.send( socketData[ String( request.query.room ) ] );
|
||||||
|
} else {
|
||||||
|
response.status( 400 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'/socket/joinRoom',
|
||||||
|
cors( corsOpts ),
|
||||||
|
( request: express.Request, response: express.Response ) => {
|
||||||
|
if ( request.query.room ) {
|
||||||
|
if ( socketData[ String( request.query.room ) ] ) {
|
||||||
|
response.send( 'ok' );
|
||||||
|
} else {
|
||||||
|
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.status( 404 ).send( 'ERR_NO_ROOM_SPECIFIED' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.options( '/socket/update', cors( corsOpts ) );
|
||||||
|
app.post(
|
||||||
|
'/socket/update',
|
||||||
|
cors( corsOpts ),
|
||||||
|
bodyParser.json(),
|
||||||
|
( request: express.Request, response: express.Response ) => {
|
||||||
|
if ( socketData[ request.body.roomName ] ) {
|
||||||
|
if ( request.body.event === 'tampering' ) {
|
||||||
|
const clients = clientReference[ request.body.roomName ];
|
||||||
|
|
||||||
|
for ( const client in clients ) {
|
||||||
|
if ( importantClients[ clients[ client ] ] ) {
|
||||||
|
importantClients[ clients[ client ] ]
|
||||||
|
.response.write( 'data: ' + JSON.stringify( {
|
||||||
|
'type': 'tampering-msg',
|
||||||
|
'data': true
|
||||||
|
} ) + '\n\n' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.send( 'ok' );
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
socketData[ request.body.roomName ].roomToken
|
||||||
|
=== request.body.roomToken
|
||||||
|
) {
|
||||||
|
let send = false;
|
||||||
|
let update = '';
|
||||||
|
|
||||||
|
if ( request.body.event === 'playback-start-update' ) {
|
||||||
|
send = true;
|
||||||
|
update = 'playback-start';
|
||||||
|
socketData[ request.body.roomName ]
|
||||||
|
.playbackStart = request.body.data;
|
||||||
|
} else if ( request.body.event === 'playback-update' ) {
|
||||||
|
send = true;
|
||||||
|
update = 'playback';
|
||||||
|
socketData[ request.body.roomName ]
|
||||||
|
.playbackStatus = request.body.data;
|
||||||
|
} else if ( request.body.event === 'playlist-update' ) {
|
||||||
|
send = true;
|
||||||
|
update = 'playlist';
|
||||||
|
socketData[ request.body.roomName ]
|
||||||
|
.playlist = request.body.data;
|
||||||
|
} else if ( request.body.event === 'playlist-index-update' ) {
|
||||||
|
send = true;
|
||||||
|
update = 'playlist-index';
|
||||||
|
socketData[ request.body.roomName ]
|
||||||
|
.playlistIndex = request.body.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( send ) {
|
||||||
|
const clients = clientReference[ request.body.roomName ];
|
||||||
|
|
||||||
|
for ( const client in clients ) {
|
||||||
|
if ( connectedClients[ clients[ client ] ] ) {
|
||||||
|
connectedClients[ clients[ client ] ]
|
||||||
|
.response.write( 'data: ' + JSON.stringify( {
|
||||||
|
'type': update,
|
||||||
|
'data': request.body.data
|
||||||
|
} ) + '\n\n' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.send( 'ok' );
|
||||||
|
} else {
|
||||||
|
response.status( 404 ).send( 'ERR_CANNOT_SEND' );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.status( 403 ).send( 'ERR_UNAUTHORIZED' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.status( 400 ).send( 'ERR_WRONG_REQUEST' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.options( '/socket/deleteRoom', cors( corsOpts ) );
|
||||||
|
app.post(
|
||||||
|
'/socket/deleteRoom',
|
||||||
|
cors( corsOpts ),
|
||||||
|
bodyParser.json(),
|
||||||
|
( request: express.Request, response: express.Response ) => {
|
||||||
|
if ( request.body.roomName ) {
|
||||||
|
if ( socketData[ request.body.roomName ] ) {
|
||||||
|
if (
|
||||||
|
socketData[ request.body.roomName ].roomToken
|
||||||
|
=== request.body.roomToken
|
||||||
|
) {
|
||||||
|
socketData[ request.body.roomName ] = undefined;
|
||||||
|
const clients = clientReference[ request.body.roomName ];
|
||||||
|
|
||||||
|
for ( const client in clients ) {
|
||||||
|
if ( connectedClients[ clients[ client ] ] ) {
|
||||||
|
connectedClients[ clients[ client ] ]
|
||||||
|
.response.write( 'data: ' + JSON.stringify( {
|
||||||
|
'type': 'delete-share',
|
||||||
|
'data': true
|
||||||
|
} ) + '\n\n' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.send( 403 ).send( 'ERR_UNAUTHORIZED' );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.status( 404 ).send( 'ERR_ROOM_NOT_FOUND' );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.status( 400 ).send( 'ERR_NO_ROOM_NAME' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
useSSE
|
||||||
|
};
|
||||||
385
backend/src/storage/db.ts
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
/*
|
||||||
|
* libreevent - db.js
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 03/26/2023, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import * as sqlDB from './mysqldb.js';
|
||||||
|
|
||||||
|
declare let __dirname: string | undefined;
|
||||||
|
|
||||||
|
if ( typeof __dirname === 'undefined' ) {
|
||||||
|
__dirname = path.resolve( path.dirname( '' ) );
|
||||||
|
} else {
|
||||||
|
__dirname = __dirname + '/../';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbRef = {
|
||||||
|
'user': 'music_users',
|
||||||
|
'users': 'music_users',
|
||||||
|
};
|
||||||
|
const dbh = new sqlDB.SQLDB();
|
||||||
|
|
||||||
|
dbh.connect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database (create tables, etc)
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
const initDB = (): undefined => {
|
||||||
|
( async () => {
|
||||||
|
console.log( '[ DB ] Setting up...' );
|
||||||
|
dbh.setupDB();
|
||||||
|
console.log( '[ DB ] Setting up complete!' );
|
||||||
|
} )();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve data from the database
|
||||||
|
* @param {string} db The name of the database
|
||||||
|
* @param {string} column The name of the column of the data-table in which to search for the searchQuery
|
||||||
|
* @param {string} searchQuery The query for the selected column
|
||||||
|
* @returns {Promise<object>} Returns a promise that resolves to an object containing the results.
|
||||||
|
*/
|
||||||
|
const getDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
dbh.query( {
|
||||||
|
'command': 'getFilteredData',
|
||||||
|
'property': column,
|
||||||
|
'searchQuery': searchQuery
|
||||||
|
}, dbRef[ db ] ).then( data => {
|
||||||
|
resolve( data );
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
reject( error );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the SQL LeftJoin function to obtain data from DB.
|
||||||
|
* @param {string} db DB name to get data from
|
||||||
|
* @param {string} column The column in the DB in which to search for the searchQuery
|
||||||
|
* @param {string} searchQuery The data to look for in the selected column
|
||||||
|
* @param {string} secondTable The second table on which to perform the left join function
|
||||||
|
* @param {object} columns The columns to return, list of objects: { 'db': TABLE NAME, 'column': COLUMN NAME })
|
||||||
|
* @param {string} nameOfMatchingParam Which properties should be matched to get the data, e.g. order.user_id=users.id
|
||||||
|
* @returns {Promise<Object | Error>} Returns all records from the db and all matching data specified with the matchingParam from the secondTable.
|
||||||
|
*/
|
||||||
|
const getDataWithLeftJoinFunction = ( db: string, column: string, searchQuery: string, secondTable: string, columns: object, nameOfMatchingParam: string ): Promise<object> => {
|
||||||
|
/*
|
||||||
|
LeftJoin (Select values in first table and return all corresponding values of second table):
|
||||||
|
- operation.property (the column to search for the value),
|
||||||
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
- operation.columns (The columns of both tables to be selected, list of objects: { 'db': TABLE NAME, 'column': COLUMN NAME })
|
||||||
|
- operation.secondTable (The second table to perform Join operation with)
|
||||||
|
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
||||||
|
*/
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
const settings = {
|
||||||
|
'command': 'LeftJoin',
|
||||||
|
'property': column,
|
||||||
|
'searchQuery': searchQuery,
|
||||||
|
'selection': '',
|
||||||
|
'secondTable': dbRef[ secondTable ],
|
||||||
|
'matchingParam': dbRef[ db ] + '.' + nameOfMatchingParam + '=' + dbRef[ secondTable ] + '.' + nameOfMatchingParam,
|
||||||
|
};
|
||||||
|
|
||||||
|
for ( const el in columns ) {
|
||||||
|
settings.selection += dbRef[ columns[ el ].db ] + '.' + columns[ el ].column + ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.selection = settings.selection.slice( 0, settings.selection.length - 1 );
|
||||||
|
dbh.query( settings, dbRef[ db ] ).then( data => {
|
||||||
|
resolve( data );
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
reject( error );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all data from the selected database
|
||||||
|
* @param {string} db The database of which all data should be retrieved
|
||||||
|
* @returns {Promise<object>} Returns an object containing all data
|
||||||
|
*/
|
||||||
|
const getData = ( db: string ): Promise<object> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
dbh.query( {
|
||||||
|
'command': 'getAllData'
|
||||||
|
}, dbRef[ db ] ).then( data => {
|
||||||
|
resolve( data );
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
reject( error );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data to the database
|
||||||
|
* @param {string} db The database in which the data should be written
|
||||||
|
* @param {string} column The column in which to search for the data
|
||||||
|
* @param {string} searchQuery The query with which to search
|
||||||
|
* @param {string} data The data to write. Also include the column & searchQuery parameters, if they also need to be added
|
||||||
|
* @returns {Promise<object>} Returns a promise that resolves to the interaction module return.
|
||||||
|
*/
|
||||||
|
const writeDataSimple = ( db: string, column: string, searchQuery: string, data: any ): Promise<object> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
dbh.query( {
|
||||||
|
'command': 'checkDataAvailability',
|
||||||
|
'property': column,
|
||||||
|
'searchQuery': searchQuery
|
||||||
|
}, dbRef[ db ] ).then( res => {
|
||||||
|
if ( res.length > 0 ) {
|
||||||
|
dbh.query( {
|
||||||
|
'command': 'updateData',
|
||||||
|
'property': column,
|
||||||
|
'searchQuery': searchQuery,
|
||||||
|
'newValues': data
|
||||||
|
}, dbRef[ db ] ).then( dat => {
|
||||||
|
resolve( dat );
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
reject( error );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
dbh.query( {
|
||||||
|
'command': 'addData',
|
||||||
|
'data': data
|
||||||
|
}, dbRef[ db ] ).then( dat => {
|
||||||
|
resolve( dat );
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
reject( error );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
reject( error );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete data from the database
|
||||||
|
* @param {string} db The database from which the data should be deleted
|
||||||
|
* @param {string} column The column in which to search for the data
|
||||||
|
* @param {string} searchQuery The query with which to search
|
||||||
|
* @returns {Promise<object>} Returns a promise that resolves to the interaction module return.
|
||||||
|
*/
|
||||||
|
const deleteDataSimple = ( db: string, column: string, searchQuery: string ): Promise<object> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
dbh.query( {
|
||||||
|
'command': 'deleteData',
|
||||||
|
'property': column,
|
||||||
|
'searchQuery': searchQuery
|
||||||
|
}, dbRef[ db ] ).then( dat => {
|
||||||
|
resolve( dat );
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
reject( error );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the data is available in the database.
|
||||||
|
* @param {string} db The database in which to check
|
||||||
|
* @param {string} column The column in which to search for the data
|
||||||
|
* @param {string} searchQuery The query with which to search
|
||||||
|
* @returns {Promise<boolean>} Returns a promise that resolves to a boolean (true = is available)
|
||||||
|
*/
|
||||||
|
const checkDataAvailability = ( db: string, column: string, searchQuery: string ): Promise<boolean> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
dbh.query( {
|
||||||
|
'command': 'checkDataAvailability',
|
||||||
|
'property': column,
|
||||||
|
'searchQuery': searchQuery
|
||||||
|
}, dbRef[ db ] ).then( res => {
|
||||||
|
if ( res.length > 0 ) {
|
||||||
|
resolve( true );
|
||||||
|
} else {
|
||||||
|
resolve( false );
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
reject( error );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load multiple JSON files at once
|
||||||
|
* @param {Array<string>} files The files which to load
|
||||||
|
* @returns {Promise<object>} Returns the data from all files
|
||||||
|
*/
|
||||||
|
const getJSONDataBatch = async ( files: Array<string> ): Promise<object> => {
|
||||||
|
const allFiles = {};
|
||||||
|
|
||||||
|
for ( const file in files ) {
|
||||||
|
try {
|
||||||
|
allFiles[ files[ file ] ] = await getJSONData( files[ file ] );
|
||||||
|
} catch ( err ) {
|
||||||
|
allFiles[ files[ file ] ] = 'ERROR: ' + err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all data from a JSON file
|
||||||
|
* @param {string} file The file to load (just file name, file must be in "/data/" folder, no file extension)
|
||||||
|
* @returns {Promise<object>} The data that was loaded
|
||||||
|
*/
|
||||||
|
const getJSONData = ( file: string ): Promise<object> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
fs.readFile( path.join( __dirname + '/' + file + '.json' ), ( error, data ) => {
|
||||||
|
if ( error ) {
|
||||||
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
|
} else {
|
||||||
|
if ( data.byteLength > 0 ) {
|
||||||
|
resolve( JSON.parse( data.toString() ) ?? {} );
|
||||||
|
} else {
|
||||||
|
resolve( { } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load some data from a JSON file
|
||||||
|
* @param {string} file The file to load (just file name, file must be in "/data/" folder, no file extension)
|
||||||
|
* @param {string} identifier The identifier of the element which should be loaded
|
||||||
|
* @returns {Promise<object>} The data that was loaded
|
||||||
|
*/
|
||||||
|
const getJSONDataSimple = ( file: string, identifier: string ): Promise<object> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
fs.readFile( path.join( __dirname + '/' + file + '.json' ), ( error, data ) => {
|
||||||
|
if ( error ) {
|
||||||
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
|
} else {
|
||||||
|
if ( data.byteLength > 0 ) {
|
||||||
|
resolve( JSON.parse( data.toString() )[ identifier ] ?? {} );
|
||||||
|
} else {
|
||||||
|
resolve( { } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get JSON data, but synchronous (blocking)
|
||||||
|
* @param {string} file The file to be loaded (path relative to root)
|
||||||
|
* @returns {object} Returns the JSON file
|
||||||
|
*/
|
||||||
|
const getJSONDataSync = ( file: string ): object => {
|
||||||
|
return JSON.parse( fs.readFileSync( path.join( __dirname + '/' + file ) ).toString() );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description
|
||||||
|
* @param {any} db:string
|
||||||
|
* @param {any} identifier:string
|
||||||
|
* @param {any} values:any
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
const writeJSONDataSimple = ( db: string, identifier: string, values: any ) => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
fs.readFile( path.join( __dirname + '/../../data/' + db + '.json' ), ( error, data ) => {
|
||||||
|
if ( error ) {
|
||||||
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
|
} else {
|
||||||
|
let dat = {};
|
||||||
|
|
||||||
|
if ( data.byteLength > 0 ) {
|
||||||
|
dat = JSON.parse( data.toString() ) ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
dat[ identifier ] = values;
|
||||||
|
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), error => {
|
||||||
|
if ( error ) {
|
||||||
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve( true );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data to a JSON file
|
||||||
|
* @param {string} db The database to write into
|
||||||
|
* @param {object} data The data to write
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
const writeJSONData = ( db: string, data: object ): Promise<boolean> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( data ), error => {
|
||||||
|
if ( error ) {
|
||||||
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
|
} else {
|
||||||
|
resolve( true );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete data from a JSON file
|
||||||
|
* @param {string} db The file to delete from (just filename, has to be in "/data/" folder, no file extension)
|
||||||
|
* @param {string} identifier The identifier of the element to delete
|
||||||
|
* @returns {Promise<boolean>} Returns a promise that resolves to a boolean
|
||||||
|
*/
|
||||||
|
const deleteJSONDataSimple = ( db: string, identifier: string ): Promise<boolean> => {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
fs.readFile( path.join( __dirname + '/../../data/' + db + '.json' ), ( error, data ) => {
|
||||||
|
if ( error ) {
|
||||||
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
|
} else {
|
||||||
|
let dat = {};
|
||||||
|
|
||||||
|
if ( data.byteLength > 0 ) {
|
||||||
|
dat = JSON.parse( data.toString() ) ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
delete dat[ identifier ];
|
||||||
|
fs.writeFile( path.join( __dirname + '/../../data/' + db + '.json' ), JSON.stringify( dat ), error => {
|
||||||
|
if ( error ) {
|
||||||
|
reject( 'Error occurred: Error trace: ' + error );
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve( true );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initDB,
|
||||||
|
checkDataAvailability,
|
||||||
|
deleteDataSimple,
|
||||||
|
deleteJSONDataSimple,
|
||||||
|
getData,
|
||||||
|
getDataSimple,
|
||||||
|
getDataWithLeftJoinFunction,
|
||||||
|
getJSONData,
|
||||||
|
getJSONDataBatch,
|
||||||
|
getJSONDataSimple,
|
||||||
|
getJSONDataSync,
|
||||||
|
writeDataSimple,
|
||||||
|
writeJSONData,
|
||||||
|
writeJSONDataSimple
|
||||||
|
};
|
||||||
220
backend/src/storage/mysqldb.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
* libreevent - mysqldb.js
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 07/12/2023, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import mysql from 'mysql';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
declare let __dirname: string | undefined;
|
||||||
|
|
||||||
|
if ( typeof __dirname === 'undefined' ) {
|
||||||
|
__dirname = path.resolve( path.dirname( '' ) );
|
||||||
|
} else {
|
||||||
|
__dirname = __dirname + '/../';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the connection does not work for you, you will need to add your ip
|
||||||
|
// to the whitelist of the database
|
||||||
|
|
||||||
|
class SQLConfig {
|
||||||
|
|
||||||
|
command: string;
|
||||||
|
|
||||||
|
property?: string;
|
||||||
|
|
||||||
|
searchQuery?: string;
|
||||||
|
|
||||||
|
selection?: string;
|
||||||
|
|
||||||
|
query?: string;
|
||||||
|
|
||||||
|
newValues?: object;
|
||||||
|
|
||||||
|
secondTable?: string;
|
||||||
|
|
||||||
|
matchingParam?: string;
|
||||||
|
|
||||||
|
data?: object;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SQLDB {
|
||||||
|
|
||||||
|
sqlConnection: mysql.Connection;
|
||||||
|
|
||||||
|
isRecovering: boolean;
|
||||||
|
|
||||||
|
config: object;
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.config = JSON.parse( '' + fs.readFileSync( path.join( __dirname + '/config/db.config.secret.json' ) ) );
|
||||||
|
this.sqlConnection = mysql.createConnection( this.config );
|
||||||
|
this.isRecovering = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect () {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
if ( this.isRecovering ) {
|
||||||
|
console.log( '[ SQL ] Attempting to recover from critical error' );
|
||||||
|
this.sqlConnection = mysql.createConnection( this.config );
|
||||||
|
this.isRecovering = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sqlConnection.connect( err => {
|
||||||
|
if ( err ) {
|
||||||
|
console.error( '[ SQL ]: An error ocurred whilst connecting: ' + err.stack );
|
||||||
|
reject( err );
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log( '[ SQL ] Connected to database successfully' );
|
||||||
|
self.sqlConnection.on( 'error', err => {
|
||||||
|
if ( err.code === 'ECONNRESET' ) {
|
||||||
|
self.isRecovering = true;
|
||||||
|
setTimeout( () => {
|
||||||
|
self.disconnect();
|
||||||
|
self.connect();
|
||||||
|
}, 1000 );
|
||||||
|
} else {
|
||||||
|
console.error( err );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
resolve( 'connection' );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect ( ) {
|
||||||
|
this.sqlConnection.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupDB () {
|
||||||
|
this.sqlConnection.query( 'SELECT @@default_storage_engine;', ( error, results ) => {
|
||||||
|
if ( error ) throw error;
|
||||||
|
|
||||||
|
if ( results[ 0 ][ '@@default_storage_engine' ] !== 'InnoDB' ) throw 'DB HAS TO USE InnoDB!';
|
||||||
|
} );
|
||||||
|
this.sqlConnection.query( 'CREATE TABLE music_users ( account_id INT ( 10 ) NOT NULL AUTO_INCREMENT, email TINYTEXT NOT NULL, uid TINYTEXT, lang TINYTEXT, username TINYTEXT, settings VARCHAR( 5000 ), PRIMARY KEY ( account_id ) ) ENGINE=INNODB;', error => {
|
||||||
|
if ( error ) if ( error.code !== 'ER_TABLE_EXISTS_ERROR' ) throw error;
|
||||||
|
|
||||||
|
return 'DONE';
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
query ( operation: SQLConfig, table: string ): Promise<Array<object>> {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
/*
|
||||||
|
Possible operation.command values (all need the table argument of the method call):
|
||||||
|
- getAllData: no additional instructions needed
|
||||||
|
|
||||||
|
- getFilteredData:
|
||||||
|
- operation.property (the column to search for the value),
|
||||||
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
|
||||||
|
- InnerJoin (Select values that match in both tables):
|
||||||
|
- operation.property (the column to search for the value),
|
||||||
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
- operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id)
|
||||||
|
- operation.secondTable (The second table to perform Join operation with)
|
||||||
|
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
||||||
|
|
||||||
|
- LeftJoin (Select values in first table and return all corresponding values of second table):
|
||||||
|
- operation.property (the column to search for the value),
|
||||||
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
- operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id)
|
||||||
|
- operation.secondTable (The second table to perform Join operation with)
|
||||||
|
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
||||||
|
|
||||||
|
- RightJoin (Select values in second table and return all corresponding values of first table):
|
||||||
|
- operation.property (the column to search for the value),
|
||||||
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
- operation.selection (The columns of both tables to be selected, e.g. users.name, orders.id)
|
||||||
|
- operation.secondTable (The second table to perform Join operation with)
|
||||||
|
- operation.matchingParam (Which properties should be matched to get the data, e.g. order.user_id=users.id)
|
||||||
|
|
||||||
|
- addData:
|
||||||
|
- operation.data (key-value pair with all data as values and column to insert into as key)
|
||||||
|
|
||||||
|
- deleteData:
|
||||||
|
- operation.property (the column to search for the value)
|
||||||
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
|
||||||
|
- updateData:
|
||||||
|
- operation.newValues (a object with keys being the column and value being the value to be inserted into that column, values are being
|
||||||
|
sanitised by the function)
|
||||||
|
- operation.property (the column to search for the value),
|
||||||
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
|
||||||
|
- checkDataAvailability:
|
||||||
|
- operation.property (the column to search for the value),
|
||||||
|
- operation.searchQuery (the value to search for [will be sanitised by method])
|
||||||
|
|
||||||
|
- fullCustomCommand:
|
||||||
|
- operation.query (the SQL instruction to be executed) --> NOTE: This command will not be sanitised, so use only with proper sanitisation!
|
||||||
|
*/
|
||||||
|
let command = '';
|
||||||
|
|
||||||
|
if ( operation.command === 'getAllData' ) {
|
||||||
|
command = 'SELECT * FROM ' + table;
|
||||||
|
} else if ( operation.command === 'getFilteredData' || operation.command === 'checkDataAvailability' ) {
|
||||||
|
command = 'SELECT * FROM ' + table + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
||||||
|
} else if ( operation.command === 'fullCustomCommand' ) {
|
||||||
|
command = operation.query;
|
||||||
|
} else if ( operation.command === 'addData' ) {
|
||||||
|
let keys = '';
|
||||||
|
let values = '';
|
||||||
|
|
||||||
|
for ( const key in operation.data ) {
|
||||||
|
keys += String( key ) + ', ';
|
||||||
|
values += this.sqlConnection.escape( String( operation.data[ key ] ) ) + ', ';
|
||||||
|
}
|
||||||
|
|
||||||
|
command = 'INSERT INTO ' + table + ' (' + keys.slice( 0, keys.length - 2 ) + ') VALUES (' + values.slice( 0, values.length - 2 ) + ');';
|
||||||
|
} else if ( operation.command === 'updateData' ) {
|
||||||
|
if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' );
|
||||||
|
else {
|
||||||
|
command = 'UPDATE ' + table + ' SET ';
|
||||||
|
let updatedValues = '';
|
||||||
|
|
||||||
|
for ( const value in operation.newValues ) {
|
||||||
|
updatedValues += value + ' = ' + this.sqlConnection.escape( String( operation.newValues[ value ] ) ) + ', ';
|
||||||
|
}
|
||||||
|
|
||||||
|
command += updatedValues.slice( 0, updatedValues.length - 2 );
|
||||||
|
command += ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
||||||
|
}
|
||||||
|
} else if ( operation.command === 'deleteData' ) {
|
||||||
|
if ( !operation.property || !operation.searchQuery ) reject( 'Refusing to run destructive command: Missing Constraints' );
|
||||||
|
else {
|
||||||
|
command = 'DELETE FROM ' + table + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
||||||
|
}
|
||||||
|
} else if ( operation.command === 'InnerJoin' ) {
|
||||||
|
command = 'SELECT ' + operation.selection + ' FROM ' + table + ' INNER JOIN ' + operation.secondTable + ' ON ' + operation.matchingParam + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
||||||
|
} else if ( operation.command === 'LeftJoin' ) {
|
||||||
|
command = 'SELECT ' + operation.selection + ' FROM ' + table + ' LEFT JOIN ' + operation.secondTable + ' ON ' + operation.matchingParam + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
||||||
|
} else if ( operation.command === 'RightJoin' ) {
|
||||||
|
command = 'SELECT ' + operation.selection + ' FROM ' + table + ' RIGHT JOIN ' + operation.secondTable + ' ON ' + operation.matchingParam + ' WHERE ' + operation.property + ' = ' + this.sqlConnection.escape( operation.searchQuery );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sqlConnection.query( command, ( error, results ) => {
|
||||||
|
if ( error ) reject( error );
|
||||||
|
|
||||||
|
resolve( results );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
SQLConfig, SQLDB
|
||||||
|
};
|
||||||
12
backend/src/storage/prepareDB.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import db from './db.js';
|
||||||
|
// import hash from '../security/hash.js';
|
||||||
|
|
||||||
|
db.initDB();
|
||||||
|
// setTimeout( () => {
|
||||||
|
// console.log( 'Setting up admin account' );
|
||||||
|
// hash.hashPassword( 'test' ).then( hash => {
|
||||||
|
// db.writeDataSimple( 'admin', 'email', 'info@janishutz.com', { email: 'info@janishutz.com', pass: hash, two_fa: 'enhanced' } );
|
||||||
|
// console.log( 'Complete!' );
|
||||||
|
// } );
|
||||||
|
// }, 5000 );
|
||||||
|
|
||||||
13
backend/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"allowJs": true,
|
||||||
|
"target": "ES6",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext"
|
||||||
|
},
|
||||||
|
"include": [ "./src/**/*" ],
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Authenticate - Fancy Remote Display</title>
|
|
||||||
<style>
|
|
||||||
.aligner {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
background-color: rgb(41, 41, 41);
|
|
||||||
color: white;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#key {
|
|
||||||
padding: 1vh;
|
|
||||||
font-size: 120%;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 5vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#submit {
|
|
||||||
padding: 20px;
|
|
||||||
background-color: rgb(1, 1, 88);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50px;
|
|
||||||
transition: all 1s;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#submit:hover {
|
|
||||||
background-color: rgb(1, 1, 120);
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="aligner">
|
|
||||||
<h1>Authenticate - Fancy Remote Display</h1>
|
|
||||||
<form action="/fancy/auth" method="post">
|
|
||||||
<label for="key" style="font-size: 120%;">Authentication Key</label>
|
|
||||||
<input type="text" name="key" id="key" style="margin-bottom: 1vh;">
|
|
||||||
<input type="submit" value="Authenticate" id="submit">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
.background {
|
|
||||||
position: fixed;
|
|
||||||
left: -50vw;
|
|
||||||
width: 200vw;
|
|
||||||
height: 200vw;
|
|
||||||
top: -50vw;
|
|
||||||
z-index: -1;
|
|
||||||
filter: blur(10px);
|
|
||||||
background: conic-gradient( blue, green, red, blue );
|
|
||||||
animation: gradientAnim 10s infinite linear;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat, .beat-manual {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.15 );
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat {
|
|
||||||
animation: beatAnim 0.6s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes beatAnim {
|
|
||||||
0% {
|
|
||||||
background-color: rgba( 0, 0, 0, 0.2 );
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
background-color: rgba( 0, 0, 0, 0 );
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-color: rgba( 0, 0, 0, 0.2 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradientAnim {
|
|
||||||
from {
|
|
||||||
transform: rotate( 0deg );
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate( 360deg );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
.material-symbols-outlined {
|
|
||||||
font-variation-settings:
|
|
||||||
'FILL' 0,
|
|
||||||
'wght' 400,
|
|
||||||
'GRAD' 0,
|
|
||||||
'opsz' 24
|
|
||||||
}
|
|
||||||
|
|
||||||
body, html {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-symbols {
|
|
||||||
position: absolute;
|
|
||||||
left: 10vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.6 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-symbols-wrapper {
|
|
||||||
width: 4vw;
|
|
||||||
height: 5vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-bar {
|
|
||||||
height: 60%;
|
|
||||||
background-color: white;
|
|
||||||
width: 10%;
|
|
||||||
border-radius: 50px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-1 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-2 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-3 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
animation-delay: 0.6s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes music-playing {
|
|
||||||
0% {
|
|
||||||
transform: scaleY( 1 );
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scaleY( 0.5 );
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scaleY( 1 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 80%;
|
|
||||||
margin: 2px;
|
|
||||||
padding: 1vh;
|
|
||||||
border: 1px white solid;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.4 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-details-wrapper {
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list .song-image {
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pause-icon {
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw !important;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-song-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 55vh;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5%;
|
|
||||||
margin-top: 0.25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-song {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-top: 1vh;
|
|
||||||
padding: 1vh;
|
|
||||||
text-align: center;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.4 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.fancy-view-song-art {
|
|
||||||
height: 30vh;
|
|
||||||
width: 30vh;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 30vh !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
background-color: rgba( 0, 0, 0, 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress, #progress::-webkit-progress-bar {
|
|
||||||
background-color: rgba(45, 28, 145);
|
|
||||||
color: rgba(45, 28, 145);
|
|
||||||
width: 30vw;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0px;
|
|
||||||
accent-color: white;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress::-moz-progress-bar {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress::-webkit-progress-value {
|
|
||||||
background-color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-selector-wrapper {
|
|
||||||
opacity: 0;
|
|
||||||
position: fixed;
|
|
||||||
right: 0.5%;
|
|
||||||
top: 0.5%;
|
|
||||||
padding: 0.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-selector-wrapper:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dancing-style {
|
|
||||||
font-size: 250%;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
position: fixed;
|
|
||||||
font-size: 12px;
|
|
||||||
transform: rotate(270deg);
|
|
||||||
left: -150px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=7">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Showcase - MusicPlayerV2</title>
|
|
||||||
<link rel="stylesheet" href="/fancy/showcase.css">
|
|
||||||
<link rel="stylesheet" href="/fancy/backgroundAnim.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
|
||||||
<div class="content" id="app">
|
|
||||||
<div v-if="hasLoaded" style="width: 100%">
|
|
||||||
<div class="current-song-wrapper">
|
|
||||||
<span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt">music_note</span>
|
|
||||||
<img v-else-if="playingSong.hasCoverArt && playingSong.coverArtOrigin === 'api'" :src="playingSong.coverArtURL" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
|
||||||
<img v-else :src="'/getSongCover?filename=' + playingSong.filename" class="fancy-view-song-art" id="current-image">
|
|
||||||
<div class="current-song">
|
|
||||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
|
||||||
<h1>{{ playingSong.title }}</h1>
|
|
||||||
<p class="dancing-style" v-if="playingSong.dancingStyle">{{ playingSong.dancingStyle }}</p>
|
|
||||||
<p>{{ playingSong.artist }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-selector-wrapper">
|
|
||||||
<select v-model="visualizationSettings" @change="setVisualization()">
|
|
||||||
<option value="mic">Microphone (Mic access required)</option>
|
|
||||||
<option value="bpm">BPM (might not be 100% accurate)</option>
|
|
||||||
<option value="off">No visualization except background</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="song-list-wrapper">
|
|
||||||
<div v-for="song in songQueue" class="song-list">
|
|
||||||
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
|
|
||||||
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="song-image">
|
|
||||||
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin !== 'api'" :src="'/getSongCover?filename=' + song.filename" class="song-image">
|
|
||||||
<div v-if="playingSong.filename === song.filename && isPlaying" class="playing-symbols">
|
|
||||||
<div class="playing-symbols-wrapper">
|
|
||||||
<div class="playing-bar" id="bar-1"></div>
|
|
||||||
<div class="playing-bar" id="bar-2"></div>
|
|
||||||
<div class="playing-bar" id="bar-3"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="material-symbols-outlined pause-icon" v-if="!isPlaying && playingSong.filename === song.filename">pause</span>
|
|
||||||
<div class="song-details-wrapper">
|
|
||||||
<h3>{{ song.title }}</h3>
|
|
||||||
<p>{{ song.artist }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="time-until">
|
|
||||||
{{ getTimeUntil( song ) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <img :src="" alt=""> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<h1>Loading...</h1>
|
|
||||||
</div>
|
|
||||||
<div class="background" id="background">
|
|
||||||
<div class="beat"></div>
|
|
||||||
<div class="beat-manual"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
|
|
||||||
<script src="/fancy/showcase.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
// eslint-disable-next-line no-undef
|
|
||||||
const { createApp } = Vue;
|
|
||||||
|
|
||||||
createApp( {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hasLoaded: false,
|
|
||||||
songs: [],
|
|
||||||
playingSong: {},
|
|
||||||
isPlaying: false,
|
|
||||||
pos: 0,
|
|
||||||
queuePos: 0,
|
|
||||||
colourPalette: [],
|
|
||||||
progressBar: 0,
|
|
||||||
timeTracker: null,
|
|
||||||
visualizationSettings: 'mic',
|
|
||||||
micAnalyzer: null,
|
|
||||||
beatDetected: false,
|
|
||||||
colorThief: null,
|
|
||||||
lastDispatch: new Date().getTime() - 5000,
|
|
||||||
isReconnecting: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
songQueue() {
|
|
||||||
let ret = [];
|
|
||||||
let pos = 0;
|
|
||||||
for ( let song in this.songs ) {
|
|
||||||
if ( pos >= this.queuePos ) {
|
|
||||||
ret.push( this.songs[ song ] );
|
|
||||||
}
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
getTimeUntil() {
|
|
||||||
return ( song ) => {
|
|
||||||
let timeRemaining = 0;
|
|
||||||
for ( let i = this.queuePos; i < Object.keys( this.songs ).length - 1; i++ ) {
|
|
||||||
if ( this.songs[ i ] == song ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
timeRemaining += parseInt( this.songs[ i ].duration );
|
|
||||||
}
|
|
||||||
if ( this.isPlaying ) {
|
|
||||||
if ( timeRemaining === 0 ) {
|
|
||||||
return 'Currently playing';
|
|
||||||
} else {
|
|
||||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( timeRemaining === 0 ) {
|
|
||||||
return 'Plays next';
|
|
||||||
} else {
|
|
||||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
startTimeTracker () {
|
|
||||||
this.timeTracker = setInterval( () => {
|
|
||||||
this.pos = ( new Date().getTime() - this.playingSong.startTime ) / 1000 + this.oldPos;
|
|
||||||
this.progressBar = ( this.pos / this.playingSong.duration ) * 1000;
|
|
||||||
if ( isNaN( this.progressBar ) ) {
|
|
||||||
this.progressBar = 0;
|
|
||||||
}
|
|
||||||
}, 100 );
|
|
||||||
},
|
|
||||||
stopTimeTracker () {
|
|
||||||
clearInterval( this.timeTracker );
|
|
||||||
this.oldPos = this.pos;
|
|
||||||
},
|
|
||||||
getImageData() {
|
|
||||||
return new Promise( ( resolve, reject ) => {
|
|
||||||
if ( this.playingSong.hasCoverArt ) {
|
|
||||||
setTimeout( () => {
|
|
||||||
const img = document.getElementById( 'current-image' );
|
|
||||||
if ( img.complete ) {
|
|
||||||
resolve( this.colorThief.getPalette( img ) );
|
|
||||||
} else {
|
|
||||||
img.addEventListener( 'load', () => {
|
|
||||||
resolve( this.colorThief.getPalette( img ) );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
}, 500 );
|
|
||||||
} else {
|
|
||||||
reject( 'no image' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
connect() {
|
|
||||||
this.colorThief = new ColorThief();
|
|
||||||
let source = new EventSource( '/clientDisplayNotifier', { withCredentials: true } );
|
|
||||||
source.onmessage = ( e ) => {
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = JSON.parse( e.data );
|
|
||||||
} catch ( err ) {
|
|
||||||
data = { 'type': e.data };
|
|
||||||
}
|
|
||||||
if ( data.type === 'basics' ) {
|
|
||||||
this.isPlaying = data.data.isPlaying ?? false;
|
|
||||||
this.playingSong = data.data.playingSong ?? {};
|
|
||||||
this.songs = data.data.songQueue ?? [];
|
|
||||||
this.pos = data.data.pos ?? 0;
|
|
||||||
this.oldPos = data.data.pos ?? 0;
|
|
||||||
this.progressBar = this.pos / this.playingSong.duration * 1000;
|
|
||||||
this.queuePos = data.data.queuePos ?? 0;
|
|
||||||
this.getImageData().then( palette => {
|
|
||||||
this.colourPalette = palette;
|
|
||||||
this.handleBackground();
|
|
||||||
} ).catch( () => {
|
|
||||||
this.colourPalette = [ { 'r': 255, 'g': 0, 'b': 0 }, { 'r': 0, 'g': 255, 'b': 0 }, { 'r': 0, 'g': 0, 'b': 255 } ];
|
|
||||||
this.handleBackground();
|
|
||||||
} );
|
|
||||||
} else if ( data.type === 'pos' ) {
|
|
||||||
this.pos = data.data;
|
|
||||||
this.oldPos = data.data;
|
|
||||||
this.progressBar = data.data / this.playingSong.duration * 1000;
|
|
||||||
} else if ( data.type === 'isPlaying' ) {
|
|
||||||
this.isPlaying = data.data;
|
|
||||||
this.handleBackground();
|
|
||||||
} else if ( data.type === 'songQueue' ) {
|
|
||||||
this.songs = data.data;
|
|
||||||
} else if ( data.type === 'playingSong' ) {
|
|
||||||
this.playingSong = data.data;
|
|
||||||
this.getImageData().then( palette => {
|
|
||||||
this.colourPalette = palette;
|
|
||||||
this.handleBackground();
|
|
||||||
} ).catch( () => {
|
|
||||||
this.colourPalette = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
|
|
||||||
this.handleBackground();
|
|
||||||
} );
|
|
||||||
} else if ( data.type === 'queuePos' ) {
|
|
||||||
this.queuePos = data.data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
source.onopen = () => {
|
|
||||||
this.isReconnecting = false;
|
|
||||||
this.hasLoaded = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
let self = this;
|
|
||||||
|
|
||||||
source.addEventListener( 'error', function( e ) {
|
|
||||||
if ( e.eventPhase == EventSource.CLOSED ) source.close();
|
|
||||||
|
|
||||||
if ( e.target.readyState == EventSource.CLOSED ) {
|
|
||||||
console.log( 'disconnected' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Notify about disconnect
|
|
||||||
setTimeout( () => {
|
|
||||||
if ( !self.isReconnecting ) {
|
|
||||||
self.isReconnecting = true;
|
|
||||||
self.tryReconnect();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
}, false );
|
|
||||||
},
|
|
||||||
tryReconnect() {
|
|
||||||
const int = setInterval( () => {
|
|
||||||
if ( !this.isReconnecting ) {
|
|
||||||
clearInterval( int );
|
|
||||||
} else {
|
|
||||||
connectToSSESource();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
},
|
|
||||||
handleBackground() {
|
|
||||||
let colourDetails = [];
|
|
||||||
let colours = [];
|
|
||||||
let differentEnough = true;
|
|
||||||
if ( this.colourPalette[ 0 ] ) {
|
|
||||||
for ( let i in this.colourPalette ) {
|
|
||||||
for ( let colour in colourDetails ) {
|
|
||||||
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - this.colourPalette[ i ][ 0 ] ) / 255
|
|
||||||
+ Math.abs( colourDetails[ colour ][ 1 ] - this.colourPalette[ i ][ 1 ] ) / 255
|
|
||||||
+ Math.abs( colourDetails[ colour ][ 2 ] - this.colourPalette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
|
||||||
if ( colourDiff > 15 ) {
|
|
||||||
differentEnough = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( differentEnough ) {
|
|
||||||
colourDetails.push( this.colourPalette[ i ] );
|
|
||||||
colours.push( 'rgb(' + this.colourPalette[ i ][ 0 ] + ',' + this.colourPalette[ i ][ 1 ] + ',' + this.colourPalette[ i ][ 2 ] + ')' );
|
|
||||||
}
|
|
||||||
differentEnough = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let outColours = 'conic-gradient(';
|
|
||||||
if ( colours.length < 3 ) {
|
|
||||||
for ( let i = 0; i < 3; i++ ) {
|
|
||||||
if ( colours[ i ] ) {
|
|
||||||
outColours += colours[ i ] + ',';
|
|
||||||
} else {
|
|
||||||
if ( i === 0 ) {
|
|
||||||
outColours += 'blue,';
|
|
||||||
} else if ( i === 1 ) {
|
|
||||||
outColours += 'green,';
|
|
||||||
} else if ( i === 2 ) {
|
|
||||||
outColours += 'red,';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ( colours.length < 11 ) {
|
|
||||||
for ( let i in colours ) {
|
|
||||||
outColours += colours[ i ] + ',';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for ( let i = 0; i < 10; i++ ) {
|
|
||||||
outColours += colours[ i ] + ',';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outColours += colours[ 0 ] ?? 'blue' + ')';
|
|
||||||
|
|
||||||
$( '#background' ).css( 'background', outColours );
|
|
||||||
this.setVisualization();
|
|
||||||
},
|
|
||||||
setVisualization () {
|
|
||||||
if ( Object.keys( this.playingSong ).length > 0 ) {
|
|
||||||
if ( this.visualizationSettings === 'bpm' ) {
|
|
||||||
if ( this.playingSong.bpm && this.isPlaying ) {
|
|
||||||
$( '.beat' ).show();
|
|
||||||
$( '.beat' ).css( 'animation-duration', 60 / this.playingSong.bpm );
|
|
||||||
$( '.beat' ).css( 'animation-delay', this.pos % ( 60 / this.playingSong.bpm * this.pos ) + this.playingSong.bpmOffset - ( 60 / this.playingSong.bpm * this.pos / 2 ) );
|
|
||||||
} else {
|
|
||||||
$( '.beat' ).hide();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
clearInterval( this.micAnalyzer );
|
|
||||||
} catch ( err ) {}
|
|
||||||
} else if ( this.visualizationSettings === 'off' ) {
|
|
||||||
$( '.beat' ).hide();
|
|
||||||
try {
|
|
||||||
clearInterval( this.micAnalyzer );
|
|
||||||
} catch ( err ) {}
|
|
||||||
} else if ( this.visualizationSettings === 'mic' ) {
|
|
||||||
$( '.beat-manual' ).hide();
|
|
||||||
try {
|
|
||||||
clearInterval( this.micAnalyzer );
|
|
||||||
} catch ( err ) {}
|
|
||||||
this.micAudioHandler();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log( 'not playing yet' );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
micAudioHandler () {
|
|
||||||
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
|
||||||
const analyser = audioContext.createAnalyser();
|
|
||||||
analyser.fftSize = 256;
|
|
||||||
const bufferLength = analyser.frequencyBinCount;
|
|
||||||
const dataArray = new Uint8Array( bufferLength );
|
|
||||||
|
|
||||||
navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => {
|
|
||||||
const mic = audioContext.createMediaStreamSource( stream );
|
|
||||||
mic.connect( analyser );
|
|
||||||
analyser.getByteFrequencyData( dataArray );
|
|
||||||
let prevSpectrum = null;
|
|
||||||
let threshold = 10; // Adjust as needed
|
|
||||||
this.beatDetected = false;
|
|
||||||
this.micAnalyzer = setInterval( () => {
|
|
||||||
analyser.getByteFrequencyData( dataArray );
|
|
||||||
// Convert the frequency data to a numeric array
|
|
||||||
const currentSpectrum = Array.from( dataArray );
|
|
||||||
|
|
||||||
if ( prevSpectrum ) {
|
|
||||||
// Calculate the spectral flux
|
|
||||||
const flux = this.calculateSpectralFlux( prevSpectrum, currentSpectrum );
|
|
||||||
|
|
||||||
if ( flux > threshold && !this.beatDetected ) {
|
|
||||||
// Beat detected
|
|
||||||
this.beatDetected = true;
|
|
||||||
this.animateBeat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prevSpectrum = currentSpectrum;
|
|
||||||
}, 20 );
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
animateBeat () {
|
|
||||||
$( '.beat-manual' ).stop();
|
|
||||||
const duration = Math.ceil( 60 / ( this.playingSong.bpm ?? 180 ) * 500 ) - 50;
|
|
||||||
$( '.beat-manual' ).fadeIn( 50 );
|
|
||||||
setTimeout( () => {
|
|
||||||
$( '.beat-manual' ).fadeOut( duration );
|
|
||||||
setTimeout( () => {
|
|
||||||
$( '.beat-manual' ).stop();
|
|
||||||
this.beatDetected = false;
|
|
||||||
}, duration );
|
|
||||||
}, 50 );
|
|
||||||
},
|
|
||||||
calculateSpectralFlux( prevSpectrum, currentSpectrum ) {
|
|
||||||
let flux = 0;
|
|
||||||
|
|
||||||
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
|
||||||
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
|
||||||
flux += Math.max( 0, diff );
|
|
||||||
}
|
|
||||||
|
|
||||||
return flux;
|
|
||||||
},
|
|
||||||
notifier() {
|
|
||||||
if ( parseInt( this.lastDispatch ) + 5000 < new Date().getTime() ) {
|
|
||||||
|
|
||||||
}
|
|
||||||
Notification.requestPermission();
|
|
||||||
|
|
||||||
console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
|
|
||||||
// Detect if window is currently in focus
|
|
||||||
window.onblur = () => {
|
|
||||||
this.sendNotification( 'blur' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect if browser window becomes hidden (also with blur event)
|
|
||||||
document.onvisibilitychange = () => {
|
|
||||||
if ( document.visibilityState === 'hidden' ) {
|
|
||||||
this.sendNotification( 'visibility' );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
sendNotification( notification ) {
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( { 'type': notification } ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( '/clientStatusUpdate', fetchOptions ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
|
|
||||||
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
|
|
||||||
body: 'Please return to the original webpage immediately!',
|
|
||||||
requireInteraction: true,
|
|
||||||
} )
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.connect();
|
|
||||||
this.notifier();
|
|
||||||
// if ( this.visualizationSettings === 'mic' ) {
|
|
||||||
// this.micAudioHandler();
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
isPlaying( value ) {
|
|
||||||
if ( value ) {
|
|
||||||
this.startTimeTracker();
|
|
||||||
} else {
|
|
||||||
this.stopTimeTracker();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ).mount( '#app' );
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=7">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Showcase - MusicPlayerV2</title>
|
|
||||||
<link rel="stylesheet" href="/showcase.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
|
||||||
<div class="content loading" id="loading">
|
|
||||||
<h1>Loading...</h1>
|
|
||||||
<p>Please wait</p>
|
|
||||||
</div>
|
|
||||||
<div class="content" id="app">
|
|
||||||
<div v-if="hasLoaded" style="width: 100%">
|
|
||||||
<div class="current-song-wrapper">
|
|
||||||
<span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt || playingSong.coverArtOrigin !== 'api'">music_note</span>
|
|
||||||
<img v-else-if="playingSong.hasCoverArt && playingSong.coverArtOrigin === 'api'" :src="playingSong.coverArtURL" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
|
||||||
<div class="current-song">
|
|
||||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
|
||||||
<h1>{{ playingSong.title }}</h1>
|
|
||||||
<p class="dancing-style" v-if="playingSong.dancingStyle">{{ playingSong.dancingStyle }}</p>
|
|
||||||
<p>{{ playingSong.artist }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="song-list-wrapper">
|
|
||||||
<div v-for="song in songQueue" class="song-list">
|
|
||||||
<div class="song-details-wrapper">
|
|
||||||
<h3>{{ song.title }}</h3>
|
|
||||||
<p>{{ song.artist }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="time-until">
|
|
||||||
{{ getTimeUntil( song ) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <img :src="" alt=""> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<h1>Loading...</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
|
|
||||||
<script src="/showcase.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
.material-symbols-outlined {
|
|
||||||
font-variation-settings:
|
|
||||||
'FILL' 0,
|
|
||||||
'wght' 400,
|
|
||||||
'GRAD' 0,
|
|
||||||
'opsz' 24
|
|
||||||
}
|
|
||||||
|
|
||||||
body, html {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
color: white;
|
|
||||||
background-color: rgb(29, 29, 29);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
display: none;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loaded {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-symbols {
|
|
||||||
position: absolute;
|
|
||||||
left: 10vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.6 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-symbols-wrapper {
|
|
||||||
width: 4vw;
|
|
||||||
height: 5vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
width: 80%;
|
|
||||||
margin: 2px;
|
|
||||||
padding: 1vh;
|
|
||||||
border: 1px white solid;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.4 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-details-wrapper {
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 65%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list .song-image {
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pause-icon {
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw !important;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-song-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 2%;
|
|
||||||
margin-top: 1%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-song {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-top: 1vh;
|
|
||||||
padding: 1vh;
|
|
||||||
max-width: 80%;
|
|
||||||
text-align: center;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.4 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.fancy-view-song-art {
|
|
||||||
height: 30vh;
|
|
||||||
width: 30vh;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 30vh !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
background-color: rgba( 0, 0, 0, 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress, #progress::-webkit-progress-bar {
|
|
||||||
background-color: rgba(45, 28, 145);
|
|
||||||
color: rgba(45, 28, 145);
|
|
||||||
width: 30vw;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0px;
|
|
||||||
accent-color: white;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress::-moz-progress-bar {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress::-webkit-progress-value {
|
|
||||||
background-color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-selector-wrapper {
|
|
||||||
opacity: 0;
|
|
||||||
position: fixed;
|
|
||||||
right: 0.5%;
|
|
||||||
top: 0.5%;
|
|
||||||
padding: 0.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-selector-wrapper:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dancing-style {
|
|
||||||
font-size: 250%;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
position: fixed;
|
|
||||||
font-size: 12px;
|
|
||||||
transform: rotate(270deg);
|
|
||||||
left: -150px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-until {
|
|
||||||
width: 30%;
|
|
||||||
text-align: end;
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
// eslint-disable-next-line no-undef
|
|
||||||
const { createApp } = Vue;
|
|
||||||
|
|
||||||
createApp( {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hasLoaded: false,
|
|
||||||
songs: [],
|
|
||||||
playingSong: {},
|
|
||||||
isPlaying: false,
|
|
||||||
pos: 0,
|
|
||||||
queuePos: 0,
|
|
||||||
colourPalette: [],
|
|
||||||
progressBar: 0,
|
|
||||||
timeTracker: null,
|
|
||||||
isReconnecting: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
songQueue() {
|
|
||||||
let ret = [];
|
|
||||||
let pos = 0;
|
|
||||||
for ( let song in this.songs ) {
|
|
||||||
if ( pos >= this.queuePos ) {
|
|
||||||
ret.push( this.songs[ song ] );
|
|
||||||
}
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
getTimeUntil() {
|
|
||||||
return ( song ) => {
|
|
||||||
let timeRemaining = 0;
|
|
||||||
for ( let i = this.queuePos; i < Object.keys( this.songs ).length; i++ ) {
|
|
||||||
if ( this.songs[ i ] == song ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
timeRemaining += parseInt( this.songs[ i ].duration );
|
|
||||||
}
|
|
||||||
if ( this.isPlaying ) {
|
|
||||||
if ( timeRemaining === 0 ) {
|
|
||||||
return 'Currently playing';
|
|
||||||
} else {
|
|
||||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( timeRemaining === 0 ) {
|
|
||||||
return 'Plays next';
|
|
||||||
} else {
|
|
||||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
startTimeTracker () {
|
|
||||||
try {
|
|
||||||
clearInterval( this.timeTracker );
|
|
||||||
} catch ( err ) {}
|
|
||||||
this.timeTracker = setInterval( () => {
|
|
||||||
this.pos = ( new Date().getTime() - this.playingSong.startTime ) / 1000 + this.oldPos;
|
|
||||||
this.progressBar = ( this.pos / this.playingSong.duration ) * 1000;
|
|
||||||
if ( isNaN( this.progressBar ) ) {
|
|
||||||
this.progressBar = 0;
|
|
||||||
}
|
|
||||||
}, 100 );
|
|
||||||
},
|
|
||||||
stopTimeTracker () {
|
|
||||||
clearInterval( this.timeTracker );
|
|
||||||
this.oldPos = this.pos;
|
|
||||||
},
|
|
||||||
connect() {
|
|
||||||
let source = new EventSource( '/clientDisplayNotifier', { withCredentials: true } );
|
|
||||||
source.onmessage = ( e ) => {
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = JSON.parse( e.data );
|
|
||||||
} catch ( err ) {
|
|
||||||
data = { 'type': e.data };
|
|
||||||
}
|
|
||||||
if ( data.type === 'basics' ) {
|
|
||||||
this.isPlaying = data.data.isPlaying ?? false;
|
|
||||||
this.playingSong = data.data.playingSong ?? {};
|
|
||||||
this.songs = data.data.songQueue ?? [];
|
|
||||||
this.pos = data.data.pos ?? 0;
|
|
||||||
this.oldPos = data.data.pos ?? 0;
|
|
||||||
this.progressBar = this.pos / this.playingSong.duration * 1000;
|
|
||||||
this.queuePos = data.data.queuePos ?? 0;
|
|
||||||
} else if ( data.type === 'pos' ) {
|
|
||||||
this.pos = data.data;
|
|
||||||
this.oldPos = data.data;
|
|
||||||
this.progressBar = data.data / this.playingSong.duration * 1000;
|
|
||||||
} else if ( data.type === 'isPlaying' ) {
|
|
||||||
this.isPlaying = data.data;
|
|
||||||
} else if ( data.type === 'songQueue' ) {
|
|
||||||
this.songs = data.data;
|
|
||||||
} else if ( data.type === 'playingSong' ) {
|
|
||||||
this.playingSong = data.data;
|
|
||||||
} else if ( data.type === 'queuePos' ) {
|
|
||||||
this.queuePos = data.data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
source.onopen = () => {
|
|
||||||
this.isReconnecting = false;
|
|
||||||
this.hasLoaded = true;
|
|
||||||
if ( document.fonts.status === 'loaded' ) {
|
|
||||||
document.getElementById( 'loading' ).classList.remove( 'loading' );
|
|
||||||
document.getElementById( 'app' ).classList.add( 'loaded' );
|
|
||||||
} else {
|
|
||||||
document.fonts.onloadingdone = () => {
|
|
||||||
document.getElementById( 'loading' ).classList.remove( 'loading' );
|
|
||||||
document.getElementById( 'app' ).classList.add( 'loaded' );
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
let self = this;
|
|
||||||
|
|
||||||
source.addEventListener( 'error', function( e ) {
|
|
||||||
if ( e.eventPhase == EventSource.CLOSED ) source.close();
|
|
||||||
|
|
||||||
if ( e.target.readyState == EventSource.CLOSED ) {
|
|
||||||
console.log( 'disconnected' );
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout( () => {
|
|
||||||
if ( !self.isReconnecting ) {
|
|
||||||
self.isReconnecting = true;
|
|
||||||
self.tryReconnect();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
}, false );
|
|
||||||
},
|
|
||||||
tryReconnect() {
|
|
||||||
const int = setInterval( () => {
|
|
||||||
if ( !this.isReconnecting ) {
|
|
||||||
clearInterval( int );
|
|
||||||
} else {
|
|
||||||
connectToSSESource();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.connect();
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
isPlaying( value ) {
|
|
||||||
if ( value ) {
|
|
||||||
this.startTimeTracker();
|
|
||||||
} else {
|
|
||||||
this.stopTimeTracker();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ).mount( '#app' );
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not dead
|
|
||||||
not ie 11
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# musicplayerv2
|
|
||||||
|
|
||||||
## Project setup
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
|
||||||
```
|
|
||||||
npm run serve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and minifies for production
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Customize configuration
|
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"module": "esnext",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26497
frontend/package-lock.json
generated
@@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "musicplayerv2",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"maintainers": [
|
|
||||||
"Janis Hutz <development@janishutz.com>"
|
|
||||||
],
|
|
||||||
"description": "A music player",
|
|
||||||
"homepage": "https://janishutz.com/projects/musicplayerv2",
|
|
||||||
"author": {
|
|
||||||
"name": "Janis Hutz",
|
|
||||||
"email": "development@janishutz.com",
|
|
||||||
"url": "https://janishutz.com"
|
|
||||||
},
|
|
||||||
"license": "GPL-3.0-or-later",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/simplePCBuilding/musicplayerv2/issues"
|
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"serve": "vue-cli-service serve",
|
|
||||||
"build": "vue-cli-service build",
|
|
||||||
"electron:build": "vue-cli-service electron:build",
|
|
||||||
"electron:serve": "vue-cli-service electron:serve",
|
|
||||||
"postinstall": "electron-builder install-app-deps",
|
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
|
||||||
},
|
|
||||||
"main": "background.js",
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.6.1",
|
|
||||||
"core-js": "^3.8.3",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"csv-parser": "^3.0.0",
|
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
|
||||||
"eventsource": "^2.0.2",
|
|
||||||
"express-session": "^1.17.3",
|
|
||||||
"ip": "^1.1.8",
|
|
||||||
"jquery": "^3.7.1",
|
|
||||||
"json-beautify": "^1.1.1",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"music-metadata": "^7.13.0",
|
|
||||||
"node-fetch": "^2.7.0",
|
|
||||||
"node-musickit-api": "^2.1.1",
|
|
||||||
"realtime-bpm-analyzer": "^3.2.1",
|
|
||||||
"vue": "^3.2.13",
|
|
||||||
"vue-router": "^4.0.3",
|
|
||||||
"web-audio-beat-detector": "^8.1.55"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vue/cli-plugin-babel": "~5.0.0",
|
|
||||||
"@vue/cli-plugin-router": "~5.0.0",
|
|
||||||
"@vue/cli-service": "~5.0.0",
|
|
||||||
"electron": "^13.0.0",
|
|
||||||
"electron-devtools-installer": "^3.1.0",
|
|
||||||
"vue-cli-plugin-electron-builder": "~2.1.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,24 +0,0 @@
|
|||||||
/* fallback */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Material Symbols Outlined';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100 700;
|
|
||||||
src: url(/iconFont.woff2) format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-symbols-outlined {
|
|
||||||
font-family: 'Material Symbols Outlined';
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: normal;
|
|
||||||
text-transform: none;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
word-wrap: normal;
|
|
||||||
direction: ltr;
|
|
||||||
-moz-font-feature-settings: 'liga';
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<link rel="stylesheet" href="/icon-font.css" />
|
|
||||||
<script src="/jquery.min.js"></script>
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
2
frontend/public/jquery.min.js
vendored
@@ -1,177 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-view v-slot="{ Component, route }">
|
|
||||||
<transition :name="route.meta.transition || 'fade'" mode="out-in">
|
|
||||||
<component :is="Component" />
|
|
||||||
</transition>
|
|
||||||
</router-view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root, :root.light {
|
|
||||||
--primary-color: #2c3e50;
|
|
||||||
--accent-background: rgb(30, 30, 82);
|
|
||||||
--secondary-color: white;
|
|
||||||
--background-color: white;
|
|
||||||
--popup-color: rgb(224, 224, 224);
|
|
||||||
--accent-color: #42b983;
|
|
||||||
--hover-color: rgb(165, 165, 165);
|
|
||||||
--accent-background-hover: rgb(124, 140, 236);
|
|
||||||
--overlay-color: rgba(0, 0, 0, 0.7);
|
|
||||||
--border-color: rgb(100, 100, 100);
|
|
||||||
--highlight-backdrop: rgb(143, 134, 192);
|
|
||||||
--hint-color: rgb(174, 210, 221);
|
|
||||||
--PI: 3.14159265358979;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark {
|
|
||||||
--primary-color: white;
|
|
||||||
--accent-background: rgb(56, 56, 112);
|
|
||||||
--secondary-color: white;
|
|
||||||
--background-color: rgb(32, 32, 32);
|
|
||||||
--popup-color: rgb(58, 58, 58);
|
|
||||||
--accent-color: #42b983;
|
|
||||||
--hover-color: rgb(83, 83, 83);
|
|
||||||
--accent-background-hover: #4380a8;
|
|
||||||
--overlay-color: rgba(104, 104, 104, 0.575);
|
|
||||||
--border-color: rgb(190, 190, 190);
|
|
||||||
--highlight-backdrop: rgb(85, 63, 207);
|
|
||||||
--hint-color: rgb(88, 91, 110);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media ( prefers-color-scheme: dark ) {
|
|
||||||
:root {
|
|
||||||
--primary-color: white;
|
|
||||||
--accent-background: rgb(56, 56, 112);
|
|
||||||
--secondary-color: white;
|
|
||||||
--background-color: rgb(32, 32, 32);
|
|
||||||
--popup-color: rgb(58, 58, 58);
|
|
||||||
--accent-color: #42b983;
|
|
||||||
--hover-color: rgb(83, 83, 83);
|
|
||||||
--accent-background-hover: #4380a8;
|
|
||||||
--overlay-color: rgba(104, 104, 104, 0.575);
|
|
||||||
--border-color: rgb(190, 190, 190);
|
|
||||||
--highlight-backdrop: rgb(85, 63, 207);
|
|
||||||
--hint-color: rgb(88, 91, 110);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background-color: var( --highlight-backdrop );
|
|
||||||
color: var( --secondary-color );
|
|
||||||
}
|
|
||||||
|
|
||||||
#themeSelector {
|
|
||||||
background-color: rgba( 0, 0, 0, 0 );
|
|
||||||
color: var( --primary-color );
|
|
||||||
font-size: 130%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: var( --background-color );
|
|
||||||
color: var( --primary-color );
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
transition: 0.5s;
|
|
||||||
background-color: var( --background-color );
|
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-align: center;
|
|
||||||
color: var( --primary-color );
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a {
|
|
||||||
font-weight: bold;
|
|
||||||
color: var( --primary-color );
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a.router-link-exact-active {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'app',
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
theme: '',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
changeTheme () {
|
|
||||||
if ( this.theme === '☼' ) {
|
|
||||||
document.documentElement.classList.remove( 'dark' );
|
|
||||||
document.documentElement.classList.add( 'light' );
|
|
||||||
localStorage.setItem( 'theme', '☽' );
|
|
||||||
this.theme = '☽';
|
|
||||||
} else if ( this.theme === '☽' ) {
|
|
||||||
document.documentElement.classList.remove( 'light' );
|
|
||||||
document.documentElement.classList.add( 'dark' );
|
|
||||||
localStorage.setItem( 'theme', '☼' );
|
|
||||||
this.theme = '☼';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.theme = localStorage.getItem( 'theme' ) ?? '';
|
|
||||||
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || this.theme === '☼' ) {
|
|
||||||
document.documentElement.classList.add( 'dark' );
|
|
||||||
this.theme = '☼';
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.add( 'light' );
|
|
||||||
this.theme = '☽';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
const express = require( 'express' );
|
|
||||||
let app = express();
|
|
||||||
const path = require( 'path' );
|
|
||||||
const cors = require( 'cors' );
|
|
||||||
const fs = require( 'fs' );
|
|
||||||
const bodyParser = require( 'body-parser' );
|
|
||||||
const dialog = require( 'electron' ).dialog;
|
|
||||||
const session = require( 'express-session' );
|
|
||||||
const indexer = require( './indexer.js' );
|
|
||||||
const axios = require( 'axios' );
|
|
||||||
const ip = require( 'ip' );
|
|
||||||
const jwt = require( 'jsonwebtoken' );
|
|
||||||
const shell = require( 'electron' ).shell;
|
|
||||||
const beautify = require( 'json-beautify' );
|
|
||||||
const EventSource = require( 'eventsource' );
|
|
||||||
|
|
||||||
|
|
||||||
app.use( bodyParser.urlencoded( { extended: false } ) );
|
|
||||||
app.use( bodyParser.json() );
|
|
||||||
app.use( cors() );
|
|
||||||
app.use( session( {
|
|
||||||
secret: 'aeogetwöfaöow0ofö034eö8ptqw39eöavfui786uqew9t0ez9eauigwöfqewoöaiq938w0c8p9awöäf9¨äüöe',
|
|
||||||
saveUninitialized: true,
|
|
||||||
resave: false,
|
|
||||||
} ) );
|
|
||||||
|
|
||||||
const conf = JSON.parse( fs.readFileSync( path.join( __dirname + '/config/config.json' ) ) );
|
|
||||||
|
|
||||||
// TODO: Import from config
|
|
||||||
const remoteURL = conf.connectionURL ?? 'http://localhost:3000';
|
|
||||||
let hasConnected = false;
|
|
||||||
|
|
||||||
const connect = () => {
|
|
||||||
if ( authKey !== '' && conf.doConnect ) {
|
|
||||||
axios.post( remoteURL + '/connect', { 'authKey': authKey } ).then( res => {
|
|
||||||
if ( res.status === 200 ) {
|
|
||||||
console.log( '[ BACKEND INTEGRATION ] Connection successful' );
|
|
||||||
hasConnected = true;
|
|
||||||
} else {
|
|
||||||
console.error( '[ BACKEND INTEGRATION ] Connection error occurred' );
|
|
||||||
}
|
|
||||||
} ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
connectToSSESource();
|
|
||||||
return 'connecting';
|
|
||||||
} else {
|
|
||||||
return 'noAuthKey';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let isSSEAuth = false;
|
|
||||||
let sessionToken = '';
|
|
||||||
let errorCount = 0;
|
|
||||||
let isReconnecting = false;
|
|
||||||
|
|
||||||
const connectToSSESource = () => {
|
|
||||||
if ( isSSEAuth ) {
|
|
||||||
let source = new EventSource( remoteURL + '/mainNotifier', {
|
|
||||||
https: true,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: {
|
|
||||||
'Cookie': sessionToken
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
source.onmessage = ( e ) => {
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = JSON.parse( e.data );
|
|
||||||
} catch ( err ) {
|
|
||||||
data = { 'type': e.data };
|
|
||||||
}
|
|
||||||
if ( data.type === 'blur' ) {
|
|
||||||
sendClientUpdate( data.type, data.ip );
|
|
||||||
} else if ( data.type === 'visibility' ) {
|
|
||||||
sendClientUpdate( data.type, data.ip );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
source.onopen = () => {
|
|
||||||
isReconnecting = false;
|
|
||||||
console.log( '[ BACKEND INTEGRATION ] Connection to notifier successful' );
|
|
||||||
};
|
|
||||||
|
|
||||||
source.addEventListener( 'error', function( e ) {
|
|
||||||
if ( e.eventPhase == EventSource.CLOSED ) source.close();
|
|
||||||
|
|
||||||
setTimeout( () => {
|
|
||||||
if ( !isReconnecting ) {
|
|
||||||
isReconnecting = true;
|
|
||||||
console.log( '[ BACKEND INTEGRATION ] Disconnected from notifier, reconnecting...' );
|
|
||||||
tryReconnect();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
}, false );
|
|
||||||
} else {
|
|
||||||
axios.post( remoteURL + '/authSSE', { 'authKey': authKey } ).then( res => {
|
|
||||||
if ( res.status == 200 ) {
|
|
||||||
sessionToken = res.headers[ 'set-cookie' ][ 0 ].slice( 0, res.headers[ 'set-cookie' ][ 0 ].indexOf( ';' ) );
|
|
||||||
isSSEAuth = true;
|
|
||||||
connectToSSESource();
|
|
||||||
} else {
|
|
||||||
connectToSSESource();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tryReconnect = () => {
|
|
||||||
const int = setInterval( () => {
|
|
||||||
if ( !isReconnecting ) {
|
|
||||||
clearInterval( int );
|
|
||||||
} else {
|
|
||||||
if ( errorCount > 5 ) {
|
|
||||||
isSSEAuth = false;
|
|
||||||
errorCount = 0;
|
|
||||||
} else {
|
|
||||||
errorCount += 1;
|
|
||||||
}
|
|
||||||
connectToSSESource();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
}
|
|
||||||
|
|
||||||
let authKey = conf.authKey ?? '';
|
|
||||||
|
|
||||||
connect();
|
|
||||||
|
|
||||||
|
|
||||||
let connectedClients = {};
|
|
||||||
let changedStatus = [];
|
|
||||||
|
|
||||||
let currentDetails = {
|
|
||||||
'songQueue': [],
|
|
||||||
'playingSong': {},
|
|
||||||
'pos': 0,
|
|
||||||
'isPlaying': false,
|
|
||||||
'queuePos': 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let connectedMain = {};
|
|
||||||
// TODO: Add backend integration
|
|
||||||
|
|
||||||
require( './appleMusicRoutes.js' )( app );
|
|
||||||
|
|
||||||
app.get( '/', ( request, response ) => {
|
|
||||||
response.sendFile( path.join( __dirname + '/client/showcase.html' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/getLocalIP', ( req, res ) => {
|
|
||||||
res.send( ip.address() );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/useAppleMusic', ( req, res ) => {
|
|
||||||
shell.openExternal( 'http://localhost:8081/apple-music' );
|
|
||||||
res.send( 'ok' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/openSongs', ( req, res ) => {
|
|
||||||
// res.send( '{ "data": [ "/home/janis/Music/KB2022" ] }' );
|
|
||||||
// res.send( '{ "data": [ "/mnt/storage/SORTED/Music/audio/KB2022" ] }' );
|
|
||||||
res.send( { 'data': dialog.showOpenDialogSync( { properties: [ 'openDirectory' ], title: 'Open music library folder' } ) } );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/showcase.js', ( req, res ) => {
|
|
||||||
res.sendFile( path.join( __dirname + '/client/showcase.js' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/showcase.css', ( req, res ) => {
|
|
||||||
res.sendFile( path.join( __dirname + '/client/showcase.css' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/backgroundAnim.css', ( req, res ) => {
|
|
||||||
res.sendFile( path.join( __dirname + '/client/backgroundAnim.css' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/clientDisplayNotifier', ( req, res ) => {
|
|
||||||
res.writeHead( 200, {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
} );
|
|
||||||
res.status( 200 );
|
|
||||||
res.flushHeaders();
|
|
||||||
let det = { 'type': 'basics', 'data': currentDetails };
|
|
||||||
res.write( `data: ${ JSON.stringify( det ) }\n\n` );
|
|
||||||
connectedClients[ req.session.id ] = res;
|
|
||||||
req.on( 'close', () => {
|
|
||||||
connectedClients.splice( Object.keys( connectedClients ).indexOf( req.session.id ), 1 );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/mainNotifier', ( req, res ) => {
|
|
||||||
const ipRetrieved = req.headers[ 'x-forwarded-for' ];
|
|
||||||
const ip = ipRetrieved ? ipRetrieved.split( /, / )[ 0 ] : req.connection.remoteAddress;
|
|
||||||
if ( ip === '::ffff:127.0.0.1' || ip === '::1' ) {
|
|
||||||
res.writeHead( 200, {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
} );
|
|
||||||
res.status( 200 );
|
|
||||||
res.flushHeaders();
|
|
||||||
let det = { 'type': 'basics' };
|
|
||||||
res.write( `data: ${ JSON.stringify( det ) }\n\n` );
|
|
||||||
connectedMain = res;
|
|
||||||
} else {
|
|
||||||
res.send( 'wrong' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
const sendUpdate = ( update ) => {
|
|
||||||
if ( update === 'pos' ) {
|
|
||||||
currentDetails[ 'playingSong' ][ 'startTime' ] = new Date().getTime();
|
|
||||||
for ( let client in connectedClients ) {
|
|
||||||
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'playingSong', 'data': currentDetails[ 'playingSong' ] } ) + '\n\n' );
|
|
||||||
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'pos', 'data': currentDetails[ 'pos' ] } ) + '\n\n' );
|
|
||||||
}
|
|
||||||
} else if ( update === 'playingSong' ) {
|
|
||||||
if ( !currentDetails[ 'playingSong' ] ) {
|
|
||||||
currentDetails[ 'playingSong' ] = {};
|
|
||||||
}
|
|
||||||
currentDetails[ 'playingSong' ][ 'startTime' ] = new Date().getTime();
|
|
||||||
for ( let client in connectedClients ) {
|
|
||||||
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'pos', 'data': currentDetails[ 'pos' ] } ) + '\n\n' );
|
|
||||||
}
|
|
||||||
} else if ( update === 'isPlaying' ) {
|
|
||||||
currentDetails[ 'playingSong' ][ 'startTime' ] = new Date().getTime();
|
|
||||||
for ( let client in connectedClients ) {
|
|
||||||
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'playingSong', 'data': currentDetails[ 'playingSong' ] } ) + '\n\n' );
|
|
||||||
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': 'pos', 'data': currentDetails[ 'pos' ] } ) + '\n\n' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ( let client in connectedClients ) {
|
|
||||||
connectedClients[ client ].write( 'data: ' + JSON.stringify( { 'type': update, 'data': currentDetails[ update ] } ) + '\n\n' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if connected and if not, try to authenticate with data from authKey file
|
|
||||||
|
|
||||||
if ( hasConnected ) {
|
|
||||||
if ( update === 'isPlaying' ) {
|
|
||||||
axios.post( remoteURL + '/statusUpdate', { 'type': 'playingSong', 'data': currentDetails[ 'playingSong' ], 'authKey': authKey } ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
|
|
||||||
axios.post( remoteURL + '/statusUpdate', { 'type': 'pos', 'data': currentDetails[ 'pos' ], 'authKey': authKey } ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
} else if ( update === 'pos' ) {
|
|
||||||
axios.post( remoteURL + '/statusUpdate', { 'type': 'playingSong', 'data': currentDetails[ 'playingSong' ], 'authKey': authKey } ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
|
|
||||||
axios.post( remoteURL + '/statusUpdate', { 'type': 'pos', 'data': currentDetails[ 'pos' ], 'authKey': authKey } ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
axios.post( remoteURL + '/statusUpdate', { 'type': update, 'data': currentDetails[ update ], 'authKey': authKey } ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
} else {
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedTypes = [ 'playingSong', 'isPlaying', 'songQueue', 'pos', 'queuePos' ];
|
|
||||||
app.post( '/statusUpdate', ( req, res ) => {
|
|
||||||
if ( allowedTypes.includes( req.body.type ) ) {
|
|
||||||
currentDetails[ req.body.type ] = req.body.data;
|
|
||||||
changedStatus.push( req.body.type );
|
|
||||||
sendUpdate( req.body.type );
|
|
||||||
res.send( 'ok' );
|
|
||||||
} else {
|
|
||||||
res.status( 400 ).send( 'ERR_UNKNOWN_TYPE' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
// STATUS UPDATE from the client display to send to main ui
|
|
||||||
// Send update if page is closed
|
|
||||||
const allowedMainUpdates = [ 'blur', 'visibility' ];
|
|
||||||
app.post( '/clientStatusUpdate', ( req, res ) => {
|
|
||||||
if ( allowedMainUpdates.includes( req.body.type ) ) {
|
|
||||||
const ipRetrieved = req.headers[ 'x-forwarded-for' ];
|
|
||||||
const ip = ipRetrieved ? ipRetrieved.split( /, / )[ 0 ] : req.connection.remoteAddress;
|
|
||||||
sendClientUpdate( req.body.type, ip );
|
|
||||||
res.send( 'ok' );
|
|
||||||
} else {
|
|
||||||
res.status( 400 ).send( 'ERR_UNKNOWN_TYPE' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
const sendClientUpdate = ( update, ip ) => {
|
|
||||||
try {
|
|
||||||
connectedMain.write( 'data: ' + JSON.stringify( { 'type': update, 'ip': ip } ) + '\n\n' );
|
|
||||||
} catch ( err ) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get( '/indexDirs', ( req, res ) => {
|
|
||||||
if ( req.query.dir ) {
|
|
||||||
indexer.index( req ).then( dirIndex => {
|
|
||||||
res.send( dirIndex );
|
|
||||||
} ).catch( err => {
|
|
||||||
if ( err === 'ERR_DIR_NOT_FOUND' ) {
|
|
||||||
res.status( 404 ).send( 'ERR_DIR_NOT_FOUND' );
|
|
||||||
} else {
|
|
||||||
res.status( 500 ).send( 'unable to process' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
} else {
|
|
||||||
res.status( 400 ).send( 'ERR_REQ_INCOMPLETE' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/loadPlaylist', ( req, res ) => {
|
|
||||||
const selFile = dialog.showOpenDialogSync( { properties: [ 'openFile' ], title: 'Open file with playlist' } )[ 0 ];
|
|
||||||
res.send( { 'data': JSON.parse( fs.readFileSync( selFile ) ), 'path': selFile } );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/getMetadata', async ( req, res ) => {
|
|
||||||
res.send( await indexer.analyzeFile( req.query.file ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.post( '/savePlaylist', ( req, res ) => {
|
|
||||||
fs.writeFileSync( dialog.showSaveDialogSync( {
|
|
||||||
properties: [ 'createDirectory' ],
|
|
||||||
title: 'Save the playlist as a json file',
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
extensions: [ 'json' ],
|
|
||||||
name: 'JSON files',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
defaultPath: 'songs.json'
|
|
||||||
} ), beautify( req.body, null, 2, 50 ) );
|
|
||||||
res.send( 'ok' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/getSongCover', ( req, res ) => {
|
|
||||||
if ( req.query.filename ) {
|
|
||||||
if ( indexer.getImages( req.query.filename ) ) {
|
|
||||||
res.send( indexer.getImages( req.query.filename ) );
|
|
||||||
} else {
|
|
||||||
res.status( 404 ).send( 'No cover image for this file' );
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.status( 400 ).send( 'ERR_REQ_INCOMPLETE' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/getSongFile', ( req, res ) => {
|
|
||||||
if ( req.query.filename ) {
|
|
||||||
res.sendFile( req.query.filename );
|
|
||||||
} else {
|
|
||||||
res.status( 400 ).send( 'ERR_REQ_INCOMPLETE' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
|
|
||||||
app.get( '/getAppleMusicDevToken', ( req, res ) => {
|
|
||||||
// sign dev token
|
|
||||||
const privateKey = fs.readFileSync( path.join( __dirname + '/config/apple_private_key.p8' ) ).toString();
|
|
||||||
// TODO: Remove secret
|
|
||||||
const config = JSON.parse( fs.readFileSync( path.join( __dirname + '/config/apple-music-api.config.json' ) ) );
|
|
||||||
const jwtToken = jwt.sign( {}, privateKey, {
|
|
||||||
algorithm: "ES256",
|
|
||||||
expiresIn: "180d",
|
|
||||||
issuer: config.teamID,
|
|
||||||
header: {
|
|
||||||
alg: "ES256",
|
|
||||||
kid: config.keyID
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
res.send( jwtToken );
|
|
||||||
} );
|
|
||||||
|
|
||||||
|
|
||||||
app.use( ( request, response, next ) => {
|
|
||||||
response.sendFile( path.join( __dirname + '' ) )
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.listen( 8081 );
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
/*
|
|
||||||
* MusicPlayerV2 - appleMusicRoutes.js
|
|
||||||
*
|
|
||||||
* Created by Janis Hutz 11/14/2023, Licensed under the GPL V3 License
|
|
||||||
* https://janishutz.com, development@janishutz.com
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require( 'path' );
|
|
||||||
const fs = require( 'fs' );
|
|
||||||
const csv = require( 'csv-parser' );
|
|
||||||
const dialog = require( 'electron' ).dialog;
|
|
||||||
|
|
||||||
const analyzeFile = ( filepath ) => {
|
|
||||||
return new Promise( ( resolve, reject ) => {
|
|
||||||
if ( filepath.includes( '.csv' ) ) {
|
|
||||||
// This will assume that line #1 will be song #1 in the file list
|
|
||||||
// (when sorted by name)
|
|
||||||
let results = {};
|
|
||||||
let pos = 0;
|
|
||||||
fs.createReadStream( filepath )
|
|
||||||
.pipe( csv() )
|
|
||||||
.on( 'data', ( data ) => {
|
|
||||||
results[ pos ] = data;
|
|
||||||
pos += 1;
|
|
||||||
} ).on( 'end', () => {
|
|
||||||
resolve( results );
|
|
||||||
} );
|
|
||||||
} else if ( filepath.includes( '.json' ) ) {
|
|
||||||
resolve( JSON.parse( fs.readFileSync( filepath ) ) );
|
|
||||||
} else {
|
|
||||||
reject( 'NO_CSV_OR_JSON_FILE' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ( app ) => {
|
|
||||||
app.get( '/apple-music', ( req, res ) => {
|
|
||||||
res.sendFile( path.join( __dirname + '/client/appleMusic/index.html' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/apple-music/helpers/:file', ( req, res ) => {
|
|
||||||
res.sendFile( path.join( __dirname + '/client/appleMusic/' + req.params.file ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/icon-font.css', ( req, res ) => {
|
|
||||||
res.sendFile( path.join( __dirname + '/client/icon-font.css' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/iconFont.woff2', ( req, res ) => {
|
|
||||||
res.sendFile( path.join( __dirname + '/client/iconFont.woff2' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/logo.png', ( req, res ) => {
|
|
||||||
res.sendFile( path.join( __dirname + '/client/logo.png' ) );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/apple-music/getAdditionalData', ( req, res ) => {
|
|
||||||
const filepath = dialog.showOpenDialogSync( {
|
|
||||||
properties: [ 'openFile' ],
|
|
||||||
title: 'Open file with additional data on the songs',
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'All supported files (.csv, .json)',
|
|
||||||
extensions: [ 'csv', 'json' ],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'JSON',
|
|
||||||
extensions: [ 'json' ],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CSV',
|
|
||||||
extensions: [ 'csv' ],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
} )[ 0 ];
|
|
||||||
analyzeFile( filepath ).then( analyzedFile => {
|
|
||||||
res.send( analyzedFile );
|
|
||||||
} ).catch( () => {
|
|
||||||
res.status( 500 ).send( 'no csv / json file' );
|
|
||||||
} )
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M204-318q-22-38-33-78t-11-82q0-134 93-228t227-94h7l-64-64 56-56 160 160-160 160-56-56 64-64h-7q-100 0-170 70.5T240-478q0 26 6 51t18 49l-60 60ZM481-40 321-200l160-160 56 56-64 64h7q100 0 170-70.5T720-482q0-26-6-51t-18-49l60-60q22 38 33 78t11 82q0 134-93 228t-227 94h-7l64 64-56 56Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 386 B |
|
Before Width: | Height: | Size: 582 KiB |
@@ -1,82 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import { app, protocol, BrowserWindow } from 'electron'
|
|
||||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
|
|
||||||
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer'
|
|
||||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
|
||||||
|
|
||||||
// Scheme must be registered before the app is ready
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
|
||||||
{ scheme: 'app', privileges: { secure: true, standard: true } }
|
|
||||||
])
|
|
||||||
|
|
||||||
async function createWindow() {
|
|
||||||
// Create the browser window.
|
|
||||||
const win = new BrowserWindow({
|
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
webPreferences: {
|
|
||||||
|
|
||||||
// Use pluginOptions.nodeIntegration, leave this alone
|
|
||||||
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
|
|
||||||
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
|
|
||||||
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
|
||||||
// Load the url of the dev server if in development mode
|
|
||||||
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
|
|
||||||
if (!process.env.IS_TEST) win.webContents.openDevTools()
|
|
||||||
} else {
|
|
||||||
createProtocol('app')
|
|
||||||
// Load the index.html when not in development
|
|
||||||
win.loadURL('app://./index.html')
|
|
||||||
}
|
|
||||||
require( './app.js' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quit when all windows are closed.
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
// On macOS it is common for applications and their menu bar
|
|
||||||
// to stay active until the user quits explicitly with Cmd + Q
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
// On macOS it's common to re-create a window in the app when the
|
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
|
||||||
})
|
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
|
||||||
// initialization and is ready to create browser windows.
|
|
||||||
// Some APIs can only be used after this event occurs.
|
|
||||||
app.on('ready', async () => {
|
|
||||||
if (isDevelopment && !process.env.IS_TEST) {
|
|
||||||
// Install Vue Devtools
|
|
||||||
try {
|
|
||||||
await installExtension(VUEJS3_DEVTOOLS)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Vue Devtools failed to install:', e.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createWindow()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Exit cleanly on request from parent process in development mode.
|
|
||||||
if (isDevelopment) {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
process.on('message', (data) => {
|
|
||||||
if (data === 'graceful-exit') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
app.quit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>MusicPlayerV2</title>
|
|
||||||
<script src="/apple-music/helpers/musickit.js"></script>
|
|
||||||
<link rel="stylesheet" href="/icon-font.css">
|
|
||||||
<link rel="stylesheet" href="/apple-music/helpers/playerStyle.css">
|
|
||||||
<link rel="stylesheet" href="/apple-music/helpers/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<div v-if="isShowingWarning" class="warning">
|
|
||||||
<h3>WARNING!</h3>
|
|
||||||
<p>A client display is being tampered with!</p>
|
|
||||||
<p>A desktop notification with a warning has already been dispatched.</p>
|
|
||||||
<button @click="dismissNotification()">Ok</button>
|
|
||||||
|
|
||||||
<div class="flash"></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isPreparingToPlay" class="preparingToPlay">
|
|
||||||
<span class="material-symbols-outlined loading-spinner">autorenew</span>
|
|
||||||
<h1>Loading player...</h1>
|
|
||||||
</div>
|
|
||||||
<div v-if="!isLoggedIn" class="start-page">
|
|
||||||
<div class="image-wrapper">
|
|
||||||
<img src="/logo.png" alt="Music player icon" id="logo-main">
|
|
||||||
<img src="/apple-music/helpers/appleMusicIcon.svg" alt="Apple Music Icon" id="apple-music-logo">
|
|
||||||
</div>
|
|
||||||
<h1>Apple Music integration</h1>
|
|
||||||
<button @click="logInto()" class="button">Log in</button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="home">
|
|
||||||
<div v-if="!hasSelectedPlaylist" class="song-list-wrapper">
|
|
||||||
<h1>Your playlists</h1>
|
|
||||||
<button class="button" @click="selectPlaylistFromDisk()">Load playlist file from disk</button>
|
|
||||||
<div v-if="!hasLoadedPlaylists" style="display: flex; justify-content: center; align-items: center; flex-direction: column;">
|
|
||||||
<span class="material-symbols-outlined loading-spinner">autorenew</span>
|
|
||||||
<h3>Loading playlists...</h3>
|
|
||||||
</div>
|
|
||||||
<div v-for="playlist in playlists" class="song-list" @click="selectPlaylist( playlist.id )" style="cursor: pointer;">
|
|
||||||
<h3>{{ playlist.title }}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="home">
|
|
||||||
<div class="top-bar">
|
|
||||||
<img src="/logo.png" alt="logo" class="logo">
|
|
||||||
<audio :src="'/getSongFile?filename=' + basePath + '/' + playingSong.filename" preload="metadata" id="audio-player"></audio>
|
|
||||||
<div class="player-wrapper">
|
|
||||||
<div class="player">
|
|
||||||
<div class="controls">
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" @click="control( 'previous' )">skip_previous</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" @click="control( 'replay10' )">replay_10</span>
|
|
||||||
<span class="material-symbols-outlined control-icon play-pause" v-if="!isPlaying && hasSelectedPlaylist" @click="control( 'play' )">play_arrow</span>
|
|
||||||
<span class="material-symbols-outlined control-icon play-pause" v-else-if="isPlaying && hasSelectedPlaylist" @click="control( 'pause' )">pause</span>
|
|
||||||
<span class="material-symbols-outlined control-icon play-pause" style="cursor: default;" v-else>play_disabled</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" @click="control( 'forward10' )">forward_10</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" @click="control( 'next' )" style="margin-right: 1vw;">skip_next</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-if="!isShuffleEnabled" @click="control( 'shuffleOn' )">shuffle</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-else @click="control( 'shuffleOff' )">shuffle_on</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-if="repeatMode === 'off'" @click="control( 'repeatOne' )">repeat</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-else-if="repeatMode === 'one'" @click="control( 'repeatAll' )">repeat_one_on</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasFinishedInit ? 'active': 'inactive'" v-else-if="repeatMode === 'all'" @click="control( 'repeatOff' )">repeat_on</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasSelectedPlaylist ? 'active': 'inactive'" @click="getAdditionalSongInfo()" title="Load additional song information" style="margin-left: 1vw;">upload</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasSelectedPlaylist ? 'active': 'inactive'" @click="exportCurrentPlaylist()" title="Export current playlist" style="margin-right: 1vw;">ios_share</span>
|
|
||||||
<div class="control-icon" id="settings">
|
|
||||||
<span class="material-symbols-outlined">info</span>
|
|
||||||
<div id="showIP">
|
|
||||||
<h4>IP to connect to:</h4><br>
|
|
||||||
<p>{{ localIP }}:8081</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button @click="search()">S</button>
|
|
||||||
</div>
|
|
||||||
<div class="song-info">
|
|
||||||
<div class="song-info-wrapper">
|
|
||||||
<img v-if="hasFinishedInit" :src="playingSong.coverArtURL" class="image">
|
|
||||||
<span class="material-symbols-outlined image" v-else>music_note</span>
|
|
||||||
<div class="name">
|
|
||||||
<h3>{{ playingSong.title ?? 'No song selected' }}</h3>
|
|
||||||
<p>{{ playingSong.artist }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="image"></div>
|
|
||||||
</div>
|
|
||||||
<div class="playback-pos-info">
|
|
||||||
<div style="margin-right: auto;">{{ playbackPosBeautified }}</div>
|
|
||||||
<div @click="toggleShowMode()" style="cursor: pointer;">{{ durationBeautified }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="slider">
|
|
||||||
<progress id="progress-slider" class="progress-slider" :value="sliderProgress" max="1000" @mousedown="( e ) => { setPos( e ) }"
|
|
||||||
:class="hasFinishedInit ? '' : 'slider-inactive'"></progress>
|
|
||||||
<div v-if="hasFinishedInit" id="slider-knob" @mousedown="( e ) => { startMove( e ) }"
|
|
||||||
:style="'left: ' + ( parseInt( originalPos ) + parseInt( sliderPos ) ) + 'px;'">
|
|
||||||
<div id="slider-knob-style"></div>
|
|
||||||
</div>
|
|
||||||
<div v-else id="slider-knob" class="slider-inactive" style="left: 0;">
|
|
||||||
<div id="slider-knob-style"></div>
|
|
||||||
</div>
|
|
||||||
<div id="drag-support" @mousemove="e => { handleDrag( e ) }" @mouseup="() => { stopMove(); }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pool-wrapper">
|
|
||||||
<div style="width: 100%;" class="song-list-wrapper">
|
|
||||||
<div v-for="song in songQueue" class="song-list" :class="[ isPlaying ? ( playingSong.queuePos == song.queuePos ? 'playing': 'not-playing' ) : 'not-playing', !isPlaying && playingSong.filename === song.filename ? 'active-song': undefined ]">
|
|
||||||
<img :src="song.coverArtURL" class="song-image">
|
|
||||||
<div v-if="playingSong.queuePos == song.queuePos && isPlaying" class="playing-symbols">
|
|
||||||
<div class="playing-symbols-wrapper">
|
|
||||||
<div class="playing-bar" id="bar-1"></div>
|
|
||||||
<div class="playing-bar" id="bar-2"></div>
|
|
||||||
<div class="playing-bar" id="bar-3"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="material-symbols-outlined play-icon" @click="play( song )">play_arrow</span>
|
|
||||||
<span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</span>
|
|
||||||
<h3>{{ song.title }}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
||||||
<script src="/apple-music/helpers/index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,720 +0,0 @@
|
|||||||
/*
|
|
||||||
* MusicPlayerV2 - index.js
|
|
||||||
*
|
|
||||||
* Created by Janis Hutz 11/20/2023, Licensed under the GPL V3 License
|
|
||||||
* https://janishutz.com, development@janishutz.com
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Quick side note here: This is terribly ugly code, but I was in a hurry to finish it,
|
|
||||||
so I had no time to clean it up. I will do that at some point -jh
|
|
||||||
*/
|
|
||||||
|
|
||||||
const app = Vue.createApp( {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
musicKit: null,
|
|
||||||
isLoggedIn: false,
|
|
||||||
config: {
|
|
||||||
'devToken': '',
|
|
||||||
'userToken': ''
|
|
||||||
},
|
|
||||||
playlists: {},
|
|
||||||
hasSelectedPlaylist: false,
|
|
||||||
songQueue: {},
|
|
||||||
queuePos: 0,
|
|
||||||
pos: 0,
|
|
||||||
playingSong: {},
|
|
||||||
isPlaying: false,
|
|
||||||
isShuffleEnabled: false,
|
|
||||||
repeatMode: 'off',
|
|
||||||
playbackPosBeautified: '',
|
|
||||||
durationBeautified: '',
|
|
||||||
isShowingRemainingTime: false,
|
|
||||||
localIP: '',
|
|
||||||
hasLoadedPlaylists: false,
|
|
||||||
isPreparingToPlay: false,
|
|
||||||
additionalSongInfo: {},
|
|
||||||
hasFinishedInit: false,
|
|
||||||
isShowingWarning: false,
|
|
||||||
|
|
||||||
// For use with playlists that are partially from apple music and
|
|
||||||
// local drive
|
|
||||||
isUsingCustomPlaylist: false,
|
|
||||||
rawLoadedPlaylistData: {},
|
|
||||||
basePath: '',
|
|
||||||
audioPlayer: null,
|
|
||||||
isReconnecting: false,
|
|
||||||
|
|
||||||
// slider
|
|
||||||
offset: 0,
|
|
||||||
isDragging: false,
|
|
||||||
sliderPos: 0,
|
|
||||||
originalPos: 0,
|
|
||||||
sliderProgress: 0,
|
|
||||||
active: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
logInto() {
|
|
||||||
if ( !this.musicKit.isAuthorized ) {
|
|
||||||
this.musicKit.authorize().then( () => {
|
|
||||||
this.isLoggedIn = true;
|
|
||||||
this.initMusicKit();
|
|
||||||
} );
|
|
||||||
} else {
|
|
||||||
this.musicKit.authorize().then( () => {
|
|
||||||
this.isLoggedIn = true;
|
|
||||||
this.initMusicKit();
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initMusicKit () {
|
|
||||||
fetch( '/getAppleMusicDevToken' ).then( res => {
|
|
||||||
if ( res.status === 200 ) {
|
|
||||||
res.text().then( token => {
|
|
||||||
// MusicKit global is now defined
|
|
||||||
MusicKit.configure( {
|
|
||||||
developerToken: token,
|
|
||||||
app: {
|
|
||||||
name: 'MusicPlayer',
|
|
||||||
build: '2'
|
|
||||||
},
|
|
||||||
storefrontId: 'CH',
|
|
||||||
} ).then( () => {
|
|
||||||
this.config.devToken = token;
|
|
||||||
this.musicKit = MusicKit.getInstance();
|
|
||||||
if ( this.musicKit.isAuthorized ) {
|
|
||||||
this.isLoggedIn = true;
|
|
||||||
this.config.userToken = this.musicKit.musicUserToken;
|
|
||||||
}
|
|
||||||
this.musicKit.shuffleMode = MusicKit.PlayerShuffleMode.off;
|
|
||||||
this.apiGetRequest( 'https://api.music.apple.com/v1/me/library/playlists', this.playlistHandler );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
playlistHandler ( data ) {
|
|
||||||
if ( data.status === 'ok' ) {
|
|
||||||
const d = data.data.data;
|
|
||||||
this.playlist = {};
|
|
||||||
for ( let el in d ) {
|
|
||||||
this.playlists[ d[ el ].id ] = {
|
|
||||||
title: d[ el ].attributes.name,
|
|
||||||
id: d[ el ].id,
|
|
||||||
playParams: d[ el ].attributes.playParams,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.hasLoadedPlaylists = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
apiGetRequest( url, callback ) {
|
|
||||||
if ( this.config.devToken != '' && this.config.userToken != '' ) {
|
|
||||||
fetch( url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${ this.config.devToken }`,
|
|
||||||
'Music-User-Token': this.config.userToken
|
|
||||||
}
|
|
||||||
} ).then( res => {
|
|
||||||
if ( res.status === 200 ) {
|
|
||||||
res.json().then( json => {
|
|
||||||
try {
|
|
||||||
callback( { 'status': 'ok', 'data': json } );
|
|
||||||
} catch( err ) {}
|
|
||||||
} );
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
callback( { 'status': 'error', 'error': res.status } );
|
|
||||||
} catch( err ) {}
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
} else return false;
|
|
||||||
},
|
|
||||||
getAdditionalSongInfo() {
|
|
||||||
if ( Object.keys( this.additionalSongInfo ).length < 1 ) {
|
|
||||||
fetch( '/apple-music/getAdditionalData' ).then( res => {
|
|
||||||
if ( res.status === 200 ) {
|
|
||||||
res.json().then( json => {
|
|
||||||
this.additionalSongInfo = json;
|
|
||||||
this.handleAdditionalData();
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleAdditionalData () {
|
|
||||||
if ( Object.keys( this.additionalSongInfo ).length > 0 ) {
|
|
||||||
for ( let item in this.songQueue ) {
|
|
||||||
if ( this.additionalSongInfo[ item ] ) {
|
|
||||||
for ( let d in this.additionalSongInfo[ item ] ) {
|
|
||||||
if ( !this.songQueue[ item ][ d ] ) {
|
|
||||||
this.songQueue[ item ][ d ] = this.additionalSongInfo[ item ][ d ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.playingSong = this.songQueue[ this.queuePos ];
|
|
||||||
this.sendUpdate( 'songQueue' );
|
|
||||||
this.sendUpdate( 'playingSong' );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectPlaylist( id ) {
|
|
||||||
this.isPreparingToPlay = true;
|
|
||||||
this.musicKit.setQueue( { playlist: id } ).then( () => {
|
|
||||||
try {
|
|
||||||
this.loadPlaylist();
|
|
||||||
this.hasSelectedPlaylist = true;
|
|
||||||
this.isPreparingToPlay = false;
|
|
||||||
} catch( err ) {
|
|
||||||
this.hasSelectedPlaylist = false;
|
|
||||||
console.error( err );
|
|
||||||
alert( 'We were unable to play. Please ensure that DRM (yeah sorry it is Apple Music, we cannot do anything about that) is enabled and working' );
|
|
||||||
}
|
|
||||||
} ).catch( err => {
|
|
||||||
console.error( 'ERROR whilst settings Queue', err );
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
handleDrag( e ) {
|
|
||||||
if ( this.isDragging ) {
|
|
||||||
if ( 0 < this.originalPos + e.screenX - this.offset && this.originalPos + e.screenX - this.offset < document.getElementById( 'progress-slider' ).clientWidth - 5 ) {
|
|
||||||
this.sliderPos = e.screenX - this.offset;
|
|
||||||
this.calcProgressPos();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
startMove( e ) {
|
|
||||||
this.offset = e.screenX;
|
|
||||||
this.isDragging = true;
|
|
||||||
document.getElementById( 'drag-support' ).classList.add( 'drag-support-active' );
|
|
||||||
},
|
|
||||||
stopMove() {
|
|
||||||
this.originalPos += parseInt( this.sliderPos );
|
|
||||||
this.isDragging = false;
|
|
||||||
this.offset = 0;
|
|
||||||
this.sliderPos = 0;
|
|
||||||
document.getElementById( 'drag-support' ).classList.remove( 'drag-support-active' );
|
|
||||||
this.calcPlaybackPos();
|
|
||||||
},
|
|
||||||
setPos ( e ) {
|
|
||||||
if ( this.hasSelectedPlaylist ) {
|
|
||||||
this.originalPos = e.offsetX;
|
|
||||||
this.calcProgressPos();
|
|
||||||
this.calcPlaybackPos();
|
|
||||||
if ( this.playingSong.origin === 'apple-music' ) {
|
|
||||||
this.musicKit.seekToTime( this.pos );
|
|
||||||
} else {
|
|
||||||
this.audioPlayer.currentTime = this.pos;
|
|
||||||
}
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calcProgressPos() {
|
|
||||||
this.sliderProgress = Math.ceil( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider' ).clientWidth - 5 ) * 1000 );
|
|
||||||
},
|
|
||||||
calcPlaybackPos() {
|
|
||||||
this.pos = Math.round( ( this.originalPos + parseInt( this.sliderPos ) ) / ( document.getElementById( 'progress-slider' ).clientWidth - 5 ) * this.playingSong.duration );
|
|
||||||
},
|
|
||||||
sendUpdate( update ) {
|
|
||||||
let data = {};
|
|
||||||
let up = update;
|
|
||||||
if ( update === 'pos' ) {
|
|
||||||
data = this.pos;
|
|
||||||
} else if ( update === 'playingSong' ) {
|
|
||||||
data = this.playingSong;
|
|
||||||
} else if ( update === 'isPlaying' ) {
|
|
||||||
data = this.isPlaying;
|
|
||||||
} else if ( update === 'songQueue' ) {
|
|
||||||
data = this.songQueue;
|
|
||||||
} else if ( update === 'queuePos' ) {
|
|
||||||
data = this.queuePos;
|
|
||||||
} else if ( update === 'posReset' ) {
|
|
||||||
data = 0;
|
|
||||||
up = 'pos';
|
|
||||||
}
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( { 'type': up, 'data': data } ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
loadPlaylist() {
|
|
||||||
const songQueue = this.musicKit.queue.items;
|
|
||||||
for ( let item in songQueue ) {
|
|
||||||
this.songQueue[ item ] = {
|
|
||||||
'artist': songQueue[ item ].attributes.artistName,
|
|
||||||
'title': songQueue[ item ].attributes.name,
|
|
||||||
'year': songQueue[ item ].attributes.releaseDate,
|
|
||||||
'genre': songQueue[ item ].attributes.genreNames,
|
|
||||||
'duration': Math.round( songQueue[ item ].attributes.durationInMillis / 1000 ),
|
|
||||||
'filename': songQueue[ item ].id,
|
|
||||||
'coverArtOrigin': 'api',
|
|
||||||
'hasCoverArt': true,
|
|
||||||
'queuePos': item,
|
|
||||||
'origin': 'apple-music',
|
|
||||||
}
|
|
||||||
let url = songQueue[ item ].attributes.artwork.url;
|
|
||||||
url = url.replace( '{w}', songQueue[ item ].attributes.artwork.width );
|
|
||||||
url = url.replace( '{h}', songQueue[ item ].attributes.artwork.height );
|
|
||||||
this.songQueue[ item ][ 'coverArtURL' ] = url;
|
|
||||||
this.handleAdditionalData();
|
|
||||||
this.sendUpdate( 'songQueue' );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
control( action ) {
|
|
||||||
if ( action === 'play' ) {
|
|
||||||
if ( !this.playingSong.origin ) {
|
|
||||||
this.play( this.songQueue[ 0 ] );
|
|
||||||
this.isPlaying = true;
|
|
||||||
} else {
|
|
||||||
if ( this.playingSong.origin === 'apple-music' ) {
|
|
||||||
if( !this.musicKit || !this.isPlaying ) {
|
|
||||||
this.musicKit.play().then( () => {
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} ).catch( err => {
|
|
||||||
console.log( 'player failed to start' );
|
|
||||||
console.log( err );
|
|
||||||
} );
|
|
||||||
} else {
|
|
||||||
this.musicKit.pause().then( () => {
|
|
||||||
this.musicKit.play().catch( err => {
|
|
||||||
console.log( 'player failed to start' );
|
|
||||||
console.log( err );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.audioPlayer.pause();
|
|
||||||
} catch ( err ) {}
|
|
||||||
} else {
|
|
||||||
this.audioPlayer.play();
|
|
||||||
this.musicKit.pause();
|
|
||||||
}
|
|
||||||
this.isPlaying = true;
|
|
||||||
try {
|
|
||||||
clearInterval( this.progressTracker );
|
|
||||||
} catch( err ) {};
|
|
||||||
this.progressTracker = setInterval( () => {
|
|
||||||
if ( this.playingSong.origin === 'apple-music' ) {
|
|
||||||
this.pos = parseInt( this.musicKit.currentPlaybackTime );
|
|
||||||
} else {
|
|
||||||
this.pos = parseInt( this.audioPlayer.currentTime );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( this.pos > this.playingSong.duration - 1 ) {
|
|
||||||
this.control( 'next' );
|
|
||||||
}
|
|
||||||
|
|
||||||
const minuteCount = Math.floor( this.pos / 60 );
|
|
||||||
this.playbackPosBeautified = minuteCount + ':';
|
|
||||||
if ( ( '' + minuteCount ).length === 1 ) {
|
|
||||||
this.playbackPosBeautified = '0' + minuteCount + ':';
|
|
||||||
}
|
|
||||||
const secondCount = Math.floor( this.pos - minuteCount * 60 );
|
|
||||||
if ( ( '' + secondCount ).length === 1 ) {
|
|
||||||
this.playbackPosBeautified += '0' + secondCount;
|
|
||||||
} else {
|
|
||||||
this.playbackPosBeautified += secondCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( this.isShowingRemainingTime ) {
|
|
||||||
const minuteCounts = Math.floor( ( this.playingSong.duration - this.pos ) / 60 );
|
|
||||||
this.durationBeautified = '-' + String( minuteCounts ) + ':';
|
|
||||||
if ( ( '' + minuteCounts ).length === 1 ) {
|
|
||||||
this.durationBeautified = '-0' + minuteCounts + ':';
|
|
||||||
}
|
|
||||||
const secondCounts = Math.floor( ( this.playingSong.duration - this.pos ) - minuteCounts * 60 );
|
|
||||||
if ( ( '' + secondCounts ).length === 1 ) {
|
|
||||||
this.durationBeautified += '0' + secondCounts;
|
|
||||||
} else {
|
|
||||||
this.durationBeautified += secondCounts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 50 );
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
this.sendUpdate( 'isPlaying' );
|
|
||||||
}
|
|
||||||
} else if ( action === 'pause' ) {
|
|
||||||
if ( this.playingSong.origin === 'apple-music' ) {
|
|
||||||
this.musicKit.pause();
|
|
||||||
} else {
|
|
||||||
this.audioPlayer.pause();
|
|
||||||
}
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
try {
|
|
||||||
clearInterval( this.progressTracker );
|
|
||||||
clearInterval( this.notifier );
|
|
||||||
} catch ( err ) {};
|
|
||||||
this.isPlaying = false;
|
|
||||||
this.sendUpdate( 'isPlaying' );
|
|
||||||
} else if ( action === 'replay10' ) {
|
|
||||||
if ( this.playingSong.origin === 'apple-music' ) {
|
|
||||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime > 10 ? this.musicKit.currentPlaybackTime - 10 : 0 );
|
|
||||||
this.pos = this.musicKit.currentPlaybackTime;
|
|
||||||
} else {
|
|
||||||
this.audioPlayer.currentTime = this.audioPlayer.currentTime > 10 ? this.audioPlayer.currentTime - 10 : 0;
|
|
||||||
this.pos = this.audioPlayer.currentTime;
|
|
||||||
}
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} else if ( action === 'forward10' ) {
|
|
||||||
if ( this.playingSong.origin === 'apple-music' ) {
|
|
||||||
if ( this.musicKit.currentPlaybackTime < ( this.playingSong.duration - 10 ) ) {
|
|
||||||
this.musicKit.seekToTime( this.musicKit.currentPlaybackTime + 10 );
|
|
||||||
this.pos = this.musicKit.currentPlaybackTime;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} else {
|
|
||||||
if ( this.repeatMode !== 'one' ) {
|
|
||||||
this.control( 'next' );
|
|
||||||
} else {
|
|
||||||
this.musicKit.seekToTime( 0 );
|
|
||||||
this.pos = this.musicKit.currentPlaybackTime;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( this.audioPlayer.currentTime < ( this.playingSong.duration - 10 ) ) {
|
|
||||||
this.audioPlayer.currentTime = this.audioPlayer.currentTime + 10;
|
|
||||||
this.pos = this.audioPlayer.currentTime;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} else {
|
|
||||||
if ( this.repeatMode !== 'one' ) {
|
|
||||||
this.control( 'next' );
|
|
||||||
} else {
|
|
||||||
this.audioPlayer.currentTime = 0;
|
|
||||||
this.pos = this.audioPlayer.currentTime;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ( action === 'reset' ) {
|
|
||||||
clearInterval( this.progressTracker );
|
|
||||||
this.pos = 0;
|
|
||||||
if ( this.playingSong.origin === 'apple-music' ) {
|
|
||||||
this.musicKit.seekToTime( 0 );
|
|
||||||
} else {
|
|
||||||
this.audioPlayer.currentTime = 0;
|
|
||||||
}
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} else if ( action === 'next' ) {
|
|
||||||
if ( this.queuePos < parseInt( Object.keys( this.songQueue ).length ) - 1 ) {
|
|
||||||
this.queuePos = parseInt( this.queuePos ) + 1;
|
|
||||||
this.play( this.songQueue[ this.queuePos ] );
|
|
||||||
} else {
|
|
||||||
if ( this.repeatMode === 'all' ) {
|
|
||||||
this.queuePos = 0;
|
|
||||||
this.play( this.songQueue[ 0 ] );
|
|
||||||
} else {
|
|
||||||
this.control( 'pause' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ( action === 'previous' ) {
|
|
||||||
if ( this.pos > 3 ) {
|
|
||||||
this.pos = 0;
|
|
||||||
if ( this.isUsingCustomPlaylist ) {
|
|
||||||
this.audioPlayer.currentTime = 0;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} else {
|
|
||||||
this.musicKit.seekToTime( 0 ).then( () => {
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
this.control( 'play' );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( this.queuePos > 0 ) {
|
|
||||||
this.queuePos = parseInt( this.queuePos ) - 1;
|
|
||||||
this.play( this.songQueue[ this.queuePos ] );
|
|
||||||
} else {
|
|
||||||
this.queuePos = parseInt( Object.keys( this.songQueue ).length ) - 1;
|
|
||||||
this.play[ this.songQueue[ this.queuePos ] ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if ( action === 'shuffleOff' ) {
|
|
||||||
// TODO: Make shuffle function
|
|
||||||
this.isShuffleEnabled = false;
|
|
||||||
// this.loadPlaylist();
|
|
||||||
alert( 'not implemented yet' );
|
|
||||||
} else if ( action === 'shuffleOn' ) {
|
|
||||||
this.isShuffleEnabled = true;
|
|
||||||
alert( 'not implemented yet' );
|
|
||||||
// this.loadPlaylist();
|
|
||||||
} else if ( action === 'repeatOne' ) {
|
|
||||||
this.repeatMode = 'one';
|
|
||||||
} else if ( action === 'repeatAll' ) {
|
|
||||||
this.repeatMode = 'all';
|
|
||||||
} else if ( action === 'repeatOff' ) {
|
|
||||||
this.musicKit.repeatMode = MusicKit.PlayerRepeatMode.none;
|
|
||||||
this.repeatMode = 'off';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
play( song, specificID ) {
|
|
||||||
let foundSong = specificID ?? 0;
|
|
||||||
if ( !specificID ) {
|
|
||||||
for ( let s in this.songQueue ) {
|
|
||||||
if ( this.songQueue[ s ] === song ) {
|
|
||||||
foundSong = s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.queuePos = foundSong;
|
|
||||||
this.sendUpdate( 'queuePos' );
|
|
||||||
this.pos = 0;
|
|
||||||
this.sendUpdate( 'posReset' );
|
|
||||||
this.playingSong = song;
|
|
||||||
this.sendUpdate( 'playingSong' );
|
|
||||||
if ( song.origin === 'apple-music' ) {
|
|
||||||
this.musicKit.setQueue( { 'song': song.filename } ).then( () => {
|
|
||||||
setTimeout( () => {
|
|
||||||
this.control( 'play' );
|
|
||||||
}, 500 );
|
|
||||||
} ).catch( ( err ) => {
|
|
||||||
console.log( err );
|
|
||||||
} );
|
|
||||||
} else {
|
|
||||||
setTimeout( () => {
|
|
||||||
this.control( 'play' );
|
|
||||||
}, 500 );
|
|
||||||
}
|
|
||||||
const minuteCounts = Math.floor( ( this.playingSong.duration ) / 60 );
|
|
||||||
this.durationBeautified = String( minuteCounts ) + ':';
|
|
||||||
if ( ( '' + minuteCounts ).length === 1 ) {
|
|
||||||
this.durationBeautified = '0' + minuteCounts + ':';
|
|
||||||
}
|
|
||||||
const secondCounts = Math.floor( ( this.playingSong.duration ) - minuteCounts * 60 );
|
|
||||||
if ( ( '' + secondCounts ).length === 1 ) {
|
|
||||||
this.durationBeautified += '0' + secondCounts;
|
|
||||||
} else {
|
|
||||||
this.durationBeautified += secondCounts;
|
|
||||||
}
|
|
||||||
this.hasFinishedInit = true;
|
|
||||||
},
|
|
||||||
toggleShowMode() {
|
|
||||||
this.isShowingRemainingTime = !this.isShowingRemainingTime;
|
|
||||||
},
|
|
||||||
exportCurrentPlaylist() {
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( this.songQueue ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( '/savePlaylist', fetchOptions ).then( res => {
|
|
||||||
if ( res.status === 200 ) {
|
|
||||||
console.log( 'saved' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
selectPlaylistFromDisk() {
|
|
||||||
this.isPreparingToPlay = true;
|
|
||||||
let playlistSongs = [];
|
|
||||||
fetch( '/loadPlaylist' ).then( res => {
|
|
||||||
res.json().then( data => {
|
|
||||||
this.rawLoadedPlaylistData = data.data;
|
|
||||||
this.basePath = data.path.slice( 0, data.path.lastIndexOf( '/' ) );
|
|
||||||
for ( let song in this.rawLoadedPlaylistData ) {
|
|
||||||
if ( this.rawLoadedPlaylistData[ song ].origin === 'apple-music' ) {
|
|
||||||
playlistSongs.push( this.rawLoadedPlaylistData[ song ].filename );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.musicKit.setQueue( { songs: playlistSongs } ).then( () => {
|
|
||||||
this.isUsingCustomPlaylist = true;
|
|
||||||
try {
|
|
||||||
this.loadCustomPlaylist();
|
|
||||||
this.hasSelectedPlaylist = true;
|
|
||||||
this.isPreparingToPlay = false;
|
|
||||||
} catch( err ) {
|
|
||||||
this.hasSelectedPlaylist = false;
|
|
||||||
console.error( err );
|
|
||||||
alert( 'We were unable to play. Please ensure that DRM (yeah sorry it is Apple Music, we cannot do anything about that) is enabled and working' );
|
|
||||||
}
|
|
||||||
} ).catch( err => {
|
|
||||||
console.error( 'ERROR whilst settings Queue', err );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
loadCustomPlaylist() {
|
|
||||||
const songQueue = this.musicKit.queue.items;
|
|
||||||
let offset = 0;
|
|
||||||
( async() => {
|
|
||||||
for ( let item in this.rawLoadedPlaylistData ) {
|
|
||||||
if ( this.rawLoadedPlaylistData[ item ].origin === 'apple-music' ) {
|
|
||||||
this.songQueue[ item ] = {
|
|
||||||
'artist': songQueue[ item - offset ].attributes.artistName,
|
|
||||||
'title': songQueue[ item - offset ].attributes.name,
|
|
||||||
'year': songQueue[ item - offset ].attributes.releaseDate,
|
|
||||||
'genre': songQueue[ item - offset ].attributes.genreNames,
|
|
||||||
'duration': Math.round( songQueue[ item - offset ].attributes.durationInMillis / 1000 ),
|
|
||||||
'filename': songQueue[ item - offset ].id,
|
|
||||||
'coverArtOrigin': 'api',
|
|
||||||
'hasCoverArt': true,
|
|
||||||
'queuePos': item,
|
|
||||||
'origin': 'apple-music',
|
|
||||||
'offset': offset,
|
|
||||||
}
|
|
||||||
let url = songQueue[ item - offset ].attributes.artwork.url;
|
|
||||||
url = url.replace( '{w}', songQueue[ item - offset ].attributes.artwork.width );
|
|
||||||
url = url.replace( '{h}', songQueue[ item - offset ].attributes.artwork.height );
|
|
||||||
this.songQueue[ item ][ 'coverArtURL' ] = url;
|
|
||||||
} else {
|
|
||||||
offset += 1;
|
|
||||||
const queryParameters = {
|
|
||||||
term: ( this.rawLoadedPlaylistData[ item ].artist ?? '' ) + ' ' + ( this.rawLoadedPlaylistData[ item ].title ?? '' ),
|
|
||||||
types: [ 'songs' ],
|
|
||||||
};
|
|
||||||
// TODO: Make storefront adjustable
|
|
||||||
const result = await this.musicKit.api.music( '/v1/catalog/ch/search', queryParameters );
|
|
||||||
let json;
|
|
||||||
try {
|
|
||||||
const res = await fetch( '/getMetadata?file=' + this.basePath + '/' + this.rawLoadedPlaylistData[ item ].filename );
|
|
||||||
json = await res.json();
|
|
||||||
} catch( err ) {}
|
|
||||||
if ( result.data ) {
|
|
||||||
if ( result.data.results.songs ) {
|
|
||||||
const dat = result.data.results.songs.data[ 0 ];
|
|
||||||
console.log( json );
|
|
||||||
this.songQueue[ item ] = {
|
|
||||||
'artist': dat.attributes.artistName,
|
|
||||||
'title': dat.attributes.name,
|
|
||||||
'year': dat.attributes.releaseDate,
|
|
||||||
'genre': dat.attributes.genreNames,
|
|
||||||
'duration': json ? Math.round( json.duration ) : undefined,
|
|
||||||
'filename': this.rawLoadedPlaylistData[ item ].filename,
|
|
||||||
'coverArtOrigin': 'api',
|
|
||||||
'hasCoverArt': true,
|
|
||||||
'queuePos': item,
|
|
||||||
'origin': 'local',
|
|
||||||
}
|
|
||||||
let url = dat.attributes.artwork.url;
|
|
||||||
url = url.replace( '{w}', dat.attributes.artwork.width );
|
|
||||||
url = url.replace( '{h}', dat.attributes.artwork.height );
|
|
||||||
this.songQueue[ item ][ 'coverArtURL' ] = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.handleAdditionalData();
|
|
||||||
this.sendUpdate( 'songQueue' );
|
|
||||||
}
|
|
||||||
} )();
|
|
||||||
setTimeout( () => {
|
|
||||||
this.audioPlayer = document.getElementById( 'audio-player' );
|
|
||||||
}, 1000 );
|
|
||||||
},
|
|
||||||
search() {
|
|
||||||
( async() => {
|
|
||||||
const searchTerm = prompt( 'Enter search term...' )
|
|
||||||
const queryParameters = {
|
|
||||||
term: ( searchTerm ),
|
|
||||||
types: [ 'songs' ],
|
|
||||||
};
|
|
||||||
// TODO: Make storefront adjustable
|
|
||||||
const result = await this.musicKit.api.music( '/v1/catalog/ch/search', queryParameters );
|
|
||||||
console.log( result );
|
|
||||||
} )();
|
|
||||||
},
|
|
||||||
connectToNotifier() {
|
|
||||||
let source = new EventSource( '/mainNotifier', { withCredentials: true } );
|
|
||||||
source.onmessage = ( e ) => {
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = JSON.parse( e.data );
|
|
||||||
} catch ( err ) {
|
|
||||||
data = { 'type': e.data };
|
|
||||||
}
|
|
||||||
if ( data.type === 'blur' ) {
|
|
||||||
this.isShowingWarning = true;
|
|
||||||
} else if ( data.type === 'visibility' ) {
|
|
||||||
this.isShowingWarning = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
source.onopen = () => {
|
|
||||||
this.isReconnecting = false;
|
|
||||||
console.log( 'client notifier connected successfully' );
|
|
||||||
};
|
|
||||||
|
|
||||||
let self = this;
|
|
||||||
|
|
||||||
source.addEventListener( 'error', function( e ) {
|
|
||||||
if ( e.eventPhase == EventSource.CLOSED ) source.close();
|
|
||||||
|
|
||||||
if ( e.target.readyState == EventSource.CLOSED ) {
|
|
||||||
setTimeout( () => {
|
|
||||||
if ( !self.isReconnecting ) {
|
|
||||||
console.log( 'disconnected' );
|
|
||||||
console.log( 'reconnecting...' );
|
|
||||||
self.isReconnecting = true;
|
|
||||||
self.tryReconnect();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
}
|
|
||||||
|
|
||||||
}, false );
|
|
||||||
},
|
|
||||||
tryReconnect() {
|
|
||||||
const int = setInterval( () => {
|
|
||||||
if ( !this.isReconnecting ) {
|
|
||||||
clearInterval( int );
|
|
||||||
} else {
|
|
||||||
this.connectToNotifier();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
},
|
|
||||||
dismissNotification() {
|
|
||||||
this.isShowingWarning = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
pos() {
|
|
||||||
if ( !this.isDragging ) {
|
|
||||||
this.sliderProgress = Math.ceil( this.pos / this.playingSong.duration * 1000 + 2 );
|
|
||||||
this.originalPos = Math.ceil( this.pos / this.playingSong.duration * ( document.getElementById( 'progress-slider' ).scrollWidth - 5 ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
document.addEventListener( 'keydown', ( e ) => {
|
|
||||||
if ( e.key === ' ' ) {
|
|
||||||
e.preventDefault();
|
|
||||||
if ( !this.isPlaying ) {
|
|
||||||
this.control( 'play' );
|
|
||||||
} else {
|
|
||||||
this.control( 'pause' );
|
|
||||||
}
|
|
||||||
} else if ( e.key === 'ArrowRight' ) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.control( 'next' );
|
|
||||||
} else if ( e.key === 'ArrowLeft' ) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.control( 'previous' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
if ( !window.MusicKit ) {
|
|
||||||
document.addEventListener( 'musickitloaded', () => {
|
|
||||||
self.initMusicKit();
|
|
||||||
} );
|
|
||||||
} else {
|
|
||||||
this.initMusicKit();
|
|
||||||
}
|
|
||||||
this.connectToNotifier();
|
|
||||||
fetch( '/getLocalIP' ).then( res => {
|
|
||||||
if ( res.status === 200 ) {
|
|
||||||
res.text().then( ip => {
|
|
||||||
this.localIP = ip;
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
} ).mount( '#app' );
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
.song-info {
|
|
||||||
background-color: #8e9ced;
|
|
||||||
height: 13vh;
|
|
||||||
width: 50%;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
width: 7vh;
|
|
||||||
height: 7vh;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 7vh;
|
|
||||||
margin-left: 1vh;
|
|
||||||
margin-top: 1vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info-wrapper h3 {
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 0.5vh;
|
|
||||||
margin-top: 1vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
margin-left: 5%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 3vh;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause {
|
|
||||||
font-size: 5vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inactive {
|
|
||||||
color: gray;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playback-pos-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 98%;
|
|
||||||
margin-left: 1%;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#showIP {
|
|
||||||
background-color: rgb(63, 63, 63);
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
min-height: 16vh;
|
|
||||||
padding: 2vh;
|
|
||||||
min-width: 20vw;
|
|
||||||
z-index: 10;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: 70%;
|
|
||||||
border-radius: 5px 10px 10px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#showIP h4, #showIP p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settings:hover #showIP {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#showIP::before {
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
bottom: 100%; /* At the bottom of the tooltip */
|
|
||||||
left: 0;
|
|
||||||
margin-left: 3px;
|
|
||||||
border-width: 10px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: transparent transparent rgb(63, 63, 63) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prepare to play */
|
|
||||||
|
|
||||||
.preparingToPlay {
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
position: fixed;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
animation: spin 2s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate( 0deg );
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate( 720deg );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
/*
|
|
||||||
* MusicPlayerV2 - style.css
|
|
||||||
*
|
|
||||||
* Created by Janis Hutz 11/14/2023, Licensed under the GPL V3 License
|
|
||||||
* https://janishutz.com, development@janishutz.com
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
body, html {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: rgb(49, 49, 49);
|
|
||||||
font-family: sans-serif;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Start page style */
|
|
||||||
.start-page {
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-wrapper {
|
|
||||||
height: 50vh;
|
|
||||||
width: 50vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#logo-main {
|
|
||||||
height: 50vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#apple-music-logo {
|
|
||||||
height: 10vh;
|
|
||||||
position: relative;
|
|
||||||
bottom: 10vh;
|
|
||||||
left: 41vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
padding: 20px;
|
|
||||||
background-color: rgb(1, 1, 88);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50px;
|
|
||||||
transition: all 1s;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background-color: rgb(1, 1, 120);
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main style */
|
|
||||||
|
|
||||||
.home {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pool-wrapper {
|
|
||||||
height: 84vh;
|
|
||||||
margin-top: 16vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-bar {
|
|
||||||
top: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 8;
|
|
||||||
width: 99%;
|
|
||||||
height: 15vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
border: white 2px solid;
|
|
||||||
background-color: rgb(49, 49, 49);
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-wrapper {
|
|
||||||
width: 70vw;
|
|
||||||
margin-right: auto;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 13vh;
|
|
||||||
margin-left: 3%;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Media Pool */
|
|
||||||
|
|
||||||
.playing-symbols {
|
|
||||||
position: absolute;
|
|
||||||
left: 10%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 0;
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.6 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-symbols-wrapper {
|
|
||||||
width: 4vw;
|
|
||||||
height: 5vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-bar {
|
|
||||||
height: 60%;
|
|
||||||
background-color: white;
|
|
||||||
width: 10%;
|
|
||||||
border-radius: 50px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-1 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-2 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-3 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
animation-delay: 0.6s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes music-playing {
|
|
||||||
0% {
|
|
||||||
transform: scaleY( 1 );
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scaleY( 0.5 );
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scaleY( 1 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
animation: spin 2s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate( 0deg );
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate( 720deg );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-pool {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-songs {
|
|
||||||
height: 50vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 80%;
|
|
||||||
margin: 2px;
|
|
||||||
padding: 1vh;
|
|
||||||
border: 1px white solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list h3 {
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list .song-image {
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-icon, .pause-icon {
|
|
||||||
display: none;
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing:hover .pause-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing:hover .playing-symbols {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list:hover .song-image {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-playing:hover .play-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active-song .pause-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active-song .song-image, .active-song:hover .pause-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Slider */
|
|
||||||
|
|
||||||
.progress-slider {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #baf4c9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-slider::-webkit-progress-value {
|
|
||||||
background-color: #baf4c9;
|
|
||||||
}
|
|
||||||
|
|
||||||
#slider-knob {
|
|
||||||
height: 20px;
|
|
||||||
width: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-end;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 2;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
#slider-knob-style {
|
|
||||||
background-color: #baf4c9;
|
|
||||||
height: 15px;
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#drag-support {
|
|
||||||
display: none;
|
|
||||||
opacity: 0;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 10;
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-support-active {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-inactive {
|
|
||||||
cursor: default !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 40vw;
|
|
||||||
height: 50vh;
|
|
||||||
font-size: 2vh;
|
|
||||||
background-color: rgb(255, 0, 0);
|
|
||||||
color: white;
|
|
||||||
position: fixed;
|
|
||||||
right: 1vh;
|
|
||||||
top: 1vh;
|
|
||||||
flex-direction: column;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning h3 {
|
|
||||||
font-size: 4vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning .flash {
|
|
||||||
background-color: rgba(255, 0, 0, 0.4);
|
|
||||||
animation: flashing linear infinite 1s;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes flashing {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
.background {
|
|
||||||
position: fixed;
|
|
||||||
left: -50vw;
|
|
||||||
width: 200vw;
|
|
||||||
height: 200vw;
|
|
||||||
top: -50vw;
|
|
||||||
z-index: -1;
|
|
||||||
filter: blur(10px);
|
|
||||||
background: conic-gradient( blue, green, red, blue );
|
|
||||||
animation: gradientAnim 10s infinite linear;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat, .beat-manual {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.15 );
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat {
|
|
||||||
animation: beatAnim 0.6s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes beatAnim {
|
|
||||||
0% {
|
|
||||||
background-color: rgba( 0, 0, 0, 0.2 );
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
background-color: rgba( 0, 0, 0, 0 );
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-color: rgba( 0, 0, 0, 0.2 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradientAnim {
|
|
||||||
from {
|
|
||||||
transform: rotate( 0deg );
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate( 360deg );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/* fallback */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Material Symbols Outlined';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100 700;
|
|
||||||
src: url(/iconFont.woff2) format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-symbols-outlined {
|
|
||||||
font-family: 'Material Symbols Outlined';
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: normal;
|
|
||||||
text-transform: none;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
word-wrap: normal;
|
|
||||||
direction: ltr;
|
|
||||||
-moz-font-feature-settings: 'liga';
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 582 KiB |
@@ -1,208 +0,0 @@
|
|||||||
.material-symbols-outlined {
|
|
||||||
font-variation-settings:
|
|
||||||
'FILL' 0,
|
|
||||||
'wght' 400,
|
|
||||||
'GRAD' 0,
|
|
||||||
'opsz' 24
|
|
||||||
}
|
|
||||||
|
|
||||||
body, html {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-symbols {
|
|
||||||
position: absolute;
|
|
||||||
left: 10vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.6 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-symbols-wrapper {
|
|
||||||
width: 4vw;
|
|
||||||
height: 5vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-bar {
|
|
||||||
height: 60%;
|
|
||||||
background-color: white;
|
|
||||||
width: 10%;
|
|
||||||
border-radius: 50px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-1 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-2 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-3 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
animation-delay: 0.6s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes music-playing {
|
|
||||||
0% {
|
|
||||||
transform: scaleY( 1 );
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scaleY( 0.5 );
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scaleY( 1 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 80%;
|
|
||||||
margin: 2px;
|
|
||||||
padding: 1vh;
|
|
||||||
border: 1px white solid;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.4 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-details-wrapper {
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list .song-image {
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pause-icon {
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw !important;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-song-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 55vh;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5%;
|
|
||||||
margin-top: 0.25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-song {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-top: 1vh;
|
|
||||||
padding: 1vh;
|
|
||||||
text-align: center;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.4 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.fancy-view-song-art {
|
|
||||||
height: 30vh;
|
|
||||||
width: 30vh;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 30vh !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
background-color: rgba( 0, 0, 0, 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress, #progress::-webkit-progress-bar {
|
|
||||||
background-color: rgba(45, 28, 145);
|
|
||||||
color: rgba(45, 28, 145);
|
|
||||||
width: 30vw;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0px;
|
|
||||||
accent-color: white;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress::-moz-progress-bar {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress::-webkit-progress-value {
|
|
||||||
background-color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-selector-wrapper {
|
|
||||||
opacity: 0;
|
|
||||||
position: fixed;
|
|
||||||
right: 0.5%;
|
|
||||||
top: 0.5%;
|
|
||||||
padding: 0.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-selector-wrapper:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dancing-style {
|
|
||||||
font-size: 250%;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
position: fixed;
|
|
||||||
font-size: 12px;
|
|
||||||
transform: rotate(270deg);
|
|
||||||
left: -150px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=7">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Showcase - MusicPlayerV2</title>
|
|
||||||
<link rel="stylesheet" href="/showcase.css">
|
|
||||||
<link rel="stylesheet" href="/backgroundAnim.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="info">Designed and developed by Janis Hutz <a href="https://janishutz.com" target="_blank" style="text-decoration: none; color: white;">https://janishutz.com</a></div>
|
|
||||||
<div class="content" id="app">
|
|
||||||
<div v-if="hasLoaded" style="width: 100%">
|
|
||||||
<div class="current-song-wrapper">
|
|
||||||
<span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt">music_note</span>
|
|
||||||
<img v-else-if="playingSong.hasCoverArt && playingSong.coverArtOrigin === 'api'" :src="playingSong.coverArtURL" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
|
|
||||||
<img v-else :src="'/getSongCover?filename=' + playingSong.filename" class="fancy-view-song-art" id="current-image">
|
|
||||||
<div class="current-song">
|
|
||||||
<progress max="1000" id="progress" :value="progressBar"></progress>
|
|
||||||
<h1>{{ playingSong.title }}</h1>
|
|
||||||
<p class="dancing-style" v-if="playingSong.dancingStyle">{{ playingSong.dancingStyle }}</p>
|
|
||||||
<p>{{ playingSong.artist }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-selector-wrapper">
|
|
||||||
<select v-model="visualizationSettings" @change="setVisualization()">
|
|
||||||
<option value="mic">Microphone (Mic access required)</option>
|
|
||||||
<option value="bpm">BPM (might not be 100% accurate)</option>
|
|
||||||
<option value="off">No visualization except background</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="song-list-wrapper">
|
|
||||||
<div v-for="song in songQueue" class="song-list">
|
|
||||||
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
|
|
||||||
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="song-image">
|
|
||||||
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying ) && song.coverArtOrigin !== 'api'" :src="'/getSongCover?filename=' + song.filename" class="song-image">
|
|
||||||
<div v-if="playingSong.filename === song.filename && isPlaying" class="playing-symbols">
|
|
||||||
<div class="playing-symbols-wrapper">
|
|
||||||
<div class="playing-bar" id="bar-1"></div>
|
|
||||||
<div class="playing-bar" id="bar-2"></div>
|
|
||||||
<div class="playing-bar" id="bar-3"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="material-symbols-outlined pause-icon" v-if="!isPlaying && playingSong.filename === song.filename">pause</span>
|
|
||||||
<div class="song-details-wrapper">
|
|
||||||
<h3>{{ song.title }}</h3>
|
|
||||||
<p>{{ song.artist }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="time-until">
|
|
||||||
{{ getTimeUntil( song ) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <img :src="" alt=""> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<h1>Loading...</h1>
|
|
||||||
</div>
|
|
||||||
<div class="background" id="background">
|
|
||||||
<div class="beat"></div>
|
|
||||||
<div class="beat-manual"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
|
|
||||||
<script src="/showcase.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
// eslint-disable-next-line no-undef
|
|
||||||
const { createApp } = Vue;
|
|
||||||
|
|
||||||
createApp( {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hasLoaded: false,
|
|
||||||
songs: [],
|
|
||||||
playingSong: {},
|
|
||||||
isPlaying: false,
|
|
||||||
pos: 0,
|
|
||||||
queuePos: 0,
|
|
||||||
colourPalette: [],
|
|
||||||
progressBar: 0,
|
|
||||||
timeTracker: null,
|
|
||||||
visualizationSettings: 'mic',
|
|
||||||
micAnalyzer: null,
|
|
||||||
beatDetected: false,
|
|
||||||
colorThief: null,
|
|
||||||
lastDispatch: new Date().getTime() - 5000,
|
|
||||||
isReconnecting: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
songQueue() {
|
|
||||||
let ret = [];
|
|
||||||
let pos = 0;
|
|
||||||
for ( let song in this.songs ) {
|
|
||||||
if ( pos >= this.queuePos ) {
|
|
||||||
ret.push( this.songs[ song ] );
|
|
||||||
}
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
getTimeUntil() {
|
|
||||||
return ( song ) => {
|
|
||||||
let timeRemaining = 0;
|
|
||||||
for ( let i = this.queuePos; i < Object.keys( this.songs ).length - 1; i++ ) {
|
|
||||||
if ( this.songs[ i ] == song ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
timeRemaining += parseInt( this.songs[ i ].duration );
|
|
||||||
}
|
|
||||||
if ( this.isPlaying ) {
|
|
||||||
if ( timeRemaining === 0 ) {
|
|
||||||
return 'Currently playing';
|
|
||||||
} else {
|
|
||||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( timeRemaining === 0 ) {
|
|
||||||
return 'Plays next';
|
|
||||||
} else {
|
|
||||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - this.pos / 60 ) + 'min after starting to play';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
startTimeTracker () {
|
|
||||||
this.timeTracker = setInterval( () => {
|
|
||||||
this.pos = ( new Date().getTime() - this.playingSong.startTime ) / 1000 + this.oldPos;
|
|
||||||
this.progressBar = ( this.pos / this.playingSong.duration ) * 1000;
|
|
||||||
if ( isNaN( this.progressBar ) ) {
|
|
||||||
this.progressBar = 0;
|
|
||||||
}
|
|
||||||
}, 100 );
|
|
||||||
},
|
|
||||||
stopTimeTracker () {
|
|
||||||
clearInterval( this.timeTracker );
|
|
||||||
this.oldPos = this.pos;
|
|
||||||
},
|
|
||||||
getImageData() {
|
|
||||||
return new Promise( ( resolve, reject ) => {
|
|
||||||
if ( this.playingSong.hasCoverArt ) {
|
|
||||||
setTimeout( () => {
|
|
||||||
const img = document.getElementById( 'current-image' );
|
|
||||||
if ( img.complete ) {
|
|
||||||
resolve( this.colorThief.getPalette( img ) );
|
|
||||||
} else {
|
|
||||||
img.addEventListener( 'load', () => {
|
|
||||||
resolve( this.colorThief.getPalette( img ) );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
}, 500 );
|
|
||||||
} else {
|
|
||||||
reject( 'no image' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
connect() {
|
|
||||||
this.colorThief = new ColorThief();
|
|
||||||
let source = new EventSource( '/clientDisplayNotifier', { withCredentials: true } );
|
|
||||||
source.onmessage = ( e ) => {
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = JSON.parse( e.data );
|
|
||||||
} catch ( err ) {
|
|
||||||
data = { 'type': e.data };
|
|
||||||
}
|
|
||||||
if ( data.type === 'basics' ) {
|
|
||||||
this.isPlaying = data.data.isPlaying ?? false;
|
|
||||||
this.playingSong = data.data.playingSong ?? {};
|
|
||||||
this.songs = data.data.songQueue ?? [];
|
|
||||||
this.pos = data.data.pos ?? 0;
|
|
||||||
this.oldPos = data.data.pos ?? 0;
|
|
||||||
this.progressBar = this.pos / this.playingSong.duration * 1000;
|
|
||||||
this.queuePos = data.data.queuePos ?? 0;
|
|
||||||
this.getImageData().then( palette => {
|
|
||||||
this.colourPalette = palette;
|
|
||||||
this.handleBackground();
|
|
||||||
} ).catch( () => {
|
|
||||||
this.colourPalette = [ { 'r': 255, 'g': 0, 'b': 0 }, { 'r': 0, 'g': 255, 'b': 0 }, { 'r': 0, 'g': 0, 'b': 255 } ];
|
|
||||||
this.handleBackground();
|
|
||||||
} );
|
|
||||||
} else if ( data.type === 'pos' ) {
|
|
||||||
this.pos = data.data;
|
|
||||||
this.oldPos = data.data;
|
|
||||||
this.progressBar = data.data / this.playingSong.duration * 1000;
|
|
||||||
} else if ( data.type === 'isPlaying' ) {
|
|
||||||
this.isPlaying = data.data;
|
|
||||||
this.handleBackground();
|
|
||||||
} else if ( data.type === 'songQueue' ) {
|
|
||||||
this.songs = data.data;
|
|
||||||
} else if ( data.type === 'playingSong' ) {
|
|
||||||
this.playingSong = data.data;
|
|
||||||
this.getImageData().then( palette => {
|
|
||||||
this.colourPalette = palette;
|
|
||||||
this.handleBackground();
|
|
||||||
} ).catch( () => {
|
|
||||||
this.colourPalette = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
|
|
||||||
this.handleBackground();
|
|
||||||
} );
|
|
||||||
} else if ( data.type === 'queuePos' ) {
|
|
||||||
this.queuePos = data.data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
source.onopen = () => {
|
|
||||||
this.isReconnecting = false;
|
|
||||||
this.hasLoaded = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
let self = this;
|
|
||||||
|
|
||||||
source.addEventListener( 'error', function( e ) {
|
|
||||||
if ( e.eventPhase == EventSource.CLOSED ) source.close();
|
|
||||||
|
|
||||||
if ( e.target.readyState == EventSource.CLOSED ) {
|
|
||||||
setTimeout( () => {
|
|
||||||
if ( !self.isReconnecting ) {
|
|
||||||
console.log( 'disconnected' );
|
|
||||||
self.isReconnecting = true;
|
|
||||||
self.tryReconnect();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
}
|
|
||||||
}, false );
|
|
||||||
},
|
|
||||||
tryReconnect() {
|
|
||||||
const int = setInterval( () => {
|
|
||||||
if ( !this.isReconnecting ) {
|
|
||||||
clearInterval( int );
|
|
||||||
} else {
|
|
||||||
this.connect();
|
|
||||||
}
|
|
||||||
}, 1000 );
|
|
||||||
},
|
|
||||||
handleBackground() {
|
|
||||||
let colourDetails = [];
|
|
||||||
let colours = [];
|
|
||||||
let differentEnough = true;
|
|
||||||
if ( this.colourPalette[ 0 ] ) {
|
|
||||||
for ( let i in this.colourPalette ) {
|
|
||||||
for ( let colour in colourDetails ) {
|
|
||||||
const colourDiff = ( Math.abs( colourDetails[ colour ][ 0 ] - this.colourPalette[ i ][ 0 ] ) / 255
|
|
||||||
+ Math.abs( colourDetails[ colour ][ 1 ] - this.colourPalette[ i ][ 1 ] ) / 255
|
|
||||||
+ Math.abs( colourDetails[ colour ][ 2 ] - this.colourPalette[ i ][ 2 ] ) / 255 ) / 3 * 100;
|
|
||||||
if ( colourDiff > 15 ) {
|
|
||||||
differentEnough = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( differentEnough ) {
|
|
||||||
colourDetails.push( this.colourPalette[ i ] );
|
|
||||||
colours.push( 'rgb(' + this.colourPalette[ i ][ 0 ] + ',' + this.colourPalette[ i ][ 1 ] + ',' + this.colourPalette[ i ][ 2 ] + ')' );
|
|
||||||
}
|
|
||||||
differentEnough = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let outColours = 'conic-gradient(';
|
|
||||||
if ( colours.length < 3 ) {
|
|
||||||
for ( let i = 0; i < 3; i++ ) {
|
|
||||||
if ( colours[ i ] ) {
|
|
||||||
outColours += colours[ i ] + ',';
|
|
||||||
} else {
|
|
||||||
if ( i === 0 ) {
|
|
||||||
outColours += 'blue,';
|
|
||||||
} else if ( i === 1 ) {
|
|
||||||
outColours += 'green,';
|
|
||||||
} else if ( i === 2 ) {
|
|
||||||
outColours += 'red,';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ( colours.length < 11 ) {
|
|
||||||
for ( let i in colours ) {
|
|
||||||
outColours += colours[ i ] + ',';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for ( let i = 0; i < 10; i++ ) {
|
|
||||||
outColours += colours[ i ] + ',';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outColours += colours[ 0 ] ?? 'blue' + ')';
|
|
||||||
|
|
||||||
$( '#background' ).css( 'background', outColours );
|
|
||||||
this.setVisualization();
|
|
||||||
},
|
|
||||||
setVisualization () {
|
|
||||||
if ( Object.keys( this.playingSong ).length > 0 ) {
|
|
||||||
if ( this.visualizationSettings === 'bpm' ) {
|
|
||||||
if ( this.playingSong.bpm && this.isPlaying ) {
|
|
||||||
$( '.beat' ).show();
|
|
||||||
$( '.beat' ).css( 'animation-duration', 60 / this.playingSong.bpm );
|
|
||||||
$( '.beat' ).css( 'animation-delay', this.pos % ( 60 / this.playingSong.bpm * this.pos ) + this.playingSong.bpmOffset - ( 60 / this.playingSong.bpm * this.pos / 2 ) );
|
|
||||||
} else {
|
|
||||||
$( '.beat' ).hide();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
clearInterval( this.micAnalyzer );
|
|
||||||
} catch ( err ) {}
|
|
||||||
} else if ( this.visualizationSettings === 'off' ) {
|
|
||||||
$( '.beat' ).hide();
|
|
||||||
try {
|
|
||||||
clearInterval( this.micAnalyzer );
|
|
||||||
} catch ( err ) {}
|
|
||||||
} else if ( this.visualizationSettings === 'mic' ) {
|
|
||||||
$( '.beat-manual' ).hide();
|
|
||||||
try {
|
|
||||||
clearInterval( this.micAnalyzer );
|
|
||||||
} catch ( err ) {}
|
|
||||||
this.micAudioHandler();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log( 'not playing yet' );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
micAudioHandler () {
|
|
||||||
const audioContext = new ( window.AudioContext || window.webkitAudioContext )();
|
|
||||||
const analyser = audioContext.createAnalyser();
|
|
||||||
analyser.fftSize = 256;
|
|
||||||
const bufferLength = analyser.frequencyBinCount;
|
|
||||||
const dataArray = new Uint8Array( bufferLength );
|
|
||||||
|
|
||||||
navigator.mediaDevices.getUserMedia( { audio: true } ).then( ( stream ) => {
|
|
||||||
const mic = audioContext.createMediaStreamSource( stream );
|
|
||||||
mic.connect( analyser );
|
|
||||||
analyser.getByteFrequencyData( dataArray );
|
|
||||||
let prevSpectrum = null;
|
|
||||||
let threshold = 10; // Adjust as needed
|
|
||||||
this.beatDetected = false;
|
|
||||||
this.micAnalyzer = setInterval( () => {
|
|
||||||
analyser.getByteFrequencyData( dataArray );
|
|
||||||
// Convert the frequency data to a numeric array
|
|
||||||
const currentSpectrum = Array.from( dataArray );
|
|
||||||
|
|
||||||
if ( prevSpectrum ) {
|
|
||||||
// Calculate the spectral flux
|
|
||||||
const flux = this.calculateSpectralFlux( prevSpectrum, currentSpectrum );
|
|
||||||
|
|
||||||
if ( flux > threshold && !this.beatDetected ) {
|
|
||||||
// Beat detected
|
|
||||||
this.beatDetected = true;
|
|
||||||
this.animateBeat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prevSpectrum = currentSpectrum;
|
|
||||||
}, 20 );
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
animateBeat () {
|
|
||||||
$( '.beat-manual' ).stop();
|
|
||||||
const duration = Math.ceil( 60 / ( this.playingSong.bpm ?? 180 ) * 500 ) - 50;
|
|
||||||
$( '.beat-manual' ).fadeIn( 50 );
|
|
||||||
setTimeout( () => {
|
|
||||||
$( '.beat-manual' ).fadeOut( duration );
|
|
||||||
setTimeout( () => {
|
|
||||||
$( '.beat-manual' ).stop();
|
|
||||||
this.beatDetected = false;
|
|
||||||
}, duration );
|
|
||||||
}, 50 );
|
|
||||||
},
|
|
||||||
calculateSpectralFlux( prevSpectrum, currentSpectrum ) {
|
|
||||||
let flux = 0;
|
|
||||||
|
|
||||||
for ( let i = 0; i < prevSpectrum.length; i++ ) {
|
|
||||||
const diff = currentSpectrum[ i ] - prevSpectrum[ i ];
|
|
||||||
flux += Math.max( 0, diff );
|
|
||||||
}
|
|
||||||
|
|
||||||
return flux;
|
|
||||||
},
|
|
||||||
notifier() {
|
|
||||||
if ( parseInt( this.lastDispatch ) + 5000 < new Date().getTime() ) {
|
|
||||||
|
|
||||||
}
|
|
||||||
Notification.requestPermission();
|
|
||||||
|
|
||||||
console.warn( '[ notifier ]: Status is now enabled \n\n-> Any leaving or tampering with the website will send a notification to the host' );
|
|
||||||
// Detect if window is currently in focus
|
|
||||||
window.onblur = () => {
|
|
||||||
this.sendNotification( 'blur' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect if browser window becomes hidden (also with blur event)
|
|
||||||
document.onvisibilitychange = () => {
|
|
||||||
if ( document.visibilityState === 'hidden' ) {
|
|
||||||
this.sendNotification( 'visibility' );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
sendNotification( notification ) {
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( { 'type': notification } ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( '/clientStatusUpdate', fetchOptions ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
|
|
||||||
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
|
|
||||||
body: 'Please return to the original webpage immediately!',
|
|
||||||
requireInteraction: true,
|
|
||||||
} )
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.connect();
|
|
||||||
this.notifier();
|
|
||||||
// if ( this.visualizationSettings === 'mic' ) {
|
|
||||||
// this.micAudioHandler();
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
isPlaying( value ) {
|
|
||||||
if ( value ) {
|
|
||||||
this.startTimeTracker();
|
|
||||||
} else {
|
|
||||||
this.stopTimeTracker();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ).mount( '#app' );
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="fancy-view">
|
|
||||||
<span class="material-symbols-outlined fancy-view-song-art" v-if="!song.hasCoverArt">music_note</span>
|
|
||||||
<img v-else-if="song.hasCoverArt && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="fancy-view-song-art">
|
|
||||||
<img v-else :src="'http://localhost:8081/getSongCover?filename=' + song.filename" class="fancy-view-song-art">
|
|
||||||
<button @click="exit()" id="exit-button"><span class="material-symbols-outlined" style="font-size: 4vh;">close</span></button>
|
|
||||||
<div class="controls-wrapper">
|
|
||||||
<div class="song-info">
|
|
||||||
<h3>{{ song.title }}</h3>
|
|
||||||
<p>{{ song.artist }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<span class="material-symbols-outlined control-icon" @click="control( 'previous' )">skip_previous</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" @click="control( 'replay10' )">replay_10</span>
|
|
||||||
<span class="material-symbols-outlined control-icon play-pause" v-if="!isPlaying" @click="control( 'play' )">play_arrow</span>
|
|
||||||
<span class="material-symbols-outlined control-icon play-pause" v-else-if="isPlaying" @click="control( 'pause' )">pause</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" @click="control( 'forward10' )">forward_10</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" @click="control( 'next' )">skip_next</span>
|
|
||||||
</div>
|
|
||||||
<div class="slider-wrapper">
|
|
||||||
<sliderView :active="true" :position="playbackPos" :duration="song.duration" @pos="( p ) => { setPos( p ) }"
|
|
||||||
name="fancy" class="slider"></sliderView>
|
|
||||||
<div class="playback-pos-info">
|
|
||||||
<div style="margin-right: auto;">{{ playbackPosBeautified }}</div>
|
|
||||||
<div>{{ durationBeautified }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="shuffle-repeat-wrapper">
|
|
||||||
<span class="material-symbols-outlined control-icon" v-if="!shuffle" @click="control( 'shuffleOn' )">shuffle</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" v-else @click="control( 'shuffleOff' )">shuffle_on</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" v-if="repeatMode === 'off'" @click="control( 'repeatOne' )">repeat</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" v-else-if="repeatMode === 'one'" @click="control( 'repeatAll' )">repeat_one_on</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" v-else-if="repeatMode === 'all'" @click="control( 'repeatOff' )">repeat_on</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#exit-button {
|
|
||||||
position: fixed;
|
|
||||||
right: 1vw;
|
|
||||||
top: 1vw;
|
|
||||||
background-color: rgba( 0,0,0,0 );
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var( --primary-color );
|
|
||||||
}
|
|
||||||
|
|
||||||
.fancy-view {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: fixed;
|
|
||||||
flex-direction: column;
|
|
||||||
z-index: 20;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: var( --background-color );
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-wrapper {
|
|
||||||
position: relative;
|
|
||||||
margin-top: 40px;
|
|
||||||
width: 40vh;
|
|
||||||
margin-bottom: 20px
|
|
||||||
}
|
|
||||||
|
|
||||||
.fancy-view-song-art {
|
|
||||||
height: 40vh;
|
|
||||||
width: 40vh;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 40vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
width: 50vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 3vh;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause {
|
|
||||||
font-size: 5vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playback-pos-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 98%;
|
|
||||||
margin-left: 1%;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 17px;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shuffle-repeat-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import SliderView from './sliderView.vue';
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
control ( instruction ) {
|
|
||||||
this.$emit( 'control', instruction );
|
|
||||||
},
|
|
||||||
setPos ( pos ) {
|
|
||||||
this.$emit( 'posUpdate', pos );
|
|
||||||
},
|
|
||||||
exit() {
|
|
||||||
this.$emit( 'control', 'exitFancyView' );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
SliderView,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
song: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
playbackPos: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
playbackPosBeautified: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
durationBeautified: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
shuffle: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
isPlaying: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
repeatMode: {
|
|
||||||
type: String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,388 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="media-pool" :style="isShowingFancyView ? 'overflow: hidden;' : ''">
|
|
||||||
<div v-if="hasLoadedSongs" style="width: 100%;" class="song-list-wrapper">
|
|
||||||
<div v-for="song in songQueue" class="song-list" :class="[ isPlaying ? ( currentlyPlaying === song.filename ? 'playing': 'not-playing' ) : 'not-playing', !isPlaying && currentlyPlaying === song.filename ? 'active-song': undefined ]">
|
|
||||||
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt">music_note</span>
|
|
||||||
<img v-else-if="song.hasCoverArt && song.coverArtOrigin === 'api'" :src="song.coverArtURL" class="song-image">
|
|
||||||
<img v-else :src="'http://localhost:8081/getSongCover?filename=' + song.filename" class="song-image">
|
|
||||||
<div v-if="currentlyPlaying === song.filename && isPlaying" class="playing-symbols">
|
|
||||||
<div class="playing-symbols-wrapper">
|
|
||||||
<div class="playing-bar" id="bar-1"></div>
|
|
||||||
<div class="playing-bar" id="bar-2"></div>
|
|
||||||
<div class="playing-bar" id="bar-3"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="material-symbols-outlined play-icon" @click="play( song )">play_arrow</span>
|
|
||||||
<span class="material-symbols-outlined pause-icon" @click="pause( song )">pause</span>
|
|
||||||
<h3>{{ song.title }}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="isLoadingSongs" class="no-songs">
|
|
||||||
<h3>Loading songs...</h3>
|
|
||||||
<p>Analyzing metadata...</p>
|
|
||||||
<span class="material-symbols-outlined loading-spinner">autorenew</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="errorOccurredLoading" class="no-songs">
|
|
||||||
<h3>This directory does not exist!</h3>
|
|
||||||
<button @click="loadSongs()">Load songs</button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="no-songs">
|
|
||||||
<h3>No songs loaded</h3>
|
|
||||||
<button @click="loadSongs()">Load songs</button>
|
|
||||||
<button @click="useAppleMusic()">Use AppleMusic (opens a web-browser)</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.playing-symbols {
|
|
||||||
position: absolute;
|
|
||||||
left: 10%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 0;
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
background-color: rgba( 0, 0, 0, 0.6 );
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-symbols-wrapper {
|
|
||||||
width: 4vw;
|
|
||||||
height: 5vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing-bar {
|
|
||||||
height: 60%;
|
|
||||||
background-color: white;
|
|
||||||
width: 10%;
|
|
||||||
border-radius: 50px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-1 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-2 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar-3 {
|
|
||||||
animation: music-playing 0.9s infinite ease-in-out;
|
|
||||||
animation-delay: 0.6s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes music-playing {
|
|
||||||
0% {
|
|
||||||
transform: scaleY( 1 );
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scaleY( 0.5 );
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scaleY( 1 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
animation: spin 2s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate( 0deg );
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate( 720deg );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-pool {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-songs {
|
|
||||||
height: 50vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 80%;
|
|
||||||
margin: 2px;
|
|
||||||
padding: 1vh;
|
|
||||||
border: 1px var( --border-color ) solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list h3 {
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list .song-image {
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-icon, .pause-icon {
|
|
||||||
display: none;
|
|
||||||
width: 5vw;
|
|
||||||
height: 5vw;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 5vw;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing:hover .pause-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing:hover .playing-symbols {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-list:hover .song-image {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-playing:hover .play-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active-song .pause-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active-song .song-image, .active-song:hover .pause-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'HomeView',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hasLoadedSongs: false,
|
|
||||||
isLoadingSongs: false,
|
|
||||||
allSongs: [],
|
|
||||||
songQueue: [],
|
|
||||||
loadedDirs: [],
|
|
||||||
allowedFiletypes: [ '.mp3', '.wav' ],
|
|
||||||
currentlyPlaying: '',
|
|
||||||
isPlaying: false,
|
|
||||||
songPos: 0,
|
|
||||||
repeat: false,
|
|
||||||
isShowingFancyView: false,
|
|
||||||
errorOccurredLoading: false,
|
|
||||||
coverArtSetting: 'api',
|
|
||||||
doOverride: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
update( status ) {
|
|
||||||
if ( status.type === 'playback' ) {
|
|
||||||
this.isPlaying = status.status;
|
|
||||||
} else if ( status.type === 'next' ) {
|
|
||||||
if ( this.songPos < this.songQueue.length - 1 ) {
|
|
||||||
this.songPos += 1;
|
|
||||||
this.queueHandler( 'load' );
|
|
||||||
} else {
|
|
||||||
this.songPos = 0;
|
|
||||||
if ( this.repeat ) {
|
|
||||||
this.queueHandler( 'load' );
|
|
||||||
} else {
|
|
||||||
this.isPlaying = false;
|
|
||||||
this.currentlyPlaying = '';
|
|
||||||
this.$emit( 'com', { 'type': 'startPlayback', 'song': this.songQueue[ 0 ], 'autoplay': false } );
|
|
||||||
this.$emit( 'com', { 'type': 'pause' } );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( { 'type': 'queuePos', 'data': this.songPos } ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
} else if ( status.type === 'previous' ) {
|
|
||||||
if ( this.songPos > 0 ) {
|
|
||||||
this.songPos -= 1;
|
|
||||||
} else {
|
|
||||||
this.songPos = this.songQueue.length - 1;
|
|
||||||
}
|
|
||||||
this.queueHandler( 'load' );
|
|
||||||
} else if ( status.type === 'shuffle' ) {
|
|
||||||
this.queueHandler( 'shuffle' );
|
|
||||||
} else if ( status.type === 'shuffleOff' ) {
|
|
||||||
this.queueHandler( 'shuffleOff' );
|
|
||||||
} else if ( status.type === 'repeat' ) {
|
|
||||||
this.repeat = true;
|
|
||||||
} else if ( status.type === 'repeatOff' ) {
|
|
||||||
this.repeat = false;
|
|
||||||
} else if ( status.type === 'fancyView' ) {
|
|
||||||
this.isShowingFancyView = status.status;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
queueHandler ( command ) {
|
|
||||||
if ( command === 'load' ) {
|
|
||||||
this.play( this.songQueue[ this.songPos ] );
|
|
||||||
} else if ( command === 'shuffle' ) {
|
|
||||||
let processArray = JSON.parse( JSON.stringify( this.allSongs ) );
|
|
||||||
let newOrder = [];
|
|
||||||
for ( let i = 0; i < this.allSongs.length; i++ ) {
|
|
||||||
let randNum = Math.floor( Math.random() * this.allSongs.length );
|
|
||||||
while ( newOrder.includes( randNum ) ) {
|
|
||||||
randNum = Math.floor( Math.random() * this.allSongs.length );
|
|
||||||
}
|
|
||||||
newOrder.push( randNum );
|
|
||||||
}
|
|
||||||
this.songQueue = [];
|
|
||||||
for ( let el in newOrder ) {
|
|
||||||
this.songQueue.push( processArray[ newOrder[ el ] ] );
|
|
||||||
}
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( { 'type': 'songQueue', 'data': this.songQueue } ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
} else if ( command === 'shuffleOff' ) {
|
|
||||||
this.songQueue = JSON.parse( JSON.stringify( this.allSongs ) );
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( { 'type': 'songQueue', 'data': this.songQueue } ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loadSongs() {
|
|
||||||
this.isLoadingSongs = true;
|
|
||||||
fetch( 'http://localhost:8081/openSongs' ).then( res => {
|
|
||||||
if ( res.status === 200 ) {
|
|
||||||
res.json().then( json => {
|
|
||||||
if ( Object.keys( json ).length > 0 ) {
|
|
||||||
this.loadedDirs = json.data;
|
|
||||||
this.indexFiles();
|
|
||||||
} else {
|
|
||||||
this.isLoadingSongs = false;
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
indexFiles () {
|
|
||||||
for ( let dir in this.loadedDirs ) {
|
|
||||||
fetch( 'http://localhost:8081/indexDirs?dir=' + this.loadedDirs[ dir ] + '&coverart=' + this.coverArtSetting + '&doOverride=' + this.doOverride ).then( res => {
|
|
||||||
if ( res.status === 200 ) {
|
|
||||||
this.errorOccurredLoading = false;
|
|
||||||
res.json().then( json => {
|
|
||||||
for ( let song in json ) {
|
|
||||||
this.songQueue.push( json[ song ] );
|
|
||||||
this.allSongs.push( json[ song ] );
|
|
||||||
}
|
|
||||||
this.queueHandler();
|
|
||||||
this.isLoadingSongs = false;
|
|
||||||
this.hasLoadedSongs = true;
|
|
||||||
this.$emit( 'com', { 'type': 'songsLoaded' } );
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( { 'type': 'songQueue', 'data': this.songQueue } ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
} else if ( res.status === 404 ) {
|
|
||||||
this.isLoadingSongs = false;
|
|
||||||
this.errorOccurredLoading = true;
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
play( song ) {
|
|
||||||
if ( song.filename === this.currentlyPlaying ) {
|
|
||||||
this.$emit( 'com', { 'type': 'play', 'song': song } );
|
|
||||||
} else {
|
|
||||||
for ( let s in this.songQueue ) {
|
|
||||||
if ( this.songQueue[ s ][ 'filename' ] === song.filename ) {
|
|
||||||
this.songPos = parseInt( s );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.$emit( 'com', { 'type': 'startPlayback', 'song': song } );
|
|
||||||
}
|
|
||||||
this.currentlyPlaying = song.filename;
|
|
||||||
this.update( { 'type': 'playback', 'status': true } );
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( { 'type': 'queuePos', 'data': this.songPos } ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
pause( song ) {
|
|
||||||
this.update( { 'type': 'playback', 'status': false } );
|
|
||||||
this.$emit( 'com', { 'type': 'pause', 'song': song } );
|
|
||||||
},
|
|
||||||
useAppleMusic() {
|
|
||||||
fetch( 'http://localhost:8081/useAppleMusic' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
<!-- eslint-disable no-undef -->
|
|
||||||
<template>
|
|
||||||
<div id="notifications" @click="handleNotifications();">
|
|
||||||
<div class="message-box" :class="[ location, size ]">
|
|
||||||
<div class="message-container" :class="messageType">
|
|
||||||
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
|
|
||||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
|
|
||||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
|
|
||||||
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
|
|
||||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
|
|
||||||
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
|
|
||||||
<p class="message">{{ message }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'notifications',
|
|
||||||
props: {
|
|
||||||
location: {
|
|
||||||
type: String,
|
|
||||||
'default': 'topleft',
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
'default': 'default',
|
|
||||||
}
|
|
||||||
// Size options: small, default (default option), big, bigger, huge
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
notifications: {},
|
|
||||||
queue: [],
|
|
||||||
message: '',
|
|
||||||
messageType: 'hide',
|
|
||||||
notificationDisplayTime: 0,
|
|
||||||
notificationPriority: 'normal',
|
|
||||||
currentlyDisplayedNotificationID: 0,
|
|
||||||
currentID: { 'critical': 0, 'medium': 1000, 'low': 100000 },
|
|
||||||
displayTimeCurrentNotification: 0,
|
|
||||||
notificationScheduler: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
createNotification( message, showDuration, messageType, priority ) {
|
|
||||||
/*
|
|
||||||
Takes a notification options array that contains: message, showDuration (in seconds), messageType (ok, error, progress, info) and priority (low, normal, critical).
|
|
||||||
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
|
|
||||||
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
|
|
||||||
*/
|
|
||||||
let id = 0;
|
|
||||||
|
|
||||||
if ( priority === 'critical' ) {
|
|
||||||
this.currentID[ 'critical' ] += 1;
|
|
||||||
id = this.currentID[ 'critical' ];
|
|
||||||
} else if ( priority === 'normal' ) {
|
|
||||||
this.currentID[ 'medium' ] += 1;
|
|
||||||
id = this.currentID[ 'medium' ];
|
|
||||||
} else if ( priority === 'low' ) {
|
|
||||||
this.currentID[ 'low' ] += 1;
|
|
||||||
id = this.currentID[ 'low' ];
|
|
||||||
}
|
|
||||||
this.notifications[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': messageType, 'priority': priority, 'id': id };
|
|
||||||
this.queue.push( id );
|
|
||||||
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
|
|
||||||
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
|
|
||||||
this.handleNotifications();
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
cancelNotification ( id ) {
|
|
||||||
/*
|
|
||||||
This method deletes a notification and, in case the notification is being displayed, hides it.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
delete this.notifications[ id ];
|
|
||||||
} catch ( error ) {
|
|
||||||
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.queue.splice( this.queue.indexOf( id ), 1 );
|
|
||||||
} catch {
|
|
||||||
console.debug( 'queue empty' );
|
|
||||||
}
|
|
||||||
if ( this.currentlyDisplayedNotificationID == id ) {
|
|
||||||
this.handleNotifications();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleNotifications () {
|
|
||||||
/*
|
|
||||||
This methods should NOT be called in any other component than this one!
|
|
||||||
*/
|
|
||||||
this.displayTimeCurrentNotification = 0;
|
|
||||||
this.notificationDisplayTime = 0;
|
|
||||||
this.message = '';
|
|
||||||
this.queue.sort();
|
|
||||||
if ( this.queue.length > 0 ) {
|
|
||||||
this.message = this.notifications[ this.queue[ 0 ] ][ 'message' ];
|
|
||||||
this.messageType = this.notifications[ this.queue[ 0 ] ][ 'messageType' ];
|
|
||||||
this.priority = this.notifications[ this.queue[ 0 ] ][ 'priority' ];
|
|
||||||
this.currentlyDisplayedNotificationID = this.notifications[ this.queue[ 0 ] ][ 'id' ];
|
|
||||||
this.notificationDisplayTime = this.notifications[ this.queue[ 0 ] ][ 'showDuration' ];
|
|
||||||
delete this.notifications[ this.queue[ 0 ] ];
|
|
||||||
this.queue.reverse();
|
|
||||||
this.queue.pop();
|
|
||||||
$( '.message-box' ).css( 'z-index', 20 );
|
|
||||||
} else {
|
|
||||||
this.messageType = 'hide';
|
|
||||||
$( '.message-box' ).css( 'z-index', -1 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
window.$ = window.jQuery = require( 'jquery' );
|
|
||||||
this.notificationScheduler = setInterval( () => {
|
|
||||||
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
|
|
||||||
this.handleNotifications();
|
|
||||||
} else {
|
|
||||||
this.displayTimeCurrentNotification += 0.5;
|
|
||||||
}
|
|
||||||
}, 500 );
|
|
||||||
},
|
|
||||||
unmounted ( ) {
|
|
||||||
clearInterval( this.notificationScheduler );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.message-box {
|
|
||||||
position: fixed;
|
|
||||||
z-index: -1;
|
|
||||||
color: white;
|
|
||||||
transition: all 0.5s;
|
|
||||||
width: 95vw;
|
|
||||||
right: 2.5vw;
|
|
||||||
top: 1vh;
|
|
||||||
height: 10vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.message-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
opacity: 1;
|
|
||||||
transition: all 0.5s;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.types {
|
|
||||||
color: white;
|
|
||||||
border-radius: 100%;
|
|
||||||
margin-right: auto;
|
|
||||||
margin-left: 5%;
|
|
||||||
padding: 1.5%;
|
|
||||||
font-size: 200%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-right: 5%;
|
|
||||||
text-align: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ok {
|
|
||||||
background-color: rgb(1, 71, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: rgb(114, 1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
background-color: rgb(44, 112, 151);
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
background-color: orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
z-index: 20;
|
|
||||||
background-color: rgb(0, 0, 99);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-spinner {
|
|
||||||
animation: spin 2s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate( 0deg );
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate( 720deg );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 750px) {
|
|
||||||
|
|
||||||
.default {
|
|
||||||
height: 10vh;
|
|
||||||
width: 32vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small {
|
|
||||||
height: 7vh;
|
|
||||||
width: 27vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.big {
|
|
||||||
height: 12vh;
|
|
||||||
width: 38vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bigger {
|
|
||||||
height: 15vh;
|
|
||||||
width: 43vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.huge {
|
|
||||||
height: 20vh;
|
|
||||||
width: 50vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topleft {
|
|
||||||
top: 3vh;
|
|
||||||
left: 0.5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topright {
|
|
||||||
top: 3vh;
|
|
||||||
right: 0.5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottomright {
|
|
||||||
bottom: 3vh;
|
|
||||||
right: 0.5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottomleft {
|
|
||||||
top: 3vh;
|
|
||||||
right: 0.5vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (min-width: 1500px) {
|
|
||||||
.default {
|
|
||||||
height: 10vh;
|
|
||||||
width: 15vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small {
|
|
||||||
height: 7vh;
|
|
||||||
width: 11vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.big {
|
|
||||||
height: 12vh;
|
|
||||||
width: 17vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bigger {
|
|
||||||
height: 15vh;
|
|
||||||
width: 20vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.huge {
|
|
||||||
height: 20vh;
|
|
||||||
width: 25vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="player">
|
|
||||||
<div class="controls">
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="audioLoaded ? 'active': 'inactive'" @click="control( 'previous' )">skip_previous</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="audioLoaded ? 'active': 'inactive'" @click="control( 'replay10' )">replay_10</span>
|
|
||||||
<span class="material-symbols-outlined control-icon play-pause" v-if="!isPlaying && audioLoaded" @click="control( 'play' )">play_arrow</span>
|
|
||||||
<span class="material-symbols-outlined control-icon play-pause" v-else-if="isPlaying && audioLoaded" @click="control( 'pause' )">pause</span>
|
|
||||||
<span class="material-symbols-outlined control-icon play-pause" style="cursor: default;" v-else>play_disabled</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="audioLoaded ? 'active': 'inactive'" @click="control( 'forward10' )">forward_10</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="audioLoaded ? 'active': 'inactive'" @click="control( 'next' )" style="margin-right: 1vw;">skip_next</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-if="!isShuffleEnabled" @click="control( 'shuffleOn' )">shuffle</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-else @click="control( 'shuffleOff' )">shuffle_on</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-if="repeatMode === 'off'" @click="control( 'repeatOne' )">repeat</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-else-if="repeatMode === 'one'" @click="control( 'repeatAll' )">repeat_one_on</span>
|
|
||||||
<span class="material-symbols-outlined control-icon" :class="hasLoadedSongs ? 'active': 'inactive'" v-else-if="repeatMode === 'all'" @click="control( 'repeatOff' )">repeat_on</span>
|
|
||||||
<div class="control-icon" id="settings">
|
|
||||||
<span class="material-symbols-outlined">info</span>
|
|
||||||
<div id="showIP">
|
|
||||||
<h4>IP to connect to:</h4><br>
|
|
||||||
<p>{{ localIP }}:8081</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="song-info">
|
|
||||||
<audio v-if="audioLoaded" :src="'http://localhost:8081/getSongFile?filename=' + playingSong.filename" id="music-player"></audio>
|
|
||||||
<div class="song-info-wrapper">
|
|
||||||
<div v-if="audioLoaded" @click="showFancyView()" style="cursor: pointer;">
|
|
||||||
<span class="material-symbols-outlined image" v-if="!playingSong.hasCoverArt">music_note</span>
|
|
||||||
<img v-else-if="playingSong.hasCoverArt && playingSong.coverArtOrigin === 'api'" :src="playingSong.coverArtURL" class="image">
|
|
||||||
<img v-else :src="'http://localhost:8081/getSongCover?filename=' + playingSong.filename" class="image">
|
|
||||||
</div>
|
|
||||||
<span class="material-symbols-outlined image" v-else>music_note</span>
|
|
||||||
<div class="name">
|
|
||||||
<h3>{{ playingSong.title ?? 'No song selected' }}</h3>
|
|
||||||
<p>{{ playingSong.artist }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="image"></div>
|
|
||||||
</div>
|
|
||||||
<div class="playback-pos-info">
|
|
||||||
<div style="margin-right: auto;">{{ playbackPosBeautified }}</div>
|
|
||||||
<div @click="toggleShowMode()" style="cursor: pointer;">{{ durationBeautified }}</div>
|
|
||||||
</div>
|
|
||||||
<sliderView :active="audioLoaded" :position="playbackPos" :duration="playingSong.duration" @pos="( p ) => { setPos( p ) }"
|
|
||||||
name="player"></sliderView>
|
|
||||||
</div>
|
|
||||||
<FancyView v-if="isShowingFancyView" :song="playingSong" @control="instruction => { control( instruction ) }" :isPlaying="isPlaying"
|
|
||||||
:shuffle="isShuffleEnabled" :repeatMode="repeatMode" :durationBeautified="durationBeautified"
|
|
||||||
:playbackPos="playbackPos" :playbackPosBeautified="playbackPosBeautified"
|
|
||||||
@posUpdate="pos => { setPos( pos ) }"></FancyView>
|
|
||||||
<Notifications ref="notifications" size="bigger" location="topright"></Notifications>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.song-info {
|
|
||||||
background-color: #8e9ced;
|
|
||||||
height: 13vh;
|
|
||||||
width: 50%;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
width: 7vh;
|
|
||||||
height: 7vh;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
font-size: 7vh;
|
|
||||||
margin-left: 1vh;
|
|
||||||
margin-top: 1vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-info-wrapper h3 {
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 0.5vh;
|
|
||||||
margin-top: 1vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
margin-left: 5%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 3vh;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause {
|
|
||||||
font-size: 5vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inactive {
|
|
||||||
color: gray;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playback-pos-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 98%;
|
|
||||||
margin-left: 1%;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#showIP {
|
|
||||||
background-color: rgb(63, 63, 63);
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
min-height: 16vh;
|
|
||||||
padding: 2vh;
|
|
||||||
min-width: 20vw;
|
|
||||||
z-index: 10;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: 70%;
|
|
||||||
border-radius: 5px 10px 10px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#showIP h4, #showIP p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settings:hover #showIP {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#showIP::before {
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
bottom: 100%; /* At the bottom of the tooltip */
|
|
||||||
left: 0;
|
|
||||||
margin-left: 3px;
|
|
||||||
border-width: 10px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: transparent transparent rgb(63, 63, 63) transparent;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import FancyView from './fancyView.vue';
|
|
||||||
import Notifications from './notifications.vue';
|
|
||||||
import SliderView from './sliderView.vue';
|
|
||||||
import { guess } from 'web-audio-beat-detector';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
playingSong: {},
|
|
||||||
audioLoaded: false,
|
|
||||||
isPlaying: false,
|
|
||||||
isShuffleEnabled: false,
|
|
||||||
repeatMode: 'off',
|
|
||||||
progressTracker: null,
|
|
||||||
playbackPos: 0,
|
|
||||||
playbackPosBeautified: '00:00',
|
|
||||||
durationBeautified: '--:--',
|
|
||||||
hasLoadedSongs: false,
|
|
||||||
isShowingFancyView: false,
|
|
||||||
notifier: null,
|
|
||||||
isShowingRemainingTime: false,
|
|
||||||
localIP: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
SliderView,
|
|
||||||
FancyView,
|
|
||||||
Notifications,
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
play( song, autoplay, doCrossFade = false ) {
|
|
||||||
this.playingSong = song;
|
|
||||||
this.audioLoaded = true;
|
|
||||||
this.init( doCrossFade, autoplay, song.filename );
|
|
||||||
},
|
|
||||||
// TODO: Make function that connects to status service and add various warnings.
|
|
||||||
init( doCrossFade, autoplay, filename ) {
|
|
||||||
this.control( 'reset' );
|
|
||||||
// TODO: make it support cross-fade
|
|
||||||
setTimeout( () => {
|
|
||||||
if ( autoplay ) {
|
|
||||||
this.control( 'play' );
|
|
||||||
this.isPlaying = true;
|
|
||||||
this.sendUpdate( 'isPlaying' );
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
}
|
|
||||||
this.sendUpdate( 'playingSong' );
|
|
||||||
const minuteCount = Math.floor( this.playingSong.duration / 60 );
|
|
||||||
this.durationBeautified = minuteCount + ':';
|
|
||||||
if ( ( '' + minuteCount ).length === 1 ) {
|
|
||||||
this.durationBeautified = '0' + minuteCount + ':';
|
|
||||||
}
|
|
||||||
const secondCount = Math.floor( this.playingSong.duration - minuteCount * 60 );
|
|
||||||
if ( ( '' + secondCount ).length === 1 ) {
|
|
||||||
this.durationBeautified += '0' + secondCount;
|
|
||||||
} else {
|
|
||||||
this.durationBeautified += secondCount;
|
|
||||||
}
|
|
||||||
let musicPlayer = document.getElementById( 'music-player' );
|
|
||||||
musicPlayer.onended = () => {
|
|
||||||
if ( musicPlayer.currentTime >= this.playingSong.duration - 1 ) {
|
|
||||||
if ( this.repeatMode !== 'one' ) {
|
|
||||||
this.control( 'next' );
|
|
||||||
} else {
|
|
||||||
musicPlayer.currentTime = 0;
|
|
||||||
this.control( 'play' );
|
|
||||||
this.playbackPos = musicPlayer.currentTime;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( !this.playingSong.bpm ) {
|
|
||||||
const audioContext = new AudioContext();
|
|
||||||
fetch( 'http://localhost:8081/getSongFile?filename=' + filename ).then( res => {
|
|
||||||
res.arrayBuffer().then( buf => {
|
|
||||||
audioContext.decodeAudioData( buf, audioBuffer => {
|
|
||||||
guess( audioBuffer ).then( ( data ) => {
|
|
||||||
this.playingSong.bpm = data.bpm;
|
|
||||||
this.playingSong.accurateTempo = data.tempo;
|
|
||||||
this.playingSong.bpmOffset = data.offset;
|
|
||||||
this.sendUpdate( 'playingSong' );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
}, 300 );
|
|
||||||
},
|
|
||||||
toggleShowMode() {
|
|
||||||
this.isShowingRemainingTime = !this.isShowingRemainingTime;
|
|
||||||
if ( !this.isShowingRemainingTime ) {
|
|
||||||
const minuteCounts = Math.floor( this.playingSong.duration / 60 );
|
|
||||||
this.durationBeautified = String( minuteCounts ) + ':';
|
|
||||||
if ( ( '' + minuteCounts ).length === 1 ) {
|
|
||||||
this.durationBeautified = '0' + minuteCounts + ':';
|
|
||||||
}
|
|
||||||
const secondCounts = Math.floor( this.playingSong.duration - minuteCounts * 60 );
|
|
||||||
if ( ( '' + secondCounts ).length === 1 ) {
|
|
||||||
this.durationBeautified += '0' + secondCounts;
|
|
||||||
} else {
|
|
||||||
this.durationBeautified += secondCounts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendUpdate( update ) {
|
|
||||||
let data = {};
|
|
||||||
if ( update === 'pos' ) {
|
|
||||||
data = this.playbackPos;
|
|
||||||
} else if ( update === 'playingSong' ) {
|
|
||||||
data = this.playingSong;
|
|
||||||
} else if ( update === 'isPlaying' ) {
|
|
||||||
data = this.isPlaying;
|
|
||||||
}
|
|
||||||
let fetchOptions = {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify( { 'type': update, 'data': data } ),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'charset': 'utf-8'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetch( 'http://localhost:8081/statusUpdate', fetchOptions ).catch( err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
control( action ) {
|
|
||||||
// https://css-tricks.com/lets-create-a-custom-audio-player/
|
|
||||||
let musicPlayer = document.getElementById( 'music-player' );
|
|
||||||
if ( musicPlayer ) {
|
|
||||||
if ( action === 'play' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'playback', 'status': true } );
|
|
||||||
musicPlayer.play();
|
|
||||||
this.isPlaying = true;
|
|
||||||
this.progressTracker = setInterval( () => {
|
|
||||||
this.playbackPos = musicPlayer.currentTime;
|
|
||||||
const minuteCount = Math.floor( this.playbackPos / 60 );
|
|
||||||
this.playbackPosBeautified = minuteCount + ':';
|
|
||||||
if ( ( '' + minuteCount ).length === 1 ) {
|
|
||||||
this.playbackPosBeautified = '0' + minuteCount + ':';
|
|
||||||
}
|
|
||||||
const secondCount = Math.floor( this.playbackPos - minuteCount * 60 );
|
|
||||||
if ( ( '' + secondCount ).length === 1 ) {
|
|
||||||
this.playbackPosBeautified += '0' + secondCount;
|
|
||||||
} else {
|
|
||||||
this.playbackPosBeautified += secondCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( this.isShowingRemainingTime ) {
|
|
||||||
const minuteCounts = Math.floor( ( this.playingSong.duration - this.playbackPos ) / 60 );
|
|
||||||
this.durationBeautified = '-' + String( minuteCounts ) + ':';
|
|
||||||
if ( ( '' + minuteCounts ).length === 1 ) {
|
|
||||||
this.durationBeautified = '-0' + minuteCounts + ':';
|
|
||||||
}
|
|
||||||
const secondCounts = Math.floor( ( this.playingSong.duration - this.playbackPos ) - minuteCounts * 60 );
|
|
||||||
if ( ( '' + secondCounts ).length === 1 ) {
|
|
||||||
this.durationBeautified += '0' + secondCounts;
|
|
||||||
} else {
|
|
||||||
this.durationBeautified += secondCounts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 20 );
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
this.sendUpdate( 'isPlaying' );
|
|
||||||
} else if ( action === 'pause' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'playback', 'status': false } );
|
|
||||||
musicPlayer.pause();
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
try {
|
|
||||||
clearInterval( this.progressTracker );
|
|
||||||
clearInterval( this.notifier );
|
|
||||||
} catch ( err ) {};
|
|
||||||
this.isPlaying = false;
|
|
||||||
this.sendUpdate( 'isPlaying' );
|
|
||||||
} else if ( action === 'replay10' ) {
|
|
||||||
musicPlayer.currentTime = musicPlayer.currentTime > 10 ? musicPlayer.currentTime - 10 : 0;
|
|
||||||
this.playbackPos = musicPlayer.currentTime;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} else if ( action === 'forward10' ) {
|
|
||||||
if ( musicPlayer.currentTime < ( musicPlayer.duration - 10 ) ) {
|
|
||||||
musicPlayer.currentTime = musicPlayer.currentTime + 10;
|
|
||||||
this.playbackPos = musicPlayer.currentTime;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} else {
|
|
||||||
if ( this.repeatMode !== 'one' ) {
|
|
||||||
this.control( 'next' );
|
|
||||||
} else {
|
|
||||||
musicPlayer.currentTime = 0;
|
|
||||||
this.playbackPos = musicPlayer.currentTime;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ( action === 'reset' ) {
|
|
||||||
clearInterval( this.progressTracker );
|
|
||||||
this.playbackPos = 0;
|
|
||||||
musicPlayer.currentTime = 0;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} else if ( action === 'next' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'next' } );
|
|
||||||
} else if ( action === 'previous' ) {
|
|
||||||
if ( this.playbackPos > 3 ) {
|
|
||||||
this.playbackPos = 0;
|
|
||||||
musicPlayer.currentTime = 0;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
} else {
|
|
||||||
this.$emit( 'update', { 'type': 'previous' } );
|
|
||||||
}
|
|
||||||
} else if ( action === 'shuffleOff' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'shuffleOff' } );
|
|
||||||
this.isShuffleEnabled = false;
|
|
||||||
} else if ( action === 'shuffleOn' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'shuffle' } );
|
|
||||||
this.isShuffleEnabled = true;
|
|
||||||
} else if ( action === 'repeatOne' ) {
|
|
||||||
this.repeatMode = 'one';
|
|
||||||
} else if ( action === 'repeatAll' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'repeat' } );
|
|
||||||
this.repeatMode = 'all';
|
|
||||||
} else if ( action === 'repeatOff' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'repeatOff' } );
|
|
||||||
this.repeatMode = 'off';
|
|
||||||
} else if ( action === 'exitFancyView' ) {
|
|
||||||
this.isShowingFancyView = false;
|
|
||||||
this.$emit( 'update', { 'type': 'fancyView', 'status': false } );
|
|
||||||
}
|
|
||||||
} else if ( action === 'songsLoaded' ) {
|
|
||||||
this.$refs.notifications.createNotification( 'Songs loaded successfully', 5, 'ok', 'default' );
|
|
||||||
this.hasLoadedSongs = true;
|
|
||||||
} else if ( action === 'shuffleOff' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'shuffleOff' } );
|
|
||||||
this.isShuffleEnabled = false;
|
|
||||||
} else if ( action === 'shuffleOn' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'shuffle' } );
|
|
||||||
this.isShuffleEnabled = true;
|
|
||||||
} else if ( action === 'repeatOne' ) {
|
|
||||||
this.repeatMode = 'one';
|
|
||||||
} else if ( action === 'repeatAll' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'repeat' } );
|
|
||||||
this.repeatMode = 'all';
|
|
||||||
} else if ( action === 'repeatOff' ) {
|
|
||||||
this.$emit( 'update', { 'type': 'repeatOff' } );
|
|
||||||
this.repeatMode = 'off';
|
|
||||||
} else if ( action === 'exitFancyView' ) {
|
|
||||||
this.isShowingFancyView = false;
|
|
||||||
this.$emit( 'update', { 'type': 'fancyView', 'status': false } );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setPos( pos ) {
|
|
||||||
let musicPlayer = document.getElementById( 'music-player' );
|
|
||||||
this.playbackPos = pos;
|
|
||||||
musicPlayer.currentTime = pos;
|
|
||||||
this.sendUpdate( 'pos' );
|
|
||||||
},
|
|
||||||
showFancyView() {
|
|
||||||
this.$emit( 'update', { 'type': 'fancyView', 'status': true } );
|
|
||||||
this.isShowingFancyView = true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
document.addEventListener( 'keydown', ( e ) => {
|
|
||||||
if ( e.key === ' ' ) {
|
|
||||||
e.preventDefault();
|
|
||||||
if ( !this.isPlaying ) {
|
|
||||||
this.control( 'play' );
|
|
||||||
} else {
|
|
||||||
this.control( 'pause' );
|
|
||||||
}
|
|
||||||
} else if ( e.key === 'ArrowRight' ) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.control( 'next' );
|
|
||||||
} else if ( e.key === 'ArrowLeft' ) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.control( 'previous' );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
fetch( 'http://localhost:8081/getLocalIP' ).then( res => {
|
|
||||||
if ( res.status === 200 ) {
|
|
||||||
res.text().then( ip => {
|
|
||||||
this.localIP = ip;
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
} ).catch( err => {
|
|
||||||
this.localIP = 'ERROR fetching';
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||