Prepare for new sdk

This commit is contained in:
2025-09-15 11:17:17 +02:00
parent 6b9d556e57
commit 6e93cfdf2c
19 changed files with 899 additions and 938 deletions

View File

@@ -75,7 +75,7 @@ const style = {
],
'@stylistic/eol-last': [
'error',
'always'
'never'
],
'@stylistic/function-call-spacing': [
'error',
@@ -83,7 +83,9 @@ const style = {
],
'@stylistic/function-paren-newline': [
'error',
'multiline'
{
'minItems': 3
}
],
'@stylistic/function-call-argument-newline': [
'error',

View File

@@ -13,7 +13,7 @@
"@melloware/coloris": "^0.24.0",
"@rollup/plugin-inject": "^5.0.5",
"buffer": "^6.0.3",
"colorthief": "^2.2.0",
"colorthief": "^2.6.0",
"music-metadata-browser": "^2.5.10",
"musickit-typescript": "^1.2.4",
"pinia": "^2.1.7",
@@ -23,9 +23,10 @@
"vue-router": "^4.2.5"
},
"devDependencies": {
"@eslint/js": "^9.34.0",
"@eslint/js": "^9.35.0",
"@stylistic/eslint-plugin": "^5.3.1",
"@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",
@@ -33,7 +34,7 @@
"npm-run-all2": "^6.1.1",
"sass-embedded": "^1.92.0",
"typescript": "~5.3.0",
"typescript-eslint": "^8.42.0",
"typescript-eslint": "^8.43.0",
"vite": "^7.1.4",
"vue-tsc": "^2.0.29"
}
@@ -639,9 +640,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.34.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
"integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
"version": "9.35.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz",
"integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1816,6 +1817,16 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/jquery": {
"version": "3.5.33",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz",
"integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1840,18 +1851,25 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/sizzle": {
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz",
"integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz",
"integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz",
"integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/type-utils": "8.42.0",
"@typescript-eslint/utils": "8.42.0",
"@typescript-eslint/visitor-keys": "8.42.0",
"@typescript-eslint/scope-manager": "8.43.0",
"@typescript-eslint/type-utils": "8.43.0",
"@typescript-eslint/utils": "8.43.0",
"@typescript-eslint/visitor-keys": "8.43.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -1865,7 +1883,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.42.0",
"@typescript-eslint/parser": "^8.43.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@@ -1881,16 +1899,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz",
"integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz",
"integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/typescript-estree": "8.42.0",
"@typescript-eslint/visitor-keys": "8.42.0",
"@typescript-eslint/scope-manager": "8.43.0",
"@typescript-eslint/types": "8.43.0",
"@typescript-eslint/typescript-estree": "8.43.0",
"@typescript-eslint/visitor-keys": "8.43.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1906,14 +1924,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz",
"integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz",
"integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.42.0",
"@typescript-eslint/types": "^8.42.0",
"@typescript-eslint/tsconfig-utils": "^8.43.0",
"@typescript-eslint/types": "^8.43.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1928,14 +1946,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz",
"integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz",
"integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/visitor-keys": "8.42.0"
"@typescript-eslint/types": "8.43.0",
"@typescript-eslint/visitor-keys": "8.43.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1946,9 +1964,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz",
"integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz",
"integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1963,15 +1981,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz",
"integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz",
"integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/typescript-estree": "8.42.0",
"@typescript-eslint/utils": "8.42.0",
"@typescript-eslint/types": "8.43.0",
"@typescript-eslint/typescript-estree": "8.43.0",
"@typescript-eslint/utils": "8.43.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -1988,9 +2006,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz",
"integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz",
"integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2002,16 +2020,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz",
"integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz",
"integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.42.0",
"@typescript-eslint/tsconfig-utils": "8.42.0",
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/visitor-keys": "8.42.0",
"@typescript-eslint/project-service": "8.43.0",
"@typescript-eslint/tsconfig-utils": "8.43.0",
"@typescript-eslint/types": "8.43.0",
"@typescript-eslint/visitor-keys": "8.43.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -2057,16 +2075,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz",
"integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz",
"integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/typescript-estree": "8.42.0"
"@typescript-eslint/scope-manager": "8.43.0",
"@typescript-eslint/types": "8.43.0",
"@typescript-eslint/typescript-estree": "8.43.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2081,13 +2099,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz",
"integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz",
"integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/types": "8.43.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -3420,6 +3438,20 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@eslint/js": {
"version": "9.34.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
"integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
}
},
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -4014,9 +4046,9 @@
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT"
},
"node_modules/is-buffer": {
@@ -5923,9 +5955,9 @@
}
},
"node_modules/sharp/node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz",
"integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -6040,9 +6072,9 @@
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
@@ -6406,16 +6438,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.42.0.tgz",
"integrity": "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.43.0.tgz",
"integrity": "sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"@typescript-eslint/typescript-estree": "8.42.0",
"@typescript-eslint/utils": "8.42.0"
"@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.43.0",
"@typescript-eslint/typescript-estree": "8.43.0",
"@typescript-eslint/utils": "8.43.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -17,7 +17,7 @@
"@melloware/coloris": "^0.24.0",
"@rollup/plugin-inject": "^5.0.5",
"buffer": "^6.0.3",
"colorthief": "^2.2.0",
"colorthief": "^2.6.0",
"music-metadata-browser": "^2.5.10",
"musickit-typescript": "^1.2.4",
"pinia": "^2.1.7",
@@ -27,9 +27,10 @@
"vue-router": "^4.2.5"
},
"devDependencies": {
"@eslint/js": "^9.34.0",
"@eslint/js": "^9.35.0",
"@stylistic/eslint-plugin": "^5.3.1",
"@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",
@@ -37,7 +38,7 @@
"npm-run-all2": "^6.1.1",
"sass-embedded": "^1.92.0",
"typescript": "~5.3.0",
"typescript-eslint": "^8.42.0",
"typescript-eslint": "^8.43.0",
"vite": "^7.1.4",
"vue-tsc": "^2.0.29"
}

