This commit is contained in:
Rizky 2024-02-23 00:03:39 +07:00
parent 9ba10f56d1
commit 61da09e69b
8 changed files with 318 additions and 15 deletions

File diff suppressed because one or more lines are too long

View File

@ -27,6 +27,25 @@ export const apiProxy = (api_url: string) => {
{},
{
get: (_, actionName: string) => {
if (actionName === "_url") {
return (pathname: string) => {
const to_url = new URL(base_url);
to_url.pathname = pathname;
const cur_url = new URL(location.href);
let final_url = "";
if (to_url.host === cur_url.host) {
final_url = to_url.toString();
} else {
final_url = `${cur_url.protocol}//${
cur_url.host
}/_proxy/${encodeURIComponent(to_url.toString())}`;
}
return final_url;
};
}
const createFn = (actionName: string) => {
return function (
this: { api_url: string } | undefined,

View File

@ -243,6 +243,7 @@ export const EDGlobal = {
tree: [] as NodeModel<FEntry>[],
renaming: "",
ctx_path: "",
selected: new Set<string>(),
ctx_menu_event: null as null | React.MouseEvent<
HTMLElement,
MouseEvent

View File

@ -1,14 +1,16 @@
import { useCallback, useEffect } from "react";
import { useEffect } from "react";
import { useDropzone } from "react-dropzone";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { useGlobal } from "web-utils";
import { apiProxy } from "../../../../base/load/api/api-proxy";
import { Modal } from "../../../../utils/ui/modal";
import { EDGlobal } from "../../logic/ed-global";
import { EdFileTree, reloadFileTree } from "./file-tree";
import { FEntry } from "./type";
import { EdFileList } from "./file-list";
import { EdFileTop } from "./file-top";
import { EdFileTree, reloadFileTree } from "./file-tree";
import { uploadFile } from "./file-upload";
import { FEntry } from "./type";
export const EdFileBrowser = () => {
const p = useGlobal(EDGlobal, "EDITOR");
@ -23,12 +25,14 @@ export const EdFileBrowser = () => {
p.script.api._raw(`/_file/?dir`).then((e: FEntry[]) => {
if (Array.isArray(e)) {
p.ui.popup.file.entry = { "/": e };
if (p.ui.popup.file.open) {
reloadFileTree(p);
}
p.ui.popup.file.enabled = true;
p.render();
}
});
reloadFileTree(p);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
@ -45,6 +49,7 @@ export const EdFileBrowser = () => {
onClick={() => {
p.ui.popup.file.open = true;
p.render();
reloadFileTree(p);
}}
>
<svg
@ -102,6 +107,7 @@ export const EdFileBrowser = () => {
className={cx("flex-1 flex h-full outline-none relative")}
{...getRootProps()}
>
<EdFileList />
<input {...getInputProps()} />
{isDragActive && (
<div className="absolute inset-0 flex items-center justify-center flex-col bg-blue-50 border-4 border-blue-500">

View File

@ -0,0 +1,232 @@
import {
Tree as DNDTree,
MultiBackend,
NodeModel,
getBackendOptions,
} from "@minoru/react-dnd-treeview";
import { FC, useCallback, useEffect } from "react";
import { DndProvider } from "react-dnd";
import { useGlobal, useLocal } from "web-utils";
import { EDGlobal, PG } from "../../logic/ed-global";
import { FEntry } from "./type";
const Tree = DNDTree<FEntry>;
export const EdFileList = () => {
const p = useGlobal(EDGlobal, "EDITOR");
const f = p.ui.popup.file;
const list = f.entry[f.path] || [];
const local = useLocal({
multi: false,
square: {
started: false,
start: { x: 0, y: 0 },
cur: { x: 0, y: 0 },
box: { x: 0, y: 0, w: 0, h: 0 },
},
});
const onKeyDown = useCallback((e: KeyboardEvent) => {
if (e.shiftKey || e.ctrlKey || e.metaKey) {
local.multi = true;
local.render();
}
}, []);
const onKeyUp = useCallback(() => {
local.multi = false;
local.render();
}, []);
useEffect(() => {
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keyup", onKeyUp);
};
}, []);
const tree: NodeModel<FEntry>[] = (f.entry[f.path] || [])
.filter((e) => e.type === "file")
.map((e) => {
return { id: e.name, parent: "", text: e.name, data: e };
});
const sq = local.square;
return (
<div
className="flex-1 select-none relative overflow-y-auto"
onPointerMove={(e) => {
const el = e.currentTarget;
if (sq.started) {
const box = el.getBoundingClientRect();
sq.cur.x = e.clientX - box.x;
sq.cur.y = e.clientY + el.scrollTop - box.y;
if (sq.start.x < sq.cur.x) {
sq.box.x = sq.start.x;
sq.box.w = sq.cur.x - sq.start.x;
} else {
sq.box.x = sq.cur.x;
sq.box.w = sq.start.x - sq.cur.x;
}
if (sq.start.y < sq.cur.y) {
sq.box.y = sq.start.y;
sq.box.h = sq.cur.y + -sq.start.y;
} else {
sq.box.y = sq.cur.y;
sq.box.h = sq.start.y - sq.cur.y;
}
if (sq.cur.y - el.scrollTop > box.height * 0.8) {
el.scrollTop += 5;
} else if (sq.cur.y - el.scrollTop < 50) {
el.scrollTop -= 5;
}
local.render();
}
}}
onPointerDown={(e) => {
const el = e.currentTarget;
const box = el.getBoundingClientRect();
sq.started = true;
sq.start.x = e.clientX - box.x;
sq.start.y = el.scrollTop + e.clientY - box.y;
sq.box = { x: 0, y: 0, w: 0, h: 0 };
local.render();
}}
onPointerUp={() => {
sq.started = false;
local.render();
}}
>
<div
className={cx(
"bg-blue-200 border border-blue-500 absolute z-10 bg-opacity-30 transition-opacity pointer-events-none",
css`
left: ${sq.box.x}px;
top: ${sq.box.y}px;
width: ${sq.box.w}px;
height: ${sq.box.h}px;
`,
sq.started ? "opacity-100" : "opacity-0"
)}
></div>
<div
className={cx(
"absolute inset-0 flex flex-wrap items-start content-start",
css`
ul {
display: flex;
flex: 1;
flex-wrap: wrap;
}
`
)}
onClick={() => {
f.selected.clear();
local.render();
}}
>
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree
tree={tree}
dragPreviewRender={() => <></>}
rootId=""
onDrop={() => {}}
render={(node, {}) => {
if (node.data) {
return <FileItem p={p} e={node.data} local={local} />;
}
return <></>;
}}
onDragStart={() => {
local.square.started = false;
local.render();
}}
/>
</DndProvider>
</div>
</div>
);
};
const FileItem: FC<{
p: PG;
e: FEntry;
local: { multi: boolean; render: () => void };
}> = ({ e, local, p }) => {
const f = p.ui.popup.file;
const ext = e.name.split(".").pop() || "";
const item = useLocal({ no_image: false });
return (
<div
key={e.name}
className={cx(
"flex items-stretch flex-col p-1 m-2 border-2",
css`
width: 100px;
`,
f.selected.has(e.name)
? "bg-blue-100 border-blue-600"
: "border-transparent"
)}
onClick={(ev) => {
ev.stopPropagation();
ev.preventDefault();
if (!local.multi) {
f.selected.clear();
}
if (!f.selected.has(e.name)) {
f.selected.add(e.name);
} else {
f.selected.delete(e.name);
}
local.render();
}}
>
<div
className={cx(
"flex items-center justify-center flex-1 border",
css`
min-height: 80px;
`
)}
>
{!item.no_image ? (
<>
{isImage(ext) ? (
<img
draggable={false}
src={p.script.api._url(`/_img/${f.path}/${e.name}?w=100`)}
alt={e.name + " thumbnail (100px)"}
onError={() => {
item.no_image = true;
item.render();
}}
/>
) : (
<div className="uppercase font-bold text-lg text-slate-300">
{ext}
</div>
)}
</>
) : (
<div className="uppercase font-bold text-lg text-slate-300">
NO IMG
</div>
)}
</div>
<div className="px-1 mt-2 text-ellipsis overflow-ellipsis whitespace-break-spaces break-words">
{e.name.length > 25 ? e.name.substring(0, 25) + "..." : e.name}
</div>
</div>
);
};
const isImage = (ext: string) => {
if (["gif", "jpeg", "jpg", "png", "svg", "webp"].includes(ext)) return true;
};

View File

@ -43,9 +43,17 @@ export const EdFileTree: FC<{}> = ({}) => {
return true;
}}
onDrop={async (newTree, { dropTargetId, dragSourceId }) => {
await p.script.api._raw(`/_file${dragSourceId}?move=${dropTargetId}`);
await reloadFileTree(p);
onDrop={async (newTree, { dropTargetId, dragSourceId, dragSource }) => {
if (dragSource) {
if (dragSource.data?.type === "file") {
} else {
await p.script.api._raw(
`/_file${dragSourceId}?move=${dropTargetId}`
);
await reloadFileTree(p);
}
}
}}
render={(
node,
@ -84,6 +92,16 @@ const TreeItem: FC<{
}
f.path = path;
if (!f.entry[path]) {
p.script.api._raw(`/_file${path}/?dir`).then((fe: FEntry[]) => {
if (Array.isArray(fe)) {
f.entry[path] = fe;
p.render();
}
});
}
p.render();
}}
onContextMenu={(e) => {
@ -274,7 +292,12 @@ export const reloadFileTree = async (p: PG) => {
if (exp) {
for (const [k, v] of Object.entries(p.ui.popup.file.entry)) {
if (p.ui.popup.file.entry[k] && !exp.includes(k) && k !== "/") {
if (
p.ui.popup.file.entry[k] &&
!exp.includes(k) &&
k !== "/" &&
k !== p.ui.popup.file.path
) {
delete p.ui.popup.file.entry[k];
}
}
@ -296,6 +319,16 @@ export const reloadFileTree = async (p: PG) => {
await Promise.all(promises);
}
const f = p.ui.popup.file;
if (!f.entry[f.path]) {
p.script.api._raw(`/_file${f.path}/?dir`).then((fe: FEntry[]) => {
if (Array.isArray(fe)) {
f.entry[f.path] = fe;
p.render();
}
});
}
const tree: NodeModel<FEntry>[] = p.ui.popup.file.tree;
tree.length = 0;

View File

@ -1,6 +1,9 @@
import { fetchViaProxy } from "../../../../base/load/proxy";
import { PG } from "../../logic/ed-global";
import { reloadFileTree } from "./file-tree";
export const uploadFile = async (p: PG, files: File[]) => {
await p.script.api._raw(`/_upload?to=${p.ui.popup.file.path}`, ...files);
reloadFileTree(p);
};

View File

@ -1,7 +1,5 @@
import { apiContext } from "service-srv";
import { gzipAsync } from "../../../app/srv/ws/sync/entity/zlib";
import { CORS_HEADERS } from "../server/serve-api";
import brotliPromise from "brotli-wasm";
import { apiContext } from "service-srv";
const brotli = await brotliPromise;
@ -12,11 +10,22 @@ export const _ = {
const { req } = apiContext(this);
try {
const url = new URL(decodeURIComponent(req.params["_"]));
const url = new URL(
decodeURIComponent(decodeURIComponent(req.params["_"]))
);
const body = await req.arrayBuffer();
const headers = {} as Record<string, string>;
req.headers.forEach((v, k) => {
if (k.startsWith("sec-")) return;
if (k.startsWith("connection")) return;
if (k.startsWith("dnt")) return;
if (k.startsWith("host")) return;
headers[k] = v;
});
return await fetch(url, {
method: req.method || "POST",
headers: req.headers,
headers,
body,
});
} catch (e: any) {