fix: upload q and drive size error (#220)
* refactor: usetransfers hook functions * refactor: bulk actions code style and readability * fix: use upload q inflight size for subsequent uploads * refactor: runUploadQueue execution
This commit is contained in:
@@ -16,7 +16,7 @@ import { useView } from '../../../../pages/filemanager/ViewContext'
|
|||||||
import { Context as FMContext } from '../../../../providers/FileManager'
|
import { Context as FMContext } from '../../../../providers/FileManager'
|
||||||
import { Context as SettingsContext } from '../../../../providers/Settings'
|
import { Context as SettingsContext } from '../../../../providers/Settings'
|
||||||
import { FileAction, FileTransferType, TransferStatus, ViewType } from '../../constants/transfers'
|
import { FileAction, FileTransferType, TransferStatus, ViewType } from '../../constants/transfers'
|
||||||
import { useBulkActions } from '../../hooks/useBulkActions'
|
import { BulkActionsResult, useBulkActions } from '../../hooks/useBulkActions'
|
||||||
import { useContextMenu } from '../../hooks/useContextMenu'
|
import { useContextMenu } from '../../hooks/useContextMenu'
|
||||||
import { useDragAndDrop } from '../../hooks/useDragAndDrop'
|
import { useDragAndDrop } from '../../hooks/useDragAndDrop'
|
||||||
import { useFileFiltering } from '../../hooks/useFileFiltering'
|
import { useFileFiltering } from '../../hooks/useFileFiltering'
|
||||||
@@ -118,7 +118,7 @@ type FileBrowserContextMenuBlockProps = {
|
|||||||
dropDir: Dir
|
dropDir: Dir
|
||||||
drives: DriveInfo[]
|
drives: DriveInfo[]
|
||||||
view: ViewType
|
view: ViewType
|
||||||
bulk: ReturnType<typeof useBulkActions>
|
bulk: BulkActionsResult
|
||||||
adminStamp: PostageBatch | undefined
|
adminStamp: PostageBatch | undefined
|
||||||
doRefresh: () => void
|
doRefresh: () => void
|
||||||
onContextUploadFile: () => void
|
onContextUploadFile: () => void
|
||||||
@@ -162,11 +162,11 @@ function FileBrowserContextMenuBlock({
|
|||||||
onRefresh={doRefresh}
|
onRefresh={doRefresh}
|
||||||
enableRefresh={Boolean(adminStamp)}
|
enableRefresh={Boolean(adminStamp)}
|
||||||
onUploadFile={onContextUploadFile}
|
onUploadFile={onContextUploadFile}
|
||||||
onBulkDownload={() => bulk.bulkDownload(bulk.selectedFiles)}
|
onBulkDownload={() => bulk.download(bulk.selectedFiles)}
|
||||||
onBulkRestore={() => setConfirmBulkRestore(true)}
|
onBulkRestore={() => setConfirmBulkRestore(true)}
|
||||||
onBulkDelete={() => setShowBulkDeleteModal(true)}
|
onBulkDelete={() => setShowBulkDeleteModal(true)}
|
||||||
onBulkDestroy={() => setShowDestroyDriveModal(true)}
|
onBulkDestroy={() => setShowDestroyDriveModal(true)}
|
||||||
onBulkForget={() => bulk.bulkForget(bulk.selectedFiles)}
|
onBulkForget={() => bulk.forget(bulk.selectedFiles)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -196,7 +196,6 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
|||||||
const [safePos, setSafePos] = useState<Point>(pos)
|
const [safePos, setSafePos] = useState<Point>(pos)
|
||||||
const [dropDir, setDropDir] = useState<Dir>(Dir.Down)
|
const [dropDir, setDropDir] = useState<Dir>(Dir.Down)
|
||||||
|
|
||||||
const legacyUploadRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
const contentRef = useRef<HTMLDivElement | null>(null)
|
const contentRef = useRef<HTMLDivElement | null>(null)
|
||||||
const bodyRef = useRef<HTMLDivElement | null>(null)
|
const bodyRef = useRef<HTMLDivElement | null>(null)
|
||||||
const isMountedRef = useRef(true)
|
const isMountedRef = useRef(true)
|
||||||
@@ -281,20 +280,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onContextUploadFile = () => {
|
const onContextUploadFile = () => {
|
||||||
const el = legacyUploadRef.current
|
bulk.uploadFromPicker()
|
||||||
|
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (typeof (el as HTMLInputElement).showPicker === 'function') {
|
|
||||||
;(el as HTMLInputElement).showPicker()
|
|
||||||
} else {
|
|
||||||
el.click()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
el.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(() => handleCloseContext())
|
requestAnimationFrame(() => handleCloseContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +309,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
|||||||
setShowBulkDeleteModal(false)
|
setShowBulkDeleteModal(false)
|
||||||
|
|
||||||
if (action === FileAction.Trash) {
|
if (action === FileAction.Trash) {
|
||||||
return await bulk.bulkTrash(bulk.selectedFiles)
|
return await bulk.trash(bulk.selectedFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === FileAction.Forget) {
|
if (action === FileAction.Forget) {
|
||||||
@@ -489,9 +475,9 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
|||||||
|
|
||||||
const onBulk = useMemo(
|
const onBulk = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
download: () => bulk.bulkDownload(bulk.selectedFiles),
|
download: () => bulk.download(bulk.selectedFiles),
|
||||||
restore: () => setConfirmBulkRestore(true),
|
restore: () => setConfirmBulkRestore(true),
|
||||||
forget: () => bulk.bulkForget(bulk.selectedFiles),
|
forget: () => bulk.forget(bulk.selectedFiles),
|
||||||
destroy: () => setShowDestroyDriveModal(true),
|
destroy: () => setShowDestroyDriveModal(true),
|
||||||
delete: () => setShowBulkDeleteModal(true),
|
delete: () => setShowBulkDeleteModal(true),
|
||||||
}),
|
}),
|
||||||
@@ -502,7 +488,6 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
|||||||
<>
|
<>
|
||||||
{conflictPortal}
|
{conflictPortal}
|
||||||
|
|
||||||
<input type="file" ref={legacyUploadRef} style={{ display: 'none' }} onChange={onFileSelected} multiple />
|
|
||||||
<input type="file" ref={bulk.fileInputRef} 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'}>
|
<div className="fm-file-browser-container" data-search-mode={isSearchMode ? 'true' : 'false'}>
|
||||||
@@ -605,12 +590,12 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
|||||||
onDeleteCancel={() => setShowBulkDeleteModal(false)}
|
onDeleteCancel={() => setShowBulkDeleteModal(false)}
|
||||||
onDeleteProceed={handleDeleteModalProceed}
|
onDeleteProceed={handleDeleteModalProceed}
|
||||||
onForgetConfirm={async () => {
|
onForgetConfirm={async () => {
|
||||||
await bulk.bulkForget(bulk.selectedFiles)
|
await bulk.forget(bulk.selectedFiles)
|
||||||
setConfirmBulkForget(false)
|
setConfirmBulkForget(false)
|
||||||
}}
|
}}
|
||||||
onForgetCancel={() => setConfirmBulkForget(false)}
|
onForgetCancel={() => setConfirmBulkForget(false)}
|
||||||
onRestoreConfirm={async () => {
|
onRestoreConfirm={async () => {
|
||||||
await bulk.bulkRestore(bulk.selectedFiles)
|
await bulk.restore(bulk.selectedFiles)
|
||||||
setConfirmBulkRestore(false)
|
setConfirmBulkRestore(false)
|
||||||
}}
|
}}
|
||||||
onRestoreCancel={() => setConfirmBulkRestore(false)}
|
onRestoreCancel={() => setConfirmBulkRestore(false)}
|
||||||
|
|||||||
+2
-2
@@ -1,14 +1,14 @@
|
|||||||
import { ReactElement } from 'react'
|
import { ReactElement } from 'react'
|
||||||
import DownIcon from 'remixicon-react/ArrowDownSLineIcon'
|
import DownIcon from 'remixicon-react/ArrowDownSLineIcon'
|
||||||
|
|
||||||
import { useBulkActions } from '../../../hooks/useBulkActions'
|
import { BulkActionsResult } from '../../../hooks/useBulkActions'
|
||||||
import { SortDir, SortKey } from '../../../hooks/useSorting'
|
import { SortDir, SortKey } from '../../../hooks/useSorting'
|
||||||
|
|
||||||
import { capitalizeFirstLetter } from '@/modules/filemanager/utils/common'
|
import { capitalizeFirstLetter } from '@/modules/filemanager/utils/common'
|
||||||
|
|
||||||
interface FileBrowserHeaderProps {
|
interface FileBrowserHeaderProps {
|
||||||
isSearchMode: boolean
|
isSearchMode: boolean
|
||||||
bulk: ReturnType<typeof useBulkActions>
|
bulk: BulkActionsResult
|
||||||
sortKey: SortKey
|
sortKey: SortKey
|
||||||
sortDir: SortDir
|
sortDir: SortDir
|
||||||
onSortName: () => void
|
onSortName: () => void
|
||||||
|
|||||||
@@ -435,7 +435,15 @@ export function VersionsList({ versions, headFi, restoreVersion, onDownload }: V
|
|||||||
await startDownloadingQueue(
|
await startDownloadingQueue(
|
||||||
fm,
|
fm,
|
||||||
[{ uuid, info: fileInfo }],
|
[{ uuid, info: fileInfo }],
|
||||||
[onDownload({ uuid, name: fileInfo.name, size: formatBytes(rawSize), expectedSize, driveName })],
|
[
|
||||||
|
onDownload({
|
||||||
|
uuid,
|
||||||
|
name: fileInfo.name,
|
||||||
|
size: formatBytes(rawSize),
|
||||||
|
expectedSize,
|
||||||
|
driveName: driveName ?? 'unknown',
|
||||||
|
}),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[handleCloseContext, fm, beeApi, onDownload, drives, currentDrive],
|
[handleCloseContext, fm, beeApi, onDownload, drives, currentDrive],
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export type TrackDownloadProps = {
|
|||||||
name: string
|
name: string
|
||||||
size?: string
|
size?: string
|
||||||
expectedSize?: number
|
expectedSize?: number
|
||||||
driveName?: string
|
driveName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadProgress {
|
export interface DownloadProgress {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PostageBatch } from '@ethersphere/bee-js'
|
import { PostageBatch } from '@ethersphere/bee-js'
|
||||||
import type { FileInfo } from '@solarpunkltd/file-manager-lib'
|
import type { FileInfo } from '@solarpunkltd/file-manager-lib'
|
||||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
import { type RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { Context as FMContext } from '../../../providers/FileManager'
|
import { Context as FMContext } from '../../../providers/FileManager'
|
||||||
import { Context as SettingsContext } from '../../../providers/Settings'
|
import { Context as SettingsContext } from '../../../providers/Settings'
|
||||||
@@ -9,16 +9,33 @@ import { DownloadProgress, TrackDownloadProps } from '../constants/transfers'
|
|||||||
import { getUsableStamps } from '../utils/bee'
|
import { getUsableStamps } from '../utils/bee'
|
||||||
import { formatBytes, getFileId, safeSetState } from '../utils/common'
|
import { formatBytes, getFileId, safeSetState } from '../utils/common'
|
||||||
import { FileInfoWithUUID, startDownloadingQueue } from '../utils/download'
|
import { FileInfoWithUUID, startDownloadingQueue } from '../utils/download'
|
||||||
import { FileOperation, performBulkFileOperation } from '../utils/fileOperations'
|
import { FileOperation, isElementPickerSupported, performBulkFileOperation } from '../utils/fileOperations'
|
||||||
|
|
||||||
interface BulkOptions {
|
export interface BulkOptions {
|
||||||
listToRender: FileInfo[]
|
listToRender: FileInfo[]
|
||||||
trackDownload: (props: TrackDownloadProps) => (dp: DownloadProgress) => void
|
trackDownload: (props: TrackDownloadProps) => (dp: DownloadProgress) => void
|
||||||
setErrorMessage?: (error: string) => void
|
setErrorMessage?: (error: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBulkActions({ listToRender, setErrorMessage, trackDownload }: BulkOptions) {
|
export interface BulkActionsResult {
|
||||||
const { fm, adminDrive, drives, refreshStamp, setShowError } = useContext(FMContext)
|
selectedIds: Set<string>
|
||||||
|
selectedFiles: FileInfo[]
|
||||||
|
selectedCount: number
|
||||||
|
allChecked: boolean
|
||||||
|
someChecked: boolean
|
||||||
|
fileInputRef: RefObject<HTMLInputElement | null>
|
||||||
|
toggleOne: (fi: FileInfo, checked: boolean) => void
|
||||||
|
selectAll: () => void
|
||||||
|
clearAll: () => void
|
||||||
|
uploadFromPicker: () => void
|
||||||
|
download: (list: FileInfo[]) => Promise<void>
|
||||||
|
trash: (list: FileInfo[]) => Promise<void>
|
||||||
|
restore: (list: FileInfo[]) => Promise<void>
|
||||||
|
forget: (list: FileInfo[]) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBulkActions({ listToRender, trackDownload, setErrorMessage }: BulkOptions): BulkActionsResult {
|
||||||
|
const { fm, adminDrive, currentDrive, drives, refreshStamp, setShowError } = useContext(FMContext)
|
||||||
const { beeApi } = useContext(SettingsContext)
|
const { beeApi } = useContext(SettingsContext)
|
||||||
|
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||||
@@ -69,11 +86,23 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
|
|||||||
const selectAll = useCallback(() => setSelectedIds(new Set(allIds)), [allIds])
|
const selectAll = useCallback(() => setSelectedIds(new Set(allIds)), [allIds])
|
||||||
const clearAll = useCallback(() => setSelectedIds(new Set()), [])
|
const clearAll = useCallback(() => setSelectedIds(new Set()), [])
|
||||||
|
|
||||||
const bulkUploadFromPicker = useCallback(() => {
|
const uploadFromPicker = useCallback(() => {
|
||||||
fileInputRef.current?.click()
|
const el = fileInputRef.current
|
||||||
|
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isElementPickerSupported(el)) {
|
||||||
|
el.showPicker()
|
||||||
|
} else {
|
||||||
|
el.click()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
el.click()
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const bulkDownload = useCallback(
|
const download = useCallback(
|
||||||
async (list: FileInfo[]) => {
|
async (list: FileInfo[]) => {
|
||||||
if (!fm || !list?.length) return
|
if (!fm || !list?.length) return
|
||||||
|
|
||||||
@@ -85,7 +114,7 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
|
|||||||
const rawSize = fi.customMetadata?.size as string | number | undefined
|
const rawSize = fi.customMetadata?.size as string | number | undefined
|
||||||
const prettySize = formatBytes(rawSize)
|
const prettySize = formatBytes(rawSize)
|
||||||
const expected = rawSize ? Number(rawSize) : undefined
|
const expected = rawSize ? Number(rawSize) : undefined
|
||||||
const driveName = drives.find(d => d.id.toString() === fi.driveId.toString())?.name
|
const driveName = drives.find(d => d.id.toString() === fi.driveId.toString())?.name ?? currentDrive?.name
|
||||||
const uuid = uuidV4()
|
const uuid = uuidV4()
|
||||||
|
|
||||||
infoListWitIDs[i] = { uuid, info: fi }
|
infoListWitIDs[i] = { uuid, info: fi }
|
||||||
@@ -94,16 +123,16 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
|
|||||||
name: fi.name,
|
name: fi.name,
|
||||||
size: prettySize,
|
size: prettySize,
|
||||||
expectedSize: expected,
|
expectedSize: expected,
|
||||||
driveName,
|
driveName: driveName ?? 'unknown',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await startDownloadingQueue(fm, infoListWitIDs, trackers)
|
await startDownloadingQueue(fm, infoListWitIDs, trackers)
|
||||||
},
|
},
|
||||||
[fm, trackDownload, drives],
|
[fm, currentDrive, trackDownload, drives],
|
||||||
)
|
)
|
||||||
|
|
||||||
const bulkTrash = useCallback(
|
const trash = useCallback(
|
||||||
async (list: FileInfo[]) => {
|
async (list: FileInfo[]) => {
|
||||||
if (!fm || !list?.length) return
|
if (!fm || !list?.length) return
|
||||||
|
|
||||||
@@ -126,7 +155,7 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
|
|||||||
[fm, driveStamps, clearAll, refreshStamp, setErrorMessage, setShowError],
|
[fm, driveStamps, clearAll, refreshStamp, setErrorMessage, setShowError],
|
||||||
)
|
)
|
||||||
|
|
||||||
const bulkRestore = useCallback(
|
const restore = useCallback(
|
||||||
async (list: FileInfo[]) => {
|
async (list: FileInfo[]) => {
|
||||||
if (!fm || !list?.length) return
|
if (!fm || !list?.length) return
|
||||||
|
|
||||||
@@ -149,7 +178,7 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
|
|||||||
[fm, driveStamps, refreshStamp, clearAll, setErrorMessage, setShowError],
|
[fm, driveStamps, refreshStamp, clearAll, setErrorMessage, setShowError],
|
||||||
)
|
)
|
||||||
|
|
||||||
const bulkForget = useCallback(
|
const forget = useCallback(
|
||||||
async (list: FileInfo[]) => {
|
async (list: FileInfo[]) => {
|
||||||
if (!fm || !fm.adminStamp || !adminDrive || !list?.length) return
|
if (!fm || !fm.adminStamp || !adminDrive || !list?.length) return
|
||||||
|
|
||||||
@@ -176,26 +205,22 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
|
|||||||
[fm, adminDrive, driveStamps, clearAll, refreshStamp, setErrorMessage, setShowError],
|
[fm, adminDrive, driveStamps, clearAll, refreshStamp, setErrorMessage, setShowError],
|
||||||
)
|
)
|
||||||
|
|
||||||
return useMemo(
|
return useMemo<BulkActionsResult>(
|
||||||
() => ({
|
() => ({
|
||||||
// selection
|
|
||||||
selectedIds,
|
selectedIds,
|
||||||
setSelectedIds,
|
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
selectedCount,
|
selectedCount,
|
||||||
allChecked,
|
allChecked,
|
||||||
someChecked,
|
someChecked,
|
||||||
|
fileInputRef,
|
||||||
toggleOne,
|
toggleOne,
|
||||||
selectAll,
|
selectAll,
|
||||||
clearAll,
|
clearAll,
|
||||||
// file input (for bulk upload)
|
uploadFromPicker,
|
||||||
fileInputRef,
|
download,
|
||||||
bulkUploadFromPicker,
|
trash,
|
||||||
// actions
|
restore,
|
||||||
bulkDownload,
|
forget,
|
||||||
bulkTrash,
|
|
||||||
bulkRestore,
|
|
||||||
bulkForget,
|
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -206,11 +231,11 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
|
|||||||
toggleOne,
|
toggleOne,
|
||||||
selectAll,
|
selectAll,
|
||||||
clearAll,
|
clearAll,
|
||||||
bulkUploadFromPicker,
|
uploadFromPicker,
|
||||||
bulkDownload,
|
download,
|
||||||
bulkTrash,
|
trash,
|
||||||
bulkRestore,
|
restore,
|
||||||
bulkForget,
|
forget,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { FileInfo, FileInfoOptions, UploadProgress } from '@solarpunkltd/file-manager-lib'
|
import type { DriveInfo, FileInfo, FileInfoOptions, UploadProgress } from '@solarpunkltd/file-manager-lib'
|
||||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { Context as FMContext } from '../../../providers/FileManager'
|
import { Context as FMContext } from '../../../providers/FileManager'
|
||||||
import { Context as SettingsContext } from '../../../providers/Settings'
|
import { Context as SettingsContext } from '../../../providers/Settings'
|
||||||
@@ -27,7 +27,7 @@ const ABORT_EVENT = 'abort'
|
|||||||
type ResolveResult = {
|
type ResolveResult = {
|
||||||
cancelled: boolean
|
cancelled: boolean
|
||||||
finalName?: string
|
finalName?: string
|
||||||
isReplace?: boolean
|
isReplace: boolean
|
||||||
replaceTopic?: string
|
replaceTopic?: string
|
||||||
replaceHistory?: string
|
replaceHistory?: string
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,18 @@ type UploadTask = {
|
|||||||
driveName: string
|
driveName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNameInvalid = (
|
||||||
|
finalName: string,
|
||||||
|
isReplace: boolean,
|
||||||
|
replaceHistory?: string,
|
||||||
|
replaceTopic?: string,
|
||||||
|
): boolean => {
|
||||||
|
const invalidCombo = isReplace && (!replaceHistory || !replaceTopic)
|
||||||
|
const invalidName = !finalName || finalName.trim().length === 0
|
||||||
|
|
||||||
|
return invalidCombo || invalidName
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeCustomMetadata = (meta: UploadMeta): Record<string, string> => {
|
const normalizeCustomMetadata = (meta: UploadMeta): Record<string, string> => {
|
||||||
const out: Record<string, string> = {}
|
const out: Record<string, string> = {}
|
||||||
for (const [k, v] of Object.entries(meta)) out[k] = typeof v === 'string' ? v : String(v)
|
for (const [k, v] of Object.entries(meta)) out[k] = typeof v === 'string' ? v : String(v)
|
||||||
@@ -175,7 +187,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
|
|
||||||
const isMountedRef = useRef(true)
|
const isMountedRef = useRef(true)
|
||||||
const uploadAbortsRef = useRef<AbortManager>(new AbortManager())
|
const uploadAbortsRef = useRef<AbortManager>(new AbortManager())
|
||||||
const queueRef = useRef<UploadTask[]>([])
|
const uploadTaskQueueRef = useRef<UploadTask[]>([])
|
||||||
const runningRef = useRef(false)
|
const runningRef = useRef(false)
|
||||||
const cancelledQueuedRef = useRef<Set<string>>(new Set())
|
const cancelledQueuedRef = useRef<Set<string>>(new Set())
|
||||||
const cancelledUploadingRef = useRef<Set<string>>(new Set())
|
const cancelledUploadingRef = useRef<Set<string>>(new Set())
|
||||||
@@ -195,7 +207,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
cancelledQueuedRef.current.delete(uuid)
|
cancelledQueuedRef.current.delete(uuid)
|
||||||
cancelledUploadingRef.current.delete(uuid)
|
cancelledUploadingRef.current.delete(uuid)
|
||||||
uploadAbortsRef.current.abort(uuid)
|
uploadAbortsRef.current.abort(uuid)
|
||||||
queueRef.current = queueRef.current.filter(t => {
|
uploadTaskQueueRef.current = uploadTaskQueueRef.current.filter(t => {
|
||||||
return t.uuid !== uuid
|
return t.uuid !== uuid
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
@@ -250,7 +262,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (choice.action === ConflictAction.Cancel) {
|
if (choice.action === ConflictAction.Cancel) {
|
||||||
return { cancelled: true }
|
return { cancelled: true, isReplace: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (choice.action === ConflictAction.KeepBoth) {
|
if (choice.action === ConflictAction.KeepBoth) {
|
||||||
@@ -347,7 +359,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const processUploadTask = useCallback(
|
const executeUploadTask = useCallback(
|
||||||
async (task: UploadTask) => {
|
async (task: UploadTask) => {
|
||||||
if (!fm) return
|
if (!fm) return
|
||||||
|
|
||||||
@@ -447,16 +459,13 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
[fm, files, currentStamp, trackUpload, refreshStamp, setShowError, setErrorMessage],
|
[fm, files, currentStamp, trackUpload, refreshStamp, setShowError, setErrorMessage],
|
||||||
)
|
)
|
||||||
|
|
||||||
const trackDownload = useCallback(
|
const trackDownload = useCallback((props: TrackDownloadProps) => {
|
||||||
(props: TrackDownloadProps) => {
|
|
||||||
if (!isMountedRef.current) {
|
if (!isMountedRef.current) {
|
||||||
return () => {
|
return () => {
|
||||||
// No-op function for unmounted component
|
// No-op function for unmounted component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const driveName = props.driveName ?? currentDrive?.name
|
|
||||||
|
|
||||||
let startedAt: number | undefined
|
let startedAt: number | undefined
|
||||||
let etaState: ETAState = {
|
let etaState: ETAState = {
|
||||||
lastTs: undefined,
|
lastTs: undefined,
|
||||||
@@ -470,7 +479,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
props.name,
|
props.name,
|
||||||
props.size,
|
props.size,
|
||||||
FileTransferType.Download,
|
FileTransferType.Download,
|
||||||
driveName,
|
props.driveName,
|
||||||
TransferStatus.Downloading,
|
TransferStatus.Downloading,
|
||||||
)
|
)
|
||||||
row.startedAt = undefined
|
row.startedAt = undefined
|
||||||
@@ -519,8 +528,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
const elapsedSec = currentItem?.startedAt ? Math.round((finishedAt - currentItem.startedAt) / 1000) : 0
|
const elapsedSec = currentItem?.startedAt ? Math.round((finishedAt - currentItem.startedAt) / 1000) : 0
|
||||||
|
|
||||||
if (dp.state === DownloadState.Cancelled || dp.state === DownloadState.Error) {
|
if (dp.state === DownloadState.Cancelled || dp.state === DownloadState.Error) {
|
||||||
const wasCancelled =
|
const wasCancelled = dp.state === DownloadState.Cancelled || cancelledDownloadingRef.current.has(props.uuid)
|
||||||
dp.state === DownloadState.Cancelled || cancelledDownloadingRef.current.has(props.uuid)
|
|
||||||
|
|
||||||
cancelledDownloadingRef.current.delete(props.uuid)
|
cancelledDownloadingRef.current.delete(props.uuid)
|
||||||
|
|
||||||
@@ -543,39 +551,17 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return onProgress
|
return onProgress
|
||||||
},
|
}, [])
|
||||||
// currentDrive casues rerenders and flickering during the progress tracking
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const uploadFiles = useCallback(
|
const { allTaken, reserved, sameDrive } = useMemo(() => {
|
||||||
(picked: FileList | File[]): void => {
|
if (!currentDrive) {
|
||||||
const filesArr = Array.from(picked)
|
return { allTaken: new Set(''), reserved: new Set(''), sameDrive: [] }
|
||||||
|
|
||||||
if (filesArr.length === 0 || !fm || !currentDrive || !currentStamp) return
|
|
||||||
|
|
||||||
const currentlyQueued = queueRef.current.length
|
|
||||||
const newFilesCount = filesArr.length
|
|
||||||
const totalAfterAdd = currentlyQueued + newFilesCount
|
|
||||||
|
|
||||||
if (totalAfterAdd > MAX_UPLOAD_FILES) {
|
|
||||||
setErrorMessage?.(
|
|
||||||
`You’re trying to upload ${totalAfterAdd} files, but the limit is ${MAX_UPLOAD_FILES}. Please upload fewer files.`,
|
|
||||||
)
|
|
||||||
setShowError(true)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// TODO: move out this function from the cb and use as a util for better readaility
|
|
||||||
const preflight = async (): Promise<UploadTask[]> => {
|
const progressNames = new Set<string>(uploadItems.filter(u => u.driveName === currentDrive.name).map(u => u.name))
|
||||||
const progressNames = new Set<string>(
|
|
||||||
uploadItems.filter(u => u.driveName === currentDrive.name).map(u => u.name),
|
|
||||||
)
|
|
||||||
const sameDrive = collectSameDrive(currentDrive.id.toString())
|
const sameDrive = collectSameDrive(currentDrive.id.toString())
|
||||||
const onDiskNames = new Set<string>(sameDrive.map((fi: FileInfo) => fi.name))
|
const onDiskNames = new Set<string>(sameDrive.map((fi: FileInfo) => fi.name))
|
||||||
const reserved = new Set<string>()
|
const reserved = new Set<string>()
|
||||||
const tasks: UploadTask[] = []
|
|
||||||
|
|
||||||
const allTaken = new Set<string>([
|
const allTaken = new Set<string>([
|
||||||
...Array.from(onDiskNames),
|
...Array.from(onDiskNames),
|
||||||
@@ -583,56 +569,27 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
...Array.from(progressNames),
|
...Array.from(progressNames),
|
||||||
])
|
])
|
||||||
|
|
||||||
// Track cumulative file sizes for capacity verification
|
return {
|
||||||
let fileSizeSum = 0
|
allTaken,
|
||||||
let fileCount = 0
|
reserved,
|
||||||
|
sameDrive,
|
||||||
const processFile = async (file: File): Promise<UploadTask | null> => {
|
|
||||||
if (!currentStamp || !currentStamp.usable) {
|
|
||||||
setErrorMessage?.('Stamp is not usable.')
|
|
||||||
setShowError(true)
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
}, [currentDrive, collectSameDrive, uploadItems])
|
||||||
|
|
||||||
|
const createUploadTask = useCallback(
|
||||||
|
async (file: File, drive: DriveInfo): Promise<UploadTask | null> => {
|
||||||
const uuid = uuidV4()
|
const uuid = uuidV4()
|
||||||
|
|
||||||
const meta = buildUploadMeta([file])
|
const meta = buildUploadMeta([file])
|
||||||
const prettySize = formatBytes(meta.size)
|
const prettySize = formatBytes(meta.size)
|
||||||
|
|
||||||
fileSizeSum += file.size
|
let { finalName, isReplace, replaceTopic, replaceHistory } = await resolveConflict(file.name, sameDrive, allTaken)
|
||||||
fileCount += 1
|
finalName = finalName ?? ''
|
||||||
|
|
||||||
const { ok } = verifyDriveSpace({
|
if (isNameInvalid(finalName, isReplace, replaceHistory, replaceTopic)) {
|
||||||
fm,
|
|
||||||
redundancyLevel: currentDrive.redundancyLevel,
|
|
||||||
stamp: currentStamp,
|
|
||||||
useInfoSize: true,
|
|
||||||
driveId: currentDrive.id.toString(),
|
|
||||||
adminRedundancy: adminDrive?.redundancyLevel,
|
|
||||||
fileSize: fileSizeSum,
|
|
||||||
fileCount,
|
|
||||||
cb: err => {
|
|
||||||
setErrorMessage?.(err + ' (' + truncateNameMiddle(file.name) + ')')
|
|
||||||
setShowError(true)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!ok) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let { finalName, isReplace, replaceTopic, replaceHistory } = await resolveConflict(
|
|
||||||
file.name,
|
|
||||||
sameDrive,
|
|
||||||
allTaken,
|
|
||||||
)
|
|
||||||
finalName = finalName ?? ''
|
|
||||||
|
|
||||||
const invalidCombo = Boolean(isReplace) && (!replaceHistory || !replaceTopic)
|
|
||||||
const invalidName = !finalName || finalName.trim().length === 0
|
|
||||||
|
|
||||||
if (!invalidCombo && !invalidName) {
|
|
||||||
if (reserved.has(finalName)) {
|
if (reserved.has(finalName)) {
|
||||||
const retryTaken = new Set<string>([...Array.from(allTaken), finalName])
|
const retryTaken = new Set<string>([...Array.from(allTaken), finalName])
|
||||||
const retry = await resolveConflict(finalName, sameDrive, retryTaken)
|
const retry = await resolveConflict(finalName, sameDrive, retryTaken)
|
||||||
@@ -642,10 +599,10 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
replaceHistory = retry.replaceHistory
|
replaceHistory = retry.replaceHistory
|
||||||
}
|
}
|
||||||
|
|
||||||
const retryInvalidCombo = Boolean(isReplace) && (!replaceHistory || !replaceTopic)
|
if (isNameInvalid(finalName, isReplace, replaceHistory, replaceTopic)) {
|
||||||
const retryInvalidName = !finalName || finalName.trim().length === 0
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (!retryInvalidCombo && !retryInvalidName) {
|
|
||||||
reserved.add(finalName)
|
reserved.add(finalName)
|
||||||
|
|
||||||
ensureQueuedRow(
|
ensureQueuedRow(
|
||||||
@@ -653,7 +610,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
finalName,
|
finalName,
|
||||||
isReplace ? FileTransferType.Update : FileTransferType.Upload,
|
isReplace ? FileTransferType.Update : FileTransferType.Upload,
|
||||||
prettySize,
|
prettySize,
|
||||||
currentDrive.name,
|
drive.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -664,60 +621,114 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
isReplace: Boolean(isReplace),
|
isReplace: Boolean(isReplace),
|
||||||
replaceTopic,
|
replaceTopic,
|
||||||
replaceHistory,
|
replaceHistory,
|
||||||
|
driveId: drive.id.toString(),
|
||||||
|
driveName: drive.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[allTaken, ensureQueuedRow, reserved, resolveConflict, sameDrive],
|
||||||
|
)
|
||||||
|
|
||||||
|
const preflight = useCallback(
|
||||||
|
async (filesArr: File[]): Promise<UploadTask[]> => {
|
||||||
|
if (!currentDrive || !fm) return []
|
||||||
|
|
||||||
|
if (!currentStamp || !currentStamp.usable) {
|
||||||
|
setErrorMessage?.('Stamp is not usable.')
|
||||||
|
setShowError(true)
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks: UploadTask[] = []
|
||||||
|
const inFlightSize = uploadTaskQueueRef.current.reduce((sum, t) => sum + t.file.size, 0)
|
||||||
|
const inFlightCount = uploadTaskQueueRef.current.length
|
||||||
|
let currentFileSizeSum = inFlightSize
|
||||||
|
|
||||||
|
for (let i = 0; i < filesArr.length; i++) {
|
||||||
|
const file = filesArr[i]
|
||||||
|
currentFileSizeSum += file.size
|
||||||
|
const fileCount = inFlightCount + i + 1
|
||||||
|
|
||||||
|
const { ok } = verifyDriveSpace({
|
||||||
|
fm,
|
||||||
|
redundancyLevel: currentDrive.redundancyLevel,
|
||||||
|
stamp: currentStamp,
|
||||||
|
useInfoSize: true,
|
||||||
driveId: currentDrive.id.toString(),
|
driveId: currentDrive.id.toString(),
|
||||||
driveName: currentDrive.name,
|
adminRedundancy: adminDrive?.redundancyLevel,
|
||||||
}
|
fileSize: currentFileSizeSum,
|
||||||
}
|
fileCount,
|
||||||
}
|
cb: err => {
|
||||||
|
setErrorMessage?.(err + ' (' + truncateNameMiddle(file.name) + ')')
|
||||||
|
setShowError(true)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return null
|
if (!ok) {
|
||||||
}
|
|
||||||
|
|
||||||
for (const file of filesArr) {
|
|
||||||
const task = await processFile(file)
|
|
||||||
|
|
||||||
if (task) {
|
|
||||||
tasks.push(task)
|
|
||||||
} else {
|
|
||||||
// Stop processing remaining files if capacity check failed
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const task = await createUploadTask(file, currentDrive)
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.push(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tasks
|
return tasks
|
||||||
}
|
},
|
||||||
|
[fm, currentDrive, currentStamp, adminDrive, createUploadTask, setErrorMessage, setShowError],
|
||||||
|
)
|
||||||
|
|
||||||
const runQueue = async () => {
|
const runUploadQueue = useCallback(async () => {
|
||||||
if (runningRef.current) return
|
if (runningRef.current) return
|
||||||
runningRef.current = true
|
runningRef.current = true
|
||||||
|
|
||||||
try {
|
while (uploadTaskQueueRef.current.length > 0) {
|
||||||
while (queueRef.current.length > 0) {
|
const task = uploadTaskQueueRef.current[0]
|
||||||
const task = queueRef.current[0]
|
|
||||||
|
|
||||||
if (!task) break
|
if (!task) break
|
||||||
|
|
||||||
const isCancelled = cancelledQueuedRef.current.has(task.uuid)
|
if (cancelledQueuedRef.current.has(task.uuid)) {
|
||||||
|
|
||||||
if (isCancelled) {
|
|
||||||
safeSetState(
|
safeSetState(
|
||||||
isMountedRef,
|
isMountedRef,
|
||||||
setUploadItems,
|
setUploadItems,
|
||||||
)(prev => updateTransferItems(prev, task.uuid, { status: TransferStatus.Cancelled }))
|
)(prev => updateTransferItems(prev, task.uuid, { status: TransferStatus.Cancelled }))
|
||||||
cancelledQueuedRef.current.delete(task.uuid)
|
cancelledQueuedRef.current.delete(task.uuid)
|
||||||
queueRef.current.shift()
|
|
||||||
} else {
|
} else {
|
||||||
await processUploadTask(task)
|
await executeUploadTask(task)
|
||||||
queueRef.current.shift()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadTaskQueueRef.current.shift()
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
runningRef.current = false
|
runningRef.current = false
|
||||||
|
|
||||||
if (queueRef.current.length > 0) {
|
// Race guard: uploadFiles may have appended tasks and called runUploadQueue() again
|
||||||
runQueue()
|
if (uploadTaskQueueRef.current.length > 0) {
|
||||||
}
|
runUploadQueue()
|
||||||
}
|
}
|
||||||
|
}, [executeUploadTask])
|
||||||
|
|
||||||
|
const uploadFiles = useCallback(
|
||||||
|
(picked: FileList | File[]): void => {
|
||||||
|
const filesArr = Array.from(picked)
|
||||||
|
|
||||||
|
if (filesArr.length === 0 || !fm || !currentDrive || !currentStamp) return
|
||||||
|
|
||||||
|
const currentlyQueued = uploadTaskQueueRef.current.length
|
||||||
|
const newFilesCount = filesArr.length
|
||||||
|
const totalAfterAdd = currentlyQueued + newFilesCount
|
||||||
|
|
||||||
|
if (totalAfterAdd > MAX_UPLOAD_FILES) {
|
||||||
|
setErrorMessage?.(
|
||||||
|
`You’re trying to upload ${totalAfterAdd} files, but the limit is ${MAX_UPLOAD_FILES}. Please upload fewer files.`,
|
||||||
|
)
|
||||||
|
setShowError(true)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
@@ -741,25 +752,12 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = await preflight()
|
const tasks = await preflight(filesArr)
|
||||||
queueRef.current = queueRef.current.concat(tasks)
|
uploadTaskQueueRef.current = uploadTaskQueueRef.current.concat(tasks)
|
||||||
runQueue()
|
runUploadQueue()
|
||||||
})()
|
})()
|
||||||
},
|
},
|
||||||
[
|
[fm, currentStamp, currentDrive, beeApi, setShowError, setErrorMessage, preflight, runUploadQueue],
|
||||||
fm,
|
|
||||||
currentDrive,
|
|
||||||
currentStamp,
|
|
||||||
collectSameDrive,
|
|
||||||
resolveConflict,
|
|
||||||
ensureQueuedRow,
|
|
||||||
processUploadTask,
|
|
||||||
uploadItems,
|
|
||||||
adminDrive,
|
|
||||||
setShowError,
|
|
||||||
setErrorMessage,
|
|
||||||
beeApi,
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const cancelOrDismissUpload = useCallback(
|
const cancelOrDismissUpload = useCallback(
|
||||||
@@ -774,7 +772,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
|
|
||||||
if (row.status === TransferStatus.Queued) {
|
if (row.status === TransferStatus.Queued) {
|
||||||
cancelledQueuedRef.current.add(row.uuid)
|
cancelledQueuedRef.current.add(row.uuid)
|
||||||
queueRef.current = queueRef.current.filter(t => t.uuid !== row.uuid)
|
uploadTaskQueueRef.current = uploadTaskQueueRef.current.filter(t => t.uuid !== row.uuid)
|
||||||
|
|
||||||
return prev.map(r => (r.uuid === row.uuid ? { ...r, status: TransferStatus.Cancelled } : r))
|
return prev.map(r => (r.uuid === row.uuid ? { ...r, status: TransferStatus.Cancelled } : r))
|
||||||
}
|
}
|
||||||
@@ -818,7 +816,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
|||||||
|
|
||||||
const dismissAllUploads = useCallback(() => {
|
const dismissAllUploads = useCallback(() => {
|
||||||
uploadAbortsRef.current.clear()
|
uploadAbortsRef.current.clear()
|
||||||
queueRef.current = []
|
uploadTaskQueueRef.current = []
|
||||||
cancelledQueuedRef.current.clear()
|
cancelledQueuedRef.current.clear()
|
||||||
cancelledUploadingRef.current.clear()
|
cancelledUploadingRef.current.clear()
|
||||||
setUploadItems([])
|
setUploadItems([])
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FileInfo, FileManager } from '@solarpunkltd/file-manager-lib'
|
|||||||
import { DownloadProgress, DownloadState } from '../constants/transfers'
|
import { DownloadProgress, DownloadState } from '../constants/transfers'
|
||||||
|
|
||||||
import { AbortManager } from './abortManager'
|
import { AbortManager } from './abortManager'
|
||||||
|
import { isDirectoryPickerSupported, isPickerSupported } from './fileOperations'
|
||||||
import { guessMime, VIEWERS } from './view'
|
import { guessMime, VIEWERS } from './view'
|
||||||
|
|
||||||
const downloadAborts = new AbortManager()
|
const downloadAborts = new AbortManager()
|
||||||
@@ -141,12 +142,6 @@ interface FileInfoWithHandle {
|
|||||||
cancelled?: boolean
|
cancelled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const isPickerSupported = (): boolean => typeof (window as any).showSaveFilePicker === 'function'
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const isDirectoryPickerSupported = (): boolean => typeof (window as any).showDirectoryPicker === 'function'
|
|
||||||
|
|
||||||
const isUserCancellation = (error: unknown): boolean => {
|
const isUserCancellation = (error: unknown): boolean => {
|
||||||
const errName = (error as { name?: string })?.name
|
const errName = (error as { name?: string })?.name
|
||||||
|
|
||||||
|
|||||||
@@ -151,3 +151,12 @@ export async function performBulkFileOperation({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isPickerSupported = (): boolean => typeof (window as any).showSaveFilePicker === 'function'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isDirectoryPickerSupported = (): boolean => typeof (window as any).showDirectoryPicker === 'function'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isElementPickerSupported = (el: any): boolean => typeof (el as HTMLInputElement).showPicker === 'function'
|
||||||
|
|||||||
Reference in New Issue
Block a user