Add "Tools" tab with chart issue scanner

This commit is contained in:
Geomitron
2024-12-22 18:35:43 -06:00
parent d2e40b7c24
commit a7113384e8
15 changed files with 866 additions and 13 deletions

View File

@@ -42,6 +42,7 @@
"electron-updater": "6.2.1",
"electron-window-state": "5.0.3",
"eventemitter3": "5.0.1",
"exceljs": "^4.4.0",
"fs-extra": "11.2.0",
"lodash": "4.17.21",
"parse-sng": "4.0.3",

231
pnpm-lock.yaml generated
View File

@@ -59,6 +59,9 @@ importers:
eventemitter3:
specifier: 5.0.1
version: 5.0.1
exceljs:
specifier: ^4.4.0
version: 4.4.0
fs-extra:
specifier: 11.2.0
version: 11.2.0
@@ -810,6 +813,12 @@ packages:
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@fast-csv/format@4.3.5':
resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==}
'@fast-csv/parse@4.3.6':
resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==}
'@humanwhocodes/config-array@0.11.14':
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
engines: {node: '>=10.10.0'}
@@ -1238,6 +1247,9 @@ packages:
'@types/ms@0.7.34':
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
'@types/node@14.18.63':
resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==}
'@types/node@18.16.0':
resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==}
@@ -1576,6 +1588,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
big-integer@1.6.52:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
@@ -1584,12 +1600,18 @@ packages:
resolution: {integrity: sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==}
engines: {node: '>=12'}
binary@0.3.0:
resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
bluebird-lst@1.0.9:
resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==}
bluebird@3.4.7:
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
bluebird@3.7.2:
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
@@ -1630,9 +1652,17 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buffer-indexof-polyfill@1.0.2:
resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==}
engines: {node: '>=0.10'}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
buffers@0.1.1:
resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==}
engines: {node: '>=0.2.0'}
builder-util-runtime@9.2.4:
resolution: {integrity: sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==}
engines: {node: '>=12.0.0'}
@@ -1670,6 +1700,9 @@ packages:
caniuse-lite@1.0.30001640:
resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==}
chainsaw@0.1.0:
resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==}
chalk@1.1.3:
resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
engines: {node: '>=0.10.0'}
@@ -1984,6 +2017,9 @@ packages:
resolution: {tarball: https://codeload.github.com/corbanbrook/dsp.js/tar.gz/219600bb0346ee9a00686c9875c81123e2d8780e}
version: 1.0.0
duplexer2@0.1.4:
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
@@ -2225,6 +2261,10 @@ packages:
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
exceljs@4.4.0:
resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==}
engines: {node: '>=8.3.0'}
exifreader@4.23.3:
resolution: {integrity: sha512-/Ii4jiNp/5BXdKOiWXZYrWmZFn/ANu3bMVGO7GFQufao5M52/fK2OsAPMH34PL4S79z1eZBzAoaYyBXit0zzVA==}
@@ -2244,6 +2284,10 @@ packages:
resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==}
engines: {'0': node >=0.6.0}
fast-csv@4.3.6:
resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==}
engines: {node: '>=10.0.0'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -2364,6 +2408,11 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fstream@1.0.12:
resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==}
engines: {node: '>=0.6'}
deprecated: This package is no longer supported.
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -2546,6 +2595,9 @@ packages:
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
engines: {node: '>= 4'}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immutable@4.3.4:
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
@@ -2791,6 +2843,9 @@ packages:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -2805,6 +2860,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -2820,6 +2878,9 @@ packages:
resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
listenercount@1.0.1:
resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==}
lmdb@3.0.8:
resolution: {integrity: sha512-9rp8JT4jPhCRJUL7vRARa2N06OLSYzLwQsEkhC6Qu5XbcLyM/XBLMzDlgS/K7l7c5CdURLdDk9uE+hPFIogHTQ==}
hasBin: true
@@ -2843,18 +2904,36 @@ packages:
lodash.flatten@4.4.0:
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
lodash.groupby@4.6.0:
resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
lodash.isfunction@3.0.9:
resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
lodash.isnil@4.0.0:
resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isundefined@3.0.1:
resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.union@4.6.0:
resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==}
lodash.uniq@4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -3223,6 +3302,9 @@ packages:
engines: {node: ^16.14.0 || >=18.0.0}
hasBin: true
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -3489,6 +3571,11 @@ packages:
rfc4648@1.5.3:
resolution: {integrity: sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==}
rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
@@ -3540,6 +3627,10 @@ packages:
sax@1.3.0:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
saxes@5.0.1:
resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
engines: {node: '>=10'}
scan-chart@4.1.4:
resolution: {integrity: sha512-LzFMfPxLgT7X5TCYK5Xkags3XVu3V7JAoDg7Te0A5GkSivwKA4fnwMRpdtRLiThlnspIAO2L/+lrysz1pIagmg==}
@@ -3571,6 +3662,9 @@ packages:
resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==}
engines: {node: '>= 0.4'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -3828,6 +3922,9 @@ packages:
resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==}
hasBin: true
traverse@0.3.9:
resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
@@ -3927,6 +4024,9 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
unzipper@0.10.14:
resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==}
update-browserslist-db@1.1.0:
resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==}
hasBin: true
@@ -3942,6 +4042,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
@@ -4040,6 +4144,9 @@ packages:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'}
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -4735,6 +4842,25 @@ snapshots:
'@eslint/js@8.57.0': {}
'@fast-csv/format@4.3.5':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.isboolean: 3.0.3
lodash.isequal: 4.5.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
'@fast-csv/parse@4.3.6':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.groupby: 4.6.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
lodash.isundefined: 3.0.1
lodash.uniq: 4.5.0
'@humanwhocodes/config-array@0.11.14':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
@@ -5131,6 +5257,8 @@ snapshots:
'@types/ms@0.7.34': {}
'@types/node@14.18.63': {}
'@types/node@18.16.0': {}
'@types/node@20.14.10':
@@ -5553,10 +5681,17 @@ snapshots:
base64-js@1.5.1: {}
big-integer@1.6.52: {}
binary-extensions@2.2.0: {}
binary-parser@2.2.1: {}
binary@0.3.0:
dependencies:
buffers: 0.1.1
chainsaw: 0.1.0
bl@4.1.0:
dependencies:
buffer: 5.7.1
@@ -5567,6 +5702,8 @@ snapshots:
dependencies:
bluebird: 3.7.2
bluebird@3.4.7: {}
bluebird@3.7.2: {}
boolbase@1.0.0: {}
@@ -5604,11 +5741,15 @@ snapshots:
buffer-from@1.1.2: {}
buffer-indexof-polyfill@1.0.2: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
buffers@0.1.1: {}
builder-util-runtime@9.2.4:
dependencies:
debug: 4.3.5(supports-color@5.5.0)
@@ -5682,6 +5823,10 @@ snapshots:
caniuse-lite@1.0.30001640: {}
chainsaw@0.1.0:
dependencies:
traverse: 0.3.9
chalk@1.1.3:
dependencies:
ansi-styles: 2.2.1
@@ -6008,6 +6153,10 @@ snapshots:
dsp.js@https://codeload.github.com/corbanbrook/dsp.js/tar.gz/219600bb0346ee9a00686c9875c81123e2d8780e: {}
duplexer2@0.1.4:
dependencies:
readable-stream: 2.3.8
duplexer@0.1.2: {}
eastasianwidth@0.2.0: {}
@@ -6403,6 +6552,18 @@ snapshots:
eventemitter3@5.0.1: {}
exceljs@4.4.0:
dependencies:
archiver: 5.3.2
dayjs: 1.11.11
fast-csv: 4.3.6
jszip: 3.10.1
readable-stream: 3.6.2
saxes: 5.0.1
tmp: 0.2.1
unzipper: 0.10.14
uuid: 8.3.2
exifreader@4.23.3:
optionalDependencies:
'@xmldom/xmldom': 0.8.10
@@ -6428,6 +6589,11 @@ snapshots:
extsprintf@1.4.1:
optional: true
fast-csv@4.3.6:
dependencies:
'@fast-csv/format': 4.3.5
'@fast-csv/parse': 4.3.6
fast-deep-equal@3.1.3: {}
fast-diff@1.3.0: {}
@@ -6553,6 +6719,13 @@ snapshots:
fsevents@2.3.3:
optional: true
fstream@1.0.12:
dependencies:
graceful-fs: 4.2.11
inherits: 2.0.4
mkdirp: 0.5.6
rimraf: 2.7.1
function-bind@1.1.2: {}
function.prototype.name@1.1.6:
@@ -6773,6 +6946,8 @@ snapshots:
ignore@5.3.1: {}
immediate@3.0.6: {}
immutable@4.3.4: {}
import-fresh@3.3.0:
@@ -6995,6 +7170,13 @@ snapshots:
jsonparse@1.3.1: {}
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -7010,6 +7192,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lie@3.3.0:
dependencies:
immediate: 3.0.6
lilconfig@2.1.0: {}
lilconfig@3.0.0: {}
@@ -7018,6 +7204,8 @@ snapshots:
lines-and-columns@2.0.4: {}
listenercount@1.0.1: {}
lmdb@3.0.8:
dependencies:
msgpackr: 1.10.2
@@ -7047,14 +7235,26 @@ snapshots:
lodash.flatten@4.4.0: {}
lodash.groupby@4.6.0: {}
lodash.isboolean@3.0.3: {}
lodash.isequal@4.5.0: {}
lodash.isfunction@3.0.9: {}
lodash.isnil@4.0.0: {}
lodash.isplainobject@4.0.6: {}
lodash.isundefined@3.0.1: {}
lodash.merge@4.6.2: {}
lodash.union@4.6.0: {}
lodash.uniq@4.5.0: {}
lodash@4.17.21: {}
log-symbols@4.1.0:
@@ -7526,6 +7726,8 @@ snapshots:
- bluebird
- supports-color
pako@1.0.11: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -7771,6 +7973,10 @@ snapshots:
rfc4648@1.5.3: {}
rimraf@2.7.1:
dependencies:
glob: 7.2.3
rimraf@3.0.2:
dependencies:
glob: 7.2.3
@@ -7848,6 +8054,10 @@ snapshots:
sax@1.3.0: {}
saxes@5.0.1:
dependencies:
xmlchars: 2.2.0
scan-chart@4.1.4:
dependencies:
'@noble/hashes': 1.4.0
@@ -7892,6 +8102,8 @@ snapshots:
functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
setimmediate@1.0.5: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -8193,6 +8405,8 @@ snapshots:
dependencies:
nopt: 1.0.10
traverse@0.3.9: {}
tree-kill@1.2.2: {}
truncate-utf8-bytes@1.0.2:
@@ -8295,6 +8509,19 @@ snapshots:
universalify@2.0.1: {}
unzipper@0.10.14:
dependencies:
big-integer: 1.6.52
binary: 0.3.0
bluebird: 3.4.7
buffer-indexof-polyfill: 1.0.2
duplexer2: 0.1.4
fstream: 1.0.12
graceful-fs: 4.2.11
listenercount: 1.0.1
readable-stream: 2.3.8
setimmediate: 1.0.5
update-browserslist-db@1.1.0(browserslist@4.23.2):
dependencies:
browserslist: 4.23.2
@@ -8309,6 +8536,8 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@8.3.2: {}
validate-npm-package-license@3.0.4:
dependencies:
spdx-correct: 3.2.0
@@ -8411,6 +8640,8 @@ snapshots:
xmlbuilder@15.1.1: {}
xmlchars@2.2.0: {}
y18n@5.0.8: {}
yallist@3.1.1: {}