View File

@@ -1,23 +1,52 @@
<!--
* libreevent - App.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<button @click="changeTheme();" id="themeSelector" title="Toggle between light and dark mode"><span class="material-symbols-outlined" v-html="theme"></span></button>
<router-view v-slot="{ Component, route }" id="main-view">
<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"></component>
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<script setup lang="ts">
import {
ref
} from 'vue';
import {
RouterView
} from 'vue-router';
const theme = ref( 'light_mode' );
const changeTheme = () => {
if ( theme.value === 'dark_mode' ) {
document.documentElement.classList.remove( 'dark' );
document.documentElement.classList.add( 'light' );
localStorage.setItem( 'theme', 'light_mode' );
theme.value = 'light_mode';
} else if ( theme.value === 'light_mode' ) {
document.documentElement.classList.remove( 'light' );
document.documentElement.classList.add( 'dark' );
localStorage.setItem( 'theme', 'dark_mode' );
theme.value = 'dark_mode';
}
};
theme.value = localStorage.getItem( 'theme' ) ?? '';
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === 'dark_mode' ) {
document.documentElement.classList.add( 'dark' );
theme.value = 'dark_mode';
} else {
document.documentElement.classList.add( 'light' );
theme.value = 'light_mode';
}
</script>
<style>
body {
background-color: var( --background-color );
@@ -178,33 +207,3 @@
background-position: 0px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue';
import { RouterView } from 'vue-router';
const theme = ref( 'light_mode' );
const changeTheme = () => {
if ( theme.value === 'dark_mode' ) {
document.documentElement.classList.remove( 'dark' );
document.documentElement.classList.add( 'light' );
localStorage.setItem( 'theme', 'light_mode' );
theme.value = 'light_mode';
} else if ( theme.value === 'light_mode' ) {
document.documentElement.classList.remove( 'light' );
document.documentElement.classList.add( 'dark' );
localStorage.setItem( 'theme', 'dark_mode' );
theme.value = 'dark_mode';
}
}
theme.value = localStorage.getItem( 'theme' ) ?? '';
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === 'dark_mode' ) {
document.documentElement.classList.add( 'dark' );
theme.value = 'dark_mode';
} else {
document.documentElement.classList.add( 'light' );
theme.value = 'light_mode';
}
</script>

View File

@@ -1,15 +1,18 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import {
createApp
} from 'vue';
import {
createPinia
} from 'pinia';
import App from './App.vue';
import router from './router';
import App from './App.vue'
import router from './router'
const app = createApp( App );
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use( createPinia() );
app.use( router );
// localStorage.setItem( 'url', 'http://localhost:8082' );
localStorage.setItem( 'url', 'https://music-api.janishutz.com' );
app.mount('#app')
app.mount( '#app' );

View File

