resource extraction and reading
This commit is contained in:
parent
3d9b877661
commit
44c1862869
4
.gitignore
vendored
4
.gitignore
vendored
@ -25,4 +25,6 @@ dist-ssr
|
|||||||
*.sw?
|
*.sw?
|
||||||
.env
|
.env
|
||||||
|
|
||||||
BearMetal/
|
BearMetal/
|
||||||
|
resources/
|
||||||
|
!**/*/resources/
|
@ -20,9 +20,11 @@
|
|||||||
"@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",
|
||||||
"@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/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",
|
"@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",
|
"jotai": "npm:jotai@^2.10.1",
|
||||||
|
6
deno.lock
generated
6
deno.lock
generated
@ -16,6 +16,7 @@
|
|||||||
"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:jotai@^2.10.1": "2.10.1",
|
||||||
@ -493,6 +494,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=="
|
||||||
},
|
},
|
||||||
@ -1414,12 +1418,14 @@
|
|||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@bearmetal/store@^0.0.5",
|
"jsr:@bearmetal/store@^0.0.5",
|
||||||
|
"jsr:@std/encoding@^1.0.5",
|
||||||
"jsr:@std/fs@^1.0.4",
|
"jsr:@std/fs@^1.0.4",
|
||||||
"jsr:@std/http@^1.0.8",
|
"jsr:@std/http@^1.0.8",
|
||||||
"jsr:@std/path@^1.0.6",
|
"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:jotai@^2.10.1",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "48",
|
"version": "48",
|
||||||
|
"mcVersion": "^1.21",
|
||||||
"schema": {
|
"schema": {
|
||||||
"pack.mcmeta": "json",
|
"pack.mcmeta": "json",
|
||||||
"pack.png": "image/png",
|
"pack.png": "image/png",
|
||||||
|
@ -5,6 +5,7 @@ import { ensureDir, ensureFile, exists } from "@std/fs";
|
|||||||
import { Router } from "./router.ts";
|
import { Router } from "./router.ts";
|
||||||
import { getPackVersion } from "./util/packVersion.ts";
|
import { getPackVersion } from "./util/packVersion.ts";
|
||||||
import { createTagRoutes } from "./tags/routes.ts";
|
import { createTagRoutes } from "./tags/routes.ts";
|
||||||
|
import { createResourcesRoutes } from "./resources/routes.ts";
|
||||||
|
|
||||||
const installPath = Deno.env.get("BMP_INSTALL_DIR") || "./";
|
const installPath = Deno.env.get("BMP_INSTALL_DIR") || "./";
|
||||||
|
|
||||||
@ -223,6 +224,7 @@ router.route("/api/versions")
|
|||||||
});
|
});
|
||||||
|
|
||||||
createTagRoutes(router);
|
createTagRoutes(router);
|
||||||
|
createResourcesRoutes(router);
|
||||||
|
|
||||||
sockpuppet.addHandler((req: Request) => {
|
sockpuppet.addHandler((req: Request) => {
|
||||||
if (new URL(req.url).pathname.startsWith("/api")) return;
|
if (new URL(req.url).pathname.startsWith("/api")) return;
|
||||||
|
80
server/resources/readers.ts
Normal file
80
server/resources/readers.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { encodeBase64 } from "@std/encoding/base64";
|
||||||
|
import { readDirFiles } from "../util/readDir.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 texPath = path + "/assets/minecraft/textures/block";
|
||||||
|
for await (const image of Deno.readDir(texPath)) {
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (image.name.startsWith(block.name)) {
|
||||||
|
const data = await Deno.readFile(texPath + "/" + image.name);
|
||||||
|
block.images.push("image/png;base64," + encodeBase64(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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: [],
|
||||||
|
})).slice(0, 10);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const data = await Deno.readFile(
|
||||||
|
path + "/assets/minecraft/models/item/" + 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("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("image/png;base64," + encodeBase64(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const path = "./resources/1.21.1";
|
||||||
|
console.log(await readItems(path));
|
||||||
|
}
|
37
server/resources/routes.ts
Normal file
37
server/resources/routes.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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");
|
||||||
|
for (const resourceVersion of resourceVersions) {
|
||||||
|
if (versionCompat(resourceVersion, mcVersion)) {
|
||||||
|
const resourcePath = "./resources/" + resourceVersion;
|
||||||
|
const splitPath = path.split("/");
|
||||||
|
switch (splitPath[0]) {
|
||||||
|
case "blocks": {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(await readBlocks(resourcePath)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
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();
|
||||||
|
}
|
@ -70,12 +70,13 @@ export const createTagRoutes = (router: Router) => {
|
|||||||
const tagDir = await getTagDir(store, ctx.params.namespace, version);
|
const tagDir = await getTagDir(store, ctx.params.namespace, version);
|
||||||
|
|
||||||
const tag = ctx.params.tag;
|
const tag = ctx.params.tag;
|
||||||
if (!tag) {
|
const type = ctx.params.type;
|
||||||
|
if (!tag || !type) {
|
||||||
return new Response("no tag name provided", { status: 400 });
|
return new Response("no tag name provided", { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tagFile = Deno.readTextFileSync(tagDir + "/" + tag + ".json");
|
const tagFile = Deno.readTextFileSync(`${tagDir}/${type}/${tag}.json`);
|
||||||
return new Response(tagFile, { status: 200 });
|
return new Response(tagFile, { status: 200 });
|
||||||
} catch {
|
} catch {
|
||||||
return new Response("no tag found", { status: 404 });
|
return new Response("no tag found", { status: 404 });
|
||||||
|
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] > targetVersionSplit[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import { atomWithStorage } from "jotai/utils";
|
import { atomWithStorage } from "jotai/utils";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
const key = "bmp:namespace";
|
const key = "bmp:namespace";
|
||||||
// const namespaceAtomPrimitive = atom<string>(localStorage.getItem(key) ??"");
|
// const namespaceAtomPrimitive = atom<string>(localStorage.getItem(key) ??"");
|
||||||
@ -12,3 +13,7 @@ const key = "bmp:namespace";
|
|||||||
// )
|
// )
|
||||||
|
|
||||||
export const namespaceAtom = atomWithStorage(key, "");
|
export const namespaceAtom = atomWithStorage(key, "");
|
||||||
|
|
||||||
|
export const useNamespace = () => {
|
||||||
|
return useAtom(namespaceAtom);
|
||||||
|
};
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import { Link, Route, Routes } from "react-router-dom";
|
import { Link, Route, Routes, useParams } from "react-router-dom";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { fetchJson } from "../../../util/fetchJson.ts";
|
import { fetchJson } from "../../../util/fetchJson.ts";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { namespaceAtom } from "../../../atoms/namespace.ts";
|
import { namespaceAtom, useNamespace } from "../../../atoms/namespace.ts";
|
||||||
import { Loader } from "../../../components/loader.tsx";
|
import { Loader } from "../../../components/loader.tsx";
|
||||||
import { useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { NewTagModal } from "./newTagModal.tsx";
|
import { NewTagModal } from "./newTagModal.tsx";
|
||||||
|
|
||||||
export const TagEditor = () => {
|
export const TagRouter = () => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index Component={TagList} />
|
<Route index Component={TagList} />
|
||||||
|
<Route path=":typeTag" Component={TagEditor} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -51,3 +52,76 @@ function TagList() {
|
|||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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]);
|
||||||
|
};
|
@ -9,7 +9,7 @@ import { Outlet, Route, Routes } from "react-router-dom";
|
|||||||
import { Selector } from "../components/editor/selector.tsx";
|
import { Selector } from "../components/editor/selector.tsx";
|
||||||
import { NamespaceModal } from "../components/editor/namespaceModal.tsx";
|
import { NamespaceModal } from "../components/editor/namespaceModal.tsx";
|
||||||
import { EditorWrapper } from "../components/editor/wrapper.tsx";
|
import { EditorWrapper } from "../components/editor/wrapper.tsx";
|
||||||
import { TagEditor } from "../components/editor/tags/editor.tsx";
|
import { TagRouter } from "../components/editor/tags/editor.tsx";
|
||||||
|
|
||||||
export const Editor = () => {
|
export const Editor = () => {
|
||||||
const [packName, setPackName] = useState("");
|
const [packName, setPackName] = useState("");
|
||||||
@ -125,7 +125,7 @@ export const Editor = () => {
|
|||||||
>
|
>
|
||||||
<Route
|
<Route
|
||||||
path="tags/*"
|
path="tags/*"
|
||||||
element={<TagEditor />}
|
element={<TagRouter />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user