Initial commit
133
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
.tshy
|
||||||
32
app/global.css
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
:root {
|
||||||
|
--system-ui: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Box sizing rules */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent font size inflation */
|
||||||
|
html {
|
||||||
|
-moz-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #699697;
|
||||||
|
background-image: url("../public/noise.png"),
|
||||||
|
linear-gradient(to bottom, #5be9ee 0%, #1e4172 100%);
|
||||||
|
background-repeat: repeat, repeat;
|
||||||
|
}
|
||||||
19
app/layout.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { departureMono } from "../src/fonts";
|
||||||
|
import "./global.css";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "VL2 Forge",
|
||||||
|
description: "Create .vl2 files for Tribes 2",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className={departureMono.variable}>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
app/page.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Forge } from "../src/Forge";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <Forge />;
|
||||||
|
}
|
||||||
0
docs/.nojekyll
Normal file
1
docs/404.html
Normal file
1
docs/404/index.html
Normal file
BIN
docs/DepartureMono-Regular.otf
Normal file
BIN
docs/DepartureMono-Regular.woff
Normal file
BIN
docs/DepartureMono-Regular.woff2
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-7ba65e1336b92748.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
|
||||||
1
docs/_next/static/AaZG0PNUAl--iHAutmB3I/_ssgManifest.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|
||||||
2
docs/_next/static/chunks/117-1933c4264b72dbac.js
Normal file
1
docs/_next/static/chunks/634-d90cc75f8d4e4c06.js
Normal file
1
docs/_next/static/chunks/8e1d74a4-8e519121d6db0557.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[699],{9089:function(a,t,n){n.d(t,{AMf:function(){return c}});var u=n(6231);function c(a){return(0,u.w_)({tag:"svg",attr:{viewBox:"0 0 448 512"},child:[{tag:"path",attr:{d:"M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"},child:[]}]})(a)}}}]);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[409],{7589:function(e,t,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found/page",function(){return n(3634)}])},3634:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return s}}),n(7043);let i=n(7437);n(2265);let o={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"},l={display:"inline-block"},r={display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},d={fontSize:14,fontWeight:400,lineHeight:"49px",margin:0};function s(){return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("title",{children:"404: This page could not be found."}),(0,i.jsx)("div",{style:o,children:(0,i.jsxs)("div",{children:[(0,i.jsx)("style",{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)}}"}}),(0,i.jsx)("h1",{className:"next-error-h1",style:r,children:"404"}),(0,i.jsx)("div",{style:l,children:(0,i.jsx)("h2",{style:d,children:"This page could not be found."})})]})})]})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},function(e){e.O(0,[971,117,744],function(){return e(e.s=7589)}),_N_E=e.O()}]);
|
||||||
1
docs/_next/static/chunks/app/layout-3255e2bc046de7d7.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{4345:function(e,n,t){Promise.resolve().then(t.t.bind(t,2085,23)),Promise.resolve().then(t.t.bind(t,2528,23))},2528:function(){},2085:function(e){e.exports={style:{fontFamily:"'__departureMono_6dd175', '__departureMono_Fallback_6dd175'",fontWeight:400,fontStyle:"normal"},className:"__className_6dd175",variable:"__variable_6dd175"}}},function(e){e.O(0,[499,971,117,744],function(){return e(e.s=4345)}),_N_E=e.O()}]);
|
||||||
1
docs/_next/static/chunks/app/page-960543db4d1917f1.js
Normal file
1
docs/_next/static/chunks/fd9d1056-49cc888465909d37.js
Normal file
1
docs/_next/static/chunks/framework-f66176bb897dc684.js
Normal file
1
docs/_next/static/chunks/main-app-457f09ca7bd265de.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[744],{9783:function(e,n,t){Promise.resolve().then(t.t.bind(t,2846,23)),Promise.resolve().then(t.t.bind(t,9107,23)),Promise.resolve().then(t.t.bind(t,1060,23)),Promise.resolve().then(t.t.bind(t,4707,23)),Promise.resolve().then(t.t.bind(t,80,23)),Promise.resolve().then(t.t.bind(t,6423,23))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[971,117],function(){return n(4278),n(9783)}),_N_E=e.O()}]);
|
||||||
1
docs/_next/static/chunks/main-b7113e42ca1b091d.js
Normal file
1
docs/_next/static/chunks/pages/_app-72b849fbd24ac258.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{1597:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return u(8141)}])}},function(n){var _=function(_){return n(n.s=_)};n.O(0,[774,179],function(){return _(1597),_(7253)}),_N_E=n.O()}]);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{1981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(8529)}])}},function(n){n.O(0,[888,774,179],function(){return n(n.s=1981)}),_N_E=n.O()}]);
|
||||||
1
docs/_next/static/chunks/polyfills-42372ed130431b0a.js
Normal file
1
docs/_next/static/chunks/webpack-f1b6ebce872ed2e9.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(){"use strict";var e,t,n,r,o,u,i,c,f,a={},l={};function d(e){var t=l[e];if(void 0!==t)return t.exports;var n=l[e]={id:e,loaded:!1,exports:{}},r=!0;try{a[e].call(n.exports,n,n.exports,d),r=!1}finally{r&&delete l[e]}return n.loaded=!0,n.exports}d.m=a,e=[],d.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u<e.length;u++){for(var n=e[u][0],r=e[u][1],o=e[u][2],c=!0,f=0;f<n.length;f++)i>=o&&Object.keys(d.O).every(function(e){return d.O[e](n[f])})?n.splice(f--,1):(c=!1,o<i&&(i=o));if(c){e.splice(u--,1);var a=r();void 0!==a&&(t=a)}}return t},d.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return d.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},d.t=function(e,r){if(1&r&&(e=this(e)),8&r||"object"==typeof e&&e&&(4&r&&e.__esModule||16&r&&"function"==typeof e.then))return e;var o=Object.create(null);d.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach(function(t){u[t]=function(){return e[t]}});return u.default=function(){return e},d.d(o,u),o},d.d=function(e,t){for(var n in t)d.o(t,n)&&!d.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},d.f={},d.e=function(e){return Promise.all(Object.keys(d.f).reduce(function(t,n){return d.f[n](e,t),t},[]))},d.u=function(e){},d.miniCssF=function(e){},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="_N_E:",d.l=function(e,t,n,u){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var i,c,f=document.getElementsByTagName("script"),a=0;a<f.length;a++){var l=f[a];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,d.nc&&i.setAttribute("nonce",d.nc),i.setAttribute("data-webpack",o+n),i.src=d.tu(e)),r[e]=[t];var s=function(t,n){i.onerror=i.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(function(e){return e(n)}),t)return t(n)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),c&&document.head.appendChild(i)},d.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},d.tt=function(){return void 0===u&&(u={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(u=trustedTypes.createPolicy("nextjs#bundler",u))),u},d.tu=function(e){return d.tt().createScriptURL(e)},d.p="/vl2-forge/_next/",i={272:0,499:0,359:0},d.f.j=function(e,t){var n=d.o(i,e)?i[e]:void 0;if(0!==n){if(n)t.push(n[2]);else if(/^(272|359|499)$/.test(e))i[e]=0;else{var r=new Promise(function(t,r){n=i[e]=[t,r]});t.push(n[2]=r);var o=d.p+d.u(e),u=Error();d.l(o,function(t){if(d.o(i,e)&&(0!==(n=i[e])&&(i[e]=void 0),n)){var r=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;u.message="Loading chunk "+e+" failed.\n("+r+": "+o+")",u.name="ChunkLoadError",u.type=r,u.request=o,n[1](u)}},"chunk-"+e,e)}}},d.O.j=function(e){return 0===i[e]},c=function(e,t){var n,r,o=t[0],u=t[1],c=t[2],f=0;if(o.some(function(e){return 0!==i[e]})){for(n in u)d.o(u,n)&&(d.m[n]=u[n]);if(c)var a=c(d)}for(e&&e(t);f<o.length;f++)r=o[f],d.o(i,r)&&i[r]&&i[r][0](),i[r]=0;return d.O(a)},(f=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(c.bind(null,0)),f.push=c.bind(null,f.push.bind(f))}();
|
||||||
1
docs/_next/static/css/914a77656c3e670b.css
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.Forge_Forge__dDZFe{flex:1 0 auto;display:grid;grid-template-columns:auto 1fr;grid-template-rows:1fr;align-content:stretch;font-family:var(--font-departure),monospace;color:#fff;text-shadow:2px 1px 0 #000;padding:20px;grid-gap:20px;gap:20px}.Forge_Footer__ghw3O{position:relative;display:flex;align-items:center;justify-content:center;padding:40px;background:rgba(59,55,49,.8);border-image-slice:18 26 26 23;border-image-width:16px 16px 16px 16px;border-image-outset:0 0 0 0;border-image-repeat:round repeat;border-image-source:url(/vl2-forge/_next/static/media/border.d6506fa8.png);border-style:solid;box-shadow:0 0 8px rgba(0,0,0,.3),inset 0 0 12px rgba(0,0,0,.8)}.Forge_Footer__ghw3O:before{top:-3px;left:-3px;right:-3px;bottom:-3px;border-color:rgba(255,250,234,.4) hsla(0,0%,9%,.5) hsla(0,0%,9%,.5) rgba(255,250,234,.4);border-style:solid;border-width:4px 4px 5px}.Forge_Footer__ghw3O:after,.Forge_Footer__ghw3O:before{display:block;content:"";position:absolute;pointer-events:none}.Forge_Footer__ghw3O:after{top:10px;left:10px;right:10px;bottom:10px;border-color:hsla(0,0%,9%,.5) rgba(255,250,234,.4) rgba(255,250,234,.4) hsla(0,0%,9%,.5);border-style:solid;border-width:4px}.Forge_Footer__ghw3O form{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:12px}.Forge_NameInput__lpcsg{position:relative;font-size:18px;font-family:var(--system-ui);font-weight:400;font-style:italic;line-height:1}.Forge_NameInput__lpcsg 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 10px}.Forge_NameInput__lpcsg:after{display:grid;place-content:center;content:".vl2";position:absolute;top:0;right:0;bottom:0;width:50px;color:#999}.Forge_DownloadButton__CnFTn{border:0;margin:0;padding:0 0 1px;min-width:128px;min-height:46px;font-family:var(--font-departure),monospace;font-size:18px;font-style:inherit;font-weight:inherit;line-height:1;border-radius:2px;background:url(/vl2-forge/_next/static/media/button.69ed12fe.png) transparent;background-repeat:no-repeat;background-size:150% 190%;background-position:50% 50%;border-color:rgba(255,145,105,.5) rgba(36,14,14,.7) rgba(36,14,14,.7) rgba(255,178,150,.5);border-style:solid;border-width:3px;color:#fff;text-shadow:0 0 5px rgba(255,226,82,.358),2px 3px 0 rgba(0,0,0,.5);cursor:pointer}.Forge_ListArea__OpY_R{display:flex;align-items:center;justify-content:center;padding:32px;background:rgba(2,2,84,.252);border-radius:12px}.Forge_FileList__9JOyh{width:100%;display:flex;flex-direction:column;gap:6px;margin:0;padding:0;list-style:none}.Forge_EmptyMessage__Lrlud{align-self:center;text-align:center}.Forge_AddButton__09pXD{border:0;margin:0;padding:0 0 2px;min-width:48px;min-height:48px;font-family:var(--font-departure),monospace;font-size:40px;font-style:inherit;font-weight:inherit;line-height:1;border-radius:3px;background:url(/vl2-forge/_next/static/media/button.69ed12fe.png) transparent;background-repeat:no-repeat;background-size:150% 190%;background-position:50% 50%;border-color:rgba(255,145,105,.5) rgba(36,14,14,.7) rgba(36,14,14,.7) rgba(255,178,150,.5);border-style:solid;border-width:3px;color:#fff;text-shadow:0 0 5px rgba(255,226,82,.358),2px 3px 0 rgba(0,0,0,.5);cursor:pointer;vertical-align:middle}.Forge_Header__7t3Qc{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:40px}.Forge_File__Mn05Y{display:flex;align-items:center;gap:12px}.Forge_DeleteButton__Csfdg{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,.4);cursor:pointer}.Forge_DeleteButton__Csfdg:hover{color:#ff5959}.Forge_DeleteButton__Csfdg svg{pointer-events:none}.Forge_IconContainer__AgM_T{flex:0 0 auto;display:grid;place-content:center;width:24px}.Forge_PreviewIcon__HFERe{width:100%;height:auto;max-height:24px;border:1px solid #000}.Forge_Path__GZs81{background:rgba(0,0,0,.3);padding:3px 8px;border-radius:8px}@media (max-width:767px){.Forge_Forge__dDZFe{grid-template-columns:1fr;grid-template-rows:auto 1fr;padding:12px;gap:12px}.Forge_Header__7t3Qc{flex-direction:row}.Forge_Header__7t3Qc img{width:auto;max-height:96px}}
|
||||||
1
docs/_next/static/css/c50e9c221475fcb7.css
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
@font-face{font-family:__departureMono_6dd175;src:url(/vl2-forge/_next/static/media/4df1fe70433cf083-s.p.woff2) format("woff2");font-display:swap;font-weight:400;font-style:normal}@font-face{font-family:__departureMono_Fallback_6dd175;src:local("Arial");ascent-override:71.70%;descent-override:19.56%;line-gap-override:0.00%;size-adjust:139.46%}.__className_6dd175{font-family:__departureMono_6dd175,__departureMono_Fallback_6dd175;font-weight:400;font-style:normal}.__variable_6dd175{--font-departure:"__departureMono_6dd175","__departureMono_Fallback_6dd175"}:root{--system-ui:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}*,:after,:before{box-sizing:border-box}html{text-size-adjust:100%}body,html{margin:0;padding:0}body{display:flex;flex-direction:column;min-height:100vh;background-color:#699697;background-image:url(/vl2-forge/_next/static/media/noise.27f5ca25.png),linear-gradient(180deg,#5be9ee 0,#1e4172);background-repeat:repeat,repeat}
|
||||||
BIN
docs/_next/static/media/4df1fe70433cf083-s.p.woff2
Normal file
BIN
docs/_next/static/media/border.d6506fa8.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
docs/_next/static/media/button.69ed12fe.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/_next/static/media/noise.27f5ca25.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
docs/border.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
docs/button.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
1
docs/index.html
Normal file
6
docs/index.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
2:I[9330,["699","static/chunks/8e1d74a4-8e519121d6db0557.js","634","static/chunks/634-d90cc75f8d4e4c06.js","931","static/chunks/app/page-960543db4d1917f1.js"],"Forge"]
|
||||||
|
3:I[4707,[],""]
|
||||||
|
4:I[6423,[],""]
|
||||||
|
0:["AaZG0PNUAl--iHAutmB3I",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{}],[["$","link","0",{"rel":"stylesheet","href":"/vl2-forge/_next/static/css/914a77656c3e670b.css","precedence":"next","crossOrigin":"$undefined"}]]],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/vl2-forge/_next/static/css/c50e9c221475fcb7.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","className":"__variable_6dd175","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
|
||||||
BIN
docs/logo-lg.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/logo-md.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/noise.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
5
next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||||
7
next.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
output: "export",
|
||||||
|
distDir: process.env.NODE_ENV === "production" ? "./docs" : undefined,
|
||||||
|
basePath: "/vl2-forge",
|
||||||
|
assetPrefix: "/vl2-forge/",
|
||||||
|
trailingSlash: true,
|
||||||
|
};
|
||||||
1525
package-lock.json
generated
Normal file
30
package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "vl2-forge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"author": "Brian Beck <exogen@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [],
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev demo",
|
||||||
|
"build": "npm run build:pages",
|
||||||
|
"build:pages": "next build && touch docs/.nojekyll"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"lodash.orderby": "^4.6.0",
|
||||||
|
"next": "^14.2.15",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-dropzone": "^14.2.9",
|
||||||
|
"react-icons": "^5.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "22.7.5",
|
||||||
|
"@types/react": "18.3.11",
|
||||||
|
"tshy": "^3.0.2",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/DepartureMono-Regular.otf
Normal file
BIN
public/DepartureMono-Regular.woff
Normal file
BIN
public/DepartureMono-Regular.woff2
Normal file
BIN
public/border.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
public/button.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/logo-lg.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/logo-md.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/noise.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
252
src/Forge.module.css
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
.Forge {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Footer {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(59, 55, 49, 0.8);
|
||||||
|
|
||||||
|
border-image-slice: 18 26 26 23;
|
||||||
|
border-image-width: 16px 16px 16px 16px;
|
||||||
|
border-image-outset: 0px 0px 0px 0px;
|
||||||
|
|
||||||
|
border-image-repeat: round repeat;
|
||||||
|
border-image-source: url("../public/border.png");
|
||||||
|
border-style: solid;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3), inset 0 0 12px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Footer::before {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
left: -3px;
|
||||||
|
right: -3px;
|
||||||
|
bottom: -3px;
|
||||||
|
border-top: 4px solid rgba(255, 250, 234, 0.4);
|
||||||
|
border-left: 4px solid rgba(255, 250, 234, 0.4);
|
||||||
|
border-right: 4px solid rgba(23, 23, 23, 0.5);
|
||||||
|
border-bottom: 5px solid rgba(23, 23, 23, 0.5);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Footer::after {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
border-top: 4px solid rgba(23, 23, 23, 0.5);
|
||||||
|
border-left: 4px solid rgba(23, 23, 23, 0.5);
|
||||||
|
border-right: 4px solid rgba(255, 250, 234, 0.4);
|
||||||
|
border-bottom: 4px solid rgba(255, 250, 234, 0.4);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Footer form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NameInput {
|
||||||
|
position: relative;
|
||||||
|
font-size: 18px;
|
||||||
|
font-family: var(--system-ui);
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 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 0 1px 0;
|
||||||
|
min-width: 128px;
|
||||||
|
min-height: 46px;
|
||||||
|
font-family: var(--font-departure), monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
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;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px;
|
||||||
|
background: rgba(2, 2, 84, 0.252);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.FileList {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.EmptyMessage {
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AddButton {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 2px 0;
|
||||||
|
min-width: 48px;
|
||||||
|
min-height: 48px;
|
||||||
|
font-family: var(--font-departure), monospace;
|
||||||
|
font-size: 40px;
|
||||||
|
font-style: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 3px;
|
||||||
|
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;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PreviewIcon {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 24px;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Path {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.Forge {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Header {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Header img {
|
||||||
|
width: auto;
|
||||||
|
max-height: 96px;
|
||||||
|
}
|
||||||
|
}
|
||||||
322
src/Forge.tsx
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
"use client";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
import orderBy from "lodash.orderby";
|
||||||
|
import { FaTrashAlt } from "react-icons/fa";
|
||||||
|
import styles from "./Forge.module.css";
|
||||||
|
import { base64ArrayBuffer } from "./utils";
|
||||||
|
import { hasUncaughtExceptionCaptureCallback } from "process";
|
||||||
|
|
||||||
|
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 }) {
|
||||||
|
let icon = null;
|
||||||
|
if (file.dataUri && file.type?.genericType === "image") {
|
||||||
|
icon = (
|
||||||
|
<img
|
||||||
|
className={styles.PreviewIcon}
|
||||||
|
src={file.dataUri}
|
||||||
|
width={24}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.File}>
|
||||||
|
<span className={styles.IconContainer}>{icon}</span>{" "}
|
||||||
|
<span className={styles.Path}>{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 (const path in zip.files) {
|
||||||
|
const fileObj = zip.files[path];
|
||||||
|
if (!fileObj.dir) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 [actionLog, setActionLog] = useState(() => []);
|
||||||
|
const [files, setFiles] = useState(() => new Map());
|
||||||
|
|
||||||
|
const onDrop = useCallback(async (acceptedFiles) => {
|
||||||
|
const actionLog = [];
|
||||||
|
const finalMap = new Map();
|
||||||
|
const allFiles: Array<Map<string, any>> = await Promise.all(
|
||||||
|
acceptedFiles.map((file) => handleInputFile(file))
|
||||||
|
);
|
||||||
|
allFiles.forEach((map) => {
|
||||||
|
map.forEach((file, path) => {
|
||||||
|
if (finalMap.has(path)) {
|
||||||
|
actionLog.push({
|
||||||
|
type: "overwrite",
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finalMap.set(path, file);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setFiles((prevMap) => {
|
||||||
|
return new Map([
|
||||||
|
...Array.from(prevMap.entries()),
|
||||||
|
...Array.from(finalMap.entries()),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
setActionLog((prevLog) => [...prevLog, ...actionLog]);
|
||||||
|
}, []);
|
||||||
|
const { getRootProps, getInputProps, open, isDragActive } = useDropzone({
|
||||||
|
noClick: true,
|
||||||
|
onDrop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileList = useMemo(() => {
|
||||||
|
const paths = orderBy(
|
||||||
|
Array.from(files.keys()),
|
||||||
|
[(path) => path.toLowerCase()],
|
||||||
|
["asc"]
|
||||||
|
);
|
||||||
|
return paths.map((path) => files.get(path));
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
const addButton = (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.AddButton}
|
||||||
|
aria-label="Add files"
|
||||||
|
title="Add files"
|
||||||
|
onClick={open}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = useCallback((path) => {
|
||||||
|
setFiles((files) => {
|
||||||
|
const newFiles = new Map(files);
|
||||||
|
newFiles.delete(path);
|
||||||
|
return newFiles;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className={styles.Forge} {...getRootProps()}>
|
||||||
|
<header className={styles.Header}>
|
||||||
|
<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} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className={styles.EmptyMessage}>
|
||||||
|
Drop files onto the page or press the add button!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer className={styles.Footer}>
|
||||||
|
<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>
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/fonts.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
|
||||||
|
export const departureMono = localFont({
|
||||||
|
src: "../public/DepartureMono-Regular.woff2",
|
||||||
|
weight: "400",
|
||||||
|
style: "normal",
|
||||||
|
display: "swap",
|
||||||
|
variable: "--font-departure",
|
||||||
|
});
|
||||||
49
src/utils.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
// 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
|
||||||
|
/*
|
||||||
|
MIT LICENSE
|
||||||
|
Copyright 2011 Jon Leighton
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
export function base64ArrayBuffer(arrayBuffer) {
|
||||||
|
var base64 = "";
|
||||||
|
var encodings =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
var bytes = new Uint8Array(arrayBuffer);
|
||||||
|
var byteLength = bytes.byteLength;
|
||||||
|
var byteRemainder = byteLength % 3;
|
||||||
|
var mainLength = byteLength - byteRemainder;
|
||||||
|
var a, b, c, d;
|
||||||
|
var chunk;
|
||||||
|
// Main loop deals with bytes in chunks of 3
|
||||||
|
for (var i = 0; i < mainLength; i = i + 3) {
|
||||||
|
// Combine the three bytes into a single integer
|
||||||
|
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
||||||
|
// Use bitmasks to extract 6-bit segments from the triplet
|
||||||
|
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
|
||||||
|
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
|
||||||
|
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
|
||||||
|
d = chunk & 63; // 63 = 2^6 - 1
|
||||||
|
// Convert the raw binary segments to the appropriate ASCII encoding
|
||||||
|
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
|
||||||
|
}
|
||||||
|
// Deal with the remaining bytes and padding
|
||||||
|
if (byteRemainder == 1) {
|
||||||
|
chunk = bytes[mainLength];
|
||||||
|
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
|
||||||
|
// Set the 4 least significant bits to zero
|
||||||
|
b = (chunk & 3) << 4; // 3 = 2^2 - 1
|
||||||
|
base64 += encodings[a] + encodings[b] + "==";
|
||||||
|
} else if (byteRemainder == 2) {
|
||||||
|
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
|
||||||
|
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
|
||||||
|
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
|
||||||
|
// Set the 2 least significant bits to zero
|
||||||
|
c = (chunk & 15) << 2; // 15 = 2^4 - 1
|
||||||
|
base64 += encodings[a] + encodings[b] + encodings[c] + "=";
|
||||||
|
}
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
34
tsconfig.json
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||