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 (
+
+ );
+}
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}
+
+ {
+ onDelete(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}
-
- {
- onDelete(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 (
- <>
-
-
-
-
-
-
- {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!
-
- )}
-
-
+
+
+
+
+
+
+ {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!
+
+ )}
+
- >
+
);
}
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 (
+
+ );
+}
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);
+}