View File

@@ -3,11 +3,13 @@ import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'
import { BrowseComponent } from './components/browse/browse.component'
import { SettingsComponent } from './components/settings/settings.component'
import { ToolsComponent } from './components/tools/tools.component'
import { TabPersistStrategy } from './core/tab-persist.strategy'
const routes: Routes = [
{ path: 'browse', component: BrowseComponent, data: { shouldReuse: true } },
{ path: 'library', redirectTo: '/browse' },
{ path: 'tools', component: ToolsComponent, data: { shouldReuse: true } },
{ path: 'settings', component: SettingsComponent, data: { shouldReuse: true } },
{ path: 'about', redirectTo: '/browse' },
{ path: '**', redirectTo: '/browse' },

View File

@@ -1,6 +1,7 @@
<div class="navbar p-0 min-h-0 bg-base-100" style="-webkit-app-region: drag">
<div style="-webkit-app-region: no-drag">
<button class="btn btn-ghost rounded-none" routerLinkActive="btn-active" routerLink="/browse">Browse</button>
<button class="btn btn-ghost rounded-none" routerLinkActive="btn-active" routerLink="/tools">Tools</button>
<button class="btn btn-ghost rounded-none flex flex-nowrap" routerLinkActive="btn-active" routerLink="/settings">
<i *ngIf="updateAvailable === 'error'" class="bi bi-exclamation-triangle-fill text-warning"></i>
Settings

View File

@@ -0,0 +1,62 @@
<div class="p-8 flex flex-col gap-3">
<div class="text-xl">Chart issue scanning</div>
<div class="flex gap-3">
<label class="form-control w-full">
<div class="label">
<span class="label-text">Issue scan directory</span>
</div>
<div class="join w-full">
<input
[value]="settingsService.issueScanDirectory || 'No folder selected'"
class="join-item input input-bordered cursor-default pointer-events-none flex-1"
readonly
type="text"
placeholder="No directory selected!" />
@if (settingsService.issueScanDirectory !== undefined) {
<button (click)="openIssueScanDirectory()" class="join-item btn btn-neutral">Open Folder</button>
}
<button (click)="getIssueScanDirectory()" class="join-item btn btn-primary">Choose</button>
</div>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Spreadsheet output directory</span>
</div>
<div class="join w-full">
<input
[value]="settingsService.spreadsheetOutputDirectory || 'No folder selected'"
class="join-item input input-bordered cursor-default pointer-events-none flex-1"
readonly
type="text"
placeholder="No directory selected!" />
@if (settingsService.spreadsheetOutputDirectory !== undefined) {
<button (click)="openSpreadsheetOutputDirectory()" class="join-item btn btn-neutral">Open Folder</button>
}
<button (click)="getSpreadsheetOutputDirectory()" class="join-item btn btn-primary">Choose</button>
</div>
</label>
<button
(click)="scanIssues()"
class="btn btn-primary self-end"
[attr.disabled]="
settingsService.issueScanDirectory === undefined || settingsService.spreadsheetOutputDirectory === undefined || scanning ? true : null
">
<i class="bi bi-gear-wide-connected text-lg"></i>
{{ buttonText }}
</button>
<dialog #scanErrorModal class="modal">
<div class="modal-box bg-base-100 text-base-content flex flex-col gap-2">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
<i class="bi bi-x-lg text-lg"></i>
</button>
</form>
<h3 class="text-lg font-bold">Error scanning charts for issues:</h3>
<p class="py-4">{{ scanErrorText }}</p>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</div>
</div>

View File

@@ -0,0 +1,77 @@
import { Component, ElementRef, NgZone, ViewChild } from '@angular/core'
import { SettingsService } from 'src-angular/app/core/services/settings.service'
@Component({
selector: 'app-tools',
templateUrl: './tools.component.html',
})
export class ToolsComponent {
@ViewChild('themeDropdown', { static: true }) themeDropdown: ElementRef
@ViewChild('scanErrorModal') scanErrorModal: ElementRef<HTMLDialogElement>
public scanning = false
public buttonText = 'Scan for issues'
public scanErrorText = ''
constructor(
zone: NgZone,
public settingsService: SettingsService,
) {
window.electron.on.updateIssueScan(({ status, message }) => zone.run(() => {
if (status === 'progress') {
this.buttonText = message
} else if (status === 'error') {
this.scanning = false
this.scanErrorText = message
this.scanErrorModal.nativeElement.showModal()
} else if (status === 'done') {
this.scanning = false
this.buttonText = 'Complete! (click to scan again)'
}
}))
}
openIssueScanDirectory() {
if (this.settingsService.issueScanDirectory) {
window.electron.emit.showFolder(this.settingsService.issueScanDirectory)
}
}
async getIssueScanDirectory() {
const result = await window.electron.invoke.showOpenDialog({
title: 'Choose issue scan folder',
defaultPath: this.settingsService.issueScanDirectory || '',
properties: ['openDirectory'],
})
if (result.canceled === false) {
this.settingsService.issueScanDirectory = result.filePaths[0]
}
}
openSpreadsheetOutputDirectory() {
if (this.settingsService.spreadsheetOutputDirectory) {
window.electron.emit.showFolder(this.settingsService.spreadsheetOutputDirectory)
}
}
async getSpreadsheetOutputDirectory() {
const result = await window.electron.invoke.showOpenDialog({
title: 'Choose spreadsheet output folder',
defaultPath: this.settingsService.spreadsheetOutputDirectory || '',
properties: ['openDirectory'],
})
if (result.canceled === false) {
this.settingsService.spreadsheetOutputDirectory = result.filePaths[0]
}
}
async scanIssues() {
if (this.settingsService.issueScanDirectory && this.settingsService.spreadsheetOutputDirectory) {
this.scanning = true
window.electron.emit.scanIssues()
}
}
}

View File

@@ -61,6 +61,20 @@ export class SettingsService {
this.settings.libraryPath = value
this.saveSettings()
}
get issueScanDirectory() {
return this.settings.issueScanPath
}
set issueScanDirectory(value: string | undefined) {
this.settings.issueScanPath = value
this.saveSettings()
}
get spreadsheetOutputDirectory() {
return this.settings.spreadsheetOutputPath
}
set spreadsheetOutputDirectory(value: string | undefined) {
this.settings.spreadsheetOutputPath = value
this.saveSettings()
}
get chartFolderName() {
return this.settings.chartFolderName
}

View File

@@ -1,5 +1,6 @@
import { IpcInvokeHandlers, IpcToMainEmitHandlers } from '../src-shared/interfaces/ipc.interface.js'
import { download } from './ipc/DownloadHandler.ipc.js'
import { scanIssues } from './ipc/issue-scan/IssueScanHandler.ipc.js'
import { getSettings, setSettings } from './ipc/SettingsHandler.ipc.js'
import { downloadUpdate, getCurrentVersion, getUpdateAvailable, quitAndInstall, retryUpdate } from './ipc/UpdateHandler.ipc.js'
import { getPlatform, getThemeColors, isMaximized, maximize, minimize, openUrl, quit, restore, showFile, showFolder, showOpenDialog, toggleDevTools } from './ipc/UtilHandlers.ipc.js'
@@ -31,5 +32,6 @@ export function getIpcToMainEmitHandlers(): IpcToMainEmitHandlers {
quit,
showFile,
showFolder,
scanIssues,
}
}

