2020-06-10 18:13:56 +02:00
import { EventHandler , ReactEventHandler , Registry } from "tc-shared/events" ;
import { useEffect , useRef , useState } from "react" ;
import { FileType } from "tc-shared/file/FileManager" ;
import * as ppt from "tc-backend/ppt" ;
import { SpecialKey } from "tc-shared/PPTListener" ;
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" ;
import * as Moment from "moment" ;
import { MenuEntryType , spawn_context_menu } from "tc-shared/ui/elements/ContextMenu" ;
import { BoxedInputField } from "tc-shared/ui/react-elements/InputField" ;
import {
FileBrowserEvents ,
FileTransferUrlMediaType ,
ListedFileInfo ,
TransferStatus
} from "tc-shared/ui/modal/transfer/ModalFileTransfer" ;
import * as log from "tc-shared/log" ;
import { LogCategory } from "tc-shared/log" ;
import { LoadingDots } from "tc-shared/ui/react-elements/LoadingDots" ;
import React = require ( "react" ) ;
const cssStyle = require ( "./ModalFileTransfer.scss" ) ;
interface NavigationBarProperties {
currentPath : string ;
events : Registry < FileBrowserEvents > ;
}
interface NavigationBarState {
currentPath : string ;
state : "editing" | "navigating" | "normal" ;
}
const ArrowRight = ( ) = > < div className = { cssStyle . arrow } > < div className = { cssStyle . inner } / > < / div > ;
const NavigationEntry = ( props : { events : Registry < FileBrowserEvents > , path : string , name : string } ) = > {
const [ dragHovered , setDragHovered ] = useState ( false ) ;
useEffect ( ( ) = > {
if ( ! dragHovered )
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 }
onClick = { ( ) = > props . events . fire ( "action_navigate_to" , { path : props.path } ) }
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 ( ) ;
setDragHovered ( true ) ;
} }
onDragLeave = { ( ) = > setDragHovered ( false ) }
onDrop = { event = > {
const types = event . dataTransfer . types ;
if ( types . length !== 1 )
return ;
/* TODO: Fix this code duplicate! */
if ( types [ 0 ] === FileTransferUrlMediaType ) {
/* TODO: If cross move upload! */
const fileUrls = event . dataTransfer . getData ( FileTransferUrlMediaType ) . split ( "&" ) . map ( e = > decodeURIComponent ( e ) ) ;
for ( const fileUrl of fileUrls ) {
const name = fileUrl . split ( "/" ) . last ( ) ;
const oldPath = fileUrl . split ( "/" ) . slice ( 0 , - 1 ) . join ( "/" ) + "/" ;
props . events . fire ( "action_rename_file" , {
newPath : props.path + "/" ,
oldPath : oldPath ,
oldName : name ,
newName : name
} ) ;
}
} else if ( types [ 0 ] === "Files" ) {
props . events . fire ( "action_start_upload" , { path : props.path , mode : "files" , files : [ . . . event . dataTransfer . files ] } ) ;
} else {
log . warn ( LogCategory . FILE_TRANSFER , tr ( "Received an unknown drop media type (%o)" ) , types ) ;
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 {
currentPath : this.props.currentPath ,
state : "normal" ,
}
}
render() {
let input ;
let path = this . state . currentPath ;
if ( ! path . endsWith ( "/" ) )
path += "/" ;
if ( this . state . state === "editing" ) {
input = (
< 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 ( ) }
/ >
) ;
} else if ( this . state . state === "navigating" || this . state . state === "normal" ) {
input = (
< 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 = { ( ) = > this . onRenderedPathClicked ( ) }
/ >
) ;
}
return < div className = { cssStyle . navigation } > { input } < / div > ;
}
componentDidUpdate ( prevProps : Readonly < NavigationBarProperties > , prevState : Readonly < NavigationBarState > , snapshot? : any ) : void {
setTimeout ( ( ) = > {
if ( this . refRendered . current )
this . refRendered . current . scrollLeft = 999999 ;
} , 10 ) ;
}
private onPathClicked ( event : React.MouseEvent , index : number ) {
let path ;
if ( index === - 1 )
path = "/" ;
else
path = "/" + this . state . currentPath . split ( "/" ) . filter ( e = > ! ! e ) . slice ( 0 , index + 1 ) . join ( "/" ) + "/" ;
this . props . events . fire ( "action_navigate_to" , { path : path } ) ;
event . stopPropagation ( ) ;
}
private onRenderedPathClicked() {
if ( this . state . state !== "normal" )
return ;
this . setState ( {
state : "editing"
} , ( ) = > this . refInput . current ? . focusInput ( ) ) ;
}
private onInputPathBluer() {
if ( this . state . state !== "editing" || this . ignoreBlur )
return ;
this . setState ( {
state : "normal"
} ) ;
}
private onPathEntered ( newPath : string ) {
if ( newPath === this . state . currentPath )
return ;
this . ignoreBlur = true ;
this . props . events . fire ( "action_navigate_to" , { path : newPath } ) ;
this . setState ( {
currentPath : newPath
} ) ;
}
private onButtonRefreshClicked() {
if ( this . state . state !== "normal" )
return ;
this . props . events . fire ( "action_navigate_to" , { path : this.state.currentPath } ) ;
}
@EventHandler < FileBrowserEvents > ( "action_navigate_to" )
private handleNavigateBegin() {
this . setState ( {
state : "navigating"
} , ( ) = > this . ignoreBlur = false ) ;
}
@EventHandler < FileBrowserEvents > ( "action_navigate_to_result" )
private handleNavigateResult ( event : FileBrowserEvents [ "action_navigate_to_result" ] ) {
if ( event . status === "success" )
this . lastSucceededPath = event . path ;
this . setState ( {
state : "normal" ,
currentPath : this.lastSucceededPath
} ) ;
if ( event . status !== "success" ) {
if ( event . status === "timeout" ) {
createErrorModal ( tr ( "Failed to enter path" ) , tra ( "Failed to enter given path.{:br:}Action resulted in a timeout." ) ) . open ( ) ;
} else {
createErrorModal ( tr ( "Failed to enter path" ) , tra ( "Failed to enter given path:{:br:}{0}" , event . error ) ) . open ( ) ;
}
}
}
}
interface FileListTableProperties {
currentPath : string ;
events : Registry < FileBrowserEvents > ;
}
interface FileListTableState {
state : "querying" | "normal" | "error" | "query-timeout" | "no-permissions" | "invalid-password" ;
errorMessage? : string ;
}
const FileName = ( props : { path : string , events : Registry < FileBrowserEvents > , file : ListedFileInfo } ) = > {
const [ editing , setEditing ] = useState ( props . file . mode === "create" ) ;
const [ fileName , setFileName ] = useState ( props . file . name ) ;
const refInput = useRef < HTMLInputElement > ( ) ;
let icon ;
if ( props . file . type === FileType . FILE ) {
icon = < img key = { "nicon" } src = { "img/icon_file_text.svg" } alt = { tr ( "File" ) } draggable = { false } / > ;
} else {
switch ( props . file . mode ) {
case "normal" :
icon = < img key = { "nicon" } src = { "img/icon_folder.svg" } alt = { tr ( "Directory icon" ) } title = { tr ( "Directory" ) } draggable = { false } / > ;
break ;
case "create" :
case "creating" :
case "empty" :
icon = < img key = { "nicon" } src = { "img/icon_folder_empty.svg" } alt = { tr ( "Empty directory icon" ) } title = { tr ( "Empty directory" ) } draggable = { false } / > ;
break ;
case "password" :
icon = < img key = { "nicon" } src = { "img/icon_folder_password.svg" } alt = { tr ( "Directory password protected" ) } title = { tr ( "Password protected directory" ) } draggable = { false } / > ;
break ;
default :
throw tr ( "Invalid directory state" ) ;
}
}
let name ;
if ( editing && props . file . mode !== "creating" && props . file . mode !== "uploading" ) {
name = < input
ref = { refInput }
defaultValue = { fileName }
onBlur = { event = > {
let name = event . target . value ;
setEditing ( false ) ;
if ( props . file . mode === "create" ) {
name = name || props . file . name ;
props . events . fire ( "action_create_directory" , {
path : props.path ,
name : name
} ) ;
setFileName ( name ) ;
props . file . name = name ;
props . file . mode = "creating" ;
} else {
if ( name . length > 0 && name !== props . file . name ) {
props . events . fire ( "action_rename_file" , { oldName : props.file.name , newName : name , oldPath : props.path , newPath : props.path } ) ;
setFileName ( name ) ;
}
}
} }
onKeyPress = { event = > {
if ( event . key === "Enter" ) {
event . currentTarget . blur ( ) ;
return ;
} else if ( event . key === "/" ) {
event . preventDefault ( ) ;
return ;
}
} }
onPaste = { event = > {
const input = event . currentTarget ;
setTimeout ( ( ) = > {
input . value = input . value . replace ( "/" , "" ) ;
} ) ;
} }
draggable = { false }
/ > ;
} else {
name = < a key = { "name" } onDoubleClick = { event = > {
if ( props . file . virtual || props . file . mode === "creating" || props . file . mode === "uploading" )
return ;
if ( ! ppt . key_pressed ( SpecialKey . SHIFT ) )
return ;
event . stopPropagation ( ) ;
props . events . fire ( "action_select_files" , { mode : "exclusive" , files : [ { name : props.file.name , type : props . file . type } ] } ) ;
props . events . fire ( "action_start_rename" , {
path : props.path ,
name : props.file.name
} ) ;
} } > { fileName } < / a > ;
}
props . events . reactUse ( "action_start_rename" , event = > setEditing ( event . name === props . file . name && event . path === props . path ) ) ;
props . events . reactUse ( "action_rename_file_result" , event = > {
if ( event . oldPath !== props . path || event . oldName !== props . file . name )
return ;
if ( event . status === "no-changes" )
return ;
if ( event . status === "success" ) {
props . file . name = event . newName ;
} else {
setFileName ( props . file . name ) ;
if ( event . status === "timeout" ) {
createErrorModal ( tr ( "Failed to rename file" ) , tra ( "Failed to rename file.{:br:}Action resulted in a timeout." ) ) . open ( ) ;
} else {
createErrorModal ( tr ( "Failed to rename file" ) , tra ( "Failed to rename file:{:br:}{0}" , event . error ) ) . open ( ) ;
}
}
} ) ;
useEffect ( ( ) = > {
refInput . current ? . focus ( ) ;
} ) ;
return < > { icon } { name } < / > ;
} ;
const FileSize = ( props : { path : string , events : Registry < FileBrowserEvents > , file : ListedFileInfo } ) = > {
const [ size , setSize ] = useState ( - 1 ) ;
props . events . reactUse ( "notify_transfer_status" , event = > {
if ( event . id !== props . file . transfer ? . id )
return ;
if ( props . file . transfer ? . direction !== "upload" )
return ;
switch ( event . status ) {
case "pending" :
setSize ( 0 ) ;
break ;
case "finished" :
case "none" :
setSize ( - 1 ) ;
break ;
}
} ) ;
props . events . reactUse ( "notify_transfer_progress" , event = > {
if ( event . id !== props . file . transfer ? . id )
return ;
if ( props . file . transfer ? . direction !== "upload" )
return ;
setSize ( event . fileSize ) ;
} ) ;
2020-06-10 22:44:50 +02:00
if ( size < 0 && ( props . file . size < 0 || typeof props . file . size === "undefined" ) )
2020-06-10 18:13:56 +02:00
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 > ;
} ;
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 = > {
if ( event . path !== props . file . path || event . name !== props . file . name )
return ;
setTransferStatus ( "pending" ) ;
} ) ;
props . events . reactUse ( "notify_transfer_status" , event = > {
if ( event . id !== props . file . transfer ? . id )
return ;
setTransferStatus ( event . status ) ;
if ( event . status === "finished" || event . status === "errored" )
setTransferProgress ( 100 ) ;
} ) ;
props . events . reactUse ( "notify_transfer_progress" , event = > {
if ( event . id !== props . file . transfer ? . id )
return ;
setTransferProgress ( event . progress ) ;
setTransferStatus ( event . status ) ;
} ) ;
/* reset the status after two seconds */
useEffect ( ( ) = > {
if ( transferStatus !== "finished" && transferStatus !== "errored" )
return ;
const id = setTimeout ( ( ) = > {
setTransferStatus ( "none" ) ;
} , 3 * 1000 ) ;
return ( ) = > clearTimeout ( id ) ;
} ) ;
if ( ! props . file . transfer )
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 ;
}
return (
< div className = { cssStyle . indicator + " " + color } style = { { right : ( ( 1 - transferProgress ) * 100 ) + "%" } } >
< div className = { cssStyle . status } / >
< / 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 ) ;
const onDoubleClicked = ( ) = > {
if ( file . type === FileType . DIRECTORY ) {
if ( file . mode === "creating" || file . mode === "create" )
return ;
props . events . fire ( "action_navigate_to" , {
path : file.path + file . name + "/"
} ) ;
} else {
if ( file . mode === "uploading" || file . virtual )
return ;
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 ;
if ( event . mode === "toggle" && contains )
setSelected ( ! selected ) ;
else if ( event . mode === "exclusive" ) {
setSelected ( contains ) ;
}
} ) ;
props . events . reactUse ( "notify_drag_ended" , ( ) = > setDropHovered ( false ) , dropHovered ) ;
props . events . reactUse ( "action_delete_file_result" , event = > {
event . results . forEach ( e = > {
if ( e . status !== "success" )
return ;
if ( e . path !== file . path || e . name !== file . name )
return ;
setHidden ( true ) ;
} ) ;
} , ! hidden ) ;
if ( hidden )
return null ;
return (
< TableRowElement
className = { cssStyle . directoryEntry + " " + ( selected ? cssStyle . selected : "" ) + " " + ( dropHovered ? cssStyle . hovered : "" ) }
rowData = { props . row }
columns = { props . columns }
onDoubleClick = { onDoubleClicked }
onClick = { ( ) = > props . events . fire ( "action_select_files" , { files : [ { name : file.name , type : file . type } ] , mode : ppt.key_pressed ( SpecialKey . SHIFT ) ? "toggle" : "exclusive" } ) }
onContextMenu = { e = > {
if ( ! selected ) {
if ( ! ( e . target instanceof HTMLDivElement ) ) {
/* explicitly clicked on one file */
props . events . fire ( "action_select_files" , { files : [ { name : file.name , type : file . type } ] , mode : ppt.key_pressed ( SpecialKey . SHIFT ) ? "toggle" : "exclusive" } ) ;
} else {
props . events . fire ( "action_select_files" , { files : [ ] , mode : "exclusive" } ) ;
}
}
props . events . fire ( "action_selection_context_menu" , {
pageX : e.pageX ,
pageY : e.pageY
} ) ;
e . preventDefault ( ) ;
} }
draggable = { ! props . row . userData . virtual }
onDragStart = { event = > {
if ( ! selected ) {
setSelected ( true ) ;
props . events . fire ( "action_select_files" , { files : [ { name : file.name , type : file . type } ] , mode : "exclusive" } ) ;
}
props . events . fire ( "notify_drag_started" , { event : event.nativeEvent } ) ;
} }
onDragOver = { event = > {
const types = event . dataTransfer . types ;
if ( types . length !== 1 )
return ;
if ( props . row . userData . type !== FileType . DIRECTORY ) {
event . stopPropagation ( ) ;
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 ( ) ;
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
>
< FileTransferIndicator events = { props . events } file = { props . row . userData } / >
< / TableRowElement >
) ;
} ;
@ReactEventHandler ( e = > e . props . events )
export class FileBrowser extends ReactComponentBase < FileListTableProperties , FileListTableState > {
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 ;
if ( this . state . state === "querying" ) {
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "loading" } className = { cssStyle . overlay } >
< a > < Translatable > loading < / Translatable > < LoadingDots maxDots = { 3 } / > < / a >
< / div >
) ;
} else if ( this . state . state === "error" ) {
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "query-error" } className = { cssStyle . overlay + " " + cssStyle . overlayError } >
< a > < Translatable > Failed to query directory : < / Translatable > < br / > { this . state . errorMessage } < / a >
< / div >
) ;
} else if ( this . state . state === "query-timeout" ) {
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "query-timeout" } className = { cssStyle . overlay + " " + cssStyle . overlayError } >
< a > < Translatable > Directory query timed out . < / Translatable > < br / > < Translatable > Please try again . < / Translatable > < / a >
< / div >
) ;
} else if ( this . state . state === "no-permissions" ) {
overlayOnly = true ;
overlay = ( ) = > (
< div key = { "no-permissions" } className = { cssStyle . overlay + " " + cssStyle . overlayError } >
< a > < Translatable > Directory query failed on permission < / Translatable > < br / > { this . state . errorMessage } < / a >
< / div >
) ;
} else if ( this . state . state === "invalid-password" ) {
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 >
) ;
} else if ( this . state . state === "normal" ) {
if ( this . fileList . length === 0 ) {
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 ) ;
for ( const directory of directories . sort ( ( a , b ) = > a . name > b . name ? 1 : - 1 ) ) {
rows . push ( {
columns : {
"name" : ( ) = > < FileName path = { this . currentPath } events = { this . props . events } 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 = { this . currentPath } events = { this . props . events } file = { file } / > ,
"size" : ( ) = > < FileSize path = { this . currentPath } events = { this . props . events } 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
} )
}
}
}
return (
< Table
ref = { this . refTable }
className = { cssStyle . fileTable }
bodyClassName = { cssStyle . body }
headerClassName = { cssStyle . header }
columns = { [
{ name : "name" , header : ( ) = > [
< a key = { "name-name" } > < Translatable > Name < / Translatable > < / a > , < div key = { "seperator-name" } className = { cssStyle . seperator } / >
] , width : 80 , className : cssStyle.columnName } ,
{ name : "type" , header : ( ) = > [
< a key = { "name-type" } > < Translatable > Type < / Translatable > < / a > , < div key = { "seperator-type" } className = { cssStyle . seperator } / >
] , fixedWidth : "8em" , className : cssStyle.columnType } ,
{ name : "size" , header : ( ) = > [
< a key = { "name-size" } > < Translatable > Size < / Translatable > < / a > , < div key = { "seperator-size" } className = { cssStyle . seperator } / >
] , fixedWidth : "8em" , className : cssStyle.columnSize } ,
{ name : "change-date" , header : ( ) = > [
< a key = { "name-date" } > < Translatable > Last changed < / Translatable > < / a > , < div key = { "seperator-date" } className = { cssStyle . seperator } / >
] , fixedWidth : "8em" , className : cssStyle.columnChanged } ,
] }
rows = { rows }
bodyOverlayOnly = { overlayOnly }
bodyOverlay = { overlay }
hiddenColumns = { [ "type" ] }
onHeaderContextMenu = { e = > this . onHeaderContextMenu ( e ) }
onBodyContextMenu = { e = > this . onBodyContextMenu ( e ) }
2020-06-10 22:44:50 +02:00
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 ( ) ;
} }
2020-06-10 18:13:56 +02:00
renderRow = { ( row : TableRow < ListedFileInfo > , columns , uniqueId ) = > < FileListEntry columns = { columns } row = { row } key = { uniqueId } events = { this . props . events } / > }
/ >
) ;
}
componentDidMount ( ) : void {
this . selection = [ ] ;
this . currentPath = this . props . currentPath ;
this . props . events . fire ( "query_files" , {
path : this.currentPath
} ) ;
}
2020-06-10 22:44:50 +02:00
private onDrop ( event : React.DragEvent ) {
const types = event . dataTransfer . types ;
if ( types . length !== 1 )
return ;
event . stopPropagation ( ) ;
let targetPath ;
{
let currentTarget = event . target as HTMLElement ;
while ( currentTarget && ! currentTarget . hasAttribute ( "x-drag-upload-path" ) )
currentTarget = currentTarget . parentElement ;
targetPath = currentTarget ? . getAttribute ( "x-drag-upload-path" ) || this . currentPath ;
console . log ( "Target: %o %s" , currentTarget , targetPath ) ;
}
if ( types [ 0 ] === FileTransferUrlMediaType ) {
/* TODO: If cross move upload! */
console . error ( event . dataTransfer . getData ( FileTransferUrlMediaType ) ) ;
const fileUrls = event . dataTransfer . getData ( FileTransferUrlMediaType ) . split ( "&" ) . map ( e = > decodeURIComponent ( e ) ) ;
for ( const fileUrl of fileUrls ) {
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
} ) ;
}
} else if ( types [ 0 ] === "Files" ) {
this . props . events . fire ( "action_start_upload" , { path : targetPath , mode : "files" , files : [ . . . event . dataTransfer . files ] } ) ;
} else {
log . warn ( LogCategory . FILE_TRANSFER , tr ( "Received an unknown drop media type (%o)" ) , types ) ;
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 ) {
if ( event . isDefaultPrevented ( ) ) return ;
event . preventDefault ( ) ;
this . props . events . fire ( "action_select_files" , { mode : "exclusive" , files : [ ] } ) ;
this . props . events . fire ( "action_selection_context_menu" , { pageY : event.pageY , pageX : event.pageX } ) ;
}
@EventHandler < FileBrowserEvents > ( "action_navigate_to_result" )
private handleNavigationResult ( event : FileBrowserEvents [ "action_navigate_to_result" ] ) {
if ( event . status !== "success" )
return ;
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" ] ) {
if ( event . path !== this . currentPath )
return ;
this . setState ( {
state : "querying"
} ) ;
}
@EventHandler < FileBrowserEvents > ( "query_files_result" )
private handleQueryFilesResult ( event : FileBrowserEvents [ "query_files_result" ] ) {
if ( event . status === "timeout" ) {
this . setState ( {
state : "query-timeout"
} ) ;
} else if ( event . status === "error" ) {
this . setState ( {
state : "error" ,
errorMessage : event.error || tr ( "unknown query error" )
} ) ;
} else if ( event . status === "success" ) {
this . fileList = event . files ;
this . setState ( {
state : "normal"
} ) ;
} else if ( event . status === "no-permissions" ) {
this . setState ( {
state : "no-permissions" ,
errorMessage : event.error || tr ( "unknown" )
} ) ;
} else if ( event . status === "invalid-password" ) {
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 ) ;
if ( index === - 1 )
return ;
if ( e . status === "success" )
this . fileList . splice ( index , 1 ) ;
} ) ;
event . results . forEach ( e = > {
if ( e . status === "success" )
return ;
createErrorModal ( tr ( "Failed to delete entry" ) , tra ( "Failed to delete \"{0}\":{:br:}{1}" , e . name , e . error || tr ( "Unknown error" ) ) ) . open ( ) ;
} ) ;
}
@EventHandler < FileBrowserEvents > ( "action_start_create_directory" )
private handleActionFileCreateBegin ( event : FileBrowserEvents [ "action_start_create_directory" ] ) {
let index = 0 ;
while ( this . fileList . find ( e = > e . name === ( event . defaultName + ( index > 0 ? " (" + index + ")" : "" ) ) ) )
index ++ ;
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! */
this . forceUpdate ( ( ) = > this . props . events . fire_async ( "action_select_files" , { files : [ { name : name , type : FileType . DIRECTORY } ] , mode : "exclusive" } ) ) ;
}
@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 ) ;
if ( fileIndex === - 1 )
return ;
fileIndex = this . fileList . length - fileIndex - 1 ;
const file = this . fileList [ fileIndex ] ;
if ( event . status === "success" ) {
if ( file . mode === "creating" )
file . mode = "empty" ;
return ;
} else if ( file . mode !== "creating" )
return ;
this . fileList . splice ( fileIndex , 1 ) ;
this . forceUpdate ( ) ;
if ( event . status === "timeout" ) {
createErrorModal ( tr ( "Failed to create directory" ) , tra ( "Failed to create directory.{:br:}Action resulted in a timeout." ) ) . open ( ) ;
} else {
createErrorModal ( tr ( "Failed to create directory" ) , tra ( "Failed to create directory:{:br:}{0}" , event . error ) ) . open ( ) ;
}
}
@EventHandler < FileBrowserEvents > ( "action_select_files" )
private handleActionSelectFiles ( event : FileBrowserEvents [ "action_select_files" ] ) {
if ( event . mode === "exclusive" ) {
this . selection = event . files . slice ( 0 ) ;
} else if ( event . mode === "toggle" ) {
event . files . forEach ( e = > {
const index = this . selection . map ( e = > e . name ) . findIndex ( b = > b === e . name ) ;
if ( index === - 1 )
this . selection . push ( e ) ;
else
this . selection . splice ( index ) ;
} ) ;
}
}
@EventHandler < FileBrowserEvents > ( "notify_drag_started" )
private handleNotifyDragStarted ( event : FileBrowserEvents [ "notify_drag_started" ] ) {
if ( this . selection . length === 0 ) {
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" ] ) {
if ( event . oldPath !== this . currentPath && event . newPath !== this . currentPath )
return ;
if ( event . status !== "success" )
return ;
if ( event . oldPath === event . newPath ) {
const index = this . selection . findIndex ( e = > e . name === event . oldName ) ;
if ( index !== - 1 )
this . selection [ index ] . name = event . newName ;
} else {
/* re query files, because list has changed */
this . props . events . fire ( "query_files" , { path : this.currentPath } ) ;
}
}
@EventHandler < FileBrowserEvents > ( "notify_transfer_start" )
private handleTransferStart ( event : FileBrowserEvents [ "notify_transfer_start" ] ) {
if ( event . path !== this . currentPath )
return ;
let entry = this . fileList . find ( e = > e . name === event . name ) ;
if ( ! entry ) {
if ( event . mode !== "upload" ) {
log . warn ( LogCategory . FILE_TRANSFER , tr ( "Having file download start notification for current path, but target file is unknown (%s%s)" ) , event . path , event . name ) ;
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 ) ;
if ( index === - 1 )
return ;
let element = this . fileList [ index ] ;
if ( event . status === "errored" ) {
if ( element . mode === "uploading" ) {
/* re query files, because we don't know what the server did with the errored upload */
this . props . events . fire ( "query_files" , { path : this.currentPath } ) ;
return ;
}
} else {
element . transfer . status = event . status ;
2020-06-10 22:44:50 +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" ;
}
}
}
}