import { Component , AfterViewInit , Input , ViewChildren } from "@angular/core" ; import { FileFolderComponent } from "../../components/file-folder/file-folder.component" ; import { PopUpComponent } from "../../components/pop-up/pop-up.component" ; import sortType from "../../../model/sortType" ; import keyBoardStatus from "../../../model/keyBoardStatus" ; import dragDimension from "../../../model/dragDimension" ; import { fs } from "../../../model/fs" ; import AVAILABLE_FILE_ICONS from "../../../default-values/AVAILABLE_FILE_ICONS" ; import AVAILABLE_POP_UP_ICONS from "../../../default-values/AVAILABLE_POP_UP_ICONS" ; import config from "../../config/config" ; let dynamic_port = localStorage . getItem ( "USED_PORT" ) || config . server . PORT ; let domain = `http://${ config . server . HOST }:${ dynamic_port }` ; let socket = new WebSocket ( `ws://localhost:${ dynamic_port }` ) ; let isSocketOpen = false ; const _ = ( s ) : any => document . querySelector ( s ) ; const $ = ( s ) : any => document . querySelectorAll ( s ) ; declare global { interface Window { main : any ; } } let keyState = { ctrl : false , shift : false , alt : false , caps : false } ; @ Component ( { selector : "app-file-manager" , templateUrl : "./file-manager.component.html" , styleUrls : [ "./file-manager.component.css" ] } ) export class FileManagerComponent implements AfterViewInit { @ Input ( ) theme : string = "light" ; @ ViewChildren ( FileFolderComponent ) filesAndFolders : any ; @ ViewChildren ( PopUpComponent ) popUpElement : any ; dragging : boolean = false ; drag : dragDimension = new dragDimension ( ) ; path : string [ ] = [ ] ; contents : any [ ] = [ ] ; search : any = null ; editPathInput : any = null ; sort : sortType = new sortType ( "type" , "asc" ) ; noItems : boolean = false ; error_loading : string = "" ; loading : boolean = false ; INITIAL_PATH : string = "/" ; editingPath : boolean = false ; default_icon_size_index : number = 2 ; iconSizes : string [ ] = [ "ex-sm" , "sm" , "md" , "lg" , "ex-lg" , "hg" ] ; iconSizeIndex : any = this . default_icon_size_index ; connections : any [ ] = [ ] ; popUp : any = this . initialPopUpState ( ) ; contextMenu : any = null ; paste : any = null ; inBrowser : boolean = false ; process_id : number = 0 ; background_processes : any = [ ] ; keyboard : keyBoardStatus = keyState ; isProgressActive : boolean = false ; online : boolean = navigator . onLine ; current_server : string | null = null ; current_test_connection : string | null = null ; isWin : boolean | null = null ; finished_loaded_init_contents : boolean = false ; finished_setting_up_socket : boolean = false ; max_spash_visibility_over : boolean = false ; minimum_splash_visibility : number = 0 ; isDriveListing : boolean = false ; constructor ( ) { this . setUpSocket ( ) ; this . theme = localStorage . getItem ( "theme" ) || "light" ; document . body . setAttribute ( "data-theme" , this . theme ) ; this . inBrowser = typeof window ?. main == "undefined" ; this . setIconSizeIndex ( localStorage . getItem ( "icon-size" ) ?? this . default_icon_size_index ) ; this . connections = this . getConnections ( ) ; this . connectToFileSystem ( null , true ) ; window . addEventListener ( "offline" , event => { this . online = false ; this . toast ( "error" , "You are offline" ) ; } ) ; window . addEventListener ( "online" , event => { this . online = true ; this . toast ( "success" , "Back online" ) ; } ) ; setTimeout ( ( ) => { this . max_spash_visibility_over = true ; } , this . minimum_splash_visibility ) ; } ngAfterViewInit ( ) { this . search = _ ( "#search" ) ; this . editPathInput = _ ( "#editable-path" ) ; this . arrange ( ) ; } closeProgress ( ) { this . isProgressActive = false ; } toggleProgressState ( ) { this . isProgressActive = ! this . isProgressActive ; } getProcessTitle ( type , status ) { if ( type == fs . DELETE ) { return status == "in-progress" ? "Deleting" : "Delete" ; } else if ( type == fs . NEW_FOLDER ) { return status == "in-progress" ? "Creating New Folder" : "New Folder" ; } else if ( type == fs . NEW_FILE ) { return status == "in-progress" ? "Creating New File" : "New File" ; } else if ( type == fs . RENAME ) { return status == "in-progress" ? "Renaming" : "Rename" ; } else if ( type == fs . CUT_PASTE ) { return status == "in-progress" ? "Moving" : "Move" ; } else if ( type == fs . COPY_PASTE ) { return status == "in-progress" ? "Copying" : "Copy" ; } else if ( type == fs . OPEN_FILE ) { return status == "in-progress" ? "Opening" : "Opened" ; } return "In progress" ; } setUpSocket ( ) { socket . startBackgroundProcess = ( type , data , progressIndefinite = true ) => { let process = { process_id : this . process_id , type , data , progressIndefinite , progress : 0 , status : "in-progress" } ; socket . send ( JSON . stringify ( process ) ) ; this . background_processes [ this . process_id ] = process ; return this . process_id ++ ; } ; socket . onopen = ( event ) => { isSocketOpen = true ; socket . send ( JSON . stringify ( { type : "settings" , data : { } } ) ) ; } ; socket . onmessage = ( event ) => { try { let data = JSON . parse ( event . data ) ; console . log ( data ) ; if ( data . type == "progress" ) { this . background_processes [ data . process_id ] . progress = data . progress ; } else if ( data . type == "completed" ) { this . background_processes [ data . process_id ] . status = "completed" ; if ( data . reload ) { this . loadDirContents ( this . getCWD ( ) ) ; } this . toast ( "success" , data . message ) ; } else if ( data . type == "failed" ) { console . error ( data ) ; this . background_processes [ data . process_id ] . status = "failed" ; this . toast ( "error" , data . message ) ; } else if ( data . type == "partial-success" ) { this . background_processes [ data . process_id ] . status = "failed" ; if ( data . reload ) { this . loadDirContents ( this . getCWD ( ) ) ; } this . toast ( "warning" , data . message ) ; } else if ( data . type == "open-short-folder" ) { this . background_processes [ data . process_id ] . status = "completed" ; this . loadDirContents ( data . extra_data . path ) ; } else if ( data . type == "settings" ) { this . isWin = data . data . isWin ; this . finished_setting_up_socket = true ; } } catch ( error ) { console . error ( error . message ) ; console . warn ( event . data ) ; } } ; socket . onclose = ( ) => { socket = { startBackgroundProcess : ( a = { } , b = { } , c = { } ) => null } ; isSocketOpen = false ; } ; } openDir ( data , fallback = false ) { let name = fallback ? data . name : data . folder ; if ( data . readable || data . isDrive ) { this . loadDirContents ( this . getCWD ( name ) ) ; } else { this . toast ( "error" , `Directory "${ name }" is not accessible` ) ; } } moveToDir ( event ) { let index = event . currentTarget . getAttribute ( "data-index" ) ; let temp = this . path . slice ( 0 , index ) . join ( "/" ) || "/" ; temp = this . normalize_path ( temp ) ; this . loadDirContents ( temp ) ; } goBackOneDir ( ) { if ( this . path . length < 1 ) { return ; } let path = [ ... this . path ] ; path . pop ( ) ; this . loadDirContents ( this . normalize_path ( path . join ( "/" ) ) || "/" ) ; } refresh ( ) { this . loadDirContents ( this . getCWD ( ) ) ; } normalize_path ( path ) { path = path . replace ( "://" , ":" ) . replace ( ":/" , ":" ) . replace ( ":" , "://" ) ; return path . includes ( ":" ) ? path : path . startsWith ( "/" ) ? path : `/${ path }` ; } getCWD ( extra_path = "" ) { let temp = `${ this . path . join ( "/" ) }/` ; temp = temp != "/" ? temp : "" ; temp = `${ temp }${ extra_path }` ; temp = this . normalize_path ( temp ) ; return temp || "/" ; } toast ( type = "info" , message , delay = 4000 ) { let delayOffset = 800 ; let box = document . createElement ( "div" ) ; box . classList . add ( "toast" ) ; box . classList . add ( `toast-${ type }` ) ; box . innerHTML = message ; _ ( ".toast-container" ) ?. appendChild ( box ) ; setTimeout ( ( ) => { box . classList . add ( "toast-fade" ) ; } , delay ) ; setTimeout ( ( ) => { box ?. parentNode ?. removeChild ( box ) ; } , delay + delayOffset ) ; } loadDirContents ( path , fromSplash = false ) { if ( this . loading ) { this . toast ( "warning" , "Please wait while loading is completed." ) ; return ; } if ( this . search != null ) { this . search . value = "" ; } this . loading = true ; this . contents = [ ] ; let protocol , body ; if ( this . current_server == null ) { protocol = "local" ; body = JSON . stringify ( { path } ) ; } else { protocol = "ssh" ; body = JSON . stringify ( { server_id : this . current_server , path } ) ; } fetch ( `${ domain }/fs/${ protocol }/dir-contents/` , { method : "POST" , headers : { Accept : "application/json" , "Content-Type" : "application/json" } , body } ) . then ( resp => { if ( resp . status === 200 ) { return resp . json ( ) ; } throw new Error ( "status error" ) ; } ) . then ( data => { if ( data . status ) { this . isDriveListing = data . type == "drive" ; this . path = this . parsePath ( data . path ) ; this . contents = data . contents . map ( ( item ) => { let mime_type = item . mime_type ; if ( AVAILABLE_FILE_ICONS . includes ( mime_type ) ) { item . file_icon = `file-${ mime_type }` ; } else { item . file_icon = "file-default" ; } item . selected = false ; item . isDrive = this . isDriveListing ; return item ; } ) ; this . noItems = this . contents . length < 1 ; this . error_loading = "" ; this . arrange ( ) ; } else { console . log ( `%c${ data . message }` , "color:#FC0" ) ; console . log ( `%c${ data . error_log }` , "color:#F00" ) ; if ( data . customError ) { throw { name : "customError" , message : data . message } ; } throw new Error ( "Some Error occured" ) ; } } ) . catch ( err => { console . log ( err . message ) ; let err_msg = err . name === "customError" ? err . message : "Something went wrong" ; this . toast ( "error" , err_msg ) ; this . error_loading = err_msg ; this . path = this . parsePath ( "/" ) ; this . contents = [ ] ; this . noItems = true ; } ) . finally ( ( ) => { this . loading = false ; this . editingPath = false ; if ( fromSplash ) { this . finished_loaded_init_contents = true ; } } ) ; } switchTheme ( ) { let body = document . body ; this . theme = body . getAttribute ( "data-theme" ) == "dark" ? "light" : "dark" ; body . setAttribute ( "data-theme" , this . theme ) ; localStorage . setItem ( "theme" , this . theme ) ; } mouseDown ( event ) { let isRightClick = event . button === 2 ; if ( ! ( this . keyboard . ctrl || this . keyboard . shift || isRightClick ) ) { this . clearSelection ( ) ; } let { clientX : x , clientY : y } = event ; this . drag . initY = y ; this . drag . initX = x ; this . setSelectDimensions ( x , y , 0 , 0 ) ; this . dragging = true ; } mouseMoving ( event ) { if ( ! this . dragging ) { return ; } let { clientX : x , clientY : y } = event ; if ( this . drag . initY < y && this . drag . initX < x ) { this . setSelectDimensions ( this . drag . initX , this . drag . initY , x - this . drag . initX , y - this . drag . initY ) ; } else if ( this . drag . initY < y && this . drag . initX >= x ) { this . setSelectDimensions ( x , this . drag . initY , this . drag . initX - x , y - this . drag . initY ) ; } else if ( this . drag . initY >= y && this . drag . initX < x ) { this . setSelectDimensions ( this . drag . initX , y , x - this . drag . initX , this . drag . initY - y ) ; } else if ( this . drag . initY >= y && this . drag . initX >= x ) { this . setSelectDimensions ( x , y , this . drag . initX - x , this . drag . initY - y ) ; } this . selectItemsInDragBox ( ) ; } mouseUp ( ) { this . dragging = false ; } selectItemsInDragBox ( ) { this . filesAndFolders ?. _results ?. forEach ( ( item ) => { if ( this . keyboard . ctrl && item . content . selected ) { return ; } let i = item . getDimensions ( ) , d = this . drag ; d . right = d . left + d . width ; d . bottom = d . top + d . height ; let item_left_in_drag_x = i . left > d . left && i . left < d . right ; let item_right_in_drag_x = i . right > d . left && i . right < d . right ; let item_x_in_drag_x = item_left_in_drag_x || item_right_in_drag_x ; let item_top_in_drag_y = i . top > d . top && i . top < d . bottom ; let item_bottom_in_drag_y = i . bottom > d . top && i . bottom < d . bottom ; let item_y_in_drag_y = item_top_in_drag_y || item_bottom_in_drag_y ; let item_in_drag = item_x_in_drag_x && item_y_in_drag_y ; let drag_left_in_item_x = d . left > i . left && d . left < i . right ; let drag_right_in_item_x = d . right > i . left && d . right < i . right ; let drag_x_in_item_x = drag_left_in_item_x || drag_right_in_item_x ; let drag_top_in_item_y = d . top > i . top && d . top < i . bottom ; let drag_bottom_in_item_y = d . bottom > i . top && d . bottom < i . bottom ; let drag_y_in_item_y = drag_top_in_item_y || drag_bottom_in_item_y ; let drag_in_item = drag_x_in_item_x && drag_y_in_item_y ; let drag_x_between_item_x = drag_x_in_item_x && item_y_in_drag_y ; let drag_y_between_item_y = drag_y_in_item_y && item_x_in_drag_x ; let drag_between_item = drag_x_between_item_x || drag_y_between_item_y ; let select = item_in_drag || drag_in_item || drag_between_item ; item . setItemSelection ( select ) ; } ) ; } clearSelection ( ) { this . contents = this . contents . map ( content => { content . selected = false ; return content ; } ) ; } setSelectDimensions ( x , y , w , h ) { this . drag . top = y ; this . drag . left = x ; this . drag . width = w ; this . drag . height = h ; } preventDefault ( event ) { event . preventDefault ( ) ; } updateKeyBoardState ( event ) { this . keyboard . ctrl = event . ctrlKey ; this . keyboard . shift = event . shiftKey ; this . keyboard . alt = event . altKey ; this . keyboard . caps = event . getModifierState ( "CapsLock" ) ; } shortcut ( event ) { let key = event . keyCode ; this . updateKeyBoardState ( event ) ; let ctrl = this . keyboard . ctrl ; let shift = this . keyboard . shift ; let alt = this . keyboard . alt ; let caps = this . keyboard . caps ; if ( ctrl || shift || alt ) { if ( ctrl ) { if ( key === 65 ) { event . preventDefault ( ) ; this . selectAll ( ) ; } if ( key === 70 ) { event . preventDefault ( ) ; _ ( "#search" ) ?. focus ( ) ; } if ( key === 73 ) { event . preventDefault ( ) ; this . invertAllItemSelections ( ) ; } if ( key == 82 ) { event . preventDefault ( ) ; this . refresh ( ) ; } if ( key == 107 || key == 187 ) { event . preventDefault ( ) ; this . setIconSizeIndex ( this . iconSizeIndex + 1 ) ; } if ( key == 109 || key == 189 ) { event . preventDefault ( ) ; this . setIconSizeIndex ( this . iconSizeIndex - 1 ) ; } if ( key == 96 || key == 48 ) { event . preventDefault ( ) ; this . setIconSizeIndex ( this . default_icon_size_index ) ; } if ( shift ) { let k_start = 49 , k_end = 54 , t_n = this . iconSizes . length ; if ( key >= k_start && key <= k_end ) { event . preventDefault ( ) ; this . setIconSizeIndex ( t_n - 1 - ( ( key - ( k_start - t_n ) ) % t_n ) ) ; } } } } else if ( key === 114 ) { event . preventDefault ( ) ; _ ( "#search" ) ?. focus ( ) ; } else if ( key === 115 ) { this . editThePath ( ) ; } else if ( key === 116 ) { event . preventDefault ( ) ; this . refresh ( ) ; } else if ( key === 8 ) { event . preventDefault ( ) ; this . goBackOneDir ( ) ; } } editThePath ( ) { this . editPathInput . value = this . getCWD ( ) || "/" ; this . editingPath = true ; this . editPathInput . selectionStart = 0 ; this . editPathInput . selectionEnd = this . editPathInput . value . length - 1 ; _ ( "#editable-path" ) ?. focus ( ) ; } parsePath ( url ) { return url ?. replace ( /(:\/\/|\\)/g , "/" ) ?. split ( "/" ) ?. filter ( dir => dir != "" ) ; } stopPropagation ( event ) { event ?. stopPropagation ( ) ; this . updateKeyBoardState ( event ) ; this . closeMenu ( ) ; } searching ( event ) { event ?. stopPropagation ( ) ; this . updateKeyBoardState ( event ) ; this . closeMenu ( ) ; let key = this . search . value , i = 0 ; this . filesAndFolders ?. _results ?. forEach ( ( item ) => { if ( item . getData ( ) . name . toLowerCase ( ) . indexOf ( key . toLowerCase ( ) ) == - 1 ) { item . hide ( ) ; } else { item . show ( ) ; i ++ ; } } ) ; this . noItems = i < 1 ; } strcmp ( a , b ) { let x = a . name . toLowerCase ( ) , y = b . name . toLowerCase ( ) ; return x < y ? - 1 : x > y ? 1 : 0 ; } arrange ( ) { let { key , order } = this . sort ; if ( key == "type" ) { let temp_files = [ ] , temp_folders = [ ] ; this . contents . forEach ( content => { if ( content . folder ) { temp_folders . push ( content ) ; } else { temp_files . push ( content ) ; } } ) ; temp_files = this . alphabeticalSort ( temp_files ) || [ ] ; temp_folders = this . alphabeticalSort ( temp_folders ) || [ ] ; this . contents = [ ... temp_folders , ... temp_files ] ; } if ( key == "name" ) { this . contents = this . alphabeticalSort ( this . contents ) || [ ] ; } else if ( key == "size" ) { console . error ( "sort by size not implemented" ) ; } else if ( key == "date" ) { console . error ( "sort by date not implemented" ) ; } if ( order == "desc" ) { this . contents . reverse ( ) ; } } alphabeticalSort ( data = [ ] ) { let temp = [ ... data ] ; temp . sort ( this . strcmp ) ; return temp ; } getSelectorStyle ( ) { return { top : this . drag . top + "px" , left : this . drag . left + "px" , width : this . drag . width + "px" , height : this . drag . height + "px" } ; } toggleEditPath ( ) { this . editPathInput . value = this . getCWD ( ) || "/" ; this . editingPath = ! this . editingPath ; _ ( "#editable-path" ) ?. focus ( ) ; } typingPath ( event ) { event ?. stopPropagation ( ) ; this . updateKeyBoardState ( event ) ; this . closeMenu ( ) ; let key = event . keyCode ; if ( key === 13 ) { this . goToPath ( ) ; } else if ( key === 27 ) { this . editingPath = false ; } } goToPath ( ) { this . loadDirContents ( this . editPathInput . value || "/" ) ; } updateIconSize ( offset ) { this . setIconSizeIndex ( this . iconSizeIndex + offset ) ; } selectAll ( ) { this . contents = this . contents . map ( content => { content . selected = true ; return content ; } ) ; } invertAllItemSelections ( ) { this . contents = this . contents . map ( content => { content . selected = ! content . selected ; return content ; } ) ; } setIconSizeIndex ( index ) { this . iconSizeIndex = Math . min ( this . iconSizes . length - 1 , Math . max ( 0 , index ) ) ; localStorage . setItem ( "icon-size" , this . iconSizeIndex ) ; } shiftSelection ( current ) { let start = this . getFirstSelectedItem ( ) ; if ( start != null ) { this . selectFileFolderFromIndex ( start , current ) ; } else { this . contents [ current ] . selected = true ; } } selectFileFolderFromIndex ( a , b ) { let start = Math . min ( a , b ) , end = Math . max ( a , b ) ; for ( let i = 0 , n = this . contents . length ; i < n ; i ++ ) { this . contents [ i ] . selected = i >= start && i <= end ; } } getFirstSelectedItem ( ) { for ( let i = 0 , n = this . contents . length ; i < n ; i ++ ) { if ( this . contents [ i ] . selected ) { return i ; } } return null ; } setConnections ( connections ) { localStorage . setItem ( "connections" , btoa ( JSON . stringify ( connections ) ) ) ; } getConnections ( ) { return ( JSON . parse ( atob ( localStorage . getItem ( "connections" ) ?? "" ) || "[]" ) || [ ] ) ; } getNumberOfSelectedItems ( ) { let count = 0 ; this . contents . forEach ( ( item ) => { if ( item . selected ) { count ++ ; } } ) ; return count ; } deleteConnection ( event , index ) { event ?. stopPropagation ( ) ; this . closeMenu ( ) ; this . showPopUp ( "confirm" , "Delete Connection" , `Are you sure you want to delete the connection : "${ this . connections [ index ] . name }"?` , "delete" , "Delete" , ( data ) => { let deleting_server_id = `${ this . connections [ index ] [ "user" ] }@${ this . connections [ index ] [ "server" ] }` ; this . connections = [ ... this . connections . slice ( 0 , index ) , ... this . connections . slice ( index + 1 ) ] ; this . setConnections ( this . connections ) ; this . closePopUp ( ) ; if ( this . current_server == deleting_server_id ) { this . connectToFileSystem ( null ) ; } } , "Cancel" , ( data ) => { this . closePopUp ( ) ; } , true ) ; } connectToFileSystem ( id , fromSplash = false ) { if ( this . loading ) { this . toast ( "warning" , "Please wait while loading is completed." ) ; return ; } if ( id == null ) { this . current_server = null ; this . loadDirContents ( this . INITIAL_PATH , fromSplash ) ; return ; } this . loading = true ; this . contents = [ ] ; let connection = this . connections [ id ] ; this . current_server = `${ connection . user }@${ connection . server }` ; fetch ( `${ domain }/fs/ssh/connect` , { method : "POST" , headers : { Accept : "application/json" , "Content-Type" : "application/json" } , body : JSON . stringify ( { server : connection . server , user : connection . user , password : connection . password , force : false } ) } ) . then ( resp => { return resp . json ( ) ; } ) . then ( data => { this . loading = false ; if ( data . status ) { this . loadDirContents ( this . INITIAL_PATH ) ; } else { throw data ; } } ) . catch ( error => { console . error ( error ) ; this . loading = false ; this . noItems = true ; this . error_loading = "Connection failed" ; } ) ; } testConnection ( server , user , password ) { let testing_connection = `${ user }@${ server }` ; this . current_test_connection = testing_connection ; this . popUp . test_connection = "loading" ; fetch ( `${ domain }/fs/ssh/connect` , { method : "POST" , headers : { Accept : "application/json" , "Content-Type" : "application/json" } , body : JSON . stringify ( { server , user , password , force : true } ) } ) . then ( resp => { return resp . json ( ) ; } ) . then ( data => { if ( data . status ) { if ( data . data == this . current_test_connection ) { this . popUp . test_connection = "success" ; } } else { throw new Error ( "Connection Failed" ) ; } } ) . catch ( error => { if ( testing_connection == this . current_test_connection ) { this . popUp . test_connection = "failed" ; } } ) ; } initialPopUpState ( ) { return { active : false , type : "confirm" , icon : "" , title : "" , body : "" , ok : null , cancel : null , misk : null , test_connection : null , backgroundCancellation : true } ; } showPopUp ( type = "confirm" , title , body , icon , ok , okHandler , cancel , cancelHandler , backgroundCancellation = true , misk = null , miskHandler = null ) { icon = AVAILABLE_POP_UP_ICONS . includes ( icon ?? "default" ) ? icon : "default" ; this . popUp = { active : true , type , icon , title , body , ok : ok == null ? null : { text : ok , handler : okHandler } , cancel : cancel == null ? null : { text : cancel , handler : cancelHandler } , misk : misk == null ? null : { text : misk , handler : miskHandler } , test_connection : null , backgroundCancellation } ; } closePopUp ( ) { this . popUp = this . initialPopUpState ( ) ; } handlePopUpEvent ( type , data ) { this . popUp [ type ] . handler ( data ) ; } addConnection ( ) { this . showPopUp ( "connection-properties" , "New Connection" , null , "default" , "Add" , ( data ) => { let connection = { } ; connection . protocol = "ssh" ; connection . name = data [ "connection-name" ] ; connection . description = data [ "connection-description" ] ; connection . server = data [ "connection-server" ] ; connection . user = data [ "connection-user" ] ; connection . password = data [ "connection-password" ] ; this . connections . push ( connection ) ; this . setConnections ( this . connections ) ; this . closePopUp ( ) ; } , "Cancel" , ( data ) => { this . closePopUp ( ) ; } , false , "Test" , ( data ) => { this . testConnection ( data [ "connection-server" ] , data [ "connection-user" ] , data [ "connection-password" ] ) ; } ) ; } editConnection ( event , id ) { event . stopPropagation ( ) ; this . closeMenu ( ) ; let connection = this . connections [ id ] ; this . popUpElement . _results [ 0 ] . editConnectionData = { "connection-name" : connection . name , "connection-description" : connection . description , "connection-server" : connection . server , "connection-user" : connection . user , "connection-password" : connection . password } ; this . showPopUp ( "connection-properties" , "Edit Connection" , null , "default" , "Save" , ( data ) => { let connection = { } ; connection . protocol = "ssh" ; connection . name = data [ "connection-name" ] ; connection . description = data [ "connection-description" ] ; connection . server = data [ "connection-server" ] ; connection . user = data [ "connection-user" ] ; connection . password = data [ "connection-password" ] ; this . connections [ id ] = connection ; this . setConnections ( this . connections ) ; this . closePopUp ( ) ; } , "Cancel" , ( data ) => { this . closePopUp ( ) ; } , false , "Test" , ( data ) => { this . testConnection ( data [ "connection-server" ] , data [ "connection-user" ] , data [ "connection-password" ] ) ; } ) ; } rightClick ( event , fromMain = true , isFolder = false , multipleSelected = false ) { event ?. preventDefault ( ) ; let menu = _ ( ".context-menu" ) ?. getBoundingClientRect ( ) ; let screen = document . body . getBoundingClientRect ( ) ; let w = menu . width , h = menu . height ; let offsetX = screen . width - w , offsetY = screen . height - h ; let x = event . clientX , y = event . clientY ; let left = x < offsetX ? x : x - w , top = y < offsetY ? y : y - h ; let selectedItems = null , visibleOptions = [ ] ; if ( ! fromMain ) { selectedItems = this . contents . filter ( ( item ) => item . selected ) ; if ( this . isDriveListing ) { if ( selectedItems . length == 1 ) { visibleOptions = [ "open" ] ; } } else { visibleOptions = [ "delete" , "cut" , "copy" , "rename" , "properties" ] ; if ( selectedItems . length == 1 ) { if ( selectedItems [ 0 ] . folder ) { visibleOptions . push ( `open` ) ; } } } } else { visibleOptions = [ "back" , "refresh" , "paste" , "new-folder" , "new-file" ] ; } this . contextMenu = { visibility : "hidden" , fromMain , isFolder , multipleSelected , top : top + "px" , left : left + "px" , x , y , selectedItems , visibleOptions } ; } getContextMenuStyle ( ) { return { visibility : this . contextMenu ?. visibility ?? "hidden" , top : this . contextMenu ?. top || 0 , left : this . contextMenu ?. left || 0 } ; } closeMenu ( ) { this . contextMenu = null ; } fileFolderRightClick ( event , isFolder , index ) { event ?. preventDefault ( ) ; event ?. stopPropagation ( ) ; this . closePopUp ( ) ; let multipleSelected = this . getNumberOfSelectedItems ( ) > 0 ; this . contents [ index ] . selected = true ; this . rightClick ( event , false , isFolder , multipleSelected ) ; setTimeout ( this . setMenuStyle , 0 ) ; } setMenuStyle ( ) { if ( this . contextMenu != null ) { let menu = _ ( ".context-menu" ) ?. getBoundingClientRect ( ) ; let screen = document . body . getBoundingClientRect ( ) ; let w = menu . width ; let h = menu . height ; let offsetX = screen . width - w ; let offsetY = screen . height - h ; let x = this . contextMenu . x ; let y = this . contextMenu . y ; let left = x < offsetX ? x : x - w ; let top = y < offsetY ? y : y - h ; this . contextMenu . top = top + "px" ; this . contextMenu . left = left + "px" ; this . contextMenu . visibility = "visible" ; } return false ; } newFileFolder ( type ) { let max_n = 0 ; let isFolder = type == "folder" ; let pattern = isFolder ? /^New Folder [0-9]+$/ : /^Text Document [0-9]+\.txt$/ ; let operation = isFolder ? fs . NEW_FOLDER : fs . NEW_FILE ; this . contents . forEach ( ( item ) => { if ( isFolder == item . folder && pattern . test ( item . name ) ) { max_n = Math . max ( max_n , parseInt ( item . name . split ( " " ) . pop ( ) ) ) ; } } ) ; let status = socket . startBackgroundProcess ( operation , { source : { server : this . current_server , baseFolder : this . getCWD ( ) } , files : [ isFolder ? `New Folder ${ ++ max_n }` : `Text Document ${ ++ max_n }.txt` ] } ) ; if ( status === null ) { this . toast ( "error" , "Not Connected to Server, Reconnect" ) ; } } deleteFileFolder ( ) { let operation = fs . DELETE ; let list = this . contextMenu . selectedItems . map ( ( item ) => { return { name : item . name , isFolder : item . folder } ; } ) ; this . showPopUp ( "confirm" , "Delete" , `Are you sure you want to delete ${ list . map ( ( item ) => `"${ item . name }"` ) . join ( ", " ) }` , "delete" , "Delete" , ( data ) => { let status = socket . startBackgroundProcess ( operation , { source : { server : this . current_server , baseFolder : this . getCWD ( ) } , files : list } ) ; if ( status === null ) { this . toast ( "error" , "Not Connected to Server, Reconnect" ) ; } this . closePopUp ( ) ; } , "Cancel" , ( data ) => { this . closePopUp ( ) ; } , true ) ; } hideBackgroundProcess ( id ) { delete this . background_processes [ id ] ; } isBackgroundProssessesRunning ( ) { return this . background_processes . some ( ( process ) => { return process . status == "in-progress" ; } ) ; } getContentContainerStyle ( ) { let classList = [ "contents" , "scroller" ] ; if ( ! this . isProgressActive ) { classList . push ( "wide" ) ; } if ( this . isDriveListing ) { classList . push ( "drive-container" ) ; } return classList . join ( " " ) ; } printSourceFiles ( list ) { if ( typeof list [ 0 ] == "string" ) { return list ?. join ( ", " ) ; } if ( typeof list [ 0 ] == "object" ) { return list ?. map ( ( item ) => item . name ) ?. join ( ", " ) ; } return "" ; } isItemCutForPaste ( index ) { let element = this . contents [ index ] ; return this . paste ?. type != "cut-paste" || this . paste ?. source ?. baseFolder != this . getCWD ( ) || this . paste ?. source ?. server != this . current_server ? false : this . paste ?. files ?. some ( ( item ) => { return item . name == element . name && item . isFolder == element . folder ; } ) ; } cutCopy ( type ) { let list = this . contextMenu . selectedItems . map ( ( item ) => { return { name : item . name , isFolder : item . folder } ; } ) ; this . paste = { type : type == "cut" ? fs . CUT_PASTE : fs . COPY_PASTE , source : { server : this . current_server , baseFolder : this . getCWD ( ) } , files : list } ; } isValidPaste ( src_base , target_base , files ) { src_base = this . parsePath ( src_base ) . join ( "/" ) ; target_base = this . parsePath ( target_base ) . join ( "/" ) ; if ( src_base == target_base ) { return { status : "warning" , message : "You cannot paste in same folder" } ; } let isInSourceFolder = files . filter ( item => item . isFolder ) . map ( item => this . parsePath ( `${ src_base }/${ item . name }` ) . join ( "/" ) ) . some ( new_path => target_base . startsWith ( new_path ) ) ; if ( isInSourceFolder ) { return { status : "warning" , message : "You cannot paste inside copied folder" } ; } let list = { files : [ ] , folders : [ ] } ; this . contents . forEach ( item => { list [ item . folder ? "folders" : "files" ] . push ( item . name ) ; } ) ; let doesAlreadyExist = files . some ( item => { return list [ item . isFolder ? "folders" : "files" ] . includes ( item . name ) ; } ) ; if ( doesAlreadyExist ) { return { status : "confirm" , message : "Some Files/Folders already exists" } ; } return { status : "success" } ; } getNonExistingItemsForPaste ( files ) { let list = { files : [ ] , folders : [ ] } ; this . contents . forEach ( item => { list [ item . folder ? "folders" : "files" ] . push ( item . name ) ; } ) ; files = files . filter ( item => { return ! list [ item . isFolder ? "folders" : "files" ] . includes ( item . name ) ; } ) ; return files ; } pasteIt ( ) { let paste_data = this . paste ; let targetBaseFolder = this . getCWD ( ) ; let isValid = this . isValidPaste ( paste_data . source . baseFolder , targetBaseFolder , paste_data . files ) ; if ( isValid . status == "warning" ) { this . toast ( "warning" , isValid . message || "Something went wrong" ) ; return ; } if ( isValid . status == "confirm" ) { this . showPopUp ( "confirm" , "Paste Conflict" , isValid . message || "Some Files/Folders already exists" , "paste" , "Skip" , ( data ) => { paste_data . files = this . getNonExistingItemsForPaste ( paste_data . files ) ; this . forcePaste ( paste_data . files ) ; this . closePopUp ( ) ; } , "Cancel" , ( data ) => { this . closePopUp ( ) ; } , true , "Overwrite" , ( data ) => { this . forcePaste ( paste_data . files ) ; this . closePopUp ( ) ; } ) ; } else { this . forcePaste ( paste_data . files ) ; } } forcePaste ( files ) { let paste_data = this . paste ; let status = socket . startBackgroundProcess ( paste_data . type , { source : paste_data . source , target : { server : this . current_server , baseFolder : this . getCWD ( ) } , files : files } ) ; if ( status === null ) { this . toast ( "error" , "Not Connected to Server, Reconnect" ) ; } else if ( paste_data . type == "cut-paste" ) { this . paste = null ; } } rename ( ) { let item = this . contextMenu ?. selectedItems [ 0 ] ; this . popUpElement . _results [ 0 ] . renameData = item . name ; this . showPopUp ( "rename" , `Rename ${ item . folder ? "Folder" : "File" }` , null , "rename" , "Rename" , ( data ) => { if ( data != null ) { if ( data != item . name ) { if ( this . contents . some ( existing => existing . name == data ) ) { this . toast ( "error" , `A file/folder exists with same name` ) ; } else { let status = socket . startBackgroundProcess ( fs . RENAME , { source : { server : this . current_server , baseFolder : this . getCWD ( ) } , files : [ item . name , data ] } ) ; if ( status === null ) { this . toast ( "error" , "Not Connected to Server, Reconnect" ) ; } } } this . closePopUp ( ) ; } } , "Cancel" , ( data ) => { this . closePopUp ( ) ; } , false ) ; } showSourceGit ( ) { window . open ( "https://github.com/27px/Remote-File-Manager" , "_blank" ) ; } showMyDevProfile ( ) { window . open ( "https://www.linkedin.com/in/27px/" , "_blank" ) ; } openFileInDefaultApp ( filename ) { if ( this . current_server != null ) { this . toast ( "warning" , "You cannot open a file in server, copy it and paste it to your local system to open" ) ; } else if ( this . inBrowser ) { this . toast ( "warning" , "Opening a file feature is not available in browser, try in the executable version" ) ; } else { let status = socket . startBackgroundProcess ( fs . OPEN_FILE , { source : { server : this . current_server , baseFolder : this . getCWD ( ) } , files : [ filename ] } ) ; if ( status === null ) { this . toast ( "error" , "Not Connected to Server, Reconnect" ) ; } } } }