This commit is contained in:
Rizky 2024-02-24 16:19:53 +07:00
parent bcfb1ced0c
commit 4d60d67f04
13 changed files with 582 additions and 158 deletions

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,7 @@
"@wojtekmaj/react-qr-svg": "^1.0.0", "@wojtekmaj/react-qr-svg": "^1.0.0",
"constrained-editor-plugin": "^1.3.0", "constrained-editor-plugin": "^1.3.0",
"react-resizable-panels": "^2.0.9", "react-resizable-panels": "^2.0.9",
"axios": "^1.6.7",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@paralleldrive/cuid2": "2.2.2", "@paralleldrive/cuid2": "2.2.2",
"react-contenteditable": "^3.3.7", "react-contenteditable": "^3.3.7",

View File

@ -28,14 +28,17 @@ export const apiProxy = (api_url: string) => {
{ {
get: (_, actionName: string) => { get: (_, actionName: string) => {
if (actionName === "_url") { if (actionName === "_url") {
return (pathname: string) => { return (pathname: string, proxy?: boolean) => {
const to_url = new URL(base_url); const to_url = new URL(base_url);
to_url.pathname = pathname; to_url.pathname = pathname
.split("/")
.filter((e) => e)
.join("/");
const cur_url = new URL(location.href); const cur_url = new URL(location.href);
let final_url = ""; let final_url = "";
if (to_url.host === cur_url.host) { if (to_url.host === cur_url.host || proxy === false) {
final_url = to_url.toString(); final_url = to_url.toString();
} else { } else {
final_url = `${cur_url.protocol}//${ final_url = `${cur_url.protocol}//${
@ -155,3 +158,4 @@ const fetchSendApi = async (url: string, params: any) => {
"content-type": "application/json", "content-type": "application/json",
}); });
}; };

View File

@ -1,3 +1,5 @@
import axios from "axios";
(BigInt.prototype as any).toJSON = function (): string { (BigInt.prototype as any).toJSON = function (): string {
return `BigInt::` + this.toString(); return `BigInt::` + this.toString();
}; };
@ -12,6 +14,7 @@ export const fetchViaProxy = async (
let body = null as any; let body = null as any;
let isFile = false; let isFile = false;
let uploadProgress = null as any;
const files: File[] = []; const files: File[] = [];
if (Array.isArray(data)) { if (Array.isArray(data)) {
@ -20,11 +23,15 @@ export const fetchViaProxy = async (
files.push(item); files.push(item);
isFile = true; isFile = true;
} }
if (typeof item === "function") {
uploadProgress = item;
}
} }
} else if (data instanceof File) { } else if (data instanceof File) {
isFile = true; isFile = true;
files.push(data); files.push(data);
} }
if (!isFile) { if (!isFile) {
body = JSON.stringify(data); body = JSON.stringify(data);
headers["content-type"] = "aplication/json"; headers["content-type"] = "aplication/json";
@ -53,6 +60,16 @@ export const fetchViaProxy = async (
} }
if (final_url) { if (final_url) {
if (uploadProgress) {
const res = await axios({
method: data ? "post" : undefined,
url: final_url,
data: body,
onUploadProgress: uploadProgress,
});
return res.data;
} else {
const res = await fetch( const res = await fetch(
final_url, final_url,
data data
@ -71,5 +88,6 @@ export const fetchViaProxy = async (
} }
} }
} }
}
return null; return null;
}; };

View File

@ -255,6 +255,12 @@ export const EDGlobal = {
HTMLElement, HTMLElement,
MouseEvent MouseEvent
>, >,
preview: true,
upload: {
started: false,
progress: {} as Record<string, number>,
},
}, },
code: { code: {
init: false, init: false,

View File

@ -10,10 +10,11 @@ import { EdFileTop } from "./file-top";
import { EdFileTree, reloadFileTree } from "./file-tree"; import { EdFileTree, reloadFileTree } from "./file-tree";
import { uploadFile } from "./file-upload"; import { uploadFile } from "./file-upload";
import { FEntry } from "./type"; import { FEntry } from "./type";
import { EdFilePreview } from "./file-preview";
export const EdFileBrowser = () => { export const EdFileBrowser = () => {
const p = useGlobal(EDGlobal, "EDITOR"); const p = useGlobal(EDGlobal, "EDITOR");
const f = p.ui.popup.file;
useEffect(() => { useEffect(() => {
if (!p.script.api && p.site.config?.api_url) { if (!p.script.api && p.site.config?.api_url) {
p.script.api = apiProxy(p.site.config.api_url); p.script.api = apiProxy(p.site.config.api_url);
@ -24,12 +25,12 @@ export const EdFileBrowser = () => {
p.script.api._raw(`/_file/?dir`).then((e: FEntry[]) => { p.script.api._raw(`/_file/?dir`).then((e: FEntry[]) => {
if (Array.isArray(e)) { if (Array.isArray(e)) {
p.ui.popup.file.entry = { "/": e }; f.entry = { "/": e };
if (p.ui.popup.file.open) { if (f.open) {
reloadFileTree(p); reloadFileTree(p);
} }
p.ui.popup.file.enabled = true; f.enabled = true;
p.render(); p.render();
} }
}); });
@ -40,14 +41,14 @@ export const EdFileBrowser = () => {
noClick: true, noClick: true,
}); });
if (!p.ui.popup.file.enabled) return null; if (!f.enabled) return null;
return ( return (
<> <>
<div <div
className="items-center flex px-2 cursor-pointer border border-transparent hover:bg-slate-200 transition-all hover:border-black" className="items-center flex px-2 cursor-pointer border border-transparent hover:bg-slate-200 transition-all hover:border-black"
onClick={() => { onClick={() => {
p.ui.popup.file.open = true; f.open = true;
p.render(); p.render();
reloadFileTree(p); reloadFileTree(p);
}} }}
@ -71,10 +72,10 @@ export const EdFileBrowser = () => {
<Modal <Modal
fade={false} fade={false}
open={p.ui.popup.file.open} open={f.open}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
p.ui.popup.file.open = false; f.open = false;
p.render(); p.render();
} }
}} }}
@ -82,7 +83,6 @@ export const EdFileBrowser = () => {
<div className={cx("bg-white select-none fixed inset-[50px] flex")}> <div className={cx("bg-white select-none fixed inset-[50px] flex")}>
<PanelGroup direction="horizontal" className="text-sm"> <PanelGroup direction="horizontal" className="text-sm">
<Panel <Panel
id="tree"
defaultSize={parseInt( defaultSize={parseInt(
localStorage.getItem("panel-file-left") || "18" localStorage.getItem("panel-file-left") || "18"
)} )}
@ -102,9 +102,39 @@ export const EdFileBrowser = () => {
<Panel order={2}> <Panel order={2}>
<div className="flex-1 flex h-full flex-col"> <div className="flex-1 flex h-full flex-col">
<EdFileTop /> <EdFileTop />
<PanelGroup direction="horizontal">
<Panel order={1}>
{f.upload.started ? (
<div className="flex flex-col items-center justify-center flex-1 h-full">
<div <div
className={cx("flex-1 flex h-full outline-none relative")} className={cx(
"flex flex-col items-stretch min-w-[30%]"
)}
>
<div className="border-b pb-2">
Uploading {Object.keys(f.upload.progress).length}{" "}
files
</div>
{Object.entries(f.upload.progress).map(
([name, progress]) => {
return (
<div
className="flex justify-between border-b p-1"
key={name}
>
<div>{name}</div>
<div>{Math.round(progress * 100)}%</div>
</div>
);
}
)}
</div>
</div>
) : (
<div
className={cx(
"flex-1 flex h-full outline-none relative"
)}
{...getRootProps()} {...getRootProps()}
> >
<EdFileList /> <EdFileList />
@ -131,6 +161,34 @@ export const EdFileBrowser = () => {
</div> </div>
)} )}
</div> </div>
)}
</Panel>
{f.preview && (
<>
<PanelResizeHandle
className={cx(
"border-r",
css`
width: 10px;
`
)}
/>
<Panel
order={2}
onResize={(e) => {
localStorage.setItem("panel-file-right", e + "");
}}
defaultSize={parseInt(
localStorage.getItem("panel-file-right") || "18"
)}
className="flex items-center justify-center"
minSize={12}
>
<EdFilePreview />
</Panel>
</>
)}
</PanelGroup>
</div> </div>
</Panel> </Panel>
</PanelGroup> </PanelGroup>

View File

@ -7,9 +7,10 @@ import {
import { FC, useCallback, useEffect } from "react"; import { FC, useCallback, useEffect } from "react";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import { useGlobal, useLocal } from "web-utils"; import { useGlobal, useLocal } from "web-utils";
import { Menu, MenuItem } from "../../../../utils/ui/context-menu";
import { EDGlobal, PG } from "../../logic/ed-global"; import { EDGlobal, PG } from "../../logic/ed-global";
import { FEntry } from "./type"; import { FEntry } from "./type";
import { Menu, MenuItem } from "../../../../utils/ui/context-menu"; import { reloadFileTree } from "./file-tree";
const Tree = DNDTree<FEntry>; const Tree = DNDTree<FEntry>;
@ -33,28 +34,38 @@ export const EdFileList = () => {
container: null as null | HTMLDivElement, container: null as null | HTMLDivElement,
}); });
const onKeyDown = useCallback((e: KeyboardEvent) => { const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.shiftKey || e.ctrlKey || e.metaKey) { if (e.shiftKey || e.ctrlKey || e.metaKey) {
local.multi = true; local.multi = true;
local.render(); p.render();
} }
if (e.altKey) { if (e.altKey) {
local.inverse = true; local.inverse = true;
local.multi = true;
} }
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {
if (document.activeElement?.tagName.toLowerCase() !== "input") {
f.selected.clear(); f.selected.clear();
const tree = f.entry[f.path];
if (tree) {
for (const item of tree) { for (const item of tree) {
if (item.data) f.selected.add(item.data.name); if (item.name) f.selected.add(item.name);
} }
local.render();
} }
}, []); p.render();
}
}
},
[f.entry[f.path]]
);
const onKeyUp = useCallback(() => { const onKeyUp = useCallback(() => {
local.multi = false; local.multi = false;
local.inverse = false; local.inverse = false;
local.render(); p.render();
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -87,22 +98,51 @@ export const EdFileList = () => {
> >
<MenuItem <MenuItem
label={"Rename"} label={"Rename"}
disabled={f.selected.size === 0} disabled={f.selected.size !== 1}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
f.file_ctx_menu_event = null;
p.render();
setTimeout(async () => {
const selected = [...f.selected];
const rename_to = prompt("Rename to:", selected[0]);
await p.script.api._raw(
`/_file${join(f.path, selected[0])}?rename=${rename_to}`
);
reloadFileList(p);
}, 100);
}} }}
/> />
<MenuItem <MenuItem
label={"Delete"} label={"Delete"}
disabled={f.selected.size === 0} disabled={f.selected.size === 0}
onClick={async (e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
f.file_ctx_menu_event = null;
p.render();
setTimeout(async () => {
const selected = [...f.selected].map((e) =>
f.path.endsWith("/") ? e : "/" + e
);
if (f.selected.size === 1) {
if (confirm("Delete this file ?")) { if (confirm("Delete this file ?")) {
await p.script.api._raw(`/_file${f.path}?del`); await p.script.api._raw(
`/_file${join(f.path, selected[0])}?del`
);
} }
} else {
if (confirm(`Delete ${f.selected.size} files?`)) {
for (const s of selected) {
await p.script.api._raw(`/_file${join(f.path, s)}?del`);
}
}
}
reloadFileTree(p);
}, 100);
}} }}
/> />
</Menu> </Menu>
@ -148,7 +188,9 @@ export const EdFileList = () => {
} }
if (sq.el && sq.box.w > 5 && sq.box.h > 5) { if (sq.el && sq.box.w > 5 && sq.box.h > 5) {
if (!local.multi) {
f.selected.clear(); f.selected.clear();
}
for (const [name, el] of Object.entries(local.els)) { for (const [name, el] of Object.entries(local.els)) {
if (overlaps(sq.el, el)) { if (overlaps(sq.el, el)) {
if (!local.inverse) { if (!local.inverse) {
@ -163,17 +205,16 @@ export const EdFileList = () => {
local.square.up = () => { local.square.up = () => {
window.removeEventListener("pointerup", local.square.up); window.removeEventListener("pointerup", local.square.up);
local.square.up = null; local.square.up = null;
local.multi = false;
setTimeout(() => { setTimeout(() => {
local.square.started = false; local.square.started = false;
local.render(); p.render();
}); });
}; };
window.addEventListener("pointerup", local.square.up); window.addEventListener("pointerup", local.square.up);
} }
} }
local.render(); p.render();
} }
}} }}
onPointerDown={(e) => { onPointerDown={(e) => {
@ -184,7 +225,7 @@ export const EdFileList = () => {
sq.start.x = e.clientX - box.x; sq.start.x = e.clientX - box.x;
sq.start.y = el.scrollTop + e.clientY - box.y; sq.start.y = el.scrollTop + e.clientY - box.y;
sq.box = { x: 0, y: 0, w: 0, h: 0 }; sq.box = { x: 0, y: 0, w: 0, h: 0 };
local.render(); p.render();
} }
}} }}
onPointerUp={() => { onPointerUp={() => {
@ -192,7 +233,7 @@ export const EdFileList = () => {
if (!sq.disabled && sq.started) { if (!sq.disabled && sq.started) {
sq.started = false; sq.started = false;
} }
local.render(); p.render();
}} }}
> >
<div <div
@ -202,14 +243,17 @@ export const EdFileList = () => {
} }
}} }}
className={cx( className={cx(
"bg-blue-200 border border-blue-500 absolute z-10 bg-opacity-30 transition-opacity pointer-events-none", "border absolute z-10 bg-opacity-30 transition-opacity pointer-events-none",
css` css`
left: ${sq.box.x}px; left: ${sq.box.x}px;
top: ${sq.box.y}px; top: ${sq.box.y}px;
width: ${sq.box.w}px; width: ${sq.box.w}px;
height: ${sq.box.h}px; height: ${sq.box.h}px;
`, `,
sq.started ? "opacity-100" : "opacity-0" sq.started ? "opacity-100" : "opacity-0",
local.inverse
? "bg-orange-200 border-orange-500"
: "bg-blue-200 border-blue-500"
)} )}
></div> ></div>
@ -221,13 +265,18 @@ export const EdFileList = () => {
display: flex; display: flex;
flex: 1; flex: 1;
flex-wrap: wrap; flex-wrap: wrap;
li {
margin-left: 5px;
margin-top: 5px;
}
} }
` `
)} )}
onPointerDown={() => { onPointerDown={() => {
if (!sq.disabled) { if (!sq.disabled && !local.multi) {
f.selected.clear(); f.selected.clear();
local.render(); p.render();
} }
}} }}
> >
@ -259,7 +308,7 @@ export const EdFileList = () => {
sq.start.x = e.clientX - container.x; sq.start.x = e.clientX - container.x;
sq.start.y = el.scrollTop + e.clientY - container.y; sq.start.y = el.scrollTop + e.clientY - container.y;
sq.box = { x: 0, y: 0, w: 0, h: 0 }; sq.box = { x: 0, y: 0, w: 0, h: 0 };
local.render(); p.render();
} }
} }
}} }}
@ -270,11 +319,11 @@ export const EdFileList = () => {
}} }}
onDragStart={() => { onDragStart={() => {
sq.started = false; sq.started = false;
local.render(); p.render();
}} }}
onDragEnd={() => { onDragEnd={() => {
sq.item_drag = false; sq.item_drag = false;
local.render(); p.render();
}} }}
/> />
</DndProvider> </DndProvider>
@ -331,7 +380,7 @@ const FileItem: FC<{
onPointerDown={(ev) => { onPointerDown={(ev) => {
if (f.selected.has(e.name)) { if (f.selected.has(e.name)) {
local.square.disabled = true; local.square.disabled = true;
local.render(); p.render();
return; return;
} }
if (!local.square.item_drag) { if (!local.square.item_drag) {
@ -341,9 +390,11 @@ const FileItem: FC<{
} }
if (!local.square.started && f.selected.size <= 1) { if (!local.square.started && f.selected.size <= 1) {
local.square.disabled = true; local.square.disabled = true;
if (!local.multi) {
f.selected.clear(); f.selected.clear();
}
f.selected.add(e.name); f.selected.add(e.name);
local.render(); p.render();
} }
}} }}
onPointerUp={(ev) => { onPointerUp={(ev) => {
@ -351,7 +402,7 @@ const FileItem: FC<{
if (local.square.disabled) { if (local.square.disabled) {
ev.stopPropagation(); ev.stopPropagation();
local.square.disabled = false; local.square.disabled = false;
local.render(); p.render();
} else { } else {
setTimeout(() => { setTimeout(() => {
if ( if (
@ -359,9 +410,11 @@ const FileItem: FC<{
local.square.box.h < 10 && local.square.box.h < 10 &&
!f.selected.has(e.name) !f.selected.has(e.name)
) { ) {
if (!local.multi) {
f.selected.clear(); f.selected.clear();
}
f.selected.add(e.name); f.selected.add(e.name);
local.render(); p.render();
} }
}); });
} }
@ -385,7 +438,11 @@ const FileItem: FC<{
{isImage(ext) ? ( {isImage(ext) ? (
<img <img
draggable={false} draggable={false}
src={p.script.api._url(`/_img/${f.path}/${e.name}?w=100`)} src={p.script.api._url(
`/_img${f.path.startsWith("/") ? f.path : `/${f.path}`}/${
e.name
}?w=100`
)}
alt={e.name + " thumbnail (100px)"} alt={e.name + " thumbnail (100px)"}
onError={() => { onError={() => {
item.no_image = true; item.no_image = true;
@ -411,7 +468,7 @@ const FileItem: FC<{
); );
}; };
const isImage = (ext: string) => { export const isImage = (ext: string) => {
if (["gif", "jpeg", "jpg", "png", "svg", "webp"].includes(ext)) return true; if (["gif", "jpeg", "jpg", "png", "svg", "webp"].includes(ext)) return true;
}; };
function overlaps(a: HTMLDivElement, b: HTMLDivElement) { function overlaps(a: HTMLDivElement, b: HTMLDivElement) {
@ -424,3 +481,24 @@ function overlaps(a: HTMLDivElement, b: HTMLDivElement) {
const isOverlapping = isInHoriztonalBounds && isInVerticalBounds; const isOverlapping = isInHoriztonalBounds && isInVerticalBounds;
return isOverlapping; return isOverlapping;
} }
export const reloadFileList = async (p: PG) => {
const f = p.ui.popup.file;
const res = await p.script.api._raw(`/_file${f.path}?dir`);
f.entry[f.path] = res;
p.render();
};
const join = (...arg: string[]) => {
let arr: string[] = [];
for (const s of arg) {
s.split("/").forEach((e) => {
arr.push(e);
});
}
arr = arr.filter((e) => e);
return "/" + arg.join("/");
};

View File

@ -0,0 +1,136 @@
import { useGlobal, useLocal } from "web-utils";
import { EDGlobal } from "../../logic/ed-global";
import { isImage } from "./file-list";
import { FEntry } from "./type";
export const EdFilePreview = () => {
const p = useGlobal(EDGlobal, "EDITOR");
const f = p.ui.popup.file;
const local = useLocal({ no_image: false });
const file_by_ext: Record<string, string[]> = {};
let ext = "";
let first = undefined as FEntry | undefined;
for (const file of f.selected) {
const f_ext = file.split(".").pop() || "";
if (f_ext) {
if (!ext) {
ext = f_ext;
first = f.entry[f.path]?.find((e) => e.name === file);
}
if (!file_by_ext[f_ext]) file_by_ext[f_ext] = [];
file_by_ext[f_ext].push(file);
}
}
return (
<>
{f.selected.size === 0 && (
<div className="flex flex-1 flex-col items-center">
Select File
<br />
to Preview
</div>
)}
{f.selected.size === 1 && (
<div className="flex flex-col items-stretch justify-start flex-1 h-full">
<a
className={cx(
"border-b flex items-center justify-center relative overflow-auto",
css`
height: 50%;
img {
object-fit: cover;
object-position: center top;
}
`
)}
href={p.script.api._url(
`/_file${
f.path.startsWith("/") ? f.path : `/${f.path}`
}/${first?.name}`
)}
target="_blank"
>
{!local.no_image ? (
<>
{isImage(ext) ? (
<img
draggable={false}
className="absolute inset-0 w-full h-full"
src={p.script.api._url(
`/_img${
f.path.startsWith("/") ? f.path : `/${f.path}`
}/${first?.name}?w=500&f=jpg`
)}
alt={" thumbnail (500px)"}
onError={() => {
local.no_image = true;
local.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>
)}
</a>
<div className="p-2 border-b flex justify-between">
<div>{first?.name}</div>
<div>{fileSize(first?.size || 0)}</div>
</div>
<input
type="text"
className="p-2 border-b flex justify-between"
value={p.script.api._url(
`/_file${
f.path.startsWith("/") ? f.path : `/${f.path}`
}/${first?.name}`,
false
)}
readOnly
onFocus={(e) => {
e.currentTarget.select();
}}
/>
</div>
)}
{f.selected.size > 1 && (
<div className="flex flex-col items-stretch flex-1">
<div className="pl-1">{f.selected.size} files selected:</div>
<div className="flex flex-col border-t">
{Object.entries(file_by_ext).map(([ext, file]) => {
return (
<div className="flex items-stretch border-b px-3" key={ext}>
<div className="min-w-[60px] border-r uppercase font-bold text-xs text-slate-600 flex items-center">
{ext}
</div>
<div className="flex-1 pl-1 items-center">
{file.length} file{file.length <= 1 ? "" : "s"}
</div>
</div>
);
})}
</div>
</div>
)}
</>
);
};
function fileSize(bytes: number): string {
const sizes = ["bytes", "KB", "MB", "GB", "TB"];
if (bytes === 0) return "0 bytes";
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const size = i === 0 ? bytes : (bytes / Math.pow(1024, i)).toFixed(2);
return `${size} ${sizes[i]}`;
}

View File

@ -24,7 +24,7 @@ export const EdFileTop = () => {
for (let i = 0; i <= idx; i++) npath.push(paths[i]); for (let i = 0; i <= idx; i++) npath.push(paths[i]);
return ( return (
<div <div
key={part} key={`${part}-${idx}`}
className={breadClass(p)} className={breadClass(p)}
onClick={() => breadClick(p, "/" + npath.join("/"))} onClick={() => breadClick(p, "/" + npath.join("/"))}
> >
@ -33,12 +33,25 @@ export const EdFileTop = () => {
); );
})} })}
</div> </div>
<div className={cx("p-1 border-t flex")}> <div className={cx("border-t flex justify-between")}>
<div className="flex p-1">
<div className={topClass(p)} {...getRootProps()}> <div className={topClass(p)} {...getRootProps()}>
<input {...getInputProps()} /> <input {...getInputProps()} />
Upload Upload
</div> </div>
</div> </div>
<div
className={
"flex border-l items-center justify-center min-w-[30px] cursor-pointer hover:bg-blue-50"
}
onClick={() => {
f.preview = !f.preview;
p.render();
}}
>
{!f.preview ? <PreviewLeft /> : <PreviewRight />}
</div>
</div>
</div> </div>
); );
}; };
@ -56,3 +69,40 @@ const topClass = (p: PG, className?: string) =>
"border px-2 mr-1 rounded-sm cursor-pointer hover:bg-blue-100 hover:text-blue-700 border-slate-600 hover:border-blue-600 ", "border px-2 mr-1 rounded-sm cursor-pointer hover:bg-blue-100 hover:text-blue-700 border-slate-600 hover:border-blue-600 ",
className className
); );
const icon_size = 17;
const PreviewRight = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={icon_size}
height={icon_size}
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="lucide lucide-panel-right-close"
viewBox="0 0 24 24"
>
<rect width="18" height="18" x="3" y="3" rx="2"></rect>
<path d="M15 3v18M8 9l3 3-3 3"></path>
</svg>
);
const PreviewLeft = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={icon_size}
height={icon_size}
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="lucide lucide-panel-right-open"
viewBox="0 0 24 24"
>
<rect width="18" height="18" x="3" y="3" rx="2"></rect>
<path d="M9 3v18M16 15l-3-3 3-3"></path>
</svg>
);

View File

@ -104,6 +104,7 @@ const TreeItem: FC<{
f.path === path && "border-r-2 bg-blue-100 border-r-blue-700" f.path === path && "border-r-2 bg-blue-100 border-r-blue-700"
)} )}
onClick={() => { onClick={() => {
f.selected.clear();
f.path = path; f.path = path;
p.render(); p.render();
if (!f.expanded[path] || !f.entry[path]) { if (!f.expanded[path] || !f.entry[path]) {
@ -173,13 +174,21 @@ const TreeItem: FC<{
<MenuItem <MenuItem
label={"Delete"} label={"Delete"}
disabled={ disabled={
!(f.entry[f.tree_ctx_path] && f.entry[f.tree_ctx_path]?.length === 0) !(
f.entry[f.tree_ctx_path] &&
f.entry[f.tree_ctx_path]?.length === 0
)
} }
onClick={async (e) => { onClick={async (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!(f.entry[f.tree_ctx_path] && f.entry[f.tree_ctx_path]?.length === 0)) { if (
!(
f.entry[f.tree_ctx_path] &&
f.entry[f.tree_ctx_path]?.length === 0
)
) {
alert("Can only delete empty folder!"); alert("Can only delete empty folder!");
} else { } else {
await p.script.api._raw(`/_file${f.tree_ctx_path}?del`); await p.script.api._raw(`/_file${f.tree_ctx_path}?del`);

View File

@ -3,7 +3,66 @@ import { PG } from "../../logic/ed-global";
import { reloadFileTree } from "./file-tree"; import { reloadFileTree } from "./file-tree";
export const uploadFile = async (p: PG, files: File[]) => { export const uploadFile = async (p: PG, files: File[]) => {
await p.script.api._raw(`/_upload?to=${p.ui.popup.file.path}`, ...files); const f = p.ui.popup.file;
const promises: Promise<void>[] = [];
f.upload.started = true;
f.upload.progress = {};
const pr = f.upload.progress;
let folder_created = new Set<string>();
for (const file of files) {
let path = f.path;
let filename = (file as any).path ? (file as any).path : file.name;
if ((file as any).path) {
const arr = (file as any).path.split("/") as string[];
arr.pop();
path = join(path, ...arr);
const folder = arr.filter((e) => e).join("/");
if (folder) {
folder_created.add(folder);
}
}
if (!pr[filename]) {
pr[filename] = 0.1;
}
promises.push(
p.script.api._raw(`/_upload?to=${path}`, file, (arg: any) => {
pr[filename] = arg.progress;
p.render();
})
);
}
await Promise.all(promises);
alert(
`\
Uploaded Finished:
- ${files.length} files uploaded${
folder_created.size > 0
? `\n ${[...folder_created]
.map((e) => ` - Folder ${e} created.`)
.join("\n")}`
: ""
}`
);
f.upload.progress = {};
f.upload.started = false;
p.render();
reloadFileTree(p); reloadFileTree(p);
}; };
const join = (...arg: string[]) => {
let arr: string[] = [];
for (const s of arg) {
s.split("/").forEach((e) => {
arr.push(e);
});
}
arr = arr.filter((e) => !!e.trim());
return "/" + arg.join("/");
};

View File

@ -36,7 +36,7 @@ export const MenuItem = forwardRef<
return ( return (
<button <button
{...props} {...props}
className="MenuItem flex justify-between items-center" className="MenuItem flex justify-between items-center select-none"
ref={ref} ref={ref}
role="menuitem" role="menuitem"
disabled={disabled} disabled={disabled}

BIN
bun.lockb

Binary file not shown.