mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 10:38:11 +02:00
Add file upload and download (zip for directories)
This commit is contained in:
1 parent
0d564982dc
commit
da4e02472f
7 files changed
+383
-42
No files matched your search
@@ -156,6 +156,25 @@ export function GetRawFileUrl(RootName, RelativePath) {
|
||||
return `${ApiBase}/raw/${encodeURIComponent(RootName)}/${EncodedSegments.join("/")}`;
|
||||
}
|
||||
|
||||
export function GetDownloadUrl(RootName, RelativePath) {
|
||||
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
||||
return `${ApiBase}/download?${Params}`;
|
||||
}
|
||||
|
||||
export async function UploadFiles(RootName, RelativePath, Files) {
|
||||
const Form = new FormData();
|
||||
for (const File of Files) {
|
||||
Form.append("Files", File);
|
||||
}
|
||||
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
||||
const Response = await fetch(`${ApiBase}/upload?${Params}`, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: Form,
|
||||
});
|
||||
return ParseJsonOrThrow(Response);
|
||||
}
|
||||
|
||||
export function SaveFile(RootName, RelativePath, Content) {
|
||||
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
|
||||
return FetchJson(`/file?${Params}`, { method: "PUT", body: Content });
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
CreateEntry,
|
||||
DeleteEntry,
|
||||
FetchLock,
|
||||
FetchTree,
|
||||
GetDownloadUrl,
|
||||
MoveEntry,
|
||||
RenameEntry,
|
||||
SetLock as SetLockApi,
|
||||
UploadFiles,
|
||||
} from "../Api.js";
|
||||
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
|
||||
import AccessModal from "./AccessModal.jsx";
|
||||
@@ -31,6 +33,8 @@ export default function DirectoryPane({
|
||||
const [LoadError, SetLoadError] = useState(null);
|
||||
const [ModalConfig, SetModalConfig] = useState(null);
|
||||
const [ErrorMessage, SetErrorMessage] = useState(null);
|
||||
const [IsDraggingOver, SetIsDraggingOver] = useState(false);
|
||||
const FileInputRef = useRef(null);
|
||||
|
||||
async function Reload() {
|
||||
try {
|
||||
@@ -154,16 +158,71 @@ export default function DirectoryPane({
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleUpload(Files) {
|
||||
if (!Files || Files.length === 0) return;
|
||||
|
||||
try {
|
||||
const Result = await UploadFiles(RootName, DirectoryPath, Files);
|
||||
if (Result.Errors && Result.Errors.length > 0) {
|
||||
SetErrorMessage(Result.Errors.join("; "));
|
||||
}
|
||||
await Reload();
|
||||
OnEntryCreated();
|
||||
} catch (Error) {
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function HandleUploadButtonClick() {
|
||||
FileInputRef.current?.click();
|
||||
}
|
||||
|
||||
function HandleFileInputChange(Event) {
|
||||
HandleUpload(Event.target.files);
|
||||
Event.target.value = "";
|
||||
}
|
||||
|
||||
function HandleDragOver(Event) {
|
||||
if (IsGuest) return;
|
||||
Event.preventDefault();
|
||||
SetIsDraggingOver(true);
|
||||
}
|
||||
|
||||
function HandleDragLeave(Event) {
|
||||
Event.preventDefault();
|
||||
SetIsDraggingOver(false);
|
||||
}
|
||||
|
||||
function HandleDrop(Event) {
|
||||
Event.preventDefault();
|
||||
SetIsDraggingOver(false);
|
||||
if (IsGuest) return;
|
||||
HandleUpload(Event.dataTransfer.files);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="DirectoryPane">
|
||||
{!IsGuest && (
|
||||
<div className="PreviewToolbar">
|
||||
<button onClick={OpenCreateFileModal}>New File</button>
|
||||
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
|
||||
<button onClick={HandleUploadButtonClick}>Upload</button>
|
||||
<input
|
||||
ref={FileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
onChange={HandleFileInputChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{LoadError && <div className="SidebarError">{LoadError}</div>}
|
||||
<div className="DirectoryList">
|
||||
<div
|
||||
className={`DirectoryList${IsDraggingOver ? " DraggingOver" : ""}`}
|
||||
onDragOver={HandleDragOver}
|
||||
onDragLeave={HandleDragLeave}
|
||||
onDrop={HandleDrop}
|
||||
>
|
||||
{Entries.map((Entry) => (
|
||||
<div
|
||||
className="DirectoryEntry"
|
||||
@@ -178,31 +237,41 @@ export default function DirectoryPane({
|
||||
: GetFileIcon(UseHyphenBullets)}
|
||||
</span>
|
||||
<span className="TreeLabel">{Entry.Name}</span>
|
||||
{!IsGuest && (
|
||||
<span className="TreeRowActions">
|
||||
<button
|
||||
className="TreeRowActionButton"
|
||||
onClick={(Event) => HandleRequestLockEdit(Event, Entry)}
|
||||
title="Access control"
|
||||
>
|
||||
🔒
|
||||
</button>
|
||||
<button
|
||||
className="TreeRowActionButton"
|
||||
onClick={(Event) => HandleRenameClick(Event, Entry)}
|
||||
title="Rename / move"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
className="TreeRowActionButton"
|
||||
onClick={(Event) => HandleDeleteClick(Event, Entry)}
|
||||
title="Delete"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
<span className="TreeRowActions">
|
||||
<a
|
||||
className="TreeRowActionButton"
|
||||
href={GetDownloadUrl(RootName, JoinPath(Entry.Name))}
|
||||
onClick={(Event) => Event.stopPropagation()}
|
||||
title={Entry.IsDirectory ? "Download as .zip" : "Download"}
|
||||
>
|
||||
⭳
|
||||
</a>
|
||||
{!IsGuest && (
|
||||
<>
|
||||
<button
|
||||
className="TreeRowActionButton"
|
||||
onClick={(Event) => HandleRequestLockEdit(Event, Entry)}
|
||||
title="Access control"
|
||||
>
|
||||
🔒
|
||||
</button>
|
||||
<button
|
||||
className="TreeRowActionButton"
|
||||
onClick={(Event) => HandleRenameClick(Event, Entry)}
|
||||
title="Rename / move"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
className="TreeRowActionButton"
|
||||
onClick={(Event) => HandleDeleteClick(Event, Entry)}
|
||||
title="Delete"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{Entries.length === 0 && !LoadError && (
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
FetchFile,
|
||||
FetchResolvedFile,
|
||||
GetAvatarUrl,
|
||||
GetDownloadUrl,
|
||||
GetFileUrl,
|
||||
GetRawFileUrl,
|
||||
SaveFile,
|
||||
@@ -243,6 +244,11 @@ export default function PreviewPane({
|
||||
if (FileType === "image") {
|
||||
return (
|
||||
<div className="PreviewMedia">
|
||||
<div className={ToolbarClass}>
|
||||
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<img src={GetFileUrl(RootName, FilePath)} alt={FilePath} />
|
||||
</div>
|
||||
);
|
||||
@@ -251,6 +257,11 @@ export default function PreviewPane({
|
||||
if (FileType === "video") {
|
||||
return (
|
||||
<div className="PreviewMedia">
|
||||
<div className={ToolbarClass}>
|
||||
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<video controls src={GetFileUrl(RootName, FilePath)} />
|
||||
</div>
|
||||
);
|
||||
@@ -261,6 +272,9 @@ export default function PreviewPane({
|
||||
<div className="PreviewFrame">
|
||||
<div className={ToolbarClass}>
|
||||
{!IsGuest && <button onClick={() => SetIsEditing(true)}>Edit</button>}
|
||||
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<iframe title={FilePath} sandbox="" src={GetRawFileUrl(RootName, FilePath)} />
|
||||
</div>
|
||||
@@ -272,6 +286,9 @@ export default function PreviewPane({
|
||||
<div className="PreviewMarkdown">
|
||||
<div className={ToolbarClass}>
|
||||
{!IsGuest && <button onClick={() => SetIsEditing(true)}>Edit</button>}
|
||||
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="MarkdownBody"
|
||||
@@ -294,6 +311,9 @@ export default function PreviewPane({
|
||||
<div className="PreviewText">
|
||||
<div className={ToolbarClass}>
|
||||
{!IsGuest && <button onClick={() => SetIsEditing(true)}>Edit</button>}
|
||||
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<pre className="TextBody">{ResolvedContent}</pre>
|
||||
</div>
|
||||
@@ -312,6 +332,9 @@ export default function PreviewPane({
|
||||
{IsSaving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
)}
|
||||
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
|
||||
Download
|
||||
</a>
|
||||
{SaveError && <span className="SaveErrorText">{SaveError}</span>}
|
||||
</div>
|
||||
<CodeMirror
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
CreateEntry,
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
FetchLock,
|
||||
FetchTree,
|
||||
GetAvatarUrl,
|
||||
GetDownloadUrl,
|
||||
MoveEntry,
|
||||
RenameEntry,
|
||||
SetLock as SetLockApi,
|
||||
UploadFiles,
|
||||
} from "../Api.js";
|
||||
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
|
||||
import AccessModal from "./AccessModal.jsx";
|
||||
@@ -151,19 +153,29 @@ function TreeNode({
|
||||
: GetFileIcon(UseHyphenBullets)}
|
||||
</span>
|
||||
<span className="TreeLabel">{Entry.Name}</span>
|
||||
{!IsGuest && (
|
||||
<span className="TreeRowActions">
|
||||
<button className="TreeRowActionButton" onClick={HandleLockClick} title="Access control">
|
||||
🔒
|
||||
</button>
|
||||
<button className="TreeRowActionButton" onClick={HandleRenameClick} title="Rename / move">
|
||||
✎
|
||||
</button>
|
||||
<button className="TreeRowActionButton" onClick={HandleDeleteClick} title="Delete">
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
<span className="TreeRowActions">
|
||||
<a
|
||||
className="TreeRowActionButton"
|
||||
href={GetDownloadUrl(RootName, FullPath)}
|
||||
onClick={(Event) => Event.stopPropagation()}
|
||||
title={Entry.IsDirectory ? "Download as .zip" : "Download"}
|
||||
>
|
||||
⭳
|
||||
</a>
|
||||
{!IsGuest && (
|
||||
<>
|
||||
<button className="TreeRowActionButton" onClick={HandleLockClick} title="Access control">
|
||||
🔒
|
||||
</button>
|
||||
<button className="TreeRowActionButton" onClick={HandleRenameClick} title="Rename / move">
|
||||
✎
|
||||
</button>
|
||||
<button className="TreeRowActionButton" onClick={HandleDeleteClick} title="Delete">
|
||||
✕
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{IsRenaming && (
|
||||
<PromptModal
|
||||
@@ -284,6 +296,8 @@ export default function Sidebar({
|
||||
const [LoadError, SetLoadError] = useState(null);
|
||||
const [ModalConfig, SetModalConfig] = useState(null);
|
||||
const [ErrorMessage, SetErrorMessage] = useState(null);
|
||||
const [IsDraggingOver, SetIsDraggingOver] = useState(false);
|
||||
const FileInputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (RootName) {
|
||||
@@ -345,6 +359,47 @@ export default function Sidebar({
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleUpload(Files) {
|
||||
if (!Files || Files.length === 0) return;
|
||||
|
||||
try {
|
||||
const Result = await UploadFiles(RootName, CurrentDirectoryPath, Files);
|
||||
if (Result.Errors && Result.Errors.length > 0) {
|
||||
SetErrorMessage(Result.Errors.join("; "));
|
||||
}
|
||||
OnEntryCreated();
|
||||
} catch (Error) {
|
||||
SetErrorMessage(Error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function HandleUploadButtonClick() {
|
||||
FileInputRef.current?.click();
|
||||
}
|
||||
|
||||
function HandleFileInputChange(Event) {
|
||||
HandleUpload(Event.target.files);
|
||||
Event.target.value = "";
|
||||
}
|
||||
|
||||
function HandleDragOver(Event) {
|
||||
if (IsGuest) return;
|
||||
Event.preventDefault();
|
||||
SetIsDraggingOver(true);
|
||||
}
|
||||
|
||||
function HandleDragLeave(Event) {
|
||||
Event.preventDefault();
|
||||
SetIsDraggingOver(false);
|
||||
}
|
||||
|
||||
function HandleDrop(Event) {
|
||||
Event.preventDefault();
|
||||
SetIsDraggingOver(false);
|
||||
if (IsGuest) return;
|
||||
HandleUpload(Event.dataTransfer.files);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Sidebar">
|
||||
<div className="SidebarHeader">
|
||||
@@ -364,10 +419,25 @@ export default function Sidebar({
|
||||
<div className="SidebarNewEntry SidebarNewEntryTop">
|
||||
<button onClick={OpenCreateFileModal}>New File</button>
|
||||
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
|
||||
<button onClick={HandleUploadButtonClick}>Upload</button>
|
||||
</div>
|
||||
)}
|
||||
{!IsGuest && (
|
||||
<input
|
||||
ref={FileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
onChange={HandleFileInputChange}
|
||||
/>
|
||||
)}
|
||||
{LoadError && <div className="SidebarError">{LoadError}</div>}
|
||||
<div className="SidebarTree">
|
||||
<div
|
||||
className={`SidebarTree${IsDraggingOver ? " DraggingOver" : ""}`}
|
||||
onDragOver={HandleDragOver}
|
||||
onDragLeave={HandleDragLeave}
|
||||
onDrop={HandleDrop}
|
||||
>
|
||||
{RootEntries.map((Entry) => (
|
||||
<TreeNode
|
||||
key={Entry.Name}
|
||||
@@ -395,6 +465,7 @@ export default function Sidebar({
|
||||
<div className="SidebarNewEntry">
|
||||
<button onClick={OpenCreateFileModal}>New File</button>
|
||||
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
|
||||
<button onClick={HandleUploadButtonClick}>Upload</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -227,6 +227,10 @@ img,
|
||||
line-height: 1;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.TreeRowActionButton:hover {
|
||||
@@ -309,6 +313,13 @@ img,
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.DirectoryList.DraggingOver,
|
||||
.SidebarTree.DraggingOver {
|
||||
outline: 2px dashed var(--Purple);
|
||||
outline-offset: -2px;
|
||||
background: var(--SurfaceAlt);
|
||||
}
|
||||
|
||||
.DirectoryEntry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -370,6 +381,21 @@ img,
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.DownloadButton {
|
||||
background: var(--SurfaceAlt);
|
||||
color: var(--Text);
|
||||
border: 1px solid var(--Purple);
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.DownloadButton:hover {
|
||||
background: var(--Border);
|
||||
}
|
||||
|
||||
.SaveErrorText {
|
||||
color: var(--Red);
|
||||
align-self: center;
|
||||
|
||||
Reference in new issue
Block a user