├── .gitignore ├── README.md ├── icon.jpg ├── package-lock.json ├── package.json ├── screenshot.jpg ├── src ├── config.ts ├── download-manager.ts ├── fetch.ts ├── input.ts ├── loading.ts ├── main.ts ├── menu.ts ├── mocks │ └── index.ts ├── screen.ts ├── services │ └── archive.service.ts └── utils │ ├── notification.ts │ └── progress-bar.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /*.nro 2 | /node_modules 3 | /romfs/main.js 4 | /romfs/main.js.map 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nx-archive-browser 2 | 3 | Browse and download archives from archive.org on your Nintendo Switch 4 | 5 | 6 | 7 | 8 | ## Install 9 | 10 | 1. Copy `nx-archive-browser.nro` to `/switch`. The app will appear on the Homebrew Menu. 11 | 12 | 2. Configure your archive collections and root download folder in `config/nx-archive-browser/config.json`. 13 | The keys represent collection folders inside your download folder. The values are archive.org identifier of collections. 14 | 15 | Example: 16 | 17 | ```Json 18 | { 19 | "folder": "roms", 20 | "collections": { 21 | "N64": "SomeCollectionByGhostw***", 22 | "SNES": "SomeOtherCollectionByGhostw***" 23 | } 24 | } 25 | ``` 26 | 27 | The archives will be downloaded to `sdmc:/roms/N64` and `sdmc:/roms/SNES`. 28 | 29 | Read the [legal terms](https://archive.org/about/terms.php) of archive.org. I would encourage you to only download licence free archives, archives you developed on your own or in some cases own a copy of the original product (depends where you are located). 30 | 31 | ## Credits 32 | 33 | [TooTallNate - nxjs](https://github.com/TooTallNate/nx.js) - JS runtime for the Switch 34 | 35 | 36 | ## Possible TODOs 37 | 38 | - [ ] cancel downloads 39 | - [ ] search 40 | - [ ] external meta-lists [top, popular] 41 | - [ ] metadata [in-game screenshot, description] 42 | - [ ] unzip 43 | 44 | ## LICENSE 45 | 46 | MIT License 47 | 48 | Copyright (c) 2021 - 2024 Matthias Klan 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy 51 | of this software and associated documentation files (the "Software"), to deal 52 | in the Software without restriction, including without limitation the rights 53 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 54 | copies of the Software, and to permit persons to whom the Software is 55 | furnished to do so, subject to the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be included in all 58 | copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 61 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 62 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 63 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 64 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 65 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 66 | SOFTWARE. 67 | -------------------------------------------------------------------------------- /icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mklan/nx-archive-browser/1cffe8aa80f41cfb3a36ebd04dafbef6ce967989/icon.jpg -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-archive-browser", 3 | "version": "0.1.3", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "nx-archive-browser", 9 | "version": "0.1.3", 10 | "license": "MIT", 11 | "dependencies": { 12 | "kleur": "^4.1.5", 13 | "linkedom": "^0.16.6", 14 | "nxjs-constants": "^0.0.27", 15 | "sisteransi": "^1.0.5" 16 | }, 17 | "devDependencies": { 18 | "esbuild": "^0.17.19", 19 | "nxjs-pack": "^0.0.32", 20 | "nxjs-runtime": "^0.0.44" 21 | } 22 | }, 23 | "node_modules/@esbuild/android-arm": { 24 | "version": "0.17.19", 25 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", 26 | "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", 27 | "cpu": [ 28 | "arm" 29 | ], 30 | "dev": true, 31 | "optional": true, 32 | "os": [ 33 | "android" 34 | ], 35 | "engines": { 36 | "node": ">=12" 37 | } 38 | }, 39 | "node_modules/@esbuild/android-arm64": { 40 | "version": "0.17.19", 41 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", 42 | "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", 43 | "cpu": [ 44 | "arm64" 45 | ], 46 | "dev": true, 47 | "optional": true, 48 | "os": [ 49 | "android" 50 | ], 51 | "engines": { 52 | "node": ">=12" 53 | } 54 | }, 55 | "node_modules/@esbuild/android-x64": { 56 | "version": "0.17.19", 57 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", 58 | "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", 59 | "cpu": [ 60 | "x64" 61 | ], 62 | "dev": true, 63 | "optional": true, 64 | "os": [ 65 | "android" 66 | ], 67 | "engines": { 68 | "node": ">=12" 69 | } 70 | }, 71 | "node_modules/@esbuild/darwin-arm64": { 72 | "version": "0.17.19", 73 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 74 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 75 | "cpu": [ 76 | "arm64" 77 | ], 78 | "dev": true, 79 | "optional": true, 80 | "os": [ 81 | "darwin" 82 | ], 83 | "engines": { 84 | "node": ">=12" 85 | } 86 | }, 87 | "node_modules/@esbuild/darwin-x64": { 88 | "version": "0.17.19", 89 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", 90 | "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", 91 | "cpu": [ 92 | "x64" 93 | ], 94 | "dev": true, 95 | "optional": true, 96 | "os": [ 97 | "darwin" 98 | ], 99 | "engines": { 100 | "node": ">=12" 101 | } 102 | }, 103 | "node_modules/@esbuild/freebsd-arm64": { 104 | "version": "0.17.19", 105 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", 106 | "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", 107 | "cpu": [ 108 | "arm64" 109 | ], 110 | "dev": true, 111 | "optional": true, 112 | "os": [ 113 | "freebsd" 114 | ], 115 | "engines": { 116 | "node": ">=12" 117 | } 118 | }, 119 | "node_modules/@esbuild/freebsd-x64": { 120 | "version": "0.17.19", 121 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", 122 | "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", 123 | "cpu": [ 124 | "x64" 125 | ], 126 | "dev": true, 127 | "optional": true, 128 | "os": [ 129 | "freebsd" 130 | ], 131 | "engines": { 132 | "node": ">=12" 133 | } 134 | }, 135 | "node_modules/@esbuild/linux-arm": { 136 | "version": "0.17.19", 137 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", 138 | "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", 139 | "cpu": [ 140 | "arm" 141 | ], 142 | "dev": true, 143 | "optional": true, 144 | "os": [ 145 | "linux" 146 | ], 147 | "engines": { 148 | "node": ">=12" 149 | } 150 | }, 151 | "node_modules/@esbuild/linux-arm64": { 152 | "version": "0.17.19", 153 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", 154 | "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", 155 | "cpu": [ 156 | "arm64" 157 | ], 158 | "dev": true, 159 | "optional": true, 160 | "os": [ 161 | "linux" 162 | ], 163 | "engines": { 164 | "node": ">=12" 165 | } 166 | }, 167 | "node_modules/@esbuild/linux-ia32": { 168 | "version": "0.17.19", 169 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", 170 | "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", 171 | "cpu": [ 172 | "ia32" 173 | ], 174 | "dev": true, 175 | "optional": true, 176 | "os": [ 177 | "linux" 178 | ], 179 | "engines": { 180 | "node": ">=12" 181 | } 182 | }, 183 | "node_modules/@esbuild/linux-loong64": { 184 | "version": "0.17.19", 185 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", 186 | "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", 187 | "cpu": [ 188 | "loong64" 189 | ], 190 | "dev": true, 191 | "optional": true, 192 | "os": [ 193 | "linux" 194 | ], 195 | "engines": { 196 | "node": ">=12" 197 | } 198 | }, 199 | "node_modules/@esbuild/linux-mips64el": { 200 | "version": "0.17.19", 201 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", 202 | "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", 203 | "cpu": [ 204 | "mips64el" 205 | ], 206 | "dev": true, 207 | "optional": true, 208 | "os": [ 209 | "linux" 210 | ], 211 | "engines": { 212 | "node": ">=12" 213 | } 214 | }, 215 | "node_modules/@esbuild/linux-ppc64": { 216 | "version": "0.17.19", 217 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", 218 | "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", 219 | "cpu": [ 220 | "ppc64" 221 | ], 222 | "dev": true, 223 | "optional": true, 224 | "os": [ 225 | "linux" 226 | ], 227 | "engines": { 228 | "node": ">=12" 229 | } 230 | }, 231 | "node_modules/@esbuild/linux-riscv64": { 232 | "version": "0.17.19", 233 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", 234 | "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", 235 | "cpu": [ 236 | "riscv64" 237 | ], 238 | "dev": true, 239 | "optional": true, 240 | "os": [ 241 | "linux" 242 | ], 243 | "engines": { 244 | "node": ">=12" 245 | } 246 | }, 247 | "node_modules/@esbuild/linux-s390x": { 248 | "version": "0.17.19", 249 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", 250 | "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", 251 | "cpu": [ 252 | "s390x" 253 | ], 254 | "dev": true, 255 | "optional": true, 256 | "os": [ 257 | "linux" 258 | ], 259 | "engines": { 260 | "node": ">=12" 261 | } 262 | }, 263 | "node_modules/@esbuild/linux-x64": { 264 | "version": "0.17.19", 265 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", 266 | "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", 267 | "cpu": [ 268 | "x64" 269 | ], 270 | "dev": true, 271 | "optional": true, 272 | "os": [ 273 | "linux" 274 | ], 275 | "engines": { 276 | "node": ">=12" 277 | } 278 | }, 279 | "node_modules/@esbuild/netbsd-x64": { 280 | "version": "0.17.19", 281 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", 282 | "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", 283 | "cpu": [ 284 | "x64" 285 | ], 286 | "dev": true, 287 | "optional": true, 288 | "os": [ 289 | "netbsd" 290 | ], 291 | "engines": { 292 | "node": ">=12" 293 | } 294 | }, 295 | "node_modules/@esbuild/openbsd-x64": { 296 | "version": "0.17.19", 297 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", 298 | "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", 299 | "cpu": [ 300 | "x64" 301 | ], 302 | "dev": true, 303 | "optional": true, 304 | "os": [ 305 | "openbsd" 306 | ], 307 | "engines": { 308 | "node": ">=12" 309 | } 310 | }, 311 | "node_modules/@esbuild/sunos-x64": { 312 | "version": "0.17.19", 313 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", 314 | "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", 315 | "cpu": [ 316 | "x64" 317 | ], 318 | "dev": true, 319 | "optional": true, 320 | "os": [ 321 | "sunos" 322 | ], 323 | "engines": { 324 | "node": ">=12" 325 | } 326 | }, 327 | "node_modules/@esbuild/win32-arm64": { 328 | "version": "0.17.19", 329 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", 330 | "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", 331 | "cpu": [ 332 | "arm64" 333 | ], 334 | "dev": true, 335 | "optional": true, 336 | "os": [ 337 | "win32" 338 | ], 339 | "engines": { 340 | "node": ">=12" 341 | } 342 | }, 343 | "node_modules/@esbuild/win32-ia32": { 344 | "version": "0.17.19", 345 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", 346 | "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", 347 | "cpu": [ 348 | "ia32" 349 | ], 350 | "dev": true, 351 | "optional": true, 352 | "os": [ 353 | "win32" 354 | ], 355 | "engines": { 356 | "node": ">=12" 357 | } 358 | }, 359 | "node_modules/@esbuild/win32-x64": { 360 | "version": "0.17.19", 361 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", 362 | "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", 363 | "cpu": [ 364 | "x64" 365 | ], 366 | "dev": true, 367 | "optional": true, 368 | "os": [ 369 | "win32" 370 | ], 371 | "engines": { 372 | "node": ">=12" 373 | } 374 | }, 375 | "node_modules/@tootallnate/nacp": { 376 | "version": "0.0.1", 377 | "resolved": "https://registry.npmjs.org/@tootallnate/nacp/-/nacp-0.0.1.tgz", 378 | "integrity": "sha512-SodmIB6YIEj6oVSSRzi5Ll2Mz8EhlEkYFJN4UOLrEcQMkwUo7Fv3ZXcY2AnlhcQtlUABusZgwZkYHzbtpg/Ctg==", 379 | "dev": true 380 | }, 381 | "node_modules/@tootallnate/nro": { 382 | "version": "0.1.0", 383 | "resolved": "https://registry.npmjs.org/@tootallnate/nro/-/nro-0.1.0.tgz", 384 | "integrity": "sha512-4BtKlCxgj6TyJCwekkRTzaIJfEnVn97HbOLmJUwNT4ol/mZJ4smMohfU8zaYKnQiPjVdLhZ08qKH8kQI2xszjQ==", 385 | "dev": true, 386 | "dependencies": { 387 | "@tootallnate/nacp": "^0.0.1" 388 | } 389 | }, 390 | "node_modules/@tootallnate/romfs": { 391 | "version": "0.1.0", 392 | "resolved": "https://registry.npmjs.org/@tootallnate/romfs/-/romfs-0.1.0.tgz", 393 | "integrity": "sha512-ZXEgARulK9g0wLgL2mrsxOyAfcI0pAVSi+0UUs1B3lfbBv7qaltMe37YTmn8Aa8BvJ/sc+XqTI5aA7U6NX/nRg==", 394 | "dev": true 395 | }, 396 | "node_modules/@types/node": { 397 | "version": "20.11.7", 398 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.7.tgz", 399 | "integrity": "sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==", 400 | "dev": true, 401 | "dependencies": { 402 | "undici-types": "~5.26.4" 403 | } 404 | }, 405 | "node_modules/author-regex": { 406 | "version": "1.0.0", 407 | "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", 408 | "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", 409 | "dev": true, 410 | "engines": { 411 | "node": ">=0.8" 412 | } 413 | }, 414 | "node_modules/boolbase": { 415 | "version": "1.0.0", 416 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 417 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" 418 | }, 419 | "node_modules/bytes": { 420 | "version": "3.1.2", 421 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 422 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 423 | "dev": true, 424 | "engines": { 425 | "node": ">= 0.8" 426 | } 427 | }, 428 | "node_modules/chalk": { 429 | "version": "5.3.0", 430 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", 431 | "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", 432 | "dev": true, 433 | "engines": { 434 | "node": "^12.17.0 || ^14.13 || >=16.0.0" 435 | }, 436 | "funding": { 437 | "url": "https://github.com/chalk/chalk?sponsor=1" 438 | } 439 | }, 440 | "node_modules/css-select": { 441 | "version": "5.1.0", 442 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 443 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 444 | "dependencies": { 445 | "boolbase": "^1.0.0", 446 | "css-what": "^6.1.0", 447 | "domhandler": "^5.0.2", 448 | "domutils": "^3.0.1", 449 | "nth-check": "^2.0.1" 450 | }, 451 | "funding": { 452 | "url": "https://github.com/sponsors/fb55" 453 | } 454 | }, 455 | "node_modules/css-what": { 456 | "version": "6.1.0", 457 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 458 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 459 | "engines": { 460 | "node": ">= 6" 461 | }, 462 | "funding": { 463 | "url": "https://github.com/sponsors/fb55" 464 | } 465 | }, 466 | "node_modules/cssom": { 467 | "version": "0.5.0", 468 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", 469 | "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" 470 | }, 471 | "node_modules/dom-serializer": { 472 | "version": "2.0.0", 473 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 474 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 475 | "dependencies": { 476 | "domelementtype": "^2.3.0", 477 | "domhandler": "^5.0.2", 478 | "entities": "^4.2.0" 479 | }, 480 | "funding": { 481 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 482 | } 483 | }, 484 | "node_modules/domelementtype": { 485 | "version": "2.3.0", 486 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 487 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 488 | "funding": [ 489 | { 490 | "type": "github", 491 | "url": "https://github.com/sponsors/fb55" 492 | } 493 | ] 494 | }, 495 | "node_modules/domhandler": { 496 | "version": "5.0.3", 497 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 498 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 499 | "dependencies": { 500 | "domelementtype": "^2.3.0" 501 | }, 502 | "engines": { 503 | "node": ">= 4" 504 | }, 505 | "funding": { 506 | "url": "https://github.com/fb55/domhandler?sponsor=1" 507 | } 508 | }, 509 | "node_modules/domutils": { 510 | "version": "3.1.0", 511 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 512 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 513 | "dependencies": { 514 | "dom-serializer": "^2.0.0", 515 | "domelementtype": "^2.3.0", 516 | "domhandler": "^5.0.3" 517 | }, 518 | "funding": { 519 | "url": "https://github.com/fb55/domutils?sponsor=1" 520 | } 521 | }, 522 | "node_modules/entities": { 523 | "version": "4.5.0", 524 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 525 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 526 | "engines": { 527 | "node": ">=0.12" 528 | }, 529 | "funding": { 530 | "url": "https://github.com/fb55/entities?sponsor=1" 531 | } 532 | }, 533 | "node_modules/esbuild": { 534 | "version": "0.17.19", 535 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", 536 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", 537 | "dev": true, 538 | "hasInstallScript": true, 539 | "bin": { 540 | "esbuild": "bin/esbuild" 541 | }, 542 | "engines": { 543 | "node": ">=12" 544 | }, 545 | "optionalDependencies": { 546 | "@esbuild/android-arm": "0.17.19", 547 | "@esbuild/android-arm64": "0.17.19", 548 | "@esbuild/android-x64": "0.17.19", 549 | "@esbuild/darwin-arm64": "0.17.19", 550 | "@esbuild/darwin-x64": "0.17.19", 551 | "@esbuild/freebsd-arm64": "0.17.19", 552 | "@esbuild/freebsd-x64": "0.17.19", 553 | "@esbuild/linux-arm": "0.17.19", 554 | "@esbuild/linux-arm64": "0.17.19", 555 | "@esbuild/linux-ia32": "0.17.19", 556 | "@esbuild/linux-loong64": "0.17.19", 557 | "@esbuild/linux-mips64el": "0.17.19", 558 | "@esbuild/linux-ppc64": "0.17.19", 559 | "@esbuild/linux-riscv64": "0.17.19", 560 | "@esbuild/linux-s390x": "0.17.19", 561 | "@esbuild/linux-x64": "0.17.19", 562 | "@esbuild/netbsd-x64": "0.17.19", 563 | "@esbuild/openbsd-x64": "0.17.19", 564 | "@esbuild/sunos-x64": "0.17.19", 565 | "@esbuild/win32-arm64": "0.17.19", 566 | "@esbuild/win32-ia32": "0.17.19", 567 | "@esbuild/win32-x64": "0.17.19" 568 | } 569 | }, 570 | "node_modules/html-escaper": { 571 | "version": "3.0.3", 572 | "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", 573 | "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" 574 | }, 575 | "node_modules/htmlparser2": { 576 | "version": "9.1.0", 577 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", 578 | "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", 579 | "funding": [ 580 | "https://github.com/fb55/htmlparser2?sponsor=1", 581 | { 582 | "type": "github", 583 | "url": "https://github.com/sponsors/fb55" 584 | } 585 | ], 586 | "dependencies": { 587 | "domelementtype": "^2.3.0", 588 | "domhandler": "^5.0.3", 589 | "domutils": "^3.1.0", 590 | "entities": "^4.5.0" 591 | } 592 | }, 593 | "node_modules/kleur": { 594 | "version": "4.1.5", 595 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", 596 | "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", 597 | "engines": { 598 | "node": ">=6" 599 | } 600 | }, 601 | "node_modules/linkedom": { 602 | "version": "0.16.8", 603 | "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.16.8.tgz", 604 | "integrity": "sha512-+HtHVHBb3yZKlP9pgcJdi1AIG9tsAuo+Qtlz+79cCTsxgQwDzajsZjYvpp+DEckCK/zoGVhzkADniYZQ57KcFQ==", 605 | "dependencies": { 606 | "css-select": "^5.1.0", 607 | "cssom": "^0.5.0", 608 | "html-escaper": "^3.0.3", 609 | "htmlparser2": "^9.0.0", 610 | "uhyphen": "^0.2.0" 611 | } 612 | }, 613 | "node_modules/nth-check": { 614 | "version": "2.1.1", 615 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 616 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 617 | "dependencies": { 618 | "boolbase": "^1.0.0" 619 | }, 620 | "funding": { 621 | "url": "https://github.com/fb55/nth-check?sponsor=1" 622 | } 623 | }, 624 | "node_modules/nxjs-constants": { 625 | "version": "0.0.27", 626 | "resolved": "https://registry.npmjs.org/nxjs-constants/-/nxjs-constants-0.0.27.tgz", 627 | "integrity": "sha512-0RLvCpcGrPQNXru+3mWaL0zJAkkbUfFA7nqGGOMAVb9lxzG+l7eAJO5VbaPTRvz2Q/aVjIXXV5WH14OfusnmJA==" 628 | }, 629 | "node_modules/nxjs-pack": { 630 | "version": "0.0.32", 631 | "resolved": "https://registry.npmjs.org/nxjs-pack/-/nxjs-pack-0.0.32.tgz", 632 | "integrity": "sha512-wjvjfrvHFvOo3ho1BgemwQ9e4siKFuhTDq29RsJNV3VtTnf/nmc29TpwNYdOv1rviuIhejSjYktAFcMaTGAevg==", 633 | "dev": true, 634 | "dependencies": { 635 | "@tootallnate/nacp": "^0.0.1", 636 | "@tootallnate/nro": "^0.1.0", 637 | "@tootallnate/romfs": "^0.1.0", 638 | "@types/node": "^20.10.3", 639 | "bytes": "^3.1.2", 640 | "chalk": "^5.3.0", 641 | "parse-author": "^2.0.0" 642 | }, 643 | "bin": { 644 | "nxjs-pack": "dist/nxjs-pack.js" 645 | } 646 | }, 647 | "node_modules/nxjs-runtime": { 648 | "version": "0.0.44", 649 | "resolved": "https://registry.npmjs.org/nxjs-runtime/-/nxjs-runtime-0.0.44.tgz", 650 | "integrity": "sha512-kgqeq/tTRBOVeMsRyZ2n3OgVypRRcx9+tIU3phwRLuBjf58fjD0ij+ieYW3RLCSCbE94n7k2VmBrp17rMHINFA==", 651 | "dev": true 652 | }, 653 | "node_modules/parse-author": { 654 | "version": "2.0.0", 655 | "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", 656 | "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", 657 | "dev": true, 658 | "dependencies": { 659 | "author-regex": "^1.0.0" 660 | }, 661 | "engines": { 662 | "node": ">=0.10.0" 663 | } 664 | }, 665 | "node_modules/sisteransi": { 666 | "version": "1.0.5", 667 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 668 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" 669 | }, 670 | "node_modules/uhyphen": { 671 | "version": "0.2.0", 672 | "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", 673 | "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==" 674 | }, 675 | "node_modules/undici-types": { 676 | "version": "5.26.5", 677 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 678 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 679 | "dev": true 680 | } 681 | }, 682 | "dependencies": { 683 | "@esbuild/android-arm": { 684 | "version": "0.17.19", 685 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", 686 | "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", 687 | "dev": true, 688 | "optional": true 689 | }, 690 | "@esbuild/android-arm64": { 691 | "version": "0.17.19", 692 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", 693 | "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", 694 | "dev": true, 695 | "optional": true 696 | }, 697 | "@esbuild/android-x64": { 698 | "version": "0.17.19", 699 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", 700 | "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", 701 | "dev": true, 702 | "optional": true 703 | }, 704 | "@esbuild/darwin-arm64": { 705 | "version": "0.17.19", 706 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 707 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 708 | "dev": true, 709 | "optional": true 710 | }, 711 | "@esbuild/darwin-x64": { 712 | "version": "0.17.19", 713 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", 714 | "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", 715 | "dev": true, 716 | "optional": true 717 | }, 718 | "@esbuild/freebsd-arm64": { 719 | "version": "0.17.19", 720 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", 721 | "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", 722 | "dev": true, 723 | "optional": true 724 | }, 725 | "@esbuild/freebsd-x64": { 726 | "version": "0.17.19", 727 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", 728 | "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", 729 | "dev": true, 730 | "optional": true 731 | }, 732 | "@esbuild/linux-arm": { 733 | "version": "0.17.19", 734 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", 735 | "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", 736 | "dev": true, 737 | "optional": true 738 | }, 739 | "@esbuild/linux-arm64": { 740 | "version": "0.17.19", 741 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", 742 | "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", 743 | "dev": true, 744 | "optional": true 745 | }, 746 | "@esbuild/linux-ia32": { 747 | "version": "0.17.19", 748 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", 749 | "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", 750 | "dev": true, 751 | "optional": true 752 | }, 753 | "@esbuild/linux-loong64": { 754 | "version": "0.17.19", 755 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", 756 | "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", 757 | "dev": true, 758 | "optional": true 759 | }, 760 | "@esbuild/linux-mips64el": { 761 | "version": "0.17.19", 762 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", 763 | "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", 764 | "dev": true, 765 | "optional": true 766 | }, 767 | "@esbuild/linux-ppc64": { 768 | "version": "0.17.19", 769 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", 770 | "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", 771 | "dev": true, 772 | "optional": true 773 | }, 774 | "@esbuild/linux-riscv64": { 775 | "version": "0.17.19", 776 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", 777 | "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", 778 | "dev": true, 779 | "optional": true 780 | }, 781 | "@esbuild/linux-s390x": { 782 | "version": "0.17.19", 783 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", 784 | "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", 785 | "dev": true, 786 | "optional": true 787 | }, 788 | "@esbuild/linux-x64": { 789 | "version": "0.17.19", 790 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", 791 | "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", 792 | "dev": true, 793 | "optional": true 794 | }, 795 | "@esbuild/netbsd-x64": { 796 | "version": "0.17.19", 797 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", 798 | "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", 799 | "dev": true, 800 | "optional": true 801 | }, 802 | "@esbuild/openbsd-x64": { 803 | "version": "0.17.19", 804 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", 805 | "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", 806 | "dev": true, 807 | "optional": true 808 | }, 809 | "@esbuild/sunos-x64": { 810 | "version": "0.17.19", 811 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", 812 | "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", 813 | "dev": true, 814 | "optional": true 815 | }, 816 | "@esbuild/win32-arm64": { 817 | "version": "0.17.19", 818 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", 819 | "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", 820 | "dev": true, 821 | "optional": true 822 | }, 823 | "@esbuild/win32-ia32": { 824 | "version": "0.17.19", 825 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", 826 | "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", 827 | "dev": true, 828 | "optional": true 829 | }, 830 | "@esbuild/win32-x64": { 831 | "version": "0.17.19", 832 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", 833 | "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", 834 | "dev": true, 835 | "optional": true 836 | }, 837 | "@tootallnate/nacp": { 838 | "version": "0.0.1", 839 | "resolved": "https://registry.npmjs.org/@tootallnate/nacp/-/nacp-0.0.1.tgz", 840 | "integrity": "sha512-SodmIB6YIEj6oVSSRzi5Ll2Mz8EhlEkYFJN4UOLrEcQMkwUo7Fv3ZXcY2AnlhcQtlUABusZgwZkYHzbtpg/Ctg==", 841 | "dev": true 842 | }, 843 | "@tootallnate/nro": { 844 | "version": "0.1.0", 845 | "resolved": "https://registry.npmjs.org/@tootallnate/nro/-/nro-0.1.0.tgz", 846 | "integrity": "sha512-4BtKlCxgj6TyJCwekkRTzaIJfEnVn97HbOLmJUwNT4ol/mZJ4smMohfU8zaYKnQiPjVdLhZ08qKH8kQI2xszjQ==", 847 | "dev": true, 848 | "requires": { 849 | "@tootallnate/nacp": "^0.0.1" 850 | } 851 | }, 852 | "@tootallnate/romfs": { 853 | "version": "0.1.0", 854 | "resolved": "https://registry.npmjs.org/@tootallnate/romfs/-/romfs-0.1.0.tgz", 855 | "integrity": "sha512-ZXEgARulK9g0wLgL2mrsxOyAfcI0pAVSi+0UUs1B3lfbBv7qaltMe37YTmn8Aa8BvJ/sc+XqTI5aA7U6NX/nRg==", 856 | "dev": true 857 | }, 858 | "@types/node": { 859 | "version": "20.11.7", 860 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.7.tgz", 861 | "integrity": "sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==", 862 | "dev": true, 863 | "requires": { 864 | "undici-types": "~5.26.4" 865 | } 866 | }, 867 | "author-regex": { 868 | "version": "1.0.0", 869 | "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", 870 | "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", 871 | "dev": true 872 | }, 873 | "boolbase": { 874 | "version": "1.0.0", 875 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 876 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" 877 | }, 878 | "bytes": { 879 | "version": "3.1.2", 880 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 881 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 882 | "dev": true 883 | }, 884 | "chalk": { 885 | "version": "5.3.0", 886 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", 887 | "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", 888 | "dev": true 889 | }, 890 | "css-select": { 891 | "version": "5.1.0", 892 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 893 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 894 | "requires": { 895 | "boolbase": "^1.0.0", 896 | "css-what": "^6.1.0", 897 | "domhandler": "^5.0.2", 898 | "domutils": "^3.0.1", 899 | "nth-check": "^2.0.1" 900 | } 901 | }, 902 | "css-what": { 903 | "version": "6.1.0", 904 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 905 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" 906 | }, 907 | "cssom": { 908 | "version": "0.5.0", 909 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", 910 | "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" 911 | }, 912 | "dom-serializer": { 913 | "version": "2.0.0", 914 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 915 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 916 | "requires": { 917 | "domelementtype": "^2.3.0", 918 | "domhandler": "^5.0.2", 919 | "entities": "^4.2.0" 920 | } 921 | }, 922 | "domelementtype": { 923 | "version": "2.3.0", 924 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 925 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" 926 | }, 927 | "domhandler": { 928 | "version": "5.0.3", 929 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 930 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 931 | "requires": { 932 | "domelementtype": "^2.3.0" 933 | } 934 | }, 935 | "domutils": { 936 | "version": "3.1.0", 937 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 938 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 939 | "requires": { 940 | "dom-serializer": "^2.0.0", 941 | "domelementtype": "^2.3.0", 942 | "domhandler": "^5.0.3" 943 | } 944 | }, 945 | "entities": { 946 | "version": "4.5.0", 947 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 948 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" 949 | }, 950 | "esbuild": { 951 | "version": "0.17.19", 952 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", 953 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", 954 | "dev": true, 955 | "requires": { 956 | "@esbuild/android-arm": "0.17.19", 957 | "@esbuild/android-arm64": "0.17.19", 958 | "@esbuild/android-x64": "0.17.19", 959 | "@esbuild/darwin-arm64": "0.17.19", 960 | "@esbuild/darwin-x64": "0.17.19", 961 | "@esbuild/freebsd-arm64": "0.17.19", 962 | "@esbuild/freebsd-x64": "0.17.19", 963 | "@esbuild/linux-arm": "0.17.19", 964 | "@esbuild/linux-arm64": "0.17.19", 965 | "@esbuild/linux-ia32": "0.17.19", 966 | "@esbuild/linux-loong64": "0.17.19", 967 | "@esbuild/linux-mips64el": "0.17.19", 968 | "@esbuild/linux-ppc64": "0.17.19", 969 | "@esbuild/linux-riscv64": "0.17.19", 970 | "@esbuild/linux-s390x": "0.17.19", 971 | "@esbuild/linux-x64": "0.17.19", 972 | "@esbuild/netbsd-x64": "0.17.19", 973 | "@esbuild/openbsd-x64": "0.17.19", 974 | "@esbuild/sunos-x64": "0.17.19", 975 | "@esbuild/win32-arm64": "0.17.19", 976 | "@esbuild/win32-ia32": "0.17.19", 977 | "@esbuild/win32-x64": "0.17.19" 978 | } 979 | }, 980 | "html-escaper": { 981 | "version": "3.0.3", 982 | "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", 983 | "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" 984 | }, 985 | "htmlparser2": { 986 | "version": "9.1.0", 987 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", 988 | "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", 989 | "requires": { 990 | "domelementtype": "^2.3.0", 991 | "domhandler": "^5.0.3", 992 | "domutils": "^3.1.0", 993 | "entities": "^4.5.0" 994 | } 995 | }, 996 | "kleur": { 997 | "version": "4.1.5", 998 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", 999 | "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" 1000 | }, 1001 | "linkedom": { 1002 | "version": "0.16.8", 1003 | "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.16.8.tgz", 1004 | "integrity": "sha512-+HtHVHBb3yZKlP9pgcJdi1AIG9tsAuo+Qtlz+79cCTsxgQwDzajsZjYvpp+DEckCK/zoGVhzkADniYZQ57KcFQ==", 1005 | "requires": { 1006 | "css-select": "^5.1.0", 1007 | "cssom": "^0.5.0", 1008 | "html-escaper": "^3.0.3", 1009 | "htmlparser2": "^9.0.0", 1010 | "uhyphen": "^0.2.0" 1011 | } 1012 | }, 1013 | "nth-check": { 1014 | "version": "2.1.1", 1015 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 1016 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 1017 | "requires": { 1018 | "boolbase": "^1.0.0" 1019 | } 1020 | }, 1021 | "nxjs-constants": { 1022 | "version": "0.0.27", 1023 | "resolved": "https://registry.npmjs.org/nxjs-constants/-/nxjs-constants-0.0.27.tgz", 1024 | "integrity": "sha512-0RLvCpcGrPQNXru+3mWaL0zJAkkbUfFA7nqGGOMAVb9lxzG+l7eAJO5VbaPTRvz2Q/aVjIXXV5WH14OfusnmJA==" 1025 | }, 1026 | "nxjs-pack": { 1027 | "version": "0.0.32", 1028 | "resolved": "https://registry.npmjs.org/nxjs-pack/-/nxjs-pack-0.0.32.tgz", 1029 | "integrity": "sha512-wjvjfrvHFvOo3ho1BgemwQ9e4siKFuhTDq29RsJNV3VtTnf/nmc29TpwNYdOv1rviuIhejSjYktAFcMaTGAevg==", 1030 | "dev": true, 1031 | "requires": { 1032 | "@tootallnate/nacp": "^0.0.1", 1033 | "@tootallnate/nro": "^0.1.0", 1034 | "@tootallnate/romfs": "^0.1.0", 1035 | "@types/node": "^20.10.3", 1036 | "bytes": "^3.1.2", 1037 | "chalk": "^5.3.0", 1038 | "parse-author": "^2.0.0" 1039 | } 1040 | }, 1041 | "nxjs-runtime": { 1042 | "version": "0.0.44", 1043 | "resolved": "https://registry.npmjs.org/nxjs-runtime/-/nxjs-runtime-0.0.44.tgz", 1044 | "integrity": "sha512-kgqeq/tTRBOVeMsRyZ2n3OgVypRRcx9+tIU3phwRLuBjf58fjD0ij+ieYW3RLCSCbE94n7k2VmBrp17rMHINFA==", 1045 | "dev": true 1046 | }, 1047 | "parse-author": { 1048 | "version": "2.0.0", 1049 | "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", 1050 | "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", 1051 | "dev": true, 1052 | "requires": { 1053 | "author-regex": "^1.0.0" 1054 | } 1055 | }, 1056 | "sisteransi": { 1057 | "version": "1.0.5", 1058 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 1059 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" 1060 | }, 1061 | "uhyphen": { 1062 | "version": "0.2.0", 1063 | "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", 1064 | "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==" 1065 | }, 1066 | "undici-types": { 1067 | "version": "5.26.5", 1068 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 1069 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 1070 | "dev": true 1071 | } 1072 | } 1073 | } 1074 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleId": "01ee9aec91da0000", 3 | "name": "nx-archive-browser", 4 | "version": "0.1.3", 5 | "private": true, 6 | "description": "", 7 | "author": { 8 | "name": "mklan" 9 | }, 10 | "scripts": { 11 | "build": "esbuild --bundle --sourcemap --sources-content=false --target=es2022 --format=esm src/main.ts --outfile=romfs/main.js", 12 | "nro": "nxjs-pack", 13 | "copy": "curl -T nx-archive-browser.nro ftp://192.168.1.46:5000/switch/" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "esbuild": "^0.17.19", 18 | "nxjs-pack": "^0.0.32", 19 | "nxjs-runtime": "^0.0.44" 20 | }, 21 | "dependencies": { 22 | "kleur": "^4.1.5", 23 | "linkedom": "^0.16.6", 24 | "nxjs-constants": "^0.0.27", 25 | "sisteransi": "^1.0.5" 26 | } 27 | } -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mklan/nx-archive-browser/1cffe8aa80f41cfb3a36ebd04dafbef6ce967989/screenshot.jpg -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | const defaultConfig = (path: string) => ({ 2 | folder: "archives", 3 | collections: { 4 | add: "empty", 5 | collections: "empty", 6 | to: "empty", 7 | [path]: "empty", 8 | }, 9 | }); 10 | 11 | function loadConfig(path: string) { 12 | let config; 13 | 14 | async function load() { 15 | try { 16 | const buffer = await Switch.readFile(path); 17 | 18 | return JSON.parse(new TextDecoder().decode(buffer)); 19 | } catch (e) { 20 | const config = defaultConfig(path); 21 | Switch.writeFileSync(path, JSON.stringify(config, null, 2)); 22 | return config; 23 | } 24 | } 25 | 26 | async function get(key: string) { 27 | if (config) { 28 | return config[key]; 29 | } 30 | config = await load(); 31 | return config[key]; 32 | } 33 | 34 | return { get }; 35 | } 36 | 37 | export const config = loadConfig("sdmc:/config/nx-archive-browser/config.json"); 38 | -------------------------------------------------------------------------------- /src/download-manager.ts: -------------------------------------------------------------------------------- 1 | import { createMenu } from "./menu"; 2 | import { progressBar } from "./utils/progress-bar"; 3 | 4 | type Download = { 5 | customState?: string; 6 | fileName: string; 7 | progress: number; 8 | speed: number; 9 | }; 10 | 11 | export function createDownloadManager() { 12 | let downloads: Download[] = []; 13 | 14 | const menu = createMenu({ 15 | id: "downloads", 16 | items: [], 17 | height: 5, 18 | onSelect: () => {}, 19 | }); 20 | 21 | function add(fileName: string, customState: string = "") { 22 | downloads = [ 23 | { customState, fileName, progress: 0, speed: 0 }, 24 | ...downloads, 25 | ]; 26 | } 27 | 28 | function update(fileName: string, progress: number, speed: number) { 29 | const index = downloads.findIndex( 30 | (download) => download.fileName === fileName 31 | ); 32 | if (index < 0) return; 33 | 34 | downloads[index].progress = progress; 35 | downloads[index].speed = speed; 36 | } 37 | 38 | function render() { 39 | const items = downloads.map((download) => { 40 | const isComplete = download.progress >= 1; 41 | let status = isComplete 42 | ? "complete" 43 | : `${progressBar(download.progress)} ${Math.floor( 44 | download.progress * 100 45 | )}% ${download.speed?.toFixed(2) || "?"} Mb/s `; 46 | 47 | return { 48 | meta: {}, 49 | title: `${strWidth(download.fileName, 36)} ${ 50 | download.customState || status 51 | }`, 52 | }; 53 | }); 54 | 55 | menu.setItems(items); 56 | menu.render(false); 57 | } 58 | 59 | return { render, add, update }; 60 | } 61 | 62 | function strWidth(str: string, size: number) { 63 | if (str.length >= size) { 64 | return str.slice(0, size); 65 | } 66 | const space = Array.from(Array(size - str.length)) 67 | .fill(" ") 68 | .join(""); 69 | return `${str}${space}`; 70 | } 71 | -------------------------------------------------------------------------------- /src/fetch.ts: -------------------------------------------------------------------------------- 1 | type Stats = { 2 | progress: number; 3 | receivedLength: number; 4 | contentLength: number; 5 | speed: number; 6 | }; 7 | 8 | type FetchOptions = { 9 | onProgress: (stats: Stats) => void; 10 | onDownloadStart: () => void; 11 | }; 12 | 13 | export async function fetchProgress( 14 | url: string, 15 | { onProgress, onDownloadStart }: FetchOptions 16 | ) { 17 | // Step 1: start the fetch and obtain a reader 18 | const response = await fetch(url); 19 | 20 | if (!response.ok) { 21 | throw new Error(response.status.toString()); 22 | } 23 | 24 | const reader = response.body!.getReader(); 25 | 26 | // Step 2: get total length 27 | const contentLength = +response.headers.get("Content-Length")!; 28 | 29 | // Step 3: read the data 30 | let receivedLength = 0; // received that many bytes at the moment 31 | 32 | let last = { time: 0, value: 0 }; 33 | let counter = 0; 34 | let speed = 0; 35 | 36 | const stream = new ReadableStream({ 37 | start(controller) { 38 | onDownloadStart(); 39 | return pump(); 40 | function pump(): Promise< 41 | ReadableStreamReadResult | undefined 42 | > { 43 | return reader.read().then(({ done, value }) => { 44 | // When no more data needs to be consumed, close the stream 45 | if (done) { 46 | controller.close(); 47 | return; 48 | } 49 | // Enqueue the next data chunk into our target stream 50 | controller.enqueue(value); 51 | 52 | receivedLength += value.length; 53 | 54 | const progress = receivedLength / (contentLength / 100) / 100; 55 | 56 | const current = { time: Date.now(), value: receivedLength }; 57 | 58 | if (counter % 50 === 0) { 59 | if (last.time) { 60 | const time = current.time - last.time; 61 | const val = current.value - last.value; 62 | 63 | speed = byteToMB(val / (time / 1000)); 64 | } 65 | 66 | last = { ...current }; 67 | } 68 | 69 | onProgress({ 70 | progress: isFinite(progress) ? progress : 0, 71 | receivedLength, 72 | contentLength, 73 | speed, 74 | }); 75 | 76 | counter += 1; 77 | return pump(); 78 | }); 79 | } 80 | }, 81 | }); 82 | 83 | return new Response(stream); 84 | } 85 | 86 | function byteToMB(value: number) { 87 | return value / 1024 / 1024; 88 | } 89 | -------------------------------------------------------------------------------- /src/input.ts: -------------------------------------------------------------------------------- 1 | // import readline from 'readline'; 2 | 3 | import { Hid } from "nxjs-constants"; 4 | 5 | const { Button } = Hid; 6 | 7 | const readline = {}; 8 | 9 | export function input({ onButtonDown, onButtonUp }, isNodeJS?: boolean) { 10 | if (isNodeJS) { 11 | nodeJSinput({ onButtonDown, onButtonUp }); 12 | return; 13 | } 14 | 15 | addEventListener("buttondown", (e) => { 16 | if (e.detail === Button.ZL) { 17 | onButtonDown("ZL"); 18 | } 19 | if (e.detail === Button.ZR) { 20 | onButtonDown("ZR"); 21 | } 22 | if (e.detail === Button.L) { 23 | onButtonDown("L"); 24 | } 25 | if (e.detail === Button.R) { 26 | onButtonDown("R"); 27 | } 28 | if ( 29 | [Button.Down, Button.StickLDown, Button.StickRDown].includes(e.detail) 30 | ) { 31 | onButtonDown("down"); 32 | } 33 | if ([Button.Up, Button.StickLUp, Button.StickRUp].includes(e.detail)) { 34 | onButtonDown("up"); 35 | } 36 | if ( 37 | [Button.Left, Button.StickLLeft, Button.StickRLeft].includes(e.detail) 38 | ) { 39 | onButtonDown("left"); 40 | } 41 | if ( 42 | [Button.Right, Button.StickLRight, Button.StickRRight].includes(e.detail) 43 | ) { 44 | onButtonDown("right"); 45 | } 46 | if ([Button.A].includes(e.detail)) { 47 | onButtonDown("A"); 48 | } 49 | if ([Button.B].includes(e.detail)) { 50 | onButtonDown("B"); 51 | } 52 | if ([Button.Y].includes(e.detail)) { 53 | onButtonDown("Y"); 54 | } 55 | if ([Button.X].includes(e.detail)) { 56 | onButtonDown("X"); 57 | } 58 | }); 59 | 60 | addEventListener("buttonup", (e) => { 61 | if ( 62 | [Button.Down, Button.StickLDown, Button.StickRDown].includes(e.detail) 63 | ) { 64 | onButtonUp("down"); 65 | } 66 | if ([Button.Up, Button.StickLUp, Button.StickRUp].includes(e.detail)) { 67 | onButtonUp("up"); 68 | } 69 | }); 70 | } 71 | 72 | function nodeJSinput({ onButtonDown, onButtonUp }) { 73 | readline.emitKeypressEvents(process.stdin); 74 | 75 | process.stdin.on("keypress", (ch, { name, ctrl }) => { 76 | if (name === "up") onButtonDown("up"); 77 | if (name === "down") onButtonDown("down"); 78 | if (name === "a") onButtonDown("A"); 79 | if (ctrl && name === "c") process.exit(1); 80 | }); 81 | 82 | process.stdin.setRawMode(true); 83 | process.stdin.resume(); 84 | } 85 | -------------------------------------------------------------------------------- /src/loading.ts: -------------------------------------------------------------------------------- 1 | import { createScreen } from "./screen"; 2 | 3 | export const loading = (() => { 4 | let msg = "loading..."; 5 | let isLoading = false; 6 | 7 | const screen = createScreen(80, 22); 8 | 9 | return { 10 | start: (text: string) => { 11 | msg = text; 12 | isLoading = true; 13 | }, 14 | stop: () => (isLoading = false), 15 | render: () => { 16 | console.log(screen.centerTextVert(msg)); 17 | }, 18 | isLoading: () => isLoading, 19 | }; 20 | })(); 21 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { red, cyan, green, white, yellow, bgRed } from "kleur/colors"; 2 | 3 | import { cursor, erase } from "sisteransi"; 4 | import { createDownloadManager } from "./download-manager"; 5 | import { fetchProgress } from "./fetch"; 6 | import { input } from "./input"; 7 | import { createMenu, Item } from "./menu"; 8 | import { createScreen } from "./screen"; 9 | import { download, fetchArchiveList } from "./services/archive.service"; 10 | import { notification } from "./utils/notification"; 11 | import { config } from "./config"; 12 | import { loading } from "./loading"; 13 | 14 | type Collection = { 15 | title: string; 16 | archiveName: string; 17 | }; 18 | 19 | const VISIBLE_MENU_ITEMS = 22; 20 | 21 | let currentMenu; 22 | let mainMenu; 23 | let currentCollection; 24 | 25 | let isButtonDownPressed = false; 26 | let isButtonUpPressed = false; 27 | 28 | const downloadManager = createDownloadManager(); 29 | const screen = createScreen(80, 43); 30 | 31 | const header = screen.spread3({ 32 | left: { text: "", color: white }, 33 | center: { text: "nx-archive-browser v0.1.3", color: yellow }, 34 | right: { text: "+ Exit ", color: white }, 35 | }); 36 | 37 | async function main() { 38 | // init local-storage to get the profile prompt directly on start 39 | localStorage.setItem("init", "true"); 40 | 41 | const collections = await config.get("collections"); 42 | 43 | const collectionItems: Item[] = Object.entries(collections).map( 44 | ([title, archiveName]) => ({ 45 | title, 46 | meta: { 47 | title, 48 | archiveName, 49 | }, 50 | }) 51 | ); 52 | 53 | mainMenu = createMenu({ 54 | id: "main", 55 | items: collectionItems, 56 | height: VISIBLE_MENU_ITEMS, 57 | onSelect: async (item) => { 58 | try { 59 | await enterCollection(item.meta as Collection, () => { 60 | currentMenu = mainMenu!; 61 | }); 62 | currentCollection = item.meta.title; 63 | } catch (e) { 64 | loading.stop(); 65 | notification.show(4000, "Error loading collection"); 66 | currentMenu = mainMenu!; 67 | } 68 | }, 69 | }); 70 | currentMenu = mainMenu; 71 | 72 | requestAnimationFrame(loop); 73 | 74 | let timeout; 75 | input({ 76 | onButtonDown: (key: string) => { 77 | if (loading.isLoading()) return; 78 | if (key === "up") { 79 | currentMenu!.prev(); 80 | timeout = setTimeout(() => { 81 | isButtonUpPressed = true; 82 | }, 500); 83 | } 84 | if (key === "down") { 85 | currentMenu!.next(); 86 | 87 | timeout = setTimeout(() => { 88 | isButtonDownPressed = true; 89 | }, 500); 90 | } 91 | if (key === "left") { 92 | currentMenu!.prev(VISIBLE_MENU_ITEMS); 93 | } 94 | if (key === "right") { 95 | currentMenu!.next(VISIBLE_MENU_ITEMS); 96 | } 97 | if (key === "L") { 98 | if (currentMenu!.getId() === "collection") { 99 | mainMenu!.prev(); 100 | mainMenu!.select(); 101 | } 102 | } 103 | if (key === "R") { 104 | if (currentMenu!.getId() === "collection") { 105 | mainMenu!.next(); 106 | mainMenu!.select(); 107 | } 108 | } 109 | if (key === "A") { 110 | currentMenu!.select(); 111 | } 112 | if (key === "B") { 113 | currentMenu = mainMenu!; 114 | } 115 | if (key === "Y") { 116 | localStorage.clear(); 117 | notification.show(2000, "cache cleared!"); 118 | } 119 | if (key === "X") { 120 | if (currentMenu!.getId() === "collection") { 121 | const item = currentMenu!.getSelected(); 122 | if (!item.marked) return; 123 | 124 | remove(item.meta.collection.title, item.meta.fileName); 125 | item.marked = false; 126 | } 127 | } 128 | }, 129 | onButtonUp: (key: string) => { 130 | if (loading.isLoading()) return; 131 | if (key === "up") { 132 | clearTimeout(timeout!); 133 | 134 | isButtonUpPressed = false; 135 | } 136 | if (key === "down") { 137 | clearTimeout(timeout!); 138 | 139 | isButtonDownPressed = false; 140 | } 141 | }, 142 | }); 143 | } 144 | 145 | async function listLocalFiles(collection: string) { 146 | const folder = await config.get("folder"); 147 | return Switch.readDirSync(`sdmc:/${folder}/${collection}`) || []; 148 | } 149 | 150 | async function remove(collection: string, fileName: string) { 151 | const folder = await config.get("folder"); 152 | 153 | const path = `sdmc:/${folder}/${collection}/${fileName}`; 154 | Switch.removeSync(path); 155 | notification.show(2000, `${fileName} deleted!`); 156 | } 157 | 158 | async function handleDownload(collection: Collection, item: Item) { 159 | const folder = await config.get("folder"); 160 | 161 | try { 162 | await download( 163 | collection, 164 | item.meta.fileName, 165 | `${folder}/${collection.title}`, 166 | { 167 | onDownloadStart: () => { 168 | downloadManager.add(item.meta.fileName); 169 | }, 170 | onProgress: (p) => { 171 | downloadManager.update(item.meta.fileName, p.progress, p.speed); 172 | }, 173 | } 174 | ); 175 | item.marked = true; 176 | } catch (e) { 177 | downloadManager.add(item.meta.fileName, e as string); 178 | } 179 | } 180 | 181 | async function enterCollection(collection: Collection, onExit: () => void) { 182 | loading.start(`Fetching collection: ${collection.title} ...`); 183 | const cached = localStorage.getItem(collection.archiveName); 184 | const entries = cached 185 | ? JSON.parse(cached) 186 | : await fetchArchiveList(collection.archiveName); 187 | 188 | localStorage.setItem(collection.archiveName, JSON.stringify(entries)); 189 | 190 | const localFiles = await listLocalFiles(collection.title); 191 | 192 | const titles = entries.map((entry) => ({ 193 | meta: { 194 | fileName: entry.title, 195 | collection, 196 | }, 197 | marked: localFiles.includes(entry.title), 198 | title: entry.title.slice(0, 79), 199 | })); 200 | 201 | currentMenu = createMenu({ 202 | id: "collection", 203 | items: [ 204 | { 205 | title: "<", 206 | }, 207 | ...titles, 208 | ], 209 | height: VISIBLE_MENU_ITEMS, 210 | onSelect: (item: Item) => { 211 | if (item.title === "<") { 212 | return onExit(); 213 | } 214 | handleDownload(collection, item); 215 | }, 216 | }); 217 | 218 | loading.stop(); 219 | } 220 | 221 | function render() { 222 | console.log(erase.screen); 223 | 224 | console.log(header); 225 | 226 | console.log(""); 227 | 228 | if (currentMenu!.getId() === "collection") { 229 | console.log( 230 | screen.spread3({ 231 | left: { text: ` L ${mainMenu!.getPrev().title}`, color: white }, 232 | center: { text: currentCollection!, color: cyan }, 233 | right: { text: `${mainMenu!.getNext().title} R `, color: white }, 234 | }) 235 | ); 236 | } else { 237 | console.log(""); 238 | console.log(""); 239 | } 240 | console.log(""); 241 | 242 | if (loading.isLoading()) { 243 | loading.render(); 244 | console.log(""); 245 | } else { 246 | currentMenu!.render(); 247 | } 248 | 249 | console.log(screen.right(notification.toString() + " ")); 250 | console.log( 251 | screen.centerText( 252 | "__________________________________ Downloads ___________________________________" 253 | ) 254 | ); 255 | console.log(""); 256 | 257 | downloadManager.render(); 258 | 259 | console.log( 260 | "________________________________________________________________________________" 261 | ); 262 | 263 | if (currentMenu!.getId() === "collection") { 264 | console.log( 265 | screen.spread2({ 266 | left: { text: " + Nav A Download B Home X Del", color: white }, 267 | right: { text: "github.com/mklan 2024 ", color: white }, 268 | }) 269 | ); 270 | } else { 271 | console.log( 272 | screen.spread2({ 273 | left: { text: " + Nav A Enter Y clear cache", color: white }, 274 | right: { text: "github.com/mklan 2024 ", color: white }, 275 | }) 276 | ); 277 | } 278 | } 279 | 280 | function loop() { 281 | if (isButtonUpPressed) { 282 | currentMenu!.prev(); 283 | } 284 | if (isButtonDownPressed) { 285 | currentMenu!.next(); 286 | } 287 | 288 | render(); 289 | requestAnimationFrame(loop); 290 | } 291 | 292 | main(); 293 | -------------------------------------------------------------------------------- /src/menu.ts: -------------------------------------------------------------------------------- 1 | import { red, blue, green, white, bold, bgRed } from "kleur/colors"; 2 | 3 | export type Item = { 4 | title: string; 5 | meta: Record; 6 | marked?: boolean; 7 | }; 8 | 9 | type menuOptions = { 10 | items: Item[]; 11 | height: number; 12 | id: string; 13 | onSelect: (item: Item) => void; 14 | }; 15 | 16 | export function createMenu(opts: menuOptions) { 17 | let { items, height, id, onSelect } = opts; 18 | let selected = 0; 19 | 20 | function fillSpace() { 21 | if (items.length < height) { 22 | Array.from(Array(height - items.length)).forEach(() => console.log()); 23 | } 24 | } 25 | 26 | function getId() { 27 | return id; 28 | } 29 | 30 | function setItems(newItems: Item[]) { 31 | items = newItems; 32 | } 33 | 34 | function render(highlightSelected = true) { 35 | let start = 0; 36 | let end = height; 37 | 38 | if (selected >= height / 2) { 39 | start = selected - height / 2; 40 | end = selected + height / 2; 41 | 42 | if (end >= items.length - 1) { 43 | start = items.length - 1 - height; 44 | end = items.length - 1; 45 | } 46 | } 47 | 48 | // debug console.log(selected, before, after) 49 | 50 | items.slice(start, end).forEach((item, i) => { 51 | if (highlightSelected && item.title === items[selected].title) { 52 | // console.warn('>', item.title); 53 | console.warn(item.title); 54 | } else if (item.marked) { 55 | console.log(green(item.title)); 56 | } else { 57 | console.log(item.title); 58 | } 59 | }); 60 | 61 | fillSpace(); 62 | 63 | if (items.length && highlightSelected) 64 | console.log(`\n${selected + 1}/${items.length}`); 65 | } 66 | 67 | function next(steps = 1) { 68 | selected += steps; 69 | if (selected > items.length - 1) { 70 | selected = steps > 1 ? items.length - 1 : 0; 71 | } 72 | } 73 | 74 | function prev(steps = 1) { 75 | selected -= steps; 76 | if (selected < 0) selected = steps > 1 ? 0 : items.length - 1; 77 | } 78 | 79 | function getSelected() { 80 | return items[selected]; 81 | } 82 | 83 | function getNext() { 84 | let next = selected + 1; 85 | if (next > items.length - 1) { 86 | next = 0; 87 | } 88 | return items[next]; 89 | } 90 | 91 | function getPrev() { 92 | let prev = selected - 1; 93 | if (prev < 0) { 94 | prev = items.length - 1; 95 | } 96 | return items[prev]; 97 | } 98 | 99 | function select(id?: number) { 100 | const item = items[id || selected]; 101 | onSelect(item); 102 | } 103 | 104 | return { 105 | getSelected, 106 | getNext, 107 | getPrev, 108 | select, 109 | next, 110 | prev, 111 | render, 112 | setItems, 113 | getId, 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export function getTitles(x: number) { 2 | return Array.from(Array(x)).map((_, i) => ({ 3 | fileName: `title${i}.zip`, 4 | title: `title${i}`, 5 | date: "none", 6 | size: 20, 7 | })); 8 | } 9 | -------------------------------------------------------------------------------- /src/screen.ts: -------------------------------------------------------------------------------- 1 | export function createScreen(width: number = 80, height: number = 44) { 2 | const clearScreen = [...Array(height).fill("")].join("\n"); 3 | 4 | function pad(str: string, amount: number) { 5 | return space(amount) + str; 6 | } 7 | 8 | function space(amount: number) { 9 | return Array.from(Array(amount)).fill(" ").join(""); 10 | } 11 | 12 | function centerText(str: string) { 13 | const amount = Math.floor((width - str.length) / 2); 14 | return pad(str, amount); 15 | } 16 | 17 | type SpreadPart = { 18 | text: string; 19 | color: (input: string) => string; 20 | }; 21 | 22 | type SpreadOpts = { 23 | left: SpreadPart; 24 | center: SpreadPart; 25 | right: SpreadPart; 26 | }; 27 | 28 | function spread2({ left, right }: Omit) { 29 | const amount = width - left.text.length - right.text.length; 30 | 31 | return `${left.color(left.text)}${space(amount)}${right.color(right.text)}`; 32 | } 33 | 34 | function spread3({ left, center, right }: spreadOpts) { 35 | const spaceToCenter = 36 | Math.floor((width - center.text.length) / 2) - left.text.length; 37 | 38 | const leftPart = left.text; 39 | const centerPart = pad(center.text, spaceToCenter); 40 | 41 | const spaceFromCenterPart = 42 | width - leftPart.length - centerPart.length - right.text.length; 43 | const rightPart = pad(right.text, spaceFromCenterPart); 44 | 45 | return `${left.color(leftPart)}${center.color(centerPart)}${right.color( 46 | rightPart 47 | )}`; 48 | } 49 | 50 | function right(str: string, padding: number = 0) { 51 | const padLeft = width - str.length - padding; 52 | return pad(str, padLeft); 53 | } 54 | 55 | /** @remark currently only one line */ 56 | function centerTextVert(str: string) { 57 | const text = centerText(str); 58 | const padding = [...Array(Math.floor(height / 2)).fill("")].join("\n"); 59 | return `${padding} 60 | ${text} 61 | ${padding}`; 62 | } 63 | 64 | function clear() { 65 | console.log(clearScreen); 66 | } 67 | 68 | return { centerText, centerTextVert, clear, right, spread2, spread3 }; 69 | } 70 | -------------------------------------------------------------------------------- /src/services/archive.service.ts: -------------------------------------------------------------------------------- 1 | import { parseHTML } from "linkedom"; 2 | import { getTitles } from "../mocks"; 3 | import { fetchProgress } from "../fetch"; 4 | 5 | type Collection = { 6 | title: string; 7 | archiveName: string; 8 | }; 9 | 10 | const mock = false; 11 | 12 | export async function fetchArchiveList(archiveName: string) { 13 | if (mock) { 14 | return getTitles(50); 15 | } 16 | 17 | return fetch(`https://archive.org/download/${archiveName}`) 18 | .then((res) => { 19 | if (!res.ok) { 20 | throw new Error("not found"); 21 | } 22 | return res.text(); 23 | }) 24 | .then((data) => { 25 | const dom = parseHTML(data); 26 | 27 | const rows = dom.document.querySelectorAll( 28 | ".directory-listing-table tbody tr" 29 | ); 30 | 31 | const titles = rows 32 | .filter((_, i) => i > 0) 33 | .map((row) => { 34 | const cells = row.querySelectorAll("td"); 35 | const fileName = cells[0].querySelector("a").href; 36 | return [ 37 | fileName, 38 | ...cells.map((cell) => 39 | cell.textContent.replace("(View Contents)", "") 40 | ), 41 | ]; 42 | }) 43 | .map((title) => ({ 44 | fileName: title[0], 45 | title: title[1], 46 | date: title[2], 47 | size: title[3], 48 | })); 49 | 50 | return titles; 51 | }); 52 | } 53 | 54 | export async function download( 55 | collection: Collection, 56 | fileName: string, 57 | folder: string, 58 | { onDownloadStart, onProgress } 59 | ) { 60 | const url = `https://archive.org/download/${collection.archiveName}/${fileName}`; 61 | 62 | const blob = await fetchProgress(url, { 63 | onDownloadStart, 64 | onProgress, 65 | }).then((res) => res.blob()); 66 | 67 | const buffer = await new Response(blob).arrayBuffer(); 68 | Switch.mkdirSync(`sdmc:/${folder}`); 69 | Switch.writeFileSync(`sdmc:/${folder}/${fileName}`, buffer); 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/notification.ts: -------------------------------------------------------------------------------- 1 | const createNotification = () => { 2 | let message: string[] = []; 3 | let timeoutId = 0; 4 | 5 | function show(time: number, ...msg: string[]) { 6 | message = msg; 7 | 8 | clearTimeout(timeoutId); 9 | timeoutId = setTimeout(() => { 10 | message = []; 11 | }, time); 12 | } 13 | 14 | function toString() { 15 | return message.length ? message.join(", ") : ""; 16 | } 17 | 18 | return { 19 | show, 20 | toString, 21 | }; 22 | }; 23 | 24 | export const notification = createNotification(); 25 | -------------------------------------------------------------------------------- /src/utils/progress-bar.ts: -------------------------------------------------------------------------------- 1 | export function progressBar(value: number, size: number = 20) { 2 | const fill = Math.floor(size * value); 3 | 4 | const filled = Array.from(Array(fill)).reduce((acc) => (acc += "#"), ""); 5 | const empty = Array.from(Array(size - fill)).reduce( 6 | (acc) => (acc += "_"), 7 | "" 8 | ); 9 | 10 | return `[${filled}${empty}]`; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "moduleResolution": "node", 5 | "noEmit": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "types": [ 10 | "nxjs-runtime" 11 | ] 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ] 16 | } 17 | --------------------------------------------------------------------------------