├── .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 |
--------------------------------------------------------------------------------