519c411db0
* feat: sync and update with all changes from fork * refactor: extract clipboard copy logic into custom hook * fix: correct spelling of DEFAULT_REFRESH_FREQUENCY_MS in Stamps and WalletBalance providers * refactor(ui-tests): replace fixed sleeps with condition-based waits * fix: handle null values for size and granteeCount in infoGroups * fix(lint): add newline at end of file in useClipboardCopy hook * fix(ui-tests): page.goto URL * refactor: update import paths for useClipboardCopy --------- Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
656 lines
20 KiB
TypeScript
656 lines
20 KiB
TypeScript
import { PostageBatch } from '@ethersphere/bee-js'
|
|
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
|
|
import React, {
|
|
ReactElement,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react'
|
|
|
|
import { useSearch } from '../../../../pages/filemanager/SearchContext'
|
|
import { useView } from '../../../../pages/filemanager/ViewContext'
|
|
import { Context as FMContext } from '../../../../providers/FileManager'
|
|
import { Context as SettingsContext } from '../../../../providers/Settings'
|
|
import { FileAction, FileTransferType, TransferStatus, ViewType } from '../../constants/transfers'
|
|
import { useBulkActions } from '../../hooks/useBulkActions'
|
|
import { useContextMenu } from '../../hooks/useContextMenu'
|
|
import { useDragAndDrop } from '../../hooks/useDragAndDrop'
|
|
import { useFileFiltering } from '../../hooks/useFileFiltering'
|
|
import { SortDir, SortKey, useSorting } from '../../hooks/useSorting'
|
|
import { useTransfers } from '../../hooks/useTransfers'
|
|
import { handleDestroyAndForgetDrive } from '../../utils/bee'
|
|
import { Dir, getFileId, Point, safeSetState } from '../../utils/common'
|
|
import { computeContextMenuPosition } from '../../utils/ui'
|
|
import { ProgressDestroyModal } from '../DestroyDriveModal/DestroyDriveModal'
|
|
import { ErrorModal } from '../ErrorModal/ErrorModal'
|
|
import { FileProgressNotification } from '../FileProgressNotification/FileProgressNotification'
|
|
import { NotificationBar } from '../NotificationBar/NotificationBar'
|
|
|
|
import { FileBrowserContent } from './FileBrowserContent/FileBrowserContent'
|
|
import { FileBrowserHeader } from './FileBrowserHeader/FileBrowserHeader'
|
|
import { FileBrowserContextMenu } from './FileBrowserMenu/FileBrowserContextMenu'
|
|
import { FileBrowserTopBar } from './FileBrowserTopBar/FileBrowserTopBar'
|
|
import { FileBrowserModals } from './FileBrowserModals'
|
|
|
|
import './FileBrowser.scss'
|
|
|
|
function DestroyProgressModal({
|
|
isDestroying,
|
|
isProgressModalOpen,
|
|
currentDrive,
|
|
onMinimize,
|
|
}: {
|
|
isDestroying: boolean
|
|
isProgressModalOpen: boolean
|
|
currentDrive?: DriveInfo
|
|
onMinimize: () => void
|
|
}) {
|
|
if (isProgressModalOpen && isDestroying && currentDrive) {
|
|
return <ProgressDestroyModal drive={currentDrive} onMinimize={onMinimize} />
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function DestroyingOverlay({ isDestroying, onClick }: { isDestroying: boolean; onClick: () => void }) {
|
|
if (!isDestroying) return null
|
|
|
|
return (
|
|
<div className="fm-refresh-overlay" aria-busy="true" aria-live="polite">
|
|
<div
|
|
className="fm-refresh-content"
|
|
onClick={onClick}
|
|
style={{ cursor: 'pointer' }}
|
|
title="Click to show progress modal"
|
|
>
|
|
<div className="fm-mini-spinner" role="status" aria-label="Destroying drive…" />
|
|
<span className="fm-refresh-text">Destroying drive…</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ErrorModalBlock({
|
|
showError,
|
|
label,
|
|
onOk,
|
|
}: {
|
|
showError: boolean
|
|
label: string
|
|
onOk: () => void
|
|
}): ReactElement | null {
|
|
if (!showError) {
|
|
return null
|
|
}
|
|
|
|
return <ErrorModal label={label} onClick={onOk} />
|
|
}
|
|
|
|
const extractFilesFromClipboardEvent = (e: React.ClipboardEvent): File[] => {
|
|
const out: File[] = []
|
|
const items = e.clipboardData?.items ?? []
|
|
for (let i = 0; i < items.length; i++) {
|
|
const it = items[i]
|
|
|
|
if (it.kind === 'file') {
|
|
const f = it.getAsFile()
|
|
|
|
if (f) out.push(f)
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
interface FileBrowserProps {
|
|
errorMessage?: string
|
|
setErrorMessage?: (error: string) => void
|
|
}
|
|
|
|
type FileBrowserContextMenuBlockProps = {
|
|
showContext: boolean
|
|
contextRef: React.RefObject<HTMLDivElement | null>
|
|
safePos: { x: number; y: number }
|
|
dropDir: Dir
|
|
drives: DriveInfo[]
|
|
view: ViewType
|
|
bulk: ReturnType<typeof useBulkActions>
|
|
adminStamp: PostageBatch | undefined
|
|
doRefresh: () => void
|
|
onContextUploadFile: () => void
|
|
setConfirmBulkRestore: (b: boolean) => void
|
|
setShowBulkDeleteModal: (b: boolean) => void
|
|
setShowDestroyDriveModal: (b: boolean) => void
|
|
}
|
|
|
|
function FileBrowserContextMenuBlock({
|
|
showContext,
|
|
contextRef,
|
|
safePos,
|
|
dropDir,
|
|
drives,
|
|
view,
|
|
bulk,
|
|
adminStamp,
|
|
doRefresh,
|
|
onContextUploadFile,
|
|
setConfirmBulkRestore,
|
|
setShowBulkDeleteModal,
|
|
setShowDestroyDriveModal,
|
|
}: FileBrowserContextMenuBlockProps): ReactElement | null {
|
|
if (!showContext) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={contextRef}
|
|
className="fm-file-browser-context-menu fm-context-menu"
|
|
style={{ top: safePos.y, left: safePos.x }}
|
|
data-drop={dropDir}
|
|
onMouseDown={e => e.stopPropagation()}
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
<FileBrowserContextMenu
|
|
drives={drives}
|
|
view={view}
|
|
selectedFilesCount={bulk.selectedFiles.length}
|
|
onRefresh={doRefresh}
|
|
enableRefresh={Boolean(adminStamp)}
|
|
onUploadFile={onContextUploadFile}
|
|
onBulkDownload={() => bulk.bulkDownload(bulk.selectedFiles)}
|
|
onBulkRestore={() => setConfirmBulkRestore(true)}
|
|
onBulkDelete={() => setShowBulkDeleteModal(true)}
|
|
onBulkDestroy={() => setShowDestroyDriveModal(true)}
|
|
onBulkForget={() => bulk.bulkForget(bulk.selectedFiles)}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps): ReactElement {
|
|
const { showContext, pos, contextRef, handleContextMenu, handleCloseContext } = useContextMenu<HTMLDivElement>()
|
|
const { view, setActualItemView } = useView()
|
|
const { beeApi } = useContext(SettingsContext)
|
|
const { files, adminDrive, currentDrive, resync, drives, fm, showError, setShowError } = useContext(FMContext)
|
|
const {
|
|
uploadFiles,
|
|
isUploading,
|
|
uploadItems,
|
|
isDownloading,
|
|
downloadItems,
|
|
trackDownload,
|
|
conflictPortal,
|
|
cancelOrDismissUpload,
|
|
cancelOrDismissDownload,
|
|
dismissAllUploads,
|
|
dismissAllDownloads,
|
|
} = useTransfers({ setErrorMessage })
|
|
|
|
const { query, scope, includeActive, includeTrashed } = useSearch()
|
|
|
|
const [safePos, setSafePos] = useState<Point>(pos)
|
|
const [dropDir, setDropDir] = useState<Dir>(Dir.Down)
|
|
|
|
const legacyUploadRef = useRef<HTMLInputElement | null>(null)
|
|
const contentRef = useRef<HTMLDivElement | null>(null)
|
|
const bodyRef = useRef<HTMLDivElement | null>(null)
|
|
const isMountedRef = useRef(true)
|
|
const rafIdRef = useRef<number | null>(null)
|
|
|
|
const [showBulkDeleteModal, setShowBulkDeleteModal] = useState(false)
|
|
const [showDestroyDriveModal, setShowDestroyDriveModal] = useState(false)
|
|
const [isDestroying, setIsDestroying] = useState(false)
|
|
const [isProgressModalOpen, setIsProgressModalOpen] = useState(false)
|
|
const [confirmBulkForget, setConfirmBulkForget] = useState(false)
|
|
const [confirmBulkRestore, setConfirmBulkRestore] = useState(false)
|
|
const [isRefreshing, setIsRefreshing] = useState(false)
|
|
const [pendingCancelUpload, setPendingCancelUpload] = useState<string | null>(null)
|
|
|
|
const q = query.trim().toLowerCase()
|
|
const isSearchMode = q.length > 0
|
|
|
|
const getDriveName = useCallback(
|
|
(driveId: string): string => {
|
|
const match = drives.find(d => d.id.toString() === driveId)
|
|
|
|
return match?.name ?? ''
|
|
},
|
|
[drives],
|
|
)
|
|
|
|
const openTopbarMenu = (anchorEl: HTMLElement) => {
|
|
const r = anchorEl.getBoundingClientRect()
|
|
const bodyRect = bodyRef.current?.getBoundingClientRect()
|
|
const clickX = Math.round(r.right - 6)
|
|
const minY = (bodyRect?.top ?? 0) + 8
|
|
const clickY = Math.max(Math.round(r.bottom + 6), minY)
|
|
const fakeEvt = {
|
|
preventDefault: () => {},
|
|
stopPropagation: () => {},
|
|
clientX: clickX,
|
|
clientY: clickY,
|
|
} as React.MouseEvent<HTMLDivElement>
|
|
handleContextMenu(fakeEvt)
|
|
}
|
|
|
|
const { listToRender } = useFileFiltering({
|
|
files,
|
|
currentDrive: currentDrive || null,
|
|
view,
|
|
isSearchMode,
|
|
query: q,
|
|
scope,
|
|
includeActive,
|
|
includeTrashed,
|
|
})
|
|
|
|
const { sorted, sort, toggle, reset } = useSorting(listToRender, {
|
|
persist: false,
|
|
defaultState: { key: SortKey.Timestamp, dir: SortDir.Desc },
|
|
getDriveName,
|
|
})
|
|
|
|
const sortedKey = sorted.map(f => getFileId(f)).join('|')
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
const stableSorted = useMemo(() => sorted, [sortedKey])
|
|
|
|
const bulk = useBulkActions({
|
|
listToRender,
|
|
trackDownload,
|
|
})
|
|
|
|
const { isDragging, handleDragEnter, handleDragOver, handleDragLeave, handleDrop, handleOverlayDrop } =
|
|
useDragAndDrop({
|
|
onFilesDropped: uploadFiles,
|
|
currentDrive: currentDrive || null,
|
|
})
|
|
|
|
const onFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = e.target.files
|
|
|
|
if (files && files.length > 0) {
|
|
uploadFiles(files)
|
|
}
|
|
e.target.value = ''
|
|
}
|
|
|
|
const onContextUploadFile = () => {
|
|
const el = legacyUploadRef.current
|
|
|
|
if (!el) return
|
|
|
|
try {
|
|
if (typeof (el as HTMLInputElement).showPicker === 'function') {
|
|
;(el as HTMLInputElement).showPicker()
|
|
} else {
|
|
el.click()
|
|
}
|
|
} catch {
|
|
el.click()
|
|
}
|
|
|
|
requestAnimationFrame(() => handleCloseContext())
|
|
}
|
|
|
|
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
const files = extractFilesFromClipboardEvent(e)
|
|
|
|
if (files.length === 0) return
|
|
|
|
e.preventDefault()
|
|
uploadFiles(files)
|
|
}
|
|
|
|
const handleFileBrowserContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
const t = e.target as HTMLElement
|
|
|
|
if (t.closest('.fm-file-item-context-menu, .fm-file-browser-context-menu')) return
|
|
|
|
if (!e.shiftKey && t.closest('.fm-file-item-content')) return
|
|
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
handleContextMenu(e)
|
|
}
|
|
|
|
const handleDeleteModalProceed = async (action: FileAction) => {
|
|
setShowBulkDeleteModal(false)
|
|
|
|
if (action === FileAction.Trash) {
|
|
return await bulk.bulkTrash(bulk.selectedFiles)
|
|
}
|
|
|
|
if (action === FileAction.Forget) {
|
|
return setConfirmBulkForget(true)
|
|
}
|
|
|
|
if (action === FileAction.Destroy) {
|
|
return setShowDestroyDriveModal(true)
|
|
}
|
|
}
|
|
|
|
const handleDestroyDriveConfirm = useCallback(async () => {
|
|
if (!currentDrive) return
|
|
|
|
setShowDestroyDriveModal(false)
|
|
setIsProgressModalOpen(true)
|
|
setIsDestroying(true)
|
|
|
|
await handleDestroyAndForgetDrive({
|
|
beeApi,
|
|
fm,
|
|
drive: currentDrive,
|
|
isDestroy: true,
|
|
adminDrive,
|
|
onSuccess: () => {
|
|
setIsDestroying(false)
|
|
setIsProgressModalOpen(false)
|
|
setShowDestroyDriveModal(false)
|
|
},
|
|
onError: e => {
|
|
setIsDestroying(false)
|
|
setIsProgressModalOpen(false)
|
|
setShowDestroyDriveModal(false)
|
|
setErrorMessage?.(`Error destroying drive: ${currentDrive.name}: ${e}`)
|
|
setShowError(true)
|
|
},
|
|
})
|
|
}, [
|
|
beeApi,
|
|
fm,
|
|
currentDrive,
|
|
adminDrive,
|
|
setErrorMessage,
|
|
setIsProgressModalOpen,
|
|
setShowDestroyDriveModal,
|
|
setShowError,
|
|
])
|
|
|
|
const handleUploadClose = (uuid: string) => {
|
|
const row = uploadItems.find(i => i.uuid === uuid)
|
|
|
|
if (row?.status === TransferStatus.Uploading) {
|
|
setPendingCancelUpload(uuid)
|
|
} else {
|
|
cancelOrDismissUpload(uuid)
|
|
}
|
|
}
|
|
|
|
const updateContextMenuPosition = () => {
|
|
const menu = contextRef.current
|
|
const body = bodyRef.current
|
|
|
|
if (!menu) return
|
|
|
|
const rect = menu.getBoundingClientRect()
|
|
const containerRect = body?.getBoundingClientRect() ?? null
|
|
|
|
const { safePos: sp, dropDir: dd } = computeContextMenuPosition({
|
|
clickPos: pos,
|
|
menuRect: rect,
|
|
viewport: { w: window.innerWidth, h: window.innerHeight },
|
|
margin: 8,
|
|
containerRect,
|
|
})
|
|
|
|
const topLeft = containerRect
|
|
? { x: Math.round(sp.x - containerRect.left), y: Math.round(sp.y - containerRect.top + 2) }
|
|
: sp
|
|
|
|
setSafePos(topLeft)
|
|
setDropDir(dd)
|
|
rafIdRef.current = null
|
|
}
|
|
|
|
useLayoutEffect(() => {
|
|
if (!showContext) return
|
|
|
|
if (rafIdRef.current) {
|
|
cancelAnimationFrame(rafIdRef.current)
|
|
}
|
|
|
|
rafIdRef.current = requestAnimationFrame(updateContextMenuPosition)
|
|
|
|
return () => {
|
|
if (rafIdRef.current) {
|
|
cancelAnimationFrame(rafIdRef.current)
|
|
rafIdRef.current = null
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [showContext, pos, contextRef])
|
|
|
|
useEffect(() => {
|
|
let title = currentDrive?.name || ''
|
|
|
|
if (isSearchMode) {
|
|
title = 'Search results'
|
|
|
|
if (scope === 'selected' && currentDrive?.name) {
|
|
title += ` — ${currentDrive.name}`
|
|
}
|
|
}
|
|
|
|
setActualItemView?.(title)
|
|
}, [isSearchMode, scope, currentDrive, setActualItemView])
|
|
|
|
useEffect(() => {
|
|
if (!isSearchMode) {
|
|
bulk.clearAll()
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [isSearchMode])
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
isMountedRef.current = false
|
|
|
|
if (rafIdRef.current) {
|
|
cancelAnimationFrame(rafIdRef.current)
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
const doRefresh = async () => {
|
|
handleCloseContext()
|
|
|
|
if (isRefreshing) return
|
|
|
|
setIsRefreshing(true)
|
|
|
|
try {
|
|
await resync()
|
|
} finally {
|
|
safeSetState(isMountedRef, setIsRefreshing)(false)
|
|
}
|
|
}
|
|
|
|
const showDeleteModal = showBulkDeleteModal && bulk.selectedFiles.length > 0 && view === ViewType.File
|
|
const showDragOverlay = isDragging && Boolean(currentDrive)
|
|
const fileCountText = bulk.selectedFiles.length === 1 ? 'file' : 'files'
|
|
|
|
const onBulk = useMemo(
|
|
() => ({
|
|
download: () => bulk.bulkDownload(bulk.selectedFiles),
|
|
restore: () => setConfirmBulkRestore(true),
|
|
forget: () => bulk.bulkForget(bulk.selectedFiles),
|
|
destroy: () => setShowDestroyDriveModal(true),
|
|
delete: () => setShowBulkDeleteModal(true),
|
|
}),
|
|
[bulk],
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{conflictPortal}
|
|
|
|
<input type="file" ref={legacyUploadRef} style={{ display: 'none' }} onChange={onFileSelected} multiple />
|
|
<input type="file" ref={bulk.fileInputRef} style={{ display: 'none' }} onChange={onFileSelected} multiple />
|
|
|
|
<div className="fm-file-browser-container" data-search-mode={isSearchMode ? 'true' : 'false'}>
|
|
<FileBrowserTopBar onOpenMenu={openTopbarMenu} canOpen={!isSearchMode && Boolean(currentDrive)} />
|
|
<div
|
|
className="fm-file-browser-content"
|
|
data-search-mode={isSearchMode ? 'true' : 'false'}
|
|
ref={contentRef}
|
|
onDragEnter={handleDragEnter}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onPaste={handlePaste}
|
|
onContextMenu={handleFileBrowserContextMenu}
|
|
>
|
|
<FileBrowserHeader
|
|
key={isSearchMode ? 'hdr-search' : 'hdr-normal'}
|
|
isSearchMode={isSearchMode}
|
|
bulk={bulk}
|
|
sortKey={sort.key}
|
|
sortDir={sort.dir}
|
|
onSortName={() => toggle(SortKey.Name)}
|
|
onSortSize={() => toggle(SortKey.Size)}
|
|
onSortDate={() => toggle(SortKey.Timestamp)}
|
|
onSortDrive={() => toggle(SortKey.Drive)}
|
|
onClearSort={reset}
|
|
/>
|
|
<div
|
|
className="fm-file-browser-content-body"
|
|
ref={bodyRef}
|
|
onMouseDown={e => {
|
|
if (e.button !== 0) return
|
|
handleCloseContext()
|
|
}}
|
|
>
|
|
<FileBrowserContent
|
|
key={isSearchMode ? `content-search` : `content-${currentDrive?.id.toString() ?? 'none'}`}
|
|
listToRender={stableSorted}
|
|
drives={drives}
|
|
currentDrive={currentDrive || null}
|
|
view={view}
|
|
isSearchMode={isSearchMode}
|
|
trackDownload={trackDownload}
|
|
selectedIds={bulk.selectedIds}
|
|
onToggleSelected={bulk.toggleOne}
|
|
bulkSelectedCount={bulk.selectedCount}
|
|
onBulk={onBulk}
|
|
setErrorMessage={setErrorMessage}
|
|
/>
|
|
<ErrorModalBlock
|
|
showError={Boolean(showError)}
|
|
label={errorMessage || 'An error occurred'}
|
|
onOk={() => {
|
|
setShowError(false)
|
|
setErrorMessage?.('')
|
|
|
|
return
|
|
}}
|
|
/>
|
|
|
|
<FileBrowserContextMenuBlock
|
|
showContext={showContext}
|
|
contextRef={contextRef}
|
|
safePos={safePos}
|
|
dropDir={dropDir}
|
|
drives={drives}
|
|
view={view}
|
|
bulk={bulk}
|
|
adminStamp={fm?.adminStamp}
|
|
doRefresh={doRefresh}
|
|
onContextUploadFile={onContextUploadFile}
|
|
setConfirmBulkRestore={setConfirmBulkRestore}
|
|
setShowBulkDeleteModal={setShowBulkDeleteModal}
|
|
setShowDestroyDriveModal={setShowDestroyDriveModal}
|
|
/>
|
|
</div>
|
|
|
|
{showDragOverlay && (
|
|
<div
|
|
className="fm-drag-overlay"
|
|
onDragOver={e => {
|
|
e.preventDefault()
|
|
e.dataTransfer.dropEffect = 'copy'
|
|
}}
|
|
onDrop={handleOverlayDrop}
|
|
>
|
|
<div className="fm-drag-text">Drop file(s) to upload</div>
|
|
</div>
|
|
)}
|
|
|
|
<FileBrowserModals
|
|
showDeleteModal={showDeleteModal}
|
|
selectedFiles={bulk.selectedFiles}
|
|
fileCountText={fileCountText}
|
|
currentDrive={currentDrive || null}
|
|
confirmBulkForget={confirmBulkForget}
|
|
confirmBulkRestore={confirmBulkRestore}
|
|
showDestroyDriveModal={showDestroyDriveModal}
|
|
pendingCancelUpload={pendingCancelUpload}
|
|
onDeleteCancel={() => setShowBulkDeleteModal(false)}
|
|
onDeleteProceed={handleDeleteModalProceed}
|
|
onForgetConfirm={async () => {
|
|
await bulk.bulkForget(bulk.selectedFiles)
|
|
setConfirmBulkForget(false)
|
|
}}
|
|
onForgetCancel={() => setConfirmBulkForget(false)}
|
|
onRestoreConfirm={async () => {
|
|
await bulk.bulkRestore(bulk.selectedFiles)
|
|
setConfirmBulkRestore(false)
|
|
}}
|
|
onRestoreCancel={() => setConfirmBulkRestore(false)}
|
|
onDestroyCancel={() => setShowDestroyDriveModal(false)}
|
|
onDestroyConfirm={handleDestroyDriveConfirm}
|
|
onCancelUploadConfirm={() => {
|
|
if (pendingCancelUpload) {
|
|
cancelOrDismissUpload(pendingCancelUpload)
|
|
setPendingCancelUpload(null)
|
|
}
|
|
}}
|
|
onCancelUploadCancel={() => setPendingCancelUpload(null)}
|
|
/>
|
|
|
|
{isRefreshing && (
|
|
<div className="fm-refresh-overlay" aria-busy="true" aria-live="polite">
|
|
<div className="fm-refresh-content">
|
|
<div className="fm-mini-spinner" role="status" aria-label="Syncing…" />
|
|
<span className="fm-refresh-text">Syncing latest files…</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<DestroyingOverlay isDestroying={isDestroying} onClick={() => setIsProgressModalOpen(true)} />
|
|
<DestroyProgressModal
|
|
isDestroying={isDestroying}
|
|
isProgressModalOpen={isProgressModalOpen}
|
|
currentDrive={currentDrive}
|
|
onMinimize={() => setIsProgressModalOpen(false)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="fm-file-browser-footer">
|
|
<FileProgressNotification
|
|
label="Uploading files"
|
|
type={FileTransferType.Upload}
|
|
open={isUploading}
|
|
items={uploadItems}
|
|
onRowClose={handleUploadClose}
|
|
onCloseAll={() => dismissAllUploads()}
|
|
/>
|
|
<FileProgressNotification
|
|
label="Downloading files"
|
|
type={FileTransferType.Download}
|
|
open={isDownloading}
|
|
items={downloadItems}
|
|
onRowClose={(name: string) => cancelOrDismissDownload(name)}
|
|
onCloseAll={() => dismissAllDownloads()}
|
|
/>
|
|
<NotificationBar setErrorMessage={setErrorMessage} />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|