diff --git a/app/global.css b/app/global.css index 9eaaf5f..b61e0bb 100644 --- a/app/global.css +++ b/app/global.css @@ -24,7 +24,9 @@ body { flex-direction: column; margin: 0; padding: 0; - min-height: 100vh; + height: 100vh; + max-height: 100vh; + overflow: hidden; background-color: #699697; background-image: url("../public/noise.png"), linear-gradient(to bottom, #5be9ee 0%, #1e4172 100%); diff --git a/docs/404.html b/docs/404.html index f6aa109..942c2df 100644 --- a/docs/404.html +++ b/docs/404.html @@ -1 +1 @@ -404: This page could not be found.VL2 Forge

404

This page could not be found.

\ No newline at end of file +404: This page could not be found.VL2 Forge

404

This page could not be found.

\ No newline at end of file diff --git a/docs/404/index.html b/docs/404/index.html index f6aa109..942c2df 100644 --- a/docs/404/index.html +++ b/docs/404/index.html @@ -1 +1 @@ -404: This page could not be found.VL2 Forge

404

This page could not be found.

\ No newline at end of file +404: This page could not be found.VL2 Forge

404

This page could not be found.

\ No newline at end of file diff --git a/docs/_next/static/mP5hGUGSmu-p0M7gsH5Gb/_buildManifest.js b/docs/_next/static/be_WUnb__0cQ-xWVpCmQ8/_buildManifest.js similarity index 100% rename from docs/_next/static/mP5hGUGSmu-p0M7gsH5Gb/_buildManifest.js rename to docs/_next/static/be_WUnb__0cQ-xWVpCmQ8/_buildManifest.js diff --git a/docs/_next/static/mP5hGUGSmu-p0M7gsH5Gb/_ssgManifest.js b/docs/_next/static/be_WUnb__0cQ-xWVpCmQ8/_ssgManifest.js similarity index 100% rename from docs/_next/static/mP5hGUGSmu-p0M7gsH5Gb/_ssgManifest.js rename to docs/_next/static/be_WUnb__0cQ-xWVpCmQ8/_ssgManifest.js diff --git a/docs/index.html b/docs/index.html index 08c30fa..c09dc34 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1 +1 @@ -VL2 Forge \ No newline at end of file +VL2 Forge \ No newline at end of file diff --git a/docs/index.txt b/docs/index.txt index d049f11..d8de5b7 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -1,6 +1,6 @@ 2:I[9330,["51","static/chunks/795d4814-53044e1cf9e42373.js","212","static/chunks/59650de3-d12eb82ab0e85613.js","516","static/chunks/f7333993-8f32bf8e5c1e3f32.js","240","static/chunks/53c13509-17317f399c482fd7.js","699","static/chunks/8e1d74a4-2e60cc37d7b04363.js","634","static/chunks/634-d90cc75f8d4e4c06.js","931","static/chunks/app/page-65a64361088201e6.js"],"Forge"] 3:I[4707,[],""] 4:I[6423,[],""] -0:["mP5hGUGSmu-p0M7gsH5Gb",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{}],[["$","link","0",{"rel":"stylesheet","href":"/vl2-forge/_next/static/css/1febfadc1a5e3d46.css","precedence":"next","crossOrigin":"$undefined"}]]],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/vl2-forge/_next/static/css/9805353de4b13899.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","className":"__variable_6dd175 __variable_60c549 __variable_b97ccf","children":["$","body",null,{"children":["$","$L3",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L5",null]]]] +0:["be_WUnb__0cQ-xWVpCmQ8",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{}],[["$","link","0",{"rel":"stylesheet","href":"/vl2-forge/_next/static/css/1febfadc1a5e3d46.css","precedence":"next","crossOrigin":"$undefined"}]]],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/vl2-forge/_next/static/css/9805353de4b13899.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","className":"__variable_6dd175 __variable_60c549 __variable_b97ccf","children":["$","body",null,{"children":["$","$L3",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L5",null]]]] 5:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"VL2 Forge"}],["$","meta","3",{"name":"description","content":"Create .vl2 files for Tribes 2"}],["$","meta","4",{"name":"next-size-adjust"}]] 1:null diff --git a/package-lock.json b/package-lock.json index 7107a98..56e2ba9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "vl2-forge", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { "file-saver": "^2.0.5", "jszip": "^3.10.1", @@ -21,7 +21,7 @@ "devDependencies": { "@types/node": "22.7.5", "@types/react": "18.3.11", - "tshy": "^3.0.2", + "rimraf": "^6.0.1", "typescript": "^5.6.3" } }, @@ -263,20 +263,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/attr-accept": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.4.tgz", @@ -293,19 +279,6 @@ "dev": true, "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -316,19 +289,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -360,44 +320,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -484,19 +406,6 @@ "node": ">= 12" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -514,21 +423,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/glob": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", @@ -553,19 +447,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -584,29 +465,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -617,29 +475,6 @@ "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -750,22 +585,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -834,16 +653,6 @@ } } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -899,32 +708,6 @@ "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "license": "ISC" }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/polite-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/polite-json/-/polite-json-5.0.0.tgz", - "integrity": "sha512-OLS/0XeUAcE8a2fdwemNja+udKgXNnY6yKVIXqAD2zVRx1KvY6Ato/rZ2vdzbxqYwPW0u6SCNC/bAMPNzpzxbw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -1042,36 +825,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve-import": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-import/-/resolve-import-2.0.0.tgz", - "integrity": "sha512-jpKjLibLuc8D1XEV2+7zb0aqN7I8d12u89g/v6IsgCzdVlccMQJq4TKkPw5fbhHdxhm7nbVtN+KvOTnjFf+nEA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "glob": "^11.0.0", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -1302,68 +1055,6 @@ } } }, - "node_modules/sync-content": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-2.0.1.tgz", - "integrity": "sha512-NI1mo514yFhr8pV/5Etvgh+pSBUIpoAKoiBIUwALVlQQNAwb40bTw8hhPFaip/dvv0GhpHVOq0vq8iY02ppLTg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "glob": "^11.0.0", - "mkdirp": "^3.0.1", - "path-scurry": "^2.0.0", - "rimraf": "^6.0.0", - "tshy": "^3.0.0" - }, - "bin": { - "sync-content": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tshy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tshy/-/tshy-3.0.2.tgz", - "integrity": "sha512-8GkWnAfmNXxl8iDTZ1o2H4jdaj9H7HeDKkr5qd0ZhQBCNA41D3xqTyg2Ycs51VCfmjJ5e+0v9AUmD6ylAI9Bgw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "chalk": "^5.3.0", - "chokidar": "^3.6.0", - "foreground-child": "^3.1.1", - "minimatch": "^10.0.0", - "mkdirp": "^3.0.1", - "polite-json": "^5.0.0", - "resolve-import": "^2.0.0", - "rimraf": "^6.0.0", - "sync-content": "^2.0.1", - "typescript": "^5.5.3", - "walk-up-path": "^4.0.0" - }, - "bin": { - "tshy": "dist/esm/index.js" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", @@ -1397,16 +1088,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/walk-up-path": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", - "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index baff450..a271ee8 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,11 @@ "main": "index.js", "scripts": { "dev": "next dev", - "build": "npm run build:pages", - "build:pages": "next build && touch docs/.nojekyll" + "build": "next build && touch docs/.nojekyll", + "clean": "rimraf .next docs", + "deploy": "npm run build && git add -f docs && git commit -m \"Deploy\" && git push", + "prebuild": "npm run clean", + "start": "next dev" }, "dependencies": { "file-saver": "^2.0.5", @@ -24,7 +27,7 @@ "devDependencies": { "@types/node": "22.7.5", "@types/react": "18.3.11", - "tshy": "^3.0.2", + "rimraf": "^6.0.1", "typescript": "^5.6.3" } } diff --git a/public/fire1.gif b/public/fire1.gif new file mode 100644 index 0000000..f3a72bb Binary files /dev/null and b/public/fire1.gif differ diff --git a/src/DownloadForm.module.css b/src/DownloadForm.module.css new file mode 100644 index 0000000..a572afa --- /dev/null +++ b/src/DownloadForm.module.css @@ -0,0 +1,76 @@ +.DownloadForm { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 12px; + margin: 0 auto; +} + +.NameInput { + position: relative; + font-size: 18px; + font-family: var(--font-lora); + font-weight: 500; + font-style: italic; + line-height: 1.25; +} + +.NameInput input { + --dark-bevel: rgb(37, 34, 29); + --light-bevel: rgb(108, 101, 88); + display: block; + min-width: 240px; + max-width: 100%; + min-height: 46px; + background: #fff; + border-top: 3px solid var(--dark-bevel); + border-left: 3px solid var(--dark-bevel); + border-bottom: 3px solid var(--light-bevel); + border-right: 3px solid var(--light-bevel); + color: #333; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + font-style: inherit; + line-height: inherit; + padding: 2px 50px 2px 0; + text-indent: 10px; +} + +.NameInput::after { + display: grid; + place-content: center; + content: ".vl2"; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 50px; + color: #999; +} + +.DownloadButton { + border: 0; + margin: 0; + padding: 0 12px 0 12px; + min-width: 128px; + min-height: 46px; + font-family: var(--font-alagard), monospace; + font-size: 24px; + font-style: normal; + font-weight: normal; + line-height: 1; + border-radius: 2px; + background: url("../public/button.png") transparent; + background-repeat: no-repeat; + background-size: 150% 190%; + background-position: 50% 50%; + border-top: 3px solid rgba(255, 145, 105, 0.5); + border-left: 3px solid rgba(255, 178, 150, 0.5); + border-right: 3px solid rgba(36, 14, 14, 0.7); + border-bottom: 3px solid rgba(36, 14, 14, 0.7); + color: #ffffff; + text-shadow: 0 0 5px rgba(255, 226, 82, 0.358), 2px 3px 0 rgba(0, 0, 0, 0.5); + cursor: pointer; +} diff --git a/src/DownloadForm.tsx b/src/DownloadForm.tsx new file mode 100644 index 0000000..2298a6e --- /dev/null +++ b/src/DownloadForm.tsx @@ -0,0 +1,41 @@ +import { createZipFile, saveZipFile } from "./utils"; +import styles from "./DownloadForm.module.css"; + +export function DownloadForm({ fileList }) { + return ( +
{ + event.preventDefault(); + const form = event.target as HTMLFormElement; + const fileName = form.elements["fileName"] as HTMLInputElement; + const name = fileName.value.trim(); + if (!name) { + window.alert("Name thy file."); + fileName.focus(); + } else if (!fileList.length) { + window.alert("Add some files!"); + } else { + const zip = createZipFile(fileList); + await saveZipFile(zip, `${name}.vl2`); + } + }} + > +
+ { + if (/\.vl2$/i.test(event.target.value)) { + event.target.value = event.target.value.slice(0, -4); + } + }} + /> +
+ +
+ ); +} diff --git a/src/FileItem.module.css b/src/FileItem.module.css new file mode 100644 index 0000000..c7191bb --- /dev/null +++ b/src/FileItem.module.css @@ -0,0 +1,50 @@ +.File { + display: flex; + align-items: center; + gap: 12px; +} + +.IconContainer { + flex: 0 0 auto; + display: grid; + place-content: center; + width: 24px; + font-size: 22px; + color: rgba(255, 255, 255, 0.9); +} + +.PreviewIcon { + width: auto; + height: auto; + max-width: 100%; + max-height: 24px; + border: 1px solid black; +} + +.Path { + background: rgba(0, 0, 0, 0.3); + padding: 3px 8px; + border-radius: 8px; + user-select: none; +} + +.DeleteButton { + flex: 0 0 auto; + border: 0; + background: transparent; + display: grid; + place-content: center; + font-size: 24px; + margin: 0; + padding: 0; + color: rgba(0, 0, 0, 0.4); + cursor: pointer; +} + +.DeleteButton:hover { + color: rgb(255, 89, 89); +} + +.DeleteButton svg { + pointer-events: none; +} diff --git a/src/FileItem.tsx b/src/FileItem.tsx new file mode 100644 index 0000000..a832de1 --- /dev/null +++ b/src/FileItem.tsx @@ -0,0 +1,73 @@ +import { useMemo } from "react"; +import { FaTrashAlt } from "react-icons/fa"; +import { MdTerrain } from "react-icons/md"; +import { PiCastleTurretFill } from "react-icons/pi"; +import { LuScrollText } from "react-icons/lu"; +import { FaMapPin } from "react-icons/fa6"; +import { HiMiniSpeakerWave } from "react-icons/hi2"; +import { GrTask } from "react-icons/gr"; +import styles from "./FileItem.module.css"; + +export function FileItem({ file, onDelete, onRename }) { + const icon = useMemo(() => { + if (file.dataUri && file.type?.genericType === "image") { + return ( + + ); + } else if (file.type?.genericType === "audio") { + return ; + } else if (/\.cs$/i.test(file.path)) { + return ; + } else if (/\.mis$/i.test(file.path)) { + return ; + } else if (/\.dif$/i.test(file.path)) { + return ; + } else if (/\.ter$/i.test(file.path)) { + return ; + } else if (/\.spn$/i.test(file.path)) { + return ; + } + return null; + }, [file]); + + return ( +
+ {icon}{" "} + { + let newPath = window.prompt(`Rename file (${file.path}):`, file.path); + if (newPath) { + newPath = newPath + .trim() + .replace(/\/+/g, "/") + .replace(/^\//, "") + .replace(/\/$/, "") + .trim(); + if (newPath && newPath !== file.path) { + onRename(file.path, newPath); + } + } + }} + > + {file.path} + + +
+ ); +} diff --git a/src/Forge.module.css b/src/Forge.module.css index 846e73c..464d75d 100644 --- a/src/Forge.module.css +++ b/src/Forge.module.css @@ -1,24 +1,25 @@ .Forge { - flex: 1 0 auto; + flex: 1 1 100%; display: grid; grid-template-columns: auto 1fr; - grid-template-rows: 1fr; + grid-template-rows: 1fr auto; align-content: stretch; font-family: var(--font-departure), monospace; color: #fff; - text-shadow: 2px 1px 0 rgba(0, 0, 0, 1); padding: 20px; gap: 20px; + overflow: auto; } .Footer { position: relative; + grid-column: span 2; display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 12px; - padding: 40px; + padding: 32px 40px; background: rgba(59, 55, 49, 0.8); border-image-slice: 18 26 26 23; @@ -60,84 +61,15 @@ pointer-events: none; } -.Footer form { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 12px; - margin: 0 auto; -} - -.NameInput { - position: relative; - font-size: 18px; - font-family: var(--font-lora); - font-weight: 500; - font-style: italic; - line-height: 1.25; -} - -.NameInput input { - display: block; - min-width: 240px; - max-width: 100%; - min-height: 46px; - background: #fff; - color: #333; - font-size: inherit; - font-family: inherit; - font-weight: inherit; - font-style: inherit; - line-height: inherit; - padding: 2px 50px 2px 0; - text-indent: 10px; -} - -.NameInput::after { - display: grid; - place-content: center; - content: ".vl2"; - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 50px; - color: #999; -} - -.DownloadButton { - border: 0; - margin: 0; - padding: 0 12px 0 12px; - min-width: 128px; - min-height: 46px; - font-family: var(--font-alagard), monospace; - font-size: 24px; - font-style: normal; - font-weight: normal; - line-height: 1; - border-radius: 2px; - background: url("../public/button.png") transparent; - background-repeat: no-repeat; - background-size: 150% 190%; - background-position: 50% 50%; - border-top: 3px solid rgba(255, 145, 105, 0.5); - border-left: 3px solid rgba(255, 178, 150, 0.5); - border-right: 3px solid rgba(36, 14, 14, 0.7); - border-bottom: 3px solid rgba(36, 14, 14, 0.7); - color: #ffffff; - text-shadow: 0 0 5px rgba(255, 226, 82, 0.358), 2px 3px 0 rgba(0, 0, 0, 0.5); - cursor: pointer; -} - .ListArea { - display: flex; + position: relative; + display: grid; align-items: center; - justify-content: center; + justify-content: stretch; padding: 32px; background: rgba(2, 2, 84, 0.252); border-radius: 12px; + overflow: auto; } .FileList { @@ -148,10 +80,10 @@ margin: 0; padding: 0; list-style: none; + text-shadow: 2px 1px 0 rgba(0, 0, 0, 1); } .EmptyMessage { - align-self: center; text-align: center; text-wrap: balance; line-height: 1.75; @@ -191,57 +123,6 @@ gap: 40px; } -.File { - display: flex; - align-items: center; - gap: 12px; -} - -.DeleteButton { - flex: 0 0 auto; - border: 0; - background: transparent; - display: grid; - place-content: center; - font-size: 24px; - margin: 0; - padding: 0; - color: rgba(0, 0, 0, 0.4); - cursor: pointer; -} - -.DeleteButton:hover { - color: rgb(255, 89, 89); -} - -.DeleteButton svg { - pointer-events: none; -} - -.IconContainer { - flex: 0 0 auto; - display: grid; - place-content: center; - width: 24px; - font-size: 22px; - color: rgba(255, 255, 255, 0.9); -} - -.PreviewIcon { - width: auto; - height: auto; - max-width: 100%; - max-height: 24px; - border: 1px solid black; -} - -.Path { - background: rgba(0, 0, 0, 0.3); - padding: 3px 8px; - border-radius: 8px; - user-select: none; -} - .HeaderLink { display: none; margin-left: 0; @@ -268,7 +149,7 @@ @media (max-width: 767px) { .Forge { grid-template-columns: 1fr; - grid-template-rows: auto 1fr; + grid-template-rows: auto 1fr auto; padding: 12px; gap: 12px; } @@ -286,6 +167,10 @@ display: grid; } + .Footer { + grid-column: span 1; + } + .FooterLink { display: none; } diff --git a/src/Forge.tsx b/src/Forge.tsx index 53963c4..6fe9fa7 100644 --- a/src/Forge.tsx +++ b/src/Forge.tsx @@ -1,267 +1,22 @@ "use client"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useState, useTransition } from "react"; import { useDropzone } from "react-dropzone"; -import JSZip from "jszip"; -import { saveAs } from "file-saver"; import orderBy from "lodash.orderby"; -import { FaTrashAlt, FaGithub } from "react-icons/fa"; -import { MdCastle, MdTerrain } from "react-icons/md"; -import { LuScrollText } from "react-icons/lu"; -import { FaClipboardList, FaMapPin } from "react-icons/fa6"; -import { HiMiniSpeakerWave } from "react-icons/hi2"; +import { FaGithub } from "react-icons/fa"; +import { Loading } from "./Loading"; +import { FileItem } from "./FileItem"; +import { DownloadForm } from "./DownloadForm"; +import { handleInputFile } from "./utils"; import styles from "./Forge.module.css"; -import { base64ArrayBuffer } from "./utils"; - -function detectBestPath(path) { - const parts = path.split("/"); - let folder = ""; - let basename = ""; - if (parts.length > 1) { - folder = parts.slice(0, -1).join("/"); - basename = parts[parts.length - 1]; - } else { - folder = ""; - basename = parts[0]; - } - if (folder) { - return `${folder}/${basename}`; - } - if ( - /\.(l|m|h)(male|female|bioderm)\.png$/i.test(basename) || - /^(vehicle|weapon)_.+png$/i.test(basename) || - /^dcase\d\d\.png$/i.test(basename) - ) { - folder = "textures/skins"; - } else if (/\.(ter|spn)$/i.test(basename)) { - folder = "terrains"; - } else if (/\.mis$/i.test(basename)) { - folder = "missions"; - } else if (/\.dif$/i.test(basename)) { - folder = "interiors"; - } - if (folder) { - return `${folder}/${basename}`; - } else { - return basename; - } -} - -function detectFileType(file): FileType | null { - if (file.type) { - if (/^image\//i.test(file.type)) { - return { mimeType: file.type, genericType: "image" }; - } else if (/^audio\//i.test(file.type)) { - return { mimeType: file.type, genericType: "audio" }; - } - } - if (/\.png$/i.test(file.name)) { - return { mimeType: "image/png", genericType: "image" }; - } else if (/\.jpg$/i.test(file.name)) { - return { mimeType: "image/jpeg", genericType: "image" }; - } else if (/\.bmp$/i.test(file.name)) { - return { mimeType: "image/bmp", genericType: "image" }; - } else if (/\.webp$/i.test(file.name)) { - return { mimeType: "image/webp", genericType: "image" }; - } else if (/\.gif$/i.test(file.name)) { - return { mimeType: "image/gif", genericType: "image" }; - } else if (/\.tiff$/i.test(file.name)) { - return { mimeType: "image/tiff", genericType: "image" }; - } else if (/\.svg$/i.test(file.name)) { - return { mimeType: "image/svg+xml", genericType: "image" }; - } else if (/\.wav$/i.test(file.name)) { - return { mimeType: "audio/wav", genericType: "audio" }; - } else if (/\.mp3$/i.test(file.name)) { - return { mimeType: "audio/mpeg", genericType: "audio" }; - } - if (file.type) { - return { - mimeType: file.type, - genericType: null, - }; - } - return null; -} - -function FilePreview({ file, onDelete, onRename }) { - let icon = null; - if (file.dataUri && file.type?.genericType === "image") { - icon = ( - - ); - } else if (file.type?.genericType === "audio") { - icon = ; - } else if (/\.cs$/i.test(file.path)) { - icon = ; - } else if (/\.mis$/i.test(file.path)) { - icon = ; - } else if (/\.dif$/i.test(file.path)) { - icon = ; - } else if (/\.ter$/i.test(file.path)) { - icon = ; - } else if (/\.spn$/i.test(file.path)) { - icon = ; - } - return ( -
- {icon}{" "} - { - let newPath = window.prompt(`Rename file (${file.path}):`, file.path); - if (newPath) { - newPath = newPath - .trim() - .replace(/\/+/g, "/") - .replace(/^\//, "") - .replace(/\/$/, "") - .trim(); - if (newPath && newPath !== file.path) { - onRename(file.path, newPath); - } - } - }} - > - {file.path} - - -
- ); -} - -type FileType = { - mimeType: string; - genericType: string | null; -}; - -type FileEntry = { - path: string; - buffer: ArrayBuffer; - dataUri: string | null; - date: Date | null; - unixPermissions: string | number | null; - dosPermissions: number | null; - type: FileType | null; -}; - -async function handleZipFile(file) { - const zip = await JSZip.loadAsync(file); - const map = new Map(); - for (let path in zip.files) { - const fileObj = zip.files[path]; - if (!fileObj.dir) { - path = detectBestPath(path); - const buffer = await fileObj.async("arraybuffer"); - const fileEntry = { - path, - buffer: buffer, - dataUri: null, - date: fileObj.date, - unixPermissions: fileObj.unixPermissions, - dosPermissions: fileObj.dosPermissions, - type: detectFileType(fileObj), - }; - if ( - fileEntry.type?.genericType === "image" || - fileEntry.type?.genericType === "audio" - ) { - const base64String = await fileObj.async("base64"); - fileEntry.dataUri = `data:${fileEntry.type.mimeType};base64,${base64String}`; - } - map.set(path, fileEntry); - } - } - return map; -} - -async function handleOtherFile(file) { - const map = new Map(); - let path; - if (file.path) { - path = file.path; - if (path.startsWith("/")) { - path = path.slice(1); - } - } else if (file.name) { - path = file.name; - } else { - return map; - } - path = detectBestPath(path); - const buffer = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener("load", (event) => { - resolve(event.target.result as ArrayBuffer); - }); - reader.readAsArrayBuffer(file); - }); - const fileEntry = { - path, - buffer, - dataUri: null, - date: null, - unixPermissions: null, - dosPermissions: null, - type: detectFileType(file), - }; - if ( - fileEntry.type?.genericType === "image" || - fileEntry.type?.genericType === "audio" - ) { - const base64String = base64ArrayBuffer(buffer); - fileEntry.dataUri = `data:${fileEntry.type.mimeType};base64,${base64String}`; - } - map.set(path, fileEntry); - return map; -} - -async function handleInputFile(file) { - if (/\.(zip|vl2)$/i.test(file.name)) { - return handleZipFile(file); - } else { - return handleOtherFile(file); - } -} - -export function createZipFile(files: Array) { - const zip = new JSZip(); - for (const file of files) { - zip.file(file.path, file.buffer, { - date: file.date, - dosPermissions: file.dosPermissions, - unixPermissions: file.unixPermissions, - }); - } - return zip; -} - -export async function saveZipFile(zip: JSZip, name: string) { - const blob = await zip.generateAsync({ - type: "blob", - mimeType: "application/octet-stream", - }); - saveAs(blob, name); -} export function Forge() { + const [isPending, startTransition] = useTransition(); + const [loadingCount, setLoadingCount] = useState(0); const [actionLog, setActionLog] = useState(() => []); const [files, setFiles] = useState(() => new Map()); const onDrop = useCallback(async (acceptedFiles) => { + setLoadingCount((count) => count + 1); const actionLog = []; const finalMap = new Map(); const allFiles: Array> = await Promise.all( @@ -278,13 +33,16 @@ export function Forge() { finalMap.set(path, file); }); }); - setFiles((prevMap) => { - return new Map([ - ...Array.from(prevMap.entries()), - ...Array.from(finalMap.entries()), - ]); + startTransition(() => { + setFiles((prevMap) => { + return new Map([ + ...Array.from(prevMap.entries()), + ...Array.from(finalMap.entries()), + ]); + }); + setActionLog((prevLog) => [...prevLog, ...actionLog]); }); - setActionLog((prevLog) => [...prevLog, ...actionLog]); + setLoadingCount((count) => count - 1); }, []); const { getRootProps, getInputProps, open, isDragActive } = useDropzone({ noClick: true, @@ -331,49 +89,51 @@ export function Forge() { }); }, []); + const showLoading = isPending || loadingCount > 0; + return ( - <> -
-
- - - - VL2 Forge - {addButton} -
- -
- {fileList.length ? ( -
    - {fileList.map((file) => { - return ( -
  • - -
  • - ); - })} -
- ) : ( -
- Drop files onto the page or press the add button. No need to - extract existing .vl2 files first – just drop ‘em in and it’ll do - that for you! -
- )} -
-
+
+
+ + + + VL2 Forge + {addButton} +
+ +
+ {showLoading ? : null} + {fileList.length ? ( +
    + {fileList.map((file) => { + return ( +
  • + +
  • + ); + })} +
+ ) : showLoading ? null : ( +
+ Drop files onto the page or press the add button. No need to extract + existing .vl2 files first – just drop ‘em in and it’ll take care of + that! +
+ )} +
-
{ - event.preventDefault(); - const form = event.target as HTMLFormElement; - const fileName = form.elements["fileName"] as HTMLInputElement; - const name = fileName.value.trim(); - if (!name) { - window.alert("Name thy file."); - fileName.focus(); - } else if (!fileList.length) { - window.alert("Add some files!"); - } else { - const zip = createZipFile(fileList); - await saveZipFile(zip, `${name}.vl2`); - } - }} - > -
- { - if (/\.vl2$/i.test(event.target.value)) { - event.target.value = event.target.value.slice(0, -4); - } - }} - /> -
- -
+
- +
); } diff --git a/src/Loading.module.css b/src/Loading.module.css new file mode 100644 index 0000000..93a67e5 --- /dev/null +++ b/src/Loading.module.css @@ -0,0 +1,30 @@ +.LoadingBackdrop { + display: flex; + align-items: stretch; + position: absolute; + top: 50%; + left: 50%; + width: 400px; + height: 120px; + transform: translate(-50%, -50%); + background: url("../public/fire1.gif"); + background-repeat: no-repeat; + background-size: 100% 100%; + border-radius: 10px; + z-index: 2; + text-align: center; + overflow: hidden; +} + +.LoadingText { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + color: rgb(232, 194, 154); + background: rgba(0, 0, 0, 0.95); + mix-blend-mode: hard-light; + font-family: var(--font-alagard); + font-size: 40px; + text-shadow: 0 -3px 10px rgb(251, 82, 30); +} diff --git a/src/Loading.tsx b/src/Loading.tsx new file mode 100644 index 0000000..f63a7a3 --- /dev/null +++ b/src/Loading.tsx @@ -0,0 +1,9 @@ +import styles from "./Loading.module.css"; + +export function Loading() { + return ( +
+
Loading…
+
+ ); +} diff --git a/src/utils.ts b/src/utils.ts index 965f9fa..5406413 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,21 @@ +import JSZip from "jszip"; +import { saveAs } from "file-saver"; + +export type FileType = { + mimeType: string; + genericType: string | null; +}; + +export type FileEntry = { + path: string; + buffer: ArrayBuffer; + dataUri: string | null; + date: Date | null; + unixPermissions: string | number | null; + dosPermissions: number | null; + type: FileType | null; +}; + // Converts an ArrayBuffer directly to base64, without any intermediate 'convert to string then // use window.btoa' step. According to my tests, this appears to be a faster approach: // http://jsperf.com/encoding-xhr-image-data/5 @@ -47,3 +65,172 @@ export function base64ArrayBuffer(arrayBuffer) { } return base64; } + +export function detectBestPath(path) { + const parts = path.split("/"); + let folder = ""; + let basename = ""; + if (parts.length > 1) { + folder = parts.slice(0, -1).join("/"); + basename = parts[parts.length - 1]; + } else { + folder = ""; + basename = parts[0]; + } + if (folder) { + return `${folder}/${basename}`; + } + if ( + /\.(l|m|h)(male|female|bioderm)\.png$/i.test(basename) || + /^(vehicle|weapon)_.+png$/i.test(basename) || + /^dcase\d\d\.png$/i.test(basename) + ) { + folder = "textures/skins"; + } else if (/\.(ter|spn)$/i.test(basename)) { + folder = "terrains"; + } else if (/\.mis$/i.test(basename)) { + folder = "missions"; + } else if (/\.dif$/i.test(basename)) { + folder = "interiors"; + } + if (folder) { + return `${folder}/${basename}`; + } else { + return basename; + } +} + +export function detectFileType(file): FileType | null { + if (file.type) { + if (/^image\//i.test(file.type)) { + return { mimeType: file.type, genericType: "image" }; + } else if (/^audio\//i.test(file.type)) { + return { mimeType: file.type, genericType: "audio" }; + } + } + if (/\.png$/i.test(file.name)) { + return { mimeType: "image/png", genericType: "image" }; + } else if (/\.jpg$/i.test(file.name)) { + return { mimeType: "image/jpeg", genericType: "image" }; + } else if (/\.bmp$/i.test(file.name)) { + return { mimeType: "image/bmp", genericType: "image" }; + } else if (/\.webp$/i.test(file.name)) { + return { mimeType: "image/webp", genericType: "image" }; + } else if (/\.gif$/i.test(file.name)) { + return { mimeType: "image/gif", genericType: "image" }; + } else if (/\.tiff$/i.test(file.name)) { + return { mimeType: "image/tiff", genericType: "image" }; + } else if (/\.svg$/i.test(file.name)) { + return { mimeType: "image/svg+xml", genericType: "image" }; + } else if (/\.wav$/i.test(file.name)) { + return { mimeType: "audio/wav", genericType: "audio" }; + } else if (/\.mp3$/i.test(file.name)) { + return { mimeType: "audio/mpeg", genericType: "audio" }; + } + if (file.type) { + return { + mimeType: file.type, + genericType: null, + }; + } + return null; +} + +export async function handleZipFile(file) { + const zip = await JSZip.loadAsync(file); + const map = new Map(); + for (let path in zip.files) { + const fileObj = zip.files[path]; + if (!fileObj.dir) { + path = detectBestPath(path); + const buffer = await fileObj.async("arraybuffer"); + const fileEntry = { + path, + buffer: buffer, + dataUri: null, + date: fileObj.date, + unixPermissions: fileObj.unixPermissions, + dosPermissions: fileObj.dosPermissions, + type: detectFileType(fileObj), + }; + if ( + fileEntry.type?.genericType === "image" || + fileEntry.type?.genericType === "audio" + ) { + const base64String = await fileObj.async("base64"); + fileEntry.dataUri = `data:${fileEntry.type.mimeType};base64,${base64String}`; + } + map.set(path, fileEntry); + } + } + return map; +} + +export async function handleOtherFile(file) { + const map = new Map(); + let path; + if (file.path) { + path = file.path; + if (path.startsWith("/")) { + path = path.slice(1); + } + } else if (file.name) { + path = file.name; + } else { + return map; + } + path = detectBestPath(path); + const buffer = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener("load", (event) => { + resolve(event.target.result as ArrayBuffer); + }); + reader.readAsArrayBuffer(file); + }); + const fileEntry = { + path, + buffer, + dataUri: null, + date: null, + unixPermissions: null, + dosPermissions: null, + type: detectFileType(file), + }; + if ( + fileEntry.type?.genericType === "image" || + fileEntry.type?.genericType === "audio" + ) { + const base64String = base64ArrayBuffer(buffer); + fileEntry.dataUri = `data:${fileEntry.type.mimeType};base64,${base64String}`; + } + map.set(path, fileEntry); + return map; +} + +export async function handleInputFile(file) { + if (/\.(zip|vl2)$/i.test(file.name)) { + return handleZipFile(file); + } else { + return handleOtherFile(file); + } +} + +export function createZipFile(files: Array) { + const zip = new JSZip(); + for (const file of files) { + zip.file(file.path, file.buffer, { + date: file.date, + dosPermissions: file.dosPermissions, + unixPermissions: file.unixPermissions, + }); + } + return zip; +} + +export async function saveZipFile(zip: JSZip, name: string) { + const blob = await zip.generateAsync({ + type: "blob", + mimeType: "application/octet-stream", + }); + saveAs(blob, name); +}