2025-12-25 09:14:00 -07:00
|
|
|
import {Button, Header, Icon, Modal, Placeholder, Progress, Segment, Table} from "./Theme";
|
2023-05-29 18:04:03 -06:00
|
|
|
import {
|
|
|
|
|
Checkbox,
|
2024-05-19 16:04:45 -06:00
|
|
|
Form,
|
2025-12-25 09:14:00 -07:00
|
|
|
Grid,
|
2023-05-29 18:04:03 -06:00
|
|
|
IconGroup,
|
|
|
|
|
Input,
|
|
|
|
|
PlaceholderLine,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableHeaderCell,
|
|
|
|
|
TableRow
|
|
|
|
|
} from "semantic-ui-react";
|
2025-12-25 09:14:00 -07:00
|
|
|
import {
|
|
|
|
|
APIButton,
|
|
|
|
|
DirectorySearch,
|
|
|
|
|
ErrorMessage,
|
|
|
|
|
FileIcon,
|
|
|
|
|
humanFileSize,
|
|
|
|
|
mimetypeIconName,
|
|
|
|
|
Toggle,
|
|
|
|
|
useIsIgnoredDirectory
|
|
|
|
|
} from "./Common";
|
2023-05-29 18:04:03 -06:00
|
|
|
import React, {useEffect, useState} from "react";
|
2025-12-25 09:14:00 -07:00
|
|
|
import {useDropzone} from "react-dropzone";
|
2024-02-19 13:45:41 -07:00
|
|
|
import {deleteFile, ignoreDirectory, makeDirectory, movePaths, renamePath, unignoreDirectory} from "../api";
|
2023-01-31 18:08:07 -07:00
|
|
|
import _ from 'lodash';
|
|
|
|
|
import {SortableTable} from "./SortableTable";
|
2025-12-25 09:14:00 -07:00
|
|
|
import {useBrowseFiles, useMediaDirectory, useUploadFile, useWROLMode} from "../hooks/customHooks";
|
2024-03-11 17:47:27 -06:00
|
|
|
import {FileRowTagIcon, FilesRefreshButton} from "./Files";
|
2023-02-15 20:58:07 -07:00
|
|
|
import {FilePreviewContext} from "./FilePreview";
|
2025-12-07 09:17:30 -07:00
|
|
|
import {SettingsContext, ThemeContext} from "../contexts/contexts";
|
2025-12-06 18:07:20 -07:00
|
|
|
import {BulkTagModal} from "./BulkTagModal";
|
2025-12-25 09:14:00 -07:00
|
|
|
import {InlineErrorBoundary} from "./ErrorBoundary";
|
2023-01-31 18:08:07 -07:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 18:04:03 -06:00
|
|
|
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, selectedPaths, onFileClick, onSelect, disabled}) {
|
2023-01-31 18:08:07 -07:00
|
|
|
// Creates a single table row for a folder, or a row for itself and indented rows for its children.
|
2024-02-19 13:45:41 -07:00
|
|
|
let {path, children, is_empty} = folder;
|
|
|
|
|
const ignored = useIsIgnoredDirectory(path);
|
2023-01-31 18:08:07 -07:00
|
|
|
const pathWithNoTrailingSlash = path.substring(0, path.length - 1);
|
|
|
|
|
const name = path.substring(pathWithNoTrailingSlash.lastIndexOf('/') + 1);
|
2023-05-29 18:04:03 -06:00
|
|
|
const f = <TableRow key={path} disabled={disabled}>
|
2023-01-31 18:08:07 -07:00
|
|
|
<TableCell collapsing>
|
2023-05-29 18:04:03 -06:00
|
|
|
<Checkbox checked={selectedPaths.indexOf(folder['path']) >= 0} onChange={() => onSelect(folder['path'])}/>
|
2023-01-31 18:08:07 -07:00
|
|
|
</TableCell>
|
2024-02-13 16:15:23 -07:00
|
|
|
<TableCell onClick={() => onFolderClick(path)} className='file-path' colSpan={2} disabled={is_empty}>
|
2023-01-31 18:08:07 -07:00
|
|
|
{depthIndentation(pathWithNoTrailingSlash)}
|
2023-02-14 08:08:04 -07:00
|
|
|
{is_empty ? <Icon name='folder outline'/> : <Icon name='folder'/>}
|
2024-02-19 13:45:41 -07:00
|
|
|
{ignored && <Icon name='eye slash' color='red'/>}
|
2023-01-31 18:08:07 -07:00
|
|
|
{name}
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>;
|
2023-02-15 17:40:57 -07:00
|
|
|
if (children) {
|
2023-01-31 18:08:07 -07:00
|
|
|
// Folder has children, recursively display them.
|
|
|
|
|
children = sortData(children);
|
|
|
|
|
let childPaths = [];
|
|
|
|
|
_.forEach(children, (p, k) => {
|
|
|
|
|
childPaths = [...childPaths,
|
2025-12-25 09:14:00 -07:00
|
|
|
<InlineErrorBoundary key={k}>
|
|
|
|
|
<Path path={p} onFolderClick={onFolderClick} sortData={sortData} onFileClick={onFileClick}
|
|
|
|
|
selectedPaths={selectedPaths} onSelect={onSelect} disabled={disabled}/>
|
|
|
|
|
</InlineErrorBoundary>];
|
2023-01-31 18:08:07 -07:00
|
|
|
});
|
|
|
|
|
return <React.Fragment>
|
|
|
|
|
{f}
|
|
|
|
|
{childPaths}
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
} else {
|
|
|
|
|
// Folder has no children.
|
|
|
|
|
return f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 18:04:03 -06:00
|
|
|
function File({file, onFileClick, selectedPaths, onSelect, disabled}) {
|
2023-01-31 18:08:07 -07:00
|
|
|
const {path, size} = file;
|
2023-05-29 18:04:03 -06:00
|
|
|
return <TableRow key={path} disabled={disabled}>
|
2023-01-31 18:08:07 -07:00
|
|
|
<TableCell collapsing>
|
2023-05-29 18:04:03 -06:00
|
|
|
<Checkbox checked={selectedPaths.indexOf(path) >= 0} onChange={() => onSelect(path)}/>
|
2023-01-31 18:08:07 -07:00
|
|
|
</TableCell>
|
|
|
|
|
<TableCell onClick={() => onFileClick(file)} className='file-path'>
|
|
|
|
|
{depthIndentation(path)}
|
2023-04-01 11:52:41 -06:00
|
|
|
<FileRowTagIcon file={file}/>
|
2023-01-31 18:08:07 -07:00
|
|
|
{/*null size to make icon the correct size*/}
|
|
|
|
|
<FileIcon file={file} size={null}/>
|
2023-05-29 18:04:03 -06:00
|
|
|
{pathName(path)}
|
2023-01-31 18:08:07 -07:00
|
|
|
</TableCell>
|
|
|
|
|
<TableCell collapsing textAlign='right'>
|
|
|
|
|
{humanFileSize(size)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 18:04:03 -06:00
|
|
|
function Path({path, onFolderClick, onFileClick, sortData, selectedPaths, onSelect, disabled}) {
|
2023-01-31 18:08:07 -07:00
|
|
|
if (path['path'].endsWith('/')) {
|
|
|
|
|
return <Folder
|
|
|
|
|
folder={path}
|
|
|
|
|
onFolderClick={onFolderClick}
|
|
|
|
|
sortData={sortData}
|
2023-05-29 18:04:03 -06:00
|
|
|
selectedPaths={selectedPaths}
|
2023-01-31 18:08:07 -07:00
|
|
|
onSelect={onSelect}
|
|
|
|
|
onFileClick={onFileClick}
|
2023-05-29 18:04:03 -06:00
|
|
|
disabled={disabled}
|
2023-01-31 18:08:07 -07:00
|
|
|
/>
|
|
|
|
|
} else {
|
|
|
|
|
return <File
|
|
|
|
|
file={path}
|
|
|
|
|
onFileClick={onFileClick}
|
2023-05-29 18:04:03 -06:00
|
|
|
selectedPaths={selectedPaths}
|
2023-01-31 18:08:07 -07:00
|
|
|
onSelect={onSelect}
|
2023-05-29 18:04:03 -06:00
|
|
|
disabled={disabled}
|
2023-01-31 18:08:07 -07:00
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function FileBrowser() {
|
2023-05-29 18:04:03 -06:00
|
|
|
const [selectedPaths, setSelectedPaths] = React.useState([]);
|
|
|
|
|
const [renameOpen, setRenameOpen] = React.useState(false);
|
|
|
|
|
const [moveOpen, setMoveOpen] = React.useState(false);
|
|
|
|
|
const [makeDirectoryOpen, setMakeDirectoryOpen] = React.useState(false);
|
2024-02-19 13:45:41 -07:00
|
|
|
const [ignoreDirectoryOpen, setIgnoreDirectoryOpen] = React.useState(false);
|
2025-12-06 18:07:20 -07:00
|
|
|
const [bulkTagOpen, setBulkTagOpen] = React.useState(false);
|
2025-12-25 09:14:00 -07:00
|
|
|
const [uploadOpen, setUploadOpen] = React.useState(false);
|
2023-05-29 18:04:03 -06:00
|
|
|
const [pending, setPending] = React.useState(false);
|
2024-02-19 13:45:41 -07:00
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
const {settings, fetchSettings} = React.useContext(SettingsContext);
|
2025-12-07 09:17:30 -07:00
|
|
|
const {inverted} = React.useContext(ThemeContext);
|
2023-05-29 18:04:03 -06:00
|
|
|
|
|
|
|
|
const selectedPathsCount = selectedPaths ? selectedPaths.length : 0;
|
2023-01-31 18:08:07 -07:00
|
|
|
|
|
|
|
|
const {browseFiles, openFolders, setOpenFolders, fetchFiles} = useBrowseFiles();
|
2023-07-21 15:14:09 -06:00
|
|
|
const wrolModeEnabled = useWROLMode();
|
2023-01-31 18:08:07 -07:00
|
|
|
|
2023-04-01 11:52:41 -06:00
|
|
|
const headers = [
|
|
|
|
|
{key: 'select', text: ''},
|
|
|
|
|
{key: 'path', text: 'Path', sortBy: i => i['path'].toLowerCase()},
|
|
|
|
|
{key: 'size', text: 'Size', sortBy: 'size'},
|
|
|
|
|
];
|
2023-01-31 18:08:07 -07:00
|
|
|
|
2023-04-01 11:52:41 -06:00
|
|
|
const {setPreviewFile, setCallbacks} = React.useContext(FilePreviewContext);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// Fetch files after tagging.
|
2023-04-10 12:27:55 -06:00
|
|
|
setCallbacks([() => fetchFiles(openFolders)]);
|
2023-04-01 11:52:41 -06:00
|
|
|
|
|
|
|
|
return () => setCallbacks(null);
|
|
|
|
|
}, []);
|
2023-01-31 18:08:07 -07:00
|
|
|
|
2024-02-19 13:45:41 -07:00
|
|
|
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]);
|
|
|
|
|
|
|
|
|
|
const onIgnore = async () => {
|
|
|
|
|
setIgnoreDirectoryOpen(false);
|
|
|
|
|
await fetchSettings();
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-31 18:08:07 -07:00
|
|
|
const onSelect = (path) => {
|
2023-05-29 18:04:03 -06:00
|
|
|
let newSelectedPaths;
|
|
|
|
|
// Select or deselect if the paths was previously selected.
|
|
|
|
|
if (selectedPathsCount && selectedPaths.indexOf(path) >= 0) {
|
|
|
|
|
newSelectedPaths = selectedPaths.filter(i => i !== path);
|
2023-01-31 18:08:07 -07:00
|
|
|
} else {
|
2023-05-29 18:04:03 -06:00
|
|
|
newSelectedPaths = [...selectedPaths, path];
|
2023-01-31 18:08:07 -07:00
|
|
|
}
|
2023-05-29 18:04:03 -06:00
|
|
|
setSelectedPaths(newSelectedPaths);
|
|
|
|
|
console.debug('selectedPaths', newSelectedPaths);
|
2023-01-31 18:08:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onDelete = async () => {
|
2023-05-29 18:04:03 -06:00
|
|
|
await deleteFile(selectedPaths);
|
|
|
|
|
await reset();
|
2023-01-31 18:08:07 -07:00
|
|
|
};
|
|
|
|
|
|
2023-05-29 18:04:03 -06:00
|
|
|
const handleMakeDirectory = async () => {
|
|
|
|
|
setMakeDirectoryOpen(false);
|
|
|
|
|
await reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reset = async () => {
|
|
|
|
|
setSelectedPaths([]);
|
|
|
|
|
setPending(true);
|
|
|
|
|
try {
|
|
|
|
|
await fetchFiles();
|
|
|
|
|
} finally {
|
|
|
|
|
setPending(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 09:17:30 -07:00
|
|
|
const footerClassName = `sticky-footer ${inverted}`;
|
|
|
|
|
const footer = <div className={footerClassName}>
|
|
|
|
|
<FilesRefreshButton paths={selectedPaths}/>
|
|
|
|
|
<APIButton
|
|
|
|
|
icon='trash'
|
|
|
|
|
color='red'
|
|
|
|
|
confirmContent='Are you sure you want to delete these files?'
|
|
|
|
|
confirmButton='Delete'
|
|
|
|
|
onClick={onDelete}
|
|
|
|
|
disabled={selectedPathsCount === 0}
|
|
|
|
|
obeyWROLMode={true}
|
|
|
|
|
/>
|
|
|
|
|
<Button icon='text cursor'
|
|
|
|
|
color='yellow'
|
|
|
|
|
onClick={() => setRenameOpen(true)}
|
|
|
|
|
disabled={wrolModeEnabled || selectedPathsCount !== 1}
|
|
|
|
|
/>
|
|
|
|
|
{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)}
|
|
|
|
|
/>
|
|
|
|
|
{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: '0.8em'}}
|
|
|
|
|
>
|
|
|
|
|
<IconGroup>
|
|
|
|
|
<Icon name='folder'/>
|
|
|
|
|
<Icon corner name='add'/>
|
|
|
|
|
</IconGroup>
|
|
|
|
|
</Button>
|
|
|
|
|
<MakeDirectoryModal
|
|
|
|
|
open={makeDirectoryOpen}
|
|
|
|
|
onClose={() => setMakeDirectoryOpen(false)}
|
|
|
|
|
parent={selectedPaths.length ? selectedPaths[0] : null}
|
|
|
|
|
onSubmit={handleMakeDirectory}
|
|
|
|
|
/>
|
2025-12-25 09:14:00 -07:00
|
|
|
<Button
|
|
|
|
|
icon='upload'
|
|
|
|
|
color='green'
|
|
|
|
|
disabled={wrolModeEnabled || (selectedPathsCount > 0 && !singleDirectorySelected)}
|
|
|
|
|
onClick={() => setUploadOpen(true)}
|
|
|
|
|
/>
|
|
|
|
|
<FileBrowserUploadModal
|
|
|
|
|
open={uploadOpen}
|
|
|
|
|
onClose={() => setUploadOpen(false)}
|
|
|
|
|
destination={singleDirectorySelected && selectedPaths[0] ? selectedPaths[0].slice(0, -1) : ''}
|
|
|
|
|
onComplete={reset}
|
|
|
|
|
/>
|
2025-12-07 09:17:30 -07:00
|
|
|
<Button
|
|
|
|
|
color='grey'
|
|
|
|
|
icon={isDirectoryIgnored ? 'eye' : 'eye slash'}
|
|
|
|
|
disabled={!singleDirectorySelected || wrolModeEnabled}
|
|
|
|
|
onClick={() => setIgnoreDirectoryOpen(true)}
|
|
|
|
|
/>
|
|
|
|
|
<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)}
|
|
|
|
|
/>
|
|
|
|
|
<BulkTagModal
|
|
|
|
|
open={bulkTagOpen}
|
|
|
|
|
onClose={() => setBulkTagOpen(false)}
|
|
|
|
|
paths={selectedPaths}
|
|
|
|
|
onComplete={reset}
|
|
|
|
|
/>
|
|
|
|
|
</div>;
|
2023-01-31 18:08:07 -07:00
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
} else {
|
|
|
|
|
// Add the new folder to the opened folders.
|
|
|
|
|
newFolders = [...openFolders, folder];
|
|
|
|
|
}
|
|
|
|
|
console.debug(`newFolders=${newFolders}`);
|
|
|
|
|
setOpenFolders(newFolders);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (browseFiles === null) {
|
|
|
|
|
return <Placeholder>
|
|
|
|
|
<PlaceholderLine/>
|
|
|
|
|
<PlaceholderLine/>
|
|
|
|
|
</Placeholder>;
|
2023-09-07 20:13:36 -06:00
|
|
|
} else if (browseFiles === undefined) {
|
|
|
|
|
return <ErrorMessage>Could not fetch files</ErrorMessage>
|
2023-01-31 18:08:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <>
|
2025-12-07 09:17:30 -07:00
|
|
|
<div style={{marginBottom: '4em'}}>
|
|
|
|
|
<SortableTable
|
|
|
|
|
tableProps={{unstackable: true}}
|
|
|
|
|
data={browseFiles}
|
|
|
|
|
tableHeaders={headers}
|
|
|
|
|
defaultSortColumn='path'
|
|
|
|
|
rowKey='path'
|
|
|
|
|
rowFunc={(i, sortData) => <Path
|
|
|
|
|
key={i['key']}
|
|
|
|
|
path={i}
|
|
|
|
|
onFolderClick={onFolderClick}
|
|
|
|
|
onFileClick={(i) => setPreviewFile(i)}
|
|
|
|
|
sortData={sortData}
|
|
|
|
|
selectedPaths={selectedPaths}
|
|
|
|
|
onSelect={onSelect}
|
|
|
|
|
disabled={pending}
|
|
|
|
|
/>}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{footer}
|
2023-01-31 18:08:07 -07:00
|
|
|
</>
|
|
|
|
|
}
|
2023-05-29 18:04:03 -06:00
|
|
|
|
|
|
|
|
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}
|
|
|
|
|
>
|
2025-12-07 11:32:07 -07:00
|
|
|
<Modal.Header>Rename: {name}</Modal.Header>
|
|
|
|
|
<Modal.Content>
|
2024-05-19 16:04:45 -06:00
|
|
|
<Form onSubmit={handleSubmit}>
|
|
|
|
|
<Header as='h4'>New Name</Header>
|
|
|
|
|
<pre>{parent ? parent + '/' : null}{value}</pre>
|
|
|
|
|
|
|
|
|
|
<Input fluid
|
|
|
|
|
label='Name'
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
/>
|
|
|
|
|
</Form>
|
2025-12-07 11:32:07 -07:00
|
|
|
</Modal.Content>
|
|
|
|
|
<Modal.Actions>
|
2023-05-29 18:04:03 -06:00
|
|
|
<Button
|
|
|
|
|
color='violet'
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onClick={handleSubmit}
|
|
|
|
|
>
|
|
|
|
|
Rename
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => setValue(name)}
|
|
|
|
|
floated='left'
|
|
|
|
|
>
|
|
|
|
|
Reset
|
|
|
|
|
</Button>
|
2025-12-07 11:32:07 -07:00
|
|
|
</Modal.Actions>
|
2023-05-29 18:04:03 -06:00
|
|
|
</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}
|
|
|
|
|
>
|
2025-12-07 11:32:07 -07:00
|
|
|
<Modal.Header>Move Files/Directories</Modal.Header>
|
|
|
|
|
<Modal.Content>
|
2023-05-29 18:04:03 -06:00
|
|
|
<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>
|
2025-12-07 11:32:07 -07:00
|
|
|
</Modal.Content>
|
|
|
|
|
<Modal.Actions>
|
2023-05-29 18:04:03 -06:00
|
|
|
<Button
|
|
|
|
|
color='violet'
|
|
|
|
|
onClick={handleMove}
|
|
|
|
|
disabled={pending}
|
|
|
|
|
>Move</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => setDestination(null)}
|
|
|
|
|
floated='left'
|
|
|
|
|
disabled={pending}
|
|
|
|
|
>Reset</Button>
|
2025-12-07 11:32:07 -07:00
|
|
|
</Modal.Actions>
|
2023-05-29 18:04:03 -06:00
|
|
|
</Modal>
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 13:45:41 -07:00
|
|
|
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}>
|
2025-12-07 11:32:07 -07:00
|
|
|
<Modal.Header>{header}</Modal.Header>
|
|
|
|
|
<Modal.Content>
|
2024-02-19 13:45:41 -07:00
|
|
|
{content}
|
|
|
|
|
<pre>{mediaDirectory}/{directory}</pre>
|
2025-12-07 11:32:07 -07:00
|
|
|
</Modal.Content>
|
|
|
|
|
<Modal.Actions>
|
2024-02-19 13:45:41 -07:00
|
|
|
{submitButton}
|
|
|
|
|
<Button secondary floated='left' onClick={onClose}>Close</Button>
|
2025-12-07 11:32:07 -07:00
|
|
|
</Modal.Actions>
|
2024-02-19 13:45:41 -07:00
|
|
|
</Modal>
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 18:04:03 -06:00
|
|
|
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}>
|
2025-12-07 11:32:07 -07:00
|
|
|
<Modal.Header>Make Directory</Modal.Header>
|
|
|
|
|
<Modal.Content>
|
2024-05-19 16:04:45 -06:00
|
|
|
<Form onSubmit={handleSubmit}>
|
|
|
|
|
<pre>{mediaDirectory}/{parent}{value}</pre>
|
|
|
|
|
<Input fluid
|
|
|
|
|
label='Name'
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
/>
|
|
|
|
|
</Form>
|
2025-12-07 11:32:07 -07:00
|
|
|
</Modal.Content>
|
|
|
|
|
<Modal.Actions>
|
2023-05-29 18:04:03 -06:00
|
|
|
<Button color='violet' onClick={handleSubmit}>Create</Button>
|
2025-12-07 11:32:07 -07:00
|
|
|
</Modal.Actions>
|
2023-05-29 18:04:03 -06:00
|
|
|
</Modal>
|
2025-12-07 09:17:30 -07:00
|
|
|
}
|
2025-12-25 09:14:00 -07:00
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
}
|