diff --git a/src/components/fileBrowser.jsx b/src/components/fileBrowser.jsx index 1c7839a..c9e3a8d 100644 --- a/src/components/fileBrowser.jsx +++ b/src/components/fileBrowser.jsx @@ -1,7 +1,7 @@ import { Component, Fragment } from 'react' import path from 'path' import styled from 'styled-components' -import { FileSettingsHeader, FileSettingsPopup, Label } from './fileBrowser.styled.js' +import { Label } from './fileBrowser.styled.js' import { Input } from '../styled' // images for different filetypes @@ -38,8 +38,6 @@ import ZIP from '../assets/fileTypes/zip.svg' import CaretDown from '../assets/icons/caretDown.svg' import bytesToString from '../utils/bytestring.js' -import { Button } from '../styled.js' -import assert from 'assert' import LineLoader from './LineLoader.jsx' const ROW_HEIGHT = 28 @@ -184,19 +182,6 @@ const SearchInput = styled(Input)` } ` -const FileMenuContainer = styled.div` - position: fixed; - top: ${({cursorY}) => cursorY - 3}px; - left: ${({cursorX}) => cursorX + 3}px; - background-color: var(--button-color); - z-index: 900; - border-radius: .2rem; - - div { - text-align: left; - } -` - const delay = t => new Promise(resolve => setTimeout(resolve, t)) class FileBrowser extends Component { @@ -232,10 +217,9 @@ class FileBrowser extends Component { window.addEventListener('click', this.handleWindowClick) } - componentDidUpdate = () => { + componentDidUpdate = (prevProps) => { // if the component was just created set the path - if (this.state.prevPath === "" && this.props.currentPath !== "") return this.setState({ prevPath: this.props.currentPath, files: this.props.files }) - + if (prevProps.currentPath === "" && this.props.currentPath !== "") return this.setState({ prevPath: this.props.currentPath, files: this.props.files }) // the path changed if (this.props.currentPath !== this.state.prevPath) { @@ -250,6 +234,8 @@ class FileBrowser extends Component { delay(300).then(() => this.setState({ transitionFiles: 3 * direction, files: this.props.files })) // delay(11500).then(() => this.setState({ transitionFiles: 3 })) delay(600).then(() => this.setState({ transitionFiles: 0 })) + } else if (prevProps.files !== this.props.files) { + this.setState({ files: this.props.files }) } } @@ -362,103 +348,6 @@ class FileBrowser extends Component { } } - /** - * Wrapper for the props.action function - * @param {String} a type of action to be performed - * @returns {Promise} - */ - doAction = a => this.props.action(a, this.state.clicked).catch(err => console.error(err)) - - /** - * Opens the actions menu - * @param {ElementEvent} e The event that called this function - */ - openMenu = (e, isFile) => { - e.preventDefault() - e.stopPropagation() - - assert( - typeof e.pageX === "number" - && typeof e.pageY === "number" - && typeof e.target.innerHTML === "string" - && e.target.innerHTML.length > 0 - ) - - if (isFile) { - this.setState({ - cursorX: e.pageX, - cursorY: e.pageY, - showMenu: true, - clicked: e.target.innerHTML - }) - } else { - this.setState({ - cursorX: e.pageX, - cursorY: e.pageY, - showMenu: true, - clicked: "" - }) - } - } - - openNewFolderPopup = e => { - e.stopPropagation() - return this.setState({ showNewFolder: true, showMenu: false }) - } - - handleNewFolderSubmit = e => { - e.preventDefault() - this.props.action("newfolder", e.target.newFolderName.value) - .then(() => this.setState({ showNewFolder: false })) - .catch(err => { - console.error(err) - this.setState({ showNewFolder: false }) - }) // the user should see this error - } - - renderNewFolderPopup = () => { - if (this.state.showNewFolder) return ( - e.stopPropagation()}> - - -
- - -
-
- ) - } - - /** - * Renders a simple menu to perform actions on the clicked file - */ - renderMenu = () => { - const { cursorX, cursorY, showMenu, clicked } = this.state - - if (showMenu && clicked.length) return ( - this.setState({ showMenu: false })} - cursorX={cursorX} cursorY={cursorY} - > - - - - - - ) - - if (showMenu) return ( - this.setState({ showMenu: false })} - cursorX={cursorX} cursorY={cursorY} - > - - - ) - } - /** * Render the files found in a directory * @param {Boolean} isNext are these the next files to be rendered, if so use props instead of state @@ -478,9 +367,9 @@ class FileBrowser extends Component { { this.renderImage(MimeType, Name) } { IsDir ? - this.updatePath(Name)} onContextMenu={e => this.openMenu(e, true)}>{ Name } + this.updatePath(Name)} onContextMenu={e => this.props.openMenu(e, true)}>{ Name } : - this.openMenu(e, true)}>{ Name } + this.props.openMenu(e, true)}>{ Name } } { shownColumns.datetime ? ModTime?.toLocaleString() : ModTime?.toLocaleDateString() } { !IsDir ? bytesToString(Size, {}) : "" } @@ -544,8 +433,6 @@ class FileBrowser extends Component { return ( - { this.renderMenu() } - { this.renderNewFolderPopup() } { loading === true && } @@ -609,7 +496,7 @@ class FileBrowser extends Component { - this.openMenu(e, false)}> + this.props.openMenu(e, false)}> { transitionFiles !== 0 && { this.getFiles(0, tempPath) @@ -67,13 +76,31 @@ class FileBrowserMenu extends Component { window.removeEventListener('click', this.handleWindowClick) window.removeEventListener("orientationchange", this.handleOrientationChange) + window.removeEventListener('keyup', this.handleKeyup) } - // stopPropagation does not work because it is a different event cause - handleWindowClick = () => this.setState({ showFileSettings: false }) + /** + * closes any open menu + * stopPropagation does not work here because it is a different event + */ + handleWindowClick = () => { + this.setState({ showFileSettings: false }) + this.closeMenu() + } + // update the innerwidth, used to determine wether to show one or two browsers handleOrientationChange = () => this.setState({ windowWidth: window.innerWidth }) + /** + * closes the menu if the user presses the escape key + */ + handleKeyup = e => { + if (e.key === "Escape") { + this.setState({ showFileSettings: false }) + this.closeMenu() + } + } + /** * * @param {Number} brIndex identify which browser wants new files @@ -125,62 +152,119 @@ class FileBrowserMenu extends Component { * * @param {Number} brIndex identify which browser wants to do the action * @param {String} action type of action to be performed - * @param {String} path dir or file + * @param {String} newFile only used when creating a dir or renaming */ - doAction = (brIndex, action, file) => { - return new Promise((resolve, reject) => { - const fs = this.state.browserFs[brIndex] + ":", - remote = path.join(this.state.currentPath[brIndex], file) + doAction = (brIndex, action, newFile) => { + if (!action) return this.closeMenu() - switch(action) { - case "copy": - const dstFs = this.state.browserFs[brIndex === 0 ? 1 : 0] + ":", - dstRemote = this.state.currentPath[brIndex === 0 ? 1 : 0] + // dir or file, only used when renaming + let clickedFile = this.state.menuInfo.file - console.log({ fs, remote, dstFs, dstRemote }) + const fs = this.state.browserFs[brIndex] + ":", + remote = path.join(this.state.currentPath[brIndex], clickedFile.Name || newFile), + dstFs = this.state.browserFs[brIndex === 0 ? 1 : 0] + ":", + dstRemote = path.join(this.state.currentPath[brIndex === 0 ? 1 : 0], newFile || clickedFile.Name) + switch(action) { + case "copy": + return API.request({ + url: clickedFile.IsDir ? "/sync/copy" : "/operations/copyfile", + data: Object.assign({ + _async: true + }, clickedFile.IsDir ? { srcFs: fs + remote, dstFs: dstFs + dstRemote, } : { srcFs: fs, srcRemote: remote, dstFs, dstRemote }) + }) + .catch(err => console.error(err)) + .finally(this.closeMenu) + case "move": + return API.request({ + url: clickedFile.IsDir ? "/sync/move" : "/operations/movefile", + data: Object.assign({ + _async: true + }, clickedFile.IsDir ? { srcFs: fs + remote, dstFs: dstFs + dstRemote, } : { srcFs: fs, srcRemote: remote, dstFs, dstRemote }) + }) + .catch(err => console.error(err)) + .finally(this.closeMenu) + case "delete": + return API.request({ + url: "operations/" + (clickedFile.IsDir ? "purge" : "deletefile"), + data: { fs, remote, _async: true } + }) + .then(() => { + let { files } = this.state + files[brIndex] = files[brIndex].filter(v => v.Name !== clickedFile.Name) + this.setState({ files }) + }) + .catch(err => console.error(err)) + .finally(this.closeMenu) + case "rename": + if (clickedFile.IsDir) return API.request({ + url: "/operations/mkdir", + data: { fs, remote: dstRemote } + }) + .then(() => { return API.request({ - url: "/sync/copy", + url: "/sync/move", data: { srcFs: fs + remote, - dstFs: dstFs + dstRemote, - _async: true - } - }) - .then(resolve) - .catch(err => console.error(err)) - case "move": - console.log("did move", file) - break; - case "delete": - console.log("did delete", file) - break; - case "newfolder": - return API.request({ - url: "/operations/mkdir", - data: { - fs, remote + dstFs: fs + path.join(this.state.currentPath[brIndex], newFile) } }) .then(() => { - let { files } = this.state - - files[brIndex].push({ - Name: file, - ModTime: new Date(), - Size: -1, - IsDir: true, - MimeType: "inode/directory" + return API.request({ + url: "/operations/purge", + data: { fs, remote, _async: true } }) - - this.setState({ files }) - - return resolve() + .then(() => { + let { files } = this.state + let f = files[brIndex].filter(v => v.Name === clickedFile.Name)[0] + f.Name = newFile + this.setState({ files }) + }) + .catch(err => console.error(err)) }) - .catch(reject) - default: return reject(new Error("Invalid file action")) - } - }) + .catch(err => console.error(err)) + }) + .catch(err => console.error(err)) + .finally(this.closeMenu) + + return API.request({ + url: clickedFile.IsDir ? "/sync/copy" : "/operations/movefile", + data: { srcFs: fs, + srcRemote: remote, + dstFs: fs, + dstRemote: path.join(this.state.currentPath[brIndex], newFile) + } + }) + .then(() => { + let { files } = this.state + let f = files[brIndex].filter(v => v.Name === clickedFile.Name)[0] + f.Name = newFile + this.setState({ files }) + }) + .catch(err => console.error(err)) + .finally(this.closeMenu) + case "newfolder": + return API.request({ + url: "/operations/mkdir", + data: { fs, remote } + }) + .then(() => { + let { files } = this.state + + files[brIndex].push({ + Name: newFile, + ModTime: new Date(), + Size: -1, + IsDir: true, + MimeType: "inode/directory" + }) + + this.setState({ files }) + }) + .catch(() => {}) + .finally(this.closeMenu) + default: assert(false); + } } handleColumnChange = ({target}) => { @@ -216,37 +300,53 @@ class FileBrowserMenu extends Component { } } + /** + * Opens the actions menu + * @param {ElementEvent} e The event that called this function + * @param {Boolean} isFile was a file clicked + */ + openMenu = (brIndex, e, isFile) => { + e.preventDefault() + e.stopPropagation() + + assert( + typeof e.pageX === "number" + && typeof e.pageY === "number" + && typeof e.target.innerHTML === "string" + && e.target.innerHTML.length > 0 + ) + + let file = {} + if (isFile) file = this.state.files[brIndex].filter(v => v.Name === e.target.innerHTML)[0] + + this.setState({ + menuInfo: { + file, + brIndex, + cursorX: e.pageX, + cursorY: e.pageY, + otherPath: this.state.currentPath[brIndex === 0 ? 1 : 0] + } + }) + } + + closeMenu = () => { + this.setState({ + menuInfo: { + cursorX: 0, + cursorY: 0, + brIndex: -1, + file: {}, + otherPath: "" + } + }) + } + openFileSettings = e => { e.stopPropagation() this.setState({ showFileSettings: true }) } - renderFileSettings = () => { - const { size, date, datetime } = this.state.shownColumns - - if (this.state.showFileSettings) return ( - e.stopPropagation()}> - Columns - -
- - -
- -
- - -
- -
- - -
-
-
- ) - } - switchBrowserMode = () => { if (this.state.loading[0] || this.state.loading[1]) return; @@ -275,6 +375,42 @@ class FileBrowserMenu extends Component { }, 50) } + /** + * Renders a simple menu to perform actions on the clicked file + */ + renderMenu = () => { + const { menuInfo } = this.state + const { brIndex } = menuInfo + + if (brIndex !== -1) return this.doAction(brIndex, action, file)} hideMenu={() => this.setState({ showMenu: false })} /> + } + + renderFileSettings = () => { + const { size, date, datetime } = this.state.shownColumns + + if (this.state.showFileSettings) return ( + e.stopPropagation()}> + Columns + +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ ) + } + renderRemoteButtons = () => { const { browserFs, activeBrowser } = this.state @@ -286,46 +422,28 @@ class FileBrowserMenu extends Component { renderBrowser = () => { const { files, currentPath, loading, dualBrowser, activeBrowser, shownColumns, windowWidth } = this.state + const data = files.map((files, i) => ({ + files, + setActive: () => this.setActiveBrowser(i), + updateFiles: path => this.getFiles(i, path), + currentPath: currentPath[i], + loading: loading[i], + shownColumns: shownColumns, + active: activeBrowser === i && dualBrowser, + openMenu: (e, isFile) => this.openMenu(i, e, isFile) + })) + if (windowWidth >= 1200) return ( - this.setActiveBrowser(0)} - action={(a, p) => this.doAction(0, a, p)} - files={files[0]} - updateFiles={path => this.getFiles(0, path)} - currentPath={currentPath[0]} - loading={loading[0]} - shownColumns={shownColumns} - active={activeBrowser === 0} - /> + { dualBrowser && - this.setActiveBrowser(1)} - action={(a, p) => this.doAction(1, a, p)} - files={files[1]} - updateFiles={path => this.getFiles(1, path)} - currentPath={currentPath[1]} - loading={loading[1]} - shownColumns={shownColumns} - active={activeBrowser === 1 && dualBrowser} - /> + } ) - return ( - this.setActiveBrowser(activeBrowser)} - action={(a, p) => this.doAction(activeBrowser, a, p)} - files={files[activeBrowser]} - updateFiles={path => this.getFiles(activeBrowser, path)} - currentPath={currentPath[activeBrowser]} - loading={loading[activeBrowser]} - shownColumns={shownColumns} - active={false} - /> - ) + return } renderSwitchBrowser = () => { @@ -347,9 +465,8 @@ class FileBrowserMenu extends Component { render = () => { return ( - { - this.renderFileSettings() - } + { this.renderFileSettings() } + { this.renderMenu() } File Browser Close