farjs-app
Version:
FAR.js - Cross-platform File and Archive Manager app in your terminal
163 lines (145 loc) • 4.76 kB
JavaScript
/**
* @import { Dispatch } from "@farjs/filelist/FileListData.mjs"
* @import { FileListItem } from "@farjs/filelist/api/FileListItem.mjs"
* @import { CopyProcessItem } from "./CopyProcess.mjs"
*/
import path from "path";
import nodeFs from "fs";
import fsPromises from "fs/promises";
import React, { useLayoutEffect, useRef, useState } from "react";
import Task from "@farjs/ui/task/Task.mjs";
import TaskAction from "@farjs/ui/task/TaskAction.mjs";
import MessageBox from "@farjs/ui/popup/MessageBox.mjs";
import StatusPopup from "@farjs/ui/popup/StatusPopup.mjs";
import FileListActions from "@farjs/filelist/FileListActions.mjs";
import Theme from "@farjs/ui/theme/Theme.mjs";
import MessageBoxAction from "@farjs/ui/popup/MessageBoxAction.mjs";
const h = React.createElement;
/**
* @typedef {{
* existsSync(path: string): boolean;
* rename(oldPath: string, newPath: string): Promise<void>;
* }} FS
*/
/**
* @typedef {{
* readonly dispatch: Dispatch;
* readonly actions: FileListActions;
* readonly fromPath: string;
* readonly items: readonly CopyProcessItem[];
* readonly toPath: string;
* onTopItem(topItem: FileListItem): void;
* onDone(): void;
* }} MoveProcessProps
*/
/**
* @typedef {{
* readonly currItem: string;
* readonly existing?: string;
* }} MoveState
*/
/**
* @param {MoveProcessProps} props
*/
const MoveProcess = (props) => {
const { statusPopupComp, messageBoxComp } = MoveProcess;
const [state, setState] = useState(
() => /** @type {MoveState} */ ({ currItem: "" })
);
const inProgress = useRef(false);
const existsPromise = useRef({
/** @type {Promise<boolean>} */
p: Promise.resolve(true),
/** @type {(v: boolean) => void} */
resolve: () => {},
});
const askWhenExists = useRef(true);
const currTheme = Theme.useTheme();
const moveItems = () => {
const resultP = props.items.reduce(async (resP, cpItem) => {
const res = await resP;
const { item: currItem, toName } = cpItem;
if (res && inProgress.current) {
setState((s) => {
return { ...s, currItem: currItem.name };
});
const oldPath = path.join(props.fromPath, currItem.name);
const newPath = path.join(props.toPath, toName);
const exists = !currItem.isDir && MoveProcess.fs.existsSync(newPath);
if (exists && askWhenExists.current) {
setState((s) => {
return { ...s, existing: newPath };
});
/** @type {(v: boolean) => void} */
let resolve = () => {};
const p = new Promise((res) => (resolve = res));
existsPromise.current = { p, resolve };
}
const overwrite = await existsPromise.current.p;
if (!exists || overwrite) {
await MoveProcess.fs.rename(oldPath, newPath);
props.onTopItem(currItem);
return inProgress.current;
}
return inProgress.current;
}
return res;
}, Promise.resolve(true));
resultP.then(
() => props.onDone(),
() => {
props.onDone();
props.dispatch(TaskAction(Task("Moving items", resultP)));
}
);
};
/** @type {(overwrite: boolean, all?: boolean, cancel?: boolean) => () => void} */
function onExistsAction(overwrite, all = false, cancel = false) {
return () => {
setState((s) => {
return { ...s, existing: undefined };
});
askWhenExists.current = !all;
inProgress.current = !cancel;
existsPromise.current.resolve(overwrite);
};
}
useLayoutEffect(() => {
// start
inProgress.current = true;
moveItems();
}, []);
const existing = state.existing;
return h(
React.Fragment,
null,
h(statusPopupComp, {
text: `Moving item\n${state.currItem}`,
title: "Move",
onClose: () => {
// stop
inProgress.current = false;
},
}),
existing !== undefined
? h(messageBoxComp, {
title: "Warning",
message: `File already exists.\nDo you want to overwrite it's content?\n\n${existing}`,
actions: [
MessageBoxAction("Overwrite")(onExistsAction(true)),
MessageBoxAction("All")(onExistsAction(true, true)),
MessageBoxAction("Skip")(onExistsAction(false)),
MessageBoxAction("Skip all")(onExistsAction(false, true)),
MessageBoxAction("Cancel", true)(onExistsAction(false, true, true)),
],
style: currTheme.popup.error,
})
: null
);
};
MoveProcess.displayName = "MoveProcess";
MoveProcess.statusPopupComp = StatusPopup;
MoveProcess.messageBoxComp = MessageBox;
/** @type {FS} */
MoveProcess.fs = { ...nodeFs, ...fsPromises };
export default MoveProcess;