mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-09 05:09:39 +00:00
Interface conversion, search bar layout
This commit is contained in:
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
# Ignore semantic UI in GitHub Language Statistics
|
||||
src/assets/semantic/** linguist-vendored
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -24,7 +24,7 @@
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
"[typescript]": {
|
||||
|
||||
13
angular.json
13
angular.json
@@ -85,6 +85,14 @@
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
@@ -99,9 +107,10 @@
|
||||
"buildTarget": "Bridge:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular:build:development"
|
||||
"buildTarget": "Bridge:build:development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"bootstrap-icons": "^1.11.2",
|
||||
"bottleneck": "^2.19.5",
|
||||
"comparators": "^3.0.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"electron-unhandled": "^4.0.1",
|
||||
"electron-updater": "^4.3.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
@@ -45,7 +46,9 @@
|
||||
"rimraf": "^5.0.5",
|
||||
"rxjs": "~7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"scan-chart": "^3.4.1",
|
||||
"tslib": "^2.0.0",
|
||||
"zod": "^3.22.4",
|
||||
"zone.js": "~0.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
298
pnpm-lock.yaml
generated
298
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ dependencies:
|
||||
comparators:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.5
|
||||
dayjs:
|
||||
specifier: ^1.11.10
|
||||
version: 1.11.10
|
||||
electron-unhandled:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@@ -65,9 +68,15 @@ dependencies:
|
||||
sanitize-filename:
|
||||
specifier: ^1.6.3
|
||||
version: 1.6.3
|
||||
scan-chart:
|
||||
specifier: ^3.4.1
|
||||
version: 3.4.1
|
||||
tslib:
|
||||
specifier: ^2.0.0
|
||||
version: 2.6.2
|
||||
zod:
|
||||
specifier: ^3.22.4
|
||||
version: 3.22.4
|
||||
zone.js:
|
||||
specifier: ~0.14.2
|
||||
version: 0.14.2
|
||||
@@ -3811,7 +3820,6 @@ packages:
|
||||
|
||||
/async@3.2.5:
|
||||
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
|
||||
dev: true
|
||||
|
||||
/asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
@@ -3867,6 +3875,10 @@ packages:
|
||||
dequal: 2.0.3
|
||||
dev: true
|
||||
|
||||
/b4a@1.6.4:
|
||||
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
|
||||
dev: false
|
||||
|
||||
/babel-loader@9.1.3(@babel/core@7.23.2)(webpack@5.89.0):
|
||||
resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==}
|
||||
engines: {node: '>= 14.15.0'}
|
||||
@@ -3934,7 +3946,6 @@ packages:
|
||||
|
||||
/base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
dev: true
|
||||
|
||||
/base64id@2.0.0:
|
||||
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
||||
@@ -3959,13 +3970,17 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/binary-parser@2.2.1:
|
||||
resolution: {integrity: sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
dev: true
|
||||
|
||||
/bluebird-lst@1.0.9:
|
||||
resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==}
|
||||
@@ -4163,7 +4178,6 @@ packages:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
dev: true
|
||||
|
||||
/builder-util-runtime@8.9.2:
|
||||
resolution: {integrity: sha512-rhuKm5vh7E0aAmT6i8aoSfEjxzdYEFX7zDApK+eNgOhjofnWb74d9SRJv0H/8nsgOkos0TZ4zxW0P8J4N7xQ2A==}
|
||||
@@ -4337,6 +4351,10 @@ packages:
|
||||
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
|
||||
dev: true
|
||||
|
||||
/charset-detector@0.0.2:
|
||||
resolution: {integrity: sha512-aZBFdf9aE168W7w3t9JNC0w9hZdTKjtq1hsrfbziJA4DrNI22Mfrc74gw3aADIA9bCQV2IpnbjyQcDHa3qM4rg==}
|
||||
dev: false
|
||||
|
||||
/chokidar@3.5.3:
|
||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
@@ -4352,6 +4370,10 @@ packages:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
dev: false
|
||||
|
||||
/chownr@2.0.0:
|
||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4465,6 +4487,21 @@ packages:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
requiresBuild: true
|
||||
|
||||
/color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
dev: false
|
||||
|
||||
/color@4.2.3:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
dev: false
|
||||
|
||||
/colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
dev: true
|
||||
@@ -4780,6 +4817,10 @@ packages:
|
||||
'@babel/runtime': 7.23.2
|
||||
dev: true
|
||||
|
||||
/dayjs@1.11.10:
|
||||
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
|
||||
dev: false
|
||||
|
||||
/debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
@@ -4831,7 +4872,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
dev: true
|
||||
|
||||
/deep-extend@0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dev: false
|
||||
|
||||
/deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
@@ -4930,6 +4975,11 @@ packages:
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dev: true
|
||||
|
||||
/detect-libc@2.0.2:
|
||||
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/detect-node@2.1.0:
|
||||
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
|
||||
dev: true
|
||||
@@ -5240,7 +5290,6 @@ packages:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
dev: true
|
||||
|
||||
/engine.io-client@6.5.3:
|
||||
resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==}
|
||||
@@ -5760,7 +5809,6 @@ packages:
|
||||
/events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
dev: true
|
||||
|
||||
/execa@5.1.1:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
@@ -5792,6 +5840,11 @@ packages:
|
||||
strip-final-newline: 3.0.0
|
||||
dev: true
|
||||
|
||||
/expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/exponential-backoff@3.1.1:
|
||||
resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
|
||||
dev: true
|
||||
@@ -5873,6 +5926,10 @@ packages:
|
||||
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
||||
dev: true
|
||||
|
||||
/fast-fifo@1.3.2:
|
||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||
dev: false
|
||||
|
||||
/fast-glob@3.3.1:
|
||||
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
@@ -6041,6 +6098,14 @@ packages:
|
||||
resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
|
||||
dev: true
|
||||
|
||||
/fluent-ffmpeg@2.1.2:
|
||||
resolution: {integrity: sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
async: 3.2.5
|
||||
which: 1.3.1
|
||||
dev: false
|
||||
|
||||
/follow-redirects@1.15.3(debug@4.3.2):
|
||||
resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==}
|
||||
engines: {node: '>=4.0'}
|
||||
@@ -6091,7 +6156,6 @@ packages:
|
||||
|
||||
/fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
dev: true
|
||||
|
||||
/fs-extra@10.1.0:
|
||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||
@@ -6229,6 +6293,10 @@ packages:
|
||||
get-intrinsic: 1.2.2
|
||||
dev: true
|
||||
|
||||
/github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
dev: false
|
||||
|
||||
/glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -6654,7 +6722,6 @@ packages:
|
||||
/ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
/ignore-by-default@1.0.1:
|
||||
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
||||
@@ -6725,6 +6792,10 @@ packages:
|
||||
/inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
/ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
dev: false
|
||||
|
||||
/ini@4.1.1:
|
||||
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
@@ -6786,6 +6857,10 @@ packages:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
dev: true
|
||||
|
||||
/is-arrayish@0.3.2:
|
||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||
dev: false
|
||||
|
||||
/is-bigint@1.0.4:
|
||||
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
||||
dependencies:
|
||||
@@ -7513,6 +7588,19 @@ packages:
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/midievents@2.0.0:
|
||||
resolution: {integrity: sha512-8ABFjr+IKj1ZMAERj0jo99v/ifXU6kCbx9QKj4lM5GccaU81FPk+OVgt3FptGF9zcEsGJY74pl6juRkXDQJs+A==}
|
||||
engines: {node: '>=6.9.5'}
|
||||
dev: false
|
||||
|
||||
/midifile@2.0.0:
|
||||
resolution: {integrity: sha512-JQw8RzJ3auOKR/fWIgt6gqKXKrPdq/AZjToQroqTjL0heBa7Ra0EvzRf/y/RDAC/UGMs6eIwB7jLS3nc9DHxZQ==}
|
||||
engines: {node: '>=6.9.5'}
|
||||
dependencies:
|
||||
midievents: 2.0.0
|
||||
utf-8: 2.0.0
|
||||
dev: false
|
||||
|
||||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -7560,7 +7648,6 @@ packages:
|
||||
/mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/mini-css-extract-plugin@2.7.6(webpack@5.89.0):
|
||||
resolution: {integrity: sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==}
|
||||
@@ -7683,6 +7770,10 @@ packages:
|
||||
resolution: {integrity: sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==}
|
||||
dev: true
|
||||
|
||||
/mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
dev: false
|
||||
|
||||
/mkdirp@0.5.6:
|
||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||
hasBin: true
|
||||
@@ -7754,6 +7845,10 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/napi-build-utils@1.0.2:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
||||
dev: false
|
||||
|
||||
/natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
dev: true
|
||||
@@ -7796,6 +7891,13 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/node-abi@3.52.0:
|
||||
resolution: {integrity: sha512-JJ98b02z16ILv7859irtXn4oUaFWADtvkzy2c0IAatNVX2Mc9Yoh8z6hZInn3QwvMEYhHuQloYi+TTQy67SIdQ==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
semver: 7.5.4
|
||||
dev: false
|
||||
|
||||
/node-addon-api@1.7.2:
|
||||
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
|
||||
requiresBuild: true
|
||||
@@ -7808,6 +7910,10 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/node-addon-api@6.1.0:
|
||||
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
|
||||
dev: false
|
||||
|
||||
/node-forge@1.3.1:
|
||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
@@ -8329,6 +8435,13 @@ packages:
|
||||
engines: {node: '>= 0.10'}
|
||||
dev: true
|
||||
|
||||
/parse-sng@3.1.0:
|
||||
resolution: {integrity: sha512-cZerIg4Ds2BZ6eSp0mg0JNP0r104DId88kGDjvNeQqmuo8tYBZKRHDEzhTlG5JNVaRGcxec5YWqX2edVKLEgoA==}
|
||||
dependencies:
|
||||
binary-parser: 2.2.1
|
||||
events: 3.3.0
|
||||
dev: false
|
||||
|
||||
/parse5-html-rewriting-stream@7.0.0:
|
||||
resolution: {integrity: sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==}
|
||||
dependencies:
|
||||
@@ -8593,6 +8706,25 @@ packages:
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/prebuild-install@7.1.1:
|
||||
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
detect-libc: 2.0.2
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.8
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 1.0.2
|
||||
node-abi: 3.52.0
|
||||
pump: 3.0.0
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.1
|
||||
tunnel-agent: 0.6.0
|
||||
dev: false
|
||||
|
||||
/prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -8703,7 +8835,6 @@ packages:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
once: 1.4.0
|
||||
dev: true
|
||||
|
||||
/punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
@@ -8721,6 +8852,10 @@ packages:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
dev: true
|
||||
|
||||
/queue-tick@1.0.1:
|
||||
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
|
||||
dev: false
|
||||
|
||||
/quick-lru@5.1.1:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8747,6 +8882,16 @@ packages:
|
||||
unpipe: 1.0.0
|
||||
dev: true
|
||||
|
||||
/rc@1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
dev: false
|
||||
|
||||
/react-is@18.2.0:
|
||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||
dev: true
|
||||
@@ -8805,7 +8950,6 @@ packages:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
dev: true
|
||||
|
||||
/readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
@@ -9049,7 +9193,6 @@ packages:
|
||||
|
||||
/safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
dev: true
|
||||
|
||||
/safe-regex-test@1.0.0:
|
||||
resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
|
||||
@@ -9105,6 +9248,22 @@ packages:
|
||||
/sax@1.3.0:
|
||||
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
|
||||
|
||||
/scan-chart@3.4.1:
|
||||
resolution: {integrity: sha512-nr93PLeoZSz9ZzC/RrJ5aLrmLdLaw5Ipgsg+QreeFZjppohhivSa3WFLwXcIDu4fd8l5aa1Ch/6hZiQ6eBBnEw==}
|
||||
dependencies:
|
||||
bottleneck: 2.19.5
|
||||
charset-detector: 0.0.2
|
||||
fluent-ffmpeg: 2.1.2
|
||||
lodash: 4.17.21
|
||||
midievents: 2.0.0
|
||||
midifile: 2.0.0
|
||||
parse-sng: 3.1.0
|
||||
sanitize-filename: 1.6.3
|
||||
sharp: 0.32.6
|
||||
stream-audio-fingerprint: github.com/Geomitron/stream-audio-fingerprint/197e8a7ff60165b18bf1debc23dab4814996a20d
|
||||
workerpool: 6.5.1
|
||||
dev: false
|
||||
|
||||
/schema-utils@3.3.0:
|
||||
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
@@ -9315,6 +9474,21 @@ packages:
|
||||
kind-of: 6.0.3
|
||||
dev: true
|
||||
|
||||
/sharp@0.32.6:
|
||||
resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==}
|
||||
engines: {node: '>=14.15.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
color: 4.2.3
|
||||
detect-libc: 2.0.2
|
||||
node-addon-api: 6.1.0
|
||||
prebuild-install: 7.1.1
|
||||
semver: 7.5.4
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 3.0.4
|
||||
tunnel-agent: 0.6.0
|
||||
dev: false
|
||||
|
||||
/shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -9357,6 +9531,24 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
dev: false
|
||||
|
||||
/simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
dev: false
|
||||
|
||||
/simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
dev: false
|
||||
|
||||
/simple-update-notifier@1.1.0:
|
||||
resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
@@ -9611,6 +9803,13 @@ packages:
|
||||
limiter: 1.1.5
|
||||
dev: true
|
||||
|
||||
/streamx@2.15.5:
|
||||
resolution: {integrity: sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg==}
|
||||
dependencies:
|
||||
fast-fifo: 1.3.2
|
||||
queue-tick: 1.0.1
|
||||
dev: false
|
||||
|
||||
/string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -9662,7 +9861,6 @@ packages:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/strip-ansi@3.0.1:
|
||||
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
|
||||
@@ -9698,6 +9896,11 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/strip-json-comments@3.1.1:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -9816,6 +10019,23 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/tar-fs@2.1.1:
|
||||
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
|
||||
dependencies:
|
||||
chownr: 1.1.4
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.0
|
||||
tar-stream: 2.2.0
|
||||
dev: false
|
||||
|
||||
/tar-fs@3.0.4:
|
||||
resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==}
|
||||
dependencies:
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.0
|
||||
tar-stream: 3.1.6
|
||||
dev: false
|
||||
|
||||
/tar-stream@2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -9825,7 +10045,14 @@ packages:
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
dev: true
|
||||
|
||||
/tar-stream@3.1.6:
|
||||
resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==}
|
||||
dependencies:
|
||||
b4a: 1.6.4
|
||||
fast-fifo: 1.3.2
|
||||
streamx: 2.15.5
|
||||
dev: false
|
||||
|
||||
/tar@6.2.0:
|
||||
resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==}
|
||||
@@ -10020,6 +10247,12 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -10196,12 +10429,16 @@ packages:
|
||||
punycode: 2.3.1
|
||||
dev: true
|
||||
|
||||
/utf-8@2.0.0:
|
||||
resolution: {integrity: sha512-DItg/Z20ltBzugPrb8Mx1oN0F8CqN5bD38T57YM/pF/GOzUsNVXiellI0PbJPq3e1Z7BEDNoWP1H1+4n7g54Cg==}
|
||||
engines: {node: '>=6.9.5'}
|
||||
dev: false
|
||||
|
||||
/utf8-byte-length@1.0.4:
|
||||
resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==}
|
||||
|
||||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: true
|
||||
|
||||
/utils-merge@1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
@@ -10509,6 +10746,13 @@ packages:
|
||||
has-tostringtag: 1.0.0
|
||||
dev: true
|
||||
|
||||
/which@1.3.1:
|
||||
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
dev: false
|
||||
|
||||
/which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -10528,6 +10772,10 @@ packages:
|
||||
resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==}
|
||||
dev: true
|
||||
|
||||
/workerpool@6.5.1:
|
||||
resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==}
|
||||
dev: false
|
||||
|
||||
/wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -10663,7 +10911,25 @@ packages:
|
||||
engines: {node: '>=12.20'}
|
||||
dev: true
|
||||
|
||||
/zod@3.22.4:
|
||||
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
||||
dev: false
|
||||
|
||||
/zone.js@0.14.2:
|
||||
resolution: {integrity: sha512-X4U7J1isDhoOmHmFWiLhloWc2lzMkdnumtfQ1LXzf/IOZp5NQYuMUTaviVzG/q1ugMBIXzin2AqeVJUoSEkNyQ==}
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
|
||||
github.com/Geomitron/stream-audio-fingerprint/197e8a7ff60165b18bf1debc23dab4814996a20d:
|
||||
resolution: {tarball: https://codeload.github.com/Geomitron/stream-audio-fingerprint/tar.gz/197e8a7ff60165b18bf1debc23dab4814996a20d}
|
||||
name: stream-audio-fingerprint
|
||||
version: 1.0.4
|
||||
dependencies:
|
||||
dsp.js: github.com/corbanbrook/dsp.js/219600bb0346ee9a00686c9875c81123e2d8780e
|
||||
dev: false
|
||||
|
||||
github.com/corbanbrook/dsp.js/219600bb0346ee9a00686c9875c81123e2d8780e:
|
||||
resolution: {tarball: https://codeload.github.com/corbanbrook/dsp.js/tar.gz/219600bb0346ee9a00686c9875c81123e2d8780e}
|
||||
name: dsp.js
|
||||
version: 1.0.0
|
||||
dev: false
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { AppComponent } from './app.component'
|
||||
import { BrowseComponent } from './components/browse/browse.component'
|
||||
import { ChartSidebarInstrumentComponent } from './components/browse/chart-sidebar/chart-sidebar-instrument/chart-sidebar-instrument.component'
|
||||
import { ChartSidebarComponent } from './components/browse/chart-sidebar/chart-sidebar.component'
|
||||
import { ResultTableRowComponent } from './components/browse/result-table/result-table-row/result-table-row.component'
|
||||
import { ResultTableComponent } from './components/browse/result-table/result-table.component'
|
||||
@@ -25,6 +27,7 @@ import { ProgressBarDirective } from './core/directives/progress-bar.directive'
|
||||
StatusBarComponent,
|
||||
ResultTableComponent,
|
||||
ChartSidebarComponent,
|
||||
ChartSidebarInstrumentComponent,
|
||||
ResultTableRowComponent,
|
||||
DownloadsModalComponent,
|
||||
ProgressBarDirective,
|
||||
@@ -35,6 +38,8 @@ import { ProgressBarDirective } from './core/directives/progress-bar.directive'
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientModule,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="bg-neutral rounded-md">
|
||||
<div class="text-center text-neutral-content">
|
||||
{{ getEMHXString() }}
|
||||
</div>
|
||||
<div class="indicator">
|
||||
<img class="w-12 m-2 mt-0" src="assets/images/instruments/{{ instrument }}.png" />
|
||||
<span class="indicator-item indicator-bottom indicator-start badge badge-error badge-md ml-4 mb-4 font-bold">{{ getDiff() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
|
||||
import { capitalize } from 'lodash'
|
||||
import { Instrument } from 'scan-chart'
|
||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||
import { instrumentToDiff } from 'src-shared/UtilFunctions'
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-sidebar-instrument',
|
||||
templateUrl: './chart-sidebar-instrument.component.html',
|
||||
})
|
||||
export class ChartSidebarInstrumentComponent {
|
||||
|
||||
@Input() chart: ChartData
|
||||
@Input() instrument: Instrument | 'vocals'
|
||||
|
||||
getDiff() {
|
||||
const diff = this.chart[instrumentToDiff(this.instrument)]
|
||||
return diff === null || diff < 0 ? '?' : diff
|
||||
}
|
||||
|
||||
getEMHXString() {
|
||||
if (this.instrument === 'vocals') { return 'Vocals' }
|
||||
|
||||
const difficulties = this.chart.notesData.noteCounts
|
||||
.filter(nc => nc.instrument === this.instrument && nc.count > 0)
|
||||
.map(nc => nc.difficulty)
|
||||
|
||||
if (difficulties.length === 1) {
|
||||
return capitalize(difficulties[0])
|
||||
}
|
||||
|
||||
let str = ''
|
||||
if (difficulties.includes('easy')) { str += 'E' }
|
||||
if (difficulties.includes('medium')) { str += 'M' }
|
||||
if (difficulties.includes('hard')) { str += 'H' }
|
||||
if (difficulties.includes('expert')) { str += 'X' }
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,34 @@
|
||||
<div id="sidebarCard" *ngIf="selectedVersion" class="ui fluid card">
|
||||
<div class="ui placeholder" [ngClass]="{ placeholder: albumArtSrc === '', inverted: settingsService.theme === 'dark' }">
|
||||
<img *ngIf="albumArtSrc !== null" class="ui square image" [src]="albumArtSrc" />
|
||||
<div id="sidebarCard" *ngIf="selectedChart" class="ui fluid card">
|
||||
<div class="ui placeholder">
|
||||
@if (albumArtMd5) {
|
||||
<img src="https://files.enchor.us/{{ albumArtMd5 }}.jpg" alt="Album art" loading="lazy" class="object-cover w-40" />
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="charts.length > 1" id="chartDropdown" class="ui fluid right labeled scrolling icon dropdown button">
|
||||
<div *ngIf="charts && charts.length > 1" id="chartDropdown" class="ui fluid right labeled scrolling icon dropdown button">
|
||||
<input type="hidden" name="Chart" />
|
||||
<i id="chartDropdownIcon" class="dropdown icon"></i>
|
||||
<div class="default text"></div>
|
||||
<div id="chartDropdownMenu" class="menu"></div>
|
||||
</div>
|
||||
<div id="textPanel" class="content">
|
||||
<span class="header">{{ selectedVersion.chartName }}</span>
|
||||
<span class="header">{{ selectedChart.chartName }}</span>
|
||||
<div class="description">
|
||||
<div *ngIf="songResult!.album === null"><b>Album:</b> {{ selectedVersion.album }} ({{ selectedVersion.year }})</div>
|
||||
<div *ngIf="songResult!.album !== null"><b>Year:</b> {{ selectedVersion.year }}</div>
|
||||
<div *ngIf="songResult!.genre === null"><b>Genre:</b> {{ selectedVersion.genre }}</div>
|
||||
<div>
|
||||
<b>{{ charterPlural }}</b> {{ selectedVersion.charters }}
|
||||
</div>
|
||||
<div *ngIf="selectedVersion.tags"><b>Tags:</b> {{ selectedVersion.tags }}</div>
|
||||
<div *ngIf="selectedChart.chartAlbum"><b>Album:</b> {{ selectedChart.chartAlbum }}</div>
|
||||
<div *ngIf="selectedChart.chartGenre"><b>Genre:</b> {{ selectedChart.chartGenre }}</div>
|
||||
<div *ngIf="selectedChart.chartYear"><b>Year:</b> {{ selectedChart.chartYear }}</div>
|
||||
<div><b>Charter:</b> {{ selectedChart.charter }}</div>
|
||||
<div><b>Audio Length:</b> {{ songLength }}</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui horizontal list">
|
||||
<div *ngFor="let difficulty of difficultiesList" class="item">
|
||||
<img class="ui avatar image" src="assets/images/instruments/{{ difficulty.instrument }}" />
|
||||
<div class="content">
|
||||
<div class="header">Diff: {{ difficulty.diffNumber }}</div>
|
||||
{{ difficulty.chartedDifficulties }}
|
||||
</div>
|
||||
</div>
|
||||
@if (selectedChart.notesData.hasVocals) {
|
||||
<app-chart-sidebar-instrument [chart]="selectedChart" instrument="vocals" />
|
||||
}
|
||||
@for (instrument of instruments; track $index) {
|
||||
<app-chart-sidebar-instrument [chart]="selectedChart" [instrument]="instrument" />
|
||||
}
|
||||
</div>
|
||||
<div id="sourceLinks">
|
||||
<a id="sourceLink" (click)="onSourceLinkClicked()">{{ selectedVersion.driveData.source.sourceName }}</a>
|
||||
<a id="sourceLink" (click)="onSourceLinkClicked()">{{ selectedChart.packName ?? selectedChart.applicationUsername + "'s Chart" }}</a>
|
||||
<button *ngIf="shownFolderButton()" id="folderButton" class="mini ui icon button" (click)="onFolderButtonClicked()">
|
||||
<i class="folder open outline icon"></i>
|
||||
</button>
|
||||
@@ -38,8 +36,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="downloadButtons" class="ui positive buttons">
|
||||
<div id="downloadButton" class="ui button" (click)="onDownloadClicked()">{{ downloadButtonText }}</div>
|
||||
<div *ngIf="getSelectedChartVersions().length > 1" id="versionDropdown" class="ui floating dropdown icon button">
|
||||
<div id="downloadButton" class="ui button" (click)="onDownloadClicked()">Download</div>
|
||||
<div id="versionDropdown" class="ui floating dropdown icon button">
|
||||
<i class="dropdown icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { SafeUrl } from '@angular/platform-browser'
|
||||
|
||||
import { chain, flatMap, sortBy } from 'lodash'
|
||||
import { Instrument } from 'scan-chart'
|
||||
import { SearchService } from 'src-angular/app/core/services/search.service'
|
||||
import { SettingsService } from 'src-angular/app/core/services/settings.service'
|
||||
|
||||
import { SongResult } from '../../../../../src-shared/interfaces/search.interface'
|
||||
import { ChartedDifficulty, getInstrumentIcon, Instrument, VersionResult } from '../../../../../src-shared/interfaces/songDetails.interface'
|
||||
import { groupBy } from '../../../../../src-shared/UtilFunctions'
|
||||
import { DownloadService } from '../../../core/services/download.service'
|
||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||
import { driveLink, instruments } from 'src-shared/UtilFunctions'
|
||||
|
||||
interface Difficulty {
|
||||
instrument: string
|
||||
@@ -22,252 +20,81 @@ interface Difficulty {
|
||||
})
|
||||
export class ChartSidebarComponent implements OnInit {
|
||||
|
||||
songResult: SongResult | undefined
|
||||
selectedVersion: VersionResult
|
||||
charts: VersionResult[][]
|
||||
selectedChart: ChartData | null = null
|
||||
charts: ChartData[][] | null = null
|
||||
|
||||
albumArtSrc: SafeUrl = ''
|
||||
charterPlural: string
|
||||
songLength: string
|
||||
difficultiesList: Difficulty[]
|
||||
downloadButtonText: string
|
||||
|
||||
constructor(
|
||||
private downloadService: DownloadService,
|
||||
private searchService: SearchService,
|
||||
public settingsService: SettingsService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.searchService.onNewSearch(() => {
|
||||
this.selectVersion(undefined)
|
||||
this.songResult = undefined
|
||||
this.searchService.searchUpdated.subscribe(() => {
|
||||
this.charts = null
|
||||
this.selectedChart = null
|
||||
})
|
||||
}
|
||||
|
||||
public get albumArtMd5() {
|
||||
return flatMap(this.charts ?? []).find(c => !!c.albumArtMd5)?.albumArtMd5 || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the information for the selected song.
|
||||
*/
|
||||
async onRowClicked(result: SongResult) {
|
||||
if (this.songResult === undefined || result.id !== this.songResult.id) { // Clicking the same row again will not reload
|
||||
this.songResult = result
|
||||
const results = await window.electron.invoke.getSongDetails(result.id)
|
||||
this.charts = groupBy(results, 'chartID').sort((v1, v2) => v1[0].chartName.length - v2[0].chartName.length)
|
||||
this.sortCharts()
|
||||
await this.selectChart(this.charts[0][0].chartID)
|
||||
this.initChartDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts `this.charts` and its subarrays in the correct order.
|
||||
* The chart dropdown should display in a random order, but verified charters are prioritized.
|
||||
* The version dropdown should be ordered by lastModified date.
|
||||
* (but prefer the non-pack version if it's only a few days older)
|
||||
*/
|
||||
private sortCharts() {
|
||||
for (const chart of this.charts) {
|
||||
// TODO: sort by verified charter
|
||||
this.searchService.sortChart(chart)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the chart dropdown from `this.charts` (or removes it if there's only one chart).
|
||||
*/
|
||||
private initChartDropdown() {
|
||||
// TODO
|
||||
// const values = this.charts.map(chart => {
|
||||
// const version = chart[0]
|
||||
// return {
|
||||
// value: version.chartID,
|
||||
// text: version.chartName,
|
||||
// name: `${version.chartName} <b>[${version.charters}]</b>`,
|
||||
// }
|
||||
// })
|
||||
// const $chartDropdown = $('#chartDropdown')
|
||||
// $chartDropdown.dropdown('setup menu', { values })
|
||||
// $chartDropdown.dropdown('setting', 'onChange', (chartID: number) => this.selectChart(chartID))
|
||||
// $chartDropdown.dropdown('set selected', values[0].value)
|
||||
}
|
||||
|
||||
private async selectChart(chartID: number) {
|
||||
const chart = this.charts.find(chart => chart[0].chartID === chartID)!
|
||||
await this.selectVersion(chart[0])
|
||||
this.initVersionDropdown()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sidebar to display the metadata for `selectedVersion`.
|
||||
*/
|
||||
async selectVersion(selectedVersion: VersionResult | undefined) {
|
||||
this.selectedVersion = selectedVersion!
|
||||
await new Promise<void>(resolve => setTimeout(() => resolve(), 0)) // Wait for *ngIf to update DOM
|
||||
|
||||
if (this.selectedVersion !== undefined) {
|
||||
this.updateCharterPlural()
|
||||
this.updateSongLength()
|
||||
this.updateDifficultiesList()
|
||||
this.updateDownloadButtonText()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses to display 'Charter:' or 'Charters:'.
|
||||
*/
|
||||
private updateCharterPlural() {
|
||||
this.charterPlural = this.selectedVersion.charterIDs.split('&').length === 1 ? 'Charter:' : 'Charters:'
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts `this.selectedVersion.chartMetadata.length` into a readable duration.
|
||||
*/
|
||||
private updateSongLength() {
|
||||
let seconds = this.selectedVersion.songLength
|
||||
if (seconds < 60) { this.songLength = `${seconds} second${seconds === 1 ? '' : 's'}`; return }
|
||||
let minutes = Math.floor(seconds / 60)
|
||||
let hours = 0
|
||||
while (minutes > 59) {
|
||||
hours++
|
||||
minutes -= 60
|
||||
}
|
||||
seconds = Math.floor(seconds % 60)
|
||||
this.songLength = `${hours === 0 ? '' : hours + ':'}${minutes === 0 ? '' : minutes + ':'}${seconds < 10 ? '0' + seconds : seconds}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates `dfficultiesList` with the difficulty information for the selected version.
|
||||
*/
|
||||
private updateDifficultiesList() {
|
||||
const instruments = Object.keys(this.selectedVersion.chartData.noteCounts) as Instrument[]
|
||||
this.difficultiesList = []
|
||||
for (const instrument of instruments) {
|
||||
if (instrument !== 'undefined') {
|
||||
this.difficultiesList.push({
|
||||
instrument: getInstrumentIcon(instrument),
|
||||
diffNumber: this.getDiffNumber(instrument),
|
||||
chartedDifficulties: this.getChartedDifficultiesText(instrument),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a string describing the difficulty number in the selected version.
|
||||
*/
|
||||
private getDiffNumber(instrument: Instrument) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const diffNumber: number = (this.selectedVersion as any)[`diff_${instrument}`]
|
||||
return diffNumber === -1 || diffNumber === undefined ? 'Unknown' : String(diffNumber)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a string describing the list of charted difficulties in the selected version.
|
||||
*/
|
||||
private getChartedDifficultiesText(instrument: Instrument) {
|
||||
const difficulties = Object.keys(this.selectedVersion.chartData.noteCounts[instrument]) as ChartedDifficulty[]
|
||||
if (difficulties.length === 4) { return 'Full Difficulty' }
|
||||
const difficultyNames = []
|
||||
if (difficulties.includes('x')) { difficultyNames.push('Expert') }
|
||||
if (difficulties.includes('h')) { difficultyNames.push('Hard') }
|
||||
if (difficulties.includes('m')) { difficultyNames.push('Medium') }
|
||||
if (difficulties.includes('e')) { difficultyNames.push('Easy') }
|
||||
|
||||
return difficultyNames.join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the text to display on the download button.
|
||||
*/
|
||||
private updateDownloadButtonText() {
|
||||
this.downloadButtonText = 'Download'
|
||||
if (this.selectedVersion.driveData.inChartPack) {
|
||||
this.downloadButtonText += ' Chart Pack'
|
||||
} else {
|
||||
this.downloadButtonText += (this.selectedVersion.driveData.isArchive ? ' Archive' : ' Files')
|
||||
}
|
||||
|
||||
if (this.getSelectedChartVersions().length > 1) {
|
||||
if (this.selectedVersion.versionID === this.selectedVersion.latestVersionID) {
|
||||
this.downloadButtonText += ' (Latest)'
|
||||
} else {
|
||||
this.downloadButtonText += ` (${this.getLastModifiedText(this.selectedVersion.lastModified)})`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the version dropdown from `this.selectedVersion` (or removes it if there's only one version).
|
||||
*/
|
||||
private initVersionDropdown() {
|
||||
// TODO
|
||||
// const $versionDropdown = $('#versionDropdown')
|
||||
// const versions = this.getSelectedChartVersions()
|
||||
// const values = versions.map(version => ({
|
||||
// value: version.versionID,
|
||||
// text: 'Uploaded ' + this.getLastModifiedText(version.lastModified),
|
||||
// name: 'Uploaded ' + this.getLastModifiedText(version.lastModified),
|
||||
// }))
|
||||
|
||||
// $versionDropdown.dropdown('setup menu', { values })
|
||||
// $versionDropdown.dropdown('setting', 'onChange', (versionID: number) => {
|
||||
// this.selectVersion(versions.find(version => version.versionID === versionID))
|
||||
// })
|
||||
// $versionDropdown.dropdown('set selected', values[0].value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of versions for the selected chart, sorted by `lastModified`.
|
||||
*/
|
||||
getSelectedChartVersions() {
|
||||
return this.charts.find(chart => chart[0].chartID === this.selectedVersion.chartID)!
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the <lastModified> value to a user-readable format.
|
||||
* @param lastModified The UNIX timestamp for the lastModified date.
|
||||
*/
|
||||
private getLastModifiedText(lastModified: string) {
|
||||
const date = new Date(lastModified)
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const year = date.getFullYear().toString().substr(-2)
|
||||
return `${month}/${day}/${year}`
|
||||
async onRowClicked(song: ChartData[]) {
|
||||
this.charts = chain(song)
|
||||
.groupBy(c => c.versionGroupId)
|
||||
.values()
|
||||
.map(versionGroup => sortBy(versionGroup, vg => vg.modifiedTime).reverse())
|
||||
.value()
|
||||
this.selectedChart = this.charts[0][0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the proxy link or source folder in the default browser.
|
||||
*/
|
||||
onSourceLinkClicked() {
|
||||
const source = this.selectedVersion.driveData.source
|
||||
window.electron.emit.openUrl(source.proxyLink ?? `https://drive.google.com/drive/folders/${source.sourceDriveID}`)
|
||||
window.electron.emit.openUrl(driveLink(this.selectedChart!.applicationDriveId))
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if the source folder button should be shown.
|
||||
*/
|
||||
shownFolderButton() {
|
||||
const driveData = this.selectedVersion.driveData
|
||||
return driveData.source.proxyLink || driveData.source.sourceDriveID !== driveData.folderID
|
||||
return this.selectedChart!.applicationDriveId !== this.selectedChart!.parentFolderId
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the chart folder in the default browser.
|
||||
*/
|
||||
onFolderButtonClicked() {
|
||||
window.electron.emit.openUrl(`https://drive.google.com/drive/folders/${this.selectedVersion.driveData.folderID}`)
|
||||
window.electron.emit.openUrl(driveLink(this.selectedChart!.parentFolderId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the selected version to the download queue.
|
||||
*/
|
||||
onDownloadClicked() {
|
||||
this.downloadService.addDownload(
|
||||
this.selectedVersion.versionID, {
|
||||
chartName: this.selectedVersion.chartName,
|
||||
artist: this.songResult!.artist,
|
||||
charter: this.selectedVersion.charters,
|
||||
driveData: this.selectedVersion.driveData,
|
||||
})
|
||||
// TODO
|
||||
// this.downloadService.addDownload(
|
||||
// this.selectedChart.versionID, {
|
||||
// chartName: this.selectedChart.chartName,
|
||||
// artist: this.songResult!.artist,
|
||||
// charter: this.selectedChart.charters,
|
||||
// driveData: this.selectedChart.driveData,
|
||||
// })
|
||||
}
|
||||
|
||||
public get instruments(): Instrument[] {
|
||||
if (!this.selectedChart) { return [] }
|
||||
return chain(this.selectedChart.notesData.noteCounts)
|
||||
.map(nc => nc.instrument)
|
||||
.uniq()
|
||||
.sortBy(i => instruments.indexOf(i))
|
||||
.value()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span id="chartCount" *ngIf="result.chartCount > 1">{{ result.chartCount }}</span
|
||||
>{{ result.name }}
|
||||
<span id="chartCount" *ngIf="song.length > 1">{{ song.length }}</span> {{ song[0].name }}
|
||||
</td>
|
||||
<td>{{ result.artist }}</td>
|
||||
<td>{{ result.album || 'Various' }}</td>
|
||||
<td>{{ result.genre || 'Various' }}</td>
|
||||
<td>{{ song[0].artist }}</td>
|
||||
<td>{{ song[0].album || 'Various' }}</td>
|
||||
<td>{{ song[0].genre || 'Various' }}</td>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core'
|
||||
|
||||
import { SongResult } from '../../../../../../src-shared/interfaces/search.interface'
|
||||
import { ChartData } from '../../../../../../src-shared/interfaces/search.interface'
|
||||
import { SelectionService } from '../../../../core/services/selection.service'
|
||||
|
||||
@Component({
|
||||
@@ -9,14 +9,14 @@ import { SelectionService } from '../../../../core/services/selection.service'
|
||||
styleUrls: ['./result-table-row.component.scss'],
|
||||
})
|
||||
export class ResultTableRowComponent implements AfterViewInit {
|
||||
@Input() result: SongResult
|
||||
@Input() song: ChartData[]
|
||||
|
||||
@ViewChild('checkbox', { static: true }) checkbox: ElementRef
|
||||
|
||||
constructor(private selectionService: SelectionService) { }
|
||||
|
||||
get songID() {
|
||||
return this.result.id
|
||||
return this.song[0].songId ?? this.song[0].chartId
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
|
||||
@@ -19,12 +19,8 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
app-result-table-row
|
||||
#tableRow
|
||||
*ngFor="let result of results"
|
||||
(click)="onRowClicked(result)"
|
||||
[class.active]="activeRowID === result.id"
|
||||
[result]="result"></tr>
|
||||
@for (song of songs; track song) {
|
||||
<tr app-result-table-row (click)="onRowClicked(song)" [class.active]="activeSong === song" [song]="song"></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Component, EventEmitter, OnInit, Output, QueryList, ViewChild, ViewChil
|
||||
|
||||
import Comparators from 'comparators'
|
||||
import { SettingsService } from 'src-angular/app/core/services/settings.service'
|
||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||
|
||||
import { SongResult } from '../../../../../src-shared/interfaces/search.interface'
|
||||
import { CheckboxDirective } from '../../../core/directives/checkbox.directive'
|
||||
import { SearchService } from '../../../core/services/search.service'
|
||||
import { SelectionService } from '../../../core/services/selection.service'
|
||||
@@ -16,18 +16,17 @@ import { ResultTableRowComponent } from './result-table-row/result-table-row.com
|
||||
})
|
||||
export class ResultTableComponent implements OnInit {
|
||||
|
||||
@Output() rowClicked = new EventEmitter<SongResult>()
|
||||
@Output() rowClicked = new EventEmitter<ChartData[]>()
|
||||
|
||||
@ViewChild(CheckboxDirective, { static: true }) checkboxColumn: CheckboxDirective
|
||||
@ViewChildren('tableRow') tableRows: QueryList<ResultTableRowComponent>
|
||||
|
||||
results: SongResult[] = []
|
||||
activeRowID: number | null = null
|
||||
activeSong: ChartData[] | null = null
|
||||
sortDirection: 'ascending' | 'descending' = 'descending'
|
||||
sortColumn: 'name' | 'artist' | 'album' | 'genre' | null = null
|
||||
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
public searchService: SearchService,
|
||||
private selectionService: SelectionService,
|
||||
public settingsService: SettingsService
|
||||
) { }
|
||||
@@ -37,24 +36,25 @@ export class ResultTableComponent implements OnInit {
|
||||
this.checkboxColumn.check(selected)
|
||||
})
|
||||
|
||||
this.searchService.onSearchChanged(results => {
|
||||
this.activeRowID = null
|
||||
this.results = results
|
||||
this.searchService.searchUpdated.subscribe(() => {
|
||||
this.activeSong = null
|
||||
this.updateSort()
|
||||
})
|
||||
|
||||
this.searchService.onNewSearch(() => {
|
||||
this.sortColumn = null
|
||||
})
|
||||
}
|
||||
|
||||
onRowClicked(result: SongResult) {
|
||||
this.activeRowID = result.id
|
||||
this.rowClicked.emit(result)
|
||||
get songs() {
|
||||
return this.searchService.groupedSongs
|
||||
}
|
||||
|
||||
onRowClicked(song: ChartData[]) {
|
||||
if (this.activeSong !== song) {
|
||||
this.activeSong = song
|
||||
this.rowClicked.emit(song)
|
||||
}
|
||||
}
|
||||
|
||||
onColClicked(column: 'name' | 'artist' | 'album' | 'genre') {
|
||||
if (this.results.length === 0) { return }
|
||||
if (this.songs.length === 0) { return }
|
||||
if (this.sortColumn !== column) {
|
||||
this.sortColumn = column
|
||||
this.sortDirection = 'descending'
|
||||
@@ -68,7 +68,7 @@ export class ResultTableComponent implements OnInit {
|
||||
|
||||
private updateSort() {
|
||||
if (this.sortColumn !== null) {
|
||||
this.results.sort(Comparators.comparing(this.sortColumn, { reversed: this.sortDirection === 'ascending' }))
|
||||
this.songs.sort(Comparators.comparing(this.sortColumn, { reversed: this.sortDirection === 'ascending' }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="searchMenu" class="ui bottom attached borderless menu">
|
||||
<!-- <div id="searchMenu" class="ui bottom attached borderless menu">
|
||||
<div class="item">
|
||||
<div class="ui icon input" [class.loading]="isLoading()">
|
||||
<input #searchBox type="text" placeholder=" Search..." (keyup.enter)="onSearch(searchBox.value)" />
|
||||
@@ -237,4 +237,334 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="collapse navbar grid bg-base-100 overflow-visible rounded-none" [ngClass]="showAdvanced ? 'collapse-open' : 'collapse-close'">
|
||||
<div class="flex flex-wrap justify-end gap-1">
|
||||
<!-- Search Input -->
|
||||
<div class="flex items-center order-3 md:order-2 xl:order-2 flex-none w-full md:w-auto md:flex-1 xl:flex-grow-[6] min-w-[21rem]">
|
||||
<div class="form-control w-full">
|
||||
<input type="text" [formControl]="searchControl" placeholder="What do you feel like playing today?" class="input input-bordered pr-14" />
|
||||
</div>
|
||||
<i class="bi bi-search -ml-11"></i>
|
||||
</div>
|
||||
<div class="basis-full h-0 order-4 xl:order-7"></div>
|
||||
<div class="flex order-5 md:order-5 xl:order-3">
|
||||
<!-- Instrument Dropdown -->
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-neutral rounded-btn rounded-r-none my-1">
|
||||
@if (instrument) {
|
||||
<img class="w-8 hidden sm:block" src="assets/images/instruments/{{ instrument }}.png" />
|
||||
}
|
||||
{{ instrumentDisplay(instrument) }}
|
||||
</label>
|
||||
<ul tabindex="0" class="menu dropdown-content z-[2] p-2 shadow bg-neutral text-neutral-content rounded-box w-64">
|
||||
<li>
|
||||
<a (click)="setInstrument(null, $event)">{{ instrumentDisplay(null) }}</a>
|
||||
</li>
|
||||
@for (instrument of instruments; track $index) {
|
||||
<li>
|
||||
<a (click)="setInstrument(instrument, $event)">
|
||||
<img class="w-8" src="assets/images/instruments/{{ instrument }}.png" />
|
||||
{{ instrumentDisplay(instrument) }}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Difficulty Dropdown -->
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-neutral rounded-btn rounded-l-none my-1">{{ difficultyDisplay(difficulty) }}</label>
|
||||
<ul tabindex="0" class="menu dropdown-content z-[2] p-2 shadow bg-neutral text-neutral-content rounded-box w-40">
|
||||
<li>
|
||||
<a (click)="setDifficulty(null, $event)">{{ difficultyDisplay(null) }}</a>
|
||||
</li>
|
||||
@for (difficulty of difficulties; track $index) {
|
||||
<li>
|
||||
<a (click)="setDifficulty(difficulty, $event)">{{ difficultyDisplay(difficulty) }}</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Advanced Search -->
|
||||
<div class="order-6 md:order-6 xl:order-4 xl:flex-grow-[5]">
|
||||
<button class="btn btn-ghost" (click)="setShowAdvanced(!showAdvanced)">
|
||||
Advanced Search
|
||||
<div class="cursor-pointer swap swap-rotate" [class.swap-active]="showAdvanced">
|
||||
<i class="swap-off bi bi-chevron-down"></i>
|
||||
<i class="swap-on bi bi-chevron-up"></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content justify-center">
|
||||
<form [formGroup]="advancedSearchForm">
|
||||
<div class="flex flex-wrap gap-5 justify-center">
|
||||
<div>
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div
|
||||
class="tooltip tooltip-bottom font-normal [text-wrap:balance]"
|
||||
data-tip='Search for text in these specific chart properties. Note: you can put a minus sign (-) before words to return only results without that word. (e.g. "Dragon -Dragonforce")'>
|
||||
<span class="font-bold underline decoration-dotted cursor-help">Search by</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div
|
||||
class="tooltip tooltip-bottom font-normal [text-wrap:balance]"
|
||||
data-tip="Only include results that match perfectly. (not case sensitive)">
|
||||
<span class="font-bold underline decoration-dotted cursor-help">Exact</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="tooltip tooltip-bottom font-normal [text-wrap:balance]" data-tip="Do not include results that match this.">
|
||||
<span class="font-bold underline decoration-dotted cursor-help">Exclude</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-b-0" formGroupName="name">
|
||||
<td><input type="text" placeholder="Name" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="artist">
|
||||
<td><input type="text" placeholder="Artist" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="album">
|
||||
<td><input type="text" placeholder="Album" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="genre">
|
||||
<td><input type="text" placeholder="Genre" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="year">
|
||||
<td><input type="text" placeholder="Year" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="charter">
|
||||
<td><input type="text" placeholder="Charter" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-end">
|
||||
<table class="table table-xs">
|
||||
<tbody>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">Length (minutes)</td>
|
||||
<td>
|
||||
<div class="join">
|
||||
<input type="number" placeholder="Min" class="input input-bordered join-item input-sm w-16" formControlName="minLength" />
|
||||
<input type="number" placeholder="Max" class="input input-bordered join-item input-sm w-16" formControlName="maxLength" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">
|
||||
<span
|
||||
class="label-text underline decoration-dotted cursor-help tooltip [text-wrap:balance]"
|
||||
data-tip="Also known as chart difficulty. Typically a number between 0 and 6.">
|
||||
Intensity
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="join">
|
||||
<input type="number" placeholder="Min" class="input input-bordered join-item input-sm w-16" formControlName="minIntensity" />
|
||||
<input type="number" placeholder="Max" class="input input-bordered join-item input-sm w-16" formControlName="maxIntensity" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">Average NPS</td>
|
||||
<td>
|
||||
<div class="join">
|
||||
<input type="number" placeholder="Min" class="input input-bordered join-item input-sm w-16" formControlName="minAverageNPS" />
|
||||
<input type="number" placeholder="Max" class="input input-bordered join-item input-sm w-16" formControlName="maxAverageNPS" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">Max NPS</td>
|
||||
<td>
|
||||
<div class="join">
|
||||
<input type="number" placeholder="Min" class="input input-bordered join-item input-sm w-16" formControlName="minMaxNPS" />
|
||||
<input type="number" placeholder="Max" class="input input-bordered join-item input-sm w-16" formControlName="maxMaxNPS" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">
|
||||
<span
|
||||
class="label-text underline decoration-dotted cursor-help tooltip [text-wrap:balance]"
|
||||
data-tip="The date of the last time this chart was modified in Google Drive.">
|
||||
Modified After
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="date"
|
||||
min="2012-01-01"
|
||||
[max]="todayDate"
|
||||
placeholder="YYYY/MM/DD"
|
||||
class="input input-bordered join-item input-sm w-32"
|
||||
formControlName="modifiedAfter"
|
||||
(blur)="startValidation = true"
|
||||
[class.input-error]="advancedSearchForm.invalid && startValidation" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">
|
||||
<span
|
||||
class="label-text underline decoration-dotted cursor-help tooltip [text-wrap:balance]"
|
||||
data-tip="The MD5 hash of the chart folder or .sng file. You can enter multiple values if they are separated by commas.">
|
||||
Hash
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="input input-bordered join-item input-sm w-32" formControlName="hash" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasSoloSections
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasSoloSections', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasSoloSections') === null">
|
||||
{{ formValue('hasSoloSections') === false ? 'No ' : '' }}Solo Sections
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasForcedNotes
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasForcedNotes', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasForcedNotes') === null">
|
||||
{{ formValue('hasForcedNotes') === false ? 'No ' : '' }}Forced Notes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasOpenNotes
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasOpenNotes', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasOpenNotes') === null">
|
||||
{{ formValue('hasOpenNotes') === false ? 'No ' : '' }}Open Notes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasTapNotes
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasTapNotes', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasTapNotes') === null">
|
||||
{{ formValue('hasTapNotes') === false ? 'No ' : '' }}Tap Notes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('hasLyrics', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasLyrics') === null">
|
||||
{{ formValue('hasLyrics') === false ? 'No ' : '' }}Lyrics
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('hasVocals', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasVocals') === null">
|
||||
{{ formValue('hasVocals') === false ? 'No ' : '' }}Vocals
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasRollLanes
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasRollLanes', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasRollLanes') === null">
|
||||
{{ formValue('hasRollLanes') === false ? 'No ' : '' }}Roll Lanes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input #has2xKick type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('has2xKick', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('has2xKick') === null">
|
||||
{{ formValue('has2xKick') === false ? 'No ' : '' }}2x Kick
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('hasIssues', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasIssues') === null">
|
||||
{{ formValue('hasIssues') === false ? 'No ' : '' }}Chart Issues
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('hasVideoBackground', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasVideoBackground') === null">
|
||||
{{ formValue('hasVideoBackground') === false ? 'No ' : '' }}Video Background
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('modchart', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('modchart') === null">
|
||||
{{ formValue('modchart') === false ? 'Not a ' : '' }}Modchart
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" [class.btn-disabled]="advancedSearchForm.invalid && startValidation" (click)="searchAdvanced()">
|
||||
Search{{ advancedSearchForm.invalid && startValidation ? ' ("Modified After" is invalid)' : '' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#searchMenu {
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0em;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
#searchMenu>.item {
|
||||
padding: .3em .4em;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
#searchMenu>.item:first-child {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
#searchIcon {
|
||||
cursor: default;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#advancedSearchForm {
|
||||
margin: 0em;
|
||||
border-width: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 350ms cubic-bezier(0.45, 0, 0.55, 1);
|
||||
max-height: 243.913px; /* This is its preferred height. Transition needs a static target number to work. */
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
max-height: 0px !important;
|
||||
}
|
||||
|
||||
#quantityDropdownItem, #similarityDropdownItem {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
transition: visibility 0s, opacity 350ms cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
max-width: 0px !important;
|
||||
max-height: 0px !important;
|
||||
transition:
|
||||
opacity 350ms cubic-bezier(0.45, 0, 0.55, 1),
|
||||
visibility 0s linear 350ms,
|
||||
max-width 0s linear 350ms,
|
||||
max-height 0s linear 350ms !important;
|
||||
}
|
||||
@@ -1,78 +1,246 @@
|
||||
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
||||
import { AbstractControl, FormBuilder, FormControl } from '@angular/forms'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import { distinctUntilChanged, switchMap, throttleTime } from 'rxjs'
|
||||
import { Difficulty, Instrument } from 'scan-chart'
|
||||
import { SearchService } from 'src-angular/app/core/services/search.service'
|
||||
|
||||
import { getDefaultSearch } from '../../../../../src-shared/interfaces/search.interface'
|
||||
import { difficulties, difficultyDisplay, instrumentDisplay, instruments } from 'src-shared/UtilFunctions'
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-bar',
|
||||
templateUrl: './search-bar.component.html',
|
||||
styleUrls: ['./search-bar.component.scss'],
|
||||
})
|
||||
export class SearchBarComponent implements AfterViewInit {
|
||||
export class SearchBarComponent implements OnInit, AfterViewInit {
|
||||
|
||||
@ViewChild('searchIcon', { static: true }) searchIcon: ElementRef
|
||||
@ViewChild('quantityDropdown', { static: true }) quantityDropdown: ElementRef
|
||||
@ViewChild('similarityDropdown', { static: true }) similarityDropdown: ElementRef
|
||||
@ViewChild('diffSlider', { static: true }) diffSlider: ElementRef
|
||||
@ViewChild('hasSoloSections') hasSoloSections: ElementRef<HTMLInputElement>
|
||||
@ViewChild('hasForcedNotes') hasForcedNotes: ElementRef<HTMLInputElement>
|
||||
@ViewChild('hasOpenNotes') hasOpenNotes: ElementRef<HTMLInputElement>
|
||||
@ViewChild('hasTapNotes') hasTapNotes: ElementRef<HTMLInputElement>
|
||||
@ViewChild('hasRollLanes') hasRollLanes: ElementRef<HTMLInputElement>
|
||||
@ViewChild('has2xKick') has2xKick: ElementRef<HTMLInputElement>
|
||||
|
||||
isError = false
|
||||
showAdvanced = false
|
||||
searchSettings = getDefaultSearch()
|
||||
private sliderInitialized = false
|
||||
public showAdvanced = false
|
||||
public instruments = instruments
|
||||
public difficulties = difficulties
|
||||
public instrumentDisplay = instrumentDisplay
|
||||
public difficultyDisplay = difficultyDisplay
|
||||
|
||||
constructor(public searchService: SearchService) { }
|
||||
public advancedSearchForm: ReturnType<this['getAdvancedSearchForm']>
|
||||
public startValidation = false
|
||||
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
private fb: FormBuilder,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.searchControl.valueChanges.pipe(
|
||||
throttleTime(400, undefined, { leading: true, trailing: true }),
|
||||
distinctUntilChanged(),
|
||||
switchMap(search => this.searchService.search(search || '*'))
|
||||
).subscribe()
|
||||
|
||||
this.initializeAdvancedSearchForm()
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// TODO
|
||||
// $(this.searchIcon.nativeElement).popup({
|
||||
// onShow: () => this.isError, // Only show the popup if there is an error
|
||||
// })
|
||||
this.searchService.onSearchErrorStateUpdate(isError => {
|
||||
this.isError = isError
|
||||
this.updateDisabledControls()
|
||||
this.searchService.instrument.valueChanges.subscribe(() => {
|
||||
this.updateDisabledControls()
|
||||
})
|
||||
// $(this.quantityDropdown.nativeElement).dropdown({
|
||||
// onChange: (value: string) => {
|
||||
// this.searchSettings.quantity = value as 'all' | 'any'
|
||||
// },
|
||||
// })
|
||||
// $(this.similarityDropdown.nativeElement).dropdown({
|
||||
// onChange: (value: string) => {
|
||||
// this.searchSettings.similarity = value as 'similar' | 'exact'
|
||||
// },
|
||||
// })
|
||||
}
|
||||
|
||||
onSearch(query: string) {
|
||||
this.searchSettings.query = query
|
||||
this.searchSettings.limit = 50 + 1
|
||||
this.searchSettings.offset = 0
|
||||
this.searchService.newSearch(this.searchSettings)
|
||||
}
|
||||
|
||||
onAdvancedSearchClick() {
|
||||
this.showAdvanced = !this.showAdvanced
|
||||
|
||||
if (!this.sliderInitialized) {
|
||||
setTimeout(() => { // Initialization requires this element to not be collapsed
|
||||
// TODO
|
||||
// $(this.diffSlider.nativeElement).slider({
|
||||
// min: 0,
|
||||
// max: 6,
|
||||
// start: 0,
|
||||
// end: 6,
|
||||
// step: 1,
|
||||
// onChange: (_length: number, min: number, max: number) => {
|
||||
// this.searchSettings.minDiff = min
|
||||
// this.searchSettings.maxDiff = max
|
||||
// },
|
||||
// })
|
||||
}, 50)
|
||||
this.sliderInitialized = true
|
||||
setShowAdvanced(showAdvanced: boolean) {
|
||||
this.showAdvanced = showAdvanced
|
||||
if (showAdvanced) {
|
||||
this.startValidation = false
|
||||
this.searchControl.disable()
|
||||
} else {
|
||||
this.searchControl.enable()
|
||||
}
|
||||
}
|
||||
|
||||
isLoading() {
|
||||
return this.searchService.isLoading()
|
||||
get searchControl() {
|
||||
return this.searchService.searchControl
|
||||
}
|
||||
get instrument() {
|
||||
return this.searchService.instrument.value
|
||||
}
|
||||
setInstrument(instrument: Instrument | null, event: MouseEvent) {
|
||||
this.searchService.instrument.setValue(instrument)
|
||||
if (event.target instanceof HTMLElement) {
|
||||
event.target.parentElement?.parentElement?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
get difficulty() {
|
||||
return this.searchService.difficulty.value
|
||||
}
|
||||
setDifficulty(difficulty: Difficulty | null, event: MouseEvent) {
|
||||
this.searchService.difficulty.setValue(difficulty)
|
||||
if (event.target instanceof HTMLElement) {
|
||||
event.target.parentElement?.parentElement?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
get logoType() {
|
||||
switch (localStorage.getItem('theme')) {
|
||||
case 'emerald': return 'emerald'
|
||||
case 'halloween': return 'halloween'
|
||||
case 'lemonade': return 'lemonade'
|
||||
case 'night': return 'night'
|
||||
case 'synthwave': return 'synthwave'
|
||||
case 'aqua': return 'orange'
|
||||
case 'valentine': return 'valentine'
|
||||
case 'winter': return 'winter'
|
||||
case 'aren': return 'aren'
|
||||
case 'froogs': return 'froogs'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
get todayDate() {
|
||||
return dayjs().format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
// TODO: run this when infinite scroll should happen
|
||||
// @HostListener("window:scroll", [])
|
||||
// onScroll(): void {
|
||||
// if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||
// if (this.searchService.areMorePages && !this.searchService.searchLoading) {
|
||||
// this.searchService.search(this.searchControl.value || '*', true).subscribe()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
initializeAdvancedSearchForm() {
|
||||
this.advancedSearchForm = this.getAdvancedSearchForm() as ReturnType<this['getAdvancedSearchForm']>
|
||||
|
||||
for (const key of ['name', 'artist', 'album', 'genre', 'year', 'charter'] as const) {
|
||||
this.advancedSearchForm.get(key)?.get('exact')?.disable()
|
||||
this.advancedSearchForm.get(key)?.get('exclude')?.disable()
|
||||
this.advancedSearchForm.get(key)?.get('value')?.valueChanges.subscribe(value => {
|
||||
if (value) {
|
||||
this.advancedSearchForm.get(key)?.get('exact')?.enable()
|
||||
this.advancedSearchForm.get(key)?.get('exclude')?.enable()
|
||||
} else {
|
||||
this.advancedSearchForm.get(key)?.get('exact')?.disable()
|
||||
this.advancedSearchForm.get(key)?.get('exact')?.setValue(false)
|
||||
this.advancedSearchForm.get(key)?.get('exclude')?.disable()
|
||||
this.advancedSearchForm.get(key)?.get('exclude')?.setValue(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateDisabledControls() {
|
||||
const isDrums = this.searchService.instrument.value === 'drums'
|
||||
const isAny = this.searchService.instrument.value === null
|
||||
const explanation = 'Not available for the current instrument.'
|
||||
|
||||
this.hasSoloSections.nativeElement.disabled = isDrums && !isAny
|
||||
this.hasForcedNotes.nativeElement.disabled = isDrums && !isAny
|
||||
this.hasOpenNotes.nativeElement.disabled = isDrums && !isAny
|
||||
this.hasTapNotes.nativeElement.disabled = isDrums && !isAny
|
||||
this.hasRollLanes.nativeElement.disabled = !isDrums && !isAny
|
||||
this.has2xKick.nativeElement.disabled = !isDrums && !isAny
|
||||
|
||||
this.hasSoloSections.nativeElement.title = isDrums && !isAny ? explanation : ''
|
||||
this.hasForcedNotes.nativeElement.title = isDrums && !isAny ? explanation : ''
|
||||
this.hasOpenNotes.nativeElement.title = isDrums && !isAny ? explanation : ''
|
||||
this.hasTapNotes.nativeElement.title = isDrums && !isAny ? explanation : ''
|
||||
this.hasRollLanes.nativeElement.title = !isDrums && !isAny ? explanation : ''
|
||||
this.has2xKick.nativeElement.title = !isDrums && !isAny ? explanation : ''
|
||||
|
||||
if (!isAny) {
|
||||
if (isDrums) {
|
||||
this.advancedSearchForm.get('hasSoloSections')?.setValue(null)
|
||||
this.advancedSearchForm.get('hasForcedNotes')?.setValue(null)
|
||||
this.advancedSearchForm.get('hasOpenNotes')?.setValue(null)
|
||||
this.advancedSearchForm.get('hasTapNotes')?.setValue(null)
|
||||
this.hasSoloSections.nativeElement.indeterminate = true
|
||||
this.hasForcedNotes.nativeElement.indeterminate = true
|
||||
this.hasOpenNotes.nativeElement.indeterminate = true
|
||||
this.hasTapNotes.nativeElement.indeterminate = true
|
||||
} else {
|
||||
this.advancedSearchForm.get('hasRollLanes')?.setValue(null)
|
||||
this.advancedSearchForm.get('has2xKick')?.setValue(null)
|
||||
this.hasRollLanes.nativeElement.indeterminate = true
|
||||
this.has2xKick.nativeElement.indeterminate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAdvancedSearchForm() {
|
||||
return this.fb.group({
|
||||
name: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
artist: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
album: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
genre: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
year: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
charter: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
minLength: null as number | null,
|
||||
maxLength: null as number | null,
|
||||
minIntensity: null as number | null,
|
||||
maxIntensity: null as number | null,
|
||||
minAverageNPS: null as number | null,
|
||||
maxAverageNPS: null as number | null,
|
||||
minMaxNPS: null as number | null,
|
||||
maxMaxNPS: null as number | null,
|
||||
modifiedAfter: this.fb.nonNullable.control('', { validators: dateVaidator }),
|
||||
hash: this.fb.nonNullable.control(''),
|
||||
hasSoloSections: null as boolean | null,
|
||||
hasForcedNotes: null as boolean | null,
|
||||
hasOpenNotes: null as boolean | null,
|
||||
hasTapNotes: null as boolean | null,
|
||||
hasLyrics: null as boolean | null,
|
||||
hasVocals: null as boolean | null,
|
||||
hasRollLanes: null as boolean | null,
|
||||
has2xKick: null as boolean | null,
|
||||
hasIssues: null as boolean | null,
|
||||
hasVideoBackground: null as boolean | null,
|
||||
modchart: null as boolean | null,
|
||||
})
|
||||
}
|
||||
|
||||
clickCheckbox(key: string, event: MouseEvent) {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
const control = this.advancedSearchForm.get(key) as FormControl<boolean | null>
|
||||
if (control.value === true) {
|
||||
control.setValue(false)
|
||||
event.target.checked = false
|
||||
} else if (control.value === false) {
|
||||
control.setValue(null)
|
||||
event.target.checked = false
|
||||
event.target.indeterminate = true
|
||||
} else if (control.value === null) {
|
||||
control.setValue(true)
|
||||
event.target.checked = true
|
||||
event.target.indeterminate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formValue(key: string) {
|
||||
return this.advancedSearchForm.get(key)?.value
|
||||
}
|
||||
|
||||
searchAdvanced() {
|
||||
this.startValidation = true
|
||||
if (this.advancedSearchForm.valid && !this.searchService.searchLoading) {
|
||||
this.searchService.advancedSearch({
|
||||
instrument: this.instrument,
|
||||
difficulty: this.difficulty,
|
||||
...this.advancedSearchForm.getRawValue(),
|
||||
}).subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dateVaidator(control: AbstractControl) {
|
||||
if (control.value && isNaN(Date.parse(control.value))) {
|
||||
return { 'dateVaidator': true }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<div id="bottomMenu" class="ui bottom borderless menu">
|
||||
<div *ngIf="resultCount > 0" class="item">{{ resultCount }}{{ allResultsVisible ? '' : '+' }} Result{{ resultCount === 1 ? '' : 's' }}</div>
|
||||
<div *ngIf="(searchService.groupedSongs?.length ?? 0) > 0" class="item">
|
||||
{{ searchService.groupedSongs.length }}{{ allResultsVisible ? '' : '+' }} Result{{ searchService.groupedSongs.length === 1 ? '' : 's' }}
|
||||
</div>
|
||||
<div class="item">
|
||||
<button *ngIf="selectedResults.length > 1" (click)="downloadSelected()" class="ui positive button">
|
||||
Download {{ selectedResults.length }} Results
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChangeDetectorRef, Component } from '@angular/core'
|
||||
|
||||
import { VersionResult } from '../../../../../src-shared/interfaces/songDetails.interface'
|
||||
import { groupBy } from '../../../../../src-shared/UtilFunctions'
|
||||
import { DownloadService } from '../../../core/services/download.service'
|
||||
import { SearchService } from '../../../core/services/search.service'
|
||||
@@ -13,17 +12,19 @@ import { SelectionService } from '../../../core/services/selection.service'
|
||||
})
|
||||
export class StatusBarComponent {
|
||||
|
||||
resultCount = 0
|
||||
multipleCompleted = false
|
||||
downloading = false
|
||||
error = false
|
||||
percent = 0
|
||||
batchResults: VersionResult[]
|
||||
chartGroups: VersionResult[][]
|
||||
// TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
batchResults: any[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
chartGroups: any[][]
|
||||
|
||||
constructor(
|
||||
private downloadService: DownloadService,
|
||||
private searchService: SearchService,
|
||||
public searchService: SearchService,
|
||||
private selectionService: SelectionService,
|
||||
ref: ChangeDetectorRef
|
||||
) {
|
||||
@@ -36,14 +37,10 @@ export class StatusBarComponent {
|
||||
ref.detectChanges()
|
||||
}, 0)
|
||||
})
|
||||
|
||||
searchService.onSearchChanged(() => {
|
||||
this.resultCount = searchService.resultCount
|
||||
})
|
||||
}
|
||||
|
||||
get allResultsVisible() {
|
||||
return this.searchService.allResultsVisible
|
||||
return false // this.searchService.allResultsVisible
|
||||
}
|
||||
|
||||
get selectedResults() {
|
||||
@@ -57,7 +54,8 @@ export class StatusBarComponent {
|
||||
|
||||
async downloadSelected() {
|
||||
this.chartGroups = []
|
||||
this.batchResults = await window.electron.invoke.getBatchSongDetails(this.selectedResults.map(result => result.id))
|
||||
// TODO
|
||||
// this.batchResults = await window.electron.invoke.getBatchSongDetails(this.selectedResults.map(result => result.id))
|
||||
const versionGroups = groupBy(this.batchResults, 'songID')
|
||||
for (const versionGroup of versionGroups) {
|
||||
if (versionGroup.findIndex(version => version.chartID !== versionGroup[0].chartID) !== -1) {
|
||||
@@ -68,7 +66,7 @@ export class StatusBarComponent {
|
||||
|
||||
if (this.chartGroups.length === 0) {
|
||||
for (const versions of versionGroups) {
|
||||
this.searchService.sortChart(versions)
|
||||
// this.searchService.sortChart(versions)
|
||||
const downloadVersion = versions[0]
|
||||
const downloadSong = this.selectedResults.find(song => song.id === downloadVersion.songID)!
|
||||
this.downloadService.addDownload(
|
||||
@@ -89,7 +87,7 @@ export class StatusBarComponent {
|
||||
downloadAllCharts() {
|
||||
const songChartGroups = groupBy(this.batchResults, 'songID', 'chartID')
|
||||
for (const chart of songChartGroups) {
|
||||
this.searchService.sortChart(chart)
|
||||
// this.searchService.sortChart(chart)
|
||||
const downloadVersion = chart[0]
|
||||
const downloadSong = this.selectedResults.find(song => song.id === downloadVersion.songID)!
|
||||
this.downloadService.addDownload(
|
||||
|
||||
@@ -1,116 +1,183 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { EventEmitter, Injectable } from '@angular/core'
|
||||
import { FormControl } from '@angular/forms'
|
||||
|
||||
import { SongResult, SongSearch } from '../../../../src-shared/interfaces/search.interface'
|
||||
import { VersionResult } from '../../../../src-shared/interfaces/songDetails.interface'
|
||||
import { chain, xorBy } from 'lodash'
|
||||
import { catchError, mergeMap, tap, throwError, timer } from 'rxjs'
|
||||
import { Difficulty, Instrument } from 'scan-chart'
|
||||
import { AdvancedSearch, ChartData, SearchResult } from 'src-shared/interfaces/search.interface'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SearchService {
|
||||
|
||||
private resultsChangedEmitter = new EventEmitter<SongResult[]>() // For when any results change
|
||||
private newResultsEmitter = new EventEmitter<SongResult[]>() // For when a new search happens
|
||||
private errorStateEmitter = new EventEmitter<boolean>() // To indicate the search's error state
|
||||
private results: SongResult[] = []
|
||||
private awaitingResults = false
|
||||
private currentQuery: SongSearch
|
||||
private _allResultsVisible = true
|
||||
public searchLoading = false
|
||||
public songsResponse: Partial<SearchResult>
|
||||
public currentPage = 1
|
||||
public searchUpdated = new EventEmitter<Partial<SearchResult>>()
|
||||
public isDefaultSearch = true
|
||||
|
||||
async newSearch(query: SongSearch) {
|
||||
if (this.awaitingResults) { return }
|
||||
this.awaitingResults = true
|
||||
this.currentQuery = query
|
||||
try {
|
||||
this.results = this.trimLastChart(await window.electron.invoke.songSearch(this.currentQuery))
|
||||
this.errorStateEmitter.emit(false)
|
||||
} catch (err) {
|
||||
this.results = []
|
||||
console.log(err.message)
|
||||
this.errorStateEmitter.emit(true)
|
||||
}
|
||||
this.awaitingResults = false
|
||||
public groupedSongs: ChartData[][]
|
||||
|
||||
this.newResultsEmitter.emit(this.results)
|
||||
this.resultsChangedEmitter.emit(this.results)
|
||||
}
|
||||
public availableIcons: string[]
|
||||
|
||||
isLoading() {
|
||||
return this.awaitingResults
|
||||
}
|
||||
public searchControl = new FormControl('', { nonNullable: true })
|
||||
public isSng: FormControl<boolean>
|
||||
public instrument: FormControl<Instrument | null>
|
||||
public difficulty: FormControl<Difficulty | null>
|
||||
|
||||
/**
|
||||
* Event emitted when new search results are returned
|
||||
* or when more results are added to an existing search.
|
||||
* (emitted after `onNewSearch`)
|
||||
*/
|
||||
onSearchChanged(callback: (results: SongResult[]) => void) {
|
||||
this.resultsChangedEmitter.subscribe(callback)
|
||||
}
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
) {
|
||||
this.isSng = new FormControl<boolean>((localStorage.getItem('isSng') ?? 'true') === 'true', { nonNullable: true })
|
||||
this.isSng.valueChanges.subscribe(isSng => localStorage.setItem('isSng', `${isSng}`))
|
||||
|
||||
/**
|
||||
* Event emitted when a new search query is typed in.
|
||||
* (emitted before `onSearchChanged`)
|
||||
*/
|
||||
onNewSearch(callback: (results: SongResult[]) => void) {
|
||||
this.newResultsEmitter.subscribe(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Event emitted when the error state of the search changes.
|
||||
* (emitted before `onSearchChanged`)
|
||||
*/
|
||||
onSearchErrorStateUpdate(callback: (isError: boolean) => void) {
|
||||
this.errorStateEmitter.subscribe(callback)
|
||||
}
|
||||
|
||||
get resultCount() {
|
||||
return this.results.length
|
||||
}
|
||||
|
||||
async updateScroll() {
|
||||
if (!this.awaitingResults && !this._allResultsVisible) {
|
||||
this.awaitingResults = true
|
||||
this.currentQuery.offset += 50
|
||||
this.results.push(...this.trimLastChart(await window.electron.invoke.songSearch(this.currentQuery)))
|
||||
this.awaitingResults = false
|
||||
|
||||
this.resultsChangedEmitter.emit(this.results)
|
||||
}
|
||||
}
|
||||
|
||||
trimLastChart(results: SongResult[]) {
|
||||
if (results.length > 50) {
|
||||
results.splice(50, 1)
|
||||
this._allResultsVisible = false
|
||||
} else {
|
||||
this._allResultsVisible = true
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
get allResultsVisible() {
|
||||
return this._allResultsVisible
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders `versionResults` by lastModified date, but prefer the
|
||||
* non-pack version if it's only a few days older.
|
||||
*/
|
||||
sortChart(versionResults: VersionResult[]) {
|
||||
const dates: { [versionID: number]: number } = {}
|
||||
versionResults.forEach(version => dates[version.versionID] = new Date(version.lastModified).getTime())
|
||||
versionResults.sort((v1, v2) => {
|
||||
const diff = dates[v2.versionID] - dates[v1.versionID]
|
||||
if (Math.abs(diff) < 6.048e+8 && v1.driveData.inChartPack !== v2.driveData.inChartPack) {
|
||||
if (v1.driveData.inChartPack) {
|
||||
return 1 // prioritize v2
|
||||
} else {
|
||||
return -1 // prioritize v1
|
||||
}
|
||||
} else {
|
||||
return diff
|
||||
this.instrument = new FormControl<Instrument>(
|
||||
(localStorage.getItem('instrument') === 'null' ? null : localStorage.getItem('instrument')) as Instrument
|
||||
)
|
||||
this.instrument.valueChanges.subscribe(instrument => {
|
||||
localStorage.setItem('instrument', `${instrument}`)
|
||||
if (this.songsResponse.page) {
|
||||
this.search(this.searchControl.value || '*').subscribe()
|
||||
}
|
||||
})
|
||||
|
||||
this.difficulty = new FormControl<Difficulty>(
|
||||
(localStorage.getItem('difficulty') === 'null' ? null : localStorage.getItem('difficulty')) as Difficulty
|
||||
)
|
||||
this.difficulty.valueChanges.subscribe(difficulty => {
|
||||
localStorage.setItem('difficulty', `${difficulty}`)
|
||||
if (this.songsResponse.page) {
|
||||
this.search(this.searchControl.value || '*').subscribe()
|
||||
}
|
||||
})
|
||||
|
||||
this.http.get<{ "name": string; "sha1": string }[]>('https://clonehero.gitlab.io/sources/icons.json').subscribe(result => {
|
||||
this.availableIcons = result.map(r => r.name)
|
||||
})
|
||||
|
||||
this.search().subscribe()
|
||||
}
|
||||
|
||||
get areMorePages() { return this.songsResponse.page && this.groupedSongs.length === this.songsResponse.page * 20 }
|
||||
|
||||
/**
|
||||
* General search, uses the `/search?q=` endpoint.
|
||||
*
|
||||
* If fetching the next page, set `nextPage=true` to incremement the page count in the search.
|
||||
*
|
||||
* Leave the search term blank to fetch the songs with charts most recently added.
|
||||
*/
|
||||
public search(search = '*', nextPage = false) {
|
||||
this.searchLoading = true
|
||||
this.isDefaultSearch = search === '*'
|
||||
|
||||
if (nextPage) {
|
||||
this.currentPage++
|
||||
} else {
|
||||
this.currentPage = 1
|
||||
}
|
||||
|
||||
let retries = 10
|
||||
return this.http.post<SearchResult>(`/api/search`, {
|
||||
search,
|
||||
page: this.currentPage,
|
||||
instrument: this.instrument.value,
|
||||
difficulty: this.difficulty.value,
|
||||
}).pipe(
|
||||
catchError((err, caught) => {
|
||||
if (err.status === 400 || retries-- <= 0) {
|
||||
this.searchLoading = false
|
||||
console.log(err)
|
||||
return throwError(() => err)
|
||||
} else {
|
||||
return timer(2000).pipe(mergeMap(() => caught))
|
||||
}
|
||||
}),
|
||||
tap(response => {
|
||||
this.searchLoading = false
|
||||
|
||||
if (!nextPage) {
|
||||
// Don't reload results if they are the same
|
||||
if (this.groupedSongs && xorBy(this.songsResponse.data, response.data, r => r.chartId).length === 0) {
|
||||
return
|
||||
} else {
|
||||
this.groupedSongs = []
|
||||
}
|
||||
}
|
||||
this.songsResponse = response
|
||||
|
||||
this.groupedSongs.push(
|
||||
...chain(response.data)
|
||||
.groupBy(c => c.songId ?? -1 * c.chartId)
|
||||
.values()
|
||||
.value()
|
||||
)
|
||||
|
||||
this.searchUpdated.emit(response)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public advancedSearch(search: AdvancedSearch) {
|
||||
this.searchLoading = true
|
||||
this.isDefaultSearch = false
|
||||
|
||||
let retries = 10
|
||||
return this.http.post<{ data: SearchResult['data'] }>(`/api/search/advanced`, search).pipe(
|
||||
catchError((err, caught) => {
|
||||
if (err.status === 400 || retries-- <= 0) {
|
||||
this.searchLoading = false
|
||||
console.log(err)
|
||||
return throwError(() => err)
|
||||
} else {
|
||||
return timer(2000).pipe(mergeMap(() => caught))
|
||||
}
|
||||
}),
|
||||
tap(response => {
|
||||
this.searchLoading = false
|
||||
|
||||
// Don't reload results if they are the same
|
||||
if (this.groupedSongs && xorBy(this.songsResponse.data, response.data, r => r.chartId).length === 0) {
|
||||
return
|
||||
} else {
|
||||
this.groupedSongs = []
|
||||
}
|
||||
|
||||
this.songsResponse = response
|
||||
|
||||
this.groupedSongs.push(
|
||||
...chain(response.data)
|
||||
.groupBy(c => c.songId ?? -1 * c.chartId)
|
||||
.values()
|
||||
.value()
|
||||
)
|
||||
|
||||
this.searchUpdated.emit(response)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: maybe use this, or delete it
|
||||
/**
|
||||
* Orders `versionResults` by lastModified date, but prefer the
|
||||
* non-pack version if it's only a few days older.
|
||||
*/
|
||||
// sortChart(versionResults: VersionResult[]) {
|
||||
// const dates: { [versionID: number]: number } = {}
|
||||
// versionResults.forEach(version => dates[version.versionID] = new Date(version.lastModified).getTime())
|
||||
// versionResults.sort((v1, v2) => {
|
||||
// const diff = dates[v2.versionID] - dates[v1.versionID]
|
||||
// if (Math.abs(diff) < 6.048e+8 && v1.driveData.inChartPack !== v2.driveData.inChartPack) {
|
||||
// if (v1.driveData.inChartPack) {
|
||||
// return 1 // prioritize v2
|
||||
// } else {
|
||||
// return -1 // prioritize v1
|
||||
// }
|
||||
// } else {
|
||||
// return diff
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventEmitter, Injectable } from '@angular/core'
|
||||
|
||||
import { SongResult } from '../../../../src-shared/interfaces/search.interface'
|
||||
import { SearchResult } from '../../../../src-shared/interfaces/search.interface'
|
||||
import { SearchService } from './search.service'
|
||||
|
||||
// Note: this class prevents event cycles by only emitting events if the checkbox changes
|
||||
@@ -10,7 +10,7 @@ import { SearchService } from './search.service'
|
||||
})
|
||||
export class SelectionService {
|
||||
|
||||
private searchResults: SongResult[] = []
|
||||
private searchResults: Partial<SearchResult>
|
||||
|
||||
private selectAllChangedEmitter = new EventEmitter<boolean>()
|
||||
private selectionChangedCallbacks: { [songID: number]: (selection: boolean) => void } = {}
|
||||
@@ -19,14 +19,14 @@ export class SelectionService {
|
||||
private selections: { [songID: number]: boolean | undefined } = {}
|
||||
|
||||
constructor(searchService: SearchService) {
|
||||
searchService.onSearchChanged(results => {
|
||||
searchService.searchUpdated.subscribe(results => {
|
||||
this.searchResults = results
|
||||
if (this.allSelected) {
|
||||
this.selectAll() // Select newly added rows if allSelected
|
||||
}
|
||||
})
|
||||
|
||||
searchService.onNewSearch(results => {
|
||||
searchService.searchUpdated.subscribe(results => {
|
||||
this.searchResults = results
|
||||
this.selectionChangedCallbacks = {}
|
||||
this.selections = {}
|
||||
@@ -35,7 +35,9 @@ export class SelectionService {
|
||||
}
|
||||
|
||||
getSelectedResults() {
|
||||
return this.searchResults.filter(result => this.selections[result.id] === true)
|
||||
// TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return [] as any[] // this.searchResults.filter(result => this.selections[result.id] === true)
|
||||
}
|
||||
|
||||
onSelectAllChanged(callback: (selected: boolean) => void) {
|
||||
@@ -57,7 +59,8 @@ export class SelectionService {
|
||||
this.selectAllChangedEmitter.emit(false)
|
||||
}
|
||||
|
||||
setTimeout(() => this.searchResults.forEach(result => this.deselectSong(result.id)), 0)
|
||||
// TODO
|
||||
// setTimeout(() => this.searchResults.forEach(result => this.deselectSong(result.id)), 0)
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
@@ -66,7 +69,8 @@ export class SelectionService {
|
||||
this.selectAllChangedEmitter.emit(true)
|
||||
}
|
||||
|
||||
setTimeout(() => this.searchResults.forEach(result => this.selectSong(result.id)), 0)
|
||||
// TODO
|
||||
// setTimeout(() => this.searchResults.forEach(result => this.selectSong(result.id)), 0)
|
||||
}
|
||||
|
||||
deselectSong(songID: number) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
|
||||
import { Difficulty, Instrument } from 'scan-chart'
|
||||
|
||||
import { Settings, themes } from '../../../../src-shared/Settings'
|
||||
|
||||
@Injectable({
|
||||
@@ -31,6 +33,22 @@ export class SettingsService {
|
||||
this.document.documentElement.setAttribute('data-theme', theme)
|
||||
}
|
||||
|
||||
get instrument() {
|
||||
return this.settings.instrument
|
||||
}
|
||||
set instrument(newValue: Instrument | null) {
|
||||
this.settings.instrument = newValue
|
||||
this.saveSettings()
|
||||
}
|
||||
|
||||
get difficulty() {
|
||||
return this.settings.difficulty
|
||||
}
|
||||
set difficulty(newValue: Difficulty | null) {
|
||||
this.settings.difficulty = newValue
|
||||
this.saveSettings()
|
||||
}
|
||||
|
||||
// Individual getters/setters
|
||||
get libraryDirectory() {
|
||||
return this.settings.libraryPath
|
||||
|
||||
BIN
src-angular/assets/images/instruments/guitarcoop.png
Normal file
BIN
src-angular/assets/images/instruments/guitarcoop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src-angular/assets/images/instruments/guitarcoopghl.png
Normal file
BIN
src-angular/assets/images/instruments/guitarcoopghl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
src-angular/assets/images/instruments/rhythmghl.png
Normal file
BIN
src-angular/assets/images/instruments/rhythmghl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
@@ -1,7 +1,4 @@
|
||||
import { IpcInvokeHandlers, IpcToMainEmitHandlers } from '../src-shared/interfaces/ipc.interface'
|
||||
import { getBatchSongDetails } from './ipc/browse/BatchSongDetailsHandler.ipc'
|
||||
import { songSearch } from './ipc/browse/SearchHandler.ipc'
|
||||
import { getSongDetails } from './ipc/browse/SongDetailsHandler.ipc'
|
||||
import { download } from './ipc/download/DownloadHandler'
|
||||
import { getSettings, setSettings } from './ipc/SettingsHandler.ipc'
|
||||
import { downloadUpdate, getCurrentVersion, getUpdateAvailable, quitAndInstall, retryUpdate } from './ipc/UpdateHandler.ipc'
|
||||
@@ -10,9 +7,6 @@ import { isMaximized, maximize, minimize, openUrl, quit, restore, showFile, show
|
||||
export function getIpcInvokeHandlers(): IpcInvokeHandlers {
|
||||
return {
|
||||
getSettings,
|
||||
songSearch,
|
||||
getSongDetails,
|
||||
getBatchSongDetails,
|
||||
getCurrentVersion,
|
||||
getUpdateAvailable,
|
||||
isMaximized,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { serverURL } from '../../../src-shared/Paths'
|
||||
|
||||
export async function getBatchSongDetails(songIds: number[]) {
|
||||
const response = await fetch(`https://${serverURL}/api/data/song-versions/${songIds.join(',')}`)
|
||||
return await response.json()
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { SongSearch } from '../../../src-shared/interfaces/search.interface'
|
||||
import { serverURL } from '../../../src-shared/Paths'
|
||||
|
||||
export async function songSearch(search: SongSearch) {
|
||||
const response = await fetch(`https://${serverURL}/api/search`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(search),
|
||||
})
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { serverURL } from '../../../src-shared/Paths'
|
||||
|
||||
export async function getSongDetails(songId: number) {
|
||||
const response = await fetch(`https://${serverURL}/api/data/song-versions/${songId}`)
|
||||
return await response.json()
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { parse } from 'path'
|
||||
import { rimraf } from 'rimraf'
|
||||
|
||||
import { NewDownload, ProgressType } from '../../../src-shared/interfaces/download.interface'
|
||||
import { DriveFile } from '../../../src-shared/interfaces/songDetails.interface'
|
||||
import { sanitizeFilename } from '../../../src-shared/UtilFunctions'
|
||||
import { hasVideoExtension } from '../../ElectronUtilFunctions'
|
||||
import { emitIpcEvent } from '../../main'
|
||||
@@ -26,7 +25,9 @@ export class ChartDownload {
|
||||
private cancelFn: undefined | (() => void)
|
||||
|
||||
private callbacks = {} as Callbacks
|
||||
private files: DriveFile[]
|
||||
// TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private files: any[]
|
||||
private percent = 0 // Needs to be stored here because errors won't know the exact percent
|
||||
private tempPath: string
|
||||
private wasCanceled = false
|
||||
@@ -60,7 +61,9 @@ export class ChartDownload {
|
||||
this.callbacks[event] = callback
|
||||
}
|
||||
|
||||
filterDownloadFiles(files: DriveFile[]) {
|
||||
// TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
filterDownloadFiles(files: any[]) {
|
||||
return files.filter(file => {
|
||||
return (file.name !== 'ch.dat') && (settings.downloadVideos || !hasVideoExtension(file.name))
|
||||
})
|
||||
|
||||
@@ -19,9 +19,6 @@ function getListenerAdder<K extends keyof IpcFromMainEmitEvents>(key: K) {
|
||||
const electronApi: ContextBridgeApi = {
|
||||
invoke: {
|
||||
getSettings: getInvoker('getSettings'),
|
||||
songSearch: getInvoker('songSearch'),
|
||||
getSongDetails: getInvoker('getSongDetails'),
|
||||
getBatchSongDetails: getInvoker('getBatchSongDetails'),
|
||||
getCurrentVersion: getInvoker('getCurrentVersion'),
|
||||
getUpdateAvailable: getInvoker('getUpdateAvailable'),
|
||||
isMaximized: getInvoker('isMaximized'),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Difficulty, Instrument } from 'scan-chart'
|
||||
|
||||
export const themes = [
|
||||
'business',
|
||||
'dark',
|
||||
@@ -21,6 +23,8 @@ export interface Settings {
|
||||
downloadVideos: boolean // If background videos should be downloaded
|
||||
theme: typeof themes[number] // The name of the currently enabled UI theme
|
||||
libraryPath: string | undefined // The path to the user's library
|
||||
instrument: Instrument | null // The instrument selected by default, or `null` for "Any Instrument"
|
||||
difficulty: Difficulty | null // The difficulty selected by default, or `null` for "Any Difficulty"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,4 +35,6 @@ export const defaultSettings: Settings = {
|
||||
downloadVideos: true,
|
||||
theme: 'dark',
|
||||
libraryPath: undefined,
|
||||
instrument: 'guitar',
|
||||
difficulty: null,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import sanitize from 'sanitize-filename'
|
||||
import { Difficulty, Instrument } from 'scan-chart'
|
||||
|
||||
// WARNING: do not import anything related to Electron; the code will not compile correctly.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AnyFunction = (...args: any) => any
|
||||
|
||||
/** Overwrites the type of a nested property in `T` with `U`. */
|
||||
export type Overwrite<T, U> = U extends object ? (
|
||||
T extends object ? {
|
||||
[K in keyof T]: K extends keyof U ? Overwrite<T[K], U[K]> : T[K];
|
||||
} : U
|
||||
) : U
|
||||
export type RequireMatchingProps<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> }
|
||||
|
||||
/**
|
||||
* @returns `https://drive.google.com/open?id=${fileID}`
|
||||
*/
|
||||
export function driveLink(fileId: string) {
|
||||
return `https://drive.google.com/open?id=${fileId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `filename` with all invalid filename characters replaced.
|
||||
*/
|
||||
@@ -58,3 +74,48 @@ export function groupBy<T>(objectList: T[], ...keys: (keyof T)[]) {
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export const instruments = [
|
||||
'guitar', 'guitarcoop', 'rhythm', 'bass', 'drums', 'keys', 'guitarghl', 'guitarcoopghl', 'rhythmghl', 'bassghl',
|
||||
] as const satisfies Readonly<Instrument[]>
|
||||
export const difficulties = ['expert', 'hard', 'medium', 'easy'] as const satisfies Readonly<Difficulty[]>
|
||||
|
||||
export function instrumentDisplay(instrument: Instrument | null) {
|
||||
switch (instrument) {
|
||||
case 'guitar': return 'Lead Guitar'
|
||||
case 'guitarcoop': return 'Co-op Guitar'
|
||||
case 'rhythm': return 'Rhythm Guitar'
|
||||
case 'bass': return 'Bass Guitar'
|
||||
case 'drums': return 'Drums'
|
||||
case 'keys': return 'Keys'
|
||||
case 'guitarghl': return 'GHL (6-fret) Lead Guitar'
|
||||
case 'guitarcoopghl': return 'GHL (6-fret) Co-op Guitar'
|
||||
case 'rhythmghl': return 'GHL (6-fret) Rhythm Guitar'
|
||||
case 'bassghl': return 'GHL (6-fret) Bass Guitar'
|
||||
case null: return 'Any Instrument'
|
||||
}
|
||||
}
|
||||
export function difficultyDisplay(difficulty: Difficulty | null) {
|
||||
switch (difficulty) {
|
||||
case 'expert': return 'Expert'
|
||||
case 'hard': return 'Hard'
|
||||
case 'medium': return 'Medium'
|
||||
case 'easy': return 'Easy'
|
||||
case null: return 'Any Difficulty'
|
||||
}
|
||||
}
|
||||
export function instrumentToDiff(instrument: Instrument | 'vocals') {
|
||||
switch (instrument) {
|
||||
case 'guitar': return 'diff_guitar'
|
||||
case 'guitarcoop': return 'diff_guitar_coop'
|
||||
case 'rhythm': return 'diff_rhythm'
|
||||
case 'bass': return 'diff_bass'
|
||||
case 'drums': return 'diff_drums'
|
||||
case 'keys': return 'diff_keys'
|
||||
case 'guitarghl': return 'diff_guitarghl'
|
||||
case 'guitarcoopghl': return 'diff_guitar_coop_ghl'
|
||||
case 'rhythmghl': return 'diff_rhythm_ghl'
|
||||
case 'bassghl': return 'diff_bassghl'
|
||||
case 'vocals': return 'diff_vocals'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DriveChart } from './songDetails.interface'
|
||||
|
||||
/**
|
||||
* Represents a user's request to interact with the download system.
|
||||
@@ -16,7 +15,10 @@ export interface NewDownload {
|
||||
chartName: string
|
||||
artist: string
|
||||
charter: string
|
||||
driveData: DriveChart & { inChartPack: boolean }
|
||||
// TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
driveData: any
|
||||
// driveData: DriveChart & { inChartPack: boolean }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,6 @@ import { UpdateInfo } from 'electron-updater'
|
||||
|
||||
import { Settings } from '../Settings'
|
||||
import { Download, DownloadProgress } from './download.interface'
|
||||
import { SongResult, SongSearch } from './search.interface'
|
||||
import { VersionResult } from './songDetails.interface'
|
||||
import { UpdateProgress } from './update.interface'
|
||||
|
||||
export interface ContextBridgeApi {
|
||||
@@ -27,18 +25,6 @@ export interface IpcInvokeEvents {
|
||||
input: void
|
||||
output: Settings
|
||||
}
|
||||
songSearch: {
|
||||
input: SongSearch
|
||||
output: SongResult[]
|
||||
}
|
||||
getSongDetails: {
|
||||
input: SongResult['id']
|
||||
output: VersionResult[]
|
||||
}
|
||||
getBatchSongDetails: {
|
||||
input: number[]
|
||||
output: VersionResult[]
|
||||
}
|
||||
getCurrentVersion: {
|
||||
input: void
|
||||
output: string
|
||||
|
||||
@@ -1,96 +1,252 @@
|
||||
/**
|
||||
* Represents a user's song search query.
|
||||
*/
|
||||
export interface SongSearch { // TODO: make limit a setting that's not always 51
|
||||
query: string
|
||||
quantity: 'all' | 'any'
|
||||
similarity: 'similar' | 'exact'
|
||||
fields: SearchFields
|
||||
tags: SearchTags
|
||||
instruments: SearchInstruments
|
||||
difficulties: SearchDifficulties
|
||||
minDiff: number
|
||||
maxDiff: number
|
||||
limit: number
|
||||
offset: number
|
||||
import { EventType, FolderIssueType, MetadataIssueType, NotesData } from 'scan-chart'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { difficulties, instruments, Overwrite } from '../UtilFunctions'
|
||||
|
||||
export const GeneralSearchSchema = z.object({
|
||||
search: z.string(),
|
||||
page: z.number().positive(),
|
||||
instrument: z.enum(instruments).nullable(),
|
||||
difficulty: z.enum(difficulties).nullable(),
|
||||
})
|
||||
export type GeneralSearch = z.infer<typeof GeneralSearchSchema>
|
||||
|
||||
const md5Validator = z.string().regex(/^[a-f0-9]{32}$/, 'Invalid MD5 hash')
|
||||
|
||||
export const AdvancedSearchSchema = z.object({
|
||||
instrument: z.enum(instruments).nullable(),
|
||||
difficulty: z.enum(difficulties).nullable(),
|
||||
name: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
artist: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
album: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
genre: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
year: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
charter: z.object({ value: z.string(), exact: z.boolean(), exclude: z.boolean() }),
|
||||
minLength: z.number().nullable(),
|
||||
maxLength: z.number().nullable(),
|
||||
minIntensity: z.number().nullable(),
|
||||
maxIntensity: z.number().nullable(),
|
||||
minAverageNPS: z.number().nullable(),
|
||||
maxAverageNPS: z.number().nullable(),
|
||||
minMaxNPS: z.number().nullable(),
|
||||
maxMaxNPS: z.number().nullable(),
|
||||
modifiedAfter: z.string().regex(/^\d+-\d{2}-\d{2}$/, 'Invalid date').or(z.literal('')).nullable(),
|
||||
hash: z.string().transform(data =>
|
||||
data === '' || data.split(',').every(hash => md5Validator.safeParse(hash).success) ? data : 'invalid'
|
||||
).nullable(),
|
||||
hasSoloSections: z.boolean().nullable(),
|
||||
hasForcedNotes: z.boolean().nullable(),
|
||||
hasOpenNotes: z.boolean().nullable(),
|
||||
hasTapNotes: z.boolean().nullable(),
|
||||
hasLyrics: z.boolean().nullable(),
|
||||
hasVocals: z.boolean().nullable(),
|
||||
hasRollLanes: z.boolean().nullable(),
|
||||
has2xKick: z.boolean().nullable(),
|
||||
hasIssues: z.boolean().nullable(),
|
||||
hasVideoBackground: z.boolean().nullable(),
|
||||
modchart: z.boolean().nullable(),
|
||||
})
|
||||
export type AdvancedSearch = z.infer<typeof AdvancedSearchSchema>
|
||||
|
||||
export const advancedSearchTextProperties = [
|
||||
'name',
|
||||
'artist',
|
||||
'album',
|
||||
'genre',
|
||||
'year',
|
||||
'charter',
|
||||
] as const
|
||||
|
||||
export const advancedSearchNumberProperties = [
|
||||
'minLength',
|
||||
'maxLength',
|
||||
'minIntensity',
|
||||
'maxIntensity',
|
||||
'minAverageNPS',
|
||||
'maxAverageNPS',
|
||||
'minMaxNPS',
|
||||
'maxMaxNPS',
|
||||
] as const
|
||||
|
||||
export const advancedSearchBooleanProperties = [
|
||||
'hasSoloSections',
|
||||
'hasForcedNotes',
|
||||
'hasOpenNotes',
|
||||
'hasTapNotes',
|
||||
'hasLyrics',
|
||||
'hasVocals',
|
||||
'hasRollLanes',
|
||||
'has2xKick',
|
||||
'hasIssues',
|
||||
'hasVideoBackground',
|
||||
'modchart',
|
||||
] as const
|
||||
|
||||
export const ReportSchema = z.object({
|
||||
chartId: z.number().positive(),
|
||||
reason: z.string(),
|
||||
extraInfo: z.string(),
|
||||
})
|
||||
export type Report = z.infer<typeof ReportSchema>
|
||||
|
||||
export type NoteStringNotesData = Overwrite<NotesData, {
|
||||
maxNps: {
|
||||
notes: {
|
||||
type: keyof typeof EventType
|
||||
}[]
|
||||
}[]
|
||||
}>
|
||||
|
||||
export interface FolderIssue {
|
||||
folderIssue: FolderIssueType
|
||||
description: string
|
||||
}
|
||||
|
||||
export function getDefaultSearch(): SongSearch {
|
||||
return {
|
||||
query: '',
|
||||
quantity: 'all',
|
||||
similarity: 'similar',
|
||||
fields: { name: true, artist: true, album: true, genre: true, year: true, charter: true, tag: true },
|
||||
tags: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'sections': false, 'star power': false, 'forcing': false, 'taps': false, 'lyrics': false,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'video': false, 'stems': false, 'solo sections': false, 'open notes': false,
|
||||
},
|
||||
instruments: {
|
||||
guitar: false, bass: false, rhythm: false, keys: false,
|
||||
drums: false, guitarghl: false, bassghl: false, vocals: false,
|
||||
},
|
||||
difficulties: { expert: false, hard: false, medium: false, easy: false },
|
||||
minDiff: 0,
|
||||
maxDiff: 6,
|
||||
limit: 50 + 1,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
export type ChartData = SearchResult['data'][number]
|
||||
export interface SearchResult {
|
||||
found: number
|
||||
out_of: number
|
||||
page: number
|
||||
search_time_ms: number
|
||||
data: {
|
||||
/** The song name. */
|
||||
name: string | null
|
||||
/** The song artist. */
|
||||
artist: string | null
|
||||
/** The song album. */
|
||||
album: string | null
|
||||
/** The song genre. */
|
||||
genre: string | null
|
||||
/** The song year. */
|
||||
year: string | null
|
||||
/** The name of the chart, or `null` if the same as `name`. */
|
||||
chartName: string | null
|
||||
/** The genre of the chart, or `null` if the same as `genre`. */
|
||||
chartGenre: string | null
|
||||
/** The album of the chart, or `null` if the same as `album`. */
|
||||
chartAlbum: string | null
|
||||
/** The year of the chart, or `null` if the same as `year`. */
|
||||
chartYear: string | null
|
||||
/** The unique database identifier for the chart. */
|
||||
chartId: number
|
||||
/** The unique database identifier for the song, or `null` if there is only one chart of the song. */
|
||||
songId: number | null
|
||||
/** The MD5 hash of the normalized album art file. */
|
||||
albumArtMd5: string | null
|
||||
/** The MD5 hash of the chart folder or .sng file. */
|
||||
md5: string
|
||||
/** The MD5 hash of just the .chart or .mid file. */
|
||||
chartMd5: string
|
||||
/**
|
||||
* Different versions of the same chart have the same `versionGroupId`.
|
||||
* All charts in a version group have this set to the smallest `id` in the group.
|
||||
*/
|
||||
versionGroupId: number
|
||||
/** The chart's charter(s). */
|
||||
charter: string | null
|
||||
/** The length of the chart's audio, in milliseconds. If there are stems, this is the length of the longest stem. */
|
||||
song_length: number | null
|
||||
/** The difficulty rating of the chart as a whole. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_band: number | null
|
||||
/** The difficulty rating of the lead guitar chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_guitar: number | null
|
||||
/** The difficulty rating of the co-op guitar chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_guitar_coop: number | null
|
||||
/** The difficulty rating of the rhythm guitar chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_rhythm: number | null
|
||||
/** The difficulty rating of the bass guitar chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_bass: number | null
|
||||
/** The difficulty rating of the drums chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_drums: number | null
|
||||
/** The difficulty rating of the Phase Shift "real drums" chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_drums_real: number | null
|
||||
/** The difficulty rating of the keys chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_keys: number | null
|
||||
/** The difficulty rating of the GHL (6-fret) lead guitar chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_guitarghl: number | null
|
||||
/** The difficulty rating of the GHL (6-fret) co-op guitar chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_guitar_coop_ghl: number | null
|
||||
/** The difficulty rating of the GHL (6-fret) rhythm guitar chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_rhythm_ghl: number | null
|
||||
/** The difficulty rating of the GHL (6-fret) bass guitar chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_bassghl: number | null
|
||||
/** The difficulty rating of the vocals chart. Usually an integer between 0 and 6 (inclusive) */
|
||||
diff_vocals: number | null
|
||||
/** The number of milliseconds into the song where the chart's audio preview should start playing. */
|
||||
preview_start_time: number | null
|
||||
/** The name of the icon to be displayed on the chart. Usually represents a charter or setlist. */
|
||||
icon: string | null
|
||||
/** A text phrase that will be displayed before the chart begins. */
|
||||
loading_phrase: string | null
|
||||
/** The ordinal position of the song on the album. This is `undefined` if it's not on an album. */
|
||||
album_track: number | null
|
||||
/** The ordinal position of the chart in its setlist. This is `undefined` if it's not on a setlist. */
|
||||
playlist_track: number | null
|
||||
/** `true` if the chart is a modchart. This only affects how the chart is filtered and displayed, and doesn't impact gameplay. */
|
||||
modchart: boolean | null
|
||||
/** The amount of time the game should delay the start of the track in milliseconds. */
|
||||
delay: number | null
|
||||
/** The amount of time the game should delay the start of the track in seconds. */
|
||||
chart_offset: number | null
|
||||
/** Overrides the default HOPO threshold with a specified value in ticks. Only applies to .mid charts. */
|
||||
hopo_frequency: number | null
|
||||
/** Sets the HOPO threshold to be a 1/8th step. Only applies to .mid charts. */
|
||||
eighthnote_hopo: boolean | null
|
||||
/** Overrides the .mid note number for Star Power on 5-Fret Guitar. Valid values are 103 and 116. Only applies to .mid charts. */
|
||||
multiplier_note: number | null
|
||||
/**
|
||||
* The amount of time that should be skipped from the beginning of the video background in milliseconds.
|
||||
* A negative value will delay the start of the video by that many milliseconds.
|
||||
*/
|
||||
video_start_time: number | null
|
||||
/** `true` if the "drums" track should be interpreted as 5-lane drums. */
|
||||
five_lane_drums: boolean | null
|
||||
/** `true` if the "drums" track should be interpreted as 4-lane pro drums. */
|
||||
pro_drums: boolean | null
|
||||
/** `true` if the chart's end events should be used to end the chart early. Only applies to .mid charts. */
|
||||
end_events: boolean | null
|
||||
/** Data describing properties of the .chart or .mid file. `undefined` if the .chart or .mid file couldn't be parsed. */
|
||||
notesData: NoteStringNotesData
|
||||
/** Issues with the chart files. */
|
||||
folderIssues: FolderIssue[]
|
||||
/** Issues with the chart's metadata. */
|
||||
metadataIssues: MetadataIssueType[]
|
||||
/** `true` if the chart has a video background. */
|
||||
hasVideoBackground: boolean
|
||||
/** The date of the last time this chart was modified in Google Drive. */
|
||||
modifiedTime: string
|
||||
|
||||
export interface SearchFields {
|
||||
name: boolean
|
||||
artist: boolean
|
||||
album: boolean
|
||||
genre: boolean
|
||||
year: boolean
|
||||
charter: boolean
|
||||
tag: boolean
|
||||
}
|
||||
|
||||
export interface SearchTags {
|
||||
'sections': boolean // Tag inverted
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'star power': boolean // Tag inverted
|
||||
'forcing': boolean // Tag inverted
|
||||
'taps': boolean
|
||||
'lyrics': boolean
|
||||
'video': boolean
|
||||
'stems': boolean
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'solo sections': boolean
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'open notes': boolean
|
||||
}
|
||||
|
||||
export interface SearchInstruments {
|
||||
guitar: boolean
|
||||
bass: boolean
|
||||
rhythm: boolean
|
||||
keys: boolean
|
||||
drums: boolean
|
||||
guitarghl: boolean
|
||||
bassghl: boolean
|
||||
vocals: boolean
|
||||
}
|
||||
|
||||
export interface SearchDifficulties {
|
||||
expert: boolean
|
||||
hard: boolean
|
||||
medium: boolean
|
||||
easy: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single song search result.
|
||||
*/
|
||||
export interface SongResult {
|
||||
id: number
|
||||
chartCount: number
|
||||
name: string
|
||||
artist: string
|
||||
album: string
|
||||
genre: string
|
||||
year: string
|
||||
/** The Drive ID of the chart's application folder. */
|
||||
applicationDriveId: string
|
||||
/** The primary username of the chart's application's applicant, or `null` if packName is not `null` */
|
||||
applicationUsername: string
|
||||
/** The name of the pack source, or `null` if the application is not a pack source. */
|
||||
packName: string | null
|
||||
/** The `folderId` of the Google Drive folder that contains the chart (or the shortcut to it). */
|
||||
parentFolderId: string
|
||||
/**
|
||||
* A string containing the relative path from the application folder to the `DriveChart`.
|
||||
*
|
||||
* Doesn't contain the application folder name, the file name (for file charts), or leading/trailing slashes.
|
||||
*/
|
||||
drivePath: string
|
||||
/** The Drive ID of the chart file, or `null` if the chart is a chart folder. */
|
||||
driveFileId: string | null
|
||||
/** The file name of the chart file, or `null` if the chart is a chart folder. */
|
||||
driveFileName: string | null
|
||||
/** If there is more than one chart contained inside this `DriveChart`. */
|
||||
driveChartIsPack: boolean
|
||||
/**
|
||||
* A string containing the relative path from the `DriveChart`'s archive to the chart inside the archive.
|
||||
*
|
||||
* Doesn't contain the archive name, the chart file name (for file charts), or leading/trailing slashes.
|
||||
*
|
||||
* An empty string if the `DriveChart` is not an archive.
|
||||
*/
|
||||
archivePath: string
|
||||
/**
|
||||
* The name of the .sng file. `null` if the chart is not a .sng file.
|
||||
*/
|
||||
chartFileName: string | null
|
||||
}[]
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Represents a single chart version.
|
||||
*/
|
||||
export interface VersionResult {
|
||||
versionID: number
|
||||
chartID: number
|
||||
songID: number
|
||||
latestVersionID: number
|
||||
latestSetlistVersionID: number
|
||||
name: string
|
||||
chartName: string
|
||||
artist: string
|
||||
album: string
|
||||
genre: string
|
||||
year: string
|
||||
songDataIncorrect: boolean
|
||||
driveData: DriveChart & { inChartPack: boolean }
|
||||
md5: string
|
||||
lastModified: string
|
||||
icon: string
|
||||
charters: string
|
||||
charterIDs: string
|
||||
tags: string | null
|
||||
songLength: number
|
||||
diff_band: number
|
||||
diff_guitar: number
|
||||
diff_bass: number
|
||||
diff_rhythm: number
|
||||
diff_drums: number
|
||||
diff_keys: number
|
||||
diff_guitarghl: number
|
||||
diff_bassghl: number
|
||||
chartData: ChartData
|
||||
}
|
||||
|
||||
export interface DriveChart {
|
||||
source: DriveSource
|
||||
isArchive: boolean
|
||||
downloadPath: string
|
||||
filesHash: string
|
||||
folderName: string
|
||||
folderID: string
|
||||
files: DriveFile[]
|
||||
}
|
||||
|
||||
export interface DriveSource {
|
||||
isSetlistSource: boolean
|
||||
isDriveFileSource?: boolean
|
||||
setlistIcon?: string
|
||||
sourceUserIDs: number[]
|
||||
sourceName: string
|
||||
sourceDriveID: string
|
||||
proxyLink?: string
|
||||
}
|
||||
|
||||
export interface DriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
webContentLink: string
|
||||
modifiedTime: string
|
||||
md5Checksum: string
|
||||
size: string
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
hasSections: boolean
|
||||
hasStarPower: boolean
|
||||
hasForced: boolean
|
||||
hasTap: boolean
|
||||
hasOpen: {
|
||||
[instrument: string]: boolean
|
||||
}
|
||||
hasSoloSections: boolean
|
||||
hasLyrics: boolean
|
||||
is120: boolean
|
||||
hasBrokenNotes: boolean
|
||||
noteCounts: {
|
||||
[instrument in Instrument]: {
|
||||
[difficulty in ChartedDifficulty]: number
|
||||
}
|
||||
}
|
||||
/** number of seconds */
|
||||
length: number
|
||||
/** number of seconds */
|
||||
effectiveLength: number
|
||||
}
|
||||
|
||||
export type Instrument = 'guitar' | 'bass' | 'rhythm' | 'keys' | 'drums' | 'guitarghl' | 'bassghl' | 'vocals' | 'undefined'
|
||||
export type ChartedDifficulty = 'x' | 'h' | 'm' | 'e'
|
||||
|
||||
export function getInstrumentIcon(instrument: Instrument) {
|
||||
return `${instrument}.png`
|
||||
}
|
||||
Reference in New Issue
Block a user