chore: immediately insert file node and show progress inside super note when uploading a new file (#2876) [skip e2e]
This commit is contained in:
@@ -23,6 +23,7 @@ export interface FilesClientInterface {
|
||||
finishUpload(
|
||||
operation: EncryptAndUploadFileOperation,
|
||||
fileMetadata: FileMetadata,
|
||||
uuid: string,
|
||||
): Promise<FileItem | ClientDisplayableError>
|
||||
|
||||
downloadFile(
|
||||
|
||||
@@ -17,6 +17,12 @@ import {
|
||||
isEncryptedPayload,
|
||||
VaultListingInterface,
|
||||
SharedVaultListingInterface,
|
||||
DecryptedPayload,
|
||||
FillItemContent,
|
||||
PayloadVaultOverrides,
|
||||
PayloadTimestampDefaults,
|
||||
CreateItemFromPayload,
|
||||
DecryptedItemInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { LoggerInterface, spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils'
|
||||
@@ -246,6 +252,7 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
public async finishUpload(
|
||||
operation: EncryptAndUploadFileOperation,
|
||||
fileMetadata: FileMetadata,
|
||||
uuid: string,
|
||||
): Promise<FileItem | ClientDisplayableError> {
|
||||
const uploadSessionClosed = await this.api.closeUploadSession(
|
||||
operation.getValetToken(),
|
||||
@@ -268,16 +275,22 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
remoteIdentifier: result.remoteIdentifier,
|
||||
}
|
||||
|
||||
const file = await this.mutator.createItem<FileItem>(
|
||||
ContentType.TYPES.File,
|
||||
FillItemContentSpecialized(fileContent),
|
||||
true,
|
||||
operation.vault,
|
||||
)
|
||||
const filePayload = new DecryptedPayload<FileContent>({
|
||||
uuid,
|
||||
content_type: ContentType.TYPES.File,
|
||||
content: FillItemContent<FileContent>(FillItemContentSpecialized(fileContent)),
|
||||
dirty: true,
|
||||
...PayloadVaultOverrides(operation.vault),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
const fileItem = CreateItemFromPayload(filePayload) as DecryptedItemInterface<FileContent>
|
||||
|
||||
const insertedItem = await this.mutator.insertItem<FileItem>(fileItem)
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return file
|
||||
return insertedItem
|
||||
}
|
||||
|
||||
private async decryptCachedEntry(file: FileItem, entry: EncryptedBytes): Promise<DecryptedBytes> {
|
||||
|
||||
@@ -8,12 +8,22 @@ import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEf
|
||||
import Portal from './Portal/Portal'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
|
||||
type FileDragTargetData = {
|
||||
type FileDragTargetCommonData = {
|
||||
tooltipText: string
|
||||
callback: (files: FileItem) => void
|
||||
note?: SNNote
|
||||
}
|
||||
|
||||
type FileDragTargetCallbacks =
|
||||
| {
|
||||
callback: (files: FileItem) => void
|
||||
handleFileUpload?: never
|
||||
}
|
||||
| {
|
||||
handleFileUpload: (fileOrHandle: File | FileSystemFileHandle) => void
|
||||
callback?: never
|
||||
}
|
||||
type FileDragTargetData = FileDragTargetCommonData & FileDragTargetCallbacks
|
||||
|
||||
type FileDnDContextData = {
|
||||
isDraggingFiles: boolean
|
||||
addDragTarget: (target: HTMLElement, data: FileDragTargetData) => void
|
||||
@@ -203,6 +213,11 @@ const FileDragNDropProvider = ({ application, children }: Props) => {
|
||||
|
||||
const dragTarget = closestDragTarget ? dragTargets.current.get(closestDragTarget) : undefined
|
||||
|
||||
if (dragTarget?.handleFileUpload) {
|
||||
dragTarget.handleFileUpload(fileOrHandle)
|
||||
return
|
||||
}
|
||||
|
||||
const uploadedFile = await application.filesController.uploadNewFile(fileOrHandle, {
|
||||
note: dragTarget?.note,
|
||||
})
|
||||
@@ -211,7 +226,9 @@ const FileDragNDropProvider = ({ application, children }: Props) => {
|
||||
return
|
||||
}
|
||||
|
||||
dragTarget?.callback(uploadedFile)
|
||||
if (dragTarget?.callback) {
|
||||
dragTarget.callback(uploadedFile)
|
||||
}
|
||||
})
|
||||
|
||||
dragCounter.current = 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import { NoteType, SNNote } from '@standardnotes/snjs'
|
||||
import { useEffect } from 'react'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { useFileDragNDrop } from '../FileDragNDropProvider'
|
||||
@@ -20,17 +20,28 @@ const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement, file
|
||||
const target = noteViewElement
|
||||
|
||||
if (target) {
|
||||
addDragTarget(target, {
|
||||
tooltipText: 'Drop your files to upload and link them to the current note',
|
||||
callback: async (uploadedFile) => {
|
||||
await linkingController.linkItems(note, uploadedFile)
|
||||
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
||||
mutator.protected = note.protected
|
||||
})
|
||||
filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid)
|
||||
},
|
||||
note,
|
||||
})
|
||||
const tooltipText = 'Drop your files to upload and link them to the current note'
|
||||
if (note.noteType === NoteType.Super) {
|
||||
addDragTarget(target, {
|
||||
tooltipText,
|
||||
handleFileUpload: (fileOrHandle) => {
|
||||
filesController.uploadAndInsertFileToCurrentNote(fileOrHandle)
|
||||
},
|
||||
note,
|
||||
})
|
||||
} else {
|
||||
addDragTarget(target, {
|
||||
tooltipText,
|
||||
callback: async (uploadedFile) => {
|
||||
await linkingController.linkItems(note, uploadedFile)
|
||||
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
||||
mutator.protected = note.protected
|
||||
})
|
||||
filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid)
|
||||
},
|
||||
note,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createCommand, LexicalCommand } from 'lexical'
|
||||
|
||||
export const INSERT_FILE_COMMAND: LexicalCommand<string> = createCommand('INSERT_FILE_COMMAND')
|
||||
export const UPLOAD_AND_INSERT_FILE_COMMAND: LexicalCommand<File> = createCommand('UPLOAD_AND_INSERT_FILE_COMMAND')
|
||||
export const INSERT_BUBBLE_COMMAND: LexicalCommand<string> = createCommand('INSERT_BUBBLE_COMMAND')
|
||||
export const INSERT_DATETIME_COMMAND: LexicalCommand<'date' | 'time' | 'datetime'> =
|
||||
createCommand('INSERT_DATETIME_COMMAND')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { INSERT_FILE_COMMAND } from '../Commands'
|
||||
import { INSERT_FILE_COMMAND, UPLOAD_AND_INSERT_FILE_COMMAND } from '../Commands'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -19,45 +19,24 @@ import { FilesControllerEvent } from '@/Controllers/FilesController'
|
||||
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import Spinner from '../../../Spinner/Spinner'
|
||||
import Modal from '../../Lexical/UI/Modal'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { isMobileScreen } from '../../../../Utils'
|
||||
|
||||
export const OPEN_FILE_UPLOAD_MODAL_COMMAND = createCommand('OPEN_FILE_UPLOAD_MODAL_COMMAND')
|
||||
|
||||
function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClose: () => void }) {
|
||||
const application = useApplication()
|
||||
function UploadFileDialog({ onClose }: { onClose: () => void }) {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const filesController = useFilesController()
|
||||
const linkingController = useLinkingController()
|
||||
|
||||
const [file, setFile] = useState<File>()
|
||||
const [isUploadingFile, setIsUploadingFile] = useState(false)
|
||||
|
||||
const onClick = () => {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsUploadingFile(true)
|
||||
filesController
|
||||
.uploadNewFile(file)
|
||||
.then((uploadedFile) => {
|
||||
if (!uploadedFile) {
|
||||
return
|
||||
}
|
||||
editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid)
|
||||
void linkingController.linkItemToSelectedItem(uploadedFile)
|
||||
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
||||
mutator.protected = currentNote.protected
|
||||
})
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
setIsUploadingFile(false)
|
||||
onClose()
|
||||
})
|
||||
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -72,13 +51,9 @@ function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClo
|
||||
}}
|
||||
/>
|
||||
<div className="mt-1.5 flex justify-end">
|
||||
{isUploadingFile ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
|
||||
Upload
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@@ -99,17 +74,23 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
||||
|
||||
const uploadFilesList = (files: FileList) => {
|
||||
Array.from(files).forEach(async (file) => {
|
||||
try {
|
||||
const uploadedFile = await filesController.uploadNewFile(file)
|
||||
if (uploadedFile) {
|
||||
editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid)
|
||||
void linkingController.linkItemToSelectedItem(uploadedFile)
|
||||
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
||||
mutator.protected = currentNote.protected
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
|
||||
})
|
||||
}
|
||||
|
||||
const insertFileNode = (uuid: string, onInsert?: (node: FileNode) => void) => {
|
||||
editor.update(() => {
|
||||
const fileNode = $createFileNode(uuid)
|
||||
$insertNodes([fileNode])
|
||||
if ($isRootOrShadowRoot(fileNode.getParentOrThrow())) {
|
||||
$wrapNodeInElement(fileNode, $createParagraphNode).selectEnd()
|
||||
}
|
||||
const newLineNode = $createParagraphNode()
|
||||
fileNode.getParentOrThrow().insertAfter(newLineNode)
|
||||
newLineNode.selectEnd()
|
||||
editor.focus()
|
||||
if (onInsert) {
|
||||
onInsert(fileNode)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -118,14 +99,34 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
||||
editor.registerCommand<string>(
|
||||
INSERT_FILE_COMMAND,
|
||||
(payload) => {
|
||||
const fileNode = $createFileNode(payload)
|
||||
$insertNodes([fileNode])
|
||||
if ($isRootOrShadowRoot(fileNode.getParentOrThrow())) {
|
||||
$wrapNodeInElement(fileNode, $createParagraphNode).selectEnd()
|
||||
}
|
||||
const newLineNode = $createParagraphNode()
|
||||
fileNode.getParentOrThrow().insertAfter(newLineNode)
|
||||
|
||||
insertFileNode(payload)
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
UPLOAD_AND_INSERT_FILE_COMMAND,
|
||||
(file) => {
|
||||
const note = currentNote
|
||||
let fileNode: FileNode | undefined
|
||||
filesController
|
||||
.uploadNewFile(file, {
|
||||
showToast: false,
|
||||
onUploadStart(fileUuid) {
|
||||
insertFileNode(fileUuid, (node) => (fileNode = node))
|
||||
},
|
||||
})
|
||||
.then((uploadedFile) => {
|
||||
if (uploadedFile) {
|
||||
void linkingController.linkItems(note, uploadedFile)
|
||||
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
||||
mutator.protected = note.protected
|
||||
})
|
||||
} else {
|
||||
editor.update(() => fileNode?.remove())
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
@@ -150,28 +151,26 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
||||
},
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
),
|
||||
editor.registerNodeTransform(FileNode, (node) => {
|
||||
/**
|
||||
* When adding the node we wrap it with a paragraph to avoid insertion errors,
|
||||
* however that causes issues with selection. We unwrap the node to fix that.
|
||||
*/
|
||||
const parent = node.getParent()
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
if (parent.getChildrenSize() === 1) {
|
||||
parent.insertBefore(node)
|
||||
parent.remove()
|
||||
}
|
||||
}),
|
||||
)
|
||||
}, [application, currentNote.protected, editor, filesController, linkingController])
|
||||
}, [application, currentNote, editor, filesController, linkingController])
|
||||
|
||||
useEffect(() => {
|
||||
const disposer = filesController.addEventObserver((event, data) => {
|
||||
if (event === FilesControllerEvent.FileUploadedToNote) {
|
||||
if (event === FilesControllerEvent.FileUploadedToNote && data[FilesControllerEvent.FileUploadedToNote]) {
|
||||
const fileUuid = data[FilesControllerEvent.FileUploadedToNote].uuid
|
||||
editor.dispatchCommand(INSERT_FILE_COMMAND, fileUuid)
|
||||
} else if (event === FilesControllerEvent.UploadAndInsertFile && data[FilesControllerEvent.UploadAndInsertFile]) {
|
||||
const { fileOrHandle } = data[FilesControllerEvent.UploadAndInsertFile]
|
||||
if (fileOrHandle instanceof FileSystemFileHandle) {
|
||||
fileOrHandle
|
||||
.getFile()
|
||||
.then((file) => {
|
||||
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
|
||||
})
|
||||
.catch(console.error)
|
||||
} else {
|
||||
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, fileOrHandle)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -181,7 +180,7 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
||||
if (showFileUploadModal) {
|
||||
return (
|
||||
<Modal onClose={() => setShowFileUploadModal(false)} title="Upload File">
|
||||
<UploadFileDialog currentNote={currentNote} onClose={() => setShowFileUploadModal(false)} />
|
||||
<UploadFileDialog onClose={() => setShowFileUploadModal(false)} />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import FilePreview from '@/Components/FilePreview/FilePreview'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
import { FilesControllerEvent } from '@/Controllers/FilesController'
|
||||
|
||||
export type FileComponentProps = Readonly<{
|
||||
className: Readonly<{
|
||||
@@ -19,10 +22,11 @@ export type FileComponentProps = Readonly<{
|
||||
setZoomLevel: (zoomLevel: number) => void
|
||||
}>
|
||||
|
||||
export function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoomLevel }: FileComponentProps) {
|
||||
function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoomLevel }: FileComponentProps) {
|
||||
const application = useApplication()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const file = useMemo(() => application.items.findItem<FileItem>(fileUuid), [application, fileUuid])
|
||||
const [file, setFile] = useState(() => application.items.findItem<FileItem>(fileUuid))
|
||||
const uploadProgress = application.filesController.uploadProgressMap.get(fileUuid)
|
||||
|
||||
const [canLoad, setCanLoad] = useState(false)
|
||||
|
||||
@@ -90,6 +94,41 @@ export function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel,
|
||||
)
|
||||
}, [editor, isSelected, nodeKey, setSelected])
|
||||
|
||||
useEffect(() => {
|
||||
return application.filesController.addEventObserver((event, data) => {
|
||||
if (event === FilesControllerEvent.FileUploadFinished && data[FilesControllerEvent.FileUploadFinished]) {
|
||||
const { uploadedFile } = data[FilesControllerEvent.FileUploadFinished]
|
||||
if (uploadedFile.uuid === fileUuid) {
|
||||
setFile(uploadedFile)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [application.filesController, fileUuid])
|
||||
|
||||
if (uploadProgress && (uploadProgress.progress < 100 || !file)) {
|
||||
const progress = uploadProgress.progress
|
||||
return (
|
||||
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||
<div className="flex flex-col items-center justify-center gap-2 p-4 text-center" ref={blockWrapperRef}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Spinner className="h-4 w-4" />
|
||||
Uploading file "{uploadProgress.file.name}"... ({progress}%)
|
||||
</div>
|
||||
<div className="w-full max-w-[50%] overflow-hidden rounded bg-contrast">
|
||||
<div
|
||||
className="h-2 rounded rounded-tl-none bg-info transition-[width] duration-100"
|
||||
role="progressbar"
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
}}
|
||||
aria-valuenow={progress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BlockWithAlignableContents>
|
||||
)
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
return (
|
||||
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||
@@ -114,3 +153,5 @@ export function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel,
|
||||
</BlockWithAlignableContents>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(FileComponent)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DOMConversionMap, DOMExportOutput, EditorConfig, ElementFormatType, LexicalEditor, NodeKey } from 'lexical'
|
||||
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import { $createFileNode, convertToFileElement } from './FileUtils'
|
||||
import { FileComponent } from './FileComponent'
|
||||
import FileComponent from './FileComponent'
|
||||
import { SerializedFileNode } from './SerializedFileNode'
|
||||
import { ItemNodeInterface } from '../../ItemNodeInterface'
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
ProtectionsClientInterface,
|
||||
SNNote,
|
||||
SyncServiceInterface,
|
||||
UuidGenerator,
|
||||
VaultServiceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/toast'
|
||||
@@ -52,14 +53,22 @@ const NonMutatingFileActions = [FileItemActionType.DownloadFile, FileItemActionT
|
||||
|
||||
type FileContextMenuLocation = { x: number; y: number }
|
||||
|
||||
export type FilesControllerEventData = {
|
||||
[FilesControllerEvent.FileUploadedToNote]: {
|
||||
uuid: string
|
||||
}
|
||||
export enum FilesControllerEvent {
|
||||
FileUploadedToNote = 'FileUploadedToNote',
|
||||
FileUploadFinished = 'FileUploadFinished',
|
||||
UploadAndInsertFile = 'UploadAndInsertFile',
|
||||
}
|
||||
|
||||
export enum FilesControllerEvent {
|
||||
FileUploadedToNote,
|
||||
export type FilesControllerEventData = {
|
||||
[FilesControllerEvent.FileUploadedToNote]?: {
|
||||
uuid: string
|
||||
}
|
||||
[FilesControllerEvent.FileUploadFinished]?: {
|
||||
uploadedFile: FileItem
|
||||
}
|
||||
[FilesControllerEvent.UploadAndInsertFile]?: {
|
||||
fileOrHandle: File | FileSystemFileHandle
|
||||
}
|
||||
}
|
||||
|
||||
export class FilesController extends AbstractViewController<FilesControllerEvent, FilesControllerEventData> {
|
||||
@@ -73,6 +82,14 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
reader = this.shouldUseStreamingAPI ? StreamingFileReader : ClassicFileReader
|
||||
maxFileSize = this.reader.maximumFileSize()
|
||||
|
||||
uploadProgressMap: Map<
|
||||
string,
|
||||
{
|
||||
file: File
|
||||
progress: number
|
||||
}
|
||||
> = new Map()
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
;(this.notesController as unknown) = undefined
|
||||
@@ -111,6 +128,8 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
setShowFileContextMenu: action,
|
||||
setShowProtectedOverlay: action,
|
||||
setFileContextMenuLocation: action,
|
||||
|
||||
uploadProgressMap: observable,
|
||||
})
|
||||
|
||||
this.disposers.push(
|
||||
@@ -453,9 +472,11 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
options: {
|
||||
showToast?: boolean
|
||||
note?: SNNote
|
||||
onUploadStart?: (fileUuid: string) => void
|
||||
onUploadFinish?: () => void
|
||||
} = {},
|
||||
): Promise<FileItem | undefined> {
|
||||
const { showToast = true, note } = options
|
||||
const { showToast = true, note, onUploadStart, onUploadFinish } = options
|
||||
|
||||
let toastId: string | undefined
|
||||
let canShowProgressNotification = false
|
||||
@@ -482,6 +503,17 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
return
|
||||
}
|
||||
|
||||
const uuid = UuidGenerator.GenerateUuid()
|
||||
|
||||
this.uploadProgressMap.set(uuid, {
|
||||
file: fileToUpload,
|
||||
progress: 0,
|
||||
})
|
||||
|
||||
if (onUploadStart) {
|
||||
onUploadStart(uuid)
|
||||
}
|
||||
|
||||
const vaultForNote = note ? this.vaults.getItemVault(note) : undefined
|
||||
|
||||
const operation = await this.files.beginNewFileUpload(
|
||||
@@ -499,6 +531,11 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
|
||||
const initialProgress = operation.getProgress().percentComplete
|
||||
|
||||
this.uploadProgressMap.set(uuid, {
|
||||
file: fileToUpload,
|
||||
progress: initialProgress,
|
||||
})
|
||||
|
||||
if (showToast) {
|
||||
if (this.mobileDevice && canShowProgressNotification) {
|
||||
toastId = await this.mobileDevice.displayNotification({
|
||||
@@ -521,6 +558,10 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
await this.files.pushBytesForUpload(operation, data, index, isLast)
|
||||
|
||||
const percentComplete = Math.round(operation.getProgress().percentComplete)
|
||||
this.uploadProgressMap.set(uuid, {
|
||||
file: fileToUpload,
|
||||
progress: percentComplete,
|
||||
})
|
||||
if (toastId) {
|
||||
if (this.mobileDevice && canShowProgressNotification) {
|
||||
await this.mobileDevice.displayNotification({
|
||||
@@ -547,7 +588,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
fileResult.mimeType = await this.archiveService.getMimeType(ext)
|
||||
}
|
||||
|
||||
const uploadedFile = await this.files.finishUpload(operation, fileResult)
|
||||
const uploadedFile = await this.files.finishUpload(operation, fileResult, uuid)
|
||||
|
||||
if (uploadedFile instanceof ClientDisplayableError) {
|
||||
addToast({
|
||||
@@ -557,6 +598,14 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
throw new Error(uploadedFile.text)
|
||||
}
|
||||
|
||||
if (onUploadFinish) {
|
||||
onUploadFinish()
|
||||
}
|
||||
|
||||
this.notifyEvent(FilesControllerEvent.FileUploadFinished, {
|
||||
[FilesControllerEvent.FileUploadFinished]: { uploadedFile },
|
||||
})
|
||||
|
||||
if (toastId) {
|
||||
if (this.mobileDevice && canShowProgressNotification) {
|
||||
this.mobileDevice.cancelNotification(toastId).catch(console.error)
|
||||
@@ -635,6 +684,12 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
||||
})
|
||||
}
|
||||
|
||||
uploadAndInsertFileToCurrentNote(fileOrHandle: File | FileSystemFileHandle) {
|
||||
this.notifyEvent(FilesControllerEvent.UploadAndInsertFile, {
|
||||
[FilesControllerEvent.UploadAndInsertFile]: { fileOrHandle },
|
||||
})
|
||||
}
|
||||
|
||||
deleteFilesPermanently = async (files: FileItem[]) => {
|
||||
const title = Strings.trashItemsTitle
|
||||
const text = files.length === 1 ? StringUtils.deleteFile(files[0].name) : Strings.deleteMultipleFiles
|
||||
|
||||
Reference in New Issue
Block a user