Compare commits
4 Commits
a40fad7194
...
9598d587c6
| Author | SHA1 | Date | |
|---|---|---|---|
| 9598d587c6 | |||
| f44138efd3 | |||
| 082a52dc3e | |||
| f7c4fdc2ef |
319
package-lock.json
generated
319
package-lock.json
generated
@@ -9,7 +9,6 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.6.4",
|
||||
"wave-ui": "^3.3.0",
|
||||
@@ -17,6 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-pwa": "^0.16.0"
|
||||
}
|
||||
@@ -2451,6 +2451,15 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
@@ -2724,6 +2733,20 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@@ -2760,6 +2783,115 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||
@@ -2917,6 +3049,12 @@
|
||||
"integrity": "sha512-y4A7YfQcDGPAeSWM1IuoWzXpg9RY1nwHzHSwRtCSQFp9FgAVDgdWlFf0RbdWfLWQ2WUI+bddUgk5RgTjqRE6FQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -3360,6 +3498,15 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -3748,6 +3895,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@@ -4292,10 +4448,6 @@
|
||||
"integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nosleep.js": {
|
||||
"version": "0.12.0",
|
||||
"integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA=="
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@@ -4615,6 +4767,15 @@
|
||||
"jsesc": "bin/jsesc"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@@ -4720,6 +4881,15 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -4825,6 +4995,18 @@
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@@ -4947,6 +5129,20 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string.prototype.matchall": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
|
||||
@@ -5025,6 +5221,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz",
|
||||
@@ -5132,6 +5340,21 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
|
||||
@@ -5700,6 +5923,56 @@
|
||||
"workbox-core": "7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -5725,11 +5998,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "concurrently -n vite,server -c cyan,yellow \"npm run dev:vite\" \"npm run dev:server\"",
|
||||
"dev:vite": "vite --clearScreen false",
|
||||
"dev:server": "PORT=5173 node server.js",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "node server.js",
|
||||
@@ -12,7 +14,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.6.4",
|
||||
"wave-ui": "^3.3.0",
|
||||
@@ -20,7 +21,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-pwa": "^0.16.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
221
server.js
221
server.js
@@ -3,233 +3,32 @@ import express from 'express'
|
||||
import { WebSocketServer } from 'ws'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, join } from 'path'
|
||||
import { setupWebSocketHandler } from './src/websocket-handler.js'
|
||||
import { printServerInfo } from './src/server-utils.js'
|
||||
|
||||
// Import shared game logic
|
||||
// We need to read it as a dynamic import since it uses ES modules
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
// Inline the game state logic for the server (avoid complex ESM import from src/)
|
||||
function createInitialState() {
|
||||
return {
|
||||
order: true,
|
||||
visuForm: false,
|
||||
visuStriscia: true,
|
||||
modalitaPartita: "3/5",
|
||||
sp: {
|
||||
striscia: { home: [0], guest: [0] },
|
||||
servHome: true,
|
||||
punt: { home: 0, guest: 0 },
|
||||
set: { home: 0, guest: 0 },
|
||||
nomi: { home: "Antoniana", guest: "Guest" },
|
||||
form: {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
storicoServizio: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function checkVittoria(state) {
|
||||
const puntHome = state.sp.punt.home
|
||||
const puntGuest = state.sp.punt.guest
|
||||
const setHome = state.sp.set.home
|
||||
const setGuest = state.sp.set.guest
|
||||
const totSet = setHome + setGuest
|
||||
let isSetDecisivo = false
|
||||
if (state.modalitaPartita === "2/3") {
|
||||
isSetDecisivo = totSet >= 2
|
||||
} else {
|
||||
isSetDecisivo = totSet >= 4
|
||||
}
|
||||
const punteggioVittoria = isSetDecisivo ? 15 : 25
|
||||
if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) return true
|
||||
if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function applyAction(state, action) {
|
||||
const s = JSON.parse(JSON.stringify(state))
|
||||
switch (action.type) {
|
||||
case "incPunt": {
|
||||
const team = action.team
|
||||
if (checkVittoria(s)) break
|
||||
s.sp.storicoServizio.push({
|
||||
servHome: s.sp.servHome,
|
||||
cambioPalla: (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome),
|
||||
})
|
||||
s.sp.punt[team]++
|
||||
if (team === "home") {
|
||||
s.sp.striscia.home.push(s.sp.punt.home)
|
||||
s.sp.striscia.guest.push(" ")
|
||||
} else {
|
||||
s.sp.striscia.guest.push(s.sp.punt.guest)
|
||||
s.sp.striscia.home.push(" ")
|
||||
}
|
||||
const cambioPalla = (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome)
|
||||
if (cambioPalla) {
|
||||
s.sp.form[team].push(s.sp.form[team].shift())
|
||||
}
|
||||
s.sp.servHome = team === "home"
|
||||
break
|
||||
}
|
||||
case "decPunt": {
|
||||
if (s.sp.striscia.home.length > 1 && s.sp.storicoServizio.length > 0) {
|
||||
const tmpHome = s.sp.striscia.home.pop()
|
||||
s.sp.striscia.guest.pop()
|
||||
const statoServizio = s.sp.storicoServizio.pop()
|
||||
if (tmpHome === " ") {
|
||||
s.sp.punt.guest--
|
||||
if (statoServizio.cambioPalla) {
|
||||
s.sp.form.guest.unshift(s.sp.form.guest.pop())
|
||||
}
|
||||
} else {
|
||||
s.sp.punt.home--
|
||||
if (statoServizio.cambioPalla) {
|
||||
s.sp.form.home.unshift(s.sp.form.home.pop())
|
||||
}
|
||||
}
|
||||
s.sp.servHome = statoServizio.servHome
|
||||
}
|
||||
break
|
||||
}
|
||||
case "incSet": {
|
||||
const team = action.team
|
||||
if (s.sp.set[team] === 2) { s.sp.set[team] = 0 } else { s.sp.set[team]++ }
|
||||
break
|
||||
}
|
||||
case "cambiaPalla": {
|
||||
if (s.sp.punt.home === 0 && s.sp.punt.guest === 0) {
|
||||
s.sp.servHome = !s.sp.servHome
|
||||
}
|
||||
break
|
||||
}
|
||||
case "resetta": {
|
||||
s.visuForm = false
|
||||
s.sp.punt.home = 0
|
||||
s.sp.punt.guest = 0
|
||||
s.sp.form = {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
}
|
||||
s.sp.striscia = { home: [0], guest: [0] }
|
||||
s.sp.storicoServizio = []
|
||||
break
|
||||
}
|
||||
case "toggleFormazione": { s.visuForm = !s.visuForm; break }
|
||||
case "toggleStriscia": { s.visuStriscia = !s.visuStriscia; break }
|
||||
case "toggleOrder": { s.order = !s.order; break }
|
||||
case "setNomi": {
|
||||
if (action.home !== undefined) s.sp.nomi.home = action.home
|
||||
if (action.guest !== undefined) s.sp.nomi.guest = action.guest
|
||||
break
|
||||
}
|
||||
case "setModalita": { s.modalitaPartita = action.modalita; break }
|
||||
case "setFormazione": {
|
||||
if (action.team && action.form) {
|
||||
s.sp.form[action.team] = [...action.form]
|
||||
}
|
||||
break
|
||||
}
|
||||
case "confermaCambi": {
|
||||
const team = action.team
|
||||
const cambi = action.cambi || []
|
||||
const form = s.sp.form[team].map((val) => String(val).trim())
|
||||
const formAggiornata = [...form]
|
||||
let valid = true
|
||||
for (const cambio of cambi) {
|
||||
const cin = (cambio.in || "").trim()
|
||||
const cout = (cambio.out || "").trim()
|
||||
if (!cin || !cout) continue
|
||||
if (!/^\d+$/.test(cin) || !/^\d+$/.test(cout)) { valid = false; break }
|
||||
if (cin === cout) { valid = false; break }
|
||||
if (formAggiornata.includes(cin)) { valid = false; break }
|
||||
if (!formAggiornata.includes(cout)) { valid = false; break }
|
||||
const idx = formAggiornata.findIndex((val) => String(val).trim() === cout)
|
||||
if (idx !== -1) { formAggiornata.splice(idx, 1, cin) }
|
||||
}
|
||||
if (valid) { s.sp.form[team] = formAggiornata }
|
||||
break
|
||||
}
|
||||
default: break
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ——— Server Setup ———
|
||||
// --- Configurazione del server ---
|
||||
|
||||
const app = express()
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
// Serve the Vite build output
|
||||
// Espone i file generati dalla build di Vite.
|
||||
app.use(express.static(join(__dirname, 'dist')))
|
||||
|
||||
// SPA fallback: serve index.html for all non-file routes
|
||||
app.get('/{*splat}', (req, res) => {
|
||||
// Fallback per SPA: restituisce `index.html` per tutte le route che non puntano a file statici.
|
||||
app.get('/{*splat}', (_req, res) => {
|
||||
res.sendFile(join(__dirname, 'dist', 'index.html'))
|
||||
})
|
||||
|
||||
const server = createServer(app)
|
||||
|
||||
// WebSocket server
|
||||
// Inizializza il server WebSocket con la logica di gioco.
|
||||
const wss = new WebSocketServer({ server })
|
||||
setupWebSocketHandler(wss)
|
||||
|
||||
// Global game state
|
||||
let gameState = createInitialState()
|
||||
|
||||
// Track client roles
|
||||
const clients = new Map() // ws -> { role: 'display' | 'controller' }
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('New WebSocket connection')
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const msg = JSON.parse(data.toString())
|
||||
|
||||
if (msg.type === 'register') {
|
||||
clients.set(ws, { role: msg.role || 'display' })
|
||||
console.log(`Client registered as: ${msg.role || 'display'}`)
|
||||
// Send current state immediately
|
||||
ws.send(JSON.stringify({ type: 'state', state: gameState }))
|
||||
return
|
||||
}
|
||||
|
||||
if (msg.type === 'action') {
|
||||
// Only controllers can send actions
|
||||
const client = clients.get(ws)
|
||||
if (!client || client.role !== 'controller') {
|
||||
console.log('Action rejected: not a controller')
|
||||
return
|
||||
}
|
||||
|
||||
// Apply the action to game state
|
||||
gameState = applyAction(gameState, msg.action)
|
||||
|
||||
// Broadcast new state to ALL connected clients
|
||||
const stateMsg = JSON.stringify({ type: 'state', state: gameState })
|
||||
wss.clients.forEach((c) => {
|
||||
if (c.readyState === 1) { // WebSocket.OPEN
|
||||
c.send(stateMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error processing message:', err)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('close', () => {
|
||||
clients.delete(ws)
|
||||
console.log('Client disconnected')
|
||||
})
|
||||
})
|
||||
|
||||
// Avvia il server HTTP.
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`\n🏐 Segnapunti Server running on:`)
|
||||
console.log(` Display: http://localhost:${PORT}/`)
|
||||
console.log(` Controller: http://localhost:${PORT}/controller`)
|
||||
console.log(`\n Per accedere da altri dispositivi sulla rete locale,`)
|
||||
console.log(` usa l'IP di questo computer, es: http://192.168.1.x:${PORT}/controller\n`)
|
||||
printServerInfo(PORT)
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<section class="controller-page">
|
||||
<!-- Connection status bar -->
|
||||
<!-- Barra di stato connessione -->
|
||||
<div class="conn-bar" :class="{ connected: wsConnected }">
|
||||
<span class="dot"></span>
|
||||
{{ wsConnected ? 'Connesso' : 'Connessione...' }}
|
||||
</div>
|
||||
|
||||
<!-- Score preview -->
|
||||
<!-- Anteprima punteggio -->
|
||||
<div class="score-preview">
|
||||
<div class="team-score home-bg" @click="sendAction({ type: 'incPunt', team: 'home' })">
|
||||
<div class="team-name">{{ state.sp.nomi.home }}</div>
|
||||
@@ -22,14 +22,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Undo row -->
|
||||
<!-- Riga annulla punto -->
|
||||
<div class="undo-row">
|
||||
<button class="btn btn-undo" @click="sendAction({ type: 'decPunt' })">
|
||||
↩ ANNULLA PUNTO
|
||||
ANNULLA PUNTO
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Set buttons -->
|
||||
<!-- Pulsanti set -->
|
||||
<div class="action-row">
|
||||
<button class="btn btn-set home-bg" @click="sendAction({ type: 'incSet', team: 'home' })">
|
||||
SET {{ state.sp.nomi.home }}
|
||||
@@ -39,35 +39,35 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<!-- Controlli principali -->
|
||||
<div class="controls">
|
||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'cambiaPalla' })" :disabled="!isPunteggioZeroZero">
|
||||
🏐 Cambio Palla
|
||||
Cambio Palla
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleFormazione' })">
|
||||
{{ state.visuForm ? '🔢 Punteggio' : '📋 Formazioni' }}
|
||||
{{ state.visuForm ? 'Punteggio' : 'Formazioni' }}
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleStriscia' })">
|
||||
📊 Striscia
|
||||
Striscia
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleOrder' })">
|
||||
🔄 Inverti
|
||||
Inverti
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="speak()">
|
||||
🔊 Voce
|
||||
Voce
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="openConfig()">
|
||||
⚙️ Config
|
||||
Config
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="openCambiTeam()">
|
||||
🔀 Cambi
|
||||
Cambi
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="confirmReset = true">
|
||||
🗑️ Reset
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Reset confirmation -->
|
||||
<!-- Finestra conferma reset -->
|
||||
<div class="overlay" v-if="confirmReset" @click.self="confirmReset = false">
|
||||
<div class="dialog">
|
||||
<div class="dialog-title">Azzero punteggio?</div>
|
||||
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Config dialog -->
|
||||
<!-- Finestra configurazione -->
|
||||
<div class="overlay" v-if="showConfig" @click.self="showConfig = false">
|
||||
<div class="dialog dialog-config">
|
||||
<div class="dialog-title">Configurazione</div>
|
||||
@@ -143,7 +143,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cambi team selection -->
|
||||
<!-- Selezione squadra per cambi -->
|
||||
<div class="overlay" v-if="showCambiTeam" @click.self="showCambiTeam = false">
|
||||
<div class="dialog">
|
||||
<div class="dialog-title">Scegli squadra</div>
|
||||
@@ -154,7 +154,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cambi dialog -->
|
||||
<!-- Finestra gestione cambi -->
|
||||
<div class="overlay" v-if="showCambi" @click.self="closeCambi()">
|
||||
<div class="dialog">
|
||||
<div class="dialog-title">{{ state.sp.nomi[cambiTeam] }}: CAMBIO</div>
|
||||
@@ -181,6 +181,10 @@ export default {
|
||||
return {
|
||||
ws: null,
|
||||
wsConnected: false,
|
||||
isConnecting: false,
|
||||
reconnectTimeout: null,
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectDelay: 30000,
|
||||
confirmReset: false,
|
||||
showConfig: false,
|
||||
showCambiTeam: false,
|
||||
@@ -236,44 +240,185 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.connectWebSocket()
|
||||
|
||||
// Gestisce l'HMR di Vite evitando riconnessioni durante la ricarica a caldo.
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeUpdate', () => {
|
||||
// Annulla eventuali tentativi di riconnessione pianificati.
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
this.reconnectTimeout = null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.ws) this.ws.close()
|
||||
// Pulisce il timeout di riconnessione.
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
this.reconnectTimeout = null
|
||||
}
|
||||
|
||||
// Chiude il WebSocket con il codice di chiusura appropriato.
|
||||
if (this.ws) {
|
||||
this.ws.onclose = null // Rimuove il listener per evitare nuove riconnessioni pianificate.
|
||||
this.ws.onerror = null
|
||||
this.ws.onmessage = null
|
||||
this.ws.onopen = null
|
||||
|
||||
// Usa il codice 1000 (chiusura normale) se la connessione e aperta.
|
||||
try {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.close(1000, 'Component unmounting')
|
||||
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||
// Se la connessione e ancora in fase di apertura, chiude direttamente.
|
||||
this.ws.close()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Controller] Error closing WebSocket:', err)
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
connectWebSocket() {
|
||||
// Evita connessioni simultanee multiple.
|
||||
if (this.isConnecting) {
|
||||
console.log('[Controller] Already connecting, skipping...')
|
||||
return
|
||||
}
|
||||
|
||||
// Chiude la connessione precedente, se presente.
|
||||
if (this.ws) {
|
||||
this.ws.onclose = null
|
||||
this.ws.onerror = null
|
||||
this.ws.onmessage = null
|
||||
this.ws.onopen = null
|
||||
try {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.close(1000, 'Reconnecting')
|
||||
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||
this.ws.close()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Controller] Error closing previous WebSocket:', err)
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
this.isConnecting = true
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${location.host}`
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
} catch (err) {
|
||||
console.error('[Controller] Failed to create WebSocket:', err)
|
||||
this.isConnecting = false
|
||||
this.scheduleReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.isConnecting = false
|
||||
this.wsConnected = true
|
||||
this.ws.send(JSON.stringify({ type: 'register', role: 'controller' }))
|
||||
this.reconnectAttempts = 0
|
||||
console.log('[Controller] Connected to server')
|
||||
|
||||
// Invia la registrazione solo se la connessione e realmente aperta.
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.send(JSON.stringify({ type: 'register', role: 'controller' }))
|
||||
} catch (err) {
|
||||
console.error('[Controller] Failed to register:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
|
||||
if (msg.type === 'state') {
|
||||
this.state = msg.state
|
||||
} else if (msg.type === 'error') {
|
||||
console.error('[Controller] Server error:', msg.message)
|
||||
// Fornisce feedback di errore all'utente.
|
||||
this.showErrorFeedback(msg.message)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('WS parse error:', e)
|
||||
console.error('[Controller] Parse error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.ws.onclose = (event) => {
|
||||
this.isConnecting = false
|
||||
this.wsConnected = false
|
||||
setTimeout(() => this.connectWebSocket(), 2000)
|
||||
console.log('[Controller] Disconnected from server', event.code, event.reason)
|
||||
|
||||
// Non riconnette durante HMR (codice 1001, "going away")
|
||||
// ne in caso di chiusura pulita (codice 1000).
|
||||
if (event.code === 1000 || event.code === 1001) {
|
||||
console.log('[Controller] Clean close, not reconnecting')
|
||||
return
|
||||
}
|
||||
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
|
||||
this.ws.onerror = () => { this.wsConnected = false }
|
||||
this.ws.onerror = (err) => {
|
||||
console.error('[Controller] WebSocket error:', err)
|
||||
this.isConnecting = false
|
||||
this.wsConnected = false
|
||||
}
|
||||
},
|
||||
|
||||
scheduleReconnect() {
|
||||
// Evita pianificazioni multiple della riconnessione.
|
||||
if (this.reconnectTimeout) {
|
||||
return
|
||||
}
|
||||
|
||||
// Applica backoff esponenziale: 1s, 2s, 4s, 8s, 16s, fino a 30s.
|
||||
const delay = Math.min(
|
||||
1000 * Math.pow(2, this.reconnectAttempts),
|
||||
this.maxReconnectDelay
|
||||
)
|
||||
this.reconnectAttempts++
|
||||
|
||||
console.log(`[Controller] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`)
|
||||
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.reconnectTimeout = null
|
||||
this.connectWebSocket()
|
||||
}, delay)
|
||||
},
|
||||
|
||||
sendAction(action) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify({ type: 'action', action }))
|
||||
if (!this.wsConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn('[Controller] Cannot send action: not connected')
|
||||
this.showErrorFeedback('Non connesso al server')
|
||||
return
|
||||
}
|
||||
|
||||
// Valida l'azione prima dell'invio.
|
||||
if (!action || !action.type) {
|
||||
console.error('[Controller] Invalid action format:', action)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.ws.send(JSON.stringify({ type: 'action', action }))
|
||||
} catch (err) {
|
||||
console.error('[Controller] Failed to send action:', err)
|
||||
this.showErrorFeedback('Errore invio comando')
|
||||
}
|
||||
},
|
||||
|
||||
showErrorFeedback(message) {
|
||||
// Feedback visivo degli errori: attualmente solo log su console.
|
||||
// In futuro puo essere esteso con notifiche a comparsa (toast).
|
||||
console.error('[Controller] Error:', message)
|
||||
},
|
||||
|
||||
doReset() {
|
||||
@@ -356,7 +501,7 @@ export default {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Connection bar */
|
||||
/* Barra stato connessione */
|
||||
.conn-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -384,7 +529,7 @@ export default {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Score preview */
|
||||
/* Anteprima punteggio */
|
||||
.score-preview {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -443,7 +588,7 @@ export default {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Undo row */
|
||||
/* Riga annulla punto */
|
||||
.undo-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -462,7 +607,7 @@ export default {
|
||||
background: rgba(255,100,50,0.2);
|
||||
}
|
||||
|
||||
/* Set buttons */
|
||||
/* Pulsanti set */
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -482,7 +627,7 @@ export default {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* Controls grid */
|
||||
/* Griglia controlli */
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@@ -526,7 +671,7 @@ export default {
|
||||
background: rgba(198, 40, 40, 0.45);
|
||||
}
|
||||
|
||||
/* Overlays & Dialogs */
|
||||
/* Overlay e finestre modali */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -593,7 +738,7 @@ export default {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Form groups */
|
||||
/* Gruppi form */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -637,7 +782,7 @@ export default {
|
||||
border-color: #64b5f6;
|
||||
}
|
||||
|
||||
/* Form grid */
|
||||
/* Griglia formazione */
|
||||
.form-grid {
|
||||
background: rgba(205, 133, 63, 0.15);
|
||||
border: 2px solid rgba(255,255,255,0.15);
|
||||
@@ -655,7 +800,7 @@ export default {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Mode buttons */
|
||||
/* Pulsanti modalita */
|
||||
.mode-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -675,7 +820,7 @@ export default {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Cambi */
|
||||
/* Sezione cambi */
|
||||
.cambi-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<section class="display-page">
|
||||
<div class="campo">
|
||||
<span v-if="state.order">
|
||||
<!-- home guest -->
|
||||
<!-- Ordine visualizzazione: home / guest -->
|
||||
<div class="hea home">
|
||||
<span :style="{ 'float': 'left' }">
|
||||
{{ state.sp.nomi.home }}
|
||||
@@ -46,7 +46,7 @@
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<!-- guest home -->
|
||||
<!-- Ordine visualizzazione: guest / home -->
|
||||
<div class="hea guest">
|
||||
<span :style="{ 'float': 'left' }">
|
||||
{{ state.sp.nomi.guest }}
|
||||
@@ -104,7 +104,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connection status indicator -->
|
||||
<!-- Indicatore stato connessione -->
|
||||
<div class="connection-status" :class="{ connected: wsConnected, disconnected: !wsConnected }">
|
||||
<span class="dot"></span>
|
||||
{{ wsConnected ? '' : 'Disconnesso' }}
|
||||
@@ -120,6 +120,10 @@ export default {
|
||||
return {
|
||||
ws: null,
|
||||
wsConnected: false,
|
||||
isConnecting: false,
|
||||
reconnectTimeout: null,
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectDelay: 30000, // Ritardo massimo di riconnessione: 30 secondi
|
||||
state: {
|
||||
order: true,
|
||||
visuForm: false,
|
||||
@@ -142,14 +146,48 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.connectWebSocket()
|
||||
// Fullscreen on mobile
|
||||
// Attiva la modalita fullscreen su dispositivi mobili.
|
||||
if (this.isMobile()) {
|
||||
try { document.documentElement.requestFullscreen() } catch (e) {}
|
||||
}
|
||||
|
||||
// Gestisce l'HMR di Vite evitando riconnessioni durante la ricarica a caldo.
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeUpdate', () => {
|
||||
// Annulla eventuali tentativi di riconnessione pianificati.
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
this.reconnectTimeout = null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Pulisce il timeout di riconnessione.
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
this.reconnectTimeout = null
|
||||
}
|
||||
|
||||
// Chiude il WebSocket con il codice di chiusura appropriato.
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
this.ws.onclose = null // Rimuove il listener per evitare nuove riconnessioni pianificate.
|
||||
this.ws.onerror = null
|
||||
this.ws.onmessage = null
|
||||
this.ws.onopen = null
|
||||
|
||||
// Usa il codice 1000 (chiusura normale) se la connessione e aperta.
|
||||
try {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.close(1000, 'Component unmounting')
|
||||
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||
// Se la connessione e ancora in fase di apertura, chiude direttamente.
|
||||
this.ws.close()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Display] Error closing WebSocket:', err)
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -157,36 +195,113 @@ export default {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
},
|
||||
connectWebSocket() {
|
||||
// Evita connessioni simultanee multiple.
|
||||
if (this.isConnecting) {
|
||||
console.log('[Display] Already connecting, skipping...')
|
||||
return
|
||||
}
|
||||
|
||||
// Chiude la connessione precedente, se presente.
|
||||
if (this.ws) {
|
||||
this.ws.onclose = null
|
||||
this.ws.onerror = null
|
||||
this.ws.onmessage = null
|
||||
this.ws.onopen = null
|
||||
try {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.close(1000, 'Reconnecting')
|
||||
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||
this.ws.close()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Display] Error closing previous WebSocket:', err)
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
this.isConnecting = true
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${location.host}`
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
} catch (err) {
|
||||
console.error('[Display] Failed to create WebSocket:', err)
|
||||
this.isConnecting = false
|
||||
this.scheduleReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.isConnecting = false
|
||||
this.wsConnected = true
|
||||
// Register as display
|
||||
this.ws.send(JSON.stringify({ type: 'register', role: 'display' }))
|
||||
this.reconnectAttempts = 0
|
||||
console.log('[Display] Connected to server')
|
||||
|
||||
// Registra il client come display solo con connessione effettivamente aperta.
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.send(JSON.stringify({ type: 'register', role: 'display' }))
|
||||
} catch (err) {
|
||||
console.error('[Display] Failed to register:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
|
||||
if (msg.type === 'state') {
|
||||
this.state = msg.state
|
||||
} else if (msg.type === 'error') {
|
||||
console.error('[Display] Server error:', msg.message)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing WS message:', e)
|
||||
console.error('[Display] Error parsing message:', e)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.ws.onclose = (event) => {
|
||||
this.isConnecting = false
|
||||
this.wsConnected = false
|
||||
// Auto-reconnect after 2 seconds
|
||||
setTimeout(() => this.connectWebSocket(), 2000)
|
||||
console.log('[Display] Disconnected from server', event.code, event.reason)
|
||||
|
||||
// Non riconnette durante HMR (codice 1001, "going away")
|
||||
// ne in caso di chiusura pulita (codice 1000).
|
||||
if (event.code === 1000 || event.code === 1001) {
|
||||
console.log('[Display] Clean close, not reconnecting')
|
||||
return
|
||||
}
|
||||
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
|
||||
this.ws.onerror = () => {
|
||||
this.ws.onerror = (err) => {
|
||||
console.error('[Display] WebSocket error:', err)
|
||||
this.isConnecting = false
|
||||
this.wsConnected = false
|
||||
}
|
||||
},
|
||||
scheduleReconnect() {
|
||||
// Evita pianificazioni multiple della riconnessione.
|
||||
if (this.reconnectTimeout) {
|
||||
return
|
||||
}
|
||||
|
||||
// Applica backoff esponenziale: 1s, 2s, 4s, 8s, 16s, fino a 30s.
|
||||
const delay = Math.min(
|
||||
1000 * Math.pow(2, this.reconnectAttempts),
|
||||
this.maxReconnectDelay
|
||||
)
|
||||
this.reconnectAttempts++
|
||||
|
||||
console.log(`[Display] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`)
|
||||
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.reconnectTimeout = null
|
||||
this.connectWebSocket()
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
<script>
|
||||
import NoSleep from "nosleep.js";
|
||||
export default {
|
||||
name: "HomePage",
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
voices: null,
|
||||
diaNomi: {
|
||||
show: false,
|
||||
home: "",
|
||||
guest: "",
|
||||
},
|
||||
visuForm: false,
|
||||
visuButt: true,
|
||||
sp: {
|
||||
servHome: true,
|
||||
punt: { home: 0, guest: 0 },
|
||||
set: { home: 0, guest: 0 },
|
||||
nomi: { home: "Antoniana", guest: "Guest" },
|
||||
form: {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.voices = window.speechSynthesis.getVoices();
|
||||
if (this.isMobile()) {
|
||||
this.speak();
|
||||
var noSleep = new NoSleep();
|
||||
noSleep.enable();
|
||||
document.documentElement.requestFullscreen();
|
||||
}
|
||||
this.abilitaTastiSpeciali();
|
||||
},
|
||||
methods: {
|
||||
closeApp() {
|
||||
var win = window.open("", "_self");
|
||||
win.close();
|
||||
},
|
||||
fullScreen() {
|
||||
document.documentElement.requestFullscreen();
|
||||
},
|
||||
isMobile() {
|
||||
if (
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
resetta() {
|
||||
this.$waveui.notify("Punteggio<br />RESETTATO", "success");
|
||||
this.visuForm = false;
|
||||
this.sp.punt.home = 0;
|
||||
this.sp.punt.guest = 0;
|
||||
this.sp.form = {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
}
|
||||
},
|
||||
incSet(team) {
|
||||
if (this.sp.set[team] == 2) {
|
||||
this.sp.set[team] = 0;
|
||||
} else {
|
||||
this.sp.set[team]++;
|
||||
}
|
||||
},
|
||||
incPunt(team) {
|
||||
this.sp.punt[team]++;
|
||||
this.sp.servHome = (team == "home");
|
||||
this.sp.form[team].push(this.sp.form[team].shift());
|
||||
},
|
||||
decPunt(team) {
|
||||
// decrementa il punteggio se è > 0.
|
||||
if (this.sp.punt[team] > 0) {
|
||||
this.sp.punt[team]--;
|
||||
this.sp.form[team].unshift(this.sp.form[team].pop());
|
||||
}
|
||||
},
|
||||
speak() {
|
||||
const msg = new SpeechSynthesisUtterance();
|
||||
if (this.sp.punt.home + this.sp.punt.guest == 0) {
|
||||
msg.text = "zero a zero";
|
||||
} else if (this.sp.punt.home == this.sp.punt.guest) {
|
||||
msg.text = this.sp.punt.home + " pari";
|
||||
} else {
|
||||
if (this.sp.servHome) {
|
||||
msg.text = this.sp.punt.home + " a " + this.sp.punt.guest;
|
||||
} else {
|
||||
msg.text = this.sp.punt.guest + " a " + this.sp.punt.home;
|
||||
}
|
||||
}
|
||||
// msg.volume = 1.0; // speech volume (default: 1.0)
|
||||
// msg.pitch = 1.0; // speech pitch (default: 1.0)
|
||||
// msg.rate = 1.0; // speech rate (default: 1.0)
|
||||
msg.lang = 'it_IT'; // speech language (default: 'en-US')
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
msg.voice = voices.find(voice => voice.name === 'Google italiano'); // voice URI (default: platform-dependent)
|
||||
// msg.onboundary = function (event) {
|
||||
// console.log('Speech reached a boundary:', event.name);
|
||||
// };
|
||||
// msg.onpause = function (event) {
|
||||
// console.log('Speech paused:', event.utterance.text.substring(event.charIndex));
|
||||
// };
|
||||
window.speechSynthesis.speak(msg);
|
||||
},
|
||||
apriDialogConfig() {
|
||||
this.disabilitaTastiSpeciali();
|
||||
this.diaNomi.show = true;
|
||||
},
|
||||
disabilitaTastiSpeciali() {
|
||||
window.removeEventListener("keydown", this.funzioneTastiSpeciali);
|
||||
},
|
||||
abilitaTastiSpeciali() {
|
||||
window.addEventListener("keydown", this.funzioneTastiSpeciali);
|
||||
},
|
||||
funzioneTastiSpeciali(e) {
|
||||
e.preventDefault();
|
||||
if (e.ctrlKey && e.key == "m") {
|
||||
this.diaNomi.show = true
|
||||
} else if (e.ctrlKey && e.key == "b") {
|
||||
this.visuButt = !this.visuButt
|
||||
} else if (e.ctrlKey && e.key == "f") {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else if (e.ctrlKey && e.key == "s") {
|
||||
this.speak();
|
||||
} else if (e.ctrlKey && e.key == "z") {
|
||||
this.visuForm = !this.visuForm
|
||||
} else if (e.ctrlKey && e.key == "ArrowUp") {
|
||||
this.incPunt("home")
|
||||
} else if (e.ctrlKey && e.key == "ArrowDown") {
|
||||
this.decPunt("home")
|
||||
} else if (e.ctrlKey && e.key == "ArrowRight") {
|
||||
this.incSet("home")
|
||||
} else if (e.shiftKey && e.key == "ArrowUp") {
|
||||
this.incPunt("guest")
|
||||
} else if (e.shiftKey && e.key == "ArrowDown") {
|
||||
this.decPunt("guest")
|
||||
} else if (e.shiftKey && e.key == "ArrowRight") {
|
||||
this.incSet("guest")
|
||||
} else if (e.ctrlKey && e.key == "ArrowLeft") {
|
||||
this.sp.servHome = !this.sp.servHome
|
||||
} else { return false }
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<w-dialog v-model="diaNomi.show" :width="500" @close="abilitaTastiSpeciali()">
|
||||
<w-input v-model="sp.nomi.home" type="text" class="pa3">Home</w-input>
|
||||
<w-input v-model="sp.nomi.guest" type="text" class="pa3">Guest</w-input>
|
||||
<w-button bg-color="success" @click="diaNomi.show = false">
|
||||
Ok
|
||||
</w-button>
|
||||
</w-dialog>
|
||||
<div class="campo">
|
||||
<div class="hea home">
|
||||
<span @click="decPunt('home')" :style="{ 'float': 'left' }">
|
||||
{{ sp.nomi.home }} <img v-if="sp.servHome" src="/serv.png" width="25" />
|
||||
<span v-if="visuForm">{{ sp.punt.home }}</span>
|
||||
</span>
|
||||
<span @click="incSet('home')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.home }}</span>
|
||||
</div>
|
||||
<div class="hea guest">
|
||||
<span @click="decPunt('guest')" :style="{ 'float': 'right' }">
|
||||
<img v-if="!sp.servHome" src="/serv.png" width="25" /> {{ sp.nomi.guest }}
|
||||
<span v-if="visuForm">{{ sp.punt.guest }}</span>
|
||||
</span>
|
||||
<span @click="incSet('guest')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.guest }}</span>
|
||||
|
||||
</div>
|
||||
<span v-if="visuForm">
|
||||
<div class="col form home" @click="incPunt('home')">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
|
||||
{{ sp.form.home[x] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form guest" @click="incPunt('guest')">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
|
||||
{{ sp.form.guest[x] }}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
<div class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</div>
|
||||
<div class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</div>
|
||||
</span>
|
||||
|
||||
<div class="bot" v-if="visuButt">
|
||||
<w-flex justify-space-between class="pa2">
|
||||
<w-confirm right align-bottom v-if="isMobile()" question="CHIUDO ?" cancel="NO" confirm="SI" @confirm="closeApp">
|
||||
<img src="/exit.png" width="25" />
|
||||
</w-confirm>
|
||||
<w-button @click="apriDialogConfig()">
|
||||
<img src="/gear.png" width="25" />
|
||||
</w-button>
|
||||
<w-button @click="sp.servHome = !sp.servHome">
|
||||
<img src="/serv.png" width="25" />
|
||||
</w-button>
|
||||
<w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta">
|
||||
RESET
|
||||
</w-confirm>
|
||||
<w-button @click="visuForm = !visuForm">
|
||||
<span v-if="visuForm">PUNTEGGIO</span>
|
||||
<span v-if="!visuForm">FORMAZIONI</span>
|
||||
</w-button>
|
||||
<w-button @click="speak">
|
||||
<img src="/speaker.png" width="25" />
|
||||
</w-button>
|
||||
</w-flex>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,240 +0,0 @@
|
||||
<section class="homepage">
|
||||
<w-dialog v-model="diaNomi.show" :width="600" @close="chiudiDialogConfig()">
|
||||
<w-input v-model="sp.nomi.home" type="text" class="pa3" tabindex="1">Nome Home</w-input>
|
||||
<w-input v-model="sp.nomi.guest" type="text" class="pa3" tabindex="2">Nome Guest</w-input>
|
||||
|
||||
<w-flex justify-center align-center class="pa3">
|
||||
<span class="mr3">Modalità partita:</span>
|
||||
<w-button
|
||||
@click="modalitaPartita = '2/3'"
|
||||
:bg-color="modalitaPartita === '2/3' ? 'success' : 'grey-light4'"
|
||||
:dark="modalitaPartita === '2/3'"
|
||||
class="ma1"
|
||||
tabindex="-1">
|
||||
2/3
|
||||
</w-button>
|
||||
<w-button
|
||||
@click="modalitaPartita = '3/5'"
|
||||
:bg-color="modalitaPartita === '3/5' ? 'success' : 'grey-light4'"
|
||||
:dark="modalitaPartita === '3/5'"
|
||||
class="ma1"
|
||||
tabindex="-1">
|
||||
3/5
|
||||
</w-button>
|
||||
</w-flex>
|
||||
|
||||
<w-flex justify-space-around class="pa3">
|
||||
<div class="campo-config">
|
||||
<div class="text-bold mb3 text-center">Formazione Home</div>
|
||||
<div class="campo-pallavolo">
|
||||
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
|
||||
<w-flex justify-center class="fila-anteriore">
|
||||
<w-input v-model="sp.form.home[3]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="6"></w-input>
|
||||
<w-input v-model="sp.form.home[2]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="5"></w-input>
|
||||
<w-input v-model="sp.form.home[1]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="4"></w-input>
|
||||
</w-flex>
|
||||
<!-- Linea dei 3 metri -->
|
||||
<div class="linea-tre-metri"></div>
|
||||
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
|
||||
<w-flex justify-center class="fila-posteriore">
|
||||
<w-input v-model="sp.form.home[4]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="7"></w-input>
|
||||
<w-input v-model="sp.form.home[5]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="8"></w-input>
|
||||
<w-input v-model="sp.form.home[0]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="3"></w-input>
|
||||
</w-flex>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="campo-config">
|
||||
<div class="text-bold mb3 text-center">Formazione Guest</div>
|
||||
<div class="campo-pallavolo">
|
||||
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
|
||||
<w-flex justify-center class="fila-anteriore">
|
||||
<w-input v-model="sp.form.guest[3]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="12"></w-input>
|
||||
<w-input v-model="sp.form.guest[2]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="11"></w-input>
|
||||
<w-input v-model="sp.form.guest[1]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="10"></w-input>
|
||||
</w-flex>
|
||||
<!-- Linea dei 3 metri -->
|
||||
<div class="linea-tre-metri"></div>
|
||||
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
|
||||
<w-flex justify-center class="fila-posteriore">
|
||||
<w-input v-model="sp.form.guest[4]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="13"></w-input>
|
||||
<w-input v-model="sp.form.guest[5]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="14"></w-input>
|
||||
<w-input v-model="sp.form.guest[0]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="9"></w-input>
|
||||
</w-flex>
|
||||
</div>
|
||||
</div>
|
||||
</w-flex>
|
||||
|
||||
<w-button @click="order = !order" class="ma2" tabindex="-1">Inverti ordine</w-button>
|
||||
<w-button bg-color="success" @click="diaNomi.show = false" class="ma2" tabindex="-1">
|
||||
Ok
|
||||
</w-button>
|
||||
</w-dialog>
|
||||
<w-dialog v-model="diaCambiTeam.show" :width="420" @close="abilitaTastiSpeciali()">
|
||||
<div class="text-bold text-center mb2">Scegli squadra</div>
|
||||
<w-flex justify-center class="pa3">
|
||||
<w-button class="ma2" @click="selezionaTeamCambi('home')">{{ sp.nomi.home }}</w-button>
|
||||
<w-button class="ma2" @click="selezionaTeamCambi('guest')">{{ sp.nomi.guest }}</w-button>
|
||||
</w-flex>
|
||||
</w-dialog>
|
||||
<w-dialog v-model="diaCambi.show" :width="360" @close="chiudiDialogCambi">
|
||||
<div class="cambi-dialog">
|
||||
<div class="cambi-title">{{ sp.nomi[diaCambi.team] }}: CAMBIO</div>
|
||||
<div class="cambi-rows">
|
||||
<div class="cambi-row">
|
||||
<w-input v-model="diaCambi[diaCambi.team].cambi[0].in" type="text" class="cambi-input cambi-in"></w-input>
|
||||
<span class="cambi-arrow">→</span>
|
||||
<w-input v-model="diaCambi[diaCambi.team].cambi[0].out" type="text" class="cambi-input cambi-out"></w-input>
|
||||
</div>
|
||||
<div class="cambi-row">
|
||||
<w-input v-model="diaCambi[diaCambi.team].cambi[1].in" type="text" class="cambi-input cambi-in"></w-input>
|
||||
<span class="cambi-arrow">→</span>
|
||||
<w-input v-model="diaCambi[diaCambi.team].cambi[1].out" type="text" class="cambi-input cambi-out"></w-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<w-flex justify-end class="pa3">
|
||||
<w-button bg-color="success" :disabled="!cambiConfermabili" @click="confermaCambi">
|
||||
CONFERMA
|
||||
</w-button>
|
||||
</w-flex>
|
||||
</w-dialog>
|
||||
<div class="campo">
|
||||
|
||||
<span v-if="order">
|
||||
<!-- home guest -->
|
||||
<div class="hea home">
|
||||
<span @click="decPunt('home')" :style="{ 'float': 'left' }">
|
||||
{{ sp.nomi.home }}
|
||||
<span class="serv-slot">
|
||||
<img v-show="sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
<span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
|
||||
</span>
|
||||
<span @click="incSet('home')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.home }}</span>
|
||||
</div>
|
||||
|
||||
<div class="hea guest">
|
||||
<span @click="decPunt('guest')" :style="{ 'float': 'right' }">
|
||||
<span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
|
||||
<span class="serv-slot">
|
||||
<img v-show="!sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
{{ sp.nomi.guest }}
|
||||
</span>
|
||||
<span @click="incSet('guest')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.guest }}</span>
|
||||
</div>
|
||||
|
||||
<span v-if="visuForm">
|
||||
<div class="col form home" @click="incPunt('home')">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
|
||||
{{ sp.form.home[x] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form guest" @click="incPunt('guest')">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
|
||||
{{ sp.form.guest[x] }}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
<w-flex class="punteggio-container">
|
||||
<w-flex justify-center align-center class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</w-flex>
|
||||
<w-flex justify-center align-center class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</w-flex>
|
||||
</w-flex>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- guest home -->
|
||||
|
||||
<div class="hea guest">
|
||||
<span @click="decPunt('guest')" :style="{ 'float': 'left' }">
|
||||
{{ sp.nomi.guest }}
|
||||
<span class="serv-slot">
|
||||
<img v-show="!sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
<span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
|
||||
</span>
|
||||
<span @click="incSet('guest')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.guest }}</span>
|
||||
</div>
|
||||
|
||||
<div class="hea home">
|
||||
<span @click="decPunt('home')" :style="{ 'float': 'right' }">
|
||||
<span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
|
||||
<span class="serv-slot">
|
||||
<img v-show="sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
{{ sp.nomi.home }}
|
||||
</span>
|
||||
<span @click="incSet('home')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.home }}</span>
|
||||
</div>
|
||||
|
||||
<span v-if="visuForm">
|
||||
<div class="col form guest" @click="incPunt('guest')">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
|
||||
{{ sp.form.guest[x] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form home" @click="incPunt('home')">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]">
|
||||
{{ sp.form.home[x] }}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
<w-flex class="punteggio-container">
|
||||
<w-flex justify-center align-center class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</w-flex>
|
||||
<w-flex justify-center align-center class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</w-flex>
|
||||
</w-flex>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
|
||||
<div class="striscia" v-if="visuStriscia">
|
||||
<div>
|
||||
<span class="text-bold mr1">{{ sp.nomi.home }}</span>
|
||||
<div v-for="h in sp.striscia.home" class="item">
|
||||
{{String(h)}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="guest">
|
||||
<span class="text-bold mr1">{{ sp.nomi.guest }}</span>
|
||||
<div v-for="h in sp.striscia.guest" class="item">
|
||||
{{String(h)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bot" v-if="visuButt">
|
||||
<w-flex justify-space-between class="pa2">
|
||||
<w-confirm right align-bottom v-if="isMobile()" question="CHIUDO ?" cancel="NO" confirm="SI" @confirm="closeApp">
|
||||
<img src="/exit.png" width="25" />
|
||||
</w-confirm>
|
||||
<w-button @click="apriDialogConfig()">
|
||||
<img src="/gear.png" width="25" />
|
||||
</w-button>
|
||||
<w-button @click="cambiaPalla" :disabled="!isPunteggioZeroZero">
|
||||
<img src="/serv.png" width="25" />
|
||||
</w-button>
|
||||
<w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta">
|
||||
RESET
|
||||
</w-confirm>
|
||||
<w-button @click="visuForm = !visuForm">
|
||||
<span v-if="visuForm">PUNTEGGIO</span>
|
||||
<span v-if="!visuForm">FORMAZIONI</span>
|
||||
</w-button>
|
||||
<w-button @click="apriDialogCambi">
|
||||
CAMBI
|
||||
</w-button>
|
||||
<w-button @click="visuStriscia = !visuStriscia">
|
||||
STRISCIA
|
||||
</w-button>
|
||||
<w-button @click="speak">
|
||||
<img src="/speaker.png" width="25" />
|
||||
</w-button>
|
||||
</w-flex>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,479 +0,0 @@
|
||||
import NoSleep from "nosleep.js";
|
||||
export default {
|
||||
name: "HomePage",
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
order: true,
|
||||
voices: null,
|
||||
diaNomi: {
|
||||
show: false,
|
||||
home: "",
|
||||
guest: "",
|
||||
},
|
||||
diaCambi: {
|
||||
show: false,
|
||||
team: "home",
|
||||
guest: { cambi: [{ in: "", out: "" }, { in: "", out: "" }] },
|
||||
home: { cambi: [{ in: "", out: "" }, { in: "", out: "" }] },
|
||||
},
|
||||
diaCambiTeam: {
|
||||
show: false,
|
||||
},
|
||||
visuForm: false,
|
||||
visuButt: true,
|
||||
visuStriscia: true,
|
||||
modalitaPartita: "3/5", // "2/3" o "3/5"
|
||||
sp: {
|
||||
striscia: { home: [0], guest: [0] },
|
||||
servHome: true,
|
||||
punt: { home: 0, guest: 0 },
|
||||
set: { home: 0, guest: 0 },
|
||||
nomi: { home: "Antoniana", guest: "Guest" },
|
||||
form: {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
storicoServizio: [], // Stack per tracciare lo stato del servizio prima di ogni punto
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.voices = window.speechSynthesis.getVoices();
|
||||
if (this.isMobile()) {
|
||||
this.speak();
|
||||
var noSleep = new NoSleep();
|
||||
noSleep.enable();
|
||||
document.documentElement.requestFullscreen();
|
||||
}
|
||||
this.abilitaTastiSpeciali();
|
||||
},
|
||||
computed: {
|
||||
isPunteggioZeroZero() {
|
||||
return this.sp.punt.home === 0 && this.sp.punt.guest === 0;
|
||||
},
|
||||
cambiConfermabili() {
|
||||
const team = this.diaCambi.team;
|
||||
const cambi = this.diaCambi[team].cambi || [];
|
||||
let hasComplete = false;
|
||||
let allValid = true;
|
||||
|
||||
cambi.forEach((cambio) => {
|
||||
const teamIn = (cambio.in || "").trim();
|
||||
const teamOut = (cambio.out || "").trim();
|
||||
|
||||
if (!teamIn && !teamOut) {
|
||||
return;
|
||||
}
|
||||
if (!teamIn || !teamOut) {
|
||||
allValid = false;
|
||||
return;
|
||||
}
|
||||
hasComplete = true;
|
||||
});
|
||||
|
||||
return allValid && hasComplete;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeApp() {
|
||||
var win = window.open("", "_self");
|
||||
win.close();
|
||||
},
|
||||
fullScreen() {
|
||||
document.documentElement.requestFullscreen();
|
||||
},
|
||||
isMobile() {
|
||||
if (
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
resetta() {
|
||||
this.$waveui.notify("Punteggio<br />RESETTATO", "success");
|
||||
this.visuForm = false;
|
||||
this.sp.punt.home = 0;
|
||||
this.sp.punt.guest = 0;
|
||||
this.sp.form = {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
}
|
||||
this.sp.striscia = { home: [0], guest: [0] }
|
||||
this.sp.storicoServizio = []
|
||||
},
|
||||
cambiaPalla() {
|
||||
if (!this.isPunteggioZeroZero) {
|
||||
this.$waveui.notify("Cambio palla consentito solo a inizio set (0-0)", "warning");
|
||||
return;
|
||||
}
|
||||
this.sp.servHome = !this.sp.servHome;
|
||||
},
|
||||
incSet(team) {
|
||||
if (this.sp.set[team] == 2) {
|
||||
this.sp.set[team] = 0;
|
||||
} else {
|
||||
this.sp.set[team]++;
|
||||
}
|
||||
},
|
||||
incPunt(team) {
|
||||
// Se il set è già terminato, evita ulteriori incrementi
|
||||
if (this.checkVittoria()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Salva lo stato del servizio PRIMA di modificarlo
|
||||
this.sp.storicoServizio.push({
|
||||
servHome: this.sp.servHome,
|
||||
cambioPalla: (team == "home" && !this.sp.servHome) || (team == "guest" && this.sp.servHome)
|
||||
});
|
||||
|
||||
this.sp.punt[team]++;
|
||||
if (team == 'home') {
|
||||
this.sp.striscia.home.push(this.sp.punt.home)
|
||||
this.sp.striscia.guest.push(' ')
|
||||
} else {
|
||||
this.sp.striscia.guest.push(this.sp.punt.guest)
|
||||
this.sp.striscia.home.push(' ')
|
||||
}
|
||||
|
||||
// Ruota la formazione solo se c'è cambio palla (conquista del servizio)
|
||||
const cambioPalla = (team == "home" && !this.sp.servHome) || (team == "guest" && this.sp.servHome);
|
||||
|
||||
if (cambioPalla) {
|
||||
this.sp.form[team].push(this.sp.form[team].shift());
|
||||
}
|
||||
|
||||
this.sp.servHome = (team == "home");
|
||||
},
|
||||
checkVittoria() {
|
||||
const puntHome = this.sp.punt.home;
|
||||
const puntGuest = this.sp.punt.guest;
|
||||
const setHome = this.sp.set.home;
|
||||
const setGuest = this.sp.set.guest;
|
||||
const totSet = setHome + setGuest;
|
||||
|
||||
// Determina se siamo nel set decisivo in base alla modalità partita
|
||||
let isSetDecisivo = false;
|
||||
if (this.modalitaPartita === "2/3") {
|
||||
// Tie-break al 3° set (quando totSet >= 2)
|
||||
isSetDecisivo = totSet >= 2;
|
||||
} else {
|
||||
// Tie-break al 5° set (quando totSet >= 4)
|
||||
isSetDecisivo = totSet >= 4;
|
||||
}
|
||||
|
||||
const punteggioVittoria = isSetDecisivo ? 15 : 25;
|
||||
|
||||
// Vittoria con punteggio >= 25 (o 15 per set decisivo) e almeno 2 punti di vantaggio
|
||||
if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) {
|
||||
return true; // Home ha vinto
|
||||
}
|
||||
if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) {
|
||||
return true; // Guest ha vinto
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
decPunt() {
|
||||
if (this.sp.striscia.home.length > 1 && this.sp.storicoServizio.length > 0) {
|
||||
var tmpHome = this.sp.striscia.home.pop()
|
||||
var tmpGuest = this.sp.striscia.guest.pop()
|
||||
var statoServizio = this.sp.storicoServizio.pop() // Recupera lo stato completo del servizio
|
||||
|
||||
if (tmpHome == ' ') {
|
||||
this.sp.punt.guest--
|
||||
// Ruota indietro solo se c'era stato un cambio palla
|
||||
if (statoServizio.cambioPalla) {
|
||||
this.sp.form.guest.unshift(this.sp.form.guest.pop());
|
||||
}
|
||||
} else {
|
||||
this.sp.punt.home--
|
||||
// Ruota indietro solo se c'era stato un cambio palla
|
||||
if (statoServizio.cambioPalla) {
|
||||
this.sp.form.home.unshift(this.sp.form.home.pop());
|
||||
}
|
||||
}
|
||||
|
||||
// Ripristina il servizio allo stato precedente
|
||||
this.sp.servHome = statoServizio.servHome;
|
||||
|
||||
}
|
||||
},
|
||||
// decPunt(team) {
|
||||
// // decrementa il punteggio se è > 0.
|
||||
// if (this.sp.punt[team] > 0) {
|
||||
// this.sp.punt[team]--;
|
||||
// this.sp.striscia.home.pop()
|
||||
// this.sp.striscia.guest.pop()
|
||||
// this.sp.form[team].unshift(this.sp.form[team].pop());
|
||||
// }
|
||||
// },
|
||||
speak() {
|
||||
const msg = new SpeechSynthesisUtterance();
|
||||
if (this.sp.punt.home + this.sp.punt.guest == 0) {
|
||||
msg.text = "zero a zero";
|
||||
} else if (this.sp.punt.home == this.sp.punt.guest) {
|
||||
msg.text = this.sp.punt.home + " pari";
|
||||
} else {
|
||||
if (this.sp.servHome) {
|
||||
msg.text = this.sp.punt.home + " a " + this.sp.punt.guest;
|
||||
} else {
|
||||
msg.text = this.sp.punt.guest + " a " + this.sp.punt.home;
|
||||
}
|
||||
}
|
||||
// msg.volume = 1.0; // speech volume (default: 1.0)
|
||||
// msg.pitch = 1.0; // speech pitch (default: 1.0)
|
||||
// msg.rate = 1.0; // speech rate (default: 1.0)
|
||||
// msg.lang = 'it_IT'; // speech language (default: 'en-US')
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
msg.voice = voices.find(voice => voice.name === 'Google italiano');
|
||||
// voice URI (default: platform-dependent)
|
||||
// msg.onboundary = function (event) {
|
||||
// console.log('Speech reached a boundary:', event.name);
|
||||
// };
|
||||
// msg.onpause = function (event) {
|
||||
// console.log('Speech paused:', event.utterance.text.substring(event.charIndex));
|
||||
// };
|
||||
window.speechSynthesis.speak(msg);
|
||||
},
|
||||
apriDialogConfig() {
|
||||
this.disabilitaTastiSpeciali();
|
||||
this.diaNomi.show = true;
|
||||
|
||||
// Aggiungi gestore Tab per il dialog
|
||||
this.dialogConfigTabHandler = (e) => {
|
||||
if (e.key === 'Tab' && this.diaNomi.show) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const dialog = document.querySelector('.w-dialog');
|
||||
if (!dialog) return;
|
||||
|
||||
const allInputs = Array.from(dialog.querySelectorAll('input[type="text"]'))
|
||||
.sort((a, b) => {
|
||||
const tabA = parseInt(a.closest('[tabindex]')?.getAttribute('tabindex') || '0');
|
||||
const tabB = parseInt(b.closest('[tabindex]')?.getAttribute('tabindex') || '0');
|
||||
return tabA - tabB;
|
||||
});
|
||||
|
||||
if (allInputs.length === 0) return;
|
||||
|
||||
// Verifica se il focus è già dentro il dialog
|
||||
const focusInDialog = dialog.contains(document.activeElement);
|
||||
|
||||
// Se non è nel dialog o non è in un input, vai al primo
|
||||
if (!focusInDialog || !allInputs.includes(document.activeElement)) {
|
||||
allInputs[0].focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigazione normale tra i campi
|
||||
const currentIndex = allInputs.indexOf(document.activeElement);
|
||||
let nextIndex;
|
||||
|
||||
if (e.shiftKey) {
|
||||
nextIndex = currentIndex <= 0 ? allInputs.length - 1 : currentIndex - 1;
|
||||
} else {
|
||||
nextIndex = currentIndex >= allInputs.length - 1 ? 0 : currentIndex + 1;
|
||||
}
|
||||
|
||||
if (allInputs[nextIndex]) {
|
||||
allInputs[nextIndex].focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', this.dialogConfigTabHandler, true);
|
||||
|
||||
// Focus immediato + retry con timeout
|
||||
this.$nextTick(() => {
|
||||
const focusFirst = () => {
|
||||
const dialog = document.querySelector('.w-dialog');
|
||||
if (dialog) {
|
||||
const firstInput = dialog.querySelector('input[type="text"]');
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
firstInput.select();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Prova immediatamente
|
||||
if (!focusFirst()) {
|
||||
// Se fallisce, riprova dopo un breve delay
|
||||
setTimeout(focusFirst, 100);
|
||||
}
|
||||
});
|
||||
},
|
||||
chiudiDialogConfig() {
|
||||
if (this.dialogConfigTabHandler) {
|
||||
window.removeEventListener('keydown', this.dialogConfigTabHandler, true);
|
||||
this.dialogConfigTabHandler = null;
|
||||
}
|
||||
this.abilitaTastiSpeciali();
|
||||
},
|
||||
resettaCambi(team) {
|
||||
const teams = team ? [team] : ["home", "guest"];
|
||||
teams.forEach((t) => {
|
||||
this.diaCambi[t].cambi.forEach((cambio) => {
|
||||
cambio.in = "";
|
||||
cambio.out = "";
|
||||
});
|
||||
});
|
||||
},
|
||||
apriDialogCambi() {
|
||||
this.disabilitaTastiSpeciali();
|
||||
this.diaCambiTeam.show = true;
|
||||
},
|
||||
apriDialogCambiTeam(team) {
|
||||
this.disabilitaTastiSpeciali();
|
||||
this.diaCambi.team = team;
|
||||
this.resettaCambi(team);
|
||||
this.diaCambi.show = true;
|
||||
},
|
||||
selezionaTeamCambi(team) {
|
||||
this.diaCambiTeam.show = false;
|
||||
this.apriDialogCambiTeam(team);
|
||||
},
|
||||
chiudiDialogCambi() {
|
||||
this.diaCambi.show = false;
|
||||
this.resettaCambi(this.diaCambi.team);
|
||||
this.abilitaTastiSpeciali();
|
||||
},
|
||||
confermaCambi() {
|
||||
if (!this.cambiConfermabili) {
|
||||
return;
|
||||
}
|
||||
|
||||
const team = this.diaCambi.team;
|
||||
const cambi = (this.diaCambi[team].cambi || [])
|
||||
.map((cambio) => ({
|
||||
team,
|
||||
in: (cambio.in || "").trim(),
|
||||
out: (cambio.out || "").trim(),
|
||||
}))
|
||||
.filter((cambio) => cambio.in || cambio.out);
|
||||
|
||||
const form = this.sp.form[team].map((val) => String(val).trim());
|
||||
const formAggiornata = [...form];
|
||||
|
||||
for (const cambio of cambi) {
|
||||
if (!/^\d+$/.test(cambio.in) || !/^\d+$/.test(cambio.out)) {
|
||||
this.$waveui.notify("Inserisci solo numeri nei campi", "warning");
|
||||
return;
|
||||
}
|
||||
if (cambio.in === cambio.out) {
|
||||
this.$waveui.notify(`Numero IN e OUT uguali per ${cambio.team}`, "warning");
|
||||
return;
|
||||
}
|
||||
if (formAggiornata.includes(cambio.in)) {
|
||||
this.$waveui.notify(`Numero ${cambio.in} già presente in formazione ${cambio.team}`, "warning");
|
||||
return;
|
||||
}
|
||||
if (!formAggiornata.includes(cambio.out)) {
|
||||
this.$waveui.notify(`Numero ${cambio.out} non presente in formazione ${cambio.team}`, "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = formAggiornata.findIndex((val) => String(val).trim() === cambio.out);
|
||||
if (idx !== -1) {
|
||||
formAggiornata.splice(idx, 1, cambio.in);
|
||||
}
|
||||
}
|
||||
|
||||
this.sp.form[team] = formAggiornata;
|
||||
|
||||
this.chiudiDialogCambi();
|
||||
},
|
||||
disabilitaTastiSpeciali() {
|
||||
window.removeEventListener("keydown", this.funzioneTastiSpeciali);
|
||||
},
|
||||
abilitaTastiSpeciali() {
|
||||
window.addEventListener("keydown", this.funzioneTastiSpeciali);
|
||||
},
|
||||
funzioneTastiSpeciali(e) {
|
||||
if (this.diaNomi.show || this.diaCambi.show || this.diaCambiTeam.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target;
|
||||
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
|
||||
const elements = [target, ...path].filter(Boolean);
|
||||
const isTypingField = elements.some((el) => {
|
||||
if (!el || !el.tagName) {
|
||||
return false;
|
||||
}
|
||||
const tag = String(el.tagName).toLowerCase();
|
||||
if (tag === "input" || tag === "textarea") {
|
||||
return true;
|
||||
}
|
||||
if (el.isContentEditable) {
|
||||
return true;
|
||||
}
|
||||
if (el.classList && (el.classList.contains("w-input") || el.classList.contains("w-textarea"))) {
|
||||
return true;
|
||||
}
|
||||
const contentEditable = el.getAttribute && el.getAttribute("contenteditable");
|
||||
return contentEditable === "true";
|
||||
});
|
||||
if (isTypingField) {
|
||||
return;
|
||||
}
|
||||
|
||||
let handled = false;
|
||||
if (e.ctrlKey && e.key == "m") {
|
||||
this.diaNomi.show = true
|
||||
handled = true;
|
||||
} else if (e.ctrlKey && e.key == "b") {
|
||||
this.visuButt = !this.visuButt
|
||||
handled = true;
|
||||
} else if (e.ctrlKey && e.key == "f") {
|
||||
document.documentElement.requestFullscreen();
|
||||
handled = true;
|
||||
} else if (e.ctrlKey && e.key == "s") {
|
||||
this.speak();
|
||||
handled = true;
|
||||
} else if (e.ctrlKey && e.key == "z") {
|
||||
this.visuForm = !this.visuForm
|
||||
handled = true;
|
||||
} else if (e.ctrlKey && e.key == "ArrowUp") {
|
||||
this.incPunt("home")
|
||||
handled = true;
|
||||
} else if (e.ctrlKey && e.key == "ArrowDown") {
|
||||
this.decPunt("home")
|
||||
handled = true;
|
||||
} else if (e.ctrlKey && e.key == "ArrowRight") {
|
||||
this.incSet("home")
|
||||
handled = true;
|
||||
} else if (e.shiftKey && e.key == "ArrowUp") {
|
||||
this.incPunt("guest")
|
||||
handled = true;
|
||||
} else if (e.shiftKey && e.key == "ArrowDown") {
|
||||
this.decPunt("guest")
|
||||
handled = true;
|
||||
} else if (e.shiftKey && e.key == "ArrowRight") {
|
||||
this.incSet("guest")
|
||||
handled = true;
|
||||
} else if (e.ctrlKey && e.key == "ArrowLeft") {
|
||||
this.cambiaPalla()
|
||||
handled = true;
|
||||
} else if (e.ctrlKey && (e.key == "c" || e.key == "C")) {
|
||||
this.apriDialogCambiTeam("home")
|
||||
handled = true;
|
||||
} else if (e.shiftKey && (e.key == "c" || e.key == "C")) {
|
||||
this.apriDialogCambiTeam("guest")
|
||||
handled = true;
|
||||
} else { return false }
|
||||
|
||||
if (handled) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
.homepage {
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
touch-action: pan-x pan-y;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior-y: contain;
|
||||
margin: 0;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #fff;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
background-color: #333;
|
||||
}
|
||||
button:focus, button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
#app {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.campo {
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
display: table;
|
||||
color: #fff;
|
||||
}
|
||||
.hea {
|
||||
float: left;
|
||||
width: 50%;
|
||||
font-size: xx-large;
|
||||
}
|
||||
.hea span {
|
||||
/* border: 1px solid #f3fb00; */
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.tal {
|
||||
text-align: left;
|
||||
}
|
||||
.tar {
|
||||
text-align: right;
|
||||
}
|
||||
.bot {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 1px;
|
||||
background-color: #111;
|
||||
}
|
||||
.col {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
.punt {
|
||||
font-size: 60vh;
|
||||
}
|
||||
.form {
|
||||
font-size: 5vh;
|
||||
border-top: #fff dashed 25px;
|
||||
padding-top: 50px;
|
||||
}
|
||||
.formtit {
|
||||
font-size: 5vh;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.formdiv {
|
||||
font-size: 20vh;
|
||||
float: left;
|
||||
width: 32%;
|
||||
}
|
||||
.home {
|
||||
background-color: black;
|
||||
color: yellow;
|
||||
}
|
||||
.guest {
|
||||
background-color: blue;
|
||||
color: white
|
||||
}
|
||||
.item-stri {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<template src="./HomePage.html"></template>
|
||||
<script src="./HomePage.js"></script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Shared game logic for segnapunti.
|
||||
* Used by both the WebSocket server and the client-side for local preview.
|
||||
* Logica di gioco condivisa per il segnapunti.
|
||||
* Utilizzata sia dal server WebSocket sia dal client per l'anteprima locale.
|
||||
*/
|
||||
|
||||
export function createInitialState() {
|
||||
@@ -51,8 +51,8 @@ export function checkVittoria(state) {
|
||||
}
|
||||
|
||||
export function applyAction(state, action) {
|
||||
// Deep-clone to avoid mutation issues (server-side)
|
||||
// Returns new state
|
||||
// Esegue un deep clone per evitare mutazioni indesiderate dello stato lato server.
|
||||
// Restituisce sempre un nuovo oggetto di stato.
|
||||
const s = JSON.parse(JSON.stringify(state))
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
45
src/server-utils.js
Normal file
45
src/server-utils.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { networkInterfaces } from 'os'
|
||||
|
||||
/**
|
||||
* Restituisce gli indirizzi IP di rete del sistema, escludendo loopback e bridge Docker.
|
||||
* @returns {string[]} Elenco degli indirizzi IP disponibili.
|
||||
*/
|
||||
export function getNetworkIPs() {
|
||||
const nets = networkInterfaces()
|
||||
const networkIPs = []
|
||||
|
||||
for (const name of Object.keys(nets)) {
|
||||
for (const net of nets[name]) {
|
||||
// Esclude loopback (127.0.0.1), indirizzi non IPv4 e bridge Docker (172.17.x.x, 172.18.x.x).
|
||||
if (net.family === 'IPv4' &&
|
||||
!net.internal &&
|
||||
!net.address.startsWith('172.17.') &&
|
||||
!net.address.startsWith('172.18.')) {
|
||||
networkIPs.push(net.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return networkIPs
|
||||
}
|
||||
|
||||
/**
|
||||
* Stampa il riepilogo di avvio del server con gli URL di accesso.
|
||||
* @param {number} port - Porta sulla quale il server e in ascolto.
|
||||
*/
|
||||
export function printServerInfo(port = 5173) {
|
||||
const networkIPs = getNetworkIPs()
|
||||
|
||||
console.log(`\nSegnapunti Server`)
|
||||
console.log(` Display: http://localhost:${port}/`)
|
||||
console.log(` Controller: http://localhost:${port}/controller`)
|
||||
|
||||
if (networkIPs.length > 0) {
|
||||
console.log(`\n Da dispositivi remoti:`)
|
||||
networkIPs.forEach(ip => {
|
||||
console.log(` http://${ip}:${port}/controller`)
|
||||
})
|
||||
}
|
||||
|
||||
console.log()
|
||||
}
|
||||
@@ -53,7 +53,7 @@ button:focus-visible {
|
||||
font-size: xx-large;
|
||||
}
|
||||
.hea span {
|
||||
/* border: 1px solid #f3fb00; */
|
||||
/* Bordo di debug: border: 1px solid #f3fb00; */
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-radius: 5px;
|
||||
|
||||
171
src/websocket-handler.js
Normal file
171
src/websocket-handler.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import { createInitialState, applyAction } from './gameState.js'
|
||||
|
||||
/**
|
||||
* Crea e configura il server WebSocket per la gestione dello stato di gioco.
|
||||
* @param {WebSocketServer} wss - Istanza del server WebSocket.
|
||||
* @returns {Object} Oggetto con metodi di gestione dello stato.
|
||||
*/
|
||||
export function setupWebSocketHandler(wss) {
|
||||
// Stato globale della partita.
|
||||
let gameState = createInitialState()
|
||||
|
||||
// Mappa dei ruoli associati ai client connessi.
|
||||
const clients = new Map() // ws -> { role: 'display' | 'controller' }
|
||||
|
||||
/**
|
||||
* Gestisce i messaggi in arrivo dal client
|
||||
*/
|
||||
function handleMessage(ws, data) {
|
||||
try {
|
||||
// Converte il payload in stringa in modo sicuro, anche se arriva come Buffer.
|
||||
const dataStr = typeof data === 'string' ? data : data.toString('utf8')
|
||||
const msg = JSON.parse(dataStr)
|
||||
|
||||
if (msg.type === 'register') {
|
||||
return handleRegister(ws, msg)
|
||||
}
|
||||
|
||||
if (msg.type === 'action') {
|
||||
return handleAction(ws, msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error processing message:', err, 'data:', data)
|
||||
// Invia l'errore solo se la connessione e ancora aperta.
|
||||
if (ws.readyState === 1) { // Stato WebSocket.OPEN
|
||||
try {
|
||||
sendError(ws, 'Invalid message format')
|
||||
} catch (sendErr) {
|
||||
console.error('Error sending error message:', sendErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestisce la registrazione di un client (display o controller)
|
||||
*/
|
||||
function handleRegister(ws, msg) {
|
||||
const role = msg.role || 'display'
|
||||
|
||||
// Valida il ruolo dichiarato dal client.
|
||||
if (!['display', 'controller'].includes(role)) {
|
||||
sendError(ws, 'Invalid role')
|
||||
return
|
||||
}
|
||||
|
||||
clients.set(ws, { role })
|
||||
console.log(`[WebSocket] Client registered as: ${role} (total clients: ${clients.size})`)
|
||||
|
||||
// Invia subito lo stato corrente, se la connessione e aperta.
|
||||
if (ws.readyState === 1) { // Stato WebSocket.OPEN
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: 'state', state: gameState }))
|
||||
} catch (err) {
|
||||
console.error('Error sending initial state:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestisce un'azione di gioco dal controller
|
||||
*/
|
||||
function handleAction(ws, msg) {
|
||||
// Solo i client controller possono inviare azioni.
|
||||
const client = clients.get(ws)
|
||||
if (!client || client.role !== 'controller') {
|
||||
sendError(ws, 'Only controllers can send actions')
|
||||
return
|
||||
}
|
||||
|
||||
// Verifica il formato dell'azione ricevuta.
|
||||
if (!msg.action || !msg.action.type) {
|
||||
sendError(ws, 'Invalid action format')
|
||||
return
|
||||
}
|
||||
|
||||
// Applica l'azione allo stato della partita.
|
||||
const previousState = gameState
|
||||
try {
|
||||
gameState = applyAction(gameState, msg.action)
|
||||
} catch (err) {
|
||||
console.error('Error applying action:', err)
|
||||
sendError(ws, 'Failed to apply action')
|
||||
gameState = previousState
|
||||
return
|
||||
}
|
||||
|
||||
// Propaga il nuovo stato a tutti i client connessi.
|
||||
broadcastState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia un messaggio di errore al client
|
||||
*/
|
||||
function sendError(ws, message) {
|
||||
if (ws.readyState === 1) { // Stato WebSocket.OPEN
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: 'error', message }))
|
||||
} catch (err) {
|
||||
console.error('Failed to send error message:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia lo stato corrente a tutti i client connessi.
|
||||
*/
|
||||
function broadcastState() {
|
||||
const stateMsg = JSON.stringify({ type: 'state', state: gameState })
|
||||
wss.clients.forEach((client) => {
|
||||
if (client.readyState === 1) { // Stato WebSocket.OPEN
|
||||
client.send(stateMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestisce la chiusura della connessione
|
||||
*/
|
||||
function handleClose(ws) {
|
||||
const client = clients.get(ws)
|
||||
const role = client?.role || 'unknown'
|
||||
console.log(`[WebSocket] Client disconnected (role: ${role})`)
|
||||
clients.delete(ws)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestisce gli errori WebSocket
|
||||
*/
|
||||
function handleError(err, ws) {
|
||||
console.error('WebSocket error:', err)
|
||||
|
||||
// In caso di frame non validi, chiude forzatamente la connessione.
|
||||
if (err.code === 'WS_ERR_INVALID_CLOSE_CODE' || err.code === 'WS_ERR_INVALID_UTF8') {
|
||||
try {
|
||||
if (ws && ws.readyState === 1) { // Stato WebSocket.OPEN
|
||||
ws.terminate() // Chiusura forzata senza handshake di chiusura.
|
||||
}
|
||||
} catch (closeErr) {
|
||||
console.error('Error closing connection:', closeErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registra gli handler per ogni nuova connessione.
|
||||
wss.on('connection', (ws) => {
|
||||
// Imposta il tipo binario per ridurre i problemi di codifica.
|
||||
ws.binaryType = 'arraybuffer'
|
||||
|
||||
ws.on('message', (data) => handleMessage(ws, data))
|
||||
ws.on('close', () => handleClose(ws))
|
||||
ws.on('error', (err) => handleError(err, ws))
|
||||
})
|
||||
|
||||
// Espone un'API pubblica per controllo esterno, se necessario.
|
||||
return {
|
||||
getState: () => gameState,
|
||||
setState: (newState) => { gameState = newState },
|
||||
broadcastState,
|
||||
getClients: () => clients,
|
||||
}
|
||||
}
|
||||
39
vite-plugin-websocket.js
Normal file
39
vite-plugin-websocket.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { WebSocketServer } from 'ws'
|
||||
import { setupWebSocketHandler } from './src/websocket-handler.js'
|
||||
import { printServerInfo } from './src/server-utils.js'
|
||||
|
||||
/**
|
||||
* Plugin Vite che integra un server WebSocket per la gestione dello stato di gioco.
|
||||
* @returns {import('vite').Plugin}
|
||||
*/
|
||||
export default function websocketPlugin() {
|
||||
return {
|
||||
name: 'vite-plugin-websocket',
|
||||
configureServer(server) {
|
||||
// Inizializza un server WebSocket collegato al server HTTP di Vite.
|
||||
// Importante: usa `noServer: true` per evitare conflitti con l'HMR di Vite.
|
||||
const wss = new WebSocketServer({ noServer: true })
|
||||
|
||||
// Registra i gestori WebSocket con la logica di gioco.
|
||||
setupWebSocketHandler(wss)
|
||||
|
||||
// Intercetta le richieste di upgrade WebSocket.
|
||||
server.httpServer.on('upgrade', (request, socket, head) => {
|
||||
// Se la richiesta non riguarda l'HMR di Vite (es. /@vite/client),
|
||||
// la gestisce il server WebSocket dell'applicazione.
|
||||
const pathname = new URL(request.url, `http://${request.headers.host}`).pathname
|
||||
|
||||
if (!pathname.startsWith('/@vite') && !pathname.startsWith('/@fs')) {
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Stampa le informazioni di accesso dopo l'avvio del server Vite.
|
||||
server.httpServer.once('listening', () => {
|
||||
setTimeout(() => printServerInfo(5173), 100)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
// Configurazione principale di Vite
|
||||
export default defineConfig({
|
||||
base: '/',
|
||||
plugins: [
|
||||
@@ -32,4 +32,15 @@ export default defineConfig({
|
||||
}
|
||||
})
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/': {
|
||||
target: 'http://127.0.0.1:5174',
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user