View File

@@ -7,6 +7,7 @@ import { dataPath, settingsPath, tempPath, themesPath } from '../../src-shared/P
import { defaultSettings, Settings } from '../../src-shared/Settings.js'
import { mainWindow } from '../main.js'
console.log(settingsPath)
export let settings = readSettings()
function readSettings() {

View File

@@ -0,0 +1,240 @@
import exceljs, { Borders } from 'exceljs'
import _ from 'lodash'
import { FolderIssueType, ScannedChart } from 'scan-chart'
export function getChartIssues(charts: { chart: ScannedChart; path: string }[]) {
const chartIssues: {
path: string
artist: string
name: string
charter: string
errorName: string
errorDescription: string
fixMandatory: boolean
}[] = []
for (const chart of charts) {
const addIssue = (
errorName: string,
errorDescription: string,
fixMandatory: boolean,
) => {
chartIssues.push({
path: chart.path,
artist: removeStyleTags(chart.chart.artist ?? ''),
name: removeStyleTags(chart.chart.name ?? ''),
charter: removeStyleTags(chart.chart.charter ?? ''),
errorName,
errorDescription,
fixMandatory,
})
}
if (chart.chart.folderIssues.length > 0) {
for (const folderIssue of chart.chart.folderIssues) {
if (folderIssue.folderIssue === 'albumArtSize') {
continue
} // Ignored; .sng conversion fixes this
addIssue(
folderIssue.folderIssue,
folderIssue.description,
(
[
'noMetadata',
'invalidMetadata',
'noAudio',
'badAudio',
'noChart',
'invalidChart',
'badChart',
] satisfies FolderIssueType[] as FolderIssueType[]
).includes(folderIssue.folderIssue),
)
}
}
for (const metadataIssue of chart.chart.metadataIssues) {
addIssue(
metadataIssue.metadataIssue,
metadataIssue.description,
['"name"', '"artist"', '"charter"'].some(property => metadataIssue.description.includes(property)),
)
}
if (chart.chart.notesData) {
for (const issue of chart.chart.notesData.chartIssues) {
addIssue(
issue.noteIssue,
`${issue.instrument ? `[${issue.instrument}]` : ''}${issue.difficulty ? `[${issue.difficulty}]` : ''} ${issue.description}`,
issue.noteIssue === 'noNotes',
)
}
}
}
return chartIssues
}
export async function getIssuesXLSX(
chartIssues: Awaited<ReturnType<typeof getChartIssues>>,
) {
const chartIssueHeaders = [
{ text: 'Artist', width: 160 / 7 },
{ text: 'Name', width: 400 / 7 },
{ text: 'Charter', width: 160 / 7 },
{ text: 'Issue Name', width: 160 / 7 },
{
text: 'Issue Description (a more detailed description of issue types can be '
+ 'found at https://drive.google.com/open?id=1UK7GsP4ZHJkOg8uREFRMY72svySaDlf0QRTGlk-ruYQ)',
width: 650 / 7,
},
{ text: 'Fix Mandatory?', width: 120 / 7 },
{ text: 'Path', width: 600 / 7 },
]
const chartIssueRows: (string | { text: string; hyperlink: string })[][] = []
for (const issue of chartIssues) {
chartIssueRows.push([
issue.artist,
issue.name,
issue.charter,
issue.errorName,
issue.errorDescription,
issue.fixMandatory ? 'yes' : 'no',
issue.path,
])
}
const gridlineBorderStyle = {
top: { style: 'thin', color: { argb: 'FFD0D0D0' } },
left: { style: 'thin', color: { argb: 'FFD0D0D0' } },
bottom: { style: 'thin', color: { argb: 'FFD0D0D0' } },
right: { style: 'thin', color: { argb: 'FFD0D0D0' } },
} satisfies Partial<Borders>
const workbook = new exceljs.Workbook()
workbook.creator = 'Chorus'
workbook.created = new Date()
workbook.modified = new Date()
const chartIssuesWorksheet = workbook.addWorksheet('Chart Issues', {
views: [{ state: 'frozen', ySplit: 1 }], // Sticky header row
})
chartIssuesWorksheet.autoFilter = {
from: { row: 1, column: 1 },
to: { row: chartIssueRows.length + 1, column: chartIssueHeaders.length },
}
chartIssueHeaders.forEach((header, index) => {
const cell = chartIssuesWorksheet.getCell(1, index + 1)
cell.value = header.text
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFD3D3D3' },
}
cell.font = { bold: true }
const column = chartIssuesWorksheet.getColumn(index + 1)
column.width = header.width
column.border = gridlineBorderStyle
})
chartIssuesWorksheet.addRows(chartIssueRows)
chartIssuesWorksheet.addConditionalFormatting({
ref: `A2:${columnNumberToLetter(chartIssueHeaders.length)}${chartIssueRows.length + 1
}`,
rules: [
{
type: 'expression',
priority: 99999,
formulae: ['MOD(ROW(),2)=0'],
style: {
fill: {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFF7F7F7' },
},
},
},
],
})
return await workbook.xlsx.writeBuffer({ useStyles: true })
}
export function columnNumberToLetter(column: number) {
let temp,
letter = ''
while (column > 0) {
temp = (column - 1) % 26
letter = String.fromCharCode(temp + 65) + letter
column = (column - temp - 1) / 26
}
return letter
}
/**
* @returns an string representation of `ms` that looks like HH:MM:SS.mm
*/
export function msToExactTime(ms: number) {
const seconds = _.round((ms / 1000) % 60, 2)
const minutes = Math.floor((ms / 1000 / 60) % 60)
const hours = Math.floor((ms / 1000 / 60 / 60) % 24)
return `${hours ? `${hours}:` : ''}${_.padStart(
minutes + '',
2,
'0',
)}:${_.padStart(seconds.toFixed(2), 5, '0')}`
}
const allowedTags = [
'align',
'allcaps',
'alpha',
'b',
'br',
'color',
'cspace',
'font',
'font-weight',
'gradient',
'i',
'indent',
'line-height',
'line-indent',
'link',
'lowercase',
'margin',
'mark',
'mspace',
'nobr',
'noparse',
'page',
'pos',
'rotate',
's',
'size',
'smallcaps',
'space',
'sprite',
'strikethrough',
'style',
'sub',
'sup',
'u',
'uppercase',
'voffset',
'width',
]
const tagPattern = allowedTags.map(tag => `\\b${tag}\\b`).join('|')
/**
* @returns `text` with all style tags removed. (e.g. "<color=#AEFFFF>Aren Eternal</color> & Geo" -> "Aren Eternal & Geo")
*/
export function removeStyleTags(text: string) {
let oldText = text
let newText = text
do {
oldText = newText
newText = newText
.replace(new RegExp(`<\\s*\\/?\\s*(?:#|${tagPattern})[^>]*>`, 'gi'), '')
.trim()
} while (newText !== oldText)
return newText
}

