2020-06-10 18:13:56 +02:00
import { EventHandler , ReactEventHandler , Registry } from "tc-shared/events" ;
2020-12-18 19:18:01 +01:00
import { useContext , useEffect , useRef , useState } from "react" ;
2020-06-10 18:13:56 +02:00
import { FileType } from "tc-shared/file/FileManager" ;
2021-03-18 18:25:20 +01:00
import { getKeyBoard , SpecialKey } from "tc-shared/PPTListener" ;
2020-06-10 18:13:56 +02:00
import { createErrorModal } from "tc-shared/ui/elements/Modal" ;
import { tra } from "tc-shared/i18n/localize" ;
import { network } from "tc-shared/ui/frames/chat" ;
import { Table , TableColumn , TableRow , TableRowElement } from "tc-shared/ui/react-elements/Table" ;
import { ReactComponentBase } from "tc-shared/ui/react-elements/ReactComponentBase" ;
import { Translatable } from "tc-shared/ui/react-elements/i18n" ;
2021-03-14 19:39:08 +01:00
import moment from "moment" ;
2020-06-10 18:13:56 +02:00
import { MenuEntryType , spawn_context_menu } from "tc-shared/ui/elements/ContextMenu" ;
import { BoxedInputField } from "tc-shared/ui/react-elements/InputField" ;
2021-01-10 17:36:57 +01:00
import { LogCategory , logWarn } from "tc-shared/log" ;
2020-12-18 19:18:01 +01:00
import { LoadingDots } from "tc-shared/ui/react-elements/LoadingDots" ;
2020-06-10 18:13:56 +02:00
import {
FileBrowserEvents ,
FileTransferUrlMediaType ,
ListedFileInfo ,
TransferStatus
2020-12-18 19:18:01 +01:00
} from "tc-shared/ui/modal/transfer/FileDefinitions" ;
import { joinClassList } from "tc-shared/ui/react-elements/Helper" ;
2021-01-10 17:36:57 +01:00
import React = require ( "react" ) ;
2020-12-18 19:18:01 +01:00
export interface FileBrowserRendererClasses {
navigation ? : {
boxedInput? : string
} ,
fileTable ? : {
table? : string ,
header? : string ,
body? : string
} ,
fileEntry ? : {
entry? : string ,
selected? : string ,
dropHovered? : string
}
}
const EventsContext = React . createContext < Registry < FileBrowserEvents > > ( undefined ) ;
const CustomClassContext = React . createContext < FileBrowserRendererClasses > ( undefined ) ;
export const FileBrowserClassContext = CustomClassContext ;
2020-06-10 18:13:56 +02:00
2020-12-18 19:18:01 +01:00
const cssStyle = require ( "./FileBrowserRenderer.scss" ) ;
2020-06-10 18:13:56 +02:00
interface NavigationBarProperties {
2020-12-18 19:18:01 +01:00
initialPath : string ;
2020-06-10 18:13:56 +02:00
events : Registry < FileBrowserEvents > ;
}
interface NavigationBarState {
currentPath : string ;
state : "editing" | "navigating" | "normal" ;
}
2020-12-18 19:18:01 +01:00
const ArrowRight = ( ) = > (
< div className = { cssStyle . arrow } >
< div className = { cssStyle . inner } / >
< / div >
) ;
2020-06-10 18:13:56 +02:00
const NavigationEntry = ( props : { events : Registry < FileBrowserEvents > , path : string , name : string } ) = > {
const [ dragHovered , setDragHovered ] = useState ( false ) ;
useEffect ( ( ) = > {
2020-09-12 15:49:20 +02:00
if ( ! dragHovered )
2020-06-10 18:13:56 +02:00
return ;
const dragListener = ( ) = > setDragHovered ( false ) ;
props . events . on ( "notify_drag_ended" , dragListener ) ;
return ( ) = > props . events . off ( "notify_drag_ended" , dragListener ) ;
} ) ;
return (
< a
className = { ( props . name . length > 9 ? cssStyle . pathShrink : "" ) + " " + ( dragHovered ? cssStyle . hovered : "" ) }
title = { props . name }
2020-12-18 19:18:01 +01:00
onClick = { event = > {
event . preventDefault ( ) ;
props . events . fire ( "action_navigate_to" , { path : props.path } ) ;
} }
2020-06-10 18:13:56 +02:00
onDragOver = { event = > {
const types = event . dataTransfer . types ;
2020-12-18 19:18:01 +01:00
if ( types . length !== 1 ) {
2020-06-10 18:13:56 +02:00
return ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
2020-09-12 15:49:20 +02:00
if ( types [ 0 ] === FileTransferUrlMediaType ) {
2020-06-10 18:13:56 +02:00
/* TODO: Detect if its remote move or internal move */
event . dataTransfer . effectAllowed = "move" ;
2020-09-12 15:49:20 +02:00
} else if ( types [ 0 ] === "Files" ) {
2020-06-10 18:13:56 +02:00
event . dataTransfer . effectAllowed = "copy" ;
} else {
return ;
}
event . preventDefault ( ) ;
setDragHovered ( true ) ;
} }
onDragLeave = { ( ) = > setDragHovered ( false ) }
onDrop = { event = > {
const types = event . dataTransfer . types ;
2020-12-18 19:18:01 +01:00
if ( types . length !== 1 ) {
2020-06-10 18:13:56 +02:00
return ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
/* TODO: Fix this code duplicate! */
2020-09-12 15:49:20 +02:00
if ( types [ 0 ] === FileTransferUrlMediaType ) {
2020-06-10 18:13:56 +02:00
/* TODO: If cross move upload! */
const fileUrls = event . dataTransfer . getData ( FileTransferUrlMediaType ) . split ( "&" ) . map ( e = > decodeURIComponent ( e ) ) ;
2020-09-12 15:49:20 +02:00
for ( const fileUrl of fileUrls ) {
2020-06-10 18:13:56 +02:00
const name = fileUrl . split ( "/" ) . last ( ) ;
const oldPath = fileUrl . split ( "/" ) . slice ( 0 , - 1 ) . join ( "/" ) + "/" ;
props . events . fire ( "action_rename_file" , {
2021-06-11 12:22:16 +02:00
newPath : props.path.endsWith ( "/" ) ? props.path : props.path + "/" ,
2020-06-10 18:13:56 +02:00
oldPath : oldPath ,
oldName : name ,
newName : name
} ) ;
}
2020-09-12 15:49:20 +02:00
} else if ( types [ 0 ] === "Files" ) {
props . events . fire ( "action_start_upload" , {
path : props.path ,
mode : "files" ,
files : [ . . . event . dataTransfer . files ]
} ) ;
2020-06-10 18:13:56 +02:00
} else {
2021-01-10 17:36:57 +01:00
logWarn ( LogCategory . FILE_TRANSFER , tr ( "Received an unknown drop media type (%o)" ) , types ) ;
2020-06-10 18:13:56 +02:00
event . preventDefault ( ) ;
return ;
}
event . preventDefault ( ) ;
} }
> { props . name } < / a >
) ;
} ;
@ReactEventHandler ( e = > e . props . events )
export class NavigationBar extends ReactComponentBase < NavigationBarProperties , NavigationBarState > {
private refRendered = React . createRef < HTMLInputElement > ( ) ;
private refInput = React . createRef < BoxedInputField > ( ) ;
private ignoreBlur = false ;
private lastSucceededPath = "" ;
protected defaultState ( ) : NavigationBarState {
return {
2020-12-18 19:18:01 +01:00
currentPath : this.props.initialPath ,
2021-03-23 15:31:59 +01:00
state : "navigating" ,
2020-06-10 18:13:56 +02:00
}
}
2021-03-23 15:31:59 +01:00
componentDidMount() {
this . props . events . fire ( "query_current_path" ) ;
}
2020-06-10 18:13:56 +02:00
render() {
let input ;
let path = this . state . currentPath ;
2021-03-23 15:31:59 +01:00
if ( ! path . endsWith ( "/" ) ) {
2020-06-10 18:13:56 +02:00
path += "/" ;
2021-03-23 15:31:59 +01:00
}
2020-06-10 18:13:56 +02:00
2020-09-12 15:49:20 +02:00
if ( this . state . state === "editing" ) {
2020-06-10 18:13:56 +02:00
input = (
2020-12-18 19:18:01 +01:00
< CustomClassContext.Consumer >
{ customClasses = > (
< BoxedInputField key = { "nav-editing" }
ref = { this . refInput }
defaultValue = { path }
leftIcon = { ( ) = >
< div key = { "left-icon" }
className = { cssStyle . directoryIcon + " " + cssStyle . containerIcon } >
< div className = { "icon_em client-file_home" } / >
< / div >
}
rightIcon = { ( ) = >
< div key = { "right-icon" }
className = { cssStyle . refreshIcon + " " + cssStyle . containerIcon } >
< div className = { "icon_em client-refresh" } / >
< / div >
}
onChange = { path = > this . onPathEntered ( path ) }
onBlur = { ( ) = > this . onInputPathBluer ( ) }
className = { customClasses ? . navigation ? . boxedInput }
/ >
) }
< / CustomClassContext.Consumer >
2020-06-10 18:13:56 +02:00
) ;
2020-09-12 15:49:20 +02:00
} else if ( this . state . state === "navigating" || this . state . state === "normal" ) {
2020-06-10 18:13:56 +02:00
input = (
2020-12-18 19:18:01 +01:00
< CustomClassContext.Consumer >
{ customClasses = > (
< BoxedInputField key = { "nav-rendered" }
ref = { this . refInput }
leftIcon = { ( ) = >
< div key = { "left-icon" }
className = { cssStyle . directoryIcon + " " + cssStyle . containerIcon }
onClick = { event = > this . onPathClicked ( event , - 1 ) } >
< div className = { "icon_em client-file_home" } / >
< / div >
}
rightIcon = { ( ) = >
< div
key = { "right-icon" }
className = { cssStyle . refreshIcon + " " + ( this . state . state === "normal" ? cssStyle . enabled : "" ) + " " + cssStyle . containerIcon }
onClick = { ( ) = > this . onButtonRefreshClicked ( ) } >
< div className = { "icon_em client-refresh" } / >
< / div >
}
inputBox = { ( ) = >
< div key = { "custom-input" } className = { cssStyle . containerPath }
ref = { this . refRendered } >
{ this . state . currentPath . split ( "/" ) . filter ( e = > ! ! e ) . map ( ( e , index , arr ) = > [
< ArrowRight key = { "arrow-right-" + index + "-" + e } / > ,
< NavigationEntry key = { "de-" + index + "-" + e }
path = { "/" + arr . slice ( 0 , index + 1 ) . join ( "/" ) + "/" }
name = { e } events = { this . props . events } / >
] ) }
< / div >
}
editable = { this . state . state === "normal" }
onFocus = { event = > ! event . defaultPrevented && this . onRenderedPathClicked ( ) }
className = { customClasses ? . navigation ? . boxedInput }
/ >
) }
< / CustomClassContext.Consumer >
2020-06-10 18:13:56 +02:00
) ;
}
2020-12-18 19:18:01 +01:00
return (
< div className = { cssStyle . navigation } >
{ input }
< / div >
) ;
2020-06-10 18:13:56 +02:00
}
componentDidUpdate ( prevProps : Readonly < NavigationBarProperties > , prevState : Readonly < NavigationBarState > , snapshot? : any ) : void {
setTimeout ( ( ) = > {
2021-03-23 15:31:59 +01:00
if ( this . refRendered . current ) {
2020-06-10 18:13:56 +02:00
this . refRendered . current . scrollLeft = 999999 ;
2021-03-23 15:31:59 +01:00
}
2020-06-10 18:13:56 +02:00
} , 10 ) ;
}
private onPathClicked ( event : React.MouseEvent , index : number ) {
let path ;
2021-03-23 15:31:59 +01:00
if ( index === - 1 ) {
2020-06-10 18:13:56 +02:00
path = "/" ;
2021-03-23 15:31:59 +01:00
} else {
2020-06-10 18:13:56 +02:00
path = "/" + this . state . currentPath . split ( "/" ) . filter ( e = > ! ! e ) . slice ( 0 , index + 1 ) . join ( "/" ) + "/" ;
2021-03-23 15:31:59 +01:00
}
2020-09-12 15:49:20 +02:00
this . props . events . fire ( "action_navigate_to" , { path : path } ) ;
2020-06-10 18:13:56 +02:00
event . stopPropagation ( ) ;
}
private onRenderedPathClicked() {
2020-12-18 19:18:01 +01:00
if ( this . state . state !== "normal" ) {
2020-06-10 18:13:56 +02:00
return ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
this . setState ( {
state : "editing"
} , ( ) = > this . refInput . current ? . focusInput ( ) ) ;
}
private onInputPathBluer() {
2021-03-23 15:31:59 +01:00
if ( this . state . state !== "editing" || this . ignoreBlur ) {
2020-06-10 18:13:56 +02:00
return ;
2021-03-23 15:31:59 +01:00
}
2020-06-10 18:13:56 +02:00
this . setState ( {
state : "normal"
} ) ;
}
private onPathEntered ( newPath : string ) {
2020-12-18 19:18:01 +01:00
if ( newPath === this . state . currentPath ) {
2020-06-10 18:13:56 +02:00
return ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
this . ignoreBlur = true ;
2020-09-12 15:49:20 +02:00
this . props . events . fire ( "action_navigate_to" , { path : newPath } ) ;
2020-06-10 18:13:56 +02:00
this . setState ( {
currentPath : newPath
} ) ;
}
private onButtonRefreshClicked() {
2020-09-12 15:49:20 +02:00
if ( this . state . state !== "normal" )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
this . props . events . fire ( "action_navigate_to" , { path : this.state.currentPath } ) ;
2020-06-10 18:13:56 +02:00
}
@EventHandler < FileBrowserEvents > ( "action_navigate_to" )
private handleNavigateBegin() {
this . setState ( {
state : "navigating"
} , ( ) = > this . ignoreBlur = false ) ;
}
2020-12-18 19:18:01 +01:00
@EventHandler < FileBrowserEvents > ( "notify_current_path" )
private handleCurrentPath ( event : FileBrowserEvents [ "notify_current_path" ] ) {
if ( event . status === "success" ) {
2020-06-10 18:13:56 +02:00
this . lastSucceededPath = event . path ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
this . setState ( {
state : "normal" ,
currentPath : this.lastSucceededPath
} ) ;
2020-09-12 15:49:20 +02:00
if ( event . status !== "success" ) {
if ( event . status === "timeout" ) {
2021-04-24 13:59:49 +02:00
createErrorModal ( tr ( "Failed to enter path" ) , tra ( "Failed to enter given path.\nAction resulted in a timeout." ) ) . open ( ) ;
2020-06-10 18:13:56 +02:00
} else {
2021-04-24 13:59:49 +02:00
createErrorModal ( tr ( "Failed to enter path" ) , tra ( "Failed to enter given path:\n{0}" , event . error ) ) . open ( ) ;
2020-06-10 18:13:56 +02:00
}
}
}
}
interface FileListTableProperties {
2020-12-18 19:18:01 +01:00
initialPath : string ;
2020-06-10 18:13:56 +02:00
events : Registry < FileBrowserEvents > ;
}
interface FileListTableState {
state : "querying" | "normal" | "error" | "query-timeout" | "no-permissions" | "invalid-password" ;
errorMessage? : string ;
}
2020-12-18 19:18:01 +01:00
const FileName = ( props : { path : string , file : ListedFileInfo } ) = > {
const events = useContext ( EventsContext ) ;
2020-06-10 18:13:56 +02:00
const [ editing , setEditing ] = useState ( props . file . mode === "create" ) ;
const [ fileName , setFileName ] = useState ( props . file . name ) ;
const refInput = useRef < HTMLInputElement > ( ) ;
let icon ;
2020-09-12 15:49:20 +02:00
if ( props . file . type === FileType . FILE ) {
icon = < img key = { "nicon" } src = { "img/icon_file_text.svg" } alt = { tr ( "File" ) } draggable = { false } / > ;
2020-06-10 18:13:56 +02:00
} else {
switch ( props . file . mode ) {
case "normal" :
2020-09-12 15:49:20 +02:00
icon = < img key = { "nicon" } src = { "img/icon_folder.svg" } alt = { tr ( "Directory icon" ) } title = { tr ( "Directory" ) }
draggable = { false } / > ;
2020-06-10 18:13:56 +02:00
break ;
case "create" :
case "creating" :
case "empty" :
2020-09-12 15:49:20 +02:00
icon = < img key = { "nicon" } src = { "img/icon_folder_empty.svg" } alt = { tr ( "Empty directory icon" ) }
title = { tr ( "Empty directory" ) } draggable = { false } / > ;
2020-06-10 18:13:56 +02:00
break ;
case "password" :
2020-09-12 15:49:20 +02:00
icon = < img key = { "nicon" } src = { "img/icon_folder_password.svg" } alt = { tr ( "Directory password protected" ) }
title = { tr ( "Password protected directory" ) } draggable = { false } / > ;
2020-06-10 18:13:56 +02:00
break ;
default :
throw tr ( "Invalid directory state" ) ;
}
}
let name ;
2020-09-12 15:49:20 +02:00
if ( editing && props . file . mode !== "creating" && props . file . mode !== "uploading" ) {
2020-06-10 18:13:56 +02:00
name = < input
ref = { refInput }
defaultValue = { fileName }
onBlur = { event = > {
let name = event . target . value ;
setEditing ( false ) ;
2020-09-12 15:49:20 +02:00
if ( props . file . mode === "create" ) {
2020-06-10 18:13:56 +02:00
name = name || props . file . name ;
2020-12-18 19:18:01 +01:00
events . fire ( "action_create_directory" , {
2020-06-10 18:13:56 +02:00
path : props.path ,
name : name
} ) ;
setFileName ( name ) ;
props . file . name = name ;
props . file . mode = "creating" ;
} else {
2020-09-12 15:49:20 +02:00
if ( name . length > 0 && name !== props . file . name ) {
2020-12-18 19:18:01 +01:00
events . fire ( "action_rename_file" , {
2020-09-12 15:49:20 +02:00
oldName : props.file.name ,
newName : name ,
oldPath : props.path ,
newPath : props.path
} ) ;
2020-06-10 18:13:56 +02:00
setFileName ( name ) ;
}
}
} }
onKeyPress = { event = > {
2020-09-12 15:49:20 +02:00
if ( event . key === "Enter" ) {
2020-06-10 18:13:56 +02:00
event . currentTarget . blur ( ) ;
return ;
2020-09-12 15:49:20 +02:00
} else if ( event . key === "/" ) {
2020-06-10 18:13:56 +02:00
event . preventDefault ( ) ;
return ;
}
} }
onPaste = { event = > {
const input = event . currentTarget ;
setTimeout ( ( ) = > {
input . value = input . value . replace ( "/" , "" ) ;
} ) ;
} }
draggable = { false }
/ > ;
} else {
name = < a key = { "name" } onDoubleClick = { event = > {
2020-09-12 15:49:20 +02:00
if ( props . file . virtual || props . file . mode === "creating" || props . file . mode === "uploading" )
2020-06-10 18:13:56 +02:00
return ;
2021-03-18 18:25:20 +01:00
if ( ! getKeyBoard ( ) . isKeyPressed ( SpecialKey . SHIFT ) )
2020-06-10 18:13:56 +02:00
return ;
event . stopPropagation ( ) ;
2020-12-18 19:18:01 +01:00
events . fire ( "action_select_files" , {
2020-09-12 15:49:20 +02:00
mode : "exclusive" ,
files : [ { name : props.file.name , type : props . file . type } ]
} ) ;
2020-12-18 19:18:01 +01:00
events . fire ( "action_start_rename" , {
2020-06-10 18:13:56 +02:00
path : props.path ,
name : props.file.name
} ) ;
} } > { fileName } < / a > ;
}
2020-12-18 19:18:01 +01:00
events . reactUse ( "action_start_rename" , event = > setEditing ( event . name === props . file . name && event . path === props . path ) ) ;
events . reactUse ( "action_rename_file_result" , event = > {
2020-09-12 15:49:20 +02:00
if ( event . oldPath !== props . path || event . oldName !== props . file . name )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
if ( event . status === "no-changes" )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
if ( event . status === "success" ) {
2020-06-10 18:13:56 +02:00
props . file . name = event . newName ;
} else {
setFileName ( props . file . name ) ;
2020-09-12 15:49:20 +02:00
if ( event . status === "timeout" ) {
2021-04-24 13:59:49 +02:00
createErrorModal ( tr ( "Failed to rename file" ) , tra ( "Failed to rename file.\nAction resulted in a timeout." ) ) . open ( ) ;
2020-06-10 18:13:56 +02:00
} else {
2021-04-24 13:59:49 +02:00
createErrorModal ( tr ( "Failed to rename file" ) , tra ( "Failed to rename file:\n{0}" , event . error ) ) . open ( ) ;
2020-06-10 18:13:56 +02:00
}
}
} ) ;
useEffect ( ( ) = > {
refInput . current ? . focus ( ) ;
} ) ;
return < > { icon } { name } < / > ;
} ;
2020-12-18 19:18:01 +01:00
const FileSize = ( props : { path : string , file : ListedFileInfo } ) = > {
const events = useContext ( EventsContext ) ;
2020-09-12 15:49:20 +02:00
const [ size , setSize ] = useState ( - 1 ) ;
2020-06-10 18:13:56 +02:00
2020-12-18 19:18:01 +01:00
events . reactUse ( "notify_transfer_status" , event = > {
2020-09-12 15:49:20 +02:00
if ( event . id !== props . file . transfer ? . id )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
if ( props . file . transfer ? . direction !== "upload" )
2020-06-10 18:13:56 +02:00
return ;
switch ( event . status ) {
case "pending" :
setSize ( 0 ) ;
break ;
case "finished" :
case "none" :
setSize ( - 1 ) ;
break ;
}
} ) ;
2020-12-18 19:18:01 +01:00
events . reactUse ( "notify_transfer_progress" , event = > {
2020-09-12 15:49:20 +02:00
if ( event . id !== props . file . transfer ? . id )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
if ( props . file . transfer ? . direction !== "upload" )
2020-06-10 18:13:56 +02:00
return ;
setSize ( event . fileSize ) ;
} ) ;
2020-12-18 19:18:01 +01:00
if ( size < 0 && ( props . file . size < 0 || typeof props . file . size === "undefined" ) ) {
return (
< a key = { "size-invalid" } >
< Translatable > unknown < / Translatable >
< / a >
) ;
}
return (
< a key = { "size" } >
{ network . format_bytes ( size >= 0 ? size : props.file.size , {
unit : "B" ,
time : "" ,
exact : false
} ) }
< / a >
) ;
2020-06-10 18:13:56 +02:00
} ;
const FileTransferIndicator = ( props : { file : ListedFileInfo , events : Registry < FileBrowserEvents > } ) = > {
const [ transferStatus , setTransferStatus ] = useState < TransferStatus > ( props . file . transfer ? . status || "none" ) ;
const [ transferProgress , setTransferProgress ] = useState ( props . file . transfer ? . percent | 0 ) ;
props . events . reactUse ( "notify_transfer_start" , event = > {
2020-09-12 15:49:20 +02:00
if ( event . path !== props . file . path || event . name !== props . file . name )
2020-06-10 18:13:56 +02:00
return ;
setTransferStatus ( "pending" ) ;
} ) ;
props . events . reactUse ( "notify_transfer_status" , event = > {
2020-09-12 15:49:20 +02:00
if ( event . id !== props . file . transfer ? . id )
2020-06-10 18:13:56 +02:00
return ;
setTransferStatus ( event . status ) ;
2020-09-12 15:49:20 +02:00
if ( event . status === "finished" || event . status === "errored" )
2020-06-10 18:13:56 +02:00
setTransferProgress ( 100 ) ;
} ) ;
props . events . reactUse ( "notify_transfer_progress" , event = > {
2020-09-12 15:49:20 +02:00
if ( event . id !== props . file . transfer ? . id )
2020-06-10 18:13:56 +02:00
return ;
setTransferProgress ( event . progress ) ;
setTransferStatus ( event . status ) ;
} ) ;
/* reset the status after two seconds */
useEffect ( ( ) = > {
2020-09-12 15:49:20 +02:00
if ( transferStatus !== "finished" && transferStatus !== "errored" )
2020-06-10 18:13:56 +02:00
return ;
const id = setTimeout ( ( ) = > {
setTransferStatus ( "none" ) ;
} , 3 * 1000 ) ;
return ( ) = > clearTimeout ( id ) ;
} ) ;
2020-09-12 15:49:20 +02:00
if ( ! props . file . transfer )
2020-06-10 18:13:56 +02:00
return null ;
let color ;
switch ( transferStatus ) {
case "pending" :
case "transferring" :
color = cssStyle . blue ;
break ;
case "errored" :
color = cssStyle . red ;
break ;
case "finished" :
color = cssStyle . green ;
break ;
case "none" :
color = cssStyle . hidden ;
break ;
}
2020-12-18 19:18:01 +01:00
2020-06-10 18:13:56 +02:00
return (
2020-09-12 15:49:20 +02:00
< div className = { cssStyle . indicator + " " + color } style = { { right : ( ( 1 - transferProgress ) * 100 ) + "%" } } >
< div className = { cssStyle . status } / >
2020-06-10 18:13:56 +02:00
< / div >
) ;
} ;
const FileListEntry = ( props : { row : TableRow < ListedFileInfo > , columns : TableColumn [ ] , events : Registry < FileBrowserEvents > } ) = > {
const file = props . row . userData ;
const [ hidden , setHidden ] = useState ( false ) ;
const [ selected , setSelected ] = useState ( false ) ;
const [ dropHovered , setDropHovered ] = useState ( false ) ;
2020-12-18 19:18:01 +01:00
const customClasses = useContext ( CustomClassContext ) ;
2020-06-10 18:13:56 +02:00
const onDoubleClicked = ( ) = > {
2020-09-12 15:49:20 +02:00
if ( file . type === FileType . DIRECTORY ) {
2020-12-18 19:18:01 +01:00
if ( file . mode === "creating" || file . mode === "create" ) {
2020-06-10 18:13:56 +02:00
return ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
props . events . fire ( "action_navigate_to" , {
path : file.path + file . name + "/"
} ) ;
} else {
2020-12-18 19:18:01 +01:00
if ( file . mode === "uploading" || file . virtual ) {
2020-06-10 18:13:56 +02:00
return ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
props . events . fire ( "action_start_download" , {
files : [ {
path : file.path ,
name : file.name
} ]
} ) ;
}
} ;
props . events . reactUse ( "action_select_files" , event = > {
const contains = event . files . findIndex ( e = > e . name === file . name && e . type === file . type ) !== - 1 ;
2020-09-12 15:49:20 +02:00
if ( event . mode === "toggle" && contains )
2020-06-10 18:13:56 +02:00
setSelected ( ! selected ) ;
2020-09-12 15:49:20 +02:00
else if ( event . mode === "exclusive" ) {
2020-06-10 18:13:56 +02:00
setSelected ( contains ) ;
}
} ) ;
props . events . reactUse ( "notify_drag_ended" , ( ) = > setDropHovered ( false ) , dropHovered ) ;
props . events . reactUse ( "action_delete_file_result" , event = > {
event . results . forEach ( e = > {
2020-09-12 15:49:20 +02:00
if ( e . status !== "success" )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
if ( e . path !== file . path || e . name !== file . name )
2020-06-10 18:13:56 +02:00
return ;
setHidden ( true ) ;
} ) ;
} , ! hidden ) ;
2020-12-18 19:18:01 +01:00
if ( hidden ) {
2020-06-10 18:13:56 +02:00
return null ;
2020-12-18 19:18:01 +01:00
}
const elementClassList = joinClassList (
cssStyle . directoryEntry , customClasses ? . fileEntry ? . entry ,
selected && cssStyle . selected , selected && customClasses ? . fileEntry ? . selected ,
dropHovered && cssStyle . hovered , dropHovered && customClasses ? . fileEntry ? . dropHovered
) ;
2020-06-10 18:13:56 +02:00
return (
< TableRowElement
2020-12-18 19:18:01 +01:00
className = { elementClassList }
2020-06-10 18:13:56 +02:00
rowData = { props . row }
columns = { props . columns }
onDoubleClick = { onDoubleClicked }
2020-09-12 15:49:20 +02:00
onClick = { ( ) = > props . events . fire ( "action_select_files" , {
files : [ { name : file.name , type : file . type } ] ,
2021-03-18 18:25:20 +01:00
mode : getKeyBoard ( ) . isKeyPressed ( SpecialKey . SHIFT ) ? "toggle" : "exclusive"
2020-09-12 15:49:20 +02:00
} ) }
2020-06-10 18:13:56 +02:00
onContextMenu = { e = > {
2020-09-12 15:49:20 +02:00
if ( ! selected ) {
if ( ! ( e . target instanceof HTMLDivElement ) ) {
2020-06-10 18:13:56 +02:00
/* explicitly clicked on one file */
2020-09-12 15:49:20 +02:00
props . events . fire ( "action_select_files" , {
files : [ { name : file.name , type : file . type } ] ,
2021-03-18 18:25:20 +01:00
mode : getKeyBoard ( ) . isKeyPressed ( SpecialKey . SHIFT ) ? "toggle" : "exclusive"
2020-09-12 15:49:20 +02:00
} ) ;
2020-06-10 18:13:56 +02:00
} else {
2020-09-12 15:49:20 +02:00
props . events . fire ( "action_select_files" , { files : [ ] , mode : "exclusive" } ) ;
2020-06-10 18:13:56 +02:00
}
}
props . events . fire ( "action_selection_context_menu" , {
pageX : e.pageX ,
pageY : e.pageY
} ) ;
2020-12-03 12:09:36 +01:00
e . stopPropagation ( ) ;
2020-06-10 18:13:56 +02:00
} }
draggable = { ! props . row . userData . virtual }
onDragStart = { event = > {
2020-09-12 15:49:20 +02:00
if ( ! selected ) {
2020-06-10 18:13:56 +02:00
setSelected ( true ) ;
2020-09-12 15:49:20 +02:00
props . events . fire ( "action_select_files" , {
files : [ { name : file.name , type : file . type } ] ,
mode : "exclusive"
} ) ;
2020-06-10 18:13:56 +02:00
}
2020-09-12 15:49:20 +02:00
props . events . fire ( "notify_drag_started" , { event : event.nativeEvent } ) ;
2020-06-10 18:13:56 +02:00
} }
onDragOver = { event = > {
const types = event . dataTransfer . types ;
2020-09-12 15:49:20 +02:00
if ( types . length !== 1 )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
if ( props . row . userData . type !== FileType . DIRECTORY ) {
2020-06-10 18:13:56 +02:00
event . stopPropagation ( ) ;
return ;
}
2020-09-12 15:49:20 +02:00
if ( types [ 0 ] === FileTransferUrlMediaType ) {
2020-06-10 18:13:56 +02:00
/* TODO: Detect if its remote move or internal move */
event . dataTransfer . effectAllowed = "move" ;
2020-09-12 15:49:20 +02:00
} else if ( types [ 0 ] === "Files" ) {
2020-06-10 18:13:56 +02:00
event . dataTransfer . effectAllowed = "copy" ;
} else {
return ;
}
event . preventDefault ( ) ;
setDropHovered ( true ) ;
} }
onDragLeave = { ( ) = > setDropHovered ( false ) }
onDragEnd = { ( ) = > props . events . fire ( "notify_drag_ended" ) }
2020-06-10 22:44:50 +02:00
x - drag - upload - path = { props . row . userData . type === FileType . DIRECTORY ? props . row . userData . path + props . row . userData . name + "/" : undefined }
2020-06-10 18:13:56 +02:00
>
2020-09-12 15:49:20 +02:00
< FileTransferIndicator events = { props . events } file = { props . row . userData } / >
2020-06-10 18:13:56 +02:00
< / TableRowElement >
) ;
} ;
2020-12-18 19:18:01 +01:00
type FileListState = {
state : "querying" | "invalid-password"
} | {
state : "no-permissions" ,
failedPermission : string
} | {
state : "error" ,
reason : string
} | {
state : "normal" ,
files : ListedFileInfo [ ]
} ;
function fileTableHeaderContextMenu ( event : React.MouseEvent , table : Table | undefined ) {
event . preventDefault ( ) ;
if ( ! table ) {
return ;
}
spawn_context_menu ( event . pageX , event . pageY , {
type : MenuEntryType . CHECKBOX ,
name : tr ( "Size" ) ,
checkbox_checked : table.state.hiddenColumns.findIndex ( e = > e === "size" ) === - 1 ,
callback : ( ) = > {
table . state . hiddenColumns . toggle ( "size" ) ;
table . forceUpdate ( ) ;
}
} , {
type : MenuEntryType . CHECKBOX ,
name : tr ( "Type" ) ,
checkbox_checked : table.state.hiddenColumns.findIndex ( e = > e === "type" ) === - 1 ,
callback : ( ) = > {
table . state . hiddenColumns . toggle ( "type" ) ;
table . forceUpdate ( ) ;
}
} , {
type : MenuEntryType . CHECKBOX ,
name : tr ( "Last changed" ) ,
checkbox_checked : table.state.hiddenColumns.findIndex ( e = > e === "change-date" ) === - 1 ,
callback : ( ) = > {
table . state . hiddenColumns . toggle ( "change-date" ) ;
table . forceUpdate ( ) ;
}
} )
}
// const FileListRenderer = React.memo((props: { path: string }) => {
// const events = useContext(EventsContext);
// const customClasses = useContext(CustomClassContext);
//
// const refTable = useRef<Table>();
//
// const [ state, setState ] = useState<FileListState>(() => {
// events.fire("query_files", { path: props.path });
// return { state: "querying" };
// });
//
// events.reactUse("query_files", event => {
// if(event.path === props.path) {
// setState({ state: "querying" });
// }
// });
//
// events.reactUse("query_files_result", event => {
// if(event.path !== props.path) {
// return;
// }
//
// switch(event.status) {
// case "no-permissions":
// setState({ state: "no-permissions", failedPermission: event.error });
// break;
//
// case "error":
// setState({ state: "error", reason: event.error });
// break;
//
// case "success":
// setState({ state: "normal", files: event.files });
// break;
//
// case "invalid-password":
// setState({ state: "invalid-password" });
// break;
//
// case "timeout":
// setState({ state: "error", reason: tr("query timeout") });
// break;
//
// default:
// setState({ state: "error", reason: tra("invalid query result state {}", event.status) });
// break;
// }
// });
//
// let rows: TableRow[] = [];
// let overlay;
//
// switch (state.state) {
// case "querying":
// overlay = () => (
// <div key={"loading"} className={cssStyle.overlay}>
// <a><Translatable>loading</Translatable><LoadingDots maxDots={3}/></a>
// </div>
// );
// break;
//
// case "error":
// overlay = () => (
// <div key={"query-error"} className={cssStyle.overlay + " " + cssStyle.overlayError}>
// <a><Translatable>Failed to query directory:</Translatable><br/>{state.reason}</a>
// </div>
// );
// break;
//
// case "no-permissions":
// overlay = () => (
// <div key={"no-permissions"} className={cssStyle.overlay + " " + cssStyle.overlayError}>
// <a><Translatable>Directory query failed on permission</Translatable><br/>{state.failedPermission}
// </a>
// </div>
// );
// break;
//
// case "invalid-password":
// /* TODO: Allow the user to enter a password */
// overlay = () => (
// <div key={"invalid-password"} className={cssStyle.overlay + " " + cssStyle.overlayError}>
// <a><Translatable>Directory query failed because it is password protected</Translatable></a>
// </div>
// );
// break;
//
// case "normal":
// if(state.files.length === 0) {
// overlay = () => (
// <div key={"no-files"} className={cssStyle.overlayEmptyFolder}>
// <a><Translatable>This folder is empty.</Translatable></a>
// </div>
// );
// } else {
// const directories = state.files.filter(e => e.type === FileType.DIRECTORY);
// const files = state.files.filter(e => e.type === FileType.FILE);
//
// for (const directory of directories.sort((a, b) => a.name > b.name ? 1 : -1)) {
// rows.push({
// columns: {
// "name": () => <FileName path={props.path} file={directory}/>,
// "type": () => <a key={"type"}><Translatable>Directory</Translatable></a>,
// "change-date": () => directory.datetime ?
// <a>{Moment(directory.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
// },
// className: cssStyle.directoryEntry,
// userData: directory
// });
// }
//
// for (const file of files.sort((a, b) => a.name > b.name ? 1 : -1)) {
// rows.push({
// columns: {
// "name": () => <FileName path={props.path} file={file}/>,
// "size": () => <FileSize path={props.path} file={file}/>,
// "type": () => <a key={"type"}><Translatable>File</Translatable></a>,
// "change-date": () => file.datetime ?
// <a key={"date"}>{Moment(file.datetime).format("DD/MM/YYYY HH:mm")}</a> :
// undefined
// },
// className: cssStyle.directoryEntry,
// userData: file
// });
// }
// }
// break;
// }
//
// return (
// <Table
// ref={refTable}
// className={joinClassList(cssStyle.fileTable, customClasses?.fileTable?.table)}
// bodyClassName={joinClassList(cssStyle.body, customClasses?.fileTable?.body)}
// headerClassName={joinClassList(cssStyle.header, customClasses?.fileTable?.header)}
// columns={[
// {
// name: "name", header: () => [
// <a key={"name-name"}><Translatable>Name</Translatable></a>,
// <div key={"seperator-name"} className={cssStyle.separator}/>
// ], width: 80, className: cssStyle.columnName
// },
// {
// name: "type", header: () => [
// <a key={"name-type"}><Translatable>Type</Translatable></a>,
// <div key={"seperator-type"} className={cssStyle.separator}/>
// ], fixedWidth: "8em", className: cssStyle.columnType
// },
// {
// name: "size", header: () => [
// <a key={"name-size"}><Translatable>Size</Translatable></a>,
// <div key={"seperator-size"} className={cssStyle.separator}/>
// ], fixedWidth: "8em", className: cssStyle.columnSize
// },
// {
// name: "change-date", header: () => [
// <a key={"name-date"}><Translatable>Last changed</Translatable></a>,
// <div key={"seperator-date"} className={cssStyle.separator}/>
// ], fixedWidth: "8em", className: cssStyle.columnChanged
// },
// ]}
// rows={rows}
//
// bodyOverlayOnly={rows.length === 0}
// bodyOverlay={overlay}
//
// hiddenColumns={["type"]}
//
// onHeaderContextMenu={e => fileTableHeaderContextMenu(e, refTable.current)}
// onBodyContextMenu={event => {
// event.preventDefault();
// events.fire("action_select_files", { mode: "exclusive", files: [] });
// events.fire("action_selection_context_menu", { pageY: event.pageY, pageX: event.pageX });
// }}
// onDrop={e => this.onDrop(e)}
// onDragOver={event => {
// const types = event.dataTransfer.types;
// if (types.length !== 1)
// return;
//
// if (types[0] === FileTransferUrlMediaType) {
// /* TODO: Detect if its remote move or internal move */
// event.dataTransfer.effectAllowed = "move";
// } else if (types[0] === "Files") {
// event.dataTransfer.effectAllowed = "copy";
// } else {
// return;
// }
//
// event.preventDefault();
// }}
//
// renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => (
// <FileListEntry columns={columns}
// row={row} key={uniqueId}
// events={this.props.events}/>
// )}
// />
// );
// });
2020-06-10 18:13:56 +02:00
@ReactEventHandler ( e = > e . props . events )
2020-12-18 19:18:01 +01:00
export class FileBrowserRenderer extends ReactComponentBase < FileListTableProperties , FileListTableState > {
2020-06-10 18:13:56 +02:00
private refTable = React . createRef < Table > ( ) ;
private currentPath : string ;
private fileList : ListedFileInfo [ ] ;
private selection : { name : string , type : FileType } [ ] = [ ] ;
protected defaultState ( ) : FileListTableState {
return {
state : "querying"
} ;
}
render() {
let rows : TableRow [ ] = [ ] ;
let overlay , overlayOnly ;
2020-09-12 15:49:20 +02:00
if ( this . state . state === "querying" ) {
2020-06-10 18:13:56 +02:00
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "loading" } className = { cssStyle . overlay } >
2020-09-12 15:49:20 +02:00
< a > < Translatable > loading < / Translatable > < LoadingDots maxDots = { 3 } / > < / a >
2020-06-10 18:13:56 +02:00
< / div >
) ;
2020-09-12 15:49:20 +02:00
} else if ( this . state . state === "error" ) {
2020-06-10 18:13:56 +02:00
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "query-error" } className = { cssStyle . overlay + " " + cssStyle . overlayError } >
2020-09-12 15:49:20 +02:00
< a > < Translatable > Failed to query directory : < / Translatable > < br / > { this . state . errorMessage } < / a >
2020-06-10 18:13:56 +02:00
< / div >
) ;
2020-09-12 15:49:20 +02:00
} else if ( this . state . state === "query-timeout" ) {
2020-06-10 18:13:56 +02:00
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "query-timeout" } className = { cssStyle . overlay + " " + cssStyle . overlayError } >
2020-09-12 15:49:20 +02:00
< a > < Translatable > Directory query timed out . < / Translatable > < br / > < Translatable > Please try
again . < / Translatable > < / a >
2020-06-10 18:13:56 +02:00
< / div >
) ;
2020-09-12 15:49:20 +02:00
} else if ( this . state . state === "no-permissions" ) {
2020-06-10 18:13:56 +02:00
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "no-permissions" } className = { cssStyle . overlay + " " + cssStyle . overlayError } >
2020-09-12 15:49:20 +02:00
< a > < Translatable > Directory query failed on permission < / Translatable > < br / > { this . state . errorMessage }
< / a >
2020-06-10 18:13:56 +02:00
< / div >
) ;
2020-09-12 15:49:20 +02:00
} else if ( this . state . state === "invalid-password" ) {
2020-06-10 18:13:56 +02:00
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "invalid-password" } className = { cssStyle . overlay + " " + cssStyle . overlayError } >
< a > < Translatable > Directory query failed because it is password protected < / Translatable > < / a >
< / div >
) ;
2020-09-12 15:49:20 +02:00
} else if ( this . state . state === "normal" ) {
if ( this . fileList . length === 0 ) {
2020-06-10 18:13:56 +02:00
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "no-files" } className = { cssStyle . overlayEmptyFolder } >
< a > < Translatable > This folder is empty . < / Translatable > < / a >
< / div >
) ;
} else {
const directories = this . fileList . filter ( e = > e . type === FileType . DIRECTORY ) ;
const files = this . fileList . filter ( e = > e . type === FileType . FILE ) ;
2020-09-12 15:49:20 +02:00
for ( const directory of directories . sort ( ( a , b ) = > a . name > b . name ? 1 : - 1 ) ) {
2020-06-10 18:13:56 +02:00
rows . push ( {
columns : {
2020-12-18 19:18:01 +01:00
"name" : ( ) = > < FileName path = { this . currentPath } file = { directory } / > ,
2020-06-10 18:13:56 +02:00
"type" : ( ) = > < a key = { "type" } > < Translatable > Directory < / Translatable > < / a > ,
2020-09-12 15:49:20 +02:00
"change-date" : ( ) = > directory . datetime ?
2021-03-14 19:39:08 +01:00
< a > { moment ( directory . datetime ) . format ( "DD/MM/YYYY HH:mm" ) } < / a > : undefined
2020-06-10 18:13:56 +02:00
} ,
className : cssStyle.directoryEntry ,
userData : directory
} )
}
2020-09-12 15:49:20 +02:00
for ( const file of files . sort ( ( a , b ) = > a . name > b . name ? 1 : - 1 ) ) {
2020-06-10 18:13:56 +02:00
rows . push ( {
columns : {
2020-12-18 19:18:01 +01:00
"name" : ( ) = > < FileName path = { this . currentPath } file = { file } / > ,
"size" : ( ) = > < FileSize path = { this . currentPath } file = { file } / > ,
2020-06-10 18:13:56 +02:00
"type" : ( ) = > < a key = { "type" } > < Translatable > File < / Translatable > < / a > ,
2020-09-12 15:49:20 +02:00
"change-date" : ( ) = > file . datetime ?
2021-03-14 19:39:08 +01:00
< a key = { "date" } > { moment ( file . datetime ) . format ( "DD/MM/YYYY HH:mm" ) } < / a > : undefined
2020-06-10 18:13:56 +02:00
} ,
className : cssStyle.directoryEntry ,
userData : file
} )
}
}
}
return (
2020-12-18 19:18:01 +01:00
< EventsContext.Provider value = { this . props . events } >
< CustomClassContext.Consumer >
{ classes = > (
< Table
ref = { this . refTable }
className = { this . classList ( cssStyle . fileTable , classes ? . fileTable ? . table ) }
bodyClassName = { this . classList ( cssStyle . body , classes ? . fileTable ? . body ) }
headerClassName = { this . classList ( cssStyle . header , classes ? . fileTable ? . header ) }
columns = { [
{
name : "name" , header : ( ) = > [
< a key = { "name-name" } > < Translatable > Name < / Translatable > < / a > ,
< div key = { "seperator-name" } className = { cssStyle . separator } / >
] , width : 80 , className : cssStyle.columnName
} ,
{
name : "type" , header : ( ) = > [
< a key = { "name-type" } > < Translatable > Type < / Translatable > < / a > ,
< div key = { "seperator-type" } className = { cssStyle . separator } / >
] , fixedWidth : "8em" , className : cssStyle.columnType
} ,
{
name : "size" , header : ( ) = > [
< a key = { "name-size" } > < Translatable > Size < / Translatable > < / a > ,
< div key = { "seperator-size" } className = { cssStyle . separator } / >
] , fixedWidth : "8em" , className : cssStyle.columnSize
} ,
{
name : "change-date" , header : ( ) = > [
< a key = { "name-date" } > < Translatable > Last changed < / Translatable > < / a > ,
< div key = { "seperator-date" } className = { cssStyle . separator } / >
] , fixedWidth : "8em" , className : cssStyle.columnChanged
} ,
] }
rows = { rows }
bodyOverlayOnly = { overlayOnly }
bodyOverlay = { overlay }
hiddenColumns = { [ "type" ] }
onHeaderContextMenu = { e = > this . onHeaderContextMenu ( e ) }
onBodyContextMenu = { e = > this . onBodyContextMenu ( e ) }
onDrop = { e = > this . onDrop ( e ) }
onDragOver = { event = > {
const types = event . dataTransfer . types ;
if ( types . length !== 1 )
return ;
if ( types [ 0 ] === FileTransferUrlMediaType ) {
/* TODO: Detect if its remote move or internal move */
event . dataTransfer . effectAllowed = "move" ;
} else if ( types [ 0 ] === "Files" ) {
event . dataTransfer . effectAllowed = "copy" ;
} else {
return ;
}
event . preventDefault ( ) ;
} }
renderRow = { ( row : TableRow < ListedFileInfo > , columns , uniqueId ) = > (
< FileListEntry columns = { columns }
row = { row } key = { uniqueId }
events = { this . props . events } / >
) }
/ >
) }
< / CustomClassContext.Consumer >
< / EventsContext.Provider >
2020-06-10 18:13:56 +02:00
) ;
}
componentDidMount ( ) : void {
this . selection = [ ] ;
2020-12-18 19:18:01 +01:00
this . currentPath = this . props . initialPath ;
this . props . events . fire ( "query_current_path" , { } ) ;
2020-06-10 18:13:56 +02:00
}
2020-06-10 22:44:50 +02:00
private onDrop ( event : React.DragEvent ) {
const types = event . dataTransfer . types ;
2020-12-18 19:18:01 +01:00
if ( types . length !== 1 ) {
2020-06-10 22:44:50 +02:00
return ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 22:44:50 +02:00
event . stopPropagation ( ) ;
let targetPath ;
{
let currentTarget = event . target as HTMLElement ;
2020-12-18 19:18:01 +01:00
while ( currentTarget && ! currentTarget . hasAttribute ( "x-drag-upload-path" ) ) {
2020-06-10 22:44:50 +02:00
currentTarget = currentTarget . parentElement ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 22:44:50 +02:00
targetPath = currentTarget ? . getAttribute ( "x-drag-upload-path" ) || this . currentPath ;
}
2020-09-12 15:49:20 +02:00
if ( types [ 0 ] === FileTransferUrlMediaType ) {
2020-12-18 19:18:01 +01:00
/* TODO: Test if we moved cross some boundaries */
2020-06-10 22:44:50 +02:00
const fileUrls = event . dataTransfer . getData ( FileTransferUrlMediaType ) . split ( "&" ) . map ( e = > decodeURIComponent ( e ) ) ;
2020-09-12 15:49:20 +02:00
for ( const fileUrl of fileUrls ) {
2020-06-10 22:44:50 +02:00
const name = fileUrl . split ( "/" ) . last ( ) ;
const oldPath = fileUrl . split ( "/" ) . slice ( 0 , - 1 ) . join ( "/" ) + "/" ;
this . props . events . fire ( "action_rename_file" , {
newPath : targetPath ,
oldPath : oldPath ,
oldName : name ,
newName : name
} ) ;
}
2020-09-12 15:49:20 +02:00
} else if ( types [ 0 ] === "Files" ) {
this . props . events . fire ( "action_start_upload" , {
path : targetPath ,
mode : "files" ,
files : [ . . . event . dataTransfer . files ]
} ) ;
2020-06-10 22:44:50 +02:00
} else {
2021-01-10 17:36:57 +01:00
logWarn ( LogCategory . FILE_TRANSFER , tr ( "Received an unknown drop media type (%o)" ) , types ) ;
2020-06-10 22:44:50 +02:00
event . preventDefault ( ) ;
return ;
}
event . preventDefault ( ) ;
}
2020-06-10 18:13:56 +02:00
private onHeaderContextMenu ( event : React.MouseEvent ) {
event . preventDefault ( ) ;
const table = this . refTable . current ;
spawn_context_menu ( event . pageX , event . pageY , {
type : MenuEntryType . CHECKBOX ,
name : tr ( "Size" ) ,
checkbox_checked : table.state.hiddenColumns.findIndex ( e = > e === "size" ) === - 1 ,
callback : ( ) = > {
table . state . hiddenColumns . toggle ( "size" ) ;
table . forceUpdate ( ) ;
}
} , {
type : MenuEntryType . CHECKBOX ,
name : tr ( "Type" ) ,
checkbox_checked : table.state.hiddenColumns.findIndex ( e = > e === "type" ) === - 1 ,
callback : ( ) = > {
table . state . hiddenColumns . toggle ( "type" ) ;
table . forceUpdate ( ) ;
}
} , {
type : MenuEntryType . CHECKBOX ,
name : tr ( "Last changed" ) ,
checkbox_checked : table.state.hiddenColumns.findIndex ( e = > e === "change-date" ) === - 1 ,
callback : ( ) = > {
table . state . hiddenColumns . toggle ( "change-date" ) ;
table . forceUpdate ( ) ;
}
} )
}
private onBodyContextMenu ( event : React.MouseEvent ) {
event . preventDefault ( ) ;
2020-09-12 15:49:20 +02:00
this . props . events . fire ( "action_select_files" , { mode : "exclusive" , files : [ ] } ) ;
2020-12-18 19:18:01 +01:00
this . props . events . fire ( "action_selection_context_menu" , { pageY : event.pageY , pageX : event.pageX } ) ;
2020-06-10 18:13:56 +02:00
}
2020-12-18 19:18:01 +01:00
@EventHandler < FileBrowserEvents > ( "notify_current_path" )
private handleNavigationResult ( event : FileBrowserEvents [ "notify_current_path" ] ) {
if ( event . status !== "success" ) {
2020-06-10 18:13:56 +02:00
return ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
this . currentPath = event . path ;
this . selection = [ ] ;
this . props . events . fire ( "query_files" , {
path : event.path
} ) ;
}
@EventHandler < FileBrowserEvents > ( "query_files" )
private handleQueryFiles ( event : FileBrowserEvents [ "query_files" ] ) {
2020-09-12 15:49:20 +02:00
if ( event . path !== this . currentPath )
2020-06-10 18:13:56 +02:00
return ;
this . setState ( {
state : "querying"
} ) ;
}
@EventHandler < FileBrowserEvents > ( "query_files_result" )
private handleQueryFilesResult ( event : FileBrowserEvents [ "query_files_result" ] ) {
2020-09-12 15:49:20 +02:00
if ( event . status === "timeout" ) {
2020-06-10 18:13:56 +02:00
this . setState ( {
state : "query-timeout"
} ) ;
2020-09-12 15:49:20 +02:00
} else if ( event . status === "error" ) {
2020-06-10 18:13:56 +02:00
this . setState ( {
state : "error" ,
errorMessage : event.error || tr ( "unknown query error" )
} ) ;
2020-09-12 15:49:20 +02:00
} else if ( event . status === "success" ) {
2020-06-10 18:13:56 +02:00
this . fileList = event . files ;
this . setState ( {
state : "normal"
} ) ;
2020-09-12 15:49:20 +02:00
} else if ( event . status === "no-permissions" ) {
2020-06-10 18:13:56 +02:00
this . setState ( {
state : "no-permissions" ,
errorMessage : event.error || tr ( "unknown" )
} ) ;
2020-09-12 15:49:20 +02:00
} else if ( event . status === "invalid-password" ) {
2020-06-10 18:13:56 +02:00
this . setState ( {
state : "invalid-password"
} ) ;
} else {
this . setState ( {
state : "error" ,
errorMessage : tr ( "invalid query result state" )
} ) ;
}
}
@EventHandler < FileBrowserEvents > ( "action_delete_file_result" )
private handleActionDeleteResult ( event : FileBrowserEvents [ "action_delete_file_result" ] ) {
event . results . forEach ( e = > {
const index = this . fileList . findIndex ( e1 = > e1 . name === e . name && e1 . path === e . path ) ;
2020-09-12 15:49:20 +02:00
if ( index === - 1 )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
if ( e . status === "success" )
2020-06-10 18:13:56 +02:00
this . fileList . splice ( index , 1 ) ;
} ) ;
event . results . forEach ( e = > {
2020-09-12 15:49:20 +02:00
if ( e . status === "success" )
2020-06-10 18:13:56 +02:00
return ;
2021-04-24 13:59:49 +02:00
createErrorModal ( tr ( "Failed to delete entry" ) , tra ( "Failed to delete \"{0}\":\n{1}" , e . name , e . error || tr ( "Unknown error" ) ) ) . open ( ) ;
2020-06-10 18:13:56 +02:00
} ) ;
}
@EventHandler < FileBrowserEvents > ( "action_start_create_directory" )
private handleActionFileCreateBegin ( event : FileBrowserEvents [ "action_start_create_directory" ] ) {
let index = 0 ;
2020-12-18 19:18:01 +01:00
while ( this . fileList . find ( e = > e . name === ( event . defaultName + ( index > 0 ? " (" + index + ")" : "" ) ) ) ) {
2020-06-10 18:13:56 +02:00
index ++ ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
const name = event . defaultName + ( index > 0 ? " (" + index + ")" : "" ) ;
this . fileList . push ( {
name : name ,
path : this.currentPath ,
type : FileType . DIRECTORY ,
size : 0 ,
datetime : Date.now ( ) ,
virtual : false ,
mode : "create"
} ) ;
/* fire_async because our children have to render first in order to have the row selected! */
2020-12-18 19:18:01 +01:00
this . forceUpdate ( ( ) = > {
this . props . events . fire_react ( "action_select_files" , {
files : [ {
name : name ,
type : FileType . DIRECTORY
} ] , mode : "exclusive"
} ) ;
} ) ;
2020-06-10 18:13:56 +02:00
}
@EventHandler < FileBrowserEvents > ( "action_create_directory_result" )
private handleActionFileCreateResult ( event : FileBrowserEvents [ "action_create_directory_result" ] ) {
let fileIndex = this . fileList . slice ( ) . reverse ( ) . findIndex ( e = > e . path === event . path && e . name === event . name ) ;
2020-09-12 15:49:20 +02:00
if ( fileIndex === - 1 )
2020-06-10 18:13:56 +02:00
return ;
fileIndex = this . fileList . length - fileIndex - 1 ;
const file = this . fileList [ fileIndex ] ;
2020-09-12 15:49:20 +02:00
if ( event . status === "success" ) {
if ( file . mode === "creating" )
2020-06-10 18:13:56 +02:00
file . mode = "empty" ;
return ;
2020-09-12 15:49:20 +02:00
} else if ( file . mode !== "creating" )
2020-06-10 18:13:56 +02:00
return ;
this . fileList . splice ( fileIndex , 1 ) ;
this . forceUpdate ( ) ;
2020-09-12 15:49:20 +02:00
if ( event . status === "timeout" ) {
2021-04-24 13:59:49 +02:00
createErrorModal ( tr ( "Failed to create directory" ) , tra ( "Failed to create directory.\nAction resulted in a timeout." ) ) . open ( ) ;
2020-06-10 18:13:56 +02:00
} else {
2021-04-24 13:59:49 +02:00
createErrorModal ( tr ( "Failed to create directory" ) , tra ( "Failed to create directory:\n{0}" , event . error ) ) . open ( ) ;
2020-06-10 18:13:56 +02:00
}
}
@EventHandler < FileBrowserEvents > ( "action_select_files" )
private handleActionSelectFiles ( event : FileBrowserEvents [ "action_select_files" ] ) {
2020-09-12 15:49:20 +02:00
if ( event . mode === "exclusive" ) {
2020-06-10 18:13:56 +02:00
this . selection = event . files . slice ( 0 ) ;
2020-09-12 15:49:20 +02:00
} else if ( event . mode === "toggle" ) {
2020-06-10 18:13:56 +02:00
event . files . forEach ( e = > {
const index = this . selection . map ( e = > e . name ) . findIndex ( b = > b === e . name ) ;
2020-09-12 15:49:20 +02:00
if ( index === - 1 )
2020-06-10 18:13:56 +02:00
this . selection . push ( e ) ;
else
this . selection . splice ( index ) ;
} ) ;
}
}
@EventHandler < FileBrowserEvents > ( "notify_drag_started" )
private handleNotifyDragStarted ( event : FileBrowserEvents [ "notify_drag_started" ] ) {
2020-09-12 15:49:20 +02:00
if ( this . selection . length === 0 ) {
2020-06-10 18:13:56 +02:00
event . event . preventDefault ( ) ;
return ;
} else {
const url = this . selection . map ( e = > encodeURIComponent ( this . currentPath + e . name ) ) . join ( "&" ) ;
event . event . dataTransfer . setData ( FileTransferUrlMediaType , url ) ;
}
}
@EventHandler < FileBrowserEvents > ( "action_rename_file_result" )
private handleFileRenameResult ( event : FileBrowserEvents [ "action_rename_file_result" ] ) {
2020-09-12 15:49:20 +02:00
if ( event . oldPath !== this . currentPath && event . newPath !== this . currentPath )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
if ( event . status !== "success" )
2020-06-10 18:13:56 +02:00
return ;
2020-09-12 15:49:20 +02:00
if ( event . oldPath === event . newPath ) {
2020-06-10 18:13:56 +02:00
const index = this . selection . findIndex ( e = > e . name === event . oldName ) ;
2020-09-12 15:49:20 +02:00
if ( index !== - 1 )
2020-06-10 18:13:56 +02:00
this . selection [ index ] . name = event . newName ;
} else {
/* re query files, because list has changed */
2020-09-12 15:49:20 +02:00
this . props . events . fire ( "query_files" , { path : this.currentPath } ) ;
2020-06-10 18:13:56 +02:00
}
}
@EventHandler < FileBrowserEvents > ( "notify_transfer_start" )
private handleTransferStart ( event : FileBrowserEvents [ "notify_transfer_start" ] ) {
2020-09-12 15:49:20 +02:00
if ( event . path !== this . currentPath )
2020-06-10 18:13:56 +02:00
return ;
let entry = this . fileList . find ( e = > e . name === event . name ) ;
2020-09-12 15:49:20 +02:00
if ( ! entry ) {
if ( event . mode !== "upload" ) {
2021-01-10 17:36:57 +01:00
logWarn ( LogCategory . FILE_TRANSFER , tr ( "Having file download start notification for current path, but target file is unknown (%s%s)" ) , event . path , event . name ) ;
2020-06-10 18:13:56 +02:00
return ;
}
entry = {
name : event.name ,
path : event.path ,
type : FileType . FILE ,
mode : "uploading" ,
virtual : true ,
datetime : Date.now ( ) ,
size : - 1
} ;
this . fileList . push ( entry ) ;
this . forceUpdate ( ) ;
}
entry . transfer = {
status : "pending" ,
direction : event.mode ,
id : event.id ,
percent : 0
} ;
}
@EventHandler < FileBrowserEvents > ( "notify_transfer_status" )
private handleTransferStatus ( event : FileBrowserEvents [ "notify_transfer_status" ] ) {
const index = this . fileList . findIndex ( e = > e . transfer ? . id === event . id ) ;
2020-12-18 19:18:01 +01:00
if ( index === - 1 ) {
2020-06-10 18:13:56 +02:00
return ;
2020-12-18 19:18:01 +01:00
}
2020-06-10 18:13:56 +02:00
let element = this . fileList [ index ] ;
2020-09-12 15:49:20 +02:00
if ( event . status === "errored" ) {
if ( element . mode === "uploading" ) {
2020-06-10 18:13:56 +02:00
/* re query files, because we don't know what the server did with the errored upload */
2020-09-12 15:49:20 +02:00
this . props . events . fire ( "query_files" , { path : this.currentPath } ) ;
2020-06-10 18:13:56 +02:00
return ;
}
} else {
element . transfer . status = event . status ;
2020-09-12 15:49:20 +02:00
if ( element . mode === "uploading" && event . status === "finished" ) {
2020-06-10 18:13:56 +02:00
/* upload finished, the element rerenders already with the correct values */
element . size = event . fileSize ;
element . mode = "normal" ;
}
}
}
}