@@ -1,9 +1,11 @@
import ColorThief from 'colorthief';
const colorThief = new ColorThief();
const getImageData = (): Promise<number[][]> => {
return new Promise( ( resolve ) => {
const img = ( document.getElementById( 'current-image' ) as HTMLImageElement );
return new Promise( resolve => {
const img = document.getElementById( 'current-image' ) as HTMLImageElement;
if ( img.complete ) {
resolve( colorThief.getPalette( img ) );
} else {
@@ -12,32 +14,39 @@ const getImageData = (): Promise<number[][]> => {
} );
}
} );
}
};
const createBackground = () => {
return new Promise( ( resolve ) => {
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 ] ) {
@@ -61,45 +70,56 @@ const createBackground = () => {
outColours += colours[ i ] + ',';
}
}
outColours += colours[ 0 ] ?? 'blue' + ')';
resolve( outColours );
} );
} );
}
};
let callbackFun = () => {};
let callbackFun = () => {}
const subscribeToBeatUpdate = ( cb: () => void ) => {
callbackFun = cb;
micAudioHandler();
}
};
const unsubscribeFromBeatUpdate = () => {
callbackFun = () => {}
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 ) => {
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
@@ -115,25 +135,27 @@ const micAudioHandler = () => {
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,
}
};

View File