View File

@@ -0,0 +1,183 @@
import Bottleneck from 'bottleneck'
import dayjs from 'dayjs'
import { shell } from 'electron'
import { createReadStream } from 'fs'
import pkg from 'fs-extra'
import _ from 'lodash'
import { SngHeader, SngStream } from 'parse-sng'
import { scanChartFolder, ScannedChart } from 'scan-chart'
import { Readable } from 'stream'
import { inspect } from 'util'
import { appearsToBeChartFolder, getExtension, hasAlbumName, hasChartExtension, hasIniExtension, hasSngExtension } from '../../../src-shared/UtilFunctions.js'
import { hasVideoExtension } from '../../ElectronUtilFunctions.js'
import { emitIpcEvent } from '../../main.js'
import { getSettings } from '../SettingsHandler.ipc.js'
import { getChartIssues, getIssuesXLSX } from './ExcelBuilder.js'
const { readdir, readFile, writeFile } = pkg
export async function scanIssues() {
const settings = await getSettings()
if (!settings.issueScanPath || !settings.spreadsheetOutputPath) {
emitIpcEvent('updateIssueScan', {
status: 'error',
message: 'Scan path or output path were not properly defined.',
})
return
}
try {
const chartFolders = await getChartFolders(settings.issueScanPath)
const limiter = new Bottleneck({ maxConcurrent: 20 }) // Ensures memory use stays bounded
const charts: { chart: ScannedChart; path: string }[] = []
for (const chartFolder of chartFolders) {
limiter.schedule(async () => {
const isSng = chartFolder.files.length === 1 && hasSngExtension(chartFolder.files[0])
const files = isSng ? await getFilesFromSng([chartFolder.path, chartFolder.files[0]].join('/')) : await getFilesFromFolder(chartFolder)
const result: { chart: ScannedChart; path: string } = {
chart: scanChartFolder(files),
path: chartFolder.path,
}
charts.push(result)
emitIpcEvent('updateIssueScan', { status: 'progress', message: `${charts.length}/${chartFolders.length} scanned...` })
})
}
await new Promise<void>((resolve, reject) => {
limiter.on('error', err => {
reject(err)
limiter.stop()
})
limiter.on('idle', async () => {
const issues = getChartIssues(charts)
const xlsx = await getIssuesXLSX(issues)
const outputPath = [settings.spreadsheetOutputPath, `chart_issues_${dayjs().format('YYYY.MM.DD_HH.mm.ss')}.xlsx`].join('/')
await writeFile(outputPath, new Uint8Array(xlsx))
await new Promise<void>(resolve => setTimeout(resolve, 500)) // Delay for OS file processing
await shell.openPath(outputPath)
emitIpcEvent('updateIssueScan', {
status: 'done',
message: `${issues.length} issues found in ${charts.length} charts. Spreadsheet saved to ${outputPath}`,
})
resolve()
})
})
} catch (err) {
emitIpcEvent('updateIssueScan', { status: 'error', message: inspect(err) })
}
}
/**
* @returns valid chart folders in `path` and all its subdirectories.
*/
async function getChartFolders(path: string) {
const chartFolders: { path: string; files: string[] }[] = []
const entries = await readdir(path, { withFileTypes: true })
const subfolders = _.chain(entries)
.filter(entry => entry.isDirectory() && entry.name !== '__MACOSX') // Apple should follow the principle of least astonishment (smh)
.map(folder => getChartFolders([path, folder.name].join('/')))
.value()
chartFolders.push(..._.flatMap(await Promise.all(subfolders)))
const sngFiles = entries.filter(entry => !entry.isDirectory() && hasSngExtension(entry.name))
chartFolders.push(...sngFiles.map(sf => ({ path, files: [sf.name] })))
if (
subfolders.length === 0 && // Charts won't contain other charts
appearsToBeChartFolder(entries.map(entry => getExtension(entry.name)))
) {
chartFolders.push({
path,
files: entries.filter(entry => !entry.isDirectory()).map(entry => entry.name),
})
emitIpcEvent('updateIssueScan', { status: 'progress', message: `${chartFolders} charts found...` })
}
return chartFolders
}
async function getFilesFromSng(sngPath: string) {
const sngStream = new SngStream(Readable.toWeb(createReadStream(sngPath)) as ReadableStream<Uint8Array>, { generateSongIni: true })
let header: SngHeader
sngStream.on('header', h => header = h)
const isFileTruncated = (fileName: string) => {
const MAX_FILE_MIB = 2048
const MAX_FILES_MIB = 5000
const sortedFiles = _.sortBy(header.fileMeta, f => f.contentsLen)
let usedSizeMib = 0
for (const sortedFile of sortedFiles) {
usedSizeMib += Number(sortedFile.contentsLen / BigInt(1024) / BigInt(1024))
if (sortedFile.filename === fileName) {
return usedSizeMib > MAX_FILES_MIB || sortedFile.contentsLen / BigInt(1024) / BigInt(1024) >= MAX_FILE_MIB
}
}
}
const files: { fileName: string; data: Uint8Array }[] = []
await new Promise<void>((resolve, reject) => {
sngStream.on('file', async (fileName, fileStream, nextFile) => {
const matchingFileMeta = header.fileMeta.find(f => f.filename === fileName)
if (hasVideoExtension(fileName) || isFileTruncated(fileName) || !matchingFileMeta) {
const reader = fileStream.getReader()
// eslint-disable-next-line no-constant-condition
while (true) {
const result = await reader.read()
if (result.done) {
break
}
}
} else {
const data = new Uint8Array(Number(matchingFileMeta.contentsLen))
let offset = 0
const reader = fileStream.getReader()
// eslint-disable-next-line no-constant-condition
while (true) {
const result = await reader.read()
if (result.done) {
break
}
data.set(result.value, offset)
offset += result.value.length
}
files.push({ fileName, data })
}
if (nextFile) {
nextFile()
} else {
resolve()
}
})
sngStream.on('error', error => reject(error))
sngStream.start()
})
return files
}
async function getFilesFromFolder(chartFolder: { path: string; files: string[] }): Promise<{ fileName: string; data: Uint8Array }[]> {
const files: { fileName: string; data: Uint8Array }[] = []
for (const fileName of chartFolder.files) {
if (hasChartExtension(fileName) || hasIniExtension(fileName) || hasAlbumName(fileName)) {
files.push({ fileName, data: await readFile(chartFolder.path + '/' + fileName) })
} else {
files.push({ fileName, data: new Uint8Array() })
}
}
return files
}

