Interface conversion, search bar layout

This commit is contained in:
Geomitron
2023-12-09 18:21:01 -06:00
parent d689843f27
commit ece0f75b99
37 changed files with 1531 additions and 760 deletions

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Ignore semantic UI in GitHub Language Statistics
src/assets/semantic/** linguist-vendored

View File

@@ -24,7 +24,7 @@
"[html]": { "[html]": {
"editor.defaultFormatter": "vscode.html-language-features", "editor.defaultFormatter": "vscode.html-language-features",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
} }
}, },
"[typescript]": { "[typescript]": {

View File

@@ -85,6 +85,14 @@
} }
], ],
"outputHashing": "all" "outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
@@ -99,9 +107,10 @@
"buildTarget": "Bridge:build:production" "buildTarget": "Bridge:build:production"
}, },
"development": { "development": {
"buildTarget": "angular:build:development" "buildTarget": "Bridge:build:development"
} }
} },
"defaultConfiguration": "development"
} }
} }
} }

View File

@@ -36,6 +36,7 @@
"bootstrap-icons": "^1.11.2", "bootstrap-icons": "^1.11.2",
"bottleneck": "^2.19.5", "bottleneck": "^2.19.5",
"comparators": "^3.0.2", "comparators": "^3.0.2",
"dayjs": "^1.11.10",
"electron-unhandled": "^4.0.1", "electron-unhandled": "^4.0.1",
"electron-updater": "^4.3.1", "electron-updater": "^4.3.1",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
@@ -45,7 +46,9 @@
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"scan-chart": "^3.4.1",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"zod": "^3.22.4",
"zone.js": "~0.14.2" "zone.js": "~0.14.2"
}, },
"devDependencies": { "devDependencies": {

298
pnpm-lock.yaml generated
View File

@@ -38,6 +38,9 @@ dependencies:
comparators: comparators:
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.5 version: 3.0.5
dayjs:
specifier: ^1.11.10
version: 1.11.10
electron-unhandled: electron-unhandled:
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1 version: 4.0.1
@@ -65,9 +68,15 @@ dependencies:
sanitize-filename: sanitize-filename:
specifier: ^1.6.3 specifier: ^1.6.3
version: 1.6.3 version: 1.6.3
scan-chart:
specifier: ^3.4.1
version: 3.4.1
tslib: tslib:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.6.2 version: 2.6.2
zod:
specifier: ^3.22.4
version: 3.22.4
zone.js: zone.js:
specifier: ~0.14.2 specifier: ~0.14.2
version: 0.14.2 version: 0.14.2
@@ -3811,7 +3820,6 @@ packages:
/async@3.2.5: /async@3.2.5:
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
dev: true
/asynckit@0.4.0: /asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -3867,6 +3875,10 @@ packages:
dequal: 2.0.3 dequal: 2.0.3
dev: true 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): /babel-loader@9.1.3(@babel/core@7.23.2)(webpack@5.89.0):
resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==} resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==}
engines: {node: '>= 14.15.0'} engines: {node: '>= 14.15.0'}
@@ -3934,7 +3946,6 @@ packages:
/base64-js@1.5.1: /base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
/base64id@2.0.0: /base64id@2.0.0:
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
@@ -3959,13 +3970,17 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/binary-parser@2.2.1:
resolution: {integrity: sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==}
engines: {node: '>=12'}
dev: false
/bl@4.1.0: /bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
dependencies: dependencies:
buffer: 5.7.1 buffer: 5.7.1
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 3.6.2 readable-stream: 3.6.2
dev: true
/bluebird-lst@1.0.9: /bluebird-lst@1.0.9:
resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==}
@@ -4163,7 +4178,6 @@ packages:
dependencies: dependencies:
base64-js: 1.5.1 base64-js: 1.5.1
ieee754: 1.2.1 ieee754: 1.2.1
dev: true
/builder-util-runtime@8.9.2: /builder-util-runtime@8.9.2:
resolution: {integrity: sha512-rhuKm5vh7E0aAmT6i8aoSfEjxzdYEFX7zDApK+eNgOhjofnWb74d9SRJv0H/8nsgOkos0TZ4zxW0P8J4N7xQ2A==} resolution: {integrity: sha512-rhuKm5vh7E0aAmT6i8aoSfEjxzdYEFX7zDApK+eNgOhjofnWb74d9SRJv0H/8nsgOkos0TZ4zxW0P8J4N7xQ2A==}
@@ -4337,6 +4351,10 @@ packages:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true dev: true
/charset-detector@0.0.2:
resolution: {integrity: sha512-aZBFdf9aE168W7w3t9JNC0w9hZdTKjtq1hsrfbziJA4DrNI22Mfrc74gw3aADIA9bCQV2IpnbjyQcDHa3qM4rg==}
dev: false
/chokidar@3.5.3: /chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
@@ -4352,6 +4370,10 @@ packages:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
/chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
dev: false
/chownr@2.0.0: /chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -4465,6 +4487,21 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
requiresBuild: true 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: /colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
dev: true dev: true
@@ -4780,6 +4817,10 @@ packages:
'@babel/runtime': 7.23.2 '@babel/runtime': 7.23.2
dev: true dev: true
/dayjs@1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dev: false
/debug@2.6.9: /debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies: peerDependencies:
@@ -4831,7 +4872,11 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dependencies: dependencies:
mimic-response: 3.1.0 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: /deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 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} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: true 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: /detect-node@2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
dev: true dev: true
@@ -5240,7 +5290,6 @@ packages:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies: dependencies:
once: 1.4.0 once: 1.4.0
dev: true
/engine.io-client@6.5.3: /engine.io-client@6.5.3:
resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==}
@@ -5760,7 +5809,6 @@ packages:
/events@3.3.0: /events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'} engines: {node: '>=0.8.x'}
dev: true
/execa@5.1.1: /execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
@@ -5792,6 +5840,11 @@ packages:
strip-final-newline: 3.0.0 strip-final-newline: 3.0.0
dev: true 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: /exponential-backoff@3.1.1:
resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
dev: true dev: true
@@ -5873,6 +5926,10 @@ packages:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
dev: true dev: true
/fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
dev: false
/fast-glob@3.3.1: /fast-glob@3.3.1:
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
engines: {node: '>=8.6.0'} engines: {node: '>=8.6.0'}
@@ -6041,6 +6098,14 @@ packages:
resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==}
dev: true 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): /follow-redirects@1.15.3(debug@4.3.2):
resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@@ -6091,7 +6156,6 @@ packages:
/fs-constants@1.0.0: /fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
dev: true
/fs-extra@10.1.0: /fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
@@ -6229,6 +6293,10 @@ packages:
get-intrinsic: 1.2.2 get-intrinsic: 1.2.2
dev: true dev: true
/github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
dev: false
/glob-parent@5.1.2: /glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -6654,7 +6722,6 @@ packages:
/ieee754@1.2.1: /ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
requiresBuild: true requiresBuild: true
dev: true
/ignore-by-default@1.0.1: /ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
@@ -6725,6 +6792,10 @@ packages:
/inherits@2.0.4: /inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
/ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
dev: false
/ini@4.1.1: /ini@4.1.1:
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -6786,6 +6857,10 @@ packages:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: true dev: true
/is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
dev: false
/is-bigint@1.0.4: /is-bigint@1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies: dependencies:
@@ -7513,6 +7588,19 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
dev: true 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: /mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -7560,7 +7648,6 @@ packages:
/mimic-response@3.1.0: /mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true
/mini-css-extract-plugin@2.7.6(webpack@5.89.0): /mini-css-extract-plugin@2.7.6(webpack@5.89.0):
resolution: {integrity: sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==} resolution: {integrity: sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==}
@@ -7683,6 +7770,10 @@ packages:
resolution: {integrity: sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==} resolution: {integrity: sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==}
dev: true dev: true
/mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
dev: false
/mkdirp@0.5.6: /mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true hasBin: true
@@ -7754,6 +7845,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/napi-build-utils@1.0.2:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
dev: false
/natural-compare@1.4.0: /natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true dev: true
@@ -7796,6 +7891,13 @@ packages:
dev: true dev: true
optional: 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: /node-addon-api@1.7.2:
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
requiresBuild: true requiresBuild: true
@@ -7808,6 +7910,10 @@ packages:
dev: true dev: true
optional: true optional: true
/node-addon-api@6.1.0:
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
dev: false
/node-forge@1.3.1: /node-forge@1.3.1:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'} engines: {node: '>= 6.13.0'}
@@ -8329,6 +8435,13 @@ packages:
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
dev: true 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: /parse5-html-rewriting-stream@7.0.0:
resolution: {integrity: sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==} resolution: {integrity: sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==}
dependencies: dependencies:
@@ -8593,6 +8706,25 @@ packages:
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true 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: /prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -8703,7 +8835,6 @@ packages:
dependencies: dependencies:
end-of-stream: 1.4.4 end-of-stream: 1.4.4
once: 1.4.0 once: 1.4.0
dev: true
/punycode@2.3.1: /punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
@@ -8721,6 +8852,10 @@ packages:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true
/queue-tick@1.0.1:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
dev: false
/quick-lru@5.1.1: /quick-lru@5.1.1:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -8747,6 +8882,16 @@ packages:
unpipe: 1.0.0 unpipe: 1.0.0
dev: true 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: /react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true dev: true
@@ -8805,7 +8950,6 @@ packages:
inherits: 2.0.4 inherits: 2.0.4
string_decoder: 1.3.0 string_decoder: 1.3.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
dev: true
/readdirp@3.6.0: /readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
@@ -9049,7 +9193,6 @@ packages:
/safe-buffer@5.2.1: /safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: true
/safe-regex-test@1.0.0: /safe-regex-test@1.0.0:
resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
@@ -9105,6 +9248,22 @@ packages:
/sax@1.3.0: /sax@1.3.0:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} 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: /schema-utils@3.3.0:
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
@@ -9315,6 +9474,21 @@ packages:
kind-of: 6.0.3 kind-of: 6.0.3
dev: true 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: /shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -9357,6 +9531,24 @@ packages:
- supports-color - supports-color
dev: true 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: /simple-update-notifier@1.1.0:
resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
@@ -9611,6 +9803,13 @@ packages:
limiter: 1.1.5 limiter: 1.1.5
dev: true 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: /string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -9662,7 +9861,6 @@ packages:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
dev: true
/strip-ansi@3.0.1: /strip-ansi@3.0.1:
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
@@ -9698,6 +9896,11 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true 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: /strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -9816,6 +10019,23 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true 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: /tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -9825,7 +10045,14 @@ packages:
fs-constants: 1.0.0 fs-constants: 1.0.0
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 3.6.2 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: /tar@6.2.0:
resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==}
@@ -10020,6 +10247,12 @@ packages:
- supports-color - supports-color
dev: true 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: /type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -10196,12 +10429,16 @@ packages:
punycode: 2.3.1 punycode: 2.3.1
dev: true 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: /utf8-byte-length@1.0.4:
resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==} resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==}
/util-deprecate@1.0.2: /util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
/utils-merge@1.0.1: /utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
@@ -10509,6 +10746,13 @@ packages:
has-tostringtag: 1.0.0 has-tostringtag: 1.0.0
dev: true dev: true
/which@1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true
dependencies:
isexe: 2.0.0
dev: false
/which@2.0.2: /which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -10528,6 +10772,10 @@ packages:
resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==}
dev: true dev: true
/workerpool@6.5.1:
resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==}
dev: false
/wrap-ansi@6.2.0: /wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -10663,7 +10911,25 @@ packages:
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
dev: true dev: true
/zod@3.22.4:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
dev: false
/zone.js@0.14.2: /zone.js@0.14.2:
resolution: {integrity: sha512-X4U7J1isDhoOmHmFWiLhloWc2lzMkdnumtfQ1LXzf/IOZp5NQYuMUTaviVzG/q1ugMBIXzin2AqeVJUoSEkNyQ==} resolution: {integrity: sha512-X4U7J1isDhoOmHmFWiLhloWc2lzMkdnumtfQ1LXzf/IOZp5NQYuMUTaviVzG/q1ugMBIXzin2AqeVJUoSEkNyQ==}
dependencies: dependencies:
tslib: 2.6.2 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

View File

@@ -1,10 +1,12 @@
import { HttpClientModule } from '@angular/common/http'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { AppRoutingModule } from './app-routing.module' import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component' import { AppComponent } from './app.component'
import { BrowseComponent } from './components/browse/browse.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 { ChartSidebarComponent } from './components/browse/chart-sidebar/chart-sidebar.component'
import { ResultTableRowComponent } from './components/browse/result-table/result-table-row/result-table-row.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' import { ResultTableComponent } from './components/browse/result-table/result-table.component'
@@ -25,6 +27,7 @@ import { ProgressBarDirective } from './core/directives/progress-bar.directive'
StatusBarComponent, StatusBarComponent,
ResultTableComponent, ResultTableComponent,
ChartSidebarComponent, ChartSidebarComponent,
ChartSidebarInstrumentComponent,
ResultTableRowComponent, ResultTableRowComponent,
DownloadsModalComponent, DownloadsModalComponent,
ProgressBarDirective, ProgressBarDirective,
@@ -35,6 +38,8 @@ import { ProgressBarDirective } from './core/directives/progress-bar.directive'
BrowserModule, BrowserModule,
AppRoutingModule, AppRoutingModule,
FormsModule, FormsModule,
ReactiveFormsModule,
HttpClientModule,
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -1,36 +1,34 @@
<div id="sidebarCard" *ngIf="selectedVersion" class="ui fluid card"> <div id="sidebarCard" *ngIf="selectedChart" class="ui fluid card">
<div class="ui placeholder" [ngClass]="{ placeholder: albumArtSrc === '', inverted: settingsService.theme === 'dark' }"> <div class="ui placeholder">
<img *ngIf="albumArtSrc !== null" class="ui square image" [src]="albumArtSrc" /> @if (albumArtMd5) {
<img src="https://files.enchor.us/{{ albumArtMd5 }}.jpg" alt="Album art" loading="lazy" class="object-cover w-40" />
}
</div> </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" /> <input type="hidden" name="Chart" />
<i id="chartDropdownIcon" class="dropdown icon"></i> <i id="chartDropdownIcon" class="dropdown icon"></i>
<div class="default text"></div> <div class="default text"></div>
<div id="chartDropdownMenu" class="menu"></div> <div id="chartDropdownMenu" class="menu"></div>
</div> </div>
<div id="textPanel" class="content"> <div id="textPanel" class="content">
<span class="header">{{ selectedVersion.chartName }}</span> <span class="header">{{ selectedChart.chartName }}</span>
<div class="description"> <div class="description">
<div *ngIf="songResult!.album === null"><b>Album:</b> {{ selectedVersion.album }} ({{ selectedVersion.year }})</div> <div *ngIf="selectedChart.chartAlbum"><b>Album:</b> {{ selectedChart.chartAlbum }}</div>
<div *ngIf="songResult!.album !== null"><b>Year:</b> {{ selectedVersion.year }}</div> <div *ngIf="selectedChart.chartGenre"><b>Genre:</b> {{ selectedChart.chartGenre }}</div>
<div *ngIf="songResult!.genre === null"><b>Genre:</b> {{ selectedVersion.genre }}</div> <div *ngIf="selectedChart.chartYear"><b>Year:</b> {{ selectedChart.chartYear }}</div>
<div> <div><b>Charter:</b> {{ selectedChart.charter }}</div>
<b>{{ charterPlural }}</b> {{ selectedVersion.charters }}
</div>
<div *ngIf="selectedVersion.tags"><b>Tags:</b> {{ selectedVersion.tags }}</div>
<div><b>Audio Length:</b> {{ songLength }}</div> <div><b>Audio Length:</b> {{ songLength }}</div>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui horizontal list"> <div class="ui horizontal list">
<div *ngFor="let difficulty of difficultiesList" class="item"> @if (selectedChart.notesData.hasVocals) {
<img class="ui avatar image" src="assets/images/instruments/{{ difficulty.instrument }}" /> <app-chart-sidebar-instrument [chart]="selectedChart" instrument="vocals" />
<div class="content"> }
<div class="header">Diff: {{ difficulty.diffNumber }}</div> @for (instrument of instruments; track $index) {
{{ difficulty.chartedDifficulties }} <app-chart-sidebar-instrument [chart]="selectedChart" [instrument]="instrument" />
</div> }
</div>
</div> </div>
<div id="sourceLinks"> <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()"> <button *ngIf="shownFolderButton()" id="folderButton" class="mini ui icon button" (click)="onFolderButtonClicked()">
<i class="folder open outline icon"></i> <i class="folder open outline icon"></i>
</button> </button>
@@ -38,8 +36,8 @@
</div> </div>
</div> </div>
<div id="downloadButtons" class="ui positive buttons"> <div id="downloadButtons" class="ui positive buttons">
<div id="downloadButton" class="ui button" (click)="onDownloadClicked()">{{ downloadButtonText }}</div> <div id="downloadButton" class="ui button" (click)="onDownloadClicked()">Download</div>
<div *ngIf="getSelectedChartVersions().length > 1" id="versionDropdown" class="ui floating dropdown icon button"> <div id="versionDropdown" class="ui floating dropdown icon button">
<i class="dropdown icon"></i> <i class="dropdown icon"></i>
</div> </div>
</div> </div>

View File

@@ -1,13 +1,11 @@
import { Component, OnInit } from '@angular/core' 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 { SearchService } from 'src-angular/app/core/services/search.service'
import { SettingsService } from 'src-angular/app/core/services/settings.service' 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 { driveLink, instruments } from 'src-shared/UtilFunctions'
import { ChartedDifficulty, getInstrumentIcon, Instrument, VersionResult } from '../../../../../src-shared/interfaces/songDetails.interface'
import { groupBy } from '../../../../../src-shared/UtilFunctions'
import { DownloadService } from '../../../core/services/download.service'
interface Difficulty { interface Difficulty {
instrument: string instrument: string
@@ -22,252 +20,81 @@ interface Difficulty {
}) })
export class ChartSidebarComponent implements OnInit { export class ChartSidebarComponent implements OnInit {
songResult: SongResult | undefined selectedChart: ChartData | null = null
selectedVersion: VersionResult charts: ChartData[][] | null = null
charts: VersionResult[][]
albumArtSrc: SafeUrl = ''
charterPlural: string
songLength: string songLength: string
difficultiesList: Difficulty[] difficultiesList: Difficulty[]
downloadButtonText: string
constructor( constructor(
private downloadService: DownloadService,
private searchService: SearchService, private searchService: SearchService,
public settingsService: SettingsService public settingsService: SettingsService
) { } ) { }
ngOnInit() { ngOnInit() {
this.searchService.onNewSearch(() => { this.searchService.searchUpdated.subscribe(() => {
this.selectVersion(undefined) this.charts = null
this.songResult = undefined this.selectedChart = null
}) })
} }
public get albumArtMd5() {
return flatMap(this.charts ?? []).find(c => !!c.albumArtMd5)?.albumArtMd5 || null
}
/** /**
* Displays the information for the selected song. * Displays the information for the selected song.
*/ */
async onRowClicked(result: SongResult) { async onRowClicked(song: ChartData[]) {
if (this.songResult === undefined || result.id !== this.songResult.id) { // Clicking the same row again will not reload this.charts = chain(song)
this.songResult = result .groupBy(c => c.versionGroupId)
const results = await window.electron.invoke.getSongDetails(result.id) .values()
this.charts = groupBy(results, 'chartID').sort((v1, v2) => v1[0].chartName.length - v2[0].chartName.length) .map(versionGroup => sortBy(versionGroup, vg => vg.modifiedTime).reverse())
this.sortCharts() .value()
await this.selectChart(this.charts[0][0].chartID) this.selectedChart = this.charts[0][0]
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}`
} }
/** /**
* Opens the proxy link or source folder in the default browser. * Opens the proxy link or source folder in the default browser.
*/ */
onSourceLinkClicked() { onSourceLinkClicked() {
const source = this.selectedVersion.driveData.source window.electron.emit.openUrl(driveLink(this.selectedChart!.applicationDriveId))
window.electron.emit.openUrl(source.proxyLink ?? `https://drive.google.com/drive/folders/${source.sourceDriveID}`)
} }
/** /**
* @returns `true` if the source folder button should be shown. * @returns `true` if the source folder button should be shown.
*/ */
shownFolderButton() { shownFolderButton() {
const driveData = this.selectedVersion.driveData return this.selectedChart!.applicationDriveId !== this.selectedChart!.parentFolderId
return driveData.source.proxyLink || driveData.source.sourceDriveID !== driveData.folderID
} }
/** /**
* Opens the chart folder in the default browser. * Opens the chart folder in the default browser.
*/ */
onFolderButtonClicked() { 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. * Adds the selected version to the download queue.
*/ */
onDownloadClicked() { onDownloadClicked() {
this.downloadService.addDownload( // TODO
this.selectedVersion.versionID, { // this.downloadService.addDownload(
chartName: this.selectedVersion.chartName, // this.selectedChart.versionID, {
artist: this.songResult!.artist, // chartName: this.selectedChart.chartName,
charter: this.selectedVersion.charters, // artist: this.songResult!.artist,
driveData: this.selectedVersion.driveData, // 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()
} }
} }

View File

@@ -4,9 +4,8 @@
</div> </div>
</td> </td>
<td> <td>
<span id="chartCount" *ngIf="result.chartCount > 1">{{ result.chartCount }}</span <span id="chartCount" *ngIf="song.length > 1">{{ song.length }}</span> {{ song[0].name }}
>{{ result.name }}
</td> </td>
<td>{{ result.artist }}</td> <td>{{ song[0].artist }}</td>
<td>{{ result.album || 'Various' }}</td> <td>{{ song[0].album || 'Various' }}</td>
<td>{{ result.genre || 'Various' }}</td> <td>{{ song[0].genre || 'Various' }}</td>

View File

@@ -1,6 +1,6 @@
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core' 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' import { SelectionService } from '../../../../core/services/selection.service'
@Component({ @Component({
@@ -9,14 +9,14 @@ import { SelectionService } from '../../../../core/services/selection.service'
styleUrls: ['./result-table-row.component.scss'], styleUrls: ['./result-table-row.component.scss'],
}) })
export class ResultTableRowComponent implements AfterViewInit { export class ResultTableRowComponent implements AfterViewInit {
@Input() result: SongResult @Input() song: ChartData[]
@ViewChild('checkbox', { static: true }) checkbox: ElementRef @ViewChild('checkbox', { static: true }) checkbox: ElementRef
constructor(private selectionService: SelectionService) { } constructor(private selectionService: SelectionService) { }
get songID() { get songID() {
return this.result.id return this.song[0].songId ?? this.song[0].chartId
} }
ngAfterViewInit() { ngAfterViewInit() {

View File

@@ -19,12 +19,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr @for (song of songs; track song) {
app-result-table-row <tr app-result-table-row (click)="onRowClicked(song)" [class.active]="activeSong === song" [song]="song"></tr>
#tableRow }
*ngFor="let result of results"
(click)="onRowClicked(result)"
[class.active]="activeRowID === result.id"
[result]="result"></tr>
</tbody> </tbody>
</table> </table>

View File

@@ -2,8 +2,8 @@ import { Component, EventEmitter, OnInit, Output, QueryList, ViewChild, ViewChil
import Comparators from 'comparators' import Comparators from 'comparators'
import { SettingsService } from 'src-angular/app/core/services/settings.service' 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 { CheckboxDirective } from '../../../core/directives/checkbox.directive'
import { SearchService } from '../../../core/services/search.service' import { SearchService } from '../../../core/services/search.service'
import { SelectionService } from '../../../core/services/selection.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 { export class ResultTableComponent implements OnInit {
@Output() rowClicked = new EventEmitter<SongResult>() @Output() rowClicked = new EventEmitter<ChartData[]>()
@ViewChild(CheckboxDirective, { static: true }) checkboxColumn: CheckboxDirective @ViewChild(CheckboxDirective, { static: true }) checkboxColumn: CheckboxDirective
@ViewChildren('tableRow') tableRows: QueryList<ResultTableRowComponent> @ViewChildren('tableRow') tableRows: QueryList<ResultTableRowComponent>
results: SongResult[] = [] activeSong: ChartData[] | null = null
activeRowID: number | null = null
sortDirection: 'ascending' | 'descending' = 'descending' sortDirection: 'ascending' | 'descending' = 'descending'
sortColumn: 'name' | 'artist' | 'album' | 'genre' | null = null sortColumn: 'name' | 'artist' | 'album' | 'genre' | null = null
constructor( constructor(
private searchService: SearchService, public searchService: SearchService,
private selectionService: SelectionService, private selectionService: SelectionService,
public settingsService: SettingsService public settingsService: SettingsService
) { } ) { }
@@ -37,24 +36,25 @@ export class ResultTableComponent implements OnInit {
this.checkboxColumn.check(selected) this.checkboxColumn.check(selected)
}) })
this.searchService.onSearchChanged(results => { this.searchService.searchUpdated.subscribe(() => {
this.activeRowID = null this.activeSong = null
this.results = results
this.updateSort() this.updateSort()
}) })
this.searchService.onNewSearch(() => {
this.sortColumn = null
})
} }
onRowClicked(result: SongResult) { get songs() {
this.activeRowID = result.id return this.searchService.groupedSongs
this.rowClicked.emit(result) }
onRowClicked(song: ChartData[]) {
if (this.activeSong !== song) {
this.activeSong = song
this.rowClicked.emit(song)
}
} }
onColClicked(column: 'name' | 'artist' | 'album' | 'genre') { onColClicked(column: 'name' | 'artist' | 'album' | 'genre') {
if (this.results.length === 0) { return } if (this.songs.length === 0) { return }
if (this.sortColumn !== column) { if (this.sortColumn !== column) {
this.sortColumn = column this.sortColumn = column
this.sortDirection = 'descending' this.sortDirection = 'descending'
@@ -68,7 +68,7 @@ export class ResultTableComponent implements OnInit {
private updateSort() { private updateSort() {
if (this.sortColumn !== null) { 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' }))
} }
} }

View File

@@ -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="item">
<div class="ui icon input" [class.loading]="isLoading()"> <div class="ui icon input" [class.loading]="isLoading()">
<input #searchBox type="text" placeholder=" Search..." (keyup.enter)="onSearch(searchBox.value)" /> <input #searchBox type="text" placeholder=" Search..." (keyup.enter)="onSearch(searchBox.value)" />
@@ -237,4 +237,334 @@
</div> </div>
</div> </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> </div>

View File

@@ -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;
}

View File

@@ -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 { SearchService } from 'src-angular/app/core/services/search.service'
import { difficulties, difficultyDisplay, instrumentDisplay, instruments } from 'src-shared/UtilFunctions'
import { getDefaultSearch } from '../../../../../src-shared/interfaces/search.interface'
@Component({ @Component({
selector: 'app-search-bar', selector: 'app-search-bar',
templateUrl: './search-bar.component.html', 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('hasSoloSections') hasSoloSections: ElementRef<HTMLInputElement>
@ViewChild('quantityDropdown', { static: true }) quantityDropdown: ElementRef @ViewChild('hasForcedNotes') hasForcedNotes: ElementRef<HTMLInputElement>
@ViewChild('similarityDropdown', { static: true }) similarityDropdown: ElementRef @ViewChild('hasOpenNotes') hasOpenNotes: ElementRef<HTMLInputElement>
@ViewChild('diffSlider', { static: true }) diffSlider: ElementRef @ViewChild('hasTapNotes') hasTapNotes: ElementRef<HTMLInputElement>
@ViewChild('hasRollLanes') hasRollLanes: ElementRef<HTMLInputElement>
@ViewChild('has2xKick') has2xKick: ElementRef<HTMLInputElement>
isError = false public showAdvanced = false
showAdvanced = false public instruments = instruments
searchSettings = getDefaultSearch() public difficulties = difficulties
private sliderInitialized = false 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() { ngAfterViewInit() {
// TODO this.updateDisabledControls()
// $(this.searchIcon.nativeElement).popup({ this.searchService.instrument.valueChanges.subscribe(() => {
// onShow: () => this.isError, // Only show the popup if there is an error this.updateDisabledControls()
// })
this.searchService.onSearchErrorStateUpdate(isError => {
this.isError = isError
}) })
// $(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) { setShowAdvanced(showAdvanced: boolean) {
this.searchSettings.query = query this.showAdvanced = showAdvanced
this.searchSettings.limit = 50 + 1 if (showAdvanced) {
this.searchSettings.offset = 0 this.startValidation = false
this.searchService.newSearch(this.searchSettings) this.searchControl.disable()
} } else {
this.searchControl.enable()
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
} }
} }
isLoading() { get searchControl() {
return this.searchService.isLoading() 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
}

View File

@@ -1,5 +1,7 @@
<div id="bottomMenu" class="ui bottom borderless menu"> <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"> <div class="item">
<button *ngIf="selectedResults.length > 1" (click)="downloadSelected()" class="ui positive button"> <button *ngIf="selectedResults.length > 1" (click)="downloadSelected()" class="ui positive button">
Download {{ selectedResults.length }} Results Download {{ selectedResults.length }} Results

View File

@@ -1,6 +1,5 @@
import { ChangeDetectorRef, Component } from '@angular/core' import { ChangeDetectorRef, Component } from '@angular/core'
import { VersionResult } from '../../../../../src-shared/interfaces/songDetails.interface'
import { groupBy } from '../../../../../src-shared/UtilFunctions' import { groupBy } from '../../../../../src-shared/UtilFunctions'
import { DownloadService } from '../../../core/services/download.service' import { DownloadService } from '../../../core/services/download.service'
import { SearchService } from '../../../core/services/search.service' import { SearchService } from '../../../core/services/search.service'
@@ -13,17 +12,19 @@ import { SelectionService } from '../../../core/services/selection.service'
}) })
export class StatusBarComponent { export class StatusBarComponent {
resultCount = 0
multipleCompleted = false multipleCompleted = false
downloading = false downloading = false
error = false error = false
percent = 0 percent = 0
batchResults: VersionResult[] // TODO
chartGroups: VersionResult[][] // eslint-disable-next-line @typescript-eslint/no-explicit-any
batchResults: any[]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
chartGroups: any[][]
constructor( constructor(
private downloadService: DownloadService, private downloadService: DownloadService,
private searchService: SearchService, public searchService: SearchService,
private selectionService: SelectionService, private selectionService: SelectionService,
ref: ChangeDetectorRef ref: ChangeDetectorRef
) { ) {
@@ -36,14 +37,10 @@ export class StatusBarComponent {
ref.detectChanges() ref.detectChanges()
}, 0) }, 0)
}) })
searchService.onSearchChanged(() => {
this.resultCount = searchService.resultCount
})
} }
get allResultsVisible() { get allResultsVisible() {
return this.searchService.allResultsVisible return false // this.searchService.allResultsVisible
} }
get selectedResults() { get selectedResults() {
@@ -57,7 +54,8 @@ export class StatusBarComponent {
async downloadSelected() { async downloadSelected() {
this.chartGroups = [] 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') const versionGroups = groupBy(this.batchResults, 'songID')
for (const versionGroup of versionGroups) { for (const versionGroup of versionGroups) {
if (versionGroup.findIndex(version => version.chartID !== versionGroup[0].chartID) !== -1) { if (versionGroup.findIndex(version => version.chartID !== versionGroup[0].chartID) !== -1) {
@@ -68,7 +66,7 @@ export class StatusBarComponent {
if (this.chartGroups.length === 0) { if (this.chartGroups.length === 0) {
for (const versions of versionGroups) { for (const versions of versionGroups) {
this.searchService.sortChart(versions) // this.searchService.sortChart(versions)
const downloadVersion = versions[0] const downloadVersion = versions[0]
const downloadSong = this.selectedResults.find(song => song.id === downloadVersion.songID)! const downloadSong = this.selectedResults.find(song => song.id === downloadVersion.songID)!
this.downloadService.addDownload( this.downloadService.addDownload(
@@ -89,7 +87,7 @@ export class StatusBarComponent {
downloadAllCharts() { downloadAllCharts() {
const songChartGroups = groupBy(this.batchResults, 'songID', 'chartID') const songChartGroups = groupBy(this.batchResults, 'songID', 'chartID')
for (const chart of songChartGroups) { for (const chart of songChartGroups) {
this.searchService.sortChart(chart) // this.searchService.sortChart(chart)
const downloadVersion = chart[0] const downloadVersion = chart[0]
const downloadSong = this.selectedResults.find(song => song.id === downloadVersion.songID)! const downloadSong = this.selectedResults.find(song => song.id === downloadVersion.songID)!
this.downloadService.addDownload( this.downloadService.addDownload(

View File

@@ -1,116 +1,183 @@
import { HttpClient } from '@angular/common/http'
import { EventEmitter, Injectable } from '@angular/core' import { EventEmitter, Injectable } from '@angular/core'
import { FormControl } from '@angular/forms'
import { SongResult, SongSearch } from '../../../../src-shared/interfaces/search.interface' import { chain, xorBy } from 'lodash'
import { VersionResult } from '../../../../src-shared/interfaces/songDetails.interface' 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({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class SearchService { export class SearchService {
private resultsChangedEmitter = new EventEmitter<SongResult[]>() // For when any results change public searchLoading = false
private newResultsEmitter = new EventEmitter<SongResult[]>() // For when a new search happens public songsResponse: Partial<SearchResult>
private errorStateEmitter = new EventEmitter<boolean>() // To indicate the search's error state public currentPage = 1
private results: SongResult[] = [] public searchUpdated = new EventEmitter<Partial<SearchResult>>()
private awaitingResults = false public isDefaultSearch = true
private currentQuery: SongSearch
private _allResultsVisible = true
async newSearch(query: SongSearch) { public groupedSongs: ChartData[][]
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
this.newResultsEmitter.emit(this.results) public availableIcons: string[]
this.resultsChangedEmitter.emit(this.results)
}
isLoading() { public searchControl = new FormControl('', { nonNullable: true })
return this.awaitingResults public isSng: FormControl<boolean>
} public instrument: FormControl<Instrument | null>
public difficulty: FormControl<Difficulty | null>
/** constructor(
* Event emitted when new search results are returned private http: HttpClient,
* or when more results are added to an existing search. ) {
* (emitted after `onNewSearch`) this.isSng = new FormControl<boolean>((localStorage.getItem('isSng') ?? 'true') === 'true', { nonNullable: true })
*/ this.isSng.valueChanges.subscribe(isSng => localStorage.setItem('isSng', `${isSng}`))
onSearchChanged(callback: (results: SongResult[]) => void) {
this.resultsChangedEmitter.subscribe(callback)
}
/** this.instrument = new FormControl<Instrument>(
* Event emitted when a new search query is typed in. (localStorage.getItem('instrument') === 'null' ? null : localStorage.getItem('instrument')) as Instrument
* (emitted before `onSearchChanged`) )
*/ this.instrument.valueChanges.subscribe(instrument => {
onNewSearch(callback: (results: SongResult[]) => void) { localStorage.setItem('instrument', `${instrument}`)
this.newResultsEmitter.subscribe(callback) if (this.songsResponse.page) {
} this.search(this.searchControl.value || '*').subscribe()
/**
* 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.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
// }
// })
// }

View File

@@ -1,6 +1,6 @@
import { EventEmitter, Injectable } from '@angular/core' 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' import { SearchService } from './search.service'
// Note: this class prevents event cycles by only emitting events if the checkbox changes // 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 { export class SelectionService {
private searchResults: SongResult[] = [] private searchResults: Partial<SearchResult>
private selectAllChangedEmitter = new EventEmitter<boolean>() private selectAllChangedEmitter = new EventEmitter<boolean>()
private selectionChangedCallbacks: { [songID: number]: (selection: boolean) => void } = {} private selectionChangedCallbacks: { [songID: number]: (selection: boolean) => void } = {}
@@ -19,14 +19,14 @@ export class SelectionService {
private selections: { [songID: number]: boolean | undefined } = {} private selections: { [songID: number]: boolean | undefined } = {}
constructor(searchService: SearchService) { constructor(searchService: SearchService) {
searchService.onSearchChanged(results => { searchService.searchUpdated.subscribe(results => {
this.searchResults = results this.searchResults = results
if (this.allSelected) { if (this.allSelected) {
this.selectAll() // Select newly added rows if allSelected this.selectAll() // Select newly added rows if allSelected
} }
}) })
searchService.onNewSearch(results => { searchService.searchUpdated.subscribe(results => {
this.searchResults = results this.searchResults = results
this.selectionChangedCallbacks = {} this.selectionChangedCallbacks = {}
this.selections = {} this.selections = {}
@@ -35,7 +35,9 @@ export class SelectionService {
} }
getSelectedResults() { 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) { onSelectAllChanged(callback: (selected: boolean) => void) {
@@ -57,7 +59,8 @@ export class SelectionService {
this.selectAllChangedEmitter.emit(false) 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() { selectAll() {
@@ -66,7 +69,8 @@ export class SelectionService {
this.selectAllChangedEmitter.emit(true) 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) { deselectSong(songID: number) {

View File

@@ -1,6 +1,8 @@
import { DOCUMENT } from '@angular/common' import { DOCUMENT } from '@angular/common'
import { Inject, Injectable } from '@angular/core' import { Inject, Injectable } from '@angular/core'
import { Difficulty, Instrument } from 'scan-chart'
import { Settings, themes } from '../../../../src-shared/Settings' import { Settings, themes } from '../../../../src-shared/Settings'
@Injectable({ @Injectable({
@@ -31,6 +33,22 @@ export class SettingsService {
this.document.documentElement.setAttribute('data-theme', theme) 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 // Individual getters/setters
get libraryDirectory() { get libraryDirectory() {
return this.settings.libraryPath return this.settings.libraryPath

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -1,7 +1,4 @@
import { IpcInvokeHandlers, IpcToMainEmitHandlers } from '../src-shared/interfaces/ipc.interface' 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 { download } from './ipc/download/DownloadHandler'
import { getSettings, setSettings } from './ipc/SettingsHandler.ipc' import { getSettings, setSettings } from './ipc/SettingsHandler.ipc'
import { downloadUpdate, getCurrentVersion, getUpdateAvailable, quitAndInstall, retryUpdate } from './ipc/UpdateHandler.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 { export function getIpcInvokeHandlers(): IpcInvokeHandlers {
return { return {
getSettings, getSettings,
songSearch,
getSongDetails,
getBatchSongDetails,
getCurrentVersion, getCurrentVersion,
getUpdateAvailable, getUpdateAvailable,
isMaximized, isMaximized,

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -2,7 +2,6 @@ import { parse } from 'path'
import { rimraf } from 'rimraf' import { rimraf } from 'rimraf'
import { NewDownload, ProgressType } from '../../../src-shared/interfaces/download.interface' import { NewDownload, ProgressType } from '../../../src-shared/interfaces/download.interface'
import { DriveFile } from '../../../src-shared/interfaces/songDetails.interface'
import { sanitizeFilename } from '../../../src-shared/UtilFunctions' import { sanitizeFilename } from '../../../src-shared/UtilFunctions'
import { hasVideoExtension } from '../../ElectronUtilFunctions' import { hasVideoExtension } from '../../ElectronUtilFunctions'
import { emitIpcEvent } from '../../main' import { emitIpcEvent } from '../../main'
@@ -26,7 +25,9 @@ export class ChartDownload {
private cancelFn: undefined | (() => void) private cancelFn: undefined | (() => void)
private callbacks = {} as Callbacks 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 percent = 0 // Needs to be stored here because errors won't know the exact percent
private tempPath: string private tempPath: string
private wasCanceled = false private wasCanceled = false
@@ -60,7 +61,9 @@ export class ChartDownload {
this.callbacks[event] = callback 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 files.filter(file => {
return (file.name !== 'ch.dat') && (settings.downloadVideos || !hasVideoExtension(file.name)) return (file.name !== 'ch.dat') && (settings.downloadVideos || !hasVideoExtension(file.name))
}) })

View File

@@ -19,9 +19,6 @@ function getListenerAdder<K extends keyof IpcFromMainEmitEvents>(key: K) {
const electronApi: ContextBridgeApi = { const electronApi: ContextBridgeApi = {
invoke: { invoke: {
getSettings: getInvoker('getSettings'), getSettings: getInvoker('getSettings'),
songSearch: getInvoker('songSearch'),
getSongDetails: getInvoker('getSongDetails'),
getBatchSongDetails: getInvoker('getBatchSongDetails'),
getCurrentVersion: getInvoker('getCurrentVersion'), getCurrentVersion: getInvoker('getCurrentVersion'),
getUpdateAvailable: getInvoker('getUpdateAvailable'), getUpdateAvailable: getInvoker('getUpdateAvailable'),
isMaximized: getInvoker('isMaximized'), isMaximized: getInvoker('isMaximized'),

View File

@@ -1,3 +1,5 @@
import { Difficulty, Instrument } from 'scan-chart'
export const themes = [ export const themes = [
'business', 'business',
'dark', 'dark',
@@ -21,6 +23,8 @@ export interface Settings {
downloadVideos: boolean // If background videos should be downloaded downloadVideos: boolean // If background videos should be downloaded
theme: typeof themes[number] // The name of the currently enabled UI theme theme: typeof themes[number] // The name of the currently enabled UI theme
libraryPath: string | undefined // The path to the user's library 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, downloadVideos: true,
theme: 'dark', theme: 'dark',
libraryPath: undefined, libraryPath: undefined,
instrument: 'guitar',
difficulty: null,
} }

View File

@@ -1,10 +1,26 @@
import sanitize from 'sanitize-filename' 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. // WARNING: do not import anything related to Electron; the code will not compile correctly.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyFunction = (...args: any) => 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. * @returns `filename` with all invalid filename characters replaced.
*/ */
@@ -58,3 +74,48 @@ export function groupBy<T>(objectList: T[], ...keys: (keyof T)[]) {
return results 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'
}
}

View File

@@ -1,4 +1,3 @@
import { DriveChart } from './songDetails.interface'
/** /**
* Represents a user's request to interact with the download system. * Represents a user's request to interact with the download system.
@@ -16,7 +15,10 @@ export interface NewDownload {
chartName: string chartName: string
artist: string artist: string
charter: string charter: string
driveData: DriveChart & { inChartPack: boolean } // TODO
// eslint-disable-next-line @typescript-eslint/no-explicit-any
driveData: any
// driveData: DriveChart & { inChartPack: boolean }
} }
/** /**

View File

@@ -3,8 +3,6 @@ import { UpdateInfo } from 'electron-updater'
import { Settings } from '../Settings' import { Settings } from '../Settings'
import { Download, DownloadProgress } from './download.interface' import { Download, DownloadProgress } from './download.interface'
import { SongResult, SongSearch } from './search.interface'
import { VersionResult } from './songDetails.interface'
import { UpdateProgress } from './update.interface' import { UpdateProgress } from './update.interface'
export interface ContextBridgeApi { export interface ContextBridgeApi {
@@ -27,18 +25,6 @@ export interface IpcInvokeEvents {
input: void input: void
output: Settings output: Settings
} }
songSearch: {
input: SongSearch
output: SongResult[]
}
getSongDetails: {
input: SongResult['id']
output: VersionResult[]
}
getBatchSongDetails: {
input: number[]
output: VersionResult[]
}
getCurrentVersion: { getCurrentVersion: {
input: void input: void
output: string output: string

View File

@@ -1,96 +1,252 @@
/** import { EventType, FolderIssueType, MetadataIssueType, NotesData } from 'scan-chart'
* Represents a user's song search query. import { z } from 'zod'
*/
export interface SongSearch { // TODO: make limit a setting that's not always 51 import { difficulties, instruments, Overwrite } from '../UtilFunctions'
query: string
quantity: 'all' | 'any' export const GeneralSearchSchema = z.object({
similarity: 'similar' | 'exact' search: z.string(),
fields: SearchFields page: z.number().positive(),
tags: SearchTags instrument: z.enum(instruments).nullable(),
instruments: SearchInstruments difficulty: z.enum(difficulties).nullable(),
difficulties: SearchDifficulties })
minDiff: number export type GeneralSearch = z.infer<typeof GeneralSearchSchema>
maxDiff: number
limit: number const md5Validator = z.string().regex(/^[a-f0-9]{32}$/, 'Invalid MD5 hash')
offset: number
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 { export type ChartData = SearchResult['data'][number]
return { export interface SearchResult {
query: '', found: number
quantity: 'all', out_of: number
similarity: 'similar', page: number
fields: { name: true, artist: true, album: true, genre: true, year: true, charter: true, tag: true }, search_time_ms: number
tags: { data: {
// eslint-disable-next-line @typescript-eslint/naming-convention /** The song name. */
'sections': false, 'star power': false, 'forcing': false, 'taps': false, 'lyrics': false, name: string | null
// eslint-disable-next-line @typescript-eslint/naming-convention /** The song artist. */
'video': false, 'stems': false, 'solo sections': false, 'open notes': false, artist: string | null
}, /** The song album. */
instruments: { album: string | null
guitar: false, bass: false, rhythm: false, keys: false, /** The song genre. */
drums: false, guitarghl: false, bassghl: false, vocals: false, genre: string | null
}, /** The song year. */
difficulties: { expert: false, hard: false, medium: false, easy: false }, year: string | null
minDiff: 0, /** The name of the chart, or `null` if the same as `name`. */
maxDiff: 6, chartName: string | null
limit: 50 + 1, /** The genre of the chart, or `null` if the same as `genre`. */
offset: 0, 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 { /** The Drive ID of the chart's application folder. */
name: boolean applicationDriveId: string
artist: boolean /** The primary username of the chart's application's applicant, or `null` if packName is not `null` */
album: boolean applicationUsername: string
genre: boolean /** The name of the pack source, or `null` if the application is not a pack source. */
year: boolean packName: string | null
charter: boolean /** The `folderId` of the Google Drive folder that contains the chart (or the shortcut to it). */
tag: boolean parentFolderId: string
} /**
* A string containing the relative path from the application folder to the `DriveChart`.
export interface SearchTags { *
'sections': boolean // Tag inverted * Doesn't contain the application folder name, the file name (for file charts), or leading/trailing slashes.
// eslint-disable-next-line @typescript-eslint/naming-convention */
'star power': boolean // Tag inverted drivePath: string
'forcing': boolean // Tag inverted /** The Drive ID of the chart file, or `null` if the chart is a chart folder. */
'taps': boolean driveFileId: string | null
'lyrics': boolean /** The file name of the chart file, or `null` if the chart is a chart folder. */
'video': boolean driveFileName: string | null
'stems': boolean /** If there is more than one chart contained inside this `DriveChart`. */
// eslint-disable-next-line @typescript-eslint/naming-convention driveChartIsPack: boolean
'solo sections': boolean /**
// eslint-disable-next-line @typescript-eslint/naming-convention * A string containing the relative path from the `DriveChart`'s archive to the chart inside the archive.
'open notes': boolean *
} * Doesn't contain the archive name, the chart file name (for file charts), or leading/trailing slashes.
*
export interface SearchInstruments { * An empty string if the `DriveChart` is not an archive.
guitar: boolean */
bass: boolean archivePath: string
rhythm: boolean /**
keys: boolean * The name of the .sng file. `null` if the chart is not a .sng file.
drums: boolean */
guitarghl: boolean chartFileName: string | null
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
} }

View File

@@ -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`
}