wip fix
This commit is contained in:
parent
bcfb1ced0c
commit
4d60d67f04
File diff suppressed because one or more lines are too long
|
|
@ -13,6 +13,7 @@
|
|||
"@wojtekmaj/react-qr-svg": "^1.0.0",
|
||||
"constrained-editor-plugin": "^1.3.0",
|
||||
"react-resizable-panels": "^2.0.9",
|
||||
"axios": "^1.6.7",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@paralleldrive/cuid2": "2.2.2",
|
||||
"react-contenteditable": "^3.3.7",
|
||||
|
|
|
|||
|
|
@ -28,14 +28,17 @@ export const apiProxy = (api_url: string) => {
|
|||
{
|
||||
get: (_, actionName: string) => {
|
||||
if (actionName === "_url") {
|
||||
return (pathname: string) => {
|
||||
return (pathname: string, proxy?: boolean) => {
|
||||
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);
|
||||
let final_url = "";
|
||||
|
||||
if (to_url.host === cur_url.host) {
|
||||
if (to_url.host === cur_url.host || proxy === false) {
|
||||
final_url = to_url.toString();
|
||||
} else {
|
||||
final_url = `${cur_url.protocol}//${
|
||||
|
|
@ -155,3 +158,4 @@ const fetchSendApi = async (url: string, params: any) => {
|
|||
"content-type": "application/json",
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import axios from "axios";
|
||||
|
||||
(BigInt.prototype as any).toJSON = function (): string {
|
||||
return `BigInt::` + this.toString();
|
||||
};
|
||||
|
|
@ -12,6 +14,7 @@ export const fetchViaProxy = async (
|
|||
|
||||
let body = null as any;
|
||||
let isFile = false;
|
||||
let uploadProgress = null as any;
|
||||
|
||||
const files: File[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
|
|
@ -20,11 +23,15 @@ export const fetchViaProxy = async (
|
|||
files.push(item);
|
||||
isFile = true;
|
||||
}
|
||||
if (typeof item === "function") {
|
||||
uploadProgress = item;
|
||||
}
|
||||
}
|
||||
} else if (data instanceof File) {
|
||||
isFile = true;
|
||||
files.push(data);
|
||||
}
|
||||
|
||||
if (!isFile) {
|
||||
body = JSON.stringify(data);
|
||||
headers["content-type"] = "aplication/json";
|
||||
|
|
@ -53,6 +60,16 @@ export const fetchViaProxy = async (
|
|||
}
|
||||
|
||||
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(
|
||||
final_url,
|
||||
data
|
||||
|
|
@ -71,5 +88,6 @@ export const fetchViaProxy = async (
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -255,6 +255,12 @@ export const EDGlobal = {
|
|||
HTMLElement,
|
||||
MouseEvent
|
||||
>,
|
||||
|
||||
preview: true,
|
||||
upload: {
|
||||
started: false,
|
||||
progress: {} as Record<string, number>,
|
||||
},
|
||||
},
|
||||
code: {
|
||||
init: false,
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import { EdFileTop } from "./file-top";
|
|||
import { EdFileTree, reloadFileTree } from "./file-tree";
|
||||
import { uploadFile } from "./file-upload";
|
||||
import { FEntry } from "./type";
|
||||
import { EdFilePreview } from "./file-preview";
|
||||
|
||||
export const EdFileBrowser = () => {
|
||||
const p = useGlobal(EDGlobal, "EDITOR");
|
||||
|
||||
const f = p.ui.popup.file;
|
||||
useEffect(() => {
|
||||
if (!p.script.api && 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[]) => {
|
||||
if (Array.isArray(e)) {
|
||||
p.ui.popup.file.entry = { "/": e };
|
||||
f.entry = { "/": e };
|
||||
|
||||
if (p.ui.popup.file.open) {
|
||||
if (f.open) {
|
||||
reloadFileTree(p);
|
||||
}
|
||||
p.ui.popup.file.enabled = true;
|
||||
f.enabled = true;
|
||||
p.render();
|
||||
}
|
||||
});
|
||||
|
|
@ -40,14 +41,14 @@ export const EdFileBrowser = () => {
|
|||
noClick: true,
|
||||
});
|
||||
|
||||
if (!p.ui.popup.file.enabled) return null;
|
||||
if (!f.enabled) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="items-center flex px-2 cursor-pointer border border-transparent hover:bg-slate-200 transition-all hover:border-black"
|
||||
onClick={() => {
|
||||
p.ui.popup.file.open = true;
|
||||
f.open = true;
|
||||
p.render();
|
||||
reloadFileTree(p);
|
||||
}}
|
||||
|
|
@ -71,10 +72,10 @@ export const EdFileBrowser = () => {
|
|||
|
||||
<Modal
|
||||
fade={false}
|
||||
open={p.ui.popup.file.open}
|
||||
open={f.open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
p.ui.popup.file.open = false;
|
||||
f.open = false;
|
||||
p.render();
|
||||
}
|
||||
}}
|
||||
|
|
@ -82,7 +83,6 @@ export const EdFileBrowser = () => {
|
|||
<div className={cx("bg-white select-none fixed inset-[50px] flex")}>
|
||||
<PanelGroup direction="horizontal" className="text-sm">
|
||||
<Panel
|
||||
id="tree"
|
||||
defaultSize={parseInt(
|
||||
localStorage.getItem("panel-file-left") || "18"
|
||||
)}
|
||||
|
|
@ -102,9 +102,39 @@ export const EdFileBrowser = () => {
|
|||
<Panel order={2}>
|
||||
<div className="flex-1 flex h-full flex-col">
|
||||
<EdFileTop />
|
||||
|
||||
<PanelGroup direction="horizontal">
|
||||
<Panel order={1}>
|
||||
{f.upload.started ? (
|
||||
<div className="flex flex-col items-center justify-center flex-1 h-full">
|
||||
<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()}
|
||||
>
|
||||
<EdFileList />
|
||||
|
|
@ -131,6 +161,34 @@ export const EdFileBrowser = () => {
|
|||
</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>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ import {
|
|||
import { FC, useCallback, useEffect } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { useGlobal, useLocal } from "web-utils";
|
||||
import { Menu, MenuItem } from "../../../../utils/ui/context-menu";
|
||||
import { EDGlobal, PG } from "../../logic/ed-global";
|
||||
import { FEntry } from "./type";
|
||||
import { Menu, MenuItem } from "../../../../utils/ui/context-menu";
|
||||
import { reloadFileTree } from "./file-tree";
|
||||
|
||||
const Tree = DNDTree<FEntry>;
|
||||
|
||||
|
|
@ -33,28 +34,38 @@ export const EdFileList = () => {
|
|||
container: null as null | HTMLDivElement,
|
||||
});
|
||||
|
||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||
local.multi = true;
|
||||
local.render();
|
||||
p.render();
|
||||
}
|
||||
|
||||
if (e.altKey) {
|
||||
local.inverse = true;
|
||||
local.multi = true;
|
||||
}
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {
|
||||
if (document.activeElement?.tagName.toLowerCase() !== "input") {
|
||||
f.selected.clear();
|
||||
const tree = f.entry[f.path];
|
||||
if (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(() => {
|
||||
local.multi = false;
|
||||
local.inverse = false;
|
||||
local.render();
|
||||
p.render();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -87,22 +98,51 @@ export const EdFileList = () => {
|
|||
>
|
||||
<MenuItem
|
||||
label={"Rename"}
|
||||
disabled={f.selected.size === 0}
|
||||
disabled={f.selected.size !== 1}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
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
|
||||
label={"Delete"}
|
||||
disabled={f.selected.size === 0}
|
||||
onClick={async (e) => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
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 ?")) {
|
||||
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>
|
||||
|
|
@ -148,7 +188,9 @@ export const EdFileList = () => {
|
|||
}
|
||||
|
||||
if (sq.el && sq.box.w > 5 && sq.box.h > 5) {
|
||||
if (!local.multi) {
|
||||
f.selected.clear();
|
||||
}
|
||||
for (const [name, el] of Object.entries(local.els)) {
|
||||
if (overlaps(sq.el, el)) {
|
||||
if (!local.inverse) {
|
||||
|
|
@ -163,17 +205,16 @@ export const EdFileList = () => {
|
|||
local.square.up = () => {
|
||||
window.removeEventListener("pointerup", local.square.up);
|
||||
local.square.up = null;
|
||||
local.multi = false;
|
||||
setTimeout(() => {
|
||||
local.square.started = false;
|
||||
local.render();
|
||||
p.render();
|
||||
});
|
||||
};
|
||||
window.addEventListener("pointerup", local.square.up);
|
||||
}
|
||||
}
|
||||
|
||||
local.render();
|
||||
p.render();
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
|
|
@ -184,7 +225,7 @@ export const EdFileList = () => {
|
|||
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();
|
||||
p.render();
|
||||
}
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
|
|
@ -192,7 +233,7 @@ export const EdFileList = () => {
|
|||
if (!sq.disabled && sq.started) {
|
||||
sq.started = false;
|
||||
}
|
||||
local.render();
|
||||
p.render();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
@ -202,14 +243,17 @@ export const EdFileList = () => {
|
|||
}
|
||||
}}
|
||||
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`
|
||||
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"
|
||||
sq.started ? "opacity-100" : "opacity-0",
|
||||
local.inverse
|
||||
? "bg-orange-200 border-orange-500"
|
||||
: "bg-blue-200 border-blue-500"
|
||||
)}
|
||||
></div>
|
||||
|
||||
|
|
@ -221,13 +265,18 @@ export const EdFileList = () => {
|
|||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
|
||||
li {
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
`
|
||||
)}
|
||||
onPointerDown={() => {
|
||||
if (!sq.disabled) {
|
||||
if (!sq.disabled && !local.multi) {
|
||||
f.selected.clear();
|
||||
local.render();
|
||||
p.render();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -259,7 +308,7 @@ export const EdFileList = () => {
|
|||
sq.start.x = e.clientX - container.x;
|
||||
sq.start.y = el.scrollTop + e.clientY - container.y;
|
||||
sq.box = { x: 0, y: 0, w: 0, h: 0 };
|
||||
local.render();
|
||||
p.render();
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
|
@ -270,11 +319,11 @@ export const EdFileList = () => {
|
|||
}}
|
||||
onDragStart={() => {
|
||||
sq.started = false;
|
||||
local.render();
|
||||
p.render();
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
sq.item_drag = false;
|
||||
local.render();
|
||||
p.render();
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
|
|
@ -331,7 +380,7 @@ const FileItem: FC<{
|
|||
onPointerDown={(ev) => {
|
||||
if (f.selected.has(e.name)) {
|
||||
local.square.disabled = true;
|
||||
local.render();
|
||||
p.render();
|
||||
return;
|
||||
}
|
||||
if (!local.square.item_drag) {
|
||||
|
|
@ -341,9 +390,11 @@ const FileItem: FC<{
|
|||
}
|
||||
if (!local.square.started && f.selected.size <= 1) {
|
||||
local.square.disabled = true;
|
||||
if (!local.multi) {
|
||||
f.selected.clear();
|
||||
}
|
||||
f.selected.add(e.name);
|
||||
local.render();
|
||||
p.render();
|
||||
}
|
||||
}}
|
||||
onPointerUp={(ev) => {
|
||||
|
|
@ -351,7 +402,7 @@ const FileItem: FC<{
|
|||
if (local.square.disabled) {
|
||||
ev.stopPropagation();
|
||||
local.square.disabled = false;
|
||||
local.render();
|
||||
p.render();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (
|
||||
|
|
@ -359,9 +410,11 @@ const FileItem: FC<{
|
|||
local.square.box.h < 10 &&
|
||||
!f.selected.has(e.name)
|
||||
) {
|
||||
if (!local.multi) {
|
||||
f.selected.clear();
|
||||
}
|
||||
f.selected.add(e.name);
|
||||
local.render();
|
||||
p.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -385,7 +438,11 @@ const FileItem: FC<{
|
|||
{isImage(ext) ? (
|
||||
<img
|
||||
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)"}
|
||||
onError={() => {
|
||||
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;
|
||||
};
|
||||
function overlaps(a: HTMLDivElement, b: HTMLDivElement) {
|
||||
|
|
@ -424,3 +481,24 @@ function overlaps(a: HTMLDivElement, b: HTMLDivElement) {
|
|||
const isOverlapping = isInHoriztonalBounds && isInVerticalBounds;
|
||||
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("/");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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]}`;
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ export const EdFileTop = () => {
|
|||
for (let i = 0; i <= idx; i++) npath.push(paths[i]);
|
||||
return (
|
||||
<div
|
||||
key={part}
|
||||
key={`${part}-${idx}`}
|
||||
className={breadClass(p)}
|
||||
onClick={() => breadClick(p, "/" + npath.join("/"))}
|
||||
>
|
||||
|
|
@ -33,12 +33,25 @@ export const EdFileTop = () => {
|
|||
);
|
||||
})}
|
||||
</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()}>
|
||||
<input {...getInputProps()} />
|
||||
Upload
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 ",
|
||||
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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ const TreeItem: FC<{
|
|||
f.path === path && "border-r-2 bg-blue-100 border-r-blue-700"
|
||||
)}
|
||||
onClick={() => {
|
||||
f.selected.clear();
|
||||
f.path = path;
|
||||
p.render();
|
||||
if (!f.expanded[path] || !f.entry[path]) {
|
||||
|
|
@ -173,13 +174,21 @@ const TreeItem: FC<{
|
|||
<MenuItem
|
||||
label={"Delete"}
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
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!");
|
||||
} else {
|
||||
await p.script.api._raw(`/_file${f.tree_ctx_path}?del`);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,66 @@ 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);
|
||||
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);
|
||||
};
|
||||
|
||||
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("/");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const MenuItem = forwardRef<
|
|||
return (
|
||||
<button
|
||||
{...props}
|
||||
className="MenuItem flex justify-between items-center"
|
||||
className="MenuItem flex justify-between items-center select-none"
|
||||
ref={ref}
|
||||
role="menuitem"
|
||||
disabled={disabled}
|
||||
|
|
|
|||
Loading…
Reference in New Issue