View File

@@ -40,6 +40,7 @@ const electronApi: ContextBridgeApi = {
quit: getEmitter('quit'),
showFolder: getEmitter('showFolder'),
showFile: getEmitter('showFile'),
scanIssues: getEmitter('scanIssues'),
},
on: {
errorLog: getListenerAdder('errorLog'),
@@ -51,6 +52,7 @@ const electronApi: ContextBridgeApi = {
queueUpdated: getListenerAdder('queueUpdated'),
maximized: getListenerAdder('maximized'),
minimized: getListenerAdder('minimized'),
updateIssueScan: getListenerAdder('updateIssueScan'),
},
}

View File

@@ -28,6 +28,8 @@ export interface Settings {
customTheme: ThemeColors | null // The colors of a custom theme
customThemePath: string | null // The last folder that contained the `customTheme`'s file
libraryPath: string | undefined // The path to the user's library
issueScanPath: string | undefined // The path to use when scanning for issues
spreadsheetOutputPath: string | undefined // The path to use when saving generated issues
chartFolderName: string // The relative path and name of the chart that is saved in `libraryPath`
isSng: boolean // If the chart should be downloaded as a .sng file or as a chart folder
isCompactTable: boolean // If the search result table should have reduced padding
@@ -47,6 +49,8 @@ export const defaultSettings: Settings = {
customTheme: null,
customThemePath: null,
libraryPath: undefined,
issueScanPath: undefined,
spreadsheetOutputPath: undefined,
chartFolderName: '{artist} - {name} ({charter})',
isSng: false,
isCompactTable: false,

View File

@@ -207,6 +207,27 @@ export function hasChartExtension(fileName: string) {
return ['chart', 'mid'].includes(getExtension(fileName).toLowerCase())
}
/**
* @returns `true` if `fileName` is a valid album fileName.
*/
export function hasAlbumName(fileName: string) {
return ['album.jpg', 'album.jpeg', 'album.png'].includes(fileName)
}
/**
* @returns `true` if `name` has a valid sng file extension.
*/
export function hasSngExtension(name: string) {
return 'sng' === getExtension(name).toLowerCase()
}
/**
* @returns `true` if `fileName` has a valid ini file extension.
*/
export function hasIniExtension(fileName: string) {
return 'ini' === getExtension(fileName).toLowerCase()
}
/**
* @returns `true` if `fileName` is a valid chart fileName.
*/
@@ -246,6 +267,16 @@ export function hasAudioName(fileName: string) {
)
}
/**
* @returns true if the list of filename `extensions` appears to be intended as a chart folder.
*/
export function appearsToBeChartFolder(extensions: string[]) {
const ext = extensions.map(extension => extension.toLowerCase())
const containsNotes = ext.includes('chart') || ext.includes('mid')
const containsAudio = ext.includes('ogg') || ext.includes('mp3') || ext.includes('wav') || ext.includes('opus')
return containsNotes || containsAudio
}
export function resolveChartFolderName(
chartFolderName: string,
chart: { name: string; artist: string; album: string; genre: string; year: string; charter: string },

View File

@@ -74,6 +74,7 @@ export interface IpcToMainEmitEvents {
quit: void
showFolder: string
showFile: string
scanIssues: void
}
export type IpcToMainEmitHandlers = {
@@ -93,6 +94,7 @@ export interface IpcFromMainEmitEvents {
queueUpdated: number[]
maximized: void
minimized: void
updateIssueScan: { status: 'progress' | 'error' | 'done'; message: string }
}
export type IpcFromMainEmitHandlers = {