@@ -1,30 +1,33 @@
/*
* MusicPlayerV2 - notificationHandler.ts
*
* Created by Janis Hutz 06/26/2024, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
// These functions handle connections to the backend with socket.io
import { io, type Socket } from "socket.io-client";
import type { SSEMap } from "./song";
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,
'autoConnect': false,
} );
this.roomName = location.pathname.split( '/' )[ 2 ];
this.isConnected = false;
@@ -38,12 +41,17 @@ class SocketConnection {
* Create a room token and connect to
* @returns {Promise<string>}
*/
connect (): Promise<any> {
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: any } ) => {
this.socket.emit(
'join-room', this.roomName, ( res: {
'status': boolean,
'msg': string,
'data': unknown
} ) => {
if ( res.status === true ) {
this.isConnected = true;
resolve( res.data );
@@ -51,35 +59,46 @@ class SocketConnection {
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 => {
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
= 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!' );
}
console.log( '[ SSE Connection ] - '
+ new Date().toISOString() + ': Connection successfully established!' );
};
this.eventSource.onmessage = ( e ) => {
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.log( '[ SSE Connection ] - '
+ new Date().toISOString()
+ ': Reconnecting due to connection error!' );
// console.debug( e );
this.eventSource = undefined;
@@ -91,11 +110,16 @@ class SocketConnection {
}
};
} else {
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Could not connect due to error ' + res.status );
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.' );
} )
.catch( () => {
console.log( '[ SSE Connection ] - '
+ new Date().toISOString()
+ ': Could not connect due to error.' );
reject( 'ERR_ROOM_CONNECTING' );
} );
} else {
@@ -116,16 +140,23 @@ class SocketConnection {
* @param {any} data
* @returns {void}
*/
emit ( event: string, data: any ): void {
emit ( event: string, data: unknown ): void {
if ( this.isConnected ) {
if ( this.useSocket ) {
this.socket.emit( event, { 'roomName': this.roomName, 'data': data } );
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: {
'method': 'post',
'body': JSON.stringify( {
'event': event,
'roomName': this.roomName,
'data': data
} ),
'credentials': 'include',
'headers': {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
@@ -140,7 +171,7 @@ class SocketConnection {
* @param {( data: any ) => void} cb The callback function / listener function
* @returns {void}
*/
registerListener ( event: string, cb: ( data: any ) => void ): void {
registerListener ( event: string, cb: ( data: unknown ) => void ): void {
if ( this.useSocket ) {
if ( this.isConnected ) {
this.socket.on( event, cb );
@@ -171,9 +202,11 @@ class SocketConnection {
if ( this.eventSource ) {
return this.eventSource!.OPEN && this.isConnected;
}
return false;
}
}
}
export default SocketConnection;

View File

@@ -1,25 +1,39 @@
import type { SearchResult, Song, SongMove } from "./song";
import type {
SearchResult, Song, SongMove
} from './song';
interface Config {
devToken: string;
userToken: string;
'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 () {
@@ -27,8 +41,8 @@ class MusicKitJSWrapper {
this.playlist = [];
this.queue = [];
this.config = {
devToken: '',
userToken: '',
'devToken': '',
'userToken': '',
};
this.isShuffleEnabled = false;
this.repeatMode = 'off';
@@ -58,14 +72,16 @@ class MusicKitJSWrapper {
this.musicKit.authorize().then( () => {
this.isLoggedIn = true;
this.init();
} ).catch( () => {
} )
.catch( () => {
this.hasEncounteredAuthError = true;
} );
} else {
this.musicKit.authorize().then( () => {
this.isLoggedIn = true;
this.init();
} ).catch( () => {
} )
.catch( () => {
this.hasEncounteredAuthError = true;
} );
}
@@ -76,25 +92,29 @@ class MusicKitJSWrapper {
* @returns {void}
*/
init (): void {
fetch( localStorage.getItem( 'url' ) + '/getAppleMusicDevToken', { credentials: 'include' } ).then( res => {
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'
'developerToken': token,
'app': {
'name': 'MusicPlayer',
'build': '3'
},
storefrontId: 'CH',
'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;
} );
} );
@@ -107,7 +127,10 @@ class MusicKitJSWrapper {
* @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 ];
return [
this.isLoggedIn,
this.hasEncounteredAuthError
];
}
/**
@@ -119,8 +142,8 @@ class MusicKitJSWrapper {
apiGetRequest ( url: string, callback: ( data: object ) => void ): void {
if ( this.config.devToken != '' && this.config.userToken != '' ) {
fetch( url, {
method: 'GET',
headers: {
'method': 'GET',
'headers': {
'Authorization': `Bearer ${ this.config.devToken }`,
'Music-User-Token': this.config.userToken
}
@@ -128,13 +151,19 @@ class MusicKitJSWrapper {
if ( res.status === 200 ) {
res.json().then( json => {
try {
callback( { 'status': 'ok', 'data': json } );
} catch( err ) { /* empty */}
callback( {
'status': 'ok',
'data': json
} );
} catch ( err ) { /* empty */ }
} );
} else {
try {
callback( { 'status': 'error', 'error': res.status } );
} catch( err ) { /* empty */}
callback( {
'status': 'error',
'error': res.status
} );
} catch ( err ) { /* empty */ }
}
} );
} else return;
@@ -152,31 +181,38 @@ class MusicKitJSWrapper {
setPlaylistByID ( id: string ): Promise<void> {
return new Promise( ( resolve, reject ) => {
this.musicKit.setQueue( { playlist: id } ).then( () => {
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
}
'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 => {
} )
.catch( err => {
console.error( err );
reject( err );
} );
@@ -192,18 +228,23 @@ class MusicKitJSWrapper {
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( () => {
this.musicKit.setQueue( {
'song': this.playlist[ this.playingSongID ].id
} ).then( () => {
setTimeout( () => {
this.control( 'play' );
}, 500 );
} ).catch( ( err ) => {
} )
.catch( err => {
console.log( err );
} );
} else {
@@ -213,6 +254,7 @@ class MusicKitJSWrapper {
this.control( 'play' );
}, 500 );
}
return true;
} else {
return false;
@@ -226,50 +268,63 @@ class MusicKitJSWrapper {
*/
control ( action: ControlAction ): boolean {
switch ( action ) {
case "play":
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":
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":
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":
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;
}
}
@@ -283,32 +338,42 @@ class MusicKitJSWrapper {
this.audioPlayer.currentTime = 0;
}
}
return false;
}
case "next":
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":
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;
}
}
@@ -317,15 +382,22 @@ class MusicKitJSWrapper {
setShuffle ( enabled: boolean ) {
this.isShuffleEnabled = enabled;
this.queue = [];
if ( enabled ) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const d = [];
for ( const el in this.playlist ) {
d.push( parseInt( el ) );
}
this.queue = d.map( value => ( { value, sort: Math.random() } ) )
this.queue = d.map( value => ( {
value,
'sort': Math.random()
} ) )
.sort( ( a, b ) => a.sort - b.sort )
.map( ( { value } ) => value );
.map( ( {
value
} ) => value );
this.queue.splice( this.queue.indexOf( this.playingSongID ), 1 );
this.queue.push( this.playingSongID );
this.queue.reverse();
@@ -334,6 +406,7 @@ class MusicKitJSWrapper {
this.queue.push( parseInt( song ) );
}
}
// Find current song ID in queue
for ( const el in this.queue ) {
if ( this.queue[ el ] === this.playingSongID ) {
@@ -359,29 +432,37 @@ class MusicKitJSWrapper {
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;
}
@@ -435,9 +516,11 @@ class MusicKitJSWrapper {
*/
getQueue (): Song[] {
const data = [];
for ( const el in this.queue ) {
data.push( this.playlist[ this.queue[ el ] ] );
}
return data;
}
@@ -449,6 +532,7 @@ class MusicKitJSWrapper {
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;
@@ -467,17 +551,20 @@ class MusicKitJSWrapper {
// TODO: Make storefront adjustable
return new Promise( ( resolve, reject ) => {
const queryParameters = {
term: ( searchTerm ),
types: [ 'songs' ],
'term': searchTerm,
'types': [ 'songs' ],
};
this.musicKit.api.music( `v1/catalog/ch/search`, queryParameters ).then( results => {
this.musicKit.api.music( 'v1/catalog/ch/search', queryParameters ).then( results => {
resolve( results );
} ).catch( e => {
} )
.catch( e => {
console.error( e );
reject( e );
} );
} );
}
}
export default MusicKitJSWrapper;

View File

@@ -1,34 +1,41 @@
/*
* MusicPlayerV2 - notificationHandler.ts
*
* Created by Janis Hutz 06/26/2024, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
// These functions handle connections to the backend with socket.io
import { io, type Socket } from "socket.io-client"
import type { SSEMap } from "./song";
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,
'autoConnect': false,
} );
this.roomName = '';
this.roomToken = '';
@@ -50,28 +57,37 @@ class NotificationHandler {
*/
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 => {
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 } ) => {
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( );
} )
.catch( );
}
} );
} else if ( res.status === 409 ) {
@@ -90,9 +106,13 @@ class NotificationHandler {
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 => {
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 = new EventSource( localStorage.getItem( 'url' ) + '/socket/connection?room=' + this.roomName, {
'withCredentials': true
} );
this.eventSource.onopen = () => {
this.isConnected = true;
@@ -100,16 +120,17 @@ class NotificationHandler {
this.reconnectRetryCount = 0;
console.log( '[ SSE Connection ] - ' + new Date().toISOString() + ': Connection successfully established!' );
resolve();
}
};
this.eventSource.onmessage = ( e ) => {
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 ) => {
this.eventSource.onerror = e => {
if ( this.isConnected ) {
this.isConnected = false;
this.eventSource?.close();
@@ -131,7 +152,8 @@ class NotificationHandler {
} else {
reject( 'ERR_ROOM_CONNECTING_STATUS_CODE' );
}
} ).catch( () => {
} )
.catch( () => {
if ( !this.connectionWasSuccessful ) {
reject( 'ERR_ROOM_CONNECTING' );
} else {
@@ -169,9 +191,14 @@ class NotificationHandler {
emit ( event: string, data: any ): void {
if ( this.isConnected ) {
if ( this.useSocket ) {
this.socket.emit( event, { 'roomToken': this.roomToken, 'roomName': this.roomName, 'data': data } );
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 );
@@ -189,10 +216,15 @@ class NotificationHandler {
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: {
'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'
}
@@ -222,22 +254,32 @@ class NotificationHandler {
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.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: {
'method': 'post',
'body': JSON.stringify( {
'roomName': this.roomName,
'roomToken': this.roomToken
} ),
'credentials': 'include',
'headers': {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
@@ -247,8 +289,10 @@ class NotificationHandler {
} else {
alert( 'Unable to delete the room you were just in. The name will be blocked until the next server restart!' );
}
return;
} ).catch( () => {
} )
.catch( () => {
return;
} );
}
@@ -258,6 +302,7 @@ class NotificationHandler {
getRoomName (): string {
return this.roomName;
}
}
export default NotificationHandler;

View File

@@ -4,90 +4,90 @@ export interface Song {
/**
* The ID. Either the apple music ID, or if from local disk, an ID starting in local_
*/
id: string;
'id': string;
/**
* Origin of the song
*/
origin: Origin;
'origin': Origin;
/**
* The cover image as a URL
*/
cover: string;
'cover': string;
/**
* The artist of the song
*/
artist: string;
'artist': string;
/**
* The name of the song
*/
title: string;
'title': string;
/**
* Duration of the song in milliseconds
*/
duration: number;
'duration': number;
/**
* (OPTIONAL) The genres this song belongs to. Can be displayed on the showcase screen, but requires settings there
*/
genres?: string[];
'genres'?: string[];
/**
* (OPTIONAL) This will be displayed in brackets on the showcase screens
*/
additionalInfo?: string;
'additionalInfo'?: string;
}
export interface SongTransmitted {
title: string;
artist: string;
duration: number;
cover: string;
additionalInfo?: string;
'title': string;
'artist': string;
'duration': number;
'cover': string;
'additionalInfo'?: string;
}
export interface ReadFile {
url: string;
filename: string;
'url': string;
'filename': string;
}
export interface SearchResult {
data: {
results: {
songs: {
data: AppleMusicSongData[],
href: string;
'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
'id': string,
'type': string;
'href': string;
'attributes': {
'albumName': string;
'artistName': string;
'artwork': {
'width': number,
'height': number,
'url': string
},
name: string;
genreNames: string[];
durationInMillis: number;
'name': string;
'genreNames': string[];
'durationInMillis': number;
}
}
export interface SongMove {
songID: string;
newPos: number;
'songID': string;
'newPos': number;
}
export interface SSEMap {

View File

@@ -1,23 +1,22 @@
/*
* LanguageSchoolHossegorBookingSystem - userStore.js
*
* Created by Janis Hutz 10/27/2023, Licensed under a proprietary License
* https://janishutz.com, development@janishutz.com
*
*
*/
import { defineStore } from 'pinia';
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': true, 'hasSubscribed': false, 'isUsingKeyboard': false, 'username': '', 'isFOSSVersion': false } ),
getters: {
getUserAuthenticated: ( state ) => state.isUserAuth,
getSubscriptionStatus: ( state ) => state.hasSubscribed,
'state': () => ( {
'isUserAuth': true,
'hasSubscribed': false,
'isUsingKeyboard': false,
'username': '',
'isFOSSVersion': false
} ),
'getters': {
'getUserAuthenticated': state => state.isUserAuth,
'getSubscriptionStatus': state => state.hasSubscribed,
},
actions: {
'actions': {
setUserAuth ( auth: boolean ) {
this.isUserAuth = auth;
},

View File

@@ -1,20 +1,40 @@
<template>
<div class="app-view">
<button id="logout" @click="logout()"><span class="material-symbols-outlined">logout</span></button>
<div class="loading-view" v-if="!hasFinishedLoading">
<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 class="home-view" v-else-if="hasFinishedLoading && isReady">
<libraryView class="library-view" :playlists="playlists" @selected-playlist="( id ) => { selectPlaylist( id ) }"
:is-logged-in="isLoggedIntoAppleMusic" @custom-playlist="( pl ) => selectCustomPlaylist( pl )"></libraryView>
<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>
<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 :class="'player-view' + ( isReady ? ( isShowingFullScreenPlayer ? ' full-screen-player' : '' ) : ' player-hidden' )" @player-state-change="( state ) => { handlePlayerStateChange( state ) }"
ref="player"></playerView>
<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>
@@ -22,10 +42,16 @@
<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 {
ref
} from 'vue';
import type {
ReadFile
} from '@/scripts/song';
import router from '@/router';
import { useUserStore } from '@/stores/userStore';
import {
useUserStore
} from '@/stores/userStore';
const isLoggedIntoAppleMusic = ref( false );
const isReady = ref( false );
@@ -41,7 +67,7 @@
} else {
isShowingFullScreenPlayer.value = true;
}
}
};
let loginChecker = 0;
@@ -51,7 +77,7 @@
if ( player.value.getAuth()[ 0 ] ) {
isLoggedIntoAppleMusic.value = true;
isReady.value = true;
player.value.getPlaylists( ( data ) => {
player.value.getPlaylists( data => {
playlists.value = data.data.data;
} );
clearInterval( loginChecker );
@@ -60,25 +86,27 @@
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 => {
fetch( localStorage.getItem( 'url' ) + '/checkUserStatus', {
'credentials': 'include'
} ).then( res => {
if ( res.status === 200 ) {
res.text().then( text => {
if ( text === 'ok' ) {
@@ -102,7 +130,7 @@
const logout = () => {
// location.href = 'http://localhost:8080/logout?return=' + location.href;
location.href = 'https://id.janishutz.com/logout?return=' + location.href;
}
};
</script>
<style scoped>

View File

@@ -3,13 +3,35 @@
<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>
<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 href="/" class="fancy-button" style="margin-left: 10px;" v-if="!reasonForRedirectHere">Log in</a>
<button href="/" class="fancy-button" style="margin-left: 10px;" v-if="reasonForRedirectHere" @click="logout()">Log out</button>
<a href="https://github.com/simplePCBuilding/MusicPlayerV2" class="fancy-button" style="margin-left: 10px;" target="_blank">GitHub</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>
@@ -20,7 +42,10 @@
<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>
<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>
@@ -29,7 +54,9 @@
</template>
<script setup lang="ts">
import { ref, type Ref } from 'vue';
import {
ref, type Ref
} from 'vue';
interface Reasons {
[key: string]: string;
@@ -39,12 +66,13 @@
'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>

View File

@@ -1,54 +1,60 @@
<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' : '' )" @click="login()"
style="margin-top: 5vh;" title="Sign in or sign up with janishutz.com ID" v-if="status"
>{{ isTryingToSignIn ? 'Signing you in...' : 'Login / Sign up' }}</button>
<p v-else>We are sorry, but we were unable to initialize the login services. Please reload the page if you wish to retry!</p>
<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"></notificationsModule>
<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 router from '@/router';
import { RouterLink } from 'vue-router';
import { useUserStore } from '@/stores/userStore';
import {
RouterLink
} from 'vue-router';
import {
useUserStore
} from '@/stores/userStore';
import notificationsModule from '@/components/notificationsModule.vue';
import { ref } from 'vue';
import {
ref
} from 'vue';
const notifications = ref( notificationsModule );
const isTryingToSignIn = ref( true );
interface JanishutzIDSDK {
setLoginSDKURL: ( url: string ) => undefined;
createSession: () => undefined;
verifySession: () => Promise<JHIDSessionStatus>
'setLoginSDKURL': ( url: string ) => undefined;
'createSession': () => undefined;
'verifySession': () => Promise<JHIDSessionStatus>
}
interface JHIDSessionStatus {
status: boolean;
username: string;
'status': boolean;
'username': string;
}
let sdk: JanishutzIDSDK;
const status = ref( true );
if ( typeof( JanishutzID ) !== 'undefined' ) {
sdk = JanishutzID();
sdk.setLoginSDKURL( localStorage.getItem( 'url' ) ?? '' );
} else {
setTimeout( () => {
notifications.value.createNotification( 'Unable to initialize account services!', 5, 'error' );
}, 1000 );
status.value = false;
}
const login = () => {
sdk.createSession();
}
};
const store = useUserStore();
@@ -56,15 +62,17 @@
router.push( localStorage.getItem( 'redirect' ) ?? '/app' );
localStorage.removeItem( 'redirect' );
} else {
if ( typeof( sdk ) !== 'undefined' ) {
if ( typeof sdk !== 'undefined' ) {
sdk.verifySession().then( res => {
if ( res.status ) {
store.isUserAuth = true;
store.username = res.username;
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' );

View File

@@ -1,20 +1,35 @@
<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="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 ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
<img
v-if="playlist[ playingSong ]"
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>
<h1 style="margin-bottom: 5px;">
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
</h1>
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p>
<progress max="1000" id="progress" :value="progressBar"></progress>
<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" v-bind:key="song.id" class="song-list">
<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>
@@ -32,7 +47,9 @@
<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>
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>
@@ -41,8 +58,12 @@
<script setup lang="ts">
import SocketConnection from '@/scripts/connection';
import type { Song } from '@/scripts/song';
import { computed, ref, type Ref } from 'vue';
import type {
Song
} from '@/scripts/song';
import {
computed, ref, type Ref
} from 'vue';
const isPlaying = ref( false );
const playlist: Ref<Song[]> = ref( [] );
@@ -52,6 +73,7 @@
const hasLoaded = ref( false );
const showCouldNotFindRoom = ref( false );
const playbackStart = ref( 0 );
let timeTracker = 0;
const conn = new SocketConnection();
@@ -61,18 +83,22 @@
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;
progressBar.value = ( pos.value / ( playlist.value[ playingSong.value ]
? playlist.value[ playingSong.value ].duration : 1 ) ) * 1000;
hasLoaded.value = true;
conn.registerListener( 'playlist', ( data ) => {
conn.registerListener( 'playlist', data => {
playlist.value = data;
} );
conn.registerListener( 'playback', ( data ) => {
conn.registerListener( 'playback', data => {
isPlaying.value = data;
if ( isPlaying.value ) {
startTimeTracker();
} else {
@@ -80,22 +106,24 @@
}
} );
conn.registerListener( 'playback-start', ( data ) => {
conn.registerListener( 'playback-start', data => {
playbackStart.value = data;
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
} );
conn.registerListener( 'playlist-index', ( data ) => {
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.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 => {
} )
.catch( e => {
console.error( e );
showCouldNotFindRoom.value = true;
} );
@@ -103,49 +131,56 @@
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;
} );
// TODO: Handle disconnect from updater (=> have it disconnect)
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';
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';
}
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;
}
@@ -156,11 +191,11 @@
location.reload();
}
}, 100 );
}
};
const stopTimeTracker = () => {
clearInterval( timeTracker );
}
};
document.addEventListener( 'visibilitychange', () => {
if ( !document.hidden ) {

View File

@@ -1,34 +1,66 @@
<template>
<div>
<span class="anti-tamper material-symbols-outlined" v-if="isAntiTamperEnabled" @click="secureModeInfo( 'toggle' )">lock</span>
<div class="anti-tamper-info" v-if="isShowingSecureModeInfo && isAntiTamperEnabled" @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>
<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 ]" :src="playlist[ playingSong ].cover" class="fancy-view-song-art" id="current-image" crossorigin="anonymous">
<img
v-if="playlist[ playingSong ]"
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>
<h1 style="margin-bottom: 5px;">
{{ playlist[ playingSong ] ? playlist[ playingSong ].title : 'Not playing' }}
</h1>
<p>{{ playlist[ playingSong ] ? playlist[ playingSong ].artist : '' }}</p>
<p class="additional-info" v-if="playlist[ playingSong ] ? ( playlist[ playingSong ].additionalInfo !== '' ) : false">{{ playlist[ playingSong ] ? playlist[ playingSong ].additionalInfo : '' }}</p>
<progress max="1000" id="progress" :value="progressBar"></progress>
<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>
<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" v-bind:key="song.id" class="song-list">
<div v-for="song in songQueue" :key="song.id" class="song-list">
<img :src="song.cover" class="song-image">
<div v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying" class="playing-symbols">
<div
v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && 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 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">
@@ -47,10 +79,12 @@
</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>
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 class="background" id="background">
<div id="background" class="background">
<div class="beat-manual"></div>
</div>
</div>
@@ -59,8 +93,12 @@
<script setup lang="ts">
import SocketConnection from '@/scripts/connection';
import type { Song } from '@/scripts/song';
import { computed, ref, type Ref } from 'vue';
import type {
Song
} from '@/scripts/song';
import {
computed, ref, type Ref
} from 'vue';
import bizualizer from '@/scripts/bizualizer';
const isPlaying = ref( false );
@@ -71,10 +109,11 @@
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 => {
@@ -82,22 +121,29 @@
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;
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 ) => {
conn.registerListener( 'playlist', data => {
playlist.value = data;
} );
conn.registerListener( 'playback', ( data ) => {
conn.registerListener( 'playback', data => {
isPlaying.value = data;
if ( isPlaying.value ) {
startTimeTracker();
} else {
@@ -105,12 +151,12 @@
}
} );
conn.registerListener( 'playback-start', ( data ) => {
conn.registerListener( 'playback-start', data => {
playbackStart.value = data;
pos.value = ( new Date().getTime() - parseInt( data ) ) / 1000;
} );
conn.registerListener( 'playlist-index', ( data ) => {
conn.registerListener( 'playlist-index', data => {
playingSong.value = parseInt( data );
setTimeout( () => {
setBackground();
@@ -118,12 +164,13 @@
} );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
conn.registerListener( 'delete-share', ( _ ) => {
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 => {
} )
.catch( e => {
console.error( e );
showCouldNotFindRoom.value = true;
} );
@@ -131,45 +178,50 @@
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;
} );
// TODO: Handle disconnect from updater (=> have it disconnect)
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';
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';
}
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( () => {
@@ -179,21 +231,23 @@
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 );
@@ -202,30 +256,31 @@
$( '.beat-manual' ).stop();
}, duration );
}, 50 );
}
};
const handleAnimationChange = () => {
if ( visualizationSettings.value === 'mic' && isPlaying.value ) {
bizualizer.subscribeToBeatUpdate( animateBeat );
} else {
bizualizer.unsubscribeFromBeatUpdate()
}
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 = () => {
@@ -233,18 +288,19 @@
sendNotification();
}
};
}
};
const sendNotification = () => {
new Notification( 'YOU ARE UNDER SURVEILLANCE', {
body: 'Please return to the original webpage immediately!',
requireInteraction: true,
'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;
@@ -253,7 +309,7 @@
} else {
isShowingSecureModeInfo.value = false;
}
}
};
</script>
<style scoped>

View File

@@ -1,416 +0,0 @@
<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="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>
<!-- TODO: Get ColorThief either from CDN or preferably as NPM module -->
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script> -->
</div>
</template>
<script setup lang="ts">
import type { Song } from '@/scripts/song';
import { computed, ref, type Ref } from 'vue';
import { ColorThief } from 'colorthief';
const hasLoaded = ref( false );
const songs: Ref<Song[]> = ref( [] );
const playingSong = ref( 0 );
const isPlaying = ref( false );
const pos = ref( 0 );
const colourPalette: string[] = [];
const progressBar = ref( 0 );
const timeTracker = ref( 0 );
const visualizationSettings = ref( 'mic' );
const micAnalyzer = ref( 0 );
const beatDetected = ref( false );
const colorThief = new ColorThief();
const songQueue = computed( () => {
let ret = [];
let pos = 0;
for ( let song in songs.value ) {
if ( pos >= playingSong.value ) {
ret.push( songs.value[ song ] );
}
pos += 1;
}
return ret;
} );
const getTimeUntil = computed( () => {
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 ( isPlaying.value ) {
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' );
</script>

View File

@@ -3,7 +3,6 @@
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
@@ -14,6 +13,6 @@
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
"types": ["node", "jquery"]
}
}