mirror of
https://github.com/exogen/vl2-forge.git
synced 2026-01-19 11:34:45 +00:00
Add loading indicator, improve icons, improve layout
This commit is contained in:
parent
41a3753623
commit
fa0b047e19
|
|
@ -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%);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
|
|||
323
package-lock.json
generated
323
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
public/fire1.gif
Normal file
BIN
public/fire1.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
76
src/DownloadForm.module.css
Normal file
76
src/DownloadForm.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
41
src/DownloadForm.tsx
Normal file
41
src/DownloadForm.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { createZipFile, saveZipFile } from "./utils";
|
||||
import styles from "./DownloadForm.module.css";
|
||||
|
||||
export function DownloadForm({ fileList }) {
|
||||
return (
|
||||
<form
|
||||
className={styles.DownloadForm}
|
||||
onSubmit={async (event) => {
|
||||
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`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.NameInput}>
|
||||
<input
|
||||
name="fileName"
|
||||
type="text"
|
||||
placeholder="name thy file"
|
||||
onChange={(event) => {
|
||||
if (/\.vl2$/i.test(event.target.value)) {
|
||||
event.target.value = event.target.value.slice(0, -4);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className={styles.DownloadButton}>
|
||||
Download
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
50
src/FileItem.module.css
Normal file
50
src/FileItem.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
73
src/FileItem.tsx
Normal file
73
src/FileItem.tsx
Normal file
|
|
@ -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 (
|
||||
<img
|
||||
className={styles.PreviewIcon}
|
||||
src={file.dataUri}
|
||||
width={24}
|
||||
alt=""
|
||||
/>
|
||||
);
|
||||
} else if (file.type?.genericType === "audio") {
|
||||
return <HiMiniSpeakerWave />;
|
||||
} else if (/\.cs$/i.test(file.path)) {
|
||||
return <LuScrollText />;
|
||||
} else if (/\.mis$/i.test(file.path)) {
|
||||
return <GrTask />;
|
||||
} else if (/\.dif$/i.test(file.path)) {
|
||||
return <PiCastleTurretFill />;
|
||||
} else if (/\.ter$/i.test(file.path)) {
|
||||
return <MdTerrain />;
|
||||
} else if (/\.spn$/i.test(file.path)) {
|
||||
return <FaMapPin />;
|
||||
}
|
||||
return null;
|
||||
}, [file]);
|
||||
|
||||
return (
|
||||
<div className={styles.File}>
|
||||
<span className={styles.IconContainer}>{icon}</span>{" "}
|
||||
<span
|
||||
className={styles.Path}
|
||||
onDoubleClick={() => {
|
||||
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}
|
||||
</span>
|
||||
<button
|
||||
className={styles.DeleteButton}
|
||||
type="button"
|
||||
aria-label="Delete"
|
||||
title="Delete"
|
||||
onClick={(event) => {
|
||||
onDelete(file.path);
|
||||
}}
|
||||
>
|
||||
<FaTrashAlt />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
400
src/Forge.tsx
400
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 = (
|
||||
<img
|
||||
className={styles.PreviewIcon}
|
||||
src={file.dataUri}
|
||||
width={24}
|
||||
alt=""
|
||||
/>
|
||||
);
|
||||
} else if (file.type?.genericType === "audio") {
|
||||
icon = <HiMiniSpeakerWave />;
|
||||
} else if (/\.cs$/i.test(file.path)) {
|
||||
icon = <LuScrollText />;
|
||||
} else if (/\.mis$/i.test(file.path)) {
|
||||
icon = <FaClipboardList />;
|
||||
} else if (/\.dif$/i.test(file.path)) {
|
||||
icon = <MdCastle />;
|
||||
} else if (/\.ter$/i.test(file.path)) {
|
||||
icon = <MdTerrain />;
|
||||
} else if (/\.spn$/i.test(file.path)) {
|
||||
icon = <FaMapPin />;
|
||||
}
|
||||
return (
|
||||
<div className={styles.File}>
|
||||
<span className={styles.IconContainer}>{icon}</span>{" "}
|
||||
<span
|
||||
className={styles.Path}
|
||||
onDoubleClick={() => {
|
||||
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}
|
||||
</span>
|
||||
<button
|
||||
className={styles.DeleteButton}
|
||||
type="button"
|
||||
aria-label="Delete"
|
||||
title="Delete"
|
||||
onClick={(event) => {
|
||||
onDelete(file.path);
|
||||
}}
|
||||
>
|
||||
<FaTrashAlt />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<string, FileEntry>();
|
||||
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<ArrayBuffer>((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<FileEntry>) {
|
||||
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<Map<string, any>> = 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 (
|
||||
<>
|
||||
<section className={styles.Forge} {...getRootProps()}>
|
||||
<header className={styles.Header}>
|
||||
<a
|
||||
className={styles.HeaderLink}
|
||||
href="https://github.com/exogen/vl2-forge"
|
||||
>
|
||||
<FaGithub aria-label="GitHub" />
|
||||
</a>
|
||||
<img
|
||||
width={210}
|
||||
height={188}
|
||||
src="/vl2-forge/logo-md.png"
|
||||
alt="VL2 Forge"
|
||||
/>
|
||||
{addButton}
|
||||
</header>
|
||||
<input {...getInputProps()} />
|
||||
<div className={styles.ListArea}>
|
||||
{fileList.length ? (
|
||||
<ul className={styles.FileList}>
|
||||
{fileList.map((file) => {
|
||||
return (
|
||||
<li key={file.path}>
|
||||
<FilePreview
|
||||
file={file}
|
||||
onDelete={handleDelete}
|
||||
onRename={handleRename}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<div className={styles.EmptyMessage}>
|
||||
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!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<section className={styles.Forge} {...getRootProps()}>
|
||||
<header className={styles.Header}>
|
||||
<a
|
||||
className={styles.HeaderLink}
|
||||
href="https://github.com/exogen/vl2-forge"
|
||||
>
|
||||
<FaGithub aria-label="GitHub" />
|
||||
</a>
|
||||
<img
|
||||
className={styles.Logo}
|
||||
width={210}
|
||||
height={188}
|
||||
src="/vl2-forge/logo-md.png"
|
||||
alt="VL2 Forge"
|
||||
/>
|
||||
{addButton}
|
||||
</header>
|
||||
<input {...getInputProps()} />
|
||||
<div className={styles.ListArea}>
|
||||
{showLoading ? <Loading /> : null}
|
||||
{fileList.length ? (
|
||||
<ul className={styles.FileList}>
|
||||
{fileList.map((file) => {
|
||||
return (
|
||||
<li key={file.path}>
|
||||
<FileItem
|
||||
file={file}
|
||||
onDelete={handleDelete}
|
||||
onRename={handleRename}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : showLoading ? null : (
|
||||
<div className={styles.EmptyMessage}>
|
||||
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!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<footer className={styles.Footer}>
|
||||
<a
|
||||
className={styles.FooterLink}
|
||||
|
|
@ -381,40 +141,8 @@ export function Forge() {
|
|||
>
|
||||
<FaGithub aria-label="GitHub" />
|
||||
</a>
|
||||
<form
|
||||
onSubmit={async (event) => {
|
||||
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`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.NameInput}>
|
||||
<input
|
||||
name="fileName"
|
||||
type="text"
|
||||
placeholder="name thy file"
|
||||
onChange={(event) => {
|
||||
if (/\.vl2$/i.test(event.target.value)) {
|
||||
event.target.value = event.target.value.slice(0, -4);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className={styles.DownloadButton}>
|
||||
Download
|
||||
</button>
|
||||
</form>
|
||||
<DownloadForm fileList={fileList} />
|
||||
</footer>
|
||||
</>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
30
src/Loading.module.css
Normal file
30
src/Loading.module.css
Normal file
|
|
@ -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);
|
||||
}
|
||||
9
src/Loading.tsx
Normal file
9
src/Loading.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import styles from "./Loading.module.css";
|
||||
|
||||
export function Loading() {
|
||||
return (
|
||||
<div className={styles.LoadingBackdrop}>
|
||||
<div className={styles.LoadingText}>Loading…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
187
src/utils.ts
187
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<string, FileEntry>();
|
||||
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<ArrayBuffer>((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<FileEntry>) {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue