Compare commits
10 Commits
7e4d854685
...
a8a903d581
Author | SHA1 | Date | |
---|---|---|---|
a8a903d581 | |||
44c1862869 | |||
3d9b877661 | |||
f6ce166b11 | |||
0517e7c2e2 | |||
0941690f91 | |||
9498f16c28 | |||
e03e8809b7 | |||
70b489213c | |||
89502213c4 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -26,3 +26,5 @@ dist-ssr
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
BearMetal/
|
BearMetal/
|
||||||
|
resources/
|
||||||
|
!**/*/resources/
|
17
deno.json
17
deno.json
@ -1,9 +1,12 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run -A --node-modules-dir npm:vite & deno run --allow-net --allow-read --allow-write --allow-env --watch ./main.ts",
|
"dev": "deno run -A --node-modules-dir npm:vite & deno run -A --watch ./server/main.ts",
|
||||||
|
"fdev": "deno run -A --node-modules-dir npm:vite",
|
||||||
"build": "deno run -A --node-modules-dir npm:vite build",
|
"build": "deno run -A --node-modules-dir npm:vite build",
|
||||||
"preview": "deno run -A --node-modules-dir npm:vite preview",
|
"preview": "deno run -A --node-modules-dir npm:vite preview",
|
||||||
"serve": "deno run --allow-net --allow-read --allow-write --allow-env ./main.ts"
|
"serve": "deno run -A ./server/main.ts",
|
||||||
|
"bdev": "deno run -A --watch ./server/main.ts",
|
||||||
|
"tmux": "./session.sh"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable", "deno.ns"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable", "deno.ns"],
|
||||||
@ -12,17 +15,25 @@
|
|||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@babel/plugin-transform-react-jsx-development": "npm:@babel/plugin-transform-react-jsx-development@^7.25.7",
|
"@babel/plugin-transform-react-jsx-development": "npm:@babel/plugin-transform-react-jsx-development@^7.25.7",
|
||||||
"@bearmetal/store": "jsr:@bearmetal/store@^0.0.4",
|
"@bearmetal/store": "jsr:@bearmetal/store@^0.0.5",
|
||||||
"@cgg/sockpuppet": "../sockpuppet.ts/server/mod.ts",
|
"@cgg/sockpuppet": "../sockpuppet.ts/server/mod.ts",
|
||||||
"@cgg/sockpuppet/client": "../sockpuppet.ts/client/mod.ts",
|
"@cgg/sockpuppet/client": "../sockpuppet.ts/client/mod.ts",
|
||||||
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0",
|
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0",
|
||||||
|
"@gfx/canvas": "jsr:@gfx/canvas@^0.5.8",
|
||||||
"@preact/preset-vite": "npm:@preact/preset-vite@^2.9.1",
|
"@preact/preset-vite": "npm:@preact/preset-vite@^2.9.1",
|
||||||
|
"@std/encoding": "jsr:@std/encoding@^1.0.5",
|
||||||
|
"@std/fs": "jsr:@std/fs@^1.0.4",
|
||||||
"@std/http": "jsr:@std/http@^1.0.8",
|
"@std/http": "jsr:@std/http@^1.0.8",
|
||||||
|
"@std/path": "jsr:@std/path@^1.0.6",
|
||||||
|
"@zip.js/zip.js": "npm:@zip.js/zip.js@^2.7.52",
|
||||||
"autoprefixer": "npm:autoprefixer@^10.4.20",
|
"autoprefixer": "npm:autoprefixer@^10.4.20",
|
||||||
"babel-plugin-transform-hook-names": "npm:babel-plugin-transform-hook-names@^1.0.2",
|
"babel-plugin-transform-hook-names": "npm:babel-plugin-transform-hook-names@^1.0.2",
|
||||||
|
"jotai": "npm:jotai@^2.10.1",
|
||||||
|
"less": "npm:less@^4.2.0",
|
||||||
"postcss": "npm:postcss@^8.4.47",
|
"postcss": "npm:postcss@^8.4.47",
|
||||||
"preact": "npm:preact@^10.24.3",
|
"preact": "npm:preact@^10.24.3",
|
||||||
"react-router-dom": "npm:react-router-dom@^6.27.0",
|
"react-router-dom": "npm:react-router-dom@^6.27.0",
|
||||||
|
"swr": "npm:swr@^2.2.5",
|
||||||
"tailwindcss": "npm:tailwindcss@^3.4.13",
|
"tailwindcss": "npm:tailwindcss@^3.4.13",
|
||||||
"vite": "npm:vite@^5.4.8"
|
"vite": "npm:vite@^5.4.8"
|
||||||
}
|
}
|
||||||
|
213
deno.lock
generated
213
deno.lock
generated
@ -1,62 +1,125 @@
|
|||||||
{
|
{
|
||||||
"version": "4",
|
"version": "4",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@bearmetal/store@^0.0.4": "0.0.4",
|
"jsr:@bearmetal/store@^0.0.5": "0.0.5",
|
||||||
|
"jsr:@denosaurs/plug@1.0.5": "1.0.5",
|
||||||
|
"jsr:@gfx/canvas@~0.5.8": "0.5.8",
|
||||||
|
"jsr:@std/assert@0.214": "0.214.0",
|
||||||
|
"jsr:@std/assert@0.217": "0.217.0",
|
||||||
"jsr:@std/cli@^1.0.6": "1.0.6",
|
"jsr:@std/cli@^1.0.6": "1.0.6",
|
||||||
|
"jsr:@std/encoding@0.214": "0.214.0",
|
||||||
|
"jsr:@std/encoding@0.217.0": "0.217.0",
|
||||||
"jsr:@std/encoding@^1.0.5": "1.0.5",
|
"jsr:@std/encoding@^1.0.5": "1.0.5",
|
||||||
|
"jsr:@std/fmt@0.214": "0.214.0",
|
||||||
"jsr:@std/fmt@^1.0.2": "1.0.2",
|
"jsr:@std/fmt@^1.0.2": "1.0.2",
|
||||||
"jsr:@std/fs@*": "1.0.4",
|
"jsr:@std/fs@*": "1.0.4",
|
||||||
|
"jsr:@std/fs@0.214": "0.214.0",
|
||||||
|
"jsr:@std/fs@0.217.0": "0.217.0",
|
||||||
"jsr:@std/fs@^1.0.4": "1.0.4",
|
"jsr:@std/fs@^1.0.4": "1.0.4",
|
||||||
"jsr:@std/http@^1.0.8": "1.0.8",
|
"jsr:@std/http@^1.0.8": "1.0.8",
|
||||||
"jsr:@std/media-types@^1.0.3": "1.0.3",
|
"jsr:@std/media-types@^1.0.3": "1.0.3",
|
||||||
"jsr:@std/net@^1.0.4": "1.0.4",
|
"jsr:@std/net@^1.0.4": "1.0.4",
|
||||||
|
"jsr:@std/path@0.214": "0.214.0",
|
||||||
|
"jsr:@std/path@0.217": "0.217.0",
|
||||||
|
"jsr:@std/path@0.217.0": "0.217.0",
|
||||||
"jsr:@std/path@^1.0.6": "1.0.6",
|
"jsr:@std/path@^1.0.6": "1.0.6",
|
||||||
"jsr:@std/streams@^1.0.7": "1.0.7",
|
"jsr:@std/streams@^1.0.7": "1.0.7",
|
||||||
"npm:@babel/plugin-transform-react-jsx-development@^7.25.7": "7.25.7_@babel+core@7.25.8",
|
"npm:@babel/plugin-transform-react-jsx-development@^7.25.7": "7.25.7_@babel+core@7.25.8",
|
||||||
"npm:@deno/vite-plugin@1": "1.0.0_vite@5.4.9",
|
"npm:@deno/vite-plugin@1": "1.0.0_vite@5.4.9",
|
||||||
"npm:@preact/preset-vite@^2.9.1": "2.9.1_@babel+core@7.25.8_vite@5.4.9_preact@10.24.3",
|
"npm:@preact/preset-vite@^2.9.1": "2.9.1_@babel+core@7.25.8_vite@5.4.9_preact@10.24.3",
|
||||||
"npm:@types/node@*": "22.5.4",
|
"npm:@types/node@*": "22.5.4",
|
||||||
|
"npm:@zip.js/zip.js@^2.7.52": "2.7.52",
|
||||||
"npm:autoprefixer@^10.4.20": "10.4.20_postcss@8.4.47",
|
"npm:autoprefixer@^10.4.20": "10.4.20_postcss@8.4.47",
|
||||||
"npm:babel-plugin-transform-hook-names@^1.0.2": "1.0.2_@babel+core@7.25.8",
|
"npm:babel-plugin-transform-hook-names@^1.0.2": "1.0.2_@babel+core@7.25.8",
|
||||||
|
"npm:jotai@^2.10.1": "2.10.1",
|
||||||
|
"npm:less@^4.2.0": "4.2.0",
|
||||||
"npm:postcss@^8.4.47": "8.4.47",
|
"npm:postcss@^8.4.47": "8.4.47",
|
||||||
"npm:preact@^10.24.3": "10.24.3",
|
"npm:preact@^10.24.3": "10.24.3",
|
||||||
"npm:react-router-dom@^6.27.0": "6.27.0_react@18.3.1_react-dom@18.3.1__react@18.3.1",
|
"npm:react-router-dom@^6.27.0": "6.27.0_react@18.3.1_react-dom@18.3.1__react@18.3.1",
|
||||||
|
"npm:swr@^2.2.5": "2.2.5_react@18.3.1",
|
||||||
"npm:tailwindcss@*": "3.4.13_postcss@8.4.47",
|
"npm:tailwindcss@*": "3.4.13_postcss@8.4.47",
|
||||||
"npm:tailwindcss@^3.4.13": "3.4.13_postcss@8.4.47",
|
"npm:tailwindcss@^3.4.13": "3.4.13_postcss@8.4.47",
|
||||||
"npm:vite@*": "5.4.9",
|
"npm:vite@*": "5.4.9",
|
||||||
"npm:vite@^5.4.8": "5.4.9"
|
"npm:vite@^5.4.8": "5.4.9"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
"@bearmetal/store@0.0.4": {
|
"@bearmetal/store@0.0.5": {
|
||||||
"integrity": "f5859476184d6f7b3957d18c7c82a37b6b89bb75e18db3186fde94ccb4253dab",
|
"integrity": "d17da24c91bcc05707deb8a55017ebdf5d8eebd2f6293dcb2bbfac57e4e3b395",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/fs@^1.0.4"
|
"jsr:@std/fs@^1.0.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@denosaurs/plug@1.0.5": {
|
||||||
|
"integrity": "04cd988da558adc226202d88c3a434d5fcc08146eaf4baf0cea0c2284b16d2bf",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/encoding@0.214",
|
||||||
|
"jsr:@std/fmt@0.214",
|
||||||
|
"jsr:@std/fs@0.214",
|
||||||
|
"jsr:@std/path@0.214"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@gfx/canvas@0.5.8": {
|
||||||
|
"integrity": "a61c80292528e7433d428556b494a0ea496dd8e6abd4a338b8b25fc04e46ea3e",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@denosaurs/plug",
|
||||||
|
"jsr:@std/encoding@0.217.0",
|
||||||
|
"jsr:@std/fs@0.217.0",
|
||||||
|
"jsr:@std/path@0.217.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@std/assert@0.214.0": {
|
||||||
|
"integrity": "55d398de76a9828fd3b1aa653f4dba3eee4c6985d90c514865d2be9bd082b140"
|
||||||
|
},
|
||||||
|
"@std/assert@0.217.0": {
|
||||||
|
"integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642"
|
||||||
|
},
|
||||||
"@std/cli@1.0.6": {
|
"@std/cli@1.0.6": {
|
||||||
"integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d"
|
"integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d"
|
||||||
},
|
},
|
||||||
|
"@std/encoding@0.214.0": {
|
||||||
|
"integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5"
|
||||||
|
},
|
||||||
|
"@std/encoding@0.217.0": {
|
||||||
|
"integrity": "b03e8ff94c98d6b6a02c02c5cf8e5d203400155516248964fc4559abc04669dc"
|
||||||
|
},
|
||||||
"@std/encoding@1.0.5": {
|
"@std/encoding@1.0.5": {
|
||||||
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
|
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
|
||||||
},
|
},
|
||||||
|
"@std/fmt@0.214.0": {
|
||||||
|
"integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4"
|
||||||
|
},
|
||||||
"@std/fmt@1.0.2": {
|
"@std/fmt@1.0.2": {
|
||||||
"integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7"
|
"integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7"
|
||||||
},
|
},
|
||||||
|
"@std/fs@0.214.0": {
|
||||||
|
"integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/assert@0.214",
|
||||||
|
"jsr:@std/path@0.214"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@std/fs@0.217.0": {
|
||||||
|
"integrity": "0bfff5f3618d68c385b28b4ffbf3a15c98293a0f1186444458b62e0111ce77b2",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/assert@0.217",
|
||||||
|
"jsr:@std/path@0.217"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@std/fs@1.0.4": {
|
"@std/fs@1.0.4": {
|
||||||
"integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c",
|
"integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/path"
|
"jsr:@std/path@^1.0.6"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/http@1.0.8": {
|
"@std/http@1.0.8": {
|
||||||
"integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd",
|
"integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/cli",
|
"jsr:@std/cli",
|
||||||
"jsr:@std/encoding",
|
"jsr:@std/encoding@^1.0.5",
|
||||||
"jsr:@std/fmt",
|
"jsr:@std/fmt@^1.0.2",
|
||||||
"jsr:@std/media-types",
|
"jsr:@std/media-types",
|
||||||
"jsr:@std/net",
|
"jsr:@std/net",
|
||||||
"jsr:@std/path",
|
"jsr:@std/path@^1.0.6",
|
||||||
"jsr:@std/streams"
|
"jsr:@std/streams"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -66,6 +129,18 @@
|
|||||||
"@std/net@1.0.4": {
|
"@std/net@1.0.4": {
|
||||||
"integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852"
|
"integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852"
|
||||||
},
|
},
|
||||||
|
"@std/path@0.214.0": {
|
||||||
|
"integrity": "d5577c0b8d66f7e8e3586d864ebdf178bb326145a3611da5a51c961740300285",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/assert@0.214"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@std/path@0.217.0": {
|
||||||
|
"integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/assert@0.217"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@std/path@1.0.6": {
|
"@std/path@1.0.6": {
|
||||||
"integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed"
|
"integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed"
|
||||||
},
|
},
|
||||||
@ -111,7 +186,7 @@
|
|||||||
"debug",
|
"debug",
|
||||||
"gensync",
|
"gensync",
|
||||||
"json5",
|
"json5",
|
||||||
"semver"
|
"semver@6.3.1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@babel/generator@7.25.7": {
|
"@babel/generator@7.25.7": {
|
||||||
@ -136,7 +211,7 @@
|
|||||||
"@babel/helper-validator-option",
|
"@babel/helper-validator-option",
|
||||||
"browserslist",
|
"browserslist",
|
||||||
"lru-cache@5.1.1",
|
"lru-cache@5.1.1",
|
||||||
"semver"
|
"semver@6.3.1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@babel/helper-module-imports@7.25.7": {
|
"@babel/helper-module-imports@7.25.7": {
|
||||||
@ -394,7 +469,7 @@
|
|||||||
"kolorist",
|
"kolorist",
|
||||||
"magic-string",
|
"magic-string",
|
||||||
"node-html-parser",
|
"node-html-parser",
|
||||||
"source-map",
|
"source-map@0.7.4",
|
||||||
"stack-trace",
|
"stack-trace",
|
||||||
"vite"
|
"vite"
|
||||||
]
|
]
|
||||||
@ -490,6 +565,9 @@
|
|||||||
"undici-types"
|
"undici-types"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@zip.js/zip.js@2.7.52": {
|
||||||
|
"integrity": "sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q=="
|
||||||
|
},
|
||||||
"ansi-regex@5.0.1": {
|
"ansi-regex@5.0.1": {
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||||
},
|
},
|
||||||
@ -599,6 +677,9 @@
|
|||||||
"readdirp"
|
"readdirp"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"client-only@0.0.1": {
|
||||||
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||||
|
},
|
||||||
"color-convert@1.9.3": {
|
"color-convert@1.9.3": {
|
||||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@ -623,6 +704,12 @@
|
|||||||
"convert-source-map@2.0.0": {
|
"convert-source-map@2.0.0": {
|
||||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||||
},
|
},
|
||||||
|
"copy-anything@2.0.6": {
|
||||||
|
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
|
||||||
|
"dependencies": [
|
||||||
|
"is-what"
|
||||||
|
]
|
||||||
|
},
|
||||||
"cross-spawn@7.0.3": {
|
"cross-spawn@7.0.3": {
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@ -699,6 +786,12 @@
|
|||||||
"entities@4.5.0": {
|
"entities@4.5.0": {
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||||
},
|
},
|
||||||
|
"errno@0.1.8": {
|
||||||
|
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
|
||||||
|
"dependencies": [
|
||||||
|
"prr"
|
||||||
|
]
|
||||||
|
},
|
||||||
"esbuild@0.21.5": {
|
"esbuild@0.21.5": {
|
||||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@ -803,6 +896,9 @@
|
|||||||
"globals@11.12.0": {
|
"globals@11.12.0": {
|
||||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
||||||
},
|
},
|
||||||
|
"graceful-fs@4.2.11": {
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||||
|
},
|
||||||
"has-flag@3.0.0": {
|
"has-flag@3.0.0": {
|
||||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
|
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
|
||||||
},
|
},
|
||||||
@ -815,6 +911,15 @@
|
|||||||
"he@1.2.0": {
|
"he@1.2.0": {
|
||||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||||
},
|
},
|
||||||
|
"iconv-lite@0.6.3": {
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dependencies": [
|
||||||
|
"safer-buffer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"image-size@0.5.5": {
|
||||||
|
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ=="
|
||||||
|
},
|
||||||
"is-binary-path@2.1.0": {
|
"is-binary-path@2.1.0": {
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@ -842,6 +947,9 @@
|
|||||||
"is-number@7.0.0": {
|
"is-number@7.0.0": {
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||||
},
|
},
|
||||||
|
"is-what@3.14.1": {
|
||||||
|
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA=="
|
||||||
|
},
|
||||||
"isexe@2.0.0": {
|
"isexe@2.0.0": {
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||||
},
|
},
|
||||||
@ -855,6 +963,9 @@
|
|||||||
"jiti@1.21.6": {
|
"jiti@1.21.6": {
|
||||||
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="
|
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="
|
||||||
},
|
},
|
||||||
|
"jotai@2.10.1": {
|
||||||
|
"integrity": "sha512-4FycO+BOTl2auLyF2Chvi6KTDqdsdDDtpaL/WHQMs8f3KS1E3loiUShQzAzFA/sMU5cJ0hz/RT1xum9YbG/zaA=="
|
||||||
|
},
|
||||||
"js-tokens@4.0.0": {
|
"js-tokens@4.0.0": {
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||||
},
|
},
|
||||||
@ -867,6 +978,21 @@
|
|||||||
"kolorist@1.8.0": {
|
"kolorist@1.8.0": {
|
||||||
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="
|
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="
|
||||||
},
|
},
|
||||||
|
"less@4.2.0": {
|
||||||
|
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
|
||||||
|
"dependencies": [
|
||||||
|
"copy-anything",
|
||||||
|
"errno",
|
||||||
|
"graceful-fs",
|
||||||
|
"image-size",
|
||||||
|
"make-dir",
|
||||||
|
"mime",
|
||||||
|
"needle",
|
||||||
|
"parse-node-version",
|
||||||
|
"source-map@0.6.1",
|
||||||
|
"tslib"
|
||||||
|
]
|
||||||
|
},
|
||||||
"lilconfig@2.1.0": {
|
"lilconfig@2.1.0": {
|
||||||
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="
|
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="
|
||||||
},
|
},
|
||||||
@ -897,6 +1023,13 @@
|
|||||||
"@jridgewell/sourcemap-codec"
|
"@jridgewell/sourcemap-codec"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"make-dir@2.1.0": {
|
||||||
|
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
||||||
|
"dependencies": [
|
||||||
|
"pify@4.0.1",
|
||||||
|
"semver@5.7.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
"merge2@1.4.1": {
|
"merge2@1.4.1": {
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
|
||||||
},
|
},
|
||||||
@ -907,6 +1040,9 @@
|
|||||||
"picomatch"
|
"picomatch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"mime@1.6.0": {
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||||
|
},
|
||||||
"minimatch@9.0.5": {
|
"minimatch@9.0.5": {
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@ -930,6 +1066,13 @@
|
|||||||
"nanoid@3.3.7": {
|
"nanoid@3.3.7": {
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
|
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
|
||||||
},
|
},
|
||||||
|
"needle@3.3.1": {
|
||||||
|
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
|
||||||
|
"dependencies": [
|
||||||
|
"iconv-lite",
|
||||||
|
"sax"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node-html-parser@6.1.13": {
|
"node-html-parser@6.1.13": {
|
||||||
"integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==",
|
"integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@ -961,6 +1104,9 @@
|
|||||||
"package-json-from-dist@1.0.1": {
|
"package-json-from-dist@1.0.1": {
|
||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||||
},
|
},
|
||||||
|
"parse-node-version@1.0.1": {
|
||||||
|
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA=="
|
||||||
|
},
|
||||||
"path-key@3.1.1": {
|
"path-key@3.1.1": {
|
||||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
|
||||||
},
|
},
|
||||||
@ -983,6 +1129,9 @@
|
|||||||
"pify@2.3.0": {
|
"pify@2.3.0": {
|
||||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
|
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
|
||||||
},
|
},
|
||||||
|
"pify@4.0.1": {
|
||||||
|
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
|
||||||
|
},
|
||||||
"pirates@4.0.6": {
|
"pirates@4.0.6": {
|
||||||
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="
|
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="
|
||||||
},
|
},
|
||||||
@ -1038,6 +1187,9 @@
|
|||||||
"preact@10.24.3": {
|
"preact@10.24.3": {
|
||||||
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="
|
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="
|
||||||
},
|
},
|
||||||
|
"prr@1.0.1": {
|
||||||
|
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw=="
|
||||||
|
},
|
||||||
"queue-microtask@1.2.3": {
|
"queue-microtask@1.2.3": {
|
||||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
|
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
|
||||||
},
|
},
|
||||||
@ -1074,7 +1226,7 @@
|
|||||||
"read-cache@1.0.0": {
|
"read-cache@1.0.0": {
|
||||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"pify"
|
"pify@2.3.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"readdirp@3.6.0": {
|
"readdirp@3.6.0": {
|
||||||
@ -1123,12 +1275,21 @@
|
|||||||
"queue-microtask"
|
"queue-microtask"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"safer-buffer@2.1.2": {
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
|
},
|
||||||
|
"sax@1.4.1": {
|
||||||
|
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
|
||||||
|
},
|
||||||
"scheduler@0.23.2": {
|
"scheduler@0.23.2": {
|
||||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"loose-envify"
|
"loose-envify"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"semver@5.7.2": {
|
||||||
|
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
|
||||||
|
},
|
||||||
"semver@6.3.1": {
|
"semver@6.3.1": {
|
||||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
|
||||||
},
|
},
|
||||||
@ -1147,6 +1308,9 @@
|
|||||||
"source-map-js@1.2.1": {
|
"source-map-js@1.2.1": {
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
|
||||||
},
|
},
|
||||||
|
"source-map@0.6.1": {
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||||
|
},
|
||||||
"source-map@0.7.4": {
|
"source-map@0.7.4": {
|
||||||
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
|
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
|
||||||
},
|
},
|
||||||
@ -1202,6 +1366,14 @@
|
|||||||
"supports-preserve-symlinks-flag@1.0.0": {
|
"supports-preserve-symlinks-flag@1.0.0": {
|
||||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
|
||||||
},
|
},
|
||||||
|
"swr@2.2.5_react@18.3.1": {
|
||||||
|
"integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
|
||||||
|
"dependencies": [
|
||||||
|
"client-only",
|
||||||
|
"react",
|
||||||
|
"use-sync-external-store"
|
||||||
|
]
|
||||||
|
},
|
||||||
"tailwindcss@3.4.13_postcss@8.4.47": {
|
"tailwindcss@3.4.13_postcss@8.4.47": {
|
||||||
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
|
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@ -1253,6 +1425,9 @@
|
|||||||
"ts-interface-checker@0.1.13": {
|
"ts-interface-checker@0.1.13": {
|
||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
||||||
},
|
},
|
||||||
|
"tslib@2.8.0": {
|
||||||
|
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
|
||||||
|
},
|
||||||
"undici-types@6.19.8": {
|
"undici-types@6.19.8": {
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
},
|
},
|
||||||
@ -1264,6 +1439,12 @@
|
|||||||
"picocolors"
|
"picocolors"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"use-sync-external-store@1.2.2_react@18.3.1": {
|
||||||
|
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
|
||||||
|
"dependencies": [
|
||||||
|
"react"
|
||||||
|
]
|
||||||
|
},
|
||||||
"util-deprecate@1.0.2": {
|
"util-deprecate@1.0.2": {
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
},
|
},
|
||||||
@ -1307,16 +1488,24 @@
|
|||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@bearmetal/store@^0.0.4",
|
"jsr:@bearmetal/store@^0.0.5",
|
||||||
|
"jsr:@gfx/canvas@~0.5.8",
|
||||||
|
"jsr:@std/encoding@^1.0.5",
|
||||||
|
"jsr:@std/fs@^1.0.4",
|
||||||
"jsr:@std/http@^1.0.8",
|
"jsr:@std/http@^1.0.8",
|
||||||
|
"jsr:@std/path@^1.0.6",
|
||||||
"npm:@babel/plugin-transform-react-jsx-development@^7.25.7",
|
"npm:@babel/plugin-transform-react-jsx-development@^7.25.7",
|
||||||
"npm:@deno/vite-plugin@1",
|
"npm:@deno/vite-plugin@1",
|
||||||
"npm:@preact/preset-vite@^2.9.1",
|
"npm:@preact/preset-vite@^2.9.1",
|
||||||
|
"npm:@zip.js/zip.js@^2.7.52",
|
||||||
"npm:autoprefixer@^10.4.20",
|
"npm:autoprefixer@^10.4.20",
|
||||||
"npm:babel-plugin-transform-hook-names@^1.0.2",
|
"npm:babel-plugin-transform-hook-names@^1.0.2",
|
||||||
|
"npm:jotai@^2.10.1",
|
||||||
|
"npm:less@^4.2.0",
|
||||||
"npm:postcss@^8.4.47",
|
"npm:postcss@^8.4.47",
|
||||||
"npm:preact@^10.24.3",
|
"npm:preact@^10.24.3",
|
||||||
"npm:react-router-dom@^6.27.0",
|
"npm:react-router-dom@^6.27.0",
|
||||||
|
"npm:swr@^2.2.5",
|
||||||
"npm:tailwindcss@^3.4.13",
|
"npm:tailwindcss@^3.4.13",
|
||||||
"npm:vite@^5.4.8"
|
"npm:vite@^5.4.8"
|
||||||
]
|
]
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + Preact + TS</title>
|
<title>Vite + Preact + TS</title>
|
||||||
|
<style lang="less">
|
||||||
|
@import './src/index.less';
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
66
main.ts
66
main.ts
@ -1,66 +0,0 @@
|
|||||||
import { SockpuppetPlus } from "@cgg/sockpuppet";
|
|
||||||
import { serveDir } from "@std/http/file-server";
|
|
||||||
import { BearMetalStore } from "@bearmetal/store";
|
|
||||||
|
|
||||||
const installPath = Deno.env.get("BMP_INSTALL_DIR") || "./";
|
|
||||||
|
|
||||||
const sockpuppet = new SockpuppetPlus();
|
|
||||||
sockpuppet.addHandler((req: Request) => {
|
|
||||||
if (new URL(req.url).pathname.startsWith("/api")) return;
|
|
||||||
|
|
||||||
return serveDir(req, {
|
|
||||||
fsRoot: installPath + "dist",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
sockpuppet.addHandler(async (req: Request) => {
|
|
||||||
if (!new URL(req.url).pathname.startsWith("/api")) return;
|
|
||||||
const store = new BearMetalStore();
|
|
||||||
|
|
||||||
console.log("API", req.url);
|
|
||||||
|
|
||||||
const API_DIR_ROUTE = new URLPattern({
|
|
||||||
pathname: "/api/dir",
|
|
||||||
search: "?list",
|
|
||||||
});
|
|
||||||
|
|
||||||
const match = API_DIR_ROUTE.exec(req.url);
|
|
||||||
if (!match) return;
|
|
||||||
|
|
||||||
switch (req.method) {
|
|
||||||
case "GET": {
|
|
||||||
const mcPath = store.get("mcPath") as string;
|
|
||||||
if (mcPath) {
|
|
||||||
for await (const file of Deno.readDir(mcPath)) {
|
|
||||||
if (file.isDirectory && file.name.startsWith("saves")) {
|
|
||||||
const worlds: string[] = [];
|
|
||||||
for await (const world of Deno.readDir(mcPath + "/" + file.name)) {
|
|
||||||
if (world.isDirectory) {
|
|
||||||
worlds.push(world.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Response(JSON.stringify({ worlds, mcPath }), {
|
|
||||||
status: 200,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Response(JSON.stringify({ mcPath }), {
|
|
||||||
status: mcPath ? 200 : 500,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
case "POST": {
|
|
||||||
const formData = await req.formData();
|
|
||||||
const dir = formData.get("dir") as string;
|
|
||||||
if (!dir) return new Response(null, { status: 400 });
|
|
||||||
|
|
||||||
Deno.env.set("MC_PATH", dir);
|
|
||||||
const mcPath = Deno.env.get("MC_PATH");
|
|
||||||
if (!mcPath) return new Response(null, { status: 500 });
|
|
||||||
|
|
||||||
return new Response(null, { status: 200 });
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return new Response(null, { status: 405 });
|
|
||||||
}
|
|
||||||
});
|
|
63
pack_versions/48.json
Normal file
63
pack_versions/48.json
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"version": "48",
|
||||||
|
"mcVersion": "^1.21",
|
||||||
|
"schema": {
|
||||||
|
"pack.mcmeta": "json",
|
||||||
|
"pack.png": "image/png",
|
||||||
|
"data": {
|
||||||
|
"<namespace>": {
|
||||||
|
"function": "function",
|
||||||
|
"structure": {
|
||||||
|
"DataVersion": "int",
|
||||||
|
"size": ["int", "int", "int"],
|
||||||
|
"palette": [{
|
||||||
|
"name": "blockId",
|
||||||
|
"properties": ["string"]
|
||||||
|
}],
|
||||||
|
"palettes": [
|
||||||
|
[{
|
||||||
|
"name": "blockId",
|
||||||
|
"properties": ["string"]
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
"blocks": [{
|
||||||
|
"state": "int",
|
||||||
|
"pos": ["int", "int", "int"],
|
||||||
|
"nbt": "nbt"
|
||||||
|
}],
|
||||||
|
"entities": [{
|
||||||
|
"pos": ["double", "double", "double"],
|
||||||
|
"blockPos": ["int", "int", "int"],
|
||||||
|
"nbt": "nbt"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"tags": "tags",
|
||||||
|
"advancment": {
|
||||||
|
"parent": "advancment",
|
||||||
|
"display": {
|
||||||
|
"icon": {
|
||||||
|
"id": "itemId",
|
||||||
|
"count": "int",
|
||||||
|
"components": ["itemComponent"]
|
||||||
|
},
|
||||||
|
"title": "jsonString",
|
||||||
|
"description": "jsonString",
|
||||||
|
"frame": "frame",
|
||||||
|
"background": "resource",
|
||||||
|
"show_toast": "bool",
|
||||||
|
"announce_to_chat": "bool",
|
||||||
|
"hidden": "bool"
|
||||||
|
},
|
||||||
|
"criteria": "criteria",
|
||||||
|
"requirements": ["criterion_name"],
|
||||||
|
"rewards": {
|
||||||
|
"experience": "int",
|
||||||
|
"function": "function",
|
||||||
|
"loot": ["loot_table"],
|
||||||
|
"recipes": ["recipe"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
251
server/main.ts
Normal file
251
server/main.ts
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import { SockpuppetPlus } from "@cgg/sockpuppet";
|
||||||
|
import { serveDir, serveFile } from "@std/http/file-server";
|
||||||
|
import { BearMetalStore } from "@bearmetal/store";
|
||||||
|
import { ensureDir, ensureFile, exists } from "@std/fs";
|
||||||
|
import { Router } from "./router.ts";
|
||||||
|
import { getPackVersion } from "./util/packVersion.ts";
|
||||||
|
import { createTagRoutes } from "./tags/routes.ts";
|
||||||
|
import { createResourcesRoutes } from "./resources/routes.ts";
|
||||||
|
|
||||||
|
const installPath = Deno.env.get("BMP_INSTALL_DIR") || "./";
|
||||||
|
|
||||||
|
const sockpuppet = new SockpuppetPlus();
|
||||||
|
sockpuppet.addHandler((req: Request) => {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
if (!url.pathname.startsWith("/images")) return;
|
||||||
|
|
||||||
|
return serveFile(req, url.searchParams.get("location") as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
// sockpuppet.addHandler((req: Request) => {
|
||||||
|
// const method = req.method;
|
||||||
|
// if (method === "OPTIONS") {
|
||||||
|
// return new Response(null, {
|
||||||
|
// status: 200,
|
||||||
|
// headers: {
|
||||||
|
// "Access-Control-Allow-Origin": "*",
|
||||||
|
// "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
router.route("/api/dir")
|
||||||
|
.get(async () => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
const mcPath = store.get("mcPath") as string;
|
||||||
|
if (mcPath) {
|
||||||
|
const worlds: {
|
||||||
|
world: string;
|
||||||
|
icon: string;
|
||||||
|
path: string;
|
||||||
|
}[] = [];
|
||||||
|
try {
|
||||||
|
for await (const file of Deno.readDir(mcPath)) {
|
||||||
|
if (file.isDirectory && file.name.startsWith("saves")) {
|
||||||
|
for await (
|
||||||
|
const world of Deno.readDir(mcPath + "/" + file.name)
|
||||||
|
) {
|
||||||
|
if (world.isDirectory) {
|
||||||
|
for await (
|
||||||
|
const f of Deno.readDir(
|
||||||
|
mcPath + "/" + file.name + "/" + world.name,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (f.name.endsWith(".dat")) {
|
||||||
|
worlds.push({
|
||||||
|
world: world.name,
|
||||||
|
icon: Deno.realPathSync(
|
||||||
|
mcPath + "/" + file.name + "/" + world.name +
|
||||||
|
"/icon.png",
|
||||||
|
),
|
||||||
|
path: Deno.realPathSync(
|
||||||
|
mcPath + "/" + file.name + "/" + world.name,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
store.set("mcPath", "");
|
||||||
|
return new Response(e, { status: 500 });
|
||||||
|
}
|
||||||
|
return new Response(JSON.stringify({ worlds, mcPath }), {
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Response(JSON.stringify({ mcPath }), {
|
||||||
|
status: mcPath ? 200 : 500,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.post(async (req) => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
const formData = await req.formData();
|
||||||
|
const dir = formData.get("mcPath") as string;
|
||||||
|
if (!dir) return new Response(null, { status: 400 });
|
||||||
|
|
||||||
|
store.set("mcPath", dir);
|
||||||
|
if (!store.get("mcPath")) return new Response(null, { status: 500 });
|
||||||
|
|
||||||
|
return new Response(null, { status: 200 });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/api/world")
|
||||||
|
.post(async (req) => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
const worldPath = await req.text();
|
||||||
|
if (!worldPath) return new Response(null, { status: 400 });
|
||||||
|
|
||||||
|
const mcPath = store.get("mcPath") as string;
|
||||||
|
if (!mcPath) {
|
||||||
|
return new Response("Tried to set world, but MC path is not set.", {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const realWorldPath = Deno.realPathSync(worldPath);
|
||||||
|
store.set("world", realWorldPath);
|
||||||
|
return new Response(null, { status: 200 });
|
||||||
|
})
|
||||||
|
.get((req) => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
const worldPath = store.get("world") as string;
|
||||||
|
if (!worldPath) return new Response(null, { status: 400 });
|
||||||
|
return new Response(worldPath.split("/").pop() as string, { status: 200 });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/api/pack")
|
||||||
|
.get(() => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ packName: store.get("packname") as string }),
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.post(async (req) => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
const formData = await req.formData();
|
||||||
|
const packName = formData.get("packName") as string;
|
||||||
|
if (!packName) return new Response(null, { status: 400 });
|
||||||
|
createPack(store, packName);
|
||||||
|
|
||||||
|
store.set("packname", packName);
|
||||||
|
return new Response(JSON.stringify({ packName }), { status: 200 });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/api/pack/namespaces")
|
||||||
|
.get(async () => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
|
||||||
|
const namespaces = Array.from(Deno.readDirSync(store.get("packlocation")))
|
||||||
|
.filter((dir) => dir.isDirectory).map((dir) => dir.name);
|
||||||
|
return new Response(JSON.stringify(namespaces), { status: 200 });
|
||||||
|
})
|
||||||
|
.post(async (req) => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
|
||||||
|
const namespace = await req.text();
|
||||||
|
if (!namespace) {
|
||||||
|
return new Response("Namespace is required", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespaceRx = /^[a-zA-Z0-9_\-]+$/;
|
||||||
|
if (!namespaceRx.test(namespace)) {
|
||||||
|
return new Response(
|
||||||
|
"Namespace must only contain letters, numbers, underscores, and dashes",
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await ensureDir(store.get("packlocation") + "/" + namespace);
|
||||||
|
return new Response(null, { status: 200 });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/api/packs")
|
||||||
|
.get(async () => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
const packs: { name: string; path: string }[] = [];
|
||||||
|
|
||||||
|
const world = store.get("world") as string;
|
||||||
|
for await (const pack of Deno.readDir(world + "/datapacks")) {
|
||||||
|
if (
|
||||||
|
pack.isDirectory &&
|
||||||
|
await exists(world + "/datapacks/" + pack.name + "/bmp_dev")
|
||||||
|
) {
|
||||||
|
packs.push({
|
||||||
|
name: pack.name,
|
||||||
|
path: Deno.realPathSync(world + "/datapacks/" + pack.name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("No BMP packs found", { status: 400 });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/api/pack/version")
|
||||||
|
.get(() => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
const version = getPackVersion(store);
|
||||||
|
return new Response(version.toString(), {
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.post(async (req) => {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
const version = await req.text();
|
||||||
|
if (!version) return new Response(null, { status: 400 });
|
||||||
|
|
||||||
|
try {
|
||||||
|
store.set("version", version);
|
||||||
|
const packMeta = Deno.readTextFileSync(
|
||||||
|
store.get("packlocation") + "/pack.mcmeta",
|
||||||
|
);
|
||||||
|
const packMetaJson = JSON.parse(packMeta);
|
||||||
|
packMetaJson.pack.pack_format = parseInt(version);
|
||||||
|
await Deno.writeTextFile(
|
||||||
|
store.get("packlocation") + "/pack.mcmeta",
|
||||||
|
JSON.stringify(packMetaJson),
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
return new Response(e, { status: 500 });
|
||||||
|
}
|
||||||
|
return new Response(null, { status: 200 });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/api/versions")
|
||||||
|
.get(() => {
|
||||||
|
const versions = Array.from(Deno.readDirSync(installPath + "pack_versions"))
|
||||||
|
.filter((v) => v.isFile).map((version) =>
|
||||||
|
version.name.replace(".json", "")
|
||||||
|
);
|
||||||
|
return new Response(JSON.stringify(versions), { status: 200 });
|
||||||
|
});
|
||||||
|
|
||||||
|
createTagRoutes(router);
|
||||||
|
createResourcesRoutes(router);
|
||||||
|
|
||||||
|
sockpuppet.addHandler((req: Request) => {
|
||||||
|
if (new URL(req.url).pathname.startsWith("/api")) return;
|
||||||
|
|
||||||
|
return serveDir(req, {
|
||||||
|
fsRoot: installPath + "dist",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sockpuppet.addHandler(router.handle);
|
||||||
|
|
||||||
|
async function createPack(store: BearMetalStore, packName: string) {
|
||||||
|
const realWorldPath = store.get("world");
|
||||||
|
store.set("packlocation", realWorldPath + "/datapacks/" + packName);
|
||||||
|
await ensureDir(store.get("packlocation"));
|
||||||
|
await ensureFile(store.get("packlocation") + "/bmp_dev");
|
||||||
|
await ensureFile(store.get("packlocation") + "/pack.mcmeta");
|
||||||
|
if (!Deno.readTextFileSync(store.get("packlocation") + "/pack.mcmeta")) {
|
||||||
|
await Deno.writeTextFile(
|
||||||
|
store.get("packlocation") + "/pack.mcmeta",
|
||||||
|
`{"pack":{"pack_format":48,"description":"${packName}"}}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
359
server/resources/readers.ts
Normal file
359
server/resources/readers.ts
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
import { encodeBase64 } from "@std/encoding/base64";
|
||||||
|
import { readDirFiles } from "../util/readDir.ts";
|
||||||
|
import { createIsometricCube } from "./renderer.ts";
|
||||||
|
|
||||||
|
interface BlockItem {
|
||||||
|
name: string;
|
||||||
|
resourceLocation: string;
|
||||||
|
images: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readBlocks = async (path: string) => {
|
||||||
|
const blocks: BlockItem[] =
|
||||||
|
(await readDirFiles(path + "/assets/minecraft/blockstates"))
|
||||||
|
.map((b) => ({
|
||||||
|
name: b.replace(".json", ""),
|
||||||
|
resourceLocation: "minecraft:" + b.replace(".json", ""),
|
||||||
|
images: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const blockTextures = await readDirFiles(
|
||||||
|
path + "/assets/minecraft/textures/block",
|
||||||
|
);
|
||||||
|
|
||||||
|
const modelPath = path + "/assets/minecraft/models/block";
|
||||||
|
for (const block of blocks) {
|
||||||
|
let name = block.name;
|
||||||
|
if (
|
||||||
|
block.name.startsWith("air") ||
|
||||||
|
block.name.startsWith("void_air") ||
|
||||||
|
block.name.startsWith("cave_air") ||
|
||||||
|
block.name.startsWith("end_gateway") ||
|
||||||
|
block.name.endsWith("_door") ||
|
||||||
|
block.name.endsWith("_trapdoor") ||
|
||||||
|
block.name.endsWith("_banner") ||
|
||||||
|
block.name.endsWith("_fence_gate") ||
|
||||||
|
block.name.endsWith("_pressure_plate") ||
|
||||||
|
block.name.endsWith("_button") ||
|
||||||
|
block.name.endsWith("lever") ||
|
||||||
|
block.name.endsWith("_sign") ||
|
||||||
|
block.name.endsWith("torch") ||
|
||||||
|
block.name.endsWith("_fence") ||
|
||||||
|
block.name.endsWith("_wall") ||
|
||||||
|
block.name.endsWith("_skull") ||
|
||||||
|
block.name.endsWith("_head") ||
|
||||||
|
block.name.includes("bubble_column") ||
|
||||||
|
block.name.includes("tripwire_hook") ||
|
||||||
|
block.name.includes("tripwire") ||
|
||||||
|
block.name == undefined ||
|
||||||
|
block.name.includes("chest") ||
|
||||||
|
block.name.includes("command")
|
||||||
|
) continue;
|
||||||
|
if (block.name.includes("water") || block.name.includes("lava")) {
|
||||||
|
const what = block.name.includes("water") ? "water" : "lava";
|
||||||
|
const itemTexLoc = path +
|
||||||
|
`/assets/minecraft/textures/block/${what}_still.png`;
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.name === "nether_portal") {
|
||||||
|
const itemTexLoc = path +
|
||||||
|
`/assets/minecraft/textures/block/nether_portal.png`;
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (["fire", "soul_fire"].includes(block.name)) {
|
||||||
|
const itemTexLoc = path +
|
||||||
|
`/assets/minecraft/textures/block/${block.name}_0.png`;
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.name.includes("cake")) {
|
||||||
|
const itemTexLoc = path +
|
||||||
|
`/assets/minecraft/textures/item/cake.png`;
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.name.endsWith("_bed")) {
|
||||||
|
const itemTexLoc = path + "/assets/minecraft/textures/entity/bed/" +
|
||||||
|
block.name.replace(/_bed$/, "") + ".png";
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.name === "nether_portal") {
|
||||||
|
const itemTexLoc = path +
|
||||||
|
`/assets/minecraft/textures/block/nether_portal.png`;
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.name.includes("dripleaf") && !block.name.includes("stem")) {
|
||||||
|
const itemTexLoc = path +
|
||||||
|
`/assets/minecraft/textures/block/${block.name}_top.png`;
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"carrots",
|
||||||
|
"potatoes",
|
||||||
|
"wheat",
|
||||||
|
"beetroots",
|
||||||
|
"grass",
|
||||||
|
"kelp",
|
||||||
|
"light",
|
||||||
|
"bell",
|
||||||
|
"redstone_wire",
|
||||||
|
"sweet_berry_bush",
|
||||||
|
"cocoa",
|
||||||
|
"nether_wart",
|
||||||
|
"repeater",
|
||||||
|
"bamboo",
|
||||||
|
"pitcher_plant",
|
||||||
|
"campfire",
|
||||||
|
"soul_campfire",
|
||||||
|
].includes(block.name)
|
||||||
|
) {
|
||||||
|
const itemTexLoc = path + "/assets/minecraft/textures/item/" +
|
||||||
|
block.name
|
||||||
|
.replace("large_fern", "fern")
|
||||||
|
.replace("tall_seagrass", "seagrass")
|
||||||
|
.replace(/ss$/, "?")
|
||||||
|
.replace(/e?s$/, "")
|
||||||
|
.replace("?", "ss")
|
||||||
|
.replace("_bush", "")
|
||||||
|
.replace("berry", "berries")
|
||||||
|
.replace("cocoa", "cocoa_beans")
|
||||||
|
.replace("_wire", "") +
|
||||||
|
".png";
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"melon_stem",
|
||||||
|
"pumpkin_stem",
|
||||||
|
"fern",
|
||||||
|
"large_fern",
|
||||||
|
"lilac",
|
||||||
|
"azure_bluet",
|
||||||
|
"red_tulip",
|
||||||
|
"orange_tulip",
|
||||||
|
"pink_tulip",
|
||||||
|
"white_tulip",
|
||||||
|
"oxeye_daisy",
|
||||||
|
"cornflower",
|
||||||
|
"lily_of_the_valley",
|
||||||
|
"wither_rose",
|
||||||
|
"rose_bush",
|
||||||
|
"peony",
|
||||||
|
"sunflower",
|
||||||
|
"torchflower_crop",
|
||||||
|
"suspicious_sand",
|
||||||
|
"suspicious_gravel",
|
||||||
|
"scaffolding",
|
||||||
|
"respawn_anchor",
|
||||||
|
"short_grass",
|
||||||
|
"tall_grass",
|
||||||
|
"seagrass",
|
||||||
|
"tall_seagrass",
|
||||||
|
"frosted_ice",
|
||||||
|
"pitcher_crop",
|
||||||
|
"iron_bars",
|
||||||
|
].includes(block.name)
|
||||||
|
) {
|
||||||
|
const textures = blockTextures.filter((t) => t.includes(block.name))
|
||||||
|
.reverse().filter((t) => !t.endsWith("mcmeta"));
|
||||||
|
const itemTexLoc = path + "/assets/minecraft/textures/block/" +
|
||||||
|
textures[0];
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.name.startsWith("waxed")) {
|
||||||
|
name = block.name.replace(/^waxed_?/, "");
|
||||||
|
}
|
||||||
|
if (block.name.startsWith("infested")) {
|
||||||
|
name = block.name.replace(/^infested_?/, "");
|
||||||
|
}
|
||||||
|
if (block.name.endsWith("_pane")) {
|
||||||
|
name = block.name.replace(/_pane$/, "");
|
||||||
|
}
|
||||||
|
if (block.name === "pink_petals") {
|
||||||
|
name = "pink_petals_4";
|
||||||
|
}
|
||||||
|
if (block.name.includes("candle")) {
|
||||||
|
const itemTexLoc = path +
|
||||||
|
`/assets/minecraft/textures/item/${block.name}.png`;
|
||||||
|
const itemImage = await Deno.readFile(itemTexLoc);
|
||||||
|
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.name.includes("cauldron")) {
|
||||||
|
const textures = blockTextures.filter((t) => t.includes("cauldron"));
|
||||||
|
for (const texture of textures) {
|
||||||
|
const texLoc = texture.replace("minecraft:", "");
|
||||||
|
const image = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/textures/block/" + texLoc,
|
||||||
|
);
|
||||||
|
const b64 = "data:image/png;base64," + encodeBase64(image);
|
||||||
|
block.images.push(await createIsometricCube(b64, b64, b64));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.name === "snow") {
|
||||||
|
name = "snow_height2";
|
||||||
|
}
|
||||||
|
const data = await Deno.readTextFile(
|
||||||
|
modelPath + "/" + name + ".json",
|
||||||
|
);
|
||||||
|
const modelJson = JSON.parse(data);
|
||||||
|
if (modelJson.textures && !modelJson.elements) {
|
||||||
|
if (modelJson.textures.all) {
|
||||||
|
const texLoc = modelJson.textures.all;
|
||||||
|
const image = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/textures/" + texLoc.replace(
|
||||||
|
"minecraft:",
|
||||||
|
"",
|
||||||
|
) + ".png",
|
||||||
|
);
|
||||||
|
const b64 = "data:image/png;base64," + encodeBase64(image);
|
||||||
|
block.images.push(await createIsometricCube(b64, b64, b64));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (modelJson.textures.front) {
|
||||||
|
const frontImage = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/textures/" +
|
||||||
|
modelJson.textures.front.replace(
|
||||||
|
"minecraft:",
|
||||||
|
"",
|
||||||
|
) + ".png",
|
||||||
|
);
|
||||||
|
|
||||||
|
const frontB64 = "data:image/png;base64," + encodeBase64(frontImage);
|
||||||
|
|
||||||
|
const topImage = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/textures/" +
|
||||||
|
(modelJson.textures.top ?? modelJson.textures.side).replace(
|
||||||
|
"minecraft:",
|
||||||
|
"",
|
||||||
|
) + ".png",
|
||||||
|
);
|
||||||
|
|
||||||
|
const topB64 = "data:image/png;base64," + encodeBase64(topImage);
|
||||||
|
|
||||||
|
const sideImage = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/textures/" +
|
||||||
|
modelJson.textures.side.replace(
|
||||||
|
"minecraft:",
|
||||||
|
"",
|
||||||
|
) + ".png",
|
||||||
|
);
|
||||||
|
|
||||||
|
const sideB64 = "data:image/png;base64," + encodeBase64(sideImage);
|
||||||
|
block.images.push(await createIsometricCube(frontB64, sideB64, topB64));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (modelJson.textures.side) {
|
||||||
|
const sideImage = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/textures/" +
|
||||||
|
modelJson.textures.side.replace(
|
||||||
|
"minecraft:",
|
||||||
|
"",
|
||||||
|
) + ".png",
|
||||||
|
);
|
||||||
|
|
||||||
|
const sideB64 = "data:image/png;base64," + encodeBase64(sideImage);
|
||||||
|
|
||||||
|
const topImage = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/textures/" +
|
||||||
|
(modelJson.textures.top ?? modelJson.textures.bottom ??
|
||||||
|
modelJson.textures.side).replace(
|
||||||
|
"minecraft:",
|
||||||
|
"",
|
||||||
|
) +
|
||||||
|
".png",
|
||||||
|
);
|
||||||
|
|
||||||
|
const topB64 = "data:image/png;base64," + encodeBase64(topImage);
|
||||||
|
block.images.push(await createIsometricCube(sideB64, sideB64, topB64));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const textures = blockTextures.filter((t) =>
|
||||||
|
t.includes(block.name) && !t.includes("mcmeta")
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const texture of textures) {
|
||||||
|
const texLoc = texture.replace("minecraft:", "");
|
||||||
|
const image = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/textures/block/" + texLoc,
|
||||||
|
);
|
||||||
|
const b64 = "data:image/png;base64," + encodeBase64(image);
|
||||||
|
block.images.push(await createIsometricCube(b64, b64, b64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readItems = async (path: string) => {
|
||||||
|
const items: BlockItem[] =
|
||||||
|
(await readDirFiles(path + "/assets/minecraft/models/item")).map((i) => ({
|
||||||
|
name: i.replace(".json", ""),
|
||||||
|
resourceLocation: "minecraft:" + i.replace(".json", ""),
|
||||||
|
images: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
let name = item.name;
|
||||||
|
if (item.name.startsWith("air")) continue;
|
||||||
|
if (item.name.startsWith("waxed")) name = item.name.replace(/^waxed_?/, "");
|
||||||
|
|
||||||
|
const data = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/models/item/" + name + ".json",
|
||||||
|
);
|
||||||
|
const json = JSON.parse(new TextDecoder().decode(data));
|
||||||
|
const texDir = path + "/assets/minecraft/textures/";
|
||||||
|
if (json.textures) {
|
||||||
|
const texLoc = json.textures.layer0;
|
||||||
|
if (texLoc) {
|
||||||
|
const data = await Deno.readFile(
|
||||||
|
texDir + texLoc.replace("minecraft:", "") + ".png",
|
||||||
|
);
|
||||||
|
item.images.push("data:image/png;base64," + encodeBase64(data));
|
||||||
|
}
|
||||||
|
} else if (json.parent) {
|
||||||
|
const parent = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/models/" +
|
||||||
|
json.parent.replace("minecraft:", "") + ".json",
|
||||||
|
);
|
||||||
|
const parentJson = JSON.parse(new TextDecoder().decode(parent));
|
||||||
|
if (parentJson.textures) {
|
||||||
|
let texLoc = parentJson.textures.all;
|
||||||
|
if (!texLoc) texLoc = parentJson.textures.side;
|
||||||
|
if (!texLoc) texLoc = parentJson.textures.top;
|
||||||
|
if (!texLoc) texLoc = parentJson.textures.bottom;
|
||||||
|
if (texLoc) {
|
||||||
|
const data = await Deno.readFile(
|
||||||
|
texDir + texLoc.replace("minecraft:", "") + ".png",
|
||||||
|
);
|
||||||
|
item.images.push("data:image/png;base64," + encodeBase64(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const path = "./resources/1.21.1";
|
||||||
|
console.log(await readItems(path));
|
||||||
|
}
|
81
server/resources/renderer.ts
Normal file
81
server/resources/renderer.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { createCanvas, Image } from "@gfx/canvas";
|
||||||
|
|
||||||
|
function loadImage(src: string): Promise<Image> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!src.startsWith("data:image/png;base64,")) {
|
||||||
|
console.log("src", src);
|
||||||
|
}
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = () => {
|
||||||
|
console.log(src);
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
img.src = src;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createIsometricCube(
|
||||||
|
face1Src: string,
|
||||||
|
face2Src: string,
|
||||||
|
face3Src: string,
|
||||||
|
width: number = 96,
|
||||||
|
) {
|
||||||
|
const canvasWidth = width; // Set a size for the canvas
|
||||||
|
const canvasHeight = Math.ceil(width / .866);
|
||||||
|
const canvas = createCanvas(canvasWidth, canvasHeight);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [face1, face2, face3] = await Promise.all([
|
||||||
|
loadImage(face1Src),
|
||||||
|
loadImage(face2Src),
|
||||||
|
loadImage(face3Src),
|
||||||
|
]).catch();
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
ctx.setTransform(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
Math.tan(degToRad(30)),
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
canvasWidth / 3.5,
|
||||||
|
);
|
||||||
|
ctx.drawImage(face1, 0, 0, canvasWidth / 2, canvasHeight / 2);
|
||||||
|
|
||||||
|
// Face 2
|
||||||
|
ctx.setTransform(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
-Math.tan(degToRad(30)),
|
||||||
|
1,
|
||||||
|
canvasWidth / 2,
|
||||||
|
canvasHeight / 2,
|
||||||
|
);
|
||||||
|
ctx.drawImage(face2, 0, 0, canvasWidth / 2, canvasHeight / 2);
|
||||||
|
|
||||||
|
// Face 3
|
||||||
|
ctx.setTransform(
|
||||||
|
1,
|
||||||
|
-Math.tan(degToRad(60)),
|
||||||
|
Math.tan(degToRad(30)),
|
||||||
|
1,
|
||||||
|
canvasWidth / 2,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
ctx.drawImage(face3, 0, 0, canvasWidth / 2, canvasWidth / 3.5);
|
||||||
|
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
} catch (e) {
|
||||||
|
// console.log("error", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const b64 = canvas.toDataURL("png");
|
||||||
|
return b64;
|
||||||
|
}
|
||||||
|
|
||||||
|
function degToRad(deg: number) {
|
||||||
|
return deg * (Math.PI / 180);
|
||||||
|
}
|
48
server/resources/routes.ts
Normal file
48
server/resources/routes.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { Router } from "../router.ts";
|
||||||
|
import { readDirDirs } from "../util/readDir.ts";
|
||||||
|
import { versionCompat } from "../util/versionCompat.ts";
|
||||||
|
import { readBlocks } from "./readers.ts";
|
||||||
|
|
||||||
|
export const createResourcesRoutes = (router: Router) => {
|
||||||
|
router.route("/api/resources/:path*")
|
||||||
|
.get(async (req, ctx) => {
|
||||||
|
const path = ctx.params.path;
|
||||||
|
if (!path) {
|
||||||
|
return new Response("no path provided", { status: 400 });
|
||||||
|
}
|
||||||
|
const format = ctx.url.searchParams.get("format");
|
||||||
|
if (!format) {
|
||||||
|
return new Response("no format provided", { status: 400 });
|
||||||
|
}
|
||||||
|
const packVersion = await Deno.readTextFile(
|
||||||
|
"./pack_versions/" + format + ".json",
|
||||||
|
);
|
||||||
|
const packVersionJson = JSON.parse(packVersion);
|
||||||
|
const mcVersion = packVersionJson.mcVersion;
|
||||||
|
const resourceVersions = await readDirDirs("./resources");
|
||||||
|
console.log("resourceVersions", resourceVersions);
|
||||||
|
for (const resourceVersion of resourceVersions) {
|
||||||
|
if (versionCompat(resourceVersion, mcVersion)) {
|
||||||
|
const resourcePath = "./resources/" + resourceVersion;
|
||||||
|
const splitPath = path.split("/");
|
||||||
|
switch (splitPath[0]) {
|
||||||
|
case "block":
|
||||||
|
case "blocks": {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(await readBlocks(resourcePath)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "item":
|
||||||
|
case "items": {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(await readBlocks(resourcePath)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return new Response("invalid path", { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
58
server/resources/unzip.ts
Normal file
58
server/resources/unzip.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { BearMetalStore } from "@bearmetal/store";
|
||||||
|
import { ZipReader } from "@zip.js/zip.js";
|
||||||
|
import { ensureFile } from "@std/fs";
|
||||||
|
|
||||||
|
export async function unzipResources(mcVersion?: string) {
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
mcVersion = mcVersion || await currentVersion(store);
|
||||||
|
|
||||||
|
console.log("mcVersion", mcVersion);
|
||||||
|
|
||||||
|
if (!mcVersion) return;
|
||||||
|
|
||||||
|
const blob = await Deno.open(
|
||||||
|
store.get("mcPath") + "/versions/" + mcVersion + "/" + mcVersion + ".jar",
|
||||||
|
);
|
||||||
|
|
||||||
|
const zip = new ZipReader(blob);
|
||||||
|
|
||||||
|
for (const entry of await zip.getEntries()) {
|
||||||
|
if (
|
||||||
|
entry.filename.startsWith("assets/") || entry.filename.startsWith("data/")
|
||||||
|
) {
|
||||||
|
// console.log("entry", entry);
|
||||||
|
await ensureFile(`./resources/${mcVersion}/${entry.filename}`);
|
||||||
|
const writer = await Deno.open(
|
||||||
|
`./resources/${mcVersion}/${entry.filename}`,
|
||||||
|
{ write: true },
|
||||||
|
);
|
||||||
|
await entry.getData?.(writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function currentVersion(store: BearMetalStore) {
|
||||||
|
const mcPath = store.get("mcPath");
|
||||||
|
if (!mcPath) return;
|
||||||
|
|
||||||
|
const versions = Array.from(Deno.readDirSync(mcPath + "/versions")).filter(
|
||||||
|
(d) => d.isDirectory,
|
||||||
|
).map((d) => d.name).sort();
|
||||||
|
let version = versions.pop();
|
||||||
|
let found = false;
|
||||||
|
versionC:
|
||||||
|
while (!found) {
|
||||||
|
for await (const file of Deno.readDir(mcPath + "/versions/" + version)) {
|
||||||
|
if (file.name.endsWith(".jar")) {
|
||||||
|
found = true;
|
||||||
|
break versionC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
version = versions.pop();
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
unzipResources();
|
||||||
|
}
|
72
server/router.ts
Normal file
72
server/router.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
export class Router {
|
||||||
|
private routes: Map<string, Handler[]> = new Map();
|
||||||
|
|
||||||
|
public route(route: string) {
|
||||||
|
const methods: Record<string, Handler> = {
|
||||||
|
get: () => undefined,
|
||||||
|
post: () => undefined,
|
||||||
|
put: () => undefined,
|
||||||
|
delete: () => undefined,
|
||||||
|
};
|
||||||
|
this.routes.set(route, this.routes.get(route) || []);
|
||||||
|
this.routes.get(route)?.push((r, c) => {
|
||||||
|
switch (r.method) {
|
||||||
|
case "GET":
|
||||||
|
return methods.get?.(r, c);
|
||||||
|
case "POST":
|
||||||
|
return methods.post?.(r, c);
|
||||||
|
case "PUT":
|
||||||
|
return methods.put?.(r, c);
|
||||||
|
case "DELETE":
|
||||||
|
return methods.delete?.(r, c);
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
get(handler: Handler) {
|
||||||
|
methods.get = handler;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
post(handler: Handler) {
|
||||||
|
methods.post = handler;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
put(handler: Handler) {
|
||||||
|
methods.put = handler;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
delete(handler: Handler) {
|
||||||
|
methods.delete = handler;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public handle = async (req: Request): Promise<Response> => {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
for (const [route, handlers] of this.routes.entries()) {
|
||||||
|
const pattern = new URLPattern({ pathname: route });
|
||||||
|
const match = pattern.exec(req.url);
|
||||||
|
if (match) {
|
||||||
|
let res;
|
||||||
|
for (const handler of handlers) {
|
||||||
|
res = await handler(req, {
|
||||||
|
url,
|
||||||
|
state: {},
|
||||||
|
params: match.pathname.groups,
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Handler = (
|
||||||
|
req: Request,
|
||||||
|
ctx: Context,
|
||||||
|
) => Promise<Response | undefined> | Response | undefined;
|
14
server/tags/getTagDir.ts
Normal file
14
server/tags/getTagDir.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { BearMetalStore } from "@bearmetal/store";
|
||||||
|
import { getDirName } from "../util/packVersion.ts";
|
||||||
|
|
||||||
|
export async function getTagDir(
|
||||||
|
store: BearMetalStore,
|
||||||
|
namespace: string,
|
||||||
|
version: number,
|
||||||
|
) {
|
||||||
|
// const versionData = JSON.parse(Deno.readTextFileSync(installPath + "pack_versions/" + version + ".json"));
|
||||||
|
|
||||||
|
const tagDir = await getDirName(version, "tags");
|
||||||
|
|
||||||
|
return `${store.get("packlocation")}/${namespace}/${tagDir}`;
|
||||||
|
}
|
135
server/tags/routes.ts
Normal file
135
server/tags/routes.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { BearMetalStore } from "@bearmetal/store";
|
||||||
|
import { getPackVersion } from "../util/packVersion.ts";
|
||||||
|
import type { Router } from "../router.ts";
|
||||||
|
import { getTagDir } from "./getTagDir.ts";
|
||||||
|
import { ensureDir } from "@std/fs/ensure-dir";
|
||||||
|
import { ensureFile } from "@std/fs/ensure-file";
|
||||||
|
|
||||||
|
export const createTagRoutes = (router: Router) => {
|
||||||
|
router.route("/api/pack/:namespace/tags")
|
||||||
|
.get(async (_, ctx) => {
|
||||||
|
if (!ctx.params.namespace) {
|
||||||
|
return new Response("somehow hit the tags endpoint without namespace", {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
|
||||||
|
const version = getPackVersion(store);
|
||||||
|
|
||||||
|
const tagDir = await getTagDir(store, ctx.params.namespace, version);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tags = Array.from(
|
||||||
|
Deno.readDirSync(
|
||||||
|
tagDir,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return new Response(JSON.stringify(tags), { status: 200 });
|
||||||
|
} catch {
|
||||||
|
return new Response("[]", { status: 200 });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post(async (req, ctx) => {
|
||||||
|
if (!ctx.params.namespace) {
|
||||||
|
return new Response("somehow hit the tags endpoint without namespace", {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
|
||||||
|
const version = getPackVersion(store);
|
||||||
|
|
||||||
|
const tagDir = await getTagDir(store, ctx.params.namespace, version);
|
||||||
|
|
||||||
|
await ensureDir(tagDir);
|
||||||
|
|
||||||
|
const { tag, type } = await req.json();
|
||||||
|
if (!tag || !type) {
|
||||||
|
return new Response("no tag name provided", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagPath = `${tagDir}/${type}/${tag}.json`;
|
||||||
|
|
||||||
|
await ensureFile(tagPath);
|
||||||
|
|
||||||
|
return new Response(tag, { status: 200 });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/api/pack/:namespace/tags/:type-:tag")
|
||||||
|
.get(async (_, ctx) => {
|
||||||
|
if (!ctx.params.namespace) {
|
||||||
|
return new Response("somehow hit the tags endpoint without namespace", {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
|
||||||
|
const version = getPackVersion(store);
|
||||||
|
|
||||||
|
const tagDir = await getTagDir(store, ctx.params.namespace, version);
|
||||||
|
|
||||||
|
const tag = ctx.params.tag;
|
||||||
|
const type = ctx.params.type;
|
||||||
|
if (!tag || !type) {
|
||||||
|
return new Response("no tag name provided", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tagFile = Deno.readTextFileSync(`${tagDir}/${type}/${tag}.json`);
|
||||||
|
return new Response(tagFile, { status: 200 });
|
||||||
|
} catch {
|
||||||
|
return new Response("no tag found", { status: 404 });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.put(async (req, ctx) => {
|
||||||
|
if (!ctx.params.namespace) {
|
||||||
|
return new Response("somehow hit the tags endpoint without namespace", {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
|
||||||
|
const version = getPackVersion(store);
|
||||||
|
|
||||||
|
const tagDir = await getTagDir(store, ctx.params.namespace, version);
|
||||||
|
|
||||||
|
const tag = ctx.params.tag;
|
||||||
|
const type = ctx.params.type;
|
||||||
|
if (!tag || !type) {
|
||||||
|
return new Response("no tag name provided", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagPath = `${tagDir}/${type}/${tag}.json`;
|
||||||
|
|
||||||
|
await ensureFile(tagPath);
|
||||||
|
|
||||||
|
await Deno.writeTextFile(tagPath, await req.text());
|
||||||
|
|
||||||
|
return new Response(tag, { status: 200 });
|
||||||
|
})
|
||||||
|
.delete(async (_, ctx) => {
|
||||||
|
if (!ctx.params.namespace) {
|
||||||
|
return new Response("somehow hit the tags endpoint without namespace", {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
using store = new BearMetalStore();
|
||||||
|
|
||||||
|
const version = getPackVersion(store);
|
||||||
|
|
||||||
|
const tagDir = await getTagDir(store, ctx.params.namespace, version);
|
||||||
|
|
||||||
|
const tag = ctx.params.tag;
|
||||||
|
if (!tag) {
|
||||||
|
return new Response("no tag name provided", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Deno.remove(tagDir + "/" + tag + ".json");
|
||||||
|
return new Response(tag, { status: 200 });
|
||||||
|
} catch {
|
||||||
|
return new Response("no tag found", { status: 404 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
32
server/util/packVersion.ts
Normal file
32
server/util/packVersion.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { BearMetalStore } from "@bearmetal/store";
|
||||||
|
|
||||||
|
export function getPackVersion(store: BearMetalStore) {
|
||||||
|
const packMeta = Deno.readTextFileSync(
|
||||||
|
store.get("packlocation") + "/pack.mcmeta",
|
||||||
|
);
|
||||||
|
const packMetaJson = JSON.parse(packMeta);
|
||||||
|
return packMetaJson.pack.pack_format as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDirName(version: number, path: string) {
|
||||||
|
const { default: versionData } = await import(
|
||||||
|
"../../pack_versions/" + version + ".json",
|
||||||
|
{
|
||||||
|
with: { type: "json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const singular = makeSingular(path);
|
||||||
|
const plural = makePlural(path);
|
||||||
|
|
||||||
|
return versionData && versionData.schema.data["<namespace>"][singular] ||
|
||||||
|
versionData.schema.data["<namespace>"][plural];
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSingular(word: string) {
|
||||||
|
return word.replace(/s$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePlural(word: string) {
|
||||||
|
return makeSingular(word) + "s";
|
||||||
|
}
|
20
server/util/readDir.ts
Normal file
20
server/util/readDir.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export const readDirFiles = async (path: string) => {
|
||||||
|
return readDirFiltered(path, (file) => file.isFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readDirDirs = async (path: string) => {
|
||||||
|
return readDirFiltered(path, (file) => file.isDirectory);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readDirFiltered = async (
|
||||||
|
path: string,
|
||||||
|
filter: (file: Deno.DirEntry) => boolean,
|
||||||
|
) => {
|
||||||
|
const files: string[] = [];
|
||||||
|
for await (const file of Deno.readDir(path)) {
|
||||||
|
if (filter(file)) {
|
||||||
|
files.push(file.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
};
|
14
server/util/versionCompat.ts
Normal file
14
server/util/versionCompat.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export const versionCompat = (version: string, targetVersion: string) => {
|
||||||
|
if (targetVersion === "*") return true;
|
||||||
|
if (targetVersion === version) return true;
|
||||||
|
if (targetVersion.startsWith("^")) {
|
||||||
|
const versionSplit = version.split(".");
|
||||||
|
const targetVersionSplit = targetVersion.split(".");
|
||||||
|
for (let i = 0; i < versionSplit.length; i++) {
|
||||||
|
if (versionSplit[i] ?? "0" > targetVersionSplit[i] ?? "0") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
16
session.sh
Executable file
16
session.sh
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SESSION=BearMetalPacker
|
||||||
|
|
||||||
|
tmux new-session -d -s $SESSION
|
||||||
|
tmux new-window -t $SESSION:1 -n "packer"
|
||||||
|
|
||||||
|
tmux select-window -t $SESSION:1
|
||||||
|
tmux send-keys "deno task bdev" C-m
|
||||||
|
|
||||||
|
tmux split-window -h
|
||||||
|
tmux send-keys "deno task fdev" C-m
|
||||||
|
|
||||||
|
tmux split-window -v
|
||||||
|
|
||||||
|
tmux attach -t $SESSION
|
10
src/app.tsx
10
src/app.tsx
@ -1,13 +1,17 @@
|
|||||||
import { BrowserRouter,Routes,Route } from "react-router-dom";
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
import { Home } from "./views/home.tsx";
|
import { Home } from "./views/home.tsx";
|
||||||
|
import { Editor } from "./views/editor.tsx";
|
||||||
|
import { Provider } from "jotai";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Provider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" Component={Home} />
|
<Route path="/" Component={Home} />
|
||||||
|
<Route path="/editor/*" Component={Editor} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
</Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
19
src/atoms/namespace.ts
Normal file
19
src/atoms/namespace.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { atomWithStorage } from "jotai/utils";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
|
const key = "bmp:namespace";
|
||||||
|
// const namespaceAtomPrimitive = atom<string>(localStorage.getItem(key) ??"");
|
||||||
|
|
||||||
|
// export const namespaceAtom = atom<string>(
|
||||||
|
// (get) => get(namespaceAtomPrimitive),
|
||||||
|
// (get, set, newStr) => {
|
||||||
|
// set(namespaceAtomPrimitive, newStr)
|
||||||
|
// localStorage.setItem(key, newStr)
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
|
||||||
|
export const namespaceAtom = atomWithStorage(key, "");
|
||||||
|
|
||||||
|
export const useNamespace = () => {
|
||||||
|
return useAtom(namespaceAtom);
|
||||||
|
};
|
62
src/components/editor/namespaceModal.tsx
Normal file
62
src/components/editor/namespaceModal.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { fetchJson } from "../../util/fetchJson.ts";
|
||||||
|
import { Loader } from "../../components/loader.tsx";
|
||||||
|
import { Modal } from "../../components/modal.tsx";
|
||||||
|
import useSwr from "swr";
|
||||||
|
|
||||||
|
export const NamespaceModal = ({ close }: { close: () => void }) => {
|
||||||
|
const { data: namespaces, isLoading } = useSwr<string[]>(
|
||||||
|
"/api/pack/namespaces",
|
||||||
|
fetchJson,
|
||||||
|
);
|
||||||
|
const [namespace, setNamespace] = useState("");
|
||||||
|
const [invalid, setInvalid] = useState("");
|
||||||
|
|
||||||
|
const createNamespace = async (ns?: string) => {
|
||||||
|
const res = await fetch("/api/pack/namespaces", {
|
||||||
|
method: "POST",
|
||||||
|
body: ns ?? namespace,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
return close();
|
||||||
|
}
|
||||||
|
|
||||||
|
setInvalid(await res.text());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal>
|
||||||
|
{isLoading
|
||||||
|
? <Loader msg="Checking existing namespaces..." />
|
||||||
|
: (
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p>Create a new namespace</p>
|
||||||
|
{!namespaces?.includes("minecraft") &&
|
||||||
|
(
|
||||||
|
<button
|
||||||
|
class="w-full"
|
||||||
|
onClick={() => createNamespace("minecraft")}
|
||||||
|
>
|
||||||
|
Create default minecraft namespace
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
createNamespace();
|
||||||
|
}}
|
||||||
|
class="flex gap-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={namespace}
|
||||||
|
onInput={(e) => setNamespace((e.target as any).value)}
|
||||||
|
/>
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
11
src/components/editor/selector.tsx
Normal file
11
src/components/editor/selector.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export const Selector = () => {
|
||||||
|
return (
|
||||||
|
<ul class="flex flex-col gap-2">
|
||||||
|
<li>
|
||||||
|
<Link to="/editor/tags">Tag Editor</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
156
src/components/editor/tags/editor.tsx
Normal file
156
src/components/editor/tags/editor.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { Link, Route, Routes, useParams } from "react-router-dom";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { fetchJson } from "../../../util/fetchJson.ts";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { namespaceAtom, useNamespace } from "../../../atoms/namespace.ts";
|
||||||
|
import { Loader } from "../../../components/loader.tsx";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { NewTagModal } from "./newTagModal.tsx";
|
||||||
|
|
||||||
|
export const TagRouter = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route index Component={TagList} />
|
||||||
|
<Route path=":typeTag" Component={TagEditor} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function TagList() {
|
||||||
|
const [namespace, _setNamespace] = useAtom(namespaceAtom);
|
||||||
|
const { data, isLoading } = useSWR<string[]>(
|
||||||
|
`/api/pack/${namespace}/tags`,
|
||||||
|
fetchJson,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showNewTagModal, setShowNewTagModal] = useState(false);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loader msg="Geez, when was the last time you swept?" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
</ul>
|
||||||
|
<Resources />
|
||||||
|
{false && (
|
||||||
|
<ul class="flex flex-col gap-2">
|
||||||
|
<li>
|
||||||
|
<button onClick={() => setShowNewTagModal(true)}>New Tag</button>
|
||||||
|
{showNewTagModal && (
|
||||||
|
<NewTagModal
|
||||||
|
close={() => {
|
||||||
|
setShowNewTagModal(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
{data?.map((tag) => (
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<Link to={`/editor/tags/${tag}`}>{tag}</Link>
|
||||||
|
<button>Delete</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tag {
|
||||||
|
replace: boolean;
|
||||||
|
values: (string | { id: string; required: boolean })[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagEditor() {
|
||||||
|
const [namespace, _setNamespace] = useNamespace();
|
||||||
|
const { typeTag } = useParams();
|
||||||
|
const { data, isLoading } = useSWR<Tag>(
|
||||||
|
`/api/pack/${namespace}/tags/${typeTag}`,
|
||||||
|
fetchJson,
|
||||||
|
);
|
||||||
|
const [tag, setTag] = useState<Tag>({
|
||||||
|
replace: false,
|
||||||
|
values: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTag(data || { replace: false, values: [] });
|
||||||
|
}, [data, isLoading]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loader msg="Your hard drive is full of... interesting things." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold">
|
||||||
|
{typeTag}:
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="replace"
|
||||||
|
checked={tag.replace}
|
||||||
|
onChange={(e) =>
|
||||||
|
setTag({ ...tag, replace: (e.target as any).checked })}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
/* {tag.values.map((value, i) => (
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onInput={(e) =>
|
||||||
|
setTag({
|
||||||
|
...tag,
|
||||||
|
values: tag.values.map((v, i) =>
|
||||||
|
i === i ? e.target.value : v
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setTag({
|
||||||
|
...tag,
|
||||||
|
values: tag.values.filter((_, i) => i !== i),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))} */
|
||||||
|
}
|
||||||
|
{tag.values.length === 0 && (
|
||||||
|
<button onClick={() => setTag({ ...tag, values: [""] })}>
|
||||||
|
Add Value
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resources() {
|
||||||
|
const { data, isLoading } = useSWR<{ name: string; images: string[] }[]>(
|
||||||
|
`/api/resources/blocks?format=48`,
|
||||||
|
fetchJson,
|
||||||
|
);
|
||||||
|
if (isLoading || !data) {
|
||||||
|
return <Loader msg="Your hard drive is full of... interesting things." />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{data.map((resource) => (
|
||||||
|
<li>
|
||||||
|
{resource.name}
|
||||||
|
<img class="min-w-12 max-h-16" src={resource.images[0]} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
66
src/components/editor/tags/newTagModal.tsx
Normal file
66
src/components/editor/tags/newTagModal.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { Modal } from "../../modal.tsx";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { namespaceAtom } from "../../../atoms/namespace.ts";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
|
export const NewTagModal = ({ close }: { close: () => void }) => {
|
||||||
|
const [tagName, setTagName] = useState("");
|
||||||
|
const [tagType, setTagType] = useState("block");
|
||||||
|
const nav = useNavigate();
|
||||||
|
const [namespace, _setNamespace] = useAtom(namespaceAtom);
|
||||||
|
|
||||||
|
const createTag = async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/pack/${namespace}/tags`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
tag: tagName,
|
||||||
|
type: tagType,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
close();
|
||||||
|
nav(`/editor/tags/${tagType}-${tagName}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Modal>
|
||||||
|
<p class="mb-2">Create a new tag</p>
|
||||||
|
<form
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
createTag();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label class="flex gap-2 items-center">
|
||||||
|
Tag Type{" "}
|
||||||
|
<select
|
||||||
|
class="flex-1"
|
||||||
|
name="tag-type"
|
||||||
|
value={tagType}
|
||||||
|
onChange={(e) => setTagType((e.target as any).value)}
|
||||||
|
>
|
||||||
|
<option value="block">Block</option>
|
||||||
|
<option value="entity">Entity</option>
|
||||||
|
<option value="item">Item</option>
|
||||||
|
<option value="function">Function</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={tagName}
|
||||||
|
onInput={(e) => setTagName((e.target as any).value)}
|
||||||
|
placeholder="Tag name"
|
||||||
|
/>
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
15
src/components/editor/wrapper.tsx
Normal file
15
src/components/editor/wrapper.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import type { FunctionComponent } from "preact";
|
||||||
|
|
||||||
|
export const EditorWrapper: FunctionComponent = ({ children }) => {
|
||||||
|
const nav = useNavigate();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button class="hollow flex items-center gap-2" onClick={() => nav(-1)}>
|
||||||
|
<span class="text-2xl">←</span>
|
||||||
|
<span class="italic">back</span>
|
||||||
|
</button>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
11
src/components/labelledHr.tsx
Normal file
11
src/components/labelledHr.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { FunctionComponent } from "preact";
|
||||||
|
|
||||||
|
export const LabelledHr: FunctionComponent = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div class="flex gap-4 items-center my-4">
|
||||||
|
<hr class="w-full" />
|
||||||
|
<label>{children}</label>
|
||||||
|
<hr class="w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
18
src/components/loader.tsx
Normal file
18
src/components/loader.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { classList } from "../util/classes.ts";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
msg?: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loader = ({ msg, class: className }: IProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={classList("flex justify-center items-center gap-4", className)}
|
||||||
|
>
|
||||||
|
{!!msg && <p>{msg}</p>}
|
||||||
|
<div class="animate-spin rounded-full max-w-full h-16 w-16 border-t-2 border-b-2 border-primary-600">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
14
src/components/modal.tsx
Normal file
14
src/components/modal.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { FunctionComponent } from "preact";
|
||||||
|
import { Portal } from "./portal.tsx";
|
||||||
|
|
||||||
|
export const Modal: FunctionComponent = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<div class="fixed inset-0 z-10 overflow-y-auto bg-black/50 grid">
|
||||||
|
<div class="place-self-center bg-white dark:bg-mixed-400 w-min min-w-64 rounded-lg p-4 overflow-scroll relative">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
68
src/components/packInfo.tsx
Normal file
68
src/components/packInfo.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import type { FunctionComponent } from "preact";
|
||||||
|
import { Loader } from "./loader.tsx";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
packName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PackInfo: FunctionComponent<IProps> = ({ packName }) => {
|
||||||
|
const [showVersions, setShowVersions] = useState(false);
|
||||||
|
const [packVersion, setPackVersion] = useState("");
|
||||||
|
const [packVersionList, setPackVersionList] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [world, setWorld] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([
|
||||||
|
fetch("/api/pack/version").then((res) => res.text()).then((text) => {
|
||||||
|
setPackVersion(text);
|
||||||
|
}),
|
||||||
|
fetch("/api/versions").then((res) => res.json()).then((json) => {
|
||||||
|
setPackVersionList(json);
|
||||||
|
}),
|
||||||
|
fetch("/api/world").then((res) => res.text()).then((text) => {
|
||||||
|
setWorld(text);
|
||||||
|
}),
|
||||||
|
]).finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updatePackVersion = async (version: string) => {
|
||||||
|
const res = await fetch("/api/pack/version", {
|
||||||
|
method: "POST",
|
||||||
|
body: version,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
setPackVersion(version);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 class="text-xl">{world} :: {packName}</h2>
|
||||||
|
<p class="bg-lime-700 rounded-full shadow-md">
|
||||||
|
{loading ? <Loader class="w-8" /> : (
|
||||||
|
<>
|
||||||
|
Datapack Version:{" "}
|
||||||
|
<span
|
||||||
|
class="relative cursor-pointer rounded-full bg-lime-200 text-black px-2 inline-block"
|
||||||
|
onClick={() => setShowVersions(!showVersions)}
|
||||||
|
>
|
||||||
|
{packVersion}
|
||||||
|
{showVersions && (
|
||||||
|
<ul class="absolute top-full left-0 bg-lime-50 rounded-md shadow-md p-2 min-w 12">
|
||||||
|
{packVersionList.map((version) => (
|
||||||
|
<li onClick={() => updatePackVersion(version)}>
|
||||||
|
{version}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
53
src/components/packsList.tsx
Normal file
53
src/components/packsList.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
type StateUpdater,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "preact/hooks";
|
||||||
|
import { Loader } from "./loader.tsx";
|
||||||
|
|
||||||
|
export const PacksList = (
|
||||||
|
{ setPackName }: { setPackName: Dispatch<StateUpdater<string>> },
|
||||||
|
) => {
|
||||||
|
const [packs, setPacks] = useState<{ name: string; path: string }[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/packs").then((res) => res.json()).then((json) => {
|
||||||
|
setPacks(json);
|
||||||
|
}).finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setPackNameThing = async (name: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
const body = new FormData();
|
||||||
|
body.set("packName", name);
|
||||||
|
const res = await fetch("/api/pack", {
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
document.title = json.packName;
|
||||||
|
setPackName(json.packName);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return loading ? <Loader /> : (
|
||||||
|
<ul>
|
||||||
|
{packs.length
|
||||||
|
? packs.map((pack) => (
|
||||||
|
<li
|
||||||
|
class="cursor-pointer even:bg-black/5"
|
||||||
|
onClick={() => setPackNameThing(pack.name)}
|
||||||
|
>
|
||||||
|
{pack.name}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
: <li>No packs found</li>}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
26
src/components/portal.tsx
Normal file
26
src/components/portal.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { createPortal } from "preact/compat";
|
||||||
|
import type { FunctionComponent } from "preact";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
el?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Portal: FunctionComponent<IProps> = (
|
||||||
|
{ children, className = "root-portal", el = "div" },
|
||||||
|
) => {
|
||||||
|
const [container, setContainer] = useState<HTMLElement>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = document.createElement(el);
|
||||||
|
container.classList.add(className);
|
||||||
|
document.body.appendChild(container);
|
||||||
|
setContainer(container);
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(container);
|
||||||
|
};
|
||||||
|
}, [className, el]);
|
||||||
|
|
||||||
|
return container ? createPortal(children, container) : <></>;
|
||||||
|
};
|
42
src/hooks/useStream.ts
Normal file
42
src/hooks/useStream.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
export const useStream = <T>(
|
||||||
|
stream: ReadableStream<T>,
|
||||||
|
onData: (data: T) => void,
|
||||||
|
onError: (err: Error) => void,
|
||||||
|
) => {
|
||||||
|
const reader = stream.getReader();
|
||||||
|
const read = async () => {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
reader.releaseLock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onData(value);
|
||||||
|
read();
|
||||||
|
};
|
||||||
|
read();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoteStream = <T>(
|
||||||
|
url: string,
|
||||||
|
onData: (data: T) => void,
|
||||||
|
onError: (err: Error) => void,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
res.body?.pipeThrough(new TextDecoderStream())
|
||||||
|
.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write(chunk) {
|
||||||
|
controller.enqueue(chunk);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
useStream(stream, onData, onError);
|
||||||
|
}, [url]);
|
||||||
|
};
|
@ -1,18 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
:root {
|
|
||||||
@apply dark:bg-mixed-600 bg-primary-100 text-dark-600 dark:text-white;
|
|
||||||
}
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body, #app, html {
|
|
||||||
@apply h-full;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
@apply bg-primary-600 text-white rounded-md px-4 py-2 font-bold;
|
|
||||||
}
|
|
||||||
}
|
|
50
src/index.less
Normal file
50
src/index.less
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
@apply dark:bg-mixed-600 bg-primary-100 text-dark-600 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
#app,
|
||||||
|
html {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@apply bg-primary-600 text-white rounded-md px-4 py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
@apply bg-white text-dark-600 rounded-md px-4 py-2;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
@apply mr-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.hollow {
|
||||||
|
@apply bg-transparent p-0 inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import { render } from 'preact'
|
import { render } from "preact";
|
||||||
import { App } from './app.tsx'
|
import { App } from "./app.tsx";
|
||||||
import './index.css'
|
|
||||||
|
|
||||||
render(<App />, document.getElementById('app') as HTMLElement)
|
render(<App />, document.getElementById("app") as HTMLElement);
|
||||||
|
3
src/util/classes.ts
Normal file
3
src/util/classes.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const classList = (...classes: (string | undefined)[]) => {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
|
};
|
2
src/util/fetchJson.ts
Normal file
2
src/util/fetchJson.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const fetchJson = (url: string, init?: RequestInit) =>
|
||||||
|
fetch(url, init).then((res) => res.json());
|
137
src/views/editor.tsx
Normal file
137
src/views/editor.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { Modal } from "../components/modal.tsx";
|
||||||
|
import { LabelledHr } from "../components/labelledHr.tsx";
|
||||||
|
import { PacksList } from "../components/packsList.tsx";
|
||||||
|
import { PackInfo } from "../components/packInfo.tsx";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { namespaceAtom } from "../atoms/namespace.ts";
|
||||||
|
import { Outlet, Route, Routes } from "react-router-dom";
|
||||||
|
import { Selector } from "../components/editor/selector.tsx";
|
||||||
|
import { NamespaceModal } from "../components/editor/namespaceModal.tsx";
|
||||||
|
import { EditorWrapper } from "../components/editor/wrapper.tsx";
|
||||||
|
import { TagRouter } from "../components/editor/tags/editor.tsx";
|
||||||
|
|
||||||
|
export const Editor = () => {
|
||||||
|
const [packName, setPackName] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [namespaces, setNamespaces] = useState<string[]>([]);
|
||||||
|
const [namespace, setNamespace] = useAtom(namespaceAtom);
|
||||||
|
|
||||||
|
const fetchNamespaces = async () => {
|
||||||
|
const res = await fetch("/api/pack/namespaces");
|
||||||
|
const json = await res.json();
|
||||||
|
setNamespaces(json);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = "BearMetal Packer";
|
||||||
|
fetch("/api/pack").then((res) => res.json()).then((json) => {
|
||||||
|
setPackName(json.packName);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
fetchNamespaces();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setPackNameThing = async (event: SubmitEvent) => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch("/api/pack", {
|
||||||
|
method: "POST",
|
||||||
|
body: new FormData(event.target as HTMLFormElement),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
document.title = packName;
|
||||||
|
setPackName(packName);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!packName) {
|
||||||
|
return (
|
||||||
|
<Modal>
|
||||||
|
{loading
|
||||||
|
? <p>Just a sec, trying to put the cats back in the bag.</p>
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
onSubmit={setPackNameThing}
|
||||||
|
>
|
||||||
|
<label class="w-full">Set pack name</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="text" name="packName" />
|
||||||
|
<button type="submit">Set</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<LabelledHr>OR</LabelledHr>
|
||||||
|
<PacksList setPackName={setPackName} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [showNamespaceModal, setShowNamespaceModal] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex h-full">
|
||||||
|
<div class="w-1/4 p-4 bg-mixed-400">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-2xl">BearMetalPacker</h1>
|
||||||
|
<PackInfo packName={packName} />
|
||||||
|
</div>
|
||||||
|
<LabelledHr>Namespaces</LabelledHr>
|
||||||
|
<div>
|
||||||
|
<ul class="w-full">
|
||||||
|
{namespaces.map((namespace) => (
|
||||||
|
<li
|
||||||
|
class="text-lg cursor-pointer even:bg-black/5"
|
||||||
|
onClick={() => setNamespace(namespace)}
|
||||||
|
>
|
||||||
|
{namespace}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li class="mt-4">
|
||||||
|
<button onClick={() => setShowNamespaceModal(true)}>
|
||||||
|
New Namespace
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{showNamespaceModal && (
|
||||||
|
<NamespaceModal
|
||||||
|
close={() => {
|
||||||
|
setShowNamespaceModal(false);
|
||||||
|
fetchNamespaces();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 w-max">
|
||||||
|
{!namespace ? <div>No namespace set</div> : (
|
||||||
|
<>
|
||||||
|
<h3 class="text-lg font-bold">
|
||||||
|
Namespace: <span class="italic">{namespace}</span>
|
||||||
|
</h3>
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<Selector />} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
element={
|
||||||
|
<EditorWrapper>
|
||||||
|
<Outlet />
|
||||||
|
</EditorWrapper>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route
|
||||||
|
path="tags/*"
|
||||||
|
element={<TagRouter />}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,25 +1,99 @@
|
|||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [mcPath, setMcPath] = useState("");
|
const [mcPath, setMcPath] = useState("");
|
||||||
|
const [worlds, setWorlds] = useState<{
|
||||||
|
world: string;
|
||||||
|
icon: string;
|
||||||
|
path: string;
|
||||||
|
}[]>([]);
|
||||||
|
|
||||||
|
const fetchWorlds = async () => {
|
||||||
|
const res = await fetch("/api/dir");
|
||||||
|
const json = await res.json();
|
||||||
|
setMcPath(json.mcPath);
|
||||||
|
setWorlds(json.worlds);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = "BearMetal Packer";
|
document.title = "BearMetal Packer";
|
||||||
|
fetchWorlds();
|
||||||
fetch("/api/dir").then(res => res.json()).then(data => {
|
|
||||||
setMcPath(data);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const submitMcPath = async (event: SubmitEvent) => {
|
||||||
|
setLoading(true);
|
||||||
|
event.preventDefault();
|
||||||
|
const form = new FormData(event.target as HTMLFormElement);
|
||||||
|
const res = await fetch("/api/dir", {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
fetchWorlds();
|
||||||
|
} else setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nav = useNavigate();
|
||||||
|
|
||||||
|
const setWorld = async (worldPath: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch("/api/world", {
|
||||||
|
method: "POST",
|
||||||
|
body: worldPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
nav("/editor");
|
||||||
|
} else setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="grid h-full">
|
<div class="grid h-full">
|
||||||
<div class="place-self-center text-center">
|
<div class="place-self-center text-center max-h-96 bg-primary-400 rounded-lg p-4 overflow-scroll">
|
||||||
<h1 class="text-3xl">Welcome BearMetal Packer</h1>
|
<h1 class="text-3xl">Welcome BearMetal Packer</h1>
|
||||||
<p>An all in one toolkit to build datapacks for Minecraft.</p>
|
<p>An all in one toolkit to build datapacks for Minecraft.</p>
|
||||||
<p>Hold tight, we're doing some heavy lifting.</p>
|
{loading ? <p>Hold tight, we're doing some heavy lifting.</p> : (
|
||||||
</div>
|
<>
|
||||||
</div>
|
{mcPath
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
<p>Minecraft directory set to {mcPath}</p>
|
||||||
|
<p>Worlds:</p>
|
||||||
|
<ul class="w-full even:bg-black/5">
|
||||||
|
{worlds.map((world) => (
|
||||||
|
<li
|
||||||
|
class="flex gap-4 items-center cursor-pointer"
|
||||||
|
onClick={() => setWorld(world.path)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={"/images?location=" + world.icon}
|
||||||
|
class="w-16 h-16"
|
||||||
|
/>
|
||||||
|
{world.world}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<p>No Minecraft directory set, please set now:</p>
|
||||||
|
<form method="POST" onSubmit={submitMcPath}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="mcPath"
|
||||||
|
placeholder="Minecraft directory"
|
||||||
|
/>
|
||||||
|
<button type="submit">Set</button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
@ -6,6 +6,10 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["Roboto", "sans-serif"],
|
||||||
|
mono: ["Roboto Mono", "monospace"],
|
||||||
|
},
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||||
"gradient-conic":
|
"gradient-conic":
|
||||||
|
7
types.ts
Normal file
7
types.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
declare global {
|
||||||
|
interface Context {
|
||||||
|
url: URL;
|
||||||
|
state: Record<string, unknown>;
|
||||||
|
params: Record<string, string | undefined>;
|
||||||
|
}
|
||||||
|
}
|
@ -16,12 +16,15 @@ export default defineConfig({
|
|||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8000",
|
target: "http://localhost:8000",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
// rewrite: (path) => path.replace(/^\/api/, ""),
|
|
||||||
},
|
},
|
||||||
"/puppet": {
|
"/puppet": {
|
||||||
target: "http://localhost:8000",
|
target: "http://localhost:8000",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
// rewrite: (path) => path.replace(/^\/puppet/, ""),
|
ws: true,
|
||||||
|
},
|
||||||
|
"/images": {
|
||||||
|
target: "http://localhost:8000",
|
||||||
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user