File dialog multiselect

This commit is contained in:
Robin E. R. Davies
2025-06-21 10:40:22 -04:00
parent 7474c2531d
commit ca6bf365fb
4 changed files with 471 additions and 167 deletions
+5 -5
View File
@@ -122,11 +122,11 @@ void json_writer::write(string_view v,bool enforceValidUtf8Encoding)
} }
} }
} }
if ((uc >= UTF16_SURROGATE_1_BASE && uc <= UTF16_SURROGATE_1_BASE + UTF16_SURROGATE_MASK) || (uc >= UTF16_SURROGATE_2_BASE && uc <= UTF16_SURROGATE_2_BASE + UTF16_SURROGATE_MASK)) // if ((uc >= UTF16_SURROGATE_1_BASE && uc <= UTF16_SURROGATE_1_BASE + UTF16_SURROGATE_MASK) || (uc >= UTF16_SURROGATE_2_BASE && uc <= UTF16_SURROGATE_2_BASE + UTF16_SURROGATE_MASK))
{ // {
// MUST not encode UTF16 surrogates in UTF8. // // MUST not encode UTF16 surrogates in UTF8.
throw_encoding_error(); // throw_encoding_error();
} // }
if (uc == '"' || uc == '\\') if (uc == '"' || uc == '\\')
{ {
+36 -4
View File
@@ -26,7 +26,7 @@ import ButtonBase, { ButtonBaseProps } from "@mui/material/ButtonBase";
export interface DraggableButtonBaseProps extends ButtonBaseProps { export interface DraggableButtonBaseProps extends ButtonBaseProps {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement>) => void; onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement>) => boolean;
onLongPressMove?: (e: React.PointerEvent<HTMLButtonElement>) => void; onLongPressMove?: (e: React.PointerEvent<HTMLButtonElement>) => void;
onLongPressEnd?: (e: React.PointerEvent<HTMLButtonElement>) => void; onLongPressEnd?: (e: React.PointerEvent<HTMLButtonElement>) => void;
longPressDelay? : number; longPressDelay? : number;
@@ -69,9 +69,34 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
let [hTimeout, setHTimeout] = React.useState<number | null>(null); let [hTimeout, setHTimeout] = React.useState<number | null>(null);
let [pointerId, setPointerId] = React.useState<number | null>(null); let [pointerId, setPointerId] = React.useState<number | null>(null);
let [longPressed, setLongPressed] = React.useState<boolean>(false); let [longPressed, setLongPressed] = React.useState<boolean>(false);
let [suppressClick, setSuppressClick] = React.useState<boolean>(false); let [clickSuppressed, setClickSuppressed] = React.useState<boolean>(false);
let [pointerDownPoint, setPointerDownPoint] = React.useState<Point >({x: 0, y: 0}); let [pointerDownPoint, setPointerDownPoint] = React.useState<Point >({x: 0, y: 0});
function handleSuppressClick(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
setSuppressClick(false);
}
function setSuppressClick(value: boolean) {
if (value !== clickSuppressed) {
setClickSuppressed(value);
if (value) {
window.addEventListener(
"click",
handleSuppressClick,
{
once: true,
capture: true
});
} else {
window.removeEventListener("click", handleSuppressClick);
}
}
}
function cancelLongPress() { function cancelLongPress() {
if (hTimeout !== null) { if (hTimeout !== null) {
window.clearTimeout(hTimeout); window.clearTimeout(hTimeout);
@@ -82,8 +107,10 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
setSuppressClick(false); setSuppressClick(false);
} }
useEffect(() => { useEffect(() => {
return () => { return () => {
setSuppressClick(false);
cancelLongPress(); cancelLongPress();
}; };
}, []); }, []);
@@ -106,10 +133,15 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
if (longPressDelay === undefined || longPressDelay >= 0) if (longPressDelay === undefined || longPressDelay >= 0)
{ {
setHTimeout(window.setTimeout(() => { setHTimeout(window.setTimeout(() => {
setHTimeout(null);
if (props.onLongPressStart) { if (props.onLongPressStart) {
props.onLongPressStart(currentTarget,e); if (!props.onLongPressStart(currentTarget,e))
{
return;
}
} }
setLongPressed(true); setLongPressed(true);
setSuppressClick(true);
}, longPressDelay??1250)); }, longPressDelay??1250));
} }
}} }}
@@ -147,7 +179,7 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
} }
}} }}
onClick={(e) => { onClick={(e) => {
if (suppressClick) { if (clickSuppressed) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
setSuppressClick(false); setSuppressClick(false);
+428 -156
View File
@@ -62,7 +62,7 @@ import UploadFileDialog from './UploadFileDialog';
import OkCancelDialog from './OkCancelDialog'; import OkCancelDialog from './OkCancelDialog';
import HomeIcon from '@mui/icons-material/Home'; import HomeIcon from '@mui/icons-material/Home';
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog'; import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata'; import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
interface Point { interface Point {
x: number; x: number;
@@ -156,11 +156,15 @@ export interface FilePropertyDialogState {
newFilePath: string; newFilePath: string;
} | null; } | null;
menuAnchorEl: null | HTMLElement; menuAnchorEl: null | HTMLElement;
menuMultiselectAnchorEl: null | HTMLElement;
newFolderDialogOpen: boolean; newFolderDialogOpen: boolean;
renameDialogOpen: boolean; renameDialogOpen: boolean;
moveDialogOpen: boolean; moveDialogOpen: boolean;
copyDialogOpen: boolean; copyDialogOpen: boolean;
initialSelection: string; initialSelection: string;
multiSelect: boolean,
selectedFiles: string[],
}; };
class DragState { class DragState {
@@ -220,11 +224,14 @@ export default withStyles(
openConfirmDeleteDialog: false, openConfirmDeleteDialog: false,
confirmCopyDialogState: null, confirmCopyDialogState: null,
menuAnchorEl: null, menuAnchorEl: null,
menuMultiselectAnchorEl: null,
newFolderDialogOpen: false, newFolderDialogOpen: false,
renameDialogOpen: false, renameDialogOpen: false,
moveDialogOpen: false, moveDialogOpen: false,
copyDialogOpen: false, copyDialogOpen: false,
initialSelection: this.props.selectedFile initialSelection: this.props.selectedFile,
multiSelect: false,
selectedFiles: []
}; };
this.requestScroll = true; this.requestScroll = true;
} }
@@ -325,58 +332,86 @@ export default withStyles(
dragToPosition: number = -1; dragToPosition: number = -1;
maxPosition: number = 0; maxPosition: number = 0;
handleLongPressStart(currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement>) { handleMultisSelectClose() {
if (!this.isTracksDirectory()) { this.setState({
return; multiSelect: false, selectedFiles: [], menuMultiselectAnchorEl: null
} });
this.dragFromPosition = parseInt(currentTarget.getAttribute("data-position") as string, 10); }
this.dragToPosition = this.dragToPosition;
let element = currentTarget;
element.style.position = "relative"; handleLongPressStart(
element.style.zIndex = "1000"; currentTarget: HTMLButtonElement,
element.style.top = "5px"; e: React.PointerEvent<HTMLButtonElement>,
element.style.left = "5px"; fileEntry: FileEntry): boolean {
element.style.background = isDarkMode() ? "#555" : "#EEF" // xxx: dark mode. if (this.state.reordering) {
if (this.lastDivRef) {
this.longPressStartPoint = screenToClient(this.lastDivRef, { x: e.screenX, y: e.screenY }); if (!this.isTracksDirectory()) {
return false;
}
this.dragFromPosition = parseInt(currentTarget.getAttribute("data-position") as string, 10);
this.dragToPosition = this.dragToPosition;
let element = currentTarget;
element.style.position = "relative";
element.style.zIndex = "1000";
element.style.top = "5px";
element.style.left = "5px";
element.style.background = isDarkMode() ? "#555" : "#EEF" // xxx: dark mode.
if (this.lastDivRef) {
this.longPressStartPoint = screenToClient(this.lastDivRef, { x: e.screenX, y: e.screenY });
}
return true;
} else if (!this.state.multiSelect) {
if (fileEntry.isProtected) {
return false;
}
this.setState(
{
multiSelect: true,
selectedFiles: [fileEntry.pathname],
}
);
return true;
} }
return false;
} }
handleLongPressMove(e: React.PointerEvent<HTMLButtonElement>) { handleLongPressMove(e: React.PointerEvent<HTMLButtonElement>) {
if (!this.isTracksDirectory()) { if (this.state.reordering) {
return; if (!this.isTracksDirectory()) {
} return;
if (this.lastDivRef) { }
let element = e.target as HTMLButtonElement; if (this.lastDivRef) {
let point = screenToClient(this.lastDivRef, { x: e.screenX, y: e.screenY }); let element = e.target as HTMLButtonElement;
if (this.longPressStartPoint) { let point = screenToClient(this.lastDivRef, { x: e.screenX, y: e.screenY });
let dy = point.y - this.longPressStartPoint.y; if (this.longPressStartPoint) {
element.style.left = (5) + "px"; let dy = point.y - this.longPressStartPoint.y;
element.style.top = (5 + dy) + "px"; element.style.left = (5) + "px";
let height = element.clientHeight; element.style.top = (5 + dy) + "px";
let positionChange_: number = 0; let height = element.clientHeight;
if (dy > 0) { let positionChange_: number = 0;
positionChange_ = Math.floor((dy + height / 2) / height); if (dy > 0) {
} else { positionChange_ = Math.floor((dy + height / 2) / height);
positionChange_ = Math.ceil((dy - height / 2) / height) } else {
} positionChange_ = Math.ceil((dy - height / 2) / height)
let toPosition = this.dragFromPosition + positionChange_; }
if (toPosition < 0) { let toPosition = this.dragFromPosition + positionChange_;
toPosition = 0; if (toPosition < 0) {
} toPosition = 0;
if (toPosition > this.maxPosition) { }
toPosition = this.maxPosition; if (toPosition > this.maxPosition) {
} toPosition = this.maxPosition;
if (toPosition !== this.dragToPosition) { }
this.dragToPosition = toPosition; if (toPosition !== this.dragToPosition) {
this.setState({ this.dragToPosition = toPosition;
dragState: { this.setState({
activeItem: element.getAttribute("data-pathname") || "", dragState: {
height: height, activeItem: element.getAttribute("data-pathname") || "",
from: this.dragFromPosition, height: height,
to: toPosition from: this.dragFromPosition,
} to: toPosition
}); }
});
}
} }
} }
} }
@@ -427,27 +462,40 @@ export default withStyles(
this.model.moveAudioFile( this.model.moveAudioFile(
this.state.currentDirectory, from, to); this.state.currentDirectory, from, to);
} }
handleLongPressEnd(e: React.PointerEvent<HTMLButtonElement>) { handleLongPressEnd(e: React.PointerEvent<HTMLButtonElement>, fileEntry: FileEntry) {
if (!this.isTracksDirectory()) { if (this.state.reordering) {
return; if (!this.isTracksDirectory()) {
return;
}
let dragState = this.state.dragState;
if (dragState && dragState.to != dragState.from) {
this.handleReorderFiles(dragState.from, dragState.to);
}
// prevent onClick from firing.
e.preventDefault();
e.stopPropagation();
let element = e.currentTarget as HTMLButtonElement;
element.style.position = "";
element.style.zIndex = "";
element.style.top = "";
element.style.left = "";
element.style.background = ""; // xxx: dark mode.
this.setState({ dragState: null });
} else if (!this.state.multiSelect) {
let selectedFile = fileEntry.pathname;
if (selectedFile === "") {
return;
}
this.setState({
multiSelect: true,
selectedFiles: [selectedFile],
});
e.stopPropagation();
e.preventDefault();
} }
let dragState = this.state.dragState;
if (dragState && dragState.to != dragState.from) {
this.handleReorderFiles(dragState.from, dragState.to);
}
// prevent onClick from firing.
e.preventDefault();
e.stopPropagation();
let element = e.currentTarget as HTMLButtonElement;
element.style.position = "";
element.style.zIndex = "";
element.style.top = "";
element.style.left = "";
element.style.background = ""; // xxx: dark mode.
this.setState({ dragState: null });
} }
onMeasureRef(div: HTMLDivElement | null) { onMeasureRef(div: HTMLDivElement | null) {
this.lastDivRef = div; this.lastDivRef = div;
@@ -544,10 +592,57 @@ export default withStyles(
return undefined; return undefined;
} }
onSelectValue(fileEntry: FileEntry) {
handleFileClick(event: React.MouseEvent<HTMLElement>, fileEntry: FileEntry) {
if (this.state.reordering) { if (this.state.reordering) {
return; return;
} }
if (event.ctrlKey || this.state.multiSelect) {
if (fileEntry.isProtected) {
// <none> cannot be multiselected.
return;
}
if (this.state.multiSelect) {
let selectedFiles = this.state.selectedFiles.slice();
let ix = selectedFiles.indexOf(fileEntry.pathname);
if (ix === -1) {
selectedFiles.push(fileEntry.pathname);
} else {
selectedFiles.splice(ix, 1);
}
if (selectedFiles.length === 0) {
this.setState({
multiSelect: false,
selectedFiles: []
});
} else {
this.setState({
multiSelect: true,
selectedFiles: selectedFiles
});
}
} else {
let selectedFiles = [fileEntry.pathname];
if (this.state.hasSelection) {
if (this.state.selectedFile !== "") {
selectedFiles.push(this.state.selectedFile);
}
}
this.setState({
multiSelect: true,
selectedFiles: selectedFiles
});
}
event.stopPropagation();
event.preventDefault();
return;
}
if (event.shiftKey) {
event.stopPropagation();
event.preventDefault();
return;
}
this.requestScroll = true; this.requestScroll = true;
if (!fileEntry.isDirectory) { if (!fileEntry.isDirectory) {
if (!this.isFolderArtwork(fileEntry.pathname)) { if (!this.isFolderArtwork(fileEntry.pathname)) {
@@ -576,8 +671,18 @@ export default withStyles(
handleMenuClose() { handleMenuClose() {
this.setState({ menuAnchorEl: null }); this.setState({ menuAnchorEl: null });
} }
handleMenuMultiSelectOpen(event: React.MouseEvent<HTMLElement>) {
this.setState({ menuMultiselectAnchorEl: event.currentTarget });
}
handleMenuMultiSelectClose() {
this.setState({ menuMultiselectAnchorEl: null });
}
canDeleteEntry(fileEntry: FileEntry): boolean {
return fileEntry.isProtected;
}
handleDelete() { handleDelete() {
if (this.state.selectedFileProtected) { if (this.state.selectedFileProtected && !this.state.multiSelect) {
return; return;
} }
this.setState({ openConfirmDeleteDialog: true }); this.setState({ openConfirmDeleteDialog: true });
@@ -589,66 +694,93 @@ export default withStyles(
let file = this.state.selectedFile; let file = this.state.selectedFile;
this.model.downloadAudioFile(file); this.model.downloadAudioFile(file);
} }
handleConfirmDelete() { async handleConfirmDelete() {
if (this.state.selectedFileProtected) { this.setState({ openConfirmDeleteDialog: false });
let files: string[] = [];
if (this.state.multiSelect) {
files = this.state.selectedFiles;
} else {
if (this.state.selectedFileProtected) {
return;
}
files = [this.state.selectedFile];
}
let selectedFile = (!this.state.multiSelect && this.state.hasSelection) ? this.state.selectedFile : undefined;
let newSelectedFiles = this.state.selectedFiles.slice();
let multiSelect = this.state.multiSelect;
let resultFiles = this.state.fileResult.files;
try {
for (let file of files) {
if (!multiSelect) {
if (selectedFile) {
let position = -1;
for (let i = 0; i < resultFiles.length; ++i) {
let file = resultFiles[i];
if (file.pathname === selectedFile) {
position = i;
break;
}
}
if (position >= 0 && position < resultFiles.length - 1) {
selectedFile = this.state.fileResult.files[position + 1].pathname;
} else if (position === this.state.fileResult.files.length - 1) {
if (position !== 0) {
selectedFile = this.state.fileResult.files[position - 1].pathname;
} else {
selectedFile = undefined;
}
} else {
selectedFile = undefined;
}
}
} else {
let ix = newSelectedFiles.indexOf(file);
if (ix >= 0) {
newSelectedFiles.splice(ix, 1);
}
}
await this.model.deleteUserFile(file);
}
} catch (e: any) {
this.model.showAlert(e);
}
// update state for what we've done.
if (!this.mounted) {
return; return;
} }
this.setState({ openConfirmDeleteDialog: false }); if (multiSelect) {
if (this.state.hasSelection) { if (newSelectedFiles.length === 0) {
let selectedFile = this.state.selectedFile; this.setState({
let position = -1; multiSelect: false,
for (let i = 0; i < this.state.fileResult.files.length; ++i) { selectedFiles: [],
let file = this.state.fileResult.files[i]; });
if (file.pathname === selectedFile) { } else {
position = i; this.setState({
} multiSelect: true,
selectedFiles: newSelectedFiles,
});
} }
let newSelection: FileEntry | undefined = undefined; } else {
if (position >= 0 && position < this.state.fileResult.files.length - 1) { if (selectedFile) {
newSelection = this.state.fileResult.files[position + 1]; this.setState({
} else if (position === this.state.fileResult.files.length - 1) { selectedFile: selectedFile
if (position !== 0) { });
newSelection = this.state.fileResult.files[position - 1];
}
} }
this.model.deleteUserFile(this.state.selectedFile)
.then(
() => {
if (newSelection) {
this.setState({
selectedFile: newSelection.pathname,
selectedFileIsDirectory: newSelection.isDirectory,
selectedFileProtected: newSelection.isProtected,
hasSelection: true,
hasFileSelection: !newSelection.isDirectory && !newSelection.isProtected
});
} else {
this.setState({
selectedFile: "",
selectedFileIsDirectory: false,
selectedFileProtected: true,
hasSelection: false,
hasFileSelection: false
});
}
this.requestFiles(this.state.navDirectory);
}
).catch(
(e: any) => {
this.model.showAlert(e.toString());
}
);
} }
this.requestFiles(this.state.navDirectory);
} }
navigate(relativeDirectory: string) { navigate(relativeDirectory: string) {
this.requestFiles(relativeDirectory); this.requestFiles(relativeDirectory);
this.setState({ navDirectory: relativeDirectory }); this.setState({ navDirectory: relativeDirectory });
} }
handleBreadcrumbNavigate(relativeDirectory: string) {
if (!this.state.reordering && !this.state.multiSelect) {
this.navigate(relativeDirectory);
}
}
getCompactTrackTitle(fileEntry: FileEntry): string { getCompactTrackTitle(fileEntry: FileEntry): string {
let metadata = fileEntry.metadata; let metadata = fileEntry.metadata;
let title = getTrackTitle(fileEntry.pathname, metadata); let title = getTrackTitle(fileEntry.pathname, metadata);
@@ -679,7 +811,7 @@ export default withStyles(
let breadcrumbs: React.ReactElement[] = [( let breadcrumbs: React.ReactElement[] = [(
<Button variant="text" <Button variant="text"
color="inherit" color="inherit"
key="h" onClick={() => { this.navigate(""); }} key="h" onClick={() => { this.handleBreadcrumbNavigate(""); }}
style={{ textTransform: "none", flexShrink: 0 }} style={{ textTransform: "none", flexShrink: 0 }}
startIcon={(<HomeIcon sx={{ mr: 0.6 }} fontSize="inherit" />)} startIcon={(<HomeIcon sx={{ mr: 0.6 }} fontSize="inherit" />)}
> >
@@ -700,7 +832,7 @@ export default withStyles(
<Button variant="text" <Button variant="text"
key={"bc" + i} key={"bc" + i}
color="inherit" color="inherit"
onClick={() => { this.navigate(breadcrumb.pathname) }} onClick={() => { this.handleBreadcrumbNavigate(breadcrumb.pathname) }}
style={{ minWidth: 0, textTransform: "none", flexShrink: 0 }} style={{ minWidth: 0, textTransform: "none", flexShrink: 0 }}
> >
<Typography variant="body2" style={{}} noWrap>{breadcrumb.displayName}</Typography> <Typography variant="body2" style={{}} noWrap>{breadcrumb.displayName}</Typography>
@@ -835,6 +967,8 @@ export default withStyles(
onEnterKey={() => { onEnterKey={() => {
if (this.state.reordering) { if (this.state.reordering) {
this.setState({ reordering: false, dragState: null }); this.setState({ reordering: false, dragState: null });
} else if (this.state.multiSelect) {
this.setState({ multiSelect: false, selectedFiles: [] });
} else { } else {
this.openSelectedFile(); this.openSelectedFile();
} }
@@ -854,12 +988,12 @@ export default withStyles(
> >
<DialogTitle style={{ <DialogTitle style={{
paddingBottom: 0, paddingBottom: 0,
background: this.state.reordering ? background: this.state.reordering || this.state.multiSelect ?
(isDarkMode() ? "#523" : "#EDF") (isDarkMode() ? "#523" : "#EDF")
: undefined, : undefined,
marginBottom: 16, paddingTop: 0 marginBottom: 16, paddingTop: 0
}} > }} >
{this.state.reordering ? ( {this.state.reordering && (
<Toolbar style={{ padding: 0 }}> <Toolbar style={{ padding: 0 }}>
<IconButton <IconButton
edge="start" edge="start"
@@ -876,7 +1010,65 @@ export default withStyles(
</Typography> </Typography>
</Toolbar> </Toolbar>
) : ( )}
{this.state.multiSelect && (
<Toolbar style={{ padding: 0 }}>
<IconButton
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => {
this.handleMultisSelectClose();
}}
>
<CloseIcon style={{ width: 24, height: 24 }} />
</IconButton>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
{this.state.selectedFiles.length.toString() + " files selected."}
</Typography>
<Button variant="dialogSecondary"
startIcon={<OldDeleteIcon />}
onClick={() => {
this.handleDelete();
}}
>
Delete
</Button>
<IconButtonEx
tooltip="More..."
aria-label="display more actions"
edge="end"
color="inherit"
style={{ opacity: 0.6 }}
onClick={(ev) => { this.handleMenuMultiSelectOpen(ev); }}
disabled={protectedDirectory}
>
<MoreIcon />
</IconButtonEx>
<Menu
id="menu-multi-select"
anchorEl={this.state.menuMultiselectAnchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={this.state.menuMultiselectAnchorEl !== null}
onClose={() => { this.handleMenuMultiSelectClose(); }}
>
{canMove && (<MenuItem onClick={() => { this.handleMenuMultiSelectClose(); this.handleMove(); }}>Move</MenuItem>)}
{canMove && (<MenuItem onClick={() => { this.handleMenuMultiSelectClose(); this.handleCopy(); }}>Copy</MenuItem>)}
</Menu>
</Toolbar>
)}
{!(this.state.reordering || this.state.multiSelect) && (
<> <>
<Toolbar style={{ padding: 0 }}> <Toolbar style={{ padding: 0 }}>
<IconButton <IconButton
@@ -934,8 +1126,8 @@ export default withStyles(
> >
<MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem> <MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem>
{(needsDivider) && (<Divider />)} {(needsDivider) && (<Divider />)}
{canMove && (<MenuItem onClick={() => { this.handleMenuClose(); this.onMove(); }}>Move</MenuItem>)} {canMove && (<MenuItem onClick={() => { this.handleMenuClose(); this.handleMove(); }}>Move</MenuItem>)}
{canMove && (<MenuItem onClick={() => { this.handleMenuClose(); this.onCopy(); }}>Copy</MenuItem>)} {canMove && (<MenuItem onClick={() => { this.handleMenuClose(); this.handleCopy(); }}>Copy</MenuItem>)}
{canRename && this.hasSelectedFileOrFolder() && !protectedItem && (<MenuItem onClick={() => { {canRename && this.hasSelectedFileOrFolder() && !protectedItem && (<MenuItem onClick={() => {
this.handleMenuClose(); this.onRename(); this.handleMenuClose(); this.onRename();
}}>Rename</MenuItem>)} }}>Rename</MenuItem>)}
@@ -943,12 +1135,12 @@ export default withStyles(
<MenuItem onClick={() => { this.handleMenuClose(); this.onReorder(); }}>Reorder</MenuItem>)} <MenuItem onClick={() => { this.handleMenuClose(); this.onReorder(); }}>Reorder</MenuItem>)}
</Menu> </Menu>
</Toolbar> </Toolbar>
<div style={{ flex: "0 0 auto", marginLeft: 0, marginRight: 0, overflowX: "clip" }}>
{this.renderBreadcrumbs()}
</div>
</> </>
)} )}
<div style={{ flex: "0 0 auto", marginLeft: 0, marginRight: 0, overflowX: "clip" }}>
{this.renderBreadcrumbs()}
</div>
</DialogTitle> </DialogTitle>
<DialogContent style={{ <DialogContent style={{
@@ -1001,7 +1193,14 @@ export default withStyles(
++trackPosition; ++trackPosition;
this.maxPosition = trackPosition; this.maxPosition = trackPosition;
} }
let selected = value.pathname === this.state.selectedFile && !this.state.reordering; let selected = false;
if (this.state.reordering) {
selected = false;
} else if (this.state.multiSelect) {
selected = this.state.selectedFiles.indexOf(value.pathname) !== -1;
} else {
selected = value.pathname === this.state.selectedFile;
}
let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)"; let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
if (isDarkMode()) { if (isDarkMode()) {
selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)"; selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)";
@@ -1023,7 +1222,7 @@ export default withStyles(
} }
return ( return (
<DraggableButtonBase key={value.pathname} <DraggableButtonBase key={value.pathname}
longPressDelay={this.state.reordering ? 0 : -1} longPressDelay={this.state.reordering ? 0 : undefined}
data-position={dataPosition} data-position={dataPosition}
data-pathname={ data-pathname={
value.metadata ? value.metadata.fileName : null value.metadata ? value.metadata.fileName : null
@@ -1034,15 +1233,15 @@ export default withStyles(
position: "relative", position: "relative",
top: dragOffset + "px", top: dragOffset + "px",
}} }}
onClick={() => this.onSelectValue(value)} onClick={(e) => this.handleFileClick(e, value)}
onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }} onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }}
onLongPressEnd={(e) => { onLongPressEnd={(e) => {
this.handleLongPressEnd(e); this.handleLongPressEnd(e, value);
} }
} }
onLongPressStart={(currentTarget, e) => { onLongPressStart={(currentTarget, e) => {
this.handleLongPressStart(currentTarget, e); return this.handleLongPressStart(currentTarget, e, value);
} }
} }
onLongPressMove={(e) => { onLongPressMove={(e) => {
@@ -1059,20 +1258,20 @@ export default withStyles(
display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis", display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis",
justifyContent: "start", alignItems: "center", width: "100%", height: "100%" justifyContent: "start", alignItems: "center", width: "100%", height: "100%"
}}> }}>
{ {
this.isFolderArtwork(value.pathname) ? ( this.isFolderArtwork(value.pathname) ? (
<img <img
onDragStart={(e) => { e.preventDefault(); }} onDragStart={(e) => { e.preventDefault(); }}
src={this.getTrackThumbnail(value)} src={this.getTrackThumbnail(value)}
style={{ width: 24, height: 24, margin: 20, borderRadius: 4 }} /> style={{ width: 24, height: 24, margin: 20, borderRadius: 4 }} />
):( ) : (
<img <img
onDragStart={(e) => { e.preventDefault(); }} onDragStart={(e) => { e.preventDefault(); }}
src={this.getTrackThumbnail(value)} src={this.getTrackThumbnail(value)}
style={{ width: 48, height: 48, margin: 8, borderRadius: 4 }} /> style={{ width: 48, height: 48, margin: 8, borderRadius: 4 }} />
) )
} }
<div style={{ <div style={{
flex: "1 1 1px", display: "flex", flexFlow: "column nowrap", flex: "1 1 1px", display: "flex", flexFlow: "column nowrap",
marginLeft: 8, marginLeft: 8,
@@ -1131,7 +1330,7 @@ export default withStyles(
} }
</div> </div>
</DialogContent> </DialogContent>
{!this.state.reordering && ( {(!this.state.reordering && !this.state.multiSelect) && (
<> <>
<Divider /> <Divider />
<DialogActions style={{ justifyContent: "stretch" }}> <DialogActions style={{ justifyContent: "stretch" }}>
@@ -1239,10 +1438,15 @@ export default withStyles(
/> />
<OkCancelDialog open={this.state.openConfirmDeleteDialog} <OkCancelDialog open={this.state.openConfirmDeleteDialog}
text={ text={
(this.isDirectory(this.state.selectedFile)) this.state.multiSelect ?
? "Are you sure you want to delete the selected directory and all its contents?" ("Are you sure you want to delete the selected files and directories and all their contents?")
: "Are you sure you want to delete the selected file?"} : (this.state.selectedFileIsDirectory
? "Are you sure you want to delete the selected directory and all its contents?"
: "Are you sure you want to delete the selected file?"
)
}
okButtonText="Delete" okButtonText="Delete"
onOk={() => this.handleConfirmDelete()} onOk={() => this.handleConfirmDelete()}
onClose={() => { onClose={() => {
@@ -1296,7 +1500,7 @@ export default withStyles(
onClose={() => { this.setState({ moveDialogOpen: false, copyDialogOpen: false }); }} onClose={() => { this.setState({ moveDialogOpen: false, copyDialogOpen: false }); }}
onOk={ onOk={
(path) => { (path) => {
this.setState({ moveDialogOpen: false }); this.setState({ moveDialogOpen: false, copyDialogOpen: false });
this.setDefaultPath(path); this.setDefaultPath(path);
if (this.state.copyDialogOpen) { if (this.state.copyDialogOpen) {
this.onExecuteCopy(path); this.onExecuteCopy(path);
@@ -1314,6 +1518,9 @@ export default withStyles(
); );
} }
openSelectedFile(): void { openSelectedFile(): void {
if (this.state.multiSelect || this.state.reordering) {
return;
}
if (this.isFolderArtwork(this.state.selectedFile)) { if (this.isFolderArtwork(this.state.selectedFile)) {
return; return;
} }
@@ -1334,13 +1541,54 @@ export default withStyles(
return pathFileNameOnly(name); return pathFileNameOnly(name);
} }
} }
private onMove(): void { private handleMove(): void {
this.setState({ moveDialogOpen: true }); this.setState({ moveDialogOpen: true });
} }
private onCopy(): void { private handleCopy(): void {
this.setState({ copyDialogOpen: true }); this.setState({ copyDialogOpen: true });
} }
private async onExecuteMoveMulti(newDirectory: string) {
let files = this.state.selectedFiles.slice();
let newFileList = this.state.selectedFiles.slice();
try {
for (let file of files) {
let fileName = pathFileName(file);
let oldFilePath = pathConcat(this.state.navDirectory, fileName);
let newFilePath = pathConcat(newDirectory, fileName);
await this.model.renameFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty);
let index = newFileList.indexOf(file);
if (index !== -1) {
newFileList.splice(index,1);
}
}
} catch (e) {
this.model.showAlert(e);
}
// update state to refelect what we did.
if (!this.mounted) {
return;
}
if (newFileList.length === 0 )
{
this.setState({
multiSelect: false,
selectedFiles: []
});
} else {
this.setState({
multiSelect: true,
selectedFiles: newFileList,
});
}
this.requestFiles(this.state.navDirectory);
}
private onExecuteMove(newDirectory: string) { private onExecuteMove(newDirectory: string) {
if (this.state.multiSelect) {
this.onExecuteMoveMulti(newDirectory);
return;
}
let fileName = pathFileName(this.state.selectedFile); let fileName = pathFileName(this.state.selectedFile);
let oldFilePath = pathConcat(this.state.navDirectory, fileName); let oldFilePath = pathConcat(this.state.navDirectory, fileName);
let newFilePath = pathConcat(newDirectory, fileName); let newFilePath = pathConcat(newDirectory, fileName);
@@ -1369,8 +1617,32 @@ export default withStyles(
this.setState({ confirmCopyDialogState: null }); this.setState({ confirmCopyDialogState: null });
} }
} }
private async onExecuteCopyMulti(newDirectory: string) {
let files = this.state.selectedFiles.slice();
let fileProperty = this.props.fileProperty;
try {
for (let fileName of files) {
let oldFilePath = pathConcat(this.state.navDirectory, fileName);
let newFilePath = pathConcat(newDirectory, fileName);
await this.model.copyFilePropertyFile(oldFilePath, newFilePath, fileProperty, true);
}
} catch (e) {
this.model.showAlert(e);
}
// update state to reflect what we did.
if (!this.mounted) {
return;
}
this.requestFiles(this.state.navDirectory);
}
private onExecuteCopy(newDirectory: string) { private onExecuteCopy(newDirectory: string) {
this.setState({ copyDialogOpen: false }); this.setState({ copyDialogOpen: false });
if (this.state.multiSelect) {
this.onExecuteCopyMulti(newDirectory);
return;
}
let fileName = pathFileName(this.state.selectedFile); let fileName = pathFileName(this.state.selectedFile);
let oldFilePath = pathConcat(this.state.navDirectory, fileName); let oldFilePath = pathConcat(this.state.navDirectory, fileName);
let newFilePath = pathConcat(newDirectory, fileName); let newFilePath = pathConcat(newDirectory, fileName);
+2 -2
View File
@@ -2019,8 +2019,8 @@ export class PiPedalModel //implements PiPedalModel
return this.copyPreset(instanceId, -1); return this.copyPreset(instanceId, -1);
} }
showAlert(message: string | Error): void { showAlert(message: any): void {
let m = message; let m: string;
if (message instanceof Error) { if (message instanceof Error) {
let e = message as Error; let e = message as Error;
if (e.message) { if (e.message) {