mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-02-08 10:12:39 +00:00
refactor: cleanup
This commit is contained in:
@@ -73,7 +73,6 @@
|
|||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"matrix-js-sdk": "^37.12.0",
|
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-css-order": "^2.1.2",
|
"prettier-plugin-css-order": "^2.1.2",
|
||||||
|
|||||||
147
pnpm-lock.yaml
generated
147
pnpm-lock.yaml
generated
@@ -125,9 +125,6 @@ importers:
|
|||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^26.1.0
|
specifier: ^26.1.0
|
||||||
version: 26.1.0
|
version: 26.1.0
|
||||||
matrix-js-sdk:
|
|
||||||
specifier: ^37.12.0
|
|
||||||
version: 37.12.0
|
|
||||||
npm-run-all:
|
npm-run-all:
|
||||||
specifier: ^4.1.5
|
specifier: ^4.1.5
|
||||||
version: 4.1.5
|
version: 4.1.5
|
||||||
@@ -710,10 +707,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0
|
'@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0
|
||||||
|
|
||||||
'@babel/runtime@7.24.7':
|
|
||||||
resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/runtime@7.28.4':
|
'@babel/runtime@7.28.4':
|
||||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -1075,10 +1068,6 @@ packages:
|
|||||||
'@material/material-color-utilities@0.3.0':
|
'@material/material-color-utilities@0.3.0':
|
||||||
resolution: {integrity: sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==}
|
resolution: {integrity: sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==}
|
||||||
|
|
||||||
'@matrix-org/matrix-sdk-crypto-wasm@15.1.0':
|
|
||||||
resolution: {integrity: sha512-ZsDdjn46J3+VxsDLmaSODuS+qtGZB/i3Cg9tWL1QPNjvAWzNaTHQ7glleByI2PKVBm83aklfuhGKT2MqE1ZsEA==}
|
|
||||||
engines: {node: '>= 18'}
|
|
||||||
|
|
||||||
'@melt-ui/pp@0.3.2':
|
'@melt-ui/pp@0.3.2':
|
||||||
resolution: {integrity: sha512-xKkPvaIAFinklLXcQOpwZ8YSpqAFxykjWf8Y/fSJQwsixV/0rcFs07hJ49hJjPy5vItvw5Qa0uOjzFUbXzBypQ==}
|
resolution: {integrity: sha512-xKkPvaIAFinklLXcQOpwZ8YSpqAFxykjWf8Y/fSJQwsixV/0rcFs07hJ49hJjPy5vItvw5Qa0uOjzFUbXzBypQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1499,9 +1488,6 @@ packages:
|
|||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
'@types/events@3.0.3':
|
|
||||||
resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==}
|
|
||||||
|
|
||||||
'@types/js-yaml@4.0.9':
|
'@types/js-yaml@4.0.9':
|
||||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||||
|
|
||||||
@@ -1511,9 +1497,6 @@ packages:
|
|||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
|
||||||
'@types/retry@0.12.0':
|
|
||||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
|
||||||
|
|
||||||
'@types/semver@7.7.1':
|
'@types/semver@7.7.1':
|
||||||
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||||
|
|
||||||
@@ -1601,9 +1584,6 @@ packages:
|
|||||||
ajv@8.17.1:
|
ajv@8.17.1:
|
||||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||||
|
|
||||||
another-json@0.2.0:
|
|
||||||
resolution: {integrity: sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==}
|
|
||||||
|
|
||||||
ansi-colors@4.1.3:
|
ansi-colors@4.1.3:
|
||||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1738,9 +1718,6 @@ packages:
|
|||||||
balanced-match@2.0.0:
|
balanced-match@2.0.0:
|
||||||
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
|
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
|
||||||
|
|
||||||
base-x@5.0.0:
|
|
||||||
resolution: {integrity: sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==}
|
|
||||||
|
|
||||||
base64-js@1.5.1:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
@@ -1775,9 +1752,6 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
bs58@6.0.0:
|
|
||||||
resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==}
|
|
||||||
|
|
||||||
buffer-crc32@0.2.13:
|
buffer-crc32@0.2.13:
|
||||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||||
|
|
||||||
@@ -1916,10 +1890,6 @@ packages:
|
|||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
content-type@1.0.5:
|
|
||||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
@@ -2383,10 +2353,6 @@ packages:
|
|||||||
eventemitter2@6.4.7:
|
eventemitter2@6.4.7:
|
||||||
resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
|
resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
|
||||||
|
|
||||||
events@3.3.0:
|
|
||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
|
||||||
engines: {node: '>=0.8.x'}
|
|
||||||
|
|
||||||
execa@4.1.0:
|
execa@4.1.0:
|
||||||
resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
|
resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3050,10 +3016,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
|
resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
|
||||||
engines: {'0': node >=0.6.0}
|
engines: {'0': node >=0.6.0}
|
||||||
|
|
||||||
jwt-decode@4.0.0:
|
|
||||||
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
keyv@5.5.5:
|
keyv@5.5.5:
|
||||||
resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==}
|
resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==}
|
||||||
|
|
||||||
@@ -3124,10 +3086,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
|
resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
loglevel@1.9.2:
|
|
||||||
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
|
|
||||||
engines: {node: '>= 0.6.0'}
|
|
||||||
|
|
||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
@@ -3157,16 +3115,6 @@ packages:
|
|||||||
mathml-tag-names@2.1.3:
|
mathml-tag-names@2.1.3:
|
||||||
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
|
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
|
||||||
|
|
||||||
matrix-events-sdk@0.0.1:
|
|
||||||
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
|
||||||
|
|
||||||
matrix-js-sdk@37.12.0:
|
|
||||||
resolution: {integrity: sha512-2JSTMtvACE4egrBPp0ZQ7fSxBXcX6xuHcQSjcoBiBxFL7W0SZCl4qsNl30pBshDTe5wARAByzIHEMv6bGVKKNA==}
|
|
||||||
engines: {node: '>=22.0.0'}
|
|
||||||
|
|
||||||
matrix-widget-api@1.13.1:
|
|
||||||
resolution: {integrity: sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w==}
|
|
||||||
|
|
||||||
mdn-data@2.12.2:
|
mdn-data@2.12.2:
|
||||||
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
|
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
|
||||||
|
|
||||||
@@ -3297,10 +3245,6 @@ packages:
|
|||||||
obug@2.1.1:
|
obug@2.1.1:
|
||||||
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
|
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
|
||||||
|
|
||||||
oidc-client-ts@3.0.1:
|
|
||||||
resolution: {integrity: sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
@@ -3319,10 +3263,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
|
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
p-retry@4.6.2:
|
|
||||||
resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
package-json-from-dist@1.0.0:
|
package-json-from-dist@1.0.0:
|
||||||
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
|
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
|
||||||
|
|
||||||
@@ -3539,9 +3479,6 @@ packages:
|
|||||||
regenerate@1.4.2:
|
regenerate@1.4.2:
|
||||||
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
|
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
|
||||||
|
|
||||||
regenerator-runtime@0.14.1:
|
|
||||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
|
||||||
|
|
||||||
regexp.prototype.flags@1.5.2:
|
regexp.prototype.flags@1.5.2:
|
||||||
resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
|
resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3592,10 +3529,6 @@ packages:
|
|||||||
restructure@3.0.2:
|
restructure@3.0.2:
|
||||||
resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==}
|
resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==}
|
||||||
|
|
||||||
retry@0.13.1:
|
|
||||||
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
|
||||||
engines: {node: '>= 4'}
|
|
||||||
|
|
||||||
reusify@1.0.4:
|
reusify@1.0.4:
|
||||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
@@ -3667,10 +3600,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
engines: {node: '>=v12.22.7'}
|
engines: {node: '>=v12.22.7'}
|
||||||
|
|
||||||
sdp-transform@2.14.2:
|
|
||||||
resolution: {integrity: sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
semver@5.7.2:
|
semver@5.7.2:
|
||||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4190,9 +4119,6 @@ packages:
|
|||||||
undici-types@5.26.5:
|
undici-types@5.26.5:
|
||||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
unhomoglyph@1.0.6:
|
|
||||||
resolution: {integrity: sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==}
|
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@2.0.1:
|
unicode-canonical-property-names-ecmascript@2.0.1:
|
||||||
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
|
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -4243,10 +4169,6 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
uuid@11.1.0:
|
|
||||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
uuid@8.3.2:
|
uuid@8.3.2:
|
||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -5222,10 +5144,6 @@ snapshots:
|
|||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.28.5
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
|
|
||||||
'@babel/runtime@7.24.7':
|
|
||||||
dependencies:
|
|
||||||
regenerator-runtime: 0.14.1
|
|
||||||
|
|
||||||
'@babel/runtime@7.28.4': {}
|
'@babel/runtime@7.28.4': {}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
@@ -5577,8 +5495,6 @@ snapshots:
|
|||||||
|
|
||||||
'@material/material-color-utilities@0.3.0': {}
|
'@material/material-color-utilities@0.3.0': {}
|
||||||
|
|
||||||
'@matrix-org/matrix-sdk-crypto-wasm@15.1.0': {}
|
|
||||||
|
|
||||||
'@melt-ui/pp@0.3.2(@melt-ui/svelte@0.86.6(svelte@5.37.1))(svelte@5.37.1)':
|
'@melt-ui/pp@0.3.2(@melt-ui/svelte@0.86.6(svelte@5.37.1))(svelte@5.37.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@melt-ui/svelte': 0.86.6(svelte@5.37.1)
|
'@melt-ui/svelte': 0.86.6(svelte@5.37.1)
|
||||||
@@ -5932,8 +5848,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/events@3.0.3': {}
|
|
||||||
|
|
||||||
'@types/js-yaml@4.0.9': {}
|
'@types/js-yaml@4.0.9': {}
|
||||||
|
|
||||||
'@types/node@20.14.10':
|
'@types/node@20.14.10':
|
||||||
@@ -5943,8 +5857,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
|
|
||||||
'@types/retry@0.12.0': {}
|
|
||||||
|
|
||||||
'@types/semver@7.7.1': {}
|
'@types/semver@7.7.1': {}
|
||||||
|
|
||||||
'@types/sinonjs__fake-timers@8.1.1': {}
|
'@types/sinonjs__fake-timers@8.1.1': {}
|
||||||
@@ -6040,8 +5952,6 @@ snapshots:
|
|||||||
json-schema-traverse: 1.0.0
|
json-schema-traverse: 1.0.0
|
||||||
require-from-string: 2.0.2
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
another-json@0.2.0: {}
|
|
||||||
|
|
||||||
ansi-colors@4.1.3: {}
|
ansi-colors@4.1.3: {}
|
||||||
|
|
||||||
ansi-escapes@4.3.2:
|
ansi-escapes@4.3.2:
|
||||||
@@ -6176,8 +6086,6 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@2.0.0: {}
|
balanced-match@2.0.0: {}
|
||||||
|
|
||||||
base-x@5.0.0: {}
|
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.9.10: {}
|
baseline-browser-mapping@2.9.10: {}
|
||||||
@@ -6215,10 +6123,6 @@ snapshots:
|
|||||||
node-releases: 2.0.27
|
node-releases: 2.0.27
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||||
|
|
||||||
bs58@6.0.0:
|
|
||||||
dependencies:
|
|
||||||
base-x: 5.0.0
|
|
||||||
|
|
||||||
buffer-crc32@0.2.13: {}
|
buffer-crc32@0.2.13: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
@@ -6354,8 +6258,6 @@ snapshots:
|
|||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
content-type@1.0.5: {}
|
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
cookie@0.6.0: {}
|
cookie@0.6.0: {}
|
||||||
@@ -7001,8 +6903,6 @@ snapshots:
|
|||||||
|
|
||||||
eventemitter2@6.4.7: {}
|
eventemitter2@6.4.7: {}
|
||||||
|
|
||||||
events@3.3.0: {}
|
|
||||||
|
|
||||||
execa@4.1.0:
|
execa@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.3
|
cross-spawn: 7.0.3
|
||||||
@@ -7696,8 +7596,6 @@ snapshots:
|
|||||||
json-schema: 0.4.0
|
json-schema: 0.4.0
|
||||||
verror: 1.10.0
|
verror: 1.10.0
|
||||||
|
|
||||||
jwt-decode@4.0.0: {}
|
|
||||||
|
|
||||||
keyv@5.5.5:
|
keyv@5.5.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@keyv/serialize': 1.1.1
|
'@keyv/serialize': 1.1.1
|
||||||
@@ -7762,8 +7660,6 @@ snapshots:
|
|||||||
slice-ansi: 4.0.0
|
slice-ansi: 4.0.0
|
||||||
wrap-ansi: 6.2.0
|
wrap-ansi: 6.2.0
|
||||||
|
|
||||||
loglevel@1.9.2: {}
|
|
||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@11.0.0: {}
|
lru-cache@11.0.0: {}
|
||||||
@@ -7792,30 +7688,6 @@ snapshots:
|
|||||||
|
|
||||||
mathml-tag-names@2.1.3: {}
|
mathml-tag-names@2.1.3: {}
|
||||||
|
|
||||||
matrix-events-sdk@0.0.1: {}
|
|
||||||
|
|
||||||
matrix-js-sdk@37.12.0:
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.24.7
|
|
||||||
'@matrix-org/matrix-sdk-crypto-wasm': 15.1.0
|
|
||||||
another-json: 0.2.0
|
|
||||||
bs58: 6.0.0
|
|
||||||
content-type: 1.0.5
|
|
||||||
jwt-decode: 4.0.0
|
|
||||||
loglevel: 1.9.2
|
|
||||||
matrix-events-sdk: 0.0.1
|
|
||||||
matrix-widget-api: 1.13.1
|
|
||||||
oidc-client-ts: 3.0.1
|
|
||||||
p-retry: 4.6.2
|
|
||||||
sdp-transform: 2.14.2
|
|
||||||
unhomoglyph: 1.0.6
|
|
||||||
uuid: 11.1.0
|
|
||||||
|
|
||||||
matrix-widget-api@1.13.1:
|
|
||||||
dependencies:
|
|
||||||
'@types/events': 3.0.3
|
|
||||||
events: 3.3.0
|
|
||||||
|
|
||||||
mdn-data@2.12.2: {}
|
mdn-data@2.12.2: {}
|
||||||
|
|
||||||
mdn-data@2.23.0: {}
|
mdn-data@2.23.0: {}
|
||||||
@@ -7927,10 +7799,6 @@ snapshots:
|
|||||||
|
|
||||||
obug@2.1.1: {}
|
obug@2.1.1: {}
|
||||||
|
|
||||||
oidc-client-ts@3.0.1:
|
|
||||||
dependencies:
|
|
||||||
jwt-decode: 4.0.0
|
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
@@ -7951,11 +7819,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
aggregate-error: 3.1.0
|
aggregate-error: 3.1.0
|
||||||
|
|
||||||
p-retry@4.6.2:
|
|
||||||
dependencies:
|
|
||||||
'@types/retry': 0.12.0
|
|
||||||
retry: 0.13.1
|
|
||||||
|
|
||||||
package-json-from-dist@1.0.0: {}
|
package-json-from-dist@1.0.0: {}
|
||||||
|
|
||||||
pako@0.2.9: {}
|
pako@0.2.9: {}
|
||||||
@@ -8137,8 +8000,6 @@ snapshots:
|
|||||||
|
|
||||||
regenerate@1.4.2: {}
|
regenerate@1.4.2: {}
|
||||||
|
|
||||||
regenerator-runtime@0.14.1: {}
|
|
||||||
|
|
||||||
regexp.prototype.flags@1.5.2:
|
regexp.prototype.flags@1.5.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
@@ -8199,8 +8060,6 @@ snapshots:
|
|||||||
|
|
||||||
restructure@3.0.2: {}
|
restructure@3.0.2: {}
|
||||||
|
|
||||||
retry@0.13.1: {}
|
|
||||||
|
|
||||||
reusify@1.0.4: {}
|
reusify@1.0.4: {}
|
||||||
|
|
||||||
rfdc@1.4.1: {}
|
rfdc@1.4.1: {}
|
||||||
@@ -8301,8 +8160,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xmlchars: 2.2.0
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
sdp-transform@2.14.2: {}
|
|
||||||
|
|
||||||
semver@5.7.2: {}
|
semver@5.7.2: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
@@ -8921,8 +8778,6 @@ snapshots:
|
|||||||
undici-types@5.26.5:
|
undici-types@5.26.5:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
unhomoglyph@1.0.6: {}
|
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||||
|
|
||||||
unicode-match-property-ecmascript@2.0.0:
|
unicode-match-property-ecmascript@2.0.0:
|
||||||
@@ -8966,8 +8821,6 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
uuid@11.1.0: {}
|
|
||||||
|
|
||||||
uuid@8.3.2: {}
|
uuid@8.3.2: {}
|
||||||
|
|
||||||
validate-npm-package-license@3.0.4:
|
validate-npm-package-license@3.0.4:
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { RoomMember } from "matrix-js-sdk";
|
|
||||||
import { matrixClient, memberColor } from "./chat";
|
|
||||||
import { theme } from "$lib/preferences";
|
|
||||||
import { hexFromArgb } from "@material/material-color-utilities";
|
|
||||||
|
|
||||||
let { members }: { members: RoomMember[] } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="member-list">
|
|
||||||
{#each members as member (member.userId)}
|
|
||||||
{@const avatar = member.getMxcAvatarUrl()}
|
|
||||||
<div class="member">
|
|
||||||
{#if avatar}
|
|
||||||
<img
|
|
||||||
class="avatar"
|
|
||||||
src={$matrixClient.mxcUrlToHttp(avatar, 32, 32)}
|
|
||||||
alt={member.name}
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
{@const color = memberColor(member, $theme)}
|
|
||||||
{@const modeColor = $theme.mode === "dark" ? color.dark : color.light}
|
|
||||||
<div
|
|
||||||
style:background={hexFromArgb(modeColor.color)}
|
|
||||||
style:color={hexFromArgb(modeColor.onColor)}
|
|
||||||
class="avatar avatar-placeholder icon"
|
|
||||||
>
|
|
||||||
person
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<span>{member.name}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.avatar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-placeholder {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 8px;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Room } from "matrix-js-sdk";
|
|
||||||
import { matrixClient, currentRoomId } from "./chat";
|
|
||||||
|
|
||||||
let { rooms }: { rooms: Room[] } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="rooms">
|
|
||||||
{#each $matrixClient.getRooms() as room}
|
|
||||||
{@const avatar = room.getMxcAvatarUrl()}
|
|
||||||
<button
|
|
||||||
class:active={$currentRoomId === room.roomId}
|
|
||||||
class="room"
|
|
||||||
onclick={() => ($currentRoomId = room.roomId)}
|
|
||||||
>
|
|
||||||
{#if avatar}
|
|
||||||
<img
|
|
||||||
alt={room.name}
|
|
||||||
src={$matrixClient.mxcUrlToHttp(avatar, 16, 16)}
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div>#</div>
|
|
||||||
{/if}
|
|
||||||
<div>{room.name}</div>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#await $matrixClient.publicRooms()}
|
|
||||||
<div>Loading...</div>
|
|
||||||
{:then rooms}
|
|
||||||
{#each rooms.chunk as room}
|
|
||||||
<button class="room" onclick={() => ($currentRoomId = room.roomId)}>
|
|
||||||
<div>#</div>
|
|
||||||
<div>{room.name}</div>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{:catch error}
|
|
||||||
<div>{error.message}</div>
|
|
||||||
{/await}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.rooms {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 8px;
|
|
||||||
padding-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
padding-block: 2px;
|
|
||||||
padding-block: 4px;
|
|
||||||
width: 100%;
|
|
||||||
height: unset;
|
|
||||||
min-height: 0;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: var(--md-sys-color-primary-container);
|
|
||||||
color: var(--md-sys-color-on-primary-container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type {
|
|
||||||
EventTimeline,
|
|
||||||
MatrixEvent,
|
|
||||||
MsgType,
|
|
||||||
Room,
|
|
||||||
RoomEvent,
|
|
||||||
RoomMember,
|
|
||||||
RoomMemberEvent,
|
|
||||||
} from "matrix-js-sdk";
|
|
||||||
import { onDestroy, onMount, tick } from "svelte";
|
|
||||||
import { matrixClient } from "./chat";
|
|
||||||
import MatrixEventComponent from "./events/MatrixEvent.svelte";
|
|
||||||
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
|
|
||||||
import { ReplayRecorder } from "$lib/charrecorder/core/recorder";
|
|
||||||
import { type Socket, io } from "socket.io-client";
|
|
||||||
import { SvelteMap } from "svelte/reactivity";
|
|
||||||
|
|
||||||
let { timeline }: { timeline: EventTimeline } = $props();
|
|
||||||
|
|
||||||
const excludeEvents = ["m.reaction", "m.room.redaction"];
|
|
||||||
|
|
||||||
let events = $state(
|
|
||||||
timeline
|
|
||||||
.getEvents()
|
|
||||||
.filter((it) => !excludeEvents.includes(it.getType()))
|
|
||||||
.reverse(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let recorder = $state(new ReplayRecorder());
|
|
||||||
let showCursor = $state(false);
|
|
||||||
|
|
||||||
let timelineElement: HTMLElement = $state()!;
|
|
||||||
|
|
||||||
async function onTimeline(
|
|
||||||
event: MatrixEvent,
|
|
||||||
room?: Room,
|
|
||||||
toStartOfTimeline?: boolean,
|
|
||||||
) {
|
|
||||||
if (room?.roomId !== timeline.getRoomId()) return;
|
|
||||||
const sender = event.getSender();
|
|
||||||
if (sender) {
|
|
||||||
live.delete(sender);
|
|
||||||
}
|
|
||||||
if (excludeEvents.includes(event.getType())) return;
|
|
||||||
if (toStartOfTimeline) {
|
|
||||||
events.push(event);
|
|
||||||
} else {
|
|
||||||
const needScroll = timelineElement.scrollTop < 20;
|
|
||||||
events.unshift(event);
|
|
||||||
if (needScroll) {
|
|
||||||
await tick();
|
|
||||||
timelineElement.scroll({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let typing = $state<string[]>([]);
|
|
||||||
|
|
||||||
function onTyping(event: MatrixEvent, member: RoomMember) {
|
|
||||||
typing = event.event.content?.["user_ids"] ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function send() {
|
|
||||||
const roomId = timeline.getRoomId();
|
|
||||||
if (!roomId) return;
|
|
||||||
const finalText = recorder.player.stepper.text
|
|
||||||
.map((token) => token.text)
|
|
||||||
.join("");
|
|
||||||
const finalRecording = recorder.finish();
|
|
||||||
if (!finalText) return;
|
|
||||||
recorder = new ReplayRecorder();
|
|
||||||
await $matrixClient.sendMessage(roomId, {
|
|
||||||
msgtype: "m.text" as MsgType.Text,
|
|
||||||
body: finalText,
|
|
||||||
// @ts-expect-error
|
|
||||||
"m.replay": finalRecording,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKey(event: KeyboardEvent) {
|
|
||||||
if (event.type === "keyup" && event.key === "Enter" && !event.shiftKey) {
|
|
||||||
send();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
recorder.next(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "keyup" && recorder.player.stepper.text.length === 0) {
|
|
||||||
recorder = new ReplayRecorder();
|
|
||||||
} else {
|
|
||||||
socket.emit("message", {
|
|
||||||
timeStamp: event.timeStamp,
|
|
||||||
type: event.type,
|
|
||||||
key: event.key,
|
|
||||||
code: event.code,
|
|
||||||
username: $matrixClient.getUserId(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let socket: Socket = $state()!;
|
|
||||||
let live = new SvelteMap<string, ReplayRecorder>();
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
socket = io("https://srv.charachorder.io");
|
|
||||||
socket.emit("join", timeline.getRoomId());
|
|
||||||
|
|
||||||
socket.on("message", async ({ message }) => {
|
|
||||||
let userRecorder = live.get(message.username);
|
|
||||||
if (!userRecorder) {
|
|
||||||
userRecorder = new ReplayRecorder();
|
|
||||||
live.set(message.username, userRecorder);
|
|
||||||
}
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
|
|
||||||
userRecorder.next(message);
|
|
||||||
|
|
||||||
if (userRecorder.player.stepper.text.length === 0) {
|
|
||||||
live.delete(message.username);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$matrixClient.on("Room.timeline" as RoomEvent.Timeline, onTimeline);
|
|
||||||
$matrixClient.on("RoomMember.typing" as RoomMemberEvent.Typing, onTyping);
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
socket?.disconnect();
|
|
||||||
$matrixClient.off("Room.timeline" as RoomEvent.Timeline, onTimeline);
|
|
||||||
$matrixClient.off("RoomMember.typing" as RoomMemberEvent.Typing, onTyping);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div bind:this={timelineElement} class="timeline">
|
|
||||||
{#each live.entries() as [userId, recorder] (userId)}
|
|
||||||
{@const roomId = timeline.getRoomId()}
|
|
||||||
{#if roomId}
|
|
||||||
{@const room = $matrixClient.getRoom(roomId)}
|
|
||||||
{@const member = room?.getMember(userId)}
|
|
||||||
{#if member}
|
|
||||||
<MatrixEventComponent sender={member} replay={recorder.player} />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{#each events as event, i (event.event["event_id"])}
|
|
||||||
{@const prev = events[i + 1]}
|
|
||||||
<MatrixEventComponent {event} sender={event.sender} {prev} {timeline} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="static-elements">
|
|
||||||
<div class="indicators"></div>
|
|
||||||
<div class="input-box">
|
|
||||||
<button class="icon">add</button>
|
|
||||||
<div
|
|
||||||
role="textbox"
|
|
||||||
tabindex="0"
|
|
||||||
class="input"
|
|
||||||
onkeydown={onKey}
|
|
||||||
onkeyup={onKey}
|
|
||||||
onfocusin={() => (showCursor = true)}
|
|
||||||
onfocusout={() => (showCursor = false)}
|
|
||||||
>
|
|
||||||
<CharRecorder replay={recorder.player} cursor={showCursor} />
|
|
||||||
</div>
|
|
||||||
<button class="icon" onclick={send}>send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
$border-radius: 16px;
|
|
||||||
|
|
||||||
.input {
|
|
||||||
flex-grow: 1;
|
|
||||||
cursor: text;
|
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
border-radius: $border-radius;
|
|
||||||
padding: 0.5em;
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
text-wrap: wrap;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-box {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 4px;
|
|
||||||
padding-block: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.static-elements {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
contain: content;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { derived, writable, type Writable } from "svelte/store";
|
|
||||||
import type {
|
|
||||||
ClientEvent,
|
|
||||||
LoginResponse,
|
|
||||||
MatrixClient,
|
|
||||||
RoomMember,
|
|
||||||
} from "matrix-js-sdk";
|
|
||||||
import { persistentWritable } from "$lib/storage";
|
|
||||||
import {
|
|
||||||
themeFromSourceColor,
|
|
||||||
argbFromHex,
|
|
||||||
type CustomColorGroup,
|
|
||||||
} from "@material/material-color-utilities";
|
|
||||||
import type { UserTheme } from "$lib/preferences";
|
|
||||||
import { MatrixRx } from "./matrix-rx/client";
|
|
||||||
|
|
||||||
export const matrixClient: Writable<MatrixClient> = writable();
|
|
||||||
|
|
||||||
export const isLoggedIn: Writable<boolean> = writable(false);
|
|
||||||
|
|
||||||
export const matrix = derived(
|
|
||||||
[matrixClient, isLoggedIn],
|
|
||||||
([matrixClient, isLoggedIn]) =>
|
|
||||||
isLoggedIn ? new MatrixRx(matrixClient) : undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const currentRoomId = persistentWritable<string | null>(
|
|
||||||
"currentRoomId",
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
function getStoredLogin(): LoginResponse | undefined {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem("matrix-login")!);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function storeLogin(response: LoginResponse) {
|
|
||||||
localStorage.setItem("matrix-login", JSON.stringify(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initMatrixClient() {
|
|
||||||
const { createClient, IndexedDBStore, IndexedDBCryptoStore } = await import(
|
|
||||||
"matrix-js-sdk"
|
|
||||||
);
|
|
||||||
|
|
||||||
const storedLogin = getStoredLogin();
|
|
||||||
|
|
||||||
const store = new IndexedDBStore({
|
|
||||||
dbName: "matrix",
|
|
||||||
indexedDB: window.indexedDB,
|
|
||||||
});
|
|
||||||
const cryptoStore = new IndexedDBCryptoStore(
|
|
||||||
window.indexedDB,
|
|
||||||
"matrix-crypto",
|
|
||||||
);
|
|
||||||
|
|
||||||
const client = createClient({
|
|
||||||
baseUrl: import.meta.env.VITE_MATRIX_URL,
|
|
||||||
userId: storedLogin?.user_id,
|
|
||||||
accessToken: storedLogin?.access_token,
|
|
||||||
timelineSupport: true,
|
|
||||||
store,
|
|
||||||
cryptoStore,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("store");
|
|
||||||
await store.startup();
|
|
||||||
console.log("cryptoStore");
|
|
||||||
await cryptoStore.startup();
|
|
||||||
console.log("client");
|
|
||||||
await client.startClient();
|
|
||||||
client.once("sync" as ClientEvent.Sync, () => {
|
|
||||||
isLoggedIn.set(client.isLoggedIn());
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginToken = new URLSearchParams(window.location.search).get(
|
|
||||||
"loginToken",
|
|
||||||
);
|
|
||||||
if (loginToken) {
|
|
||||||
storeLogin(await client.loginWithToken(loginToken));
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname);
|
|
||||||
isLoggedIn.set(client.isLoggedIn());
|
|
||||||
}
|
|
||||||
|
|
||||||
matrixClient.set(client);
|
|
||||||
console.log("done");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function memberColor(
|
|
||||||
member: RoomMember,
|
|
||||||
theme: UserTheme,
|
|
||||||
): CustomColorGroup {
|
|
||||||
let hash = 0;
|
|
||||||
member.userId.split("").forEach((char) => {
|
|
||||||
hash = char.charCodeAt(0) + ((hash << 5) - hash);
|
|
||||||
});
|
|
||||||
let color = "#";
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const value = (hash >> (i * 8)) & 0xff;
|
|
||||||
color += value.toString(16).padStart(2, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
return themeFromSourceColor(argbFromHex(theme.color), [
|
|
||||||
{ value: argbFromHex(color), name: "member", blend: true },
|
|
||||||
]).customColors.find((c) => c.color.name === "member")!;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { writable, type Writable } from "svelte/store";
|
|
||||||
import type { MatrixClient, RoomMember } from "matrix-js-sdk";
|
|
||||||
import { persistentWritable } from "$lib/storage";
|
|
||||||
import {
|
|
||||||
themeFromSourceColor,
|
|
||||||
argbFromHex,
|
|
||||||
type CustomColorGroup,
|
|
||||||
} from "@material/material-color-utilities";
|
|
||||||
import type { UserTheme } from "$lib/preferences";
|
|
||||||
|
|
||||||
export const matrixClient: Writable<MatrixClient> = writable();
|
|
||||||
|
|
||||||
export const currentRoomId = persistentWritable<string | null>(
|
|
||||||
"currentRoomId",
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
export function memberColor(
|
|
||||||
member: RoomMember,
|
|
||||||
theme: UserTheme,
|
|
||||||
): CustomColorGroup {
|
|
||||||
let hash = 0;
|
|
||||||
member.userId.split("").forEach((char) => {
|
|
||||||
hash = char.charCodeAt(0) + ((hash << 5) - hash);
|
|
||||||
});
|
|
||||||
let color = "#";
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const value = (hash >> (i * 8)) & 0xff;
|
|
||||||
color += value.toString(16).padStart(2, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
return themeFromSourceColor(argbFromHex(theme.color), [
|
|
||||||
{ value: argbFromHex(color), name: "member", blend: true },
|
|
||||||
]).customColors.find((c) => c.color.name === "member")!;
|
|
||||||
}
|
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type {
|
|
||||||
EventTimeline,
|
|
||||||
MatrixEvent,
|
|
||||||
MatrixEventEvent,
|
|
||||||
Relations,
|
|
||||||
RelationsEvent,
|
|
||||||
RoomMember,
|
|
||||||
} from "matrix-js-sdk";
|
|
||||||
import MatrixMessageEvent from "./MatrixMessageEvent.svelte";
|
|
||||||
import { matrixClient, memberColor } from "../chat";
|
|
||||||
import { theme } from "$lib/preferences";
|
|
||||||
import { hexFromArgb } from "@material/material-color-utilities";
|
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import type { Replay } from "$lib/charrecorder/core/types";
|
|
||||||
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
|
|
||||||
import type { ReplayPlayer } from "$lib/charrecorder/core/player";
|
|
||||||
import { onDestroy, onMount } from "svelte";
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
|
|
||||||
let {
|
|
||||||
event,
|
|
||||||
prev,
|
|
||||||
sender,
|
|
||||||
replay: replayPlayer,
|
|
||||||
timeline,
|
|
||||||
}: {
|
|
||||||
event?: MatrixEvent;
|
|
||||||
prev?: MatrixEvent;
|
|
||||||
sender?: RoomMember | null;
|
|
||||||
replay?: Replay | ReplayPlayer;
|
|
||||||
timeline?: EventTimeline;
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
let toolbarHover = $state(false);
|
|
||||||
let mainHover = $state(false);
|
|
||||||
|
|
||||||
let hover = $derived(toolbarHover || mainHover);
|
|
||||||
|
|
||||||
let replay: Replay | undefined = $state();
|
|
||||||
|
|
||||||
let reactions: Relations | undefined = $state(
|
|
||||||
timeline && event?.event.event_id
|
|
||||||
? timeline
|
|
||||||
.getTimelineSet()
|
|
||||||
.relations.getChildEventsForEvent(
|
|
||||||
event.event.event_id,
|
|
||||||
"m.annotation",
|
|
||||||
"m.reaction",
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
let annotations = writable<[string, Set<MatrixEvent>][] | null | undefined>();
|
|
||||||
|
|
||||||
function createRelations() {
|
|
||||||
if (!timeline || !event?.event.event_id) return;
|
|
||||||
reactions?.off("Relations.add" as RelationsEvent.Add, createRelations);
|
|
||||||
reactions?.off(
|
|
||||||
"Relations.remove" as RelationsEvent.Remove,
|
|
||||||
createRelations,
|
|
||||||
);
|
|
||||||
reactions = timeline
|
|
||||||
.getTimelineSet()
|
|
||||||
.relations.getChildEventsForEvent(
|
|
||||||
event.event.event_id,
|
|
||||||
"m.annotation",
|
|
||||||
"m.reaction",
|
|
||||||
);
|
|
||||||
reactions?.on("Relations.add" as RelationsEvent.Add, createRelations);
|
|
||||||
reactions?.on("Relations.remove" as RelationsEvent.Remove, createRelations);
|
|
||||||
reactions?.on(
|
|
||||||
"Relations.redaction" as RelationsEvent.Redaction,
|
|
||||||
createRelations,
|
|
||||||
);
|
|
||||||
annotations.set(
|
|
||||||
reactions?.getSortedAnnotationsByKey()?.filter(([, it]) => it.size > 0),
|
|
||||||
);
|
|
||||||
console.log("create");
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
createRelations();
|
|
||||||
event?.on(
|
|
||||||
"Event.relationsCreated" as MatrixEventEvent.RelationsCreated,
|
|
||||||
createRelations,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
event?.off(
|
|
||||||
"Event.relationsCreated" as MatrixEventEvent.RelationsCreated,
|
|
||||||
createRelations,
|
|
||||||
);
|
|
||||||
reactions?.off("Relations.add" as RelationsEvent.Remove, createRelations);
|
|
||||||
reactions?.off(
|
|
||||||
"Relations.remove" as RelationsEvent.Remove,
|
|
||||||
createRelations,
|
|
||||||
);
|
|
||||||
reactions?.off(
|
|
||||||
"Relations.redaction" as RelationsEvent.Redaction,
|
|
||||||
createRelations,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="event"
|
|
||||||
role="log"
|
|
||||||
onmouseover={() => (mainHover = true)}
|
|
||||||
onfocus={() => (mainHover = true)}
|
|
||||||
onmouseout={() => (mainHover = false)}
|
|
||||||
onblur={() => (mainHover = false)}
|
|
||||||
>
|
|
||||||
{#if event && hover}
|
|
||||||
<div class="backdrop" transition:fade={{ duration: 100 }}></div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if sender && !(prev && prev?.getType() === event?.getType() && prev.sender?.userId === event.sender?.userId)}
|
|
||||||
{@const color = memberColor(sender, $theme)}
|
|
||||||
{@const avatarMxc = sender.getMxcAvatarUrl()}
|
|
||||||
{#if avatarMxc}
|
|
||||||
{@const avatar = $matrixClient.mxcUrlToHttp(avatarMxc, 32, 32)}
|
|
||||||
<img
|
|
||||||
class="avatar"
|
|
||||||
src={avatar}
|
|
||||||
alt={sender.name}
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="avatar avatar-placeholder icon"
|
|
||||||
style:background={hexFromArgb(
|
|
||||||
$theme.mode === "dark" ? color.dark.color : color.light.color,
|
|
||||||
)}
|
|
||||||
style:color={hexFromArgb(
|
|
||||||
$theme.mode === "dark" ? color.dark.onColor : color.light.onColor,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
person
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="sender"
|
|
||||||
style:color={hexFromArgb(
|
|
||||||
$theme.mode === "dark" ? color.dark.color : color.light.color,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<strong>{sender.name}</strong>
|
|
||||||
{#if replay || replayPlayer}
|
|
||||||
<div class="dots">
|
|
||||||
{#each new Array(3) as _, i}
|
|
||||||
<div
|
|
||||||
style:animation-delay={i * 0.2 + "s"}
|
|
||||||
style:background={hexFromArgb(
|
|
||||||
$theme.mode === "dark" ? color.dark.color : color.light.color,
|
|
||||||
)}
|
|
||||||
class="dot"
|
|
||||||
></div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
{#if event}
|
|
||||||
{#if event.getType() === "m.room.message"}
|
|
||||||
<MatrixMessageEvent {event} bind:replay />
|
|
||||||
{:else}
|
|
||||||
<details>
|
|
||||||
<summary>{event.getType()}</summary>
|
|
||||||
<pre>{JSON.stringify(event.event, null, 2)}</pre>
|
|
||||||
</details>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{#if replayPlayer}
|
|
||||||
<CharRecorder replay={replayPlayer} cursor={true} keys={true} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if event && hover}
|
|
||||||
<div
|
|
||||||
role="toolbar"
|
|
||||||
tabindex="0"
|
|
||||||
class="toolbar"
|
|
||||||
transition:fade={{ duration: 100 }}
|
|
||||||
onmouseover={() => (toolbarHover = true)}
|
|
||||||
onfocus={() => (toolbarHover = true)}
|
|
||||||
onmouseout={() => (toolbarHover = false)}
|
|
||||||
onblur={() => (toolbarHover = false)}
|
|
||||||
>
|
|
||||||
{#if event.getType() === "m.room.message"}
|
|
||||||
{@const message = event.event.content?.["body"]}
|
|
||||||
<a
|
|
||||||
class="icon rocket"
|
|
||||||
href="/learn/sentence/?sentence={encodeURIComponent(message)}"
|
|
||||||
>rocket_launch</a
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
<button class="icon">add_reaction</button>
|
|
||||||
<button class="icon">reply</button>
|
|
||||||
{#if event.event.content?.["m.replay"]}
|
|
||||||
{#if replay}
|
|
||||||
<button class="icon" onclick={() => (replay = undefined)}>stop</button
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
class="icon"
|
|
||||||
onclick={() => (replay = event.event.content?.["m.replay"])}
|
|
||||||
>replay</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
<button class="icon">more_horiz</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $annotations && $annotations.length > 0}
|
|
||||||
<div class="reactions">
|
|
||||||
{#each $annotations as [reaction, events]}
|
|
||||||
<button class="reaction"
|
|
||||||
>{reaction} <span class="count">{events.size}</span></button
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
details {
|
|
||||||
opacity: 0.5;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
text-wrap: wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rocket {
|
|
||||||
0% {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
90% {
|
|
||||||
transform: translate(4px, -4px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.icon.rocket {
|
|
||||||
animation: rocket 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
top: -26px;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--md-sys-color-secondary-container);
|
|
||||||
padding: 4px;
|
|
||||||
color: var(--md-sys-color-on-secondary-container);
|
|
||||||
|
|
||||||
a,
|
|
||||||
button {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dots {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
animation: bounce 1s infinite;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender,
|
|
||||||
.avatar {
|
|
||||||
margin-block: 2px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
grid-area: avatar;
|
|
||||||
translate: 0 2px;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.avatar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender {
|
|
||||||
display: flex;
|
|
||||||
grid-area: sender;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reactions {
|
|
||||||
display: flex;
|
|
||||||
grid-area: reactions;
|
|
||||||
gap: 4px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reaction {
|
|
||||||
display: flex;
|
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 6px;
|
|
||||||
height: 24px;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
> .count {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.event {
|
|
||||||
display: grid;
|
|
||||||
position: relative;
|
|
||||||
grid-template-columns: 32px 1fr auto;
|
|
||||||
|
|
||||||
grid-template-areas:
|
|
||||||
"avatar sender date"
|
|
||||||
"avatar content content"
|
|
||||||
"none reactions reactions";
|
|
||||||
margin-inline: 0.5em;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding-inline: 0.5em;
|
|
||||||
padding-block: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
grid-area: content;
|
|
||||||
text-wrap: wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reactions,
|
|
||||||
.content,
|
|
||||||
.sender {
|
|
||||||
margin-inline: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backdrop {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0.25;
|
|
||||||
z-index: -1;
|
|
||||||
inset: 0;
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
|
|
||||||
import type { Replay } from "$lib/charrecorder/core/types";
|
|
||||||
import type { MatrixClient, MatrixEvent } from "matrix-js-sdk";
|
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import { matrixClient } from "../chat";
|
|
||||||
|
|
||||||
let { event, replay = $bindable() }: { event: MatrixEvent; replay?: Replay } =
|
|
||||||
$props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{#if event.event.content?.msgtype === "m.image"}
|
|
||||||
<img
|
|
||||||
src={$matrixClient.mxcUrlToHttp(event.event.content["url"])}
|
|
||||||
alt={event.event.content["body"]}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<span class="content" style:opacity={replay && 0}
|
|
||||||
>{event.event.content?.["body"]}</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{#if replay}
|
|
||||||
<div class="replay" out:fade>
|
|
||||||
<CharRecorder
|
|
||||||
{replay}
|
|
||||||
cursor={true}
|
|
||||||
keys={true}
|
|
||||||
ondone={() => (replay = undefined)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
div {
|
|
||||||
position: relative;
|
|
||||||
min-height: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
border-radius: 8px;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 16em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.replay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import type { Direction, MatrixClient, Room } from "matrix-js-sdk";
|
|
||||||
import {
|
|
||||||
filter,
|
|
||||||
map,
|
|
||||||
type Observable,
|
|
||||||
of,
|
|
||||||
distinctUntilChanged,
|
|
||||||
merge,
|
|
||||||
} from "rxjs";
|
|
||||||
import { fromMatrixClientEvent } from "./events";
|
|
||||||
|
|
||||||
function roomListDistinct(prev: Room[], curr: Room[]) {
|
|
||||||
if (prev.length !== curr.length) return false;
|
|
||||||
for (let i = 0; i < prev.length; i++) {
|
|
||||||
if (prev[i]!.roomId !== curr[i]!.roomId) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MatrixRx {
|
|
||||||
topLevelRooms$: Observable<Room[]>;
|
|
||||||
|
|
||||||
topLevelSpaces$: Observable<Room[]>;
|
|
||||||
|
|
||||||
topLevelChats$: Observable<Room[]>;
|
|
||||||
|
|
||||||
constructor(private client: MatrixClient) {
|
|
||||||
this.topLevelRooms$ = merge(
|
|
||||||
of([]),
|
|
||||||
fromMatrixClientEvent(client, "Room"),
|
|
||||||
fromMatrixClientEvent(client, "deleteRoom"),
|
|
||||||
fromMatrixClientEvent(client, "Room.myMembership"),
|
|
||||||
fromMatrixClientEvent(client, "Room.CurrentStateUpdated").pipe(
|
|
||||||
filter(
|
|
||||||
([_room, prev, curr]) =>
|
|
||||||
prev.getStateEvents("m.space.parent").length !==
|
|
||||||
curr.getStateEvents("m.space.parent").length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).pipe(
|
|
||||||
map(() =>
|
|
||||||
this.client.getVisibleRooms().filter(
|
|
||||||
(room) =>
|
|
||||||
room.getMyMembership() !== "leave" &&
|
|
||||||
room
|
|
||||||
.getLiveTimeline()
|
|
||||||
.getState("f" as Direction.Forward)
|
|
||||||
?.getStateEvents("m.space.parent").length === 0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
distinctUntilChanged(roomListDistinct),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.topLevelSpaces$ = this.topLevelRooms$.pipe(
|
|
||||||
map((rooms) => rooms.filter((room) => room.isSpaceRoom())),
|
|
||||||
distinctUntilChanged(roomListDistinct),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.topLevelChats$ = this.topLevelRooms$.pipe(
|
|
||||||
map((rooms) => rooms.filter((room) => !room.isSpaceRoom())),
|
|
||||||
distinctUntilChanged(roomListDistinct),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SpaceRx {
|
|
||||||
constructor(
|
|
||||||
private client: MatrixClient,
|
|
||||||
private space: Room,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import type { ClientEventHandlerMap, MatrixClient } from "matrix-js-sdk";
|
|
||||||
import { fromEvent, type Observable } from "rxjs";
|
|
||||||
|
|
||||||
export function fromMatrixClientEvent<T extends keyof ClientEventHandlerMap>(
|
|
||||||
client: MatrixClient,
|
|
||||||
eventName: `${T}`, // hack so we can use strings instead of enums
|
|
||||||
): Observable<Parameters<ClientEventHandlerMap[T]>> {
|
|
||||||
return fromEvent(client, eventName) as Observable<
|
|
||||||
Parameters<ClientEventHandlerMap[T]>
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import type {
|
|
||||||
MatrixClient,
|
|
||||||
MatrixEvent,
|
|
||||||
Room,
|
|
||||||
Direction,
|
|
||||||
RoomState,
|
|
||||||
RoomStateEventHandlerMap,
|
|
||||||
EventType,
|
|
||||||
} from "matrix-js-sdk";
|
|
||||||
import { fromMatrixClientEvent } from "./events";
|
|
||||||
import {
|
|
||||||
map,
|
|
||||||
filter,
|
|
||||||
merge,
|
|
||||||
startWith,
|
|
||||||
Observable,
|
|
||||||
of,
|
|
||||||
fromEvent,
|
|
||||||
concat,
|
|
||||||
defer,
|
|
||||||
} from "rxjs";
|
|
||||||
|
|
||||||
export function matrixRoom$(
|
|
||||||
client: MatrixClient,
|
|
||||||
roomId: string | undefined,
|
|
||||||
): Observable<Room | undefined> {
|
|
||||||
return merge([
|
|
||||||
fromMatrixClientEvent(client, "Room").pipe(
|
|
||||||
filter(([room]) => room.roomId === roomId),
|
|
||||||
),
|
|
||||||
fromMatrixClientEvent(client, "deleteRoom").pipe(
|
|
||||||
filter(([id]) => id === roomId),
|
|
||||||
),
|
|
||||||
]).pipe(
|
|
||||||
startWith([]),
|
|
||||||
map(() => client.getRoom(roomId) ?? undefined),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function roomTimeline$(
|
|
||||||
client: MatrixClient,
|
|
||||||
room: Room | undefined,
|
|
||||||
): Observable<MatrixEvent[] | undefined> {
|
|
||||||
if (!room) return of(undefined);
|
|
||||||
const eventTimeline = room.getLiveTimeline();
|
|
||||||
|
|
||||||
return fromMatrixClientEvent(client, "Room.timeline").pipe(
|
|
||||||
filter(
|
|
||||||
([, eventRoom]) =>
|
|
||||||
eventRoom !== undefined && eventRoom.roomId === room.roomId,
|
|
||||||
),
|
|
||||||
startWith([]),
|
|
||||||
map(() => eventTimeline.getEvents()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function roomCurrentStateEvents$(
|
|
||||||
client: MatrixClient,
|
|
||||||
room: Room,
|
|
||||||
eventType: EventType | string,
|
|
||||||
): Observable<MatrixEvent[]> {
|
|
||||||
return concat(
|
|
||||||
defer(() =>
|
|
||||||
of(
|
|
||||||
room
|
|
||||||
.getLiveTimeline()
|
|
||||||
.getState("f" as Direction.Forward)
|
|
||||||
?.getStateEvents(eventType) ?? [],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
fromMatrixClientEvent(client, "Room.CurrentStateUpdated").pipe(
|
|
||||||
filter(([room]) => room.roomId === room.roomId),
|
|
||||||
map(([_room, _prev, curr]) => curr.getStateEvents(eventType)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromRoomStateEvent<T extends keyof RoomStateEventHandlerMap>(
|
|
||||||
state: RoomState,
|
|
||||||
eventName: `${T}`,
|
|
||||||
): Observable<Parameters<RoomStateEventHandlerMap[T]>> {
|
|
||||||
return fromEvent(state, eventName) as Observable<
|
|
||||||
Parameters<RoomStateEventHandlerMap[T]>
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { EventTimeline, MatrixClient, MatrixEvent } from "matrix-js-sdk";
|
|
||||||
import { filter, map, of, startWith, type Observable } from "rxjs";
|
|
||||||
import { fromMatrixClientEvent } from "./events";
|
|
||||||
|
|
||||||
export function roomTimeline(
|
|
||||||
client: MatrixClient,
|
|
||||||
roomId: string | undefined,
|
|
||||||
): Observable<MatrixEvent[]> {
|
|
||||||
if (!roomId) return of([]);
|
|
||||||
const room = client.getRoom(roomId);
|
|
||||||
if (!room) return of([]);
|
|
||||||
const eventTimeline = room.getLiveTimeline();
|
|
||||||
|
|
||||||
return fromMatrixClientEvent(client, "Room.timeline").pipe(
|
|
||||||
filter(([, room]) => room?.roomId === roomId),
|
|
||||||
startWith([]),
|
|
||||||
map(() => eventTimeline.getEvents()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -53,6 +53,12 @@
|
|||||||
<br />
|
<br />
|
||||||
<small>{info.description}</small>
|
<small>{info.description}</small>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if info.breaking}
|
||||||
|
<br /> <i>Prevents prepended autospaces</i>
|
||||||
|
{/if}
|
||||||
|
{#if info.separator || info.breaking}
|
||||||
|
<br /> <i>Stops autocorrect</i>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<b>Unknown Action</b><br />
|
<b>Unknown Action</b><br />
|
||||||
{#if info.code > 1023}
|
{#if info.code > 1023}
|
||||||
@@ -200,13 +206,13 @@
|
|||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
max-width: 15ch;
|
max-width: 15ch;
|
||||||
|
-webkit-line-clamp: 2; /* number of lines to show */
|
||||||
|
line-clamp: 2;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
-webkit-line-clamp: 2; /* number of lines to show */
|
|
||||||
line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import { osLayout } from "$lib/os-layout";
|
|
||||||
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
|
|
||||||
import { persistentWritable } from "$lib/storage";
|
|
||||||
import { type ChordInfo, chords } from "$lib/undo-redo";
|
|
||||||
import { derived } from "svelte/store";
|
|
||||||
|
|
||||||
export const words = derived(
|
|
||||||
[chords, osLayout, KEYMAP_CODES],
|
|
||||||
([chords, layout, KEYMAP_CODES]) =>
|
|
||||||
new Map<string, ChordInfo>(
|
|
||||||
chords
|
|
||||||
.map((chord) => ({
|
|
||||||
chord,
|
|
||||||
output: chord.phrase.map((action) =>
|
|
||||||
layout.get(KEYMAP_CODES.get(action)?.keyCode ?? ""),
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
.filter(({ output }) => output.every((it) => !!it))
|
|
||||||
.map(({ chord, output }) => [output.join("").trim(), chord] as const),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
interface Score {
|
|
||||||
lastTyped: number;
|
|
||||||
score: number;
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const scores = persistentWritable<Record<string, Score>>("scores", {});
|
|
||||||
|
|
||||||
export const learnConfigDefault = {
|
|
||||||
maxScore: 3,
|
|
||||||
minScore: -3,
|
|
||||||
scoreBlend: 0.5,
|
|
||||||
weakRate: 0.8,
|
|
||||||
weakBoost: 0.5,
|
|
||||||
maxWeak: 3,
|
|
||||||
newRate: 0.3,
|
|
||||||
initialNewRate: 0.9,
|
|
||||||
initialCount: 10,
|
|
||||||
};
|
|
||||||
export const learnConfigStored = persistentWritable<
|
|
||||||
Partial<typeof learnConfigDefault>
|
|
||||||
>("learn-config", {});
|
|
||||||
export const learnConfig = derived(learnConfigStored, (config) => ({
|
|
||||||
...learnConfigDefault,
|
|
||||||
...config,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let lastWord: string | undefined;
|
|
||||||
|
|
||||||
function shuffle<T>(array: T[]): T[] {
|
|
||||||
for (let i = array.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[array[i], array[j]] = [array[j]!, array[i]!];
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomLog2<T>(array: T[], max = array.length): T | undefined {
|
|
||||||
return array[
|
|
||||||
Math.floor(Math.pow(2, Math.log2(Math.random() * Math.log2(max))))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const nextWord = derived(
|
|
||||||
[words, scores, learnConfig],
|
|
||||||
([words, scores, config]) => {
|
|
||||||
const values = Object.entries(scores).filter(([it]) => it !== lastWord);
|
|
||||||
|
|
||||||
values.sort(([, a], [, b]) => a.score - b.score);
|
|
||||||
const weakCount =
|
|
||||||
(values.findIndex(([, { score }]) => score > 0) + 1 ||
|
|
||||||
values.length + 1) - 1;
|
|
||||||
const weak = randomLog2(values, weakCount);
|
|
||||||
if (weak && Math.random() / weakCount < config.weakRate) {
|
|
||||||
lastWord = weak[0];
|
|
||||||
return weak[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
values.sort(([, { lastTyped: a }], [, { lastTyped: b }]) => a - b);
|
|
||||||
const recent = randomLog2(values);
|
|
||||||
const newRate =
|
|
||||||
values.length < config.initialCount
|
|
||||||
? config.initialNewRate
|
|
||||||
: config.newRate;
|
|
||||||
if (
|
|
||||||
recent &&
|
|
||||||
(Math.random() < Math.min(1, Math.max(0, weakCount / config.maxWeak)) ||
|
|
||||||
Math.random() > newRate)
|
|
||||||
) {
|
|
||||||
lastWord = recent[0];
|
|
||||||
return recent[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const newWord = shuffle(Array.from(words.keys())).find((it) => !scores[it]);
|
|
||||||
const word = newWord || recent?.[0] || weak?.[0];
|
|
||||||
lastWord = word;
|
|
||||||
return word;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { persistentWritable } from "$lib/storage";
|
|
||||||
|
|
||||||
interface ChordStats {
|
|
||||||
level: number;
|
|
||||||
lastUprank: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const chordStats = persistentWritable<Record<string, ChordStats>>(
|
|
||||||
"chord-stats",
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
1
src/routes/(app)/ccos/+layout.server.ts
Normal file
1
src/routes/(app)/ccos/+layout.server.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const prerender = false;
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { initMatrixClient, isLoggedIn, matrix } from "$lib/chat/chat-rx";
|
|
||||||
import { flip } from "svelte/animate";
|
|
||||||
import { slide } from "svelte/transition";
|
|
||||||
import Login from "./Login.svelte";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { browser } from "$app/environment";
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (browser) {
|
|
||||||
await initMatrixClient();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let { children } = $props();
|
|
||||||
|
|
||||||
let spaces = $derived($matrix?.topLevelSpaces$);
|
|
||||||
|
|
||||||
function spaceShort(name: string) {
|
|
||||||
return name
|
|
||||||
.split(" ")
|
|
||||||
.map((it) => it[0])
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $isLoggedIn}
|
|
||||||
<div class="layout">
|
|
||||||
<nav class="spaces">
|
|
||||||
<a href="/chat/chats" class="icon chats">chat</a>
|
|
||||||
<hr />
|
|
||||||
{#if $spaces}
|
|
||||||
<ul>
|
|
||||||
{#each $spaces as space (space.roomId)}
|
|
||||||
<li animate:flip transition:slide>
|
|
||||||
<a class="space" href="/chat/space/{space.roomId}">
|
|
||||||
{spaceShort(space.name)}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
<button class="icon">add</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Login />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
width: 60%;
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chats {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { matrixClient } from "$lib/chat/chat";
|
|
||||||
|
|
||||||
function passwordLogin() {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $matrixClient}
|
|
||||||
{#await $matrixClient.loginFlows() then flows}
|
|
||||||
{#each flows.flows as flow}
|
|
||||||
{#if flow.type === "m.login.sso"}
|
|
||||||
<a
|
|
||||||
href={$matrixClient.getSsoLoginUrl(`${window.location.origin}/chat/`)}
|
|
||||||
>
|
|
||||||
{#each flow.identity_providers as idp}
|
|
||||||
{#if idp.icon}
|
|
||||||
<img src={$matrixClient.mxcUrlToHttp(idp.icon)} alt={idp.name} />
|
|
||||||
{:else}
|
|
||||||
{idp.name}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</a>
|
|
||||||
{:else if flow.type === "m.login.password"}
|
|
||||||
<form onsubmit={passwordLogin}>
|
|
||||||
<input name="username" type="text" placeholder="Username" />
|
|
||||||
<input name="password" type="password" placeholder="Password" />
|
|
||||||
<button type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
{/if}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { browser } from "$app/environment";
|
|
||||||
import { onDestroy, onMount, setContext } from "svelte";
|
|
||||||
import type {
|
|
||||||
IndexedDBStore,
|
|
||||||
IndexedDBCryptoStore,
|
|
||||||
LoginResponse,
|
|
||||||
} from "matrix-js-sdk";
|
|
||||||
import MatrixTimeline from "$lib/chat/MatrixTimeline.svelte";
|
|
||||||
import { matrixClient, currentRoomId } from "$lib/chat/chat";
|
|
||||||
import MatrixRooms from "$lib/chat/MatrixRooms.svelte";
|
|
||||||
import MatrixRoomMembers from "$lib/chat/MatrixRoomMembers.svelte";
|
|
||||||
|
|
||||||
let loggedIn = $state(false);
|
|
||||||
let ready = $state(false);
|
|
||||||
|
|
||||||
let store: IndexedDBStore;
|
|
||||||
let cryptoStore: IndexedDBCryptoStore;
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (!browser) return;
|
|
||||||
const { createClient, IndexedDBStore, IndexedDBCryptoStore } = await import(
|
|
||||||
"matrix-js-sdk"
|
|
||||||
);
|
|
||||||
|
|
||||||
const storedLogin = getStoredLogin();
|
|
||||||
|
|
||||||
store = new IndexedDBStore({
|
|
||||||
dbName: "matrix",
|
|
||||||
indexedDB: window.indexedDB,
|
|
||||||
});
|
|
||||||
cryptoStore = new IndexedDBCryptoStore(window.indexedDB, "matrix-crypto");
|
|
||||||
|
|
||||||
$matrixClient = createClient({
|
|
||||||
baseUrl: import.meta.env.VITE_MATRIX_URL,
|
|
||||||
userId: storedLogin?.user_id,
|
|
||||||
accessToken: storedLogin?.access_token,
|
|
||||||
timelineSupport: true,
|
|
||||||
store,
|
|
||||||
cryptoStore,
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginToken = new URLSearchParams(window.location.search).get(
|
|
||||||
"loginToken",
|
|
||||||
);
|
|
||||||
if (loginToken) {
|
|
||||||
await handleLogin(await $matrixClient.loginWithToken(loginToken));
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
await postLogin();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function passwordLogin(event: SubmitEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
const form = event.target as HTMLFormElement;
|
|
||||||
const username = (form.elements.namedItem("username") as HTMLInputElement)
|
|
||||||
.value;
|
|
||||||
const password = (form.elements.namedItem("password") as HTMLInputElement)
|
|
||||||
.value;
|
|
||||||
|
|
||||||
await handleLogin(
|
|
||||||
await $matrixClient.loginWithPassword(username, password),
|
|
||||||
);
|
|
||||||
await postLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLogin(response: LoginResponse) {
|
|
||||||
localStorage.setItem("matrix-login", JSON.stringify(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postLogin() {
|
|
||||||
loggedIn = $matrixClient.isLoggedIn();
|
|
||||||
|
|
||||||
if (loggedIn) {
|
|
||||||
await store.startup();
|
|
||||||
await cryptoStore.startup();
|
|
||||||
await $matrixClient.startClient();
|
|
||||||
$matrixClient.once("sync", function (state, prevState, res) {
|
|
||||||
ready = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStoredLogin(): LoginResponse | undefined {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem("matrix-login")!);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if ($matrixClient) {
|
|
||||||
$matrixClient.stopClient();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $matrixClient && loggedIn}
|
|
||||||
{#if ready}
|
|
||||||
<div class="chat">
|
|
||||||
<div class="rooms">
|
|
||||||
<button
|
|
||||||
onclick={() => {
|
|
||||||
$matrixClient.logout(true);
|
|
||||||
$matrixClient.clearStores();
|
|
||||||
localStorage.removeItem("matrix-login");
|
|
||||||
window.location.reload();
|
|
||||||
}}>logout</button
|
|
||||||
>
|
|
||||||
<MatrixRooms rooms={$matrixClient.getRooms()} />
|
|
||||||
</div>
|
|
||||||
{#if $currentRoomId}
|
|
||||||
{@const room = $matrixClient.getRoom($currentRoomId)}
|
|
||||||
{#key room}
|
|
||||||
{#if room}
|
|
||||||
<div class="timeline">
|
|
||||||
<MatrixTimeline timeline={room.getLiveTimeline()} />
|
|
||||||
</div>
|
|
||||||
<div class="members">
|
|
||||||
<MatrixRoomMembers members={room.getJoinedMembers()} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{:else if $matrixClient}
|
|
||||||
{#await $matrixClient.loginFlows() then flows}
|
|
||||||
{#each flows.flows as flow}
|
|
||||||
{#if flow.type === "m.login.sso"}
|
|
||||||
<a
|
|
||||||
href={$matrixClient.getSsoLoginUrl(`${window.location.origin}/chat/`)}
|
|
||||||
>
|
|
||||||
{#each flow.identity_providers as idp}
|
|
||||||
{#if idp.icon}
|
|
||||||
<img src={$matrixClient.mxcUrlToHttp(idp.icon)} alt={idp.name} />
|
|
||||||
{:else}
|
|
||||||
{idp.name}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</a>
|
|
||||||
{:else if flow.type === "m.login.password"}
|
|
||||||
<!-- TODO: unambigous sso
|
|
||||||
<form onsubmit={passwordLogin}>
|
|
||||||
<input name="username" type="text" placeholder="Username" />
|
|
||||||
<input name="password" type="password" placeholder="Password" />
|
|
||||||
<button type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
-->
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.chat {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
> *:not(:last-child) {
|
|
||||||
border-right: 1px solid var(--md-sys-color-outline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rooms {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.members {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -47,19 +47,6 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chord {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compound {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { chords } from "$lib/undo-redo";
|
|
||||||
import { EditorView } from "codemirror";
|
|
||||||
import { actionToValue } from "$lib/chord-editor/action-serializer";
|
|
||||||
import { actionPlugin } from "$lib/chord-editor/action-plugin";
|
|
||||||
import { delimPlugin } from "$lib/chord-editor/chord-delim-plugin";
|
|
||||||
import {
|
|
||||||
drawSelection,
|
|
||||||
dropCursor,
|
|
||||||
highlightActiveLine,
|
|
||||||
highlightSpecialChars,
|
|
||||||
keymap,
|
|
||||||
} from "@codemirror/view";
|
|
||||||
import { history, standardKeymap } from "@codemirror/commands";
|
|
||||||
import "$lib/chord-editor/chords.grammar";
|
|
||||||
import {
|
|
||||||
chordHighlightStyle,
|
|
||||||
chordLanguageSupport,
|
|
||||||
} from "$lib/chord-editor/chords-grammar-plugin";
|
|
||||||
import { syntaxHighlighting } from "@codemirror/language";
|
|
||||||
import { autocompletion } from "@codemirror/autocomplete";
|
|
||||||
import { persistentWritable } from "$lib/storage";
|
|
||||||
import ActionList from "$lib/components/layout/ActionList.svelte";
|
|
||||||
|
|
||||||
const rawCode = persistentWritable("chord-editor-raw-code", false);
|
|
||||||
const showEdits = persistentWritable("chord-editor-show-edits", true);
|
|
||||||
let originalDoc = $derived(
|
|
||||||
$chords
|
|
||||||
.map((chord) => {
|
|
||||||
return (
|
|
||||||
chord.actions
|
|
||||||
.filter((it) => it !== 0)
|
|
||||||
.map((it) => actionToValue(it))
|
|
||||||
.join("") +
|
|
||||||
"=>" +
|
|
||||||
chord.phrase.map((it) => actionToValue(it)).join("")
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.join("\n"),
|
|
||||||
);
|
|
||||||
let editor: HTMLDivElement | undefined = $state(undefined);
|
|
||||||
let view: EditorView;
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!editor) return;
|
|
||||||
view = new EditorView({
|
|
||||||
parent: editor,
|
|
||||||
doc: originalDoc,
|
|
||||||
extensions: [
|
|
||||||
...($rawCode ? [] : [delimPlugin, actionPlugin]),
|
|
||||||
chordLanguageSupport(),
|
|
||||||
autocompletion({ icons: false, selectOnOpen: true }),
|
|
||||||
history(),
|
|
||||||
dropCursor(),
|
|
||||||
syntaxHighlighting(chordHighlightStyle),
|
|
||||||
highlightActiveLine(),
|
|
||||||
drawSelection(),
|
|
||||||
highlightSpecialChars(),
|
|
||||||
keymap.of(standardKeymap),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return () => view.destroy();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<label><input type="checkbox" bind:checked={$rawCode} />View as code</label>
|
|
||||||
<label><input type="checkbox" bind:checked={$showEdits} />Show edits</label>
|
|
||||||
|
|
||||||
<div class="split">
|
|
||||||
<ActionList />
|
|
||||||
<div
|
|
||||||
class="editor"
|
|
||||||
class:hide-edits={!$showEdits}
|
|
||||||
class:raw={$rawCode}
|
|
||||||
bind:this={editor}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.split {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
> :global(:first-child) {
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor:not(.raw) :global(.cm-line) {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor :global(.cm-deletedChunk) {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor {
|
|
||||||
min-width: 600px;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
:global(.cm-tooltip) {
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--md-sys-color-surface-variant);
|
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
|
|
||||||
:global(ul) {
|
|
||||||
font-family: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(li[role="option"][aria-selected="true"]) {
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(completion-section) {
|
|
||||||
margin-block: 8px;
|
|
||||||
border-bottom: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.change-button) {
|
|
||||||
height: 24px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-deletedLineGutter) {
|
|
||||||
background-color: var(--md-sys-color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-changedLineGutter) {
|
|
||||||
background-color: var(--md-sys-color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-changedText) {
|
|
||||||
background: linear-gradient(
|
|
||||||
var(--md-sys-color-primary),
|
|
||||||
var(--md-sys-color-primary)
|
|
||||||
)
|
|
||||||
bottom / 100% 1px no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-gutters) {
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-editor) {
|
|
||||||
outline: none;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-line) {
|
|
||||||
border-bottom: 1px solid transparent;
|
|
||||||
line-height: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-scroller) {
|
|
||||||
overflow: auto;
|
|
||||||
font-family: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-cursor) {
|
|
||||||
border-color: var(--md-sys-color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-changedLine) {
|
|
||||||
background-color: color-mix(
|
|
||||||
in srgb,
|
|
||||||
var(--md-sys-color-primary) 5%,
|
|
||||||
transparent
|
|
||||||
) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-activeLine),
|
|
||||||
:global(.cm-line:hover) {
|
|
||||||
--auto-space-show: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-activeLine) {
|
|
||||||
border-bottom: 1px solid var(--md-sys-color-surface-variant);
|
|
||||||
/*background-color: color-mix(
|
|
||||||
in srgb,
|
|
||||||
var(--md-sys-color-surface-variant) 40%,
|
|
||||||
transparent
|
|
||||||
) !important;*/
|
|
||||||
|
|
||||||
&:not(.cm-changedLine) {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cm-selectionBackground) {
|
|
||||||
background-color: var(--md-sys-color-surface-variant) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><a href="/learn/layout/">Layout</a></li>
|
|
||||||
<li><a href="/learn/chords/">Chords</a></li>
|
|
||||||
<li><a href="/learn/sentence/">Sentences</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
ul {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin: 16px;
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
width: 128px;
|
|
||||||
height: 128px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
|
|
||||||
import { ReplayRecorder } from "$lib/charrecorder/core/recorder";
|
|
||||||
import {
|
|
||||||
words,
|
|
||||||
nextWord,
|
|
||||||
scores,
|
|
||||||
learnConfigDefault,
|
|
||||||
learnConfig,
|
|
||||||
learnConfigStored,
|
|
||||||
} from "$lib/learn/chords";
|
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import ChordActionEdit from "../../config/chords/ChordActionEdit.svelte";
|
|
||||||
import TrackChords from "$lib/charrecorder/TrackChords.svelte";
|
|
||||||
import type { InferredChord } from "$lib/charrecorder/core/types";
|
|
||||||
|
|
||||||
let recorder = $derived(new ReplayRecorder($nextWord));
|
|
||||||
let start = performance.now();
|
|
||||||
$effect(() => {
|
|
||||||
start = recorder && performance.now();
|
|
||||||
});
|
|
||||||
|
|
||||||
let chords: InferredChord[] = $state([]);
|
|
||||||
|
|
||||||
function onkeyboard(event: KeyboardEvent) {
|
|
||||||
recorder.next(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function lerp(a: number, b: number, t: number) {
|
|
||||||
return a + (b - a) * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const [chord] = chords;
|
|
||||||
if (!chord) return;
|
|
||||||
|
|
||||||
console.log(chord);
|
|
||||||
|
|
||||||
if (chord.output.trim() === $nextWord) {
|
|
||||||
scores.update((scores) => {
|
|
||||||
const score = Math.max(
|
|
||||||
$learnConfig.minScore,
|
|
||||||
$learnConfig.maxScore - (performance.now() - start) / 1000,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!scores[$nextWord]) {
|
|
||||||
scores[$nextWord] = {
|
|
||||||
score,
|
|
||||||
lastTyped: performance.now(),
|
|
||||||
total: 1,
|
|
||||||
};
|
|
||||||
return scores;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldScore = scores[$nextWord].score;
|
|
||||||
scores[$nextWord].score = lerp(
|
|
||||||
score,
|
|
||||||
oldScore,
|
|
||||||
$learnConfig.scoreBlend,
|
|
||||||
);
|
|
||||||
scores[$nextWord].lastTyped = performance.now();
|
|
||||||
scores[$nextWord].total += 1;
|
|
||||||
|
|
||||||
return scores;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function skip() {
|
|
||||||
button?.blur();
|
|
||||||
scores.update((scores) => {
|
|
||||||
return scores;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let button = $state<HTMLButtonElement>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h2>WIP</h2>
|
|
||||||
|
|
||||||
<svelte:window onkeydown={onkeyboard} onkeyup={onkeyboard} />
|
|
||||||
|
|
||||||
{#key $nextWord}
|
|
||||||
<h3>
|
|
||||||
{$nextWord}
|
|
||||||
{#if $scores[$nextWord!] === undefined}
|
|
||||||
<sup class="new-word">new</sup>
|
|
||||||
{:else if ($scores[$nextWord!]?.score ?? 0) < 0}
|
|
||||||
<sup class="weak">weak</sup>
|
|
||||||
{/if}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="chord" in:fade>
|
|
||||||
<CharRecorder replay={recorder.player} cursor={true}>
|
|
||||||
<TrackChords bind:chords />
|
|
||||||
</CharRecorder>
|
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
{#key $nextWord}
|
|
||||||
<div class="hint" in:fade={{ delay: 2000, duration: 500 }}>
|
|
||||||
<ChordActionEdit chord={$words.get($nextWord!)} onsubmit={() => {}} />
|
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
<button onclick={skip} bind:this={button}>skip</button>
|
|
||||||
|
|
||||||
<section class="stats">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Weak</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each Object.entries($scores)
|
|
||||||
.sort(([, a], [, b]) => a.score - b.score)
|
|
||||||
.splice(0, 10) as [word, score]}
|
|
||||||
<tr class="decay">
|
|
||||||
<td>{word}</td>
|
|
||||||
<td><i>{score.score.toFixed(2)}</i></td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Strong</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each Object.entries($scores)
|
|
||||||
.sort(([, a], [, b]) => b.score - a.score)
|
|
||||||
.splice(0, 10) as [word, score]}
|
|
||||||
<tr class="decay">
|
|
||||||
<td>{word}</td>
|
|
||||||
<td><i>{score.score.toFixed(2)}</i></td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Rehearse</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each Object.entries($scores)
|
|
||||||
.sort(([, a], [, b]) => b.lastTyped - a.lastTyped)
|
|
||||||
.splice(0, 10) as [word, _score]}
|
|
||||||
<tr class="decay">
|
|
||||||
<td>{word}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Settings</summary>
|
|
||||||
<button onclick={() => ($scores = {})}>Reset</button>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{#each Object.entries(learnConfigDefault) as [key, value]}
|
|
||||||
<tr>
|
|
||||||
<th>{key}</th>
|
|
||||||
<td
|
|
||||||
><input
|
|
||||||
type="number"
|
|
||||||
value={$learnConfig[key as keyof typeof $learnConfig] ?? value}
|
|
||||||
step="0.1"
|
|
||||||
oninput={(event) =>
|
|
||||||
($learnConfigStored[key as keyof typeof $learnConfig] = (
|
|
||||||
event.target as HTMLInputElement
|
|
||||||
).value as any)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
disabled={!$learnConfigStored[key as keyof typeof $learnConfig]}
|
|
||||||
onclick={() =>
|
|
||||||
($learnConfigStored[key as keyof typeof $learnConfigStored] =
|
|
||||||
undefined)}>⟲</button
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use "sass:math";
|
|
||||||
|
|
||||||
input {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
width: 5ch;
|
|
||||||
color: inherit;
|
|
||||||
font: inherit;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1ch;
|
|
||||||
min-width: 20ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
sup {
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 0.8em;
|
|
||||||
|
|
||||||
&.new-word {
|
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
&.weak {
|
|
||||||
color: var(--md-sys-color-error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@for $i from 1 through 10 {
|
|
||||||
tr.decay:nth-child(#{$i}) {
|
|
||||||
opacity: 1 - math.div($i, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { setContext } from "svelte";
|
|
||||||
import Layout from "$lib/components/layout/Layout.svelte";
|
|
||||||
import type { VisualLayoutConfig } from "$lib/components/layout/visual-layout";
|
|
||||||
import { writable, derived } from "svelte/store";
|
|
||||||
import { layout } from "$lib/undo-redo";
|
|
||||||
import Action from "$lib/components/Action.svelte";
|
|
||||||
import { serialPort } from "$lib/serial/connection";
|
|
||||||
|
|
||||||
let hasStarted = $state(false);
|
|
||||||
|
|
||||||
setContext<VisualLayoutConfig>("visual-layout-config", {
|
|
||||||
scale: 50,
|
|
||||||
inactiveScale: 0.5,
|
|
||||||
inactiveOpacity: 0.4,
|
|
||||||
strokeWidth: 1,
|
|
||||||
margin: 5,
|
|
||||||
fontSize: 9,
|
|
||||||
iconFontSize: 14,
|
|
||||||
});
|
|
||||||
|
|
||||||
const actions = derived(layout, (layout) => {
|
|
||||||
const result = new Set<number>();
|
|
||||||
for (const layer of layout) {
|
|
||||||
for (const key of layer) {
|
|
||||||
result.add(key[0].action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [...result];
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentAction = writable(0);
|
|
||||||
|
|
||||||
const expected = derived(
|
|
||||||
[layout, currentAction],
|
|
||||||
([layout, currentAction]) => {
|
|
||||||
const result: Array<{ layer: number; key: number }> = [];
|
|
||||||
for (let layer = 0; layer <= layout.length; layer++) {
|
|
||||||
const layerArr = layout[layer];
|
|
||||||
if (layerArr === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (let key = 0; key <= layerArr.length; key++) {
|
|
||||||
if (layerArr[key]?.[0].action === currentAction) {
|
|
||||||
result.push({ layer, key });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const highlight = derived(
|
|
||||||
expected,
|
|
||||||
(expected) => new Set(expected.map(({ key }) => key)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const highlightAction = derived(
|
|
||||||
currentAction,
|
|
||||||
(currentAction) => new Set([currentAction]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentLayer = writable(0);
|
|
||||||
|
|
||||||
setContext("highlight", highlight);
|
|
||||||
|
|
||||||
setContext("highlight-action", highlightAction);
|
|
||||||
|
|
||||||
setContext("active-layer", currentLayer);
|
|
||||||
|
|
||||||
async function next() {
|
|
||||||
console.log("Next");
|
|
||||||
const nextAction = $actions[Math.floor(Math.random() * $actions.length)];
|
|
||||||
if (nextAction !== undefined) {
|
|
||||||
currentAction.set(nextAction);
|
|
||||||
currentLayer.set($expected[0]?.layer ?? 0);
|
|
||||||
const key = await $serialPort?.queryKey();
|
|
||||||
if ($expected.some(({ key: expectedKey }) => expectedKey === key)) {
|
|
||||||
console.log("Correct", key);
|
|
||||||
} else {
|
|
||||||
console.log("Incorrect", key);
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if ($serialPort && $layout[0]?.[0] && !hasStarted) {
|
|
||||||
hasStarted = true;
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div class="challenge">
|
|
||||||
<Action display="inline-keys" action={$currentAction}></Action>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Layout />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.challenge {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,652 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { page } from "$app/stores";
|
|
||||||
import { SvelteMap } from "svelte/reactivity";
|
|
||||||
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
|
|
||||||
import debounce from "$lib/util/debounce";
|
|
||||||
import { ReplayRecorder } from "$lib/charrecorder/core/recorder";
|
|
||||||
import { shuffleInPlace } from "$lib/util/shuffle";
|
|
||||||
import { fade, fly, slide } from "svelte/transition";
|
|
||||||
import TrackChords from "$lib/charrecorder/TrackChords.svelte";
|
|
||||||
import ChordHud from "$lib/charrecorder/ChordHud.svelte";
|
|
||||||
import type { InferredChord } from "$lib/charrecorder/core/types";
|
|
||||||
import TrackText from "$lib/charrecorder/TrackText.svelte";
|
|
||||||
import { browser } from "$app/environment";
|
|
||||||
import { expoOut } from "svelte/easing";
|
|
||||||
import { goto } from "$app/navigation";
|
|
||||||
import { untrack } from "svelte";
|
|
||||||
import {
|
|
||||||
type PageParam,
|
|
||||||
SENTENCE_TRAINER_PAGE_PARAMS,
|
|
||||||
} from "./configuration";
|
|
||||||
import {
|
|
||||||
AVG_WORD_LENGTH,
|
|
||||||
MILLIS_IN_SECOND,
|
|
||||||
SECONDS_IN_MINUTE,
|
|
||||||
} from "./constants";
|
|
||||||
import { pickNextWord } from "./word-selector";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves parameter from search URL or returns default
|
|
||||||
* @param param {@link PageParam} generic parameter that can be provided
|
|
||||||
* in search url
|
|
||||||
* @return Value of the parameter converted to its type or default value
|
|
||||||
* if parameter is not present in the URL.
|
|
||||||
*/
|
|
||||||
function getParamOrDefault<T>(param: PageParam<T>): T {
|
|
||||||
if (browser) {
|
|
||||||
const value = $page.url.searchParams.get(param.key);
|
|
||||||
if (null !== value) {
|
|
||||||
return param.parse ? param.parse(value) : (value as unknown as T);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return param.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
function viaLocalStorage<T>(key: string, initial: T) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem(key) ?? "");
|
|
||||||
} catch {
|
|
||||||
return initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay to ensure cursor is visible after focus is set.
|
|
||||||
// it is a workaround for conflict between goto call on sentence update
|
|
||||||
// and cursor focus when next word is selected.
|
|
||||||
const CURSOR_FOCUS_DELAY_MS = 10;
|
|
||||||
|
|
||||||
let masteryThresholds: [slow: number, fast: number, title: string][] = $state(
|
|
||||||
viaLocalStorage("mastery-thresholds", [
|
|
||||||
[1500, 1050, "Words"],
|
|
||||||
[3000, 2500, "Pairs"],
|
|
||||||
[5000, 3500, "Trios"],
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
localStorage.removeItem("mastery-thresholds");
|
|
||||||
localStorage.removeItem("idle-timeout");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputSentence = $derived(
|
|
||||||
getParamOrDefault(SENTENCE_TRAINER_PAGE_PARAMS.sentence),
|
|
||||||
);
|
|
||||||
|
|
||||||
const wpmTarget = $derived(
|
|
||||||
getParamOrDefault(SENTENCE_TRAINER_PAGE_PARAMS.wpm),
|
|
||||||
);
|
|
||||||
|
|
||||||
const devTools = $derived(
|
|
||||||
getParamOrDefault(SENTENCE_TRAINER_PAGE_PARAMS.showDevTools),
|
|
||||||
);
|
|
||||||
|
|
||||||
let chordInputContainer: HTMLDivElement | null = null;
|
|
||||||
|
|
||||||
let sentenceWords = $derived(inputSentence.trim().split(/\s+/));
|
|
||||||
|
|
||||||
let inputSentenceLength = $derived(inputSentence.length);
|
|
||||||
let msPerChar = $derived(
|
|
||||||
(1 / ((wpmTarget / SECONDS_IN_MINUTE) * AVG_WORD_LENGTH)) *
|
|
||||||
MILLIS_IN_SECOND,
|
|
||||||
);
|
|
||||||
let totalMs = $derived(inputSentenceLength * msPerChar);
|
|
||||||
let msPerWord = $derived(
|
|
||||||
(inputSentenceLength * msPerChar) / sentenceWords.length,
|
|
||||||
);
|
|
||||||
let currentWord = $state("");
|
|
||||||
let wordStats = new SvelteMap<string, number[]>();
|
|
||||||
let wordMastery = new SvelteMap<string, number>();
|
|
||||||
let text = $state("");
|
|
||||||
let level = $state(0);
|
|
||||||
let bestWPM = $state(0);
|
|
||||||
let wpm = $state(0);
|
|
||||||
let chords: InferredChord[] = $state([]);
|
|
||||||
let recorder = $state(new ReplayRecorder());
|
|
||||||
let idle = $state(true);
|
|
||||||
let idleTime = $state(viaLocalStorage("idle-timeout", 100));
|
|
||||||
|
|
||||||
let idleTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (wpm > bestWPM) {
|
|
||||||
bestWPM = wpm;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (browser && $page.url.searchParams) {
|
|
||||||
selectNextWord();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
localStorage.setItem("idle-timeout", idleTime.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
"mastery-thresholds",
|
|
||||||
JSON.stringify(masteryThresholds),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let words = $derived.by(() => {
|
|
||||||
const words = sentenceWords;
|
|
||||||
switch (level) {
|
|
||||||
case 0: {
|
|
||||||
shuffleInPlace(words);
|
|
||||||
return words;
|
|
||||||
}
|
|
||||||
case 1: {
|
|
||||||
const pairs = [];
|
|
||||||
for (let i = 0; i < words.length - 1; i++) {
|
|
||||||
pairs.push(`${words[i]} ${words[i + 1]}`);
|
|
||||||
}
|
|
||||||
shuffleInPlace(pairs);
|
|
||||||
return pairs;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
const trios = [];
|
|
||||||
for (let i = 0; i < words.length - 2; i++) {
|
|
||||||
trios.push(`${words[i]} ${words[i + 1]} ${words[i + 2]}`);
|
|
||||||
}
|
|
||||||
shuffleInPlace(trios);
|
|
||||||
return trios;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return [inputSentence];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
for (const [word, speeds] of wordStats.entries()) {
|
|
||||||
const level = word.split(" ").length - 1;
|
|
||||||
const masteryThreshold = masteryThresholds[level];
|
|
||||||
if (masteryThreshold === undefined) continue;
|
|
||||||
const averageSpeed = speeds.reduce((a, b) => a + b) / speeds.length;
|
|
||||||
wordMastery.set(
|
|
||||||
word,
|
|
||||||
1 -
|
|
||||||
Math.min(
|
|
||||||
1,
|
|
||||||
Math.max(
|
|
||||||
0,
|
|
||||||
(averageSpeed - masteryThreshold[1]) /
|
|
||||||
(masteryThreshold[0] - masteryThreshold[1]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let progress = $derived(
|
|
||||||
level === masteryThresholds.length
|
|
||||||
? Math.min(1, Math.max(0, bestWPM / wpmTarget))
|
|
||||||
: words.length > 0
|
|
||||||
? words.reduce((a, word) => a + (wordMastery.get(word) ?? 0), 0) /
|
|
||||||
words.length
|
|
||||||
: 0,
|
|
||||||
);
|
|
||||||
let mastered = $derived(
|
|
||||||
words.length > 0
|
|
||||||
? words.filter((it) => wordMastery.get(it) === 1).length / words.length
|
|
||||||
: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (progress === 1 && level < masteryThresholds.length) {
|
|
||||||
level++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function selectNextWord() {
|
|
||||||
const nextWord = pickNextWord(
|
|
||||||
words,
|
|
||||||
wordMastery,
|
|
||||||
untrack(() => currentWord),
|
|
||||||
);
|
|
||||||
currentWord = nextWord;
|
|
||||||
recorder = new ReplayRecorder(nextWord);
|
|
||||||
setTimeout(() => {
|
|
||||||
chordInputContainer?.focus();
|
|
||||||
}, CURSOR_FOCUS_DELAY_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkInput() {
|
|
||||||
if (recorder.player.stepper.challenge.length === 0) return;
|
|
||||||
const replay = recorder.finish(false);
|
|
||||||
const elapsed = replay.finish - replay.start! - idleTime;
|
|
||||||
if (elapsed < masteryThresholds[level]![0]) {
|
|
||||||
const prevStats = wordStats.get(currentWord) ?? [];
|
|
||||||
prevStats.push(elapsed);
|
|
||||||
wordStats.set(currentWord, prevStats.slice(-10));
|
|
||||||
}
|
|
||||||
|
|
||||||
text = "";
|
|
||||||
setTimeout(() => {
|
|
||||||
selectNextWord();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!idle || !text) return;
|
|
||||||
if (text.trim() !== currentWord.trim()) return;
|
|
||||||
if (level === masteryThresholds.length) {
|
|
||||||
const replay = recorder.finish();
|
|
||||||
const elapsed = replay.finish - replay.start!;
|
|
||||||
text = "";
|
|
||||||
recorder = new ReplayRecorder(currentWord);
|
|
||||||
console.log(elapsed, totalMs);
|
|
||||||
wpm = (totalMs / elapsed) * wpmTarget;
|
|
||||||
} else {
|
|
||||||
checkInput();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function onkey(event: KeyboardEvent) {
|
|
||||||
if (idleTimeout) {
|
|
||||||
clearTimeout(idleTimeout);
|
|
||||||
}
|
|
||||||
idle = false;
|
|
||||||
recorder.next(event);
|
|
||||||
idleTimeout = setTimeout(() => {
|
|
||||||
idle = true;
|
|
||||||
}, idleTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSentence(event: Event) {
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
params.set(
|
|
||||||
SENTENCE_TRAINER_PAGE_PARAMS.sentence.key,
|
|
||||||
(event.target as HTMLInputElement).value,
|
|
||||||
);
|
|
||||||
goto(`?${params.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const debouncedUpdateSentence = debounce(
|
|
||||||
updateSentence,
|
|
||||||
getParamOrDefault(SENTENCE_TRAINER_PAGE_PARAMS.textAreaDebounceInMillis),
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleInputAreaKeyDown(event: KeyboardEvent) {
|
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
|
||||||
event.preventDefault(); // Prevent new line.
|
|
||||||
debouncedUpdateSentence.cancel(); // Cancel any pending debounced update
|
|
||||||
updateSentence(event); // Update immediately
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1>Sentence Trainer</h1>
|
|
||||||
<textarea
|
|
||||||
rows="7"
|
|
||||||
cols="80"
|
|
||||||
oninput={debouncedUpdateSentence}
|
|
||||||
onkeydown={handleInputAreaKeyDown}>{untrack(() => inputSentence)}</textarea
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="levels">
|
|
||||||
{#each masteryThresholds as [, , title], i}
|
|
||||||
<button
|
|
||||||
class:active={level === i}
|
|
||||||
class:mastered={i < level || progress === 1}
|
|
||||||
class="threshold"
|
|
||||||
onclick={() => {
|
|
||||||
level = i;
|
|
||||||
selectNextWord();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
<button
|
|
||||||
class:active={level === masteryThresholds.length}
|
|
||||||
class:mastered={masteryThresholds.length < level || progress === 1}
|
|
||||||
class="threshold"
|
|
||||||
onclick={() => {
|
|
||||||
level = masteryThresholds.length;
|
|
||||||
selectNextWord();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{wpmTarget} WPM
|
|
||||||
</button>
|
|
||||||
{#each masteryThresholds as _, i}
|
|
||||||
<div
|
|
||||||
class="progress"
|
|
||||||
style:--progress="{-100 *
|
|
||||||
(1 - (level === i ? progress : i < level ? 1 : 0))}%"
|
|
||||||
style:--mastered="{-100 *
|
|
||||||
(1 - (level === i ? mastered : i < level ? 1 : 0))}%"
|
|
||||||
class:active={level === i}
|
|
||||||
></div>
|
|
||||||
{/each}
|
|
||||||
<div
|
|
||||||
class="progress"
|
|
||||||
style:--progress="-100%"
|
|
||||||
style:--mastered="{-100 *
|
|
||||||
(1 -
|
|
||||||
(level === masteryThresholds.length
|
|
||||||
? progress
|
|
||||||
: masteryThresholds.length < level
|
|
||||||
? 1
|
|
||||||
: 0))}%"
|
|
||||||
class:active={level === masteryThresholds.length}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="sentence">
|
|
||||||
{#each sentenceWords as _, i}
|
|
||||||
{#if i !== sentenceWords.length - 1}
|
|
||||||
{@const word = sentenceWords.slice(i, i + 2).join(" ")}
|
|
||||||
{@const mastery = wordMastery.get(word) ?? 0}
|
|
||||||
<div
|
|
||||||
class="arch"
|
|
||||||
class:mastered={mastery === 1}
|
|
||||||
style:opacity={mastery}
|
|
||||||
style:grid-row={(i % 2) + 1}
|
|
||||||
style:grid-column="{i + 1} / span 2"
|
|
||||||
style:border-bottom="none"
|
|
||||||
></div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{#each sentenceWords as word, i}
|
|
||||||
{@const mastery = wordMastery.get(word)}
|
|
||||||
<div
|
|
||||||
class="word"
|
|
||||||
class:mastered={mastery === 1}
|
|
||||||
style:opacity={mastery ?? 0}
|
|
||||||
style:grid-row={3}
|
|
||||||
style:grid-column={i + 1}
|
|
||||||
>
|
|
||||||
{word}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{#each sentenceWords as _, i}
|
|
||||||
{#if i < sentenceWords.length - 2}
|
|
||||||
{@const word = sentenceWords.slice(i, i + 3).join(" ")}
|
|
||||||
{@const mastery = wordMastery.get(word) ?? 0}
|
|
||||||
<div
|
|
||||||
class="arch"
|
|
||||||
class:mastered={mastery === 1}
|
|
||||||
style:opacity={mastery}
|
|
||||||
style:grid-row={(i % 3) + 4}
|
|
||||||
style:grid-column="{i + 1} / span 3"
|
|
||||||
style:border-top="none"
|
|
||||||
></div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if level === masteryThresholds.length}
|
|
||||||
{@const maxDigits = 4}
|
|
||||||
{@const indices = Array.from({ length: maxDigits }, (_, i) => i)}
|
|
||||||
{@const wpmString = Math.floor(bestWPM).toString().padStart(maxDigits, " ")}
|
|
||||||
<div class="finish" transition:slide>
|
|
||||||
<div
|
|
||||||
class="wpm"
|
|
||||||
style:grid-template-columns="repeat({maxDigits}, 1ch) 1ch auto"
|
|
||||||
style:opacity={progress}
|
|
||||||
style:font-size="3rem"
|
|
||||||
style:color="var(--md-sys-color-{progress === 1
|
|
||||||
? 'primary'
|
|
||||||
: 'on-background'})"
|
|
||||||
style:scale={(progress + 0.5) / 2}
|
|
||||||
>
|
|
||||||
{#each indices as i}
|
|
||||||
{@const char = wpmString[i]}
|
|
||||||
{#key char}
|
|
||||||
<div
|
|
||||||
style:grid-column={i + 1}
|
|
||||||
in:fly={{ y: 20, duration: 1000, easing: expoOut }}
|
|
||||||
out:fly={{ y: -20, duration: 1000, easing: expoOut }}
|
|
||||||
>
|
|
||||||
{char}
|
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
{/each}
|
|
||||||
<div style:grid-column={maxDigits + 3} style:justify-self="start">
|
|
||||||
WPM
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="wpm"
|
|
||||||
style:grid-template-columns="4ch 1ch auto"
|
|
||||||
style:font-size="1.5rem"
|
|
||||||
>
|
|
||||||
{#key wpm}
|
|
||||||
<div
|
|
||||||
style:grid-column={1}
|
|
||||||
style:justify-self="end"
|
|
||||||
transition:fade={{ duration: 200 }}
|
|
||||||
>
|
|
||||||
{Math.floor(wpm)}
|
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
<div style:grid-column={3} style:justify-self="start">WPM</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<ChordHud {chords} />
|
|
||||||
<div class="container">
|
|
||||||
<div
|
|
||||||
bind:this={chordInputContainer}
|
|
||||||
class="input-section"
|
|
||||||
onkeydown={onkey}
|
|
||||||
onkeyup={onkey}
|
|
||||||
tabindex="0"
|
|
||||||
role="textbox"
|
|
||||||
>
|
|
||||||
{#key recorder}
|
|
||||||
<div class="input" transition:fade={{ duration: 200 }}>
|
|
||||||
<CharRecorder replay={recorder.player} cursor={true} keys={true}>
|
|
||||||
<TrackText bind:text />
|
|
||||||
<TrackChords bind:chords />
|
|
||||||
</CharRecorder>
|
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if devTools}
|
|
||||||
<div>Dev Tools</div>
|
|
||||||
<button onclick={reset}>Reset</button>
|
|
||||||
<label>Idle Time <input bind:value={idleTime} /></label>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>Total</th>
|
|
||||||
<td
|
|
||||||
><span style:color="var(--md-sys-color-tertiary)"
|
|
||||||
>{Math.round(totalMs)}</span
|
|
||||||
>ms
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Char</th>
|
|
||||||
<td
|
|
||||||
><span style:color="var(--md-sys-color-tertiary)"
|
|
||||||
>{Math.round(msPerChar)}</span
|
|
||||||
>ms
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Word</th>
|
|
||||||
<td
|
|
||||||
><span style:color="var(--md-sys-color-tertiary)"
|
|
||||||
>{Math.round(msPerWord)}</span
|
|
||||||
>ms
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{#each masteryThresholds as _, i}
|
|
||||||
<tr>
|
|
||||||
<th>L{i + 1}</th>
|
|
||||||
<td><input bind:value={masteryThresholds[i]![0]} /></td>
|
|
||||||
<td><input bind:value={masteryThresholds[i]![1]} /></td>
|
|
||||||
<td><input bind:value={masteryThresholds[i]![2]} /></td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{#each wordStats.entries() as [word, stats]}
|
|
||||||
{@const mastery = wordMastery.get(word) ?? 0}
|
|
||||||
<tr>
|
|
||||||
<th>{word}</th>
|
|
||||||
<td
|
|
||||||
style:color="var(--md-sys-color-{mastery === 1
|
|
||||||
? 'primary'
|
|
||||||
: 'tertiary'})"
|
|
||||||
>{Math.round(mastery * 100)}%
|
|
||||||
</td>
|
|
||||||
{#each stats as stat}
|
|
||||||
<td>{stat}</td>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.levels {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 2px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wpm {
|
|
||||||
display: grid;
|
|
||||||
transition: scale 0.2s ease;
|
|
||||||
width: min-content;
|
|
||||||
|
|
||||||
* {
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.finish {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: repeat(2, 1fr);
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sentence {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: repeat(4, auto);
|
|
||||||
gap: 4px 1ch;
|
|
||||||
margin-block: 1rem;
|
|
||||||
width: min-content;
|
|
||||||
|
|
||||||
.word,
|
|
||||||
.arch {
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
|
|
||||||
&.mastered {
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.arch {
|
|
||||||
border: 2px solid var(--md-sys-color-outline);
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
position: relative;
|
|
||||||
grid-row: 2;
|
|
||||||
border: none;
|
|
||||||
background: var(--md-sys-color-outline-variant);
|
|
||||||
width: auto;
|
|
||||||
height: 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
transform: translateX(var(--progress));
|
|
||||||
background: var(--md-sys-color-outline);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
transform: translateX(var(--mastered));
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.threshold {
|
|
||||||
grid-row: 1;
|
|
||||||
justify-self: center;
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
&.mastered,
|
|
||||||
&.active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mastered {
|
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-section {
|
|
||||||
display: grid;
|
|
||||||
cursor: text;
|
|
||||||
|
|
||||||
:global(.cursor) {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
display: flex;
|
|
||||||
grid-row: 1;
|
|
||||||
grid-column: 1;
|
|
||||||
transition:
|
|
||||||
outline 0.2s ease,
|
|
||||||
border-radius 0.2s ease;
|
|
||||||
margin-block: 1rem;
|
|
||||||
outline: 2px dashed transparent;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 1rem;
|
|
||||||
max-width: 16cm;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-section:focus-within {
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
.input {
|
|
||||||
outline-color: var(--md-sys-color-primary);
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cursor) {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export interface PageParam<T> {
|
|
||||||
key: string;
|
|
||||||
default: T;
|
|
||||||
parse?: (value: string) => T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SENTENCE_TRAINER_PAGE_PARAMS: {
|
|
||||||
sentence: PageParam<string>;
|
|
||||||
wpm: PageParam<number>;
|
|
||||||
showDevTools: PageParam<boolean>;
|
|
||||||
textAreaDebounceInMillis: PageParam<number>;
|
|
||||||
} = {
|
|
||||||
sentence: {
|
|
||||||
key: "sentence",
|
|
||||||
default: "This text has been typed at the speed of thought",
|
|
||||||
},
|
|
||||||
wpm: {
|
|
||||||
key: "wpm",
|
|
||||||
default: 250,
|
|
||||||
parse: (value) => Number(value),
|
|
||||||
},
|
|
||||||
showDevTools: {
|
|
||||||
key: "dev",
|
|
||||||
default: false,
|
|
||||||
parse: (value) => value === "true",
|
|
||||||
},
|
|
||||||
textAreaDebounceInMillis: {
|
|
||||||
key: "debounceMillis",
|
|
||||||
default: 5000,
|
|
||||||
parse: (value) => Number(value),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
// Domain constants
|
|
||||||
export const AVG_WORD_LENGTH = 5;
|
|
||||||
export const SECONDS_IN_MINUTE = 60;
|
|
||||||
export const MILLIS_IN_SECOND = 1000;
|
|
||||||
|
|
||||||
// Error messages.
|
|
||||||
export const TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE =
|
|
||||||
"The sentence is too short to make N-Grams, please enter longer sentence";
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { describe, it, beforeEach, expect, vi } from "vitest";
|
|
||||||
import { pickNextWord } from "./word-selector";
|
|
||||||
import { untrack } from "svelte";
|
|
||||||
import { SvelteMap } from "svelte/reactivity";
|
|
||||||
import { TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE } from "./constants";
|
|
||||||
|
|
||||||
// Mock untrack so it simply executes the callback, allowing us to spy on its usage.
|
|
||||||
vi.mock("svelte", () => ({
|
|
||||||
untrack: vi.fn((fn: any) => fn()),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("pickNextWord", () => {
|
|
||||||
let words: string[];
|
|
||||||
let wordMastery: SvelteMap<string, number>;
|
|
||||||
let currentWord: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
|
|
||||||
// Set up sample words and mastery values.
|
|
||||||
words = ["alpha", "beta", "gamma"];
|
|
||||||
wordMastery = new SvelteMap<string, number>();
|
|
||||||
// For this test, assume none of the words are mastered.
|
|
||||||
words.forEach((word) => wordMastery.set(word, 0));
|
|
||||||
currentWord = "alpha";
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return a word different from current", () => {
|
|
||||||
// Force Math.random() to return a predictable value.
|
|
||||||
vi.spyOn(Math, "random").mockReturnValueOnce(0.3);
|
|
||||||
|
|
||||||
const nextWord = pickNextWord(words, wordMastery, currentWord);
|
|
||||||
|
|
||||||
// Since currentWord ("alpha") should be skipped, we expect next word.
|
|
||||||
expect(nextWord).toBe("beta");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should randomly skip words", () => {
|
|
||||||
// Force Math.random() to return a predictable value.
|
|
||||||
vi.spyOn(Math, "random").mockReturnValueOnce(0.6).mockReturnValueOnce(0.3);
|
|
||||||
|
|
||||||
const nextWord = pickNextWord(words, wordMastery, currentWord);
|
|
||||||
|
|
||||||
// Since currentWord ("alpha") should be skipped as current
|
|
||||||
// and "beta" should be randomly skipped we expect "gamma".
|
|
||||||
expect(nextWord).toBe("gamma");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return current word if all other words were randomly skipped", () => {
|
|
||||||
// Force Math.random() to return a predictable value.
|
|
||||||
vi.spyOn(Math, "random").mockReturnValueOnce(0.6).mockReturnValueOnce(0.6);
|
|
||||||
|
|
||||||
const nextWord = pickNextWord(words, wordMastery, currentWord);
|
|
||||||
|
|
||||||
// Since all other words have been randomly skipped, we expect
|
|
||||||
// current word to be returned.
|
|
||||||
expect(nextWord).toBe("alpha");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("current word should be passed untracked", () => {
|
|
||||||
pickNextWord(words, wordMastery, currentWord);
|
|
||||||
expect(untrack).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE if the words array is empty", () => {
|
|
||||||
const result = pickNextWord([], wordMastery, currentWord);
|
|
||||||
expect(result).toBe(TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE } from "./constants";
|
|
||||||
import { SvelteMap } from "svelte/reactivity";
|
|
||||||
|
|
||||||
export function pickNextWord(
|
|
||||||
words: string[],
|
|
||||||
wordMastery: SvelteMap<string, number>,
|
|
||||||
untrackedCurrentWord: string,
|
|
||||||
) {
|
|
||||||
const unmasteredWords = words
|
|
||||||
.map((it) => [it, wordMastery.get(it) ?? 0] as const)
|
|
||||||
.filter(([, it]) => it !== 1);
|
|
||||||
unmasteredWords.sort(([, a], [, b]) => a - b);
|
|
||||||
let nextWord =
|
|
||||||
unmasteredWords[0]?.[0] ??
|
|
||||||
words[0] ??
|
|
||||||
TOO_SHORT_SENTENCE_FOR_NGRAMS_MESSAGE;
|
|
||||||
// This is important to break infinite loop created by
|
|
||||||
// reading and writing `currentWord` inside $effect rune
|
|
||||||
for (const [word] of unmasteredWords) {
|
|
||||||
if (word === untrackedCurrentWord || Math.random() > 0.5) continue;
|
|
||||||
nextWord = word;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return nextWord;
|
|
||||||
}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { basicSetup, EditorView } from "codemirror";
|
|
||||||
import { javascript, javascriptLanguage } from "@codemirror/lang-javascript";
|
|
||||||
import { defaultKeymap } from "@codemirror/commands";
|
|
||||||
import { keymap } from "@codemirror/view";
|
|
||||||
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
|
||||||
import { tags } from "@lezer/highlight";
|
|
||||||
import LL from "$i18n/i18n-svelte";
|
|
||||||
import type { CompletionContext, Completion } from "@codemirror/autocomplete";
|
|
||||||
import { syntaxTree } from "@codemirror/language";
|
|
||||||
import { serialPort } from "$lib/serial/connection";
|
|
||||||
import examplePlugin from "./example-plugin.js?raw";
|
|
||||||
import {
|
|
||||||
charaMethods,
|
|
||||||
type ChannelCharaEventData,
|
|
||||||
type ChannelResponseEventData,
|
|
||||||
} from "./plugin-types";
|
|
||||||
|
|
||||||
let theme = EditorView.baseTheme({
|
|
||||||
".cm-editor .cm-content": {
|
|
||||||
fontFamily: '"Noto Sans Mono", monospace',
|
|
||||||
},
|
|
||||||
".cm-FoldPlaceholder": {
|
|
||||||
backgroundColor: "var(--md-sys-color-surface-variant)",
|
|
||||||
color: "var(--md-sys-color-on-surface-variant)",
|
|
||||||
},
|
|
||||||
".cm-gutters": {
|
|
||||||
backgroundColor: "var(--md-sys-color-surface-variant)",
|
|
||||||
color: "var(--md-sys-color-on-surface-variant)",
|
|
||||||
borderColor: "var(--md-sys-color-outline)",
|
|
||||||
},
|
|
||||||
".cm-activeLineGutter": {
|
|
||||||
backgroundColor: "var(--md-sys-color-tertiary)",
|
|
||||||
color: "var(--md-sys-color-on-tertiary)",
|
|
||||||
},
|
|
||||||
".cm-activeLine": {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
".cm-cursor": {
|
|
||||||
borderColor: "var(--md-sys-color-on-background)",
|
|
||||||
},
|
|
||||||
".cm-selectionBackground": {
|
|
||||||
background: "transparent !important",
|
|
||||||
backdropFilter: "invert(0.3)",
|
|
||||||
},
|
|
||||||
".cm-tooltip": {
|
|
||||||
backgroundColor: "var(--md-sys-color-background) !important",
|
|
||||||
color: "var(--md-sys-color-on-background)",
|
|
||||||
borderColor: "var(--md-sys-color-outline)",
|
|
||||||
},
|
|
||||||
".cm-tooltip-autocomplete ul li[aria-selected]": {
|
|
||||||
backgroundColor: "var(--md-sys-color-primary) !important",
|
|
||||||
color: "var(--md-sys-color-on-primary) !important",
|
|
||||||
},
|
|
||||||
".cm-completionIcon.cm-completionIcon-keyword::after": {
|
|
||||||
content: "'🗝'",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const highlightStyle = HighlightStyle.define(
|
|
||||||
[
|
|
||||||
{ tag: tags.keyword, color: "var(--md-sys-color-primary)" },
|
|
||||||
{ tag: tags.number, color: "var(--md-sys-color-secondary)" },
|
|
||||||
{ tag: tags.string, color: "var(--md-sys-color-tertiary)" },
|
|
||||||
{
|
|
||||||
tag: tags.comment,
|
|
||||||
color: "var(--md-sys-color-on-background)",
|
|
||||||
opacity: 0.6,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
all: { fontFamily: '"Noto Sans Mono", monospace', fontSize: "14px" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const globalsCompletion: Completion[] = [
|
|
||||||
{ label: "Chara", type: "class", boost: 90 },
|
|
||||||
{ label: "Actions", type: "class", boost: 90 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const actionsCompletion: Completion[] = Array.from(
|
|
||||||
$KEYMAP_CODES,
|
|
||||||
([id, info]) => {
|
|
||||||
const isValidIdentifier =
|
|
||||||
info.id && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(info.id);
|
|
||||||
return {
|
|
||||||
label: info.id
|
|
||||||
? isValidIdentifier
|
|
||||||
? info.id
|
|
||||||
: `["${info.id}"]`
|
|
||||||
: info.id!,
|
|
||||||
displayLabel: info.id,
|
|
||||||
detail: [info.title, `(0x${id.toString(16)})`, info.description]
|
|
||||||
.filter((it) => !!it)
|
|
||||||
.join(" "),
|
|
||||||
section: info.category,
|
|
||||||
boost: isValidIdentifier ? Math.min(info.id?.length ?? 0, 10) + 50 : 40,
|
|
||||||
type: "property",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
).filter((it) => it.label !== undefined);
|
|
||||||
|
|
||||||
const completion = javascriptLanguage.data.of({
|
|
||||||
autocomplete: function completeGlobals(context: CompletionContext) {
|
|
||||||
let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
|
|
||||||
if (nodeBefore.name === "VariableName") {
|
|
||||||
return {
|
|
||||||
from: nodeBefore.from,
|
|
||||||
options: globalsCompletion,
|
|
||||||
};
|
|
||||||
} else if (nodeBefore.name === "Script") {
|
|
||||||
return {
|
|
||||||
from: context.pos,
|
|
||||||
options: globalsCompletion,
|
|
||||||
};
|
|
||||||
} else if (
|
|
||||||
(nodeBefore.name === "PropertyName" || nodeBefore.name === ".") &&
|
|
||||||
nodeBefore.parent?.name === "MemberExpression" &&
|
|
||||||
nodeBefore.parent.firstChild
|
|
||||||
) {
|
|
||||||
const variable = nodeBefore.parent.firstChild;
|
|
||||||
const variableName = context.state.sliceDoc(variable.from, variable.to);
|
|
||||||
if (variableName === "Actions") {
|
|
||||||
return {
|
|
||||||
from:
|
|
||||||
nodeBefore.name === "PropertyName"
|
|
||||||
? nodeBefore.from
|
|
||||||
: nodeBefore.to,
|
|
||||||
options: actionsCompletion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let parent = nodeBefore.prevSibling;
|
|
||||||
while (parent !== null && parent?.name !== "VariableName") {
|
|
||||||
parent = parent.prevSibling;
|
|
||||||
}
|
|
||||||
if (parent) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
editorView = new EditorView({
|
|
||||||
extensions: [
|
|
||||||
basicSetup,
|
|
||||||
javascript(),
|
|
||||||
keymap.of(defaultKeymap),
|
|
||||||
theme,
|
|
||||||
syntaxHighlighting(highlightStyle),
|
|
||||||
completion,
|
|
||||||
],
|
|
||||||
parent: editor,
|
|
||||||
doc: examplePlugin,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let channels = $derived.by(() => {
|
|
||||||
if (!$serialPort) return {} as any;
|
|
||||||
return {
|
|
||||||
getVersion: (..._args: unknown[]) => Promise.resolve($serialPort.version),
|
|
||||||
getDevice: (..._args: unknown[]) => Promise.resolve($serialPort.device),
|
|
||||||
commit: (..._args: unknown[]) => {
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
"Perform a commit? Settings are already applied until the next reboot.\n\n" +
|
|
||||||
"Excessive commits can lead to premature breakdowns, as the settings storage is only rated for 10,000-25,000 commits.\n\n" +
|
|
||||||
"Click OK to perform the commit anyways.",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return Promise.resolve($serialPort.commit());
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
...Object.fromEntries(
|
|
||||||
charaMethods.map(
|
|
||||||
(it) => [it, $serialPort[it].bind($serialPort)] as const,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
} satisfies Record<string, Function>;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function onMessage(event: MessageEvent) {
|
|
||||||
if (event.origin !== "null" || event.source !== frame.contentWindow) return;
|
|
||||||
|
|
||||||
const [channel, params] = event.data;
|
|
||||||
const response = channels[channel as keyof typeof channels](...params);
|
|
||||||
frame.contentWindow!.postMessage(
|
|
||||||
{ response: await response } satisfies ChannelResponseEventData,
|
|
||||||
"*",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function runPlugin() {
|
|
||||||
frame.contentWindow?.postMessage(
|
|
||||||
{
|
|
||||||
actionCodes: $KEYMAP_CODES,
|
|
||||||
script: editorView.state.doc.toString(),
|
|
||||||
charaChannels: Object.keys(channels),
|
|
||||||
} satisfies ChannelCharaEventData,
|
|
||||||
"*",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let frame: HTMLIFrameElement;
|
|
||||||
let editor: HTMLDivElement;
|
|
||||||
let editorView: EditorView;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window onmessage={onMessage} />
|
|
||||||
<section>
|
|
||||||
<h3>Plugin</h3>
|
|
||||||
<button onclick={runPlugin}
|
|
||||||
><span class="icon">play_arrow</span>{$LL.plugin.editor.RUN()}</button
|
|
||||||
>
|
|
||||||
<div class="editor-root" bind:this={editor}></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<iframe
|
|
||||||
aria-hidden="true"
|
|
||||||
title="code sandbox"
|
|
||||||
bind:this={frame}
|
|
||||||
src="/sandbox/"
|
|
||||||
sandbox="allow-scripts"
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
padding-inline-start: 0;
|
|
||||||
padding-inline-end: 8px;
|
|
||||||
|
|
||||||
width: min-content;
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-root {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
/*******************************
|
|
||||||
* HOLD UP AND READ THIS FIRST *
|
|
||||||
*******************************
|
|
||||||
*
|
|
||||||
* Chara devices have a LIMITED number of commits.
|
|
||||||
* calling `Chara.commit()` can be a dangerous operation, which is why a confirmation dialog will be shown.
|
|
||||||
* Devices are only rated for 10,000-25,000 commits, exceeding that limit may result in premature breakdowns.
|
|
||||||
* `Chara.setSetting` or `Chara.setLayoutKey` is not affected by this, they last however only until the next boot.
|
|
||||||
*
|
|
||||||
* Chord writing is more forgiving, but keep in mind that excessive large-scale writing can still damage the device.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
const count = await Chara.getChordCount(); // => 499
|
|
||||||
const chord = await Chara.getChord(2); // => {actions: [1, 2, 3], phrase: [4, 5, 6]}
|
|
||||||
|
|
||||||
const setting = await Chara.getSetting(5); // => 0
|
|
||||||
|
|
||||||
// This, for example, would return all chords
|
|
||||||
const chords = [];
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
chords.push(await Chara.getChord(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can also print values to the browser console (F12)
|
|
||||||
console.log("Chords:", chords);
|
|
||||||
|
|
||||||
// You can access the actions by ID!
|
|
||||||
Actions.SPACE; // => {id: "SPACE", code: 32, icon: "space_bar", description: ...}
|
|
||||||
Actions[32]; // This also works
|
|
||||||
Actions[0x20]; // Or this!
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import type { CharaDevice } from "$lib/serial/device";
|
|
||||||
import type { KeyInfo } from "$lib/serial/keymap-codes";
|
|
||||||
|
|
||||||
export const charaMethods = [
|
|
||||||
"reboot",
|
|
||||||
"bootloader",
|
|
||||||
"getRamBytesAvailable",
|
|
||||||
"getSetting",
|
|
||||||
"setSetting",
|
|
||||||
"getLayoutKey",
|
|
||||||
"setLayoutKey",
|
|
||||||
"deleteChord",
|
|
||||||
"setChord",
|
|
||||||
"getChordPhrase",
|
|
||||||
"getChordCount",
|
|
||||||
"getChord",
|
|
||||||
"send",
|
|
||||||
] as const satisfies Array<keyof CharaDevice>;
|
|
||||||
|
|
||||||
export interface ChannelResponseEventData {
|
|
||||||
response: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChannelCharaEventData {
|
|
||||||
charaChannels: string[];
|
|
||||||
script: string;
|
|
||||||
actionCodes: Map<number, KeyInfo>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChannelEventData = ChannelResponseEventData | ChannelCharaEventData;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import AnimatedNumber from "$lib/components/AnimatedNumber.svelte";
|
|
||||||
import { onDestroy, onMount } from "svelte";
|
|
||||||
|
|
||||||
let interval: ReturnType<typeof setInterval>;
|
|
||||||
|
|
||||||
let value = $state(Math.round(Math.random() * 100));
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
interval = setInterval(() => {
|
|
||||||
value = Math.round(Math.random() * 100);
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
clearInterval(interval);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<p>The number is <AnimatedNumber {value} /></p>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<script>
|
|
||||||
/** @type {Promise<unknown> | undefined} */
|
|
||||||
let ongoingRequest = undefined;
|
|
||||||
/** @type {(data: unknown) => void | undefined} */
|
|
||||||
let resolveRequest = undefined;
|
|
||||||
/** @type {MessageEventSource | undefined} */
|
|
||||||
let source = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} channel
|
|
||||||
* @param {unknown} args
|
|
||||||
* @returns {Promise<unknown>}
|
|
||||||
*/
|
|
||||||
async function post(channel, args) {
|
|
||||||
while (ongoingRequest) {
|
|
||||||
await ongoingRequest;
|
|
||||||
}
|
|
||||||
ongoingRequest = new Promise((resolve) => {
|
|
||||||
resolveRequest = resolve;
|
|
||||||
source?.postMessage([channel, args], { targetOrigin: "*" });
|
|
||||||
});
|
|
||||||
ongoingRequest.then(() => {
|
|
||||||
ongoingRequest = undefined;
|
|
||||||
});
|
|
||||||
return ongoingRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MessageEvent<import('../../src/routes/plugin/plugin-types').ChannelEventData>} event
|
|
||||||
*/
|
|
||||||
function onMessage(event) {
|
|
||||||
if ("response" in event.data) {
|
|
||||||
resolveRequest?.(event.data.response);
|
|
||||||
} else {
|
|
||||||
source = event.source ?? undefined;
|
|
||||||
|
|
||||||
const Action = event.data.actionCodes;
|
|
||||||
Object.assign(
|
|
||||||
Action,
|
|
||||||
Object.fromEntries(
|
|
||||||
Object.values(event.data.actionCodes)
|
|
||||||
.filter((it) => !!it.id)
|
|
||||||
.map((it) => [it.id, it]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
new Function("Action", "Chara", event.data.script)(
|
|
||||||
Action,
|
|
||||||
Object.fromEntries(
|
|
||||||
event.data.charaChannels.map((name) => [
|
|
||||||
name,
|
|
||||||
(...args) => post(name, args),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("message", onMessage);
|
|
||||||
</script>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
FROM llama3.1
|
|
||||||
|
|
||||||
TEMPLATE """
|
|
||||||
<|system|>
|
|
||||||
Only output the sentence
|
|
||||||
<|user|>
|
|
||||||
Create a typical sentence with around 10 words that includes the word "{{ .Prompt }}".
|
|
||||||
<|assistant|>
|
|
||||||
Create a sentence that includes "{{ .Prompt }}".
|
|
||||||
"""
|
|
||||||
@@ -62,7 +62,6 @@ export default defineConfig({
|
|||||||
"client/**/*.{js,css,ico,woff2,csv,png,webp,svg,webmanifest}",
|
"client/**/*.{js,css,ico,woff2,csv,png,webp,svg,webmanifest}",
|
||||||
"prerendered/**/*.html",
|
"prerendered/**/*.html",
|
||||||
],
|
],
|
||||||
globIgnores: ["prerendered/pages/ccos/**/*"],
|
|
||||||
ignoreURLParametersMatching: [/^import|redirectUrl|loginToken$/],
|
ignoreURLParametersMatching: [/^import|redirectUrl|loginToken$/],
|
||||||
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user