mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
907 lines
33 KiB
JavaScript
907 lines
33 KiB
JavaScript
import {Button, Header, Icon, Modal, Placeholder, Progress, Segment, Table} from "./Theme";
|
|
import {
|
|
Checkbox,
|
|
Form,
|
|
Grid,
|
|
IconGroup,
|
|
Input,
|
|
PlaceholderLine,
|
|
TableBody,
|
|
TableCell,
|
|
TableHeader,
|
|
TableHeaderCell,
|
|
TableRow
|
|
} from "semantic-ui-react";
|
|
import {
|
|
APIButton,
|
|
DirectorySearch,
|
|
ErrorMessage,
|
|
FileIcon,
|
|
humanFileSize,
|
|
mimetypeIconName,
|
|
Toggle,
|
|
useIsIgnoredDirectory
|
|
} from "./Common";
|
|
import React, {useContext, useEffect, useState} from "react";
|
|
import {useDropzone} from "react-dropzone";
|
|
import {deleteFile, ignoreDirectory, makeDirectory, movePaths, renamePath, unignoreDirectory} from "../api";
|
|
import _ from 'lodash';
|
|
import {SortableTable} from "./SortableTable";
|
|
import {useBrowseFiles, useElementWidth, useMediaDirectory, useUploadFile, useWROLMode} from "../hooks/customHooks";
|
|
import {FileRowTagIcon, FilesRefreshButton} from "./Files";
|
|
import {FilePreviewContext} from "./FilePreview";
|
|
import {DragSelectionContext, DragSelectionProvider, SettingsContext, ThemeContext} from "../contexts/contexts";
|
|
import {BulkTagModal} from "./BulkTagModal";
|
|
import {InlineErrorBoundary} from "./ErrorBoundary";
|
|
import {TaggedDeleteConfirmModal} from "./TaggedDeleteConfirmModal";
|
|
|
|
function depthIndentation(path) {
|
|
// Repeated spaces for every folder a path is in.
|
|
const depth = (path.match(/\//g) || []).length;
|
|
return '\xa0\xa0\xa0\xa0'.repeat(depth);
|
|
}
|
|
|
|
export function pathDirectory(path) {
|
|
// Return the full path of the path's directory.
|
|
// pathDirectory('/some/directory/file.txt') -> '/some/directory'
|
|
return path.substring(0, path.lastIndexOf('/'));
|
|
}
|
|
|
|
export function pathName(path) {
|
|
// Return the file's name.
|
|
// pathName('/some/directory/file.txt') -> 'file.txt'
|
|
// pathName('/some/directory') -> 'directory'
|
|
if (path.endsWith('/')) {
|
|
// Path is a directory, return its name.
|
|
path = path.substring(0, path.length - 1);
|
|
}
|
|
return path.substring(path.lastIndexOf('/') + 1);
|
|
}
|
|
|
|
export function splitPathParentAndName(path) {
|
|
if (path.endsWith('/')) {
|
|
// Path is a directory, return its name.
|
|
path = path.substring(0, path.length - 1);
|
|
}
|
|
const slashIndex = path.lastIndexOf('/');
|
|
return [path.substring(0, slashIndex), path.substring(slashIndex + 1, path.length)];
|
|
}
|
|
|
|
function Folder({folder, onFolderClick, sortData, onFileClick, disabled, loadingFolders}) {
|
|
const {handleDragStart, handleDragMove, getDragState, shouldAllowClick, selectedPaths, onSelect} = useContext(DragSelectionContext);
|
|
// Creates a single table row for a folder, or a row for itself and indented rows for its children.
|
|
let {path, children, is_empty} = folder;
|
|
const ignored = useIsIgnoredDirectory(path);
|
|
const pathWithNoTrailingSlash = path.substring(0, path.length - 1);
|
|
const name = path.substring(pathWithNoTrailingSlash.lastIndexOf('/') + 1);
|
|
const isInSelectedPaths = selectedPaths.indexOf(folder['path']) >= 0;
|
|
const {isDragSelecting, isDragDeselecting} = getDragState(path, isInSelectedPaths);
|
|
// Preview selection: show checked if selecting, unchecked if deselecting
|
|
const showAsSelected = (isInSelectedPaths && !isDragDeselecting) || isDragSelecting;
|
|
// Visual highlight class
|
|
const dragClass = isDragSelecting ? 'drag-selecting' : (isDragDeselecting ? 'drag-deselecting' : '');
|
|
|
|
const isLoading = loadingFolders && loadingFolders.has(path);
|
|
const depth = (path.match(/\//g) || []).length;
|
|
|
|
const f = <TableRow
|
|
key={path}
|
|
disabled={isLoading || disabled}
|
|
className={dragClass}
|
|
onMouseDown={(e) => handleDragStart(path, e)}
|
|
onMouseEnter={() => handleDragMove(path)}
|
|
>
|
|
<TableCell collapsing>
|
|
<Checkbox checked={showAsSelected} onChange={() => onSelect(folder['path'])}/>
|
|
</TableCell>
|
|
<TableCell onClick={() => shouldAllowClick() && onFolderClick(path)} className='file-path' colSpan={2} disabled={is_empty}>
|
|
{depthIndentation(pathWithNoTrailingSlash)}
|
|
{is_empty ? <Icon name='folder outline'/> : <Icon name='folder'/>}
|
|
{ignored && <Icon name='eye slash' color='red'/>}
|
|
{name}
|
|
</TableCell>
|
|
</TableRow>;
|
|
if (children) {
|
|
// Folder has children, recursively display them.
|
|
children = sortData(children);
|
|
let childPaths = [];
|
|
_.forEach(children, (p, k) => {
|
|
childPaths = [...childPaths,
|
|
<InlineErrorBoundary key={k}>
|
|
<Path path={p} onFolderClick={onFolderClick} sortData={sortData} onFileClick={onFileClick}
|
|
disabled={disabled} loadingFolders={loadingFolders}/>
|
|
</InlineErrorBoundary>];
|
|
});
|
|
return <React.Fragment>
|
|
{f}
|
|
{childPaths}
|
|
</React.Fragment>
|
|
} else if (isLoading) {
|
|
// Folder is loading - show skeleton rows
|
|
return <React.Fragment>
|
|
{f}
|
|
<SkeletonFileRow depth={depth}/>
|
|
</React.Fragment>;
|
|
} else {
|
|
// Folder has no children.
|
|
return f;
|
|
}
|
|
}
|
|
|
|
function File({file, onFileClick, disabled}) {
|
|
const {handleDragStart, handleDragMove, getDragState, shouldAllowClick, selectedPaths, onSelect} = useContext(DragSelectionContext);
|
|
const {path, size} = file;
|
|
const isInSelectedPaths = selectedPaths.indexOf(path) >= 0;
|
|
const {isDragSelecting, isDragDeselecting} = getDragState(path, isInSelectedPaths);
|
|
// Preview selection: show checked if selecting, unchecked if deselecting
|
|
const showAsSelected = (isInSelectedPaths && !isDragDeselecting) || isDragSelecting;
|
|
// Visual highlight class
|
|
const dragClass = isDragSelecting ? 'drag-selecting' : (isDragDeselecting ? 'drag-deselecting' : '');
|
|
return <TableRow
|
|
key={path}
|
|
disabled={disabled}
|
|
className={dragClass}
|
|
onMouseDown={(e) => handleDragStart(path, e)}
|
|
onMouseEnter={() => handleDragMove(path)}
|
|
>
|
|
<TableCell collapsing>
|
|
<Checkbox checked={showAsSelected} onChange={() => onSelect(path)}/>
|
|
</TableCell>
|
|
<TableCell onClick={() => shouldAllowClick() && onFileClick(file)} className='file-path'>
|
|
{depthIndentation(path)}
|
|
<FileRowTagIcon file={file}/>
|
|
{/*null size to make icon the correct size*/}
|
|
<FileIcon file={file} size={null}/>
|
|
{pathName(path)}
|
|
</TableCell>
|
|
<TableCell collapsing textAlign='right'>
|
|
{humanFileSize(size)}
|
|
</TableCell>
|
|
</TableRow>
|
|
}
|
|
|
|
function Path({path, onFolderClick, onFileClick, sortData, disabled, loadingFolders}) {
|
|
if (path['path'].endsWith('/')) {
|
|
return <Folder
|
|
folder={path}
|
|
onFolderClick={onFolderClick}
|
|
sortData={sortData}
|
|
onFileClick={onFileClick}
|
|
disabled={disabled}
|
|
loadingFolders={loadingFolders}
|
|
/>
|
|
} else {
|
|
return <File
|
|
file={path}
|
|
onFileClick={onFileClick}
|
|
disabled={disabled}
|
|
/>
|
|
}
|
|
}
|
|
|
|
function SkeletonFileRow({depth}) {
|
|
const indentation = '\xa0\xa0\xa0\xa0'.repeat(depth);
|
|
return (
|
|
<TableRow>
|
|
<TableCell collapsing>
|
|
<Checkbox disabled/>
|
|
</TableCell>
|
|
<TableCell className='file-path' colSpan={2}>
|
|
{indentation}
|
|
<Placeholder style={{display: 'inline-block', width: '200px', marginLeft: '0.5em'}}>
|
|
<PlaceholderLine/>
|
|
</Placeholder>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
}
|
|
|
|
// Below this footer width (px) the action buttons are icon-only; at or above it they expand to show labels.
|
|
// The eight labeled buttons need ~1020px to fit on a single row; this leaves a small buffer.
|
|
const FOOTER_LABELS_MIN_WIDTH = 1050;
|
|
|
|
export function FileBrowser() {
|
|
const [selectedPaths, setSelectedPaths] = React.useState([]);
|
|
const [renameOpen, setRenameOpen] = React.useState(false);
|
|
const [moveOpen, setMoveOpen] = React.useState(false);
|
|
const [makeDirectoryOpen, setMakeDirectoryOpen] = React.useState(false);
|
|
const [ignoreDirectoryOpen, setIgnoreDirectoryOpen] = React.useState(false);
|
|
const [bulkTagOpen, setBulkTagOpen] = React.useState(false);
|
|
const [uploadOpen, setUploadOpen] = React.useState(false);
|
|
const [pending, setPending] = React.useState(false);
|
|
// Tracks which folders are currently loading their children
|
|
const [loadingFolders, setLoadingFolders] = React.useState(new Set());
|
|
// Only true if only one path is selected, and it is a directory.
|
|
const [singleDirectorySelected, setSingleDirectorySelected] = React.useState(false);
|
|
// Only true if single directory is selected, and it is in settings['ignored_directories']
|
|
const [isDirectoryIgnored, setIsDirectoryIgnored] = React.useState(false);
|
|
// Tracks folder that was opened while selected - its children will be auto-selected when they load
|
|
const pendingAutoSelectRef = React.useRef(null);
|
|
// Tagged file groups that would be deleted - shown in confirmation modal
|
|
const [taggedFileGroups, setTaggedFileGroups] = React.useState(null);
|
|
|
|
const {settings, fetchSettings} = React.useContext(SettingsContext);
|
|
const {inverted} = React.useContext(ThemeContext);
|
|
|
|
const selectedPathsCount = selectedPaths ? selectedPaths.length : 0;
|
|
|
|
const {browseFiles, openFolders, setOpenFolders, fetchFiles} = useBrowseFiles();
|
|
const wrolModeEnabled = useWROLMode();
|
|
|
|
const headers = [
|
|
{key: 'select', text: ''},
|
|
{key: 'path', text: 'Path', sortBy: i => i['path'].toLowerCase()},
|
|
{key: 'size', text: 'Size', sortBy: 'size'},
|
|
];
|
|
|
|
const {setPreviewFile, setCallbacks} = React.useContext(FilePreviewContext);
|
|
|
|
useEffect(() => {
|
|
// Fetch files after tagging.
|
|
setCallbacks([() => fetchFiles(openFolders)]);
|
|
|
|
return () => setCallbacks(null);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (selectedPaths.length === 1 && selectedPaths[0].endsWith('/')) {
|
|
setSingleDirectorySelected(true);
|
|
if (!_.isEmpty(settings)) {
|
|
// Used to toggle between ignore/un-ignore (eye/eye lash) icon.
|
|
const directory = selectedPaths[0].slice(0, -1);
|
|
setIsDirectoryIgnored(settings['ignored_directories'].indexOf(directory) >= 0);
|
|
}
|
|
} else {
|
|
setSingleDirectorySelected(false);
|
|
}
|
|
}, [selectedPaths, settings]);
|
|
|
|
// Auto-select immediate children when a selected folder is opened
|
|
useEffect(() => {
|
|
const folderToAutoSelect = pendingAutoSelectRef.current;
|
|
if (folderToAutoSelect && browseFiles && typeof browseFiles === 'object') {
|
|
// browseFiles is a dict: {'map/': {path: 'map/', children: {...}}, ...}
|
|
// children is also a dict: {'file.txt': {path: 'map/file.txt', ...}, ...}
|
|
const findFolderChildren = (files, targetFolder) => {
|
|
if (!files || typeof files !== 'object') return null;
|
|
// Convert dict to array of values for iteration
|
|
const fileList = Array.isArray(files) ? files : Object.values(files);
|
|
for (const file of fileList) {
|
|
if (file.path === targetFolder && file.children) {
|
|
// Return children as array of values
|
|
return Array.isArray(file.children) ? file.children : Object.values(file.children);
|
|
}
|
|
if (file.children) {
|
|
const found = findFolderChildren(file.children, targetFolder);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const children = findFolderChildren(browseFiles, folderToAutoSelect);
|
|
if (children && children.length > 0) {
|
|
const childPaths = children.map(child => child.path);
|
|
const newSelectedPaths = [...new Set([...selectedPaths, ...childPaths])];
|
|
setSelectedPaths(newSelectedPaths);
|
|
}
|
|
pendingAutoSelectRef.current = null;
|
|
}
|
|
}, [browseFiles]);
|
|
|
|
// Clear loading states when file data updates
|
|
useEffect(() => {
|
|
if (loadingFolders.size > 0) {
|
|
setLoadingFolders(new Set());
|
|
}
|
|
}, [browseFiles]);
|
|
|
|
const onIgnore = async () => {
|
|
setIgnoreDirectoryOpen(false);
|
|
await fetchSettings();
|
|
}
|
|
|
|
const handleDeleteComplete = async () => {
|
|
// Remove deleted directories from openFolders to prevent fetching non-existent directories
|
|
const deletedDirs = selectedPaths.filter(p => p.endsWith('/'));
|
|
if (deletedDirs.length > 0 && openFolders && openFolders.length > 0) {
|
|
const newOpenFolders = openFolders.filter(folder =>
|
|
!deletedDirs.some(deleted => folder === deleted || folder.startsWith(deleted))
|
|
);
|
|
setOpenFolders(newOpenFolders.length > 0 ? newOpenFolders : null);
|
|
}
|
|
await reset();
|
|
};
|
|
|
|
const onDelete = async () => {
|
|
const result = await deleteFile(selectedPaths);
|
|
if (result && result.tagged) {
|
|
// Show modal with tagged file groups for user to confirm.
|
|
setTaggedFileGroups(result.file_groups);
|
|
return;
|
|
}
|
|
await handleDeleteComplete();
|
|
};
|
|
|
|
const onForceDelete = async () => {
|
|
setTaggedFileGroups(null);
|
|
const result = await deleteFile(selectedPaths, true);
|
|
if (result && result.tagged) return; // unexpected tagged response
|
|
if (result && !result.ok) return; // error already toasted
|
|
await handleDeleteComplete();
|
|
};
|
|
|
|
const handleMakeDirectory = async () => {
|
|
setMakeDirectoryOpen(false);
|
|
await reset();
|
|
}
|
|
|
|
const reset = async () => {
|
|
setSelectedPaths([]);
|
|
setPending(true);
|
|
try {
|
|
await fetchFiles();
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
const [footerRef, footerWidth] = useElementWidth();
|
|
// Expand icon-only buttons to include labels once the footer is wide enough to fit them.
|
|
const showLabels = footerWidth >= FOOTER_LABELS_MIN_WIDTH;
|
|
// Semantic UI renders a labeled icon button when `content` and `labelPosition` are set, else icon-only.
|
|
const labelProps = (content) => showLabels ? {content, labelPosition: 'left'} : {};
|
|
|
|
const footerClassName = `sticky-footer ${inverted}`;
|
|
const footer = <div className={footerClassName} ref={footerRef}>
|
|
<FilesRefreshButton paths={selectedPaths} showLabel={showLabels}/>
|
|
<APIButton
|
|
icon='trash'
|
|
color='red'
|
|
confirmContent='Are you sure you want to delete these files?'
|
|
confirmButton='Delete'
|
|
onClick={onDelete}
|
|
disabled={selectedPathsCount === 0}
|
|
obeyWROLMode={true}
|
|
{...labelProps('Delete')}
|
|
/>
|
|
<Button icon='text cursor'
|
|
color='yellow'
|
|
onClick={() => setRenameOpen(true)}
|
|
disabled={wrolModeEnabled || selectedPathsCount !== 1}
|
|
{...labelProps('Rename')}
|
|
/>
|
|
{selectedPathsCount === 1 ?
|
|
<RenameModal
|
|
open={renameOpen}
|
|
onClose={() => setRenameOpen(false)}
|
|
onPending={setPending}
|
|
path={selectedPaths[0]}
|
|
onSubmit={reset}
|
|
/> : null}
|
|
<Button icon='move'
|
|
color='teal'
|
|
disabled={wrolModeEnabled || selectedPathsCount === 0}
|
|
onClick={() => setMoveOpen(true)}
|
|
{...labelProps('Move')}
|
|
/>
|
|
{selectedPathsCount > 0 ?
|
|
<MoveModal open={moveOpen}
|
|
onClose={() => setMoveOpen(false)}
|
|
paths={selectedPaths}
|
|
onSubmit={reset}
|
|
/> : null}
|
|
<Button
|
|
color='blue'
|
|
onClick={() => setMakeDirectoryOpen(true)}
|
|
disabled={wrolModeEnabled}
|
|
style={{paddingLeft: '1em', paddingRight: showLabels ? '1em' : '0.8em'}}
|
|
>
|
|
<IconGroup>
|
|
<Icon name='folder'/>
|
|
<Icon corner name='add'/>
|
|
</IconGroup>
|
|
{showLabels ? <> New Folder</> : null}
|
|
</Button>
|
|
<MakeDirectoryModal
|
|
open={makeDirectoryOpen}
|
|
onClose={() => setMakeDirectoryOpen(false)}
|
|
parent={selectedPaths.length ? selectedPaths[0] : null}
|
|
onSubmit={handleMakeDirectory}
|
|
/>
|
|
<Button
|
|
icon='upload'
|
|
color='green'
|
|
disabled={wrolModeEnabled || (selectedPathsCount > 0 && !singleDirectorySelected)}
|
|
onClick={() => setUploadOpen(true)}
|
|
{...labelProps('Upload')}
|
|
/>
|
|
<FileBrowserUploadModal
|
|
open={uploadOpen}
|
|
onClose={() => setUploadOpen(false)}
|
|
destination={singleDirectorySelected && selectedPaths[0] ? selectedPaths[0].slice(0, -1) : ''}
|
|
onComplete={reset}
|
|
/>
|
|
<Button
|
|
color='grey'
|
|
icon={isDirectoryIgnored ? 'eye' : 'eye slash'}
|
|
disabled={!singleDirectorySelected || wrolModeEnabled}
|
|
onClick={() => setIgnoreDirectoryOpen(true)}
|
|
{...labelProps(isDirectoryIgnored ? 'Unignore' : 'Ignore')}
|
|
/>
|
|
<IgnoreDirectoryModal
|
|
open={ignoreDirectoryOpen}
|
|
onClose={onIgnore}
|
|
onSubmit={onIgnore}
|
|
directory={selectedPaths.length === 1 ? selectedPaths[0] : null}
|
|
ignored={isDirectoryIgnored}
|
|
/>
|
|
<Button
|
|
color='violet'
|
|
icon='tags'
|
|
disabled={selectedPathsCount === 0}
|
|
onClick={() => setBulkTagOpen(true)}
|
|
{...labelProps('Tag')}
|
|
/>
|
|
<BulkTagModal
|
|
open={bulkTagOpen}
|
|
onClose={() => setBulkTagOpen(false)}
|
|
paths={selectedPaths}
|
|
onComplete={reset}
|
|
/>
|
|
<TaggedDeleteConfirmModal
|
|
open={taggedFileGroups !== null}
|
|
taggedFileGroups={taggedFileGroups}
|
|
onCancel={() => setTaggedFileGroups(null)}
|
|
onConfirm={onForceDelete}
|
|
/>
|
|
</div>;
|
|
|
|
const onFolderClick = async (folder) => {
|
|
let newFolders;
|
|
if (openFolders.indexOf(folder) >= 0) {
|
|
// Remove the folder that was clicked on, as well as its sub-folders.
|
|
newFolders = openFolders.filter(i => !i.startsWith(folder));
|
|
// Unselect any children of the closed folder (they're no longer visible)
|
|
const newSelectedPaths = selectedPaths.filter(p => !p.startsWith(folder) || p === folder);
|
|
if (newSelectedPaths.length !== selectedPaths.length) {
|
|
setSelectedPaths(newSelectedPaths);
|
|
}
|
|
} else {
|
|
// Add the new folder to the opened folders.
|
|
newFolders = [...openFolders, folder];
|
|
// Mark folder as loading so skeleton rows show
|
|
setLoadingFolders(prev => new Set([...prev, folder]));
|
|
// If folder is selected, mark it for auto-selection of its children
|
|
if (selectedPaths.includes(folder)) {
|
|
pendingAutoSelectRef.current = folder;
|
|
}
|
|
}
|
|
console.debug(`newFolders=${newFolders}`);
|
|
setOpenFolders(newFolders);
|
|
}
|
|
|
|
if (browseFiles === null) {
|
|
return <Placeholder>
|
|
<PlaceholderLine/>
|
|
<PlaceholderLine/>
|
|
</Placeholder>;
|
|
} else if (browseFiles === undefined) {
|
|
return <ErrorMessage>Could not fetch files</ErrorMessage>
|
|
}
|
|
|
|
return <DragSelectionProvider
|
|
selectedPaths={selectedPaths}
|
|
setSelectedPaths={setSelectedPaths}
|
|
browseFiles={browseFiles}
|
|
openFolders={openFolders}
|
|
>
|
|
<FileBrowserContent
|
|
browseFiles={browseFiles}
|
|
headers={headers}
|
|
onFolderClick={onFolderClick}
|
|
setPreviewFile={setPreviewFile}
|
|
pending={pending}
|
|
loadingFolders={loadingFolders}
|
|
/>
|
|
{footer}
|
|
</DragSelectionProvider>
|
|
}
|
|
|
|
function FileBrowserContent({browseFiles, headers, onFolderClick, setPreviewFile, pending, loadingFolders}) {
|
|
const {isDragging} = useContext(DragSelectionContext);
|
|
const containerClassName = isDragging ? 'file-browser-dragging' : '';
|
|
|
|
return (
|
|
<div style={{marginBottom: '4em'}} className={containerClassName}>
|
|
<SortableTable
|
|
tableProps={{unstackable: true, selectable: true, striped: true}}
|
|
data={browseFiles}
|
|
tableHeaders={headers}
|
|
defaultSortColumn='path'
|
|
rowKey='path'
|
|
rowFunc={(i, sortData) => (
|
|
<Path
|
|
key={i['key']}
|
|
path={i}
|
|
onFolderClick={onFolderClick}
|
|
onFileClick={(file) => setPreviewFile(file)}
|
|
sortData={sortData}
|
|
disabled={pending}
|
|
loadingFolders={loadingFolders}
|
|
/>
|
|
)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function RenameModal({open, onClose, path, onSubmit, onPending}) {
|
|
const [parent, name] = splitPathParentAndName(path);
|
|
const [value, setValue] = React.useState(name);
|
|
const disabled = value === name;
|
|
|
|
const handleInputChange = (e, props) => {
|
|
if (e) {
|
|
e.preventDefault();
|
|
}
|
|
setValue(props.value);
|
|
}
|
|
|
|
const handleSubmit = async (e) => {
|
|
if (e) {
|
|
e.preventDefault();
|
|
}
|
|
try {
|
|
console.log(`Renaming: ${path} to ${value}`);
|
|
onPending(true);
|
|
await renamePath(path, value);
|
|
} finally {
|
|
onClose();
|
|
onPending(false);
|
|
if (onSubmit) {
|
|
await onSubmit();
|
|
}
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
setValue(pathName(path));
|
|
}, [path]);
|
|
|
|
return <Modal closeIcon
|
|
open={open}
|
|
onClose={onClose}
|
|
>
|
|
<Modal.Header>Rename: {name}</Modal.Header>
|
|
<Modal.Content>
|
|
<Form onSubmit={handleSubmit}>
|
|
<Header as='h4'>New Name</Header>
|
|
<pre>{parent ? parent + '/' : null}{value}</pre>
|
|
|
|
<Input fluid
|
|
label='Name'
|
|
value={value}
|
|
onChange={handleInputChange}
|
|
/>
|
|
</Form>
|
|
</Modal.Content>
|
|
<Modal.Actions>
|
|
<Button
|
|
color='violet'
|
|
disabled={disabled}
|
|
onClick={handleSubmit}
|
|
>
|
|
Rename
|
|
</Button>
|
|
<Button
|
|
onClick={() => setValue(name)}
|
|
floated='left'
|
|
>
|
|
Reset
|
|
</Button>
|
|
</Modal.Actions>
|
|
</Modal>
|
|
}
|
|
|
|
export function MoveModal({open, paths, onClose, onSubmit}) {
|
|
const [destination, setDestination] = React.useState('');
|
|
const [pending, setPending] = React.useState(false);
|
|
|
|
const handleDirectory = (value) => {
|
|
setDestination(value);
|
|
}
|
|
|
|
const handleMove = async () => {
|
|
try {
|
|
setPending(true);
|
|
await movePaths(destination, paths);
|
|
onClose();
|
|
} catch (e) {
|
|
console.error(e);
|
|
setPending(false);
|
|
}
|
|
if (onSubmit) {
|
|
await onSubmit();
|
|
}
|
|
}
|
|
|
|
const fileCell = i => <TableCell>
|
|
<pre style={{marginTop: 0, marginBottom: 0}}>{i}</pre>
|
|
</TableCell>;
|
|
|
|
const destinationRows = paths.map(i => <TableRow key={i}>
|
|
{fileCell(i)}
|
|
{destination ? fileCell(destination + '/' + pathName(i)) : fileCell(pathName(i))}
|
|
</TableRow>);
|
|
|
|
return <Modal closeIcon
|
|
onClose={onClose}
|
|
open={open}
|
|
>
|
|
<Modal.Header>Move Files/Directories</Modal.Header>
|
|
<Modal.Content>
|
|
<p>Search for a directory to move your files into:</p>
|
|
<DirectorySearch onSelect={handleDirectory} disabled={pending}/>
|
|
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHeaderCell>Source</TableHeaderCell>
|
|
<TableHeaderCell>Destination</TableHeaderCell>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{destinationRows}
|
|
</TableBody>
|
|
</Table>
|
|
</Modal.Content>
|
|
<Modal.Actions>
|
|
<Button
|
|
color='violet'
|
|
onClick={handleMove}
|
|
disabled={pending}
|
|
>Move</Button>
|
|
<Button
|
|
onClick={() => setDestination(null)}
|
|
floated='left'
|
|
disabled={pending}
|
|
>Reset</Button>
|
|
</Modal.Actions>
|
|
</Modal>
|
|
}
|
|
|
|
export function IgnoreDirectoryModal({open, onClose, onSubmit, directory, ignored}) {
|
|
const mediaDirectory = useMediaDirectory();
|
|
const [pending, setPending] = React.useState(false);
|
|
|
|
const handleIgnore = async () => {
|
|
try {
|
|
setPending(true);
|
|
if (ignored) {
|
|
await unignoreDirectory(`${mediaDirectory}/${directory}`);
|
|
} else {
|
|
await ignoreDirectory(`${mediaDirectory}/${directory}`);
|
|
}
|
|
if (onSubmit) {
|
|
onSubmit();
|
|
}
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
const header = ignored ? 'Remove Ignore Directory' : 'Ignore Directory';
|
|
const content = ignored ?
|
|
'Remove ignore of the following directory? You must refresh your files after this.'
|
|
: 'Ignore the following directory? The files within will not be indexed and will not show in search results.';
|
|
let submitButton = <Button primary
|
|
disabled={pending}
|
|
onClick={handleIgnore}
|
|
>{ignored ? 'Un-ignore' : 'Ignore'}</Button>;
|
|
|
|
return <Modal closeIcon open={open} onClose={onClose}>
|
|
<Modal.Header>{header}</Modal.Header>
|
|
<Modal.Content>
|
|
{content}
|
|
<pre>{mediaDirectory}/{directory}</pre>
|
|
</Modal.Content>
|
|
<Modal.Actions>
|
|
{submitButton}
|
|
<Button secondary floated='left' onClick={onClose}>Close</Button>
|
|
</Modal.Actions>
|
|
</Modal>
|
|
}
|
|
|
|
export function MakeDirectoryModal({open, onClose, parent, onSubmit}) {
|
|
const [value, setValue] = useState('');
|
|
const mediaDirectory = useMediaDirectory();
|
|
|
|
const handleInputChange = (e, props) => {
|
|
if (e) {
|
|
e.preventDefault();
|
|
}
|
|
setValue(props.value);
|
|
}
|
|
|
|
const handleSubmit = async (e) => {
|
|
if (e) {
|
|
e.preventDefault();
|
|
}
|
|
try {
|
|
const path = parent ? `${parent}/${value}` : value;
|
|
await makeDirectory(path);
|
|
setValue('');
|
|
if (onSubmit) {
|
|
await onSubmit();
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
return <Modal closeIcon open={open} onClose={onClose}>
|
|
<Modal.Header>Make Directory</Modal.Header>
|
|
<Modal.Content>
|
|
<Form onSubmit={handleSubmit}>
|
|
<pre>{mediaDirectory}/{parent}{value}</pre>
|
|
<Input fluid
|
|
label='Name'
|
|
value={value}
|
|
onChange={handleInputChange}
|
|
/>
|
|
</Form>
|
|
</Modal.Content>
|
|
<Modal.Actions>
|
|
<Button color='violet' onClick={handleSubmit}>Create</Button>
|
|
</Modal.Actions>
|
|
</Modal>
|
|
}
|
|
|
|
export function FileBrowserUploadModal({open, onClose, destination, onComplete}) {
|
|
const mediaDirectory = useMediaDirectory();
|
|
const {t} = React.useContext(ThemeContext);
|
|
|
|
const {
|
|
setFiles,
|
|
progresses,
|
|
setDestination,
|
|
doClear,
|
|
tagsSelector,
|
|
overwrite,
|
|
setOverwrite,
|
|
overallProgress,
|
|
inProgress,
|
|
} = useUploadFile();
|
|
|
|
const onDrop = React.useCallback(async (acceptedFiles) => {
|
|
if (acceptedFiles && acceptedFiles.length > 0) {
|
|
setFiles(acceptedFiles);
|
|
}
|
|
}, [setFiles]);
|
|
|
|
const {getRootProps, getInputProps} = useDropzone({onDrop, disabled: inProgress});
|
|
|
|
// Set destination when modal opens or destination changes
|
|
useEffect(() => {
|
|
if (open) {
|
|
setDestination(destination);
|
|
}
|
|
}, [open, destination, setDestination]);
|
|
|
|
// Clear upload state when destination is cleared
|
|
useEffect(() => {
|
|
if (!destination && open) {
|
|
doClear();
|
|
}
|
|
}, [destination, open, doClear]);
|
|
|
|
const handleClose = () => {
|
|
if (!inProgress) {
|
|
doClear();
|
|
onClose();
|
|
if (onComplete) {
|
|
onComplete();
|
|
}
|
|
}
|
|
};
|
|
|
|
let progressBars;
|
|
if (!_.isEmpty(progresses)) {
|
|
progressBars = Object.entries(progresses).map(([name, value]) => {
|
|
const {percent, status, type} = value;
|
|
let color = 'grey';
|
|
let statusString;
|
|
if (status === 'complete') {
|
|
color = 'green';
|
|
statusString = 'Complete:';
|
|
} else if (status === 'pending') {
|
|
statusString = 'Pending:';
|
|
} else if (status === 'failed') {
|
|
color = 'red';
|
|
statusString = 'Failed:';
|
|
} else if (status === 'conflicting') {
|
|
color = 'orange';
|
|
statusString = 'Already Exists:';
|
|
}
|
|
let indicating = status === 'pending';
|
|
|
|
return <Grid.Row key={name}>
|
|
<Grid.Column mobile={3} tablet={2} computer={1}>
|
|
<Icon name={mimetypeIconName(type, name.toLowerCase())} size='big'/>
|
|
</Grid.Column>
|
|
<Grid.Column mobile={13} tablet={14} computer={15}>
|
|
<Progress progress indicating={indicating} percent={percent} color={color}>
|
|
<p {...t}>{statusString} {name}</p>
|
|
</Progress>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
})
|
|
}
|
|
|
|
const displayPath = destination ? `${mediaDirectory}/${destination}` : mediaDirectory;
|
|
|
|
return <Modal closeIcon open={open} onClose={handleClose} size='large'>
|
|
<Modal.Header>Upload to: {displayPath}</Modal.Header>
|
|
<Modal.Content>
|
|
<Form>
|
|
{tagsSelector}
|
|
<div style={{marginTop: '1em'}}>
|
|
<Toggle
|
|
checked={overwrite}
|
|
label='Overwrite existing files'
|
|
onChange={() => setOverwrite(!overwrite)}
|
|
disabled={inProgress}
|
|
/>
|
|
</div>
|
|
</Form>
|
|
|
|
<Segment>
|
|
<Grid columns={1}>
|
|
<Grid.Row>
|
|
<Grid.Column style={{padding: '1em'}}>
|
|
<Form>
|
|
<div {...getRootProps()}>
|
|
<input {...getInputProps()}/>
|
|
<Grid textAlign='center'>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<Header icon>
|
|
<Icon name='file text'/>
|
|
Click here, or drop files here to upload
|
|
</Header>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
</div>
|
|
</Form>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
</Segment>
|
|
|
|
<Grid>
|
|
<Grid.Row columns={1}>
|
|
<Grid.Column>
|
|
{progresses && Object.keys(progresses).length > 1 && <Progress progress
|
|
percent={overallProgress}
|
|
color='blue'
|
|
label='Overall Progress'
|
|
/>}
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
<Grid columns={2}>
|
|
{progressBars}
|
|
</Grid>
|
|
</Modal.Content>
|
|
<Modal.Actions>
|
|
<Button
|
|
onClick={handleClose}
|
|
disabled={inProgress}
|
|
>
|
|
Close
|
|
</Button>
|
|
</Modal.Actions>
|
|
</Modal>
|
|
}
|