Closes #15 added file action menu

not all buttons implemented yet
This commit is contained in:
controlol 2021-11-04 00:31:52 +01:00
parent 81e0d5fbdc
commit 243967259c
4 changed files with 173 additions and 68 deletions

View File

@ -1,7 +1,8 @@
import { Component, Fragment } from 'react' import { Component, Fragment } from 'react'
import path from 'path' import path from 'path'
import styled from 'styled-components' import styled from 'styled-components'
import { Input, Label } from './fileBrowser.styled.js' import { FileSettingsHeader, FileSettingsPopup, Label } from './fileBrowser.styled.js'
import { Input } from '../styled'
// images for different filetypes // images for different filetypes
import Back from '../assets/icons/arrowLeft.svg' import Back from '../assets/icons/arrowLeft.svg'
@ -41,8 +42,8 @@ import { Button } from '../styled.js'
import assert from 'assert' import assert from 'assert'
import LineLoader from './LineLoader.jsx' import LineLoader from './LineLoader.jsx'
const ROW_HEIGHT = 20 const ROW_HEIGHT = 28
const ROW_GAP = 8 const ROW_GAP = 0
const DATA_PADDING = 3 const DATA_PADDING = 3
const DEBOUNCE_THRESHOLD = 100 const DEBOUNCE_THRESHOLD = 100
@ -89,10 +90,12 @@ const GridFileBrowser = styled.div`
display: grid; display: grid;
grid-template-columns: 1px 1fr ${({shownColumns}) => shownColumns.datetime ? "10rem" : ""} ${({shownColumns}) => shownColumns.date ? "6rem" : ""} ${({shownColumns}) => shownColumns.size ? "6rem" : ""}; grid-template-columns: 1px 1fr ${({shownColumns}) => shownColumns.datetime ? "10rem" : ""} ${({shownColumns}) => shownColumns.date ? "6rem" : ""} ${({shownColumns}) => shownColumns.size ? "6rem" : ""};
align-items: center; align-items: center;
gap: .5rem 1.5rem; /* gap: .5rem 1.5rem; */
gap: 0 1.5rem;
width: 100%; width: 100%;
transition: transform .3s; transition: transform .3s;
padding: .5rem; /* padding: .5rem; */
padding: 0 .5rem;
` `
const BrowserHeader = styled.div` const BrowserHeader = styled.div`
@ -131,6 +134,7 @@ const EllipsisP = styled.p`
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
line-height: 28px;
` `
const SpanPathDirectory = styled.span` const SpanPathDirectory = styled.span`
@ -180,6 +184,19 @@ 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)) const delay = t => new Promise(resolve => setTimeout(resolve, t))
class FileBrowser extends Component { class FileBrowser extends Component {
@ -193,6 +210,7 @@ class FileBrowser extends Component {
prevPath: "", prevPath: "",
transitionFiles: 0, transitionFiles: 0,
showMenu: false, showMenu: false,
showNewFolder: false,
cursorX: 0, cursorX: 0,
cursorY: 0, cursorY: 0,
clicked: "", clicked: "",
@ -239,7 +257,9 @@ class FileBrowser extends Component {
window.removeEventListener('click', this.handleWindowClick) window.removeEventListener('click', this.handleWindowClick)
} }
handleWindowClick = () => this.setState({ showMenu: false }) handleWindowClick = () => this.setState({ showMenu: false, showNewFolder: false })
isMenuOpen = () => this.props.menuOpen || this.state.showMenu || this.state.showNewFolder
// used to filter the files // used to filter the files
handleInputChange({target}) { handleInputChange({target}) {
@ -254,7 +274,7 @@ class FileBrowser extends Component {
// change the way files should be ordered // change the way files should be ordered
updateOrder = orderBy => { updateOrder = orderBy => {
if (this.props.menuOpen || this.state.showMenu) return; if (this.isMenuOpen()) return;
if (this.state.orderBy === orderBy) { if (this.state.orderBy === orderBy) {
this.setState({ orderAscending: !this.state.orderAscending }) this.setState({ orderAscending: !this.state.orderAscending })
@ -301,7 +321,7 @@ class FileBrowser extends Component {
// after the user clicks on a folder // after the user clicks on a folder
updatePath = name => { updatePath = name => {
if (this.props.menuOpen || this.state.showMenu) return; if (this.isMenuOpen()) return;
const newPath = path.join(this.props.currentPath, name) const newPath = path.join(this.props.currentPath, name)
@ -311,7 +331,7 @@ class FileBrowser extends Component {
// after the user clicks on the back button // after the user clicks on the back button
previousDirectory = () => { previousDirectory = () => {
if (this.props.menuOpen || this.state.showMenu) return; if (this.isMenuOpen()) return;
let currentPath = this.props.currentPath.split("/") let currentPath = this.props.currentPath.split("/")
currentPath.pop() currentPath.pop()
@ -322,7 +342,7 @@ class FileBrowser extends Component {
// after the user click on the home button // after the user click on the home button
rootDirectory = () => { rootDirectory = () => {
if (this.props.menuOpen || this.state.showMenu) return; if (this.isMenuOpen()) return;
this.props.updateFiles("/") this.props.updateFiles("/")
this.setState({filter: ""}) this.setState({filter: ""})
@ -330,7 +350,7 @@ class FileBrowser extends Component {
// after the user clicks on a path piece // after the user clicks on a path piece
goToPath = index => { goToPath = index => {
if (this.props.menuOpen || this.state.showMenu) return; if (this.isMenuOpen()) return;
let currentPath = this.props.currentPath.split("/") let currentPath = this.props.currentPath.split("/")
@ -353,8 +373,9 @@ class FileBrowser extends Component {
* Opens the actions menu * Opens the actions menu
* @param {ElementEvent} e The event that called this function * @param {ElementEvent} e The event that called this function
*/ */
openMenu = (e) => { openMenu = (e, isFile) => {
e.preventDefault() e.preventDefault()
e.stopPropagation()
assert( assert(
typeof e.pageX === "number" typeof e.pageX === "number"
@ -363,34 +384,78 @@ class FileBrowser extends Component {
&& e.target.innerHTML.length > 0 && e.target.innerHTML.length > 0
) )
this.setState({ if (isFile) {
cursorX: e.pageX, this.setState({
cursorY: e.pageY, cursorX: e.pageX,
showMenu: true, cursorY: e.pageY,
clicked: e.target.innerHTML 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 (
<FileSettingsPopup onClick={e => e.stopPropagation()}>
<label htmlFor="newFolderName">
<FileSettingsHeader> New Folder </FileSettingsHeader>
</label>
<form onSubmit={this.handleNewFolderSubmit}>
<Input type="text" name="newFolderName" id="newFolderName" autoFocus defaultValue="" autoComplete="off" />
<input type="submit" style={{visibility: "hidden"}} />
</form>
</FileSettingsPopup>
)
} }
/** /**
* Renders a simple menu to perform actions on the clicked file * Renders a simple menu to perform actions on the clicked file
*/ */
renderMenu = () => { renderMenu = () => {
if (this.state.showMenu) return ( const { cursorX, cursorY, showMenu, clicked } = this.state
<div
if (showMenu && clicked.length) return (
<FileMenuContainer
onMouseLeave={() => this.setState({ showMenu: false })} onMouseLeave={() => this.setState({ showMenu: false })}
style={{ cursorX={cursorX} cursorY={cursorY}
position: "fixed",
top: this.state.cursorY - 3,
left: this.state.cursorX + 3,
backgroundColor: "var(--button-color)",
zIndex: 900,
borderRadius: "3px"
}}
> >
<Button onClick={() => this.doAction("copy")}> Copy </Button> <Button onClick={() => this.doAction("copy")}> Copy </Button>
<Button onClick={() => this.doAction("move")}> Move </Button> <Button onClick={() => this.doAction("move")}> Move </Button>
<Button onClick={() => this.doAction("delete")}> Delete </Button> <Button onClick={() => this.doAction("delete")}> Delete </Button>
</div> <Button onClick={this.openNewFolderPopup}> New Folder </Button>
</FileMenuContainer>
)
if (showMenu) return (
<FileMenuContainer
onMouseLeave={() => this.setState({ showMenu: false })}
cursorX={cursorX} cursorY={cursorY}
>
<Button onClick={this.openNewFolderPopup}> New Folder </Button>
</FileMenuContainer>
) )
} }
@ -408,17 +473,17 @@ class FileBrowser extends Component {
files = this.getOrderedItems(files).slice(this.state.from, this.state.to) files = this.getOrderedItems(files).slice(this.state.from, this.state.to)
return files return files
.map(v => ( .map(({ Name, IsDir, Size, ModTime, MimeType }) => (
<Fragment key={v.Name + "file"}> <Fragment key={Name + "file"}>
{ this.renderImage(v.MimeType, v.Name) } { this.renderImage(MimeType, Name) }
{ {
v.IsDir ? IsDir ?
<DirNameP onClick={() => this.updatePath(v.Name)} onContextMenu={this.openMenu}>{ v.Name }</DirNameP> <DirNameP onClick={() => this.updatePath(Name)} onContextMenu={e => this.openMenu(e, true)}>{ Name }</DirNameP>
: :
<FilenameP onContextMenu={this.openMenu}>{ v.Name }</FilenameP> <FilenameP onContextMenu={e => this.openMenu(e, true)}>{ Name }</FilenameP>
} }
<ModifiedP shownColumns={shownColumns}> { shownColumns.datetime ? v.ModTime?.toLocaleString() : v.ModTime?.toLocaleDateString() } </ModifiedP> <ModifiedP shownColumns={shownColumns}> { shownColumns.datetime ? ModTime?.toLocaleString() : ModTime?.toLocaleDateString() } </ModifiedP>
<SizeP shownColumns={shownColumns}> { !v.IsDir ? bytesToString(v.Size, {}) : "" } </SizeP> <SizeP shownColumns={shownColumns}> { !IsDir ? bytesToString(Size, {}) : "" } </SizeP>
</Fragment> </Fragment>
)) ))
} }
@ -479,12 +544,10 @@ class FileBrowser extends Component {
return ( return (
<FBContainer active={active} onClick={setActive}> <FBContainer active={active} onClick={setActive}>
{ { this.renderMenu() }
this.renderMenu() { this.renderNewFolderPopup() }
} { loading === true && <LineLoader/> }
{
loading ? <LineLoader/> : ""
}
<BrowserHeader> <BrowserHeader>
<BrowserHeaderDiv> <BrowserHeaderDiv>
<BrowseImage src={Back} alt="up directory" width="25" height="25" onClick={this.previousDirectory} /> <BrowseImage src={Back} alt="up directory" width="25" height="25" onClick={this.previousDirectory} />
@ -546,7 +609,7 @@ class FileBrowser extends Component {
</GridFileBrowser> </GridFileBrowser>
</BrowserHeader> </BrowserHeader>
<BrowserWrapper onScroll={this.handleGridScroll}> <BrowserWrapper onScroll={this.handleGridScroll} onContextMenu={e => this.openMenu(e, false)}>
{ {
transitionFiles !== 0 && transitionFiles !== 0 &&
<GridFileBrowser shownColumns={shownColumns} style={{ <GridFileBrowser shownColumns={shownColumns} style={{

View File

@ -1,24 +1,6 @@
import styled from 'styled-components' import styled from 'styled-components'
import { Button } from '../styled' import { Button } from '../styled'
export const Input = styled.input`
width: 100%;
padding: .6em 1em;
margin: .5em 0;
display: inline-block;
border-radius: 4px;
border: 1px solid var(--primary-color);
background-color: var(--background-color);
color: white;
transition: border .3s ease-in-out;
font-size: .9rem;
&:focus {
outline: none;
background-color: #282828;
}
`
export const Label = styled.label` export const Label = styled.label`
margin-right: .7vw; margin-right: .7vw;
` `

View File

@ -4,6 +4,7 @@ import API from '../utils/API'
import FileBrowser from './fileBrowser' import FileBrowser from './fileBrowser'
import { BrowserSettingButton, FileBrowserRemotes, FileBrowsersContainer, FileBrowserSettings, FileBrowserWrapper, FileSettingsPopup, FileSettingsHeader, FileColumnSettingsContainer, RemoteButton } from './fileBrowser.styled' import { BrowserSettingButton, FileBrowserRemotes, FileBrowsersContainer, FileBrowserSettings, FileBrowserWrapper, FileSettingsPopup, FileSettingsHeader, FileColumnSettingsContainer, RemoteButton } from './fileBrowser.styled'
import assert from 'assert' import assert from 'assert'
import path from 'path'
import BrowserSingle from '../assets/icons/browserSingle.svg' import BrowserSingle from '../assets/icons/browserSingle.svg'
import BrowserDual from '../assets/icons/browserDual.svg' import BrowserDual from '../assets/icons/browserDual.svg'
@ -126,18 +127,57 @@ class FileBrowserMenu extends Component {
* @param {String} action type of action to be performed * @param {String} action type of action to be performed
* @param {String} path dir or file * @param {String} path dir or file
*/ */
doAction = (brIndex, action, path) => { doAction = (brIndex, action, file) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fs = this.state.browserFs[brIndex] + ":",
remote = path.join(this.state.currentPath[brIndex], file)
switch(action) { switch(action) {
case "copy": case "copy":
console.log("did copy", path) const dstFs = this.state.browserFs[brIndex === 0 ? 1 : 0] + ":",
break; dstRemote = this.state.currentPath[brIndex === 0 ? 1 : 0]
console.log({ fs, remote, dstFs, dstRemote })
return API.request({
url: "/sync/copy",
data: {
srcFs: fs + remote,
dstFs: dstFs + dstRemote,
_async: true
}
})
.then(resolve)
.catch(err => console.error(err))
case "move": case "move":
console.log("did move", path) console.log("did move", file)
break; break;
case "delete": case "delete":
console.log("did delete", path) console.log("did delete", file)
break; break;
case "newfolder":
return API.request({
url: "/operations/mkdir",
data: {
fs, remote
}
})
.then(() => {
let { files } = this.state
files[brIndex].push({
Name: file,
ModTime: new Date(),
Size: -1,
IsDir: true,
MimeType: "inode/directory"
})
this.setState({ files })
return resolve()
})
.catch(reject)
default: return reject(new Error("Invalid file action")) default: return reject(new Error("Invalid file action"))
} }
}) })
@ -236,8 +276,10 @@ class FileBrowserMenu extends Component {
} }
renderRemoteButtons = () => { renderRemoteButtons = () => {
const { browserFs, activeBrowser } = this.state
return this.props.remotes.map(v => ( return this.props.remotes.map(v => (
<RemoteButton key={v.name} onClick={() => this.setRemote(v.name)}> { v.name } </RemoteButton> <RemoteButton key={v.name} onClick={() => this.setRemote(v.name)} active={browserFs[activeBrowser] === v.name}> { v.name } </RemoteButton>
)) ))
} }

View File

@ -364,4 +364,22 @@ export const Checkbox = styled.input`
height: 1rem; height: 1rem;
outline: unset; outline: unset;
} }
`
export const Input = styled.input`
width: 100%;
padding: .6em 1em;
margin: .5em 0;
display: inline-block;
border-radius: 4px;
border: 1px solid var(--primary-color);
background-color: var(--button-color);
color: white;
transition: border .3s ease-in-out;
font-size: .9rem;
&:focus {
outline: none;
background-color: var(--button-hover);
}
` `