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",
|
"@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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -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,21 +60,32 @@ export const fetchViaProxy = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (final_url) {
|
if (final_url) {
|
||||||
const res = await fetch(
|
if (uploadProgress) {
|
||||||
final_url,
|
const res = await axios({
|
||||||
data
|
method: data ? "post" : undefined,
|
||||||
? {
|
url: final_url,
|
||||||
method: "POST",
|
data: body,
|
||||||
body,
|
onUploadProgress: uploadProgress,
|
||||||
headers,
|
});
|
||||||
}
|
|
||||||
: undefined
|
return res.data;
|
||||||
);
|
} else {
|
||||||
const raw = await res.text();
|
const res = await fetch(
|
||||||
try {
|
final_url,
|
||||||
return JSON.parse(raw);
|
data
|
||||||
} catch (e) {
|
? {
|
||||||
return raw;
|
method: "POST",
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
const raw = await res.text();
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,35 +102,93 @@ 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">
|
||||||
<div
|
<Panel order={1}>
|
||||||
className={cx("flex-1 flex h-full outline-none relative")}
|
{f.upload.started ? (
|
||||||
{...getRootProps()}
|
<div className="flex flex-col items-center justify-center flex-1 h-full">
|
||||||
>
|
<div
|
||||||
<EdFileList />
|
className={cx(
|
||||||
<input {...getInputProps()} />
|
"flex flex-col items-stretch min-w-[30%]"
|
||||||
{isDragActive && (
|
)}
|
||||||
<div className="absolute inset-0 flex items-center justify-center flex-col bg-blue-50 border-4 border-blue-500">
|
>
|
||||||
<svg
|
<div className="border-b pb-2">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
Uploading {Object.keys(f.upload.progress).length}{" "}
|
||||||
width="24"
|
files
|
||||||
height="24"
|
</div>
|
||||||
fill="none"
|
{Object.entries(f.upload.progress).map(
|
||||||
stroke="currentColor"
|
([name, progress]) => {
|
||||||
strokeLinecap="round"
|
return (
|
||||||
strokeLinejoin="round"
|
<div
|
||||||
strokeWidth="2"
|
className="flex justify-between border-b p-1"
|
||||||
className="lucide lucide-upload"
|
key={name}
|
||||||
viewBox="0 0 24 24"
|
>
|
||||||
|
<div>{name}</div>
|
||||||
|
<div>{Math.round(progress * 100)}%</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex-1 flex h-full outline-none relative"
|
||||||
|
)}
|
||||||
|
{...getRootProps()}
|
||||||
>
|
>
|
||||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
|
<EdFileList />
|
||||||
<path d="M17 8L12 3 7 8"></path>
|
<input {...getInputProps()} />
|
||||||
<path d="M12 3L12 15"></path>
|
{isDragActive && (
|
||||||
</svg>
|
<div className="absolute inset-0 flex items-center justify-center flex-col bg-blue-50 border-4 border-blue-500">
|
||||||
<div>Drag Here to Upload</div>
|
<svg
|
||||||
</div>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
className="lucide lucide-upload"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
|
||||||
|
<path d="M17 8L12 3 7 8"></path>
|
||||||
|
<path d="M12 3L12 15"></path>
|
||||||
|
</svg>
|
||||||
|
<div>Drag Here to Upload</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>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PanelGroup>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
</PanelGroup>
|
</PanelGroup>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
(e: KeyboardEvent) => {
|
||||||
local.multi = true;
|
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||||
local.render();
|
local.multi = true;
|
||||||
}
|
p.render();
|
||||||
|
|
||||||
if (e.altKey) {
|
|
||||||
local.inverse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {
|
|
||||||
f.selected.clear();
|
|
||||||
for (const item of tree) {
|
|
||||||
if (item.data) f.selected.add(item.data.name);
|
|
||||||
}
|
}
|
||||||
local.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.name) f.selected.add(item.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
if (confirm("Delete this file ?")) {
|
p.render();
|
||||||
await p.script.api._raw(`/_file${f.path}?del`);
|
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${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) {
|
||||||
f.selected.clear();
|
if (!local.multi) {
|
||||||
|
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;
|
||||||
f.selected.clear();
|
if (!local.multi) {
|
||||||
|
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)
|
||||||
) {
|
) {
|
||||||
f.selected.clear();
|
if (!local.multi) {
|
||||||
|
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("/");
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
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,10 +33,23 @@ export const EdFileTop = () => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className={cx("p-1 border-t flex")}>
|
<div className={cx("border-t flex justify-between")}>
|
||||||
<div className={topClass(p)} {...getRootProps()}>
|
<div className="flex p-1">
|
||||||
<input {...getInputProps()} />
|
<div className={topClass(p)} {...getRootProps()}>
|
||||||
Upload
|
<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>
|
</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>
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
|
|
|
||||||
|
|
@ -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("/");
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue