feat: Added "Image from URL" option and markdown image syntax support to Super. Also allows pasting images from copied web content (#2218)
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
connect-src * data: blob:;
|
connect-src * data: blob:;
|
||||||
style-src 'unsafe-inline' 'self' http://localhost:* http://127.0.0.1:45653;
|
style-src 'unsafe-inline' 'self' http://localhost:* http://127.0.0.1:45653;
|
||||||
frame-src * blob:;
|
frame-src * blob:;
|
||||||
|
img-src * data: blob:;
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import { disableIosTextFieldZoom } from '@/Utils'
|
import { disableIosTextFieldZoom, getPlatform } from '@/Utils'
|
||||||
import { IsWebPlatform, WebAppVersion } from '@/Constants/Version'
|
import { IsWebPlatform, WebAppVersion } from '@/Constants/Version'
|
||||||
import { DesktopManagerInterface, Platform, SNLog } from '@standardnotes/snjs'
|
import { DesktopManagerInterface, Platform, SNLog } from '@standardnotes/snjs'
|
||||||
import ApplicationGroupView from './Components/ApplicationGroupView/ApplicationGroupView'
|
import ApplicationGroupView from './Components/ApplicationGroupView/ApplicationGroupView'
|
||||||
@@ -101,7 +101,7 @@ if (IsWebPlatform) {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const device = window.reactNativeDevice || new WebDevice(WebAppVersion)
|
const device = window.reactNativeDevice || new WebDevice(WebAppVersion)
|
||||||
window.platform = device.platform
|
window.platform = getPlatform(device)
|
||||||
|
|
||||||
startApplication(window.defaultSyncServer, device, window.enabledUnfinishedFeatures, window.websocketUrl).catch(
|
startApplication(window.defaultSyncServer, device, window.enabledUnfinishedFeatures, window.websocketUrl).catch(
|
||||||
console.error,
|
console.error,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { CollapsibleContentNode } from '../../Plugins/CollapsiblePlugin/Collapsi
|
|||||||
import { CollapsibleTitleNode } from '../../Plugins/CollapsiblePlugin/CollapsibleTitleNode'
|
import { CollapsibleTitleNode } from '../../Plugins/CollapsiblePlugin/CollapsibleTitleNode'
|
||||||
import { FileNode } from '../../Plugins/EncryptedFilePlugin/Nodes/FileNode'
|
import { FileNode } from '../../Plugins/EncryptedFilePlugin/Nodes/FileNode'
|
||||||
import { BubbleNode } from '../../Plugins/ItemBubblePlugin/Nodes/BubbleNode'
|
import { BubbleNode } from '../../Plugins/ItemBubblePlugin/Nodes/BubbleNode'
|
||||||
|
import { RemoteImageNode } from '../../Plugins/RemoteImagePlugin/RemoteImageNode'
|
||||||
|
|
||||||
export const BlockEditorNodes = [
|
export const BlockEditorNodes = [
|
||||||
AutoLinkNode,
|
AutoLinkNode,
|
||||||
@@ -38,4 +39,5 @@ export const BlockEditorNodes = [
|
|||||||
YouTubeNode,
|
YouTubeNode,
|
||||||
FileNode,
|
FileNode,
|
||||||
BubbleNode,
|
BubbleNode,
|
||||||
|
RemoteImageNode,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ElementTransformer,
|
ElementTransformer,
|
||||||
TEXT_FORMAT_TRANSFORMERS,
|
TEXT_FORMAT_TRANSFORMERS,
|
||||||
TEXT_MATCH_TRANSFORMERS,
|
TEXT_MATCH_TRANSFORMERS,
|
||||||
|
TextMatchTransformer,
|
||||||
} from '@lexical/markdown'
|
} from '@lexical/markdown'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +13,11 @@ import {
|
|||||||
$isHorizontalRuleNode,
|
$isHorizontalRuleNode,
|
||||||
} from '@lexical/react/LexicalHorizontalRuleNode'
|
} from '@lexical/react/LexicalHorizontalRuleNode'
|
||||||
import { LexicalNode } from 'lexical'
|
import { LexicalNode } from 'lexical'
|
||||||
|
import {
|
||||||
|
$createRemoteImageNode,
|
||||||
|
$isRemoteImageNode,
|
||||||
|
RemoteImageNode,
|
||||||
|
} from './Plugins/RemoteImagePlugin/RemoteImageNode'
|
||||||
|
|
||||||
const HorizontalRule: ElementTransformer = {
|
const HorizontalRule: ElementTransformer = {
|
||||||
dependencies: [HorizontalRuleNode],
|
dependencies: [HorizontalRuleNode],
|
||||||
@@ -33,8 +39,29 @@ const HorizontalRule: ElementTransformer = {
|
|||||||
type: 'element',
|
type: 'element',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IMAGE: TextMatchTransformer = {
|
||||||
|
dependencies: [RemoteImageNode],
|
||||||
|
export: (node) => {
|
||||||
|
if (!$isRemoteImageNode(node)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return ``
|
||||||
|
},
|
||||||
|
importRegExp: /!(?:\[([^[]*)\])(?:\(([^(]+)\))/,
|
||||||
|
regExp: /!(?:\[([^[]*)\])(?:\(([^(]+)\))$/,
|
||||||
|
replace: (textNode, match) => {
|
||||||
|
const [, alt, src] = match
|
||||||
|
const imageNode = $createRemoteImageNode(src, alt)
|
||||||
|
textNode.replace(imageNode)
|
||||||
|
},
|
||||||
|
trigger: ')',
|
||||||
|
type: 'text-match',
|
||||||
|
}
|
||||||
|
|
||||||
export const MarkdownTransformers = [
|
export const MarkdownTransformers = [
|
||||||
CHECK_LIST,
|
CHECK_LIST,
|
||||||
|
IMAGE,
|
||||||
...ELEMENT_TRANSFORMERS,
|
...ELEMENT_TRANSFORMERS,
|
||||||
...TEXT_FORMAT_TRANSFORMERS,
|
...TEXT_FORMAT_TRANSFORMERS,
|
||||||
...TEXT_MATCH_TRANSFORMERS,
|
...TEXT_MATCH_TRANSFORMERS,
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { GetDatetimeBlockOptions } from './Options/DateTime'
|
|||||||
import { isMobileScreen } from '@/Utils'
|
import { isMobileScreen } from '@/Utils'
|
||||||
import { useApplication } from '@/Components/ApplicationProvider'
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
import { GetIndentOutdentBlockOptions } from './Options/IndentOutdent'
|
import { GetIndentOutdentBlockOptions } from './Options/IndentOutdent'
|
||||||
|
import { GetRemoteImageBlockOption } from './Options/RemoteImage'
|
||||||
|
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
|
||||||
|
|
||||||
export default function BlockPickerMenuPlugin(): JSX.Element {
|
export default function BlockPickerMenuPlugin(): JSX.Element {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
@@ -46,6 +48,9 @@ export default function BlockPickerMenuPlugin(): JSX.Element {
|
|||||||
GetTableBlockOption(() =>
|
GetTableBlockOption(() =>
|
||||||
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
||||||
),
|
),
|
||||||
|
GetRemoteImageBlockOption(() => {
|
||||||
|
showModal('Insert image from URL', (onClose) => <InsertRemoteImageDialog onClose={onClose} />)
|
||||||
|
}),
|
||||||
GetNumberedListBlockOption(editor),
|
GetNumberedListBlockOption(editor),
|
||||||
GetBulletedListBlockOption(editor),
|
GetBulletedListBlockOption(editor),
|
||||||
GetChecklistBlockOption(editor),
|
GetChecklistBlockOption(editor),
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||||
|
import { GetRemoteImageBlock } from '../../Blocks/RemoteImage'
|
||||||
|
import { BlockPickerOption } from '../BlockPickerOption'
|
||||||
|
|
||||||
|
export function GetRemoteImageBlockOption(onSelect: () => void) {
|
||||||
|
const block = GetRemoteImageBlock(onSelect)
|
||||||
|
return new BlockPickerOption(block.name, {
|
||||||
|
iconName: block.iconName as LexicalIconName,
|
||||||
|
keywords: block.keywords,
|
||||||
|
onSelect: block.onSelect,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function GetRemoteImageBlock(onSelect: () => void) {
|
||||||
|
return { name: 'Image from URL', iconName: 'file-image', keywords: ['image', 'url'], onSelect }
|
||||||
|
}
|
||||||
@@ -6,3 +6,4 @@ export const INSERT_TIME_COMMAND: LexicalCommand<string> = createCommand('INSERT
|
|||||||
export const INSERT_DATE_COMMAND: LexicalCommand<string> = createCommand('INSERT_DATE_COMMAND')
|
export const INSERT_DATE_COMMAND: LexicalCommand<string> = createCommand('INSERT_DATE_COMMAND')
|
||||||
export const INSERT_DATETIME_COMMAND: LexicalCommand<string> = createCommand('INSERT_DATETIME_COMMAND')
|
export const INSERT_DATETIME_COMMAND: LexicalCommand<string> = createCommand('INSERT_DATETIME_COMMAND')
|
||||||
export const INSERT_PASSWORD_COMMAND: LexicalCommand<string> = createCommand('INSERT_PASSWORD_COMMAND')
|
export const INSERT_PASSWORD_COMMAND: LexicalCommand<string> = createCommand('INSERT_PASSWORD_COMMAND')
|
||||||
|
export const INSERT_REMOTE_IMAGE_COMMAND: LexicalCommand<string> = createCommand('INSERT_REMOTE_IMAGE_COMMAND')
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/u
|
|||||||
import { classNames } from '@standardnotes/snjs'
|
import { classNames } from '@standardnotes/snjs'
|
||||||
import { SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
|
import { SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
|
||||||
import { useApplication } from '@/Components/ApplicationProvider'
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
|
import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
|
||||||
|
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
|
||||||
|
|
||||||
const MobileToolbarPlugin = () => {
|
const MobileToolbarPlugin = () => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
@@ -127,6 +129,9 @@ const MobileToolbarPlugin = () => {
|
|||||||
GetTableBlock(() =>
|
GetTableBlock(() =>
|
||||||
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
|
||||||
),
|
),
|
||||||
|
GetRemoteImageBlock(() => {
|
||||||
|
showModal('Insert image from URL', (onClose) => <InsertRemoteImageDialog onClose={onClose} />)
|
||||||
|
}),
|
||||||
GetNumberedListBlock(editor),
|
GetNumberedListBlock(editor),
|
||||||
GetBulletedListBlock(editor),
|
GetBulletedListBlock(editor),
|
||||||
GetChecklistBlock(editor),
|
GetChecklistBlock(editor),
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
|
import Icon from '@/Components/Icon/Icon'
|
||||||
|
import Spinner from '@/Components/Spinner/Spinner'
|
||||||
|
import { isDesktopApplication } from '@/Utils'
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
|
import { classNames } from '@standardnotes/snjs'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import { $createFileNode } from '../EncryptedFilePlugin/Nodes/FileUtils'
|
||||||
|
import { RemoteImageNode } from './RemoteImageNode'
|
||||||
|
|
||||||
|
const RemoteImageComponent = ({ src, alt, node }: { src: string; alt?: string; node: RemoteImageNode }) => {
|
||||||
|
const application = useApplication()
|
||||||
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
|
const [didImageLoad, setDidImageLoad] = useState(false)
|
||||||
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
|
||||||
|
const fetchAndUploadImage = useCallback(async () => {
|
||||||
|
setIsSaving(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(src)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
const file = new File([blob], src, { type: blob.type })
|
||||||
|
|
||||||
|
const { filesController, linkingController } = application.getViewControllerManager()
|
||||||
|
|
||||||
|
const uploadedFile = await filesController.uploadNewFile(file, false)
|
||||||
|
|
||||||
|
if (!uploadedFile) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.update(() => {
|
||||||
|
const fileNode = $createFileNode(uploadedFile.uuid)
|
||||||
|
node.replace(fileNode)
|
||||||
|
})
|
||||||
|
|
||||||
|
void linkingController.linkItemToSelectedItem(uploadedFile)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
|
}, [application, editor, node, src])
|
||||||
|
|
||||||
|
const canShowSaveButton = application.isNativeMobileWeb() || isDesktopApplication()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex min-h-[2rem] flex-col items-center gap-2.5">
|
||||||
|
<img
|
||||||
|
alt={alt}
|
||||||
|
src={src}
|
||||||
|
onLoad={() => {
|
||||||
|
setDidImageLoad(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{didImageLoad && canShowSaveButton && (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
'flex items-center gap-2.5 rounded border border-border bg-default px-2.5 py-1.5',
|
||||||
|
!isSaving && 'hover:bg-info hover:text-info-contrast',
|
||||||
|
)}
|
||||||
|
onClick={fetchAndUploadImage}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<>
|
||||||
|
<Spinner className="h-4 w-4" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon type="download" />
|
||||||
|
Save image to Files
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoteImageComponent
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||||
|
import { DOMConversionMap, DOMExportOutput, LexicalNode, Spread } from 'lexical'
|
||||||
|
import RemoteImageComponent from './RemoteImageComponent'
|
||||||
|
|
||||||
|
type SerializedRemoteImageNode = Spread<
|
||||||
|
{
|
||||||
|
version: 1
|
||||||
|
type: 'unencrypted-image'
|
||||||
|
alt: string | undefined
|
||||||
|
src: string
|
||||||
|
},
|
||||||
|
SerializedDecoratorBlockNode
|
||||||
|
>
|
||||||
|
|
||||||
|
export class RemoteImageNode extends DecoratorBlockNode {
|
||||||
|
__alt: string | undefined
|
||||||
|
__src: string
|
||||||
|
|
||||||
|
static getType(): string {
|
||||||
|
return 'unencrypted-image'
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(src: string, alt?: string) {
|
||||||
|
super()
|
||||||
|
this.__src = src
|
||||||
|
this.__alt = alt
|
||||||
|
}
|
||||||
|
|
||||||
|
static clone(node: RemoteImageNode): RemoteImageNode {
|
||||||
|
return new RemoteImageNode(node.__src, node.__alt)
|
||||||
|
}
|
||||||
|
|
||||||
|
static importJSON(serializedNode: SerializedRemoteImageNode): RemoteImageNode {
|
||||||
|
const node = $createRemoteImageNode(serializedNode.src, serializedNode.alt)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJSON(): SerializedRemoteImageNode {
|
||||||
|
return {
|
||||||
|
...super.exportJSON(),
|
||||||
|
src: this.__src,
|
||||||
|
alt: this.__alt,
|
||||||
|
version: 1,
|
||||||
|
type: 'unencrypted-image',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static importDOM(): DOMConversionMap<HTMLDivElement> | null {
|
||||||
|
return {
|
||||||
|
img: (domNode: HTMLDivElement) => {
|
||||||
|
if (domNode.tagName !== 'IMG') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
conversion: () => {
|
||||||
|
if (!(domNode instanceof HTMLImageElement)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
node: $createRemoteImageNode(domNode.currentSrc || domNode.src, domNode.alt),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
priority: 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportDOM(): DOMExportOutput {
|
||||||
|
const element = document.createElement('img')
|
||||||
|
if (this.__alt) {
|
||||||
|
element.setAttribute('alt', this.__alt)
|
||||||
|
}
|
||||||
|
element.setAttribute('src', this.__src)
|
||||||
|
return { element }
|
||||||
|
}
|
||||||
|
|
||||||
|
decorate(): JSX.Element {
|
||||||
|
return <RemoteImageComponent node={this} src={this.__src} alt={this.__alt} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $isRemoteImageNode(node: RemoteImageNode | LexicalNode | null | undefined): node is RemoteImageNode {
|
||||||
|
return node instanceof RemoteImageNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $createRemoteImageNode(src: string, alt?: string): RemoteImageNode {
|
||||||
|
return new RemoteImageNode(src, alt)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
|
import { $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_NORMAL } from 'lexical'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import Button from '../../Lexical/UI/Button'
|
||||||
|
import { DialogActions } from '../../Lexical/UI/Dialog'
|
||||||
|
import TextInput from '../../Lexical/UI/TextInput'
|
||||||
|
import { INSERT_REMOTE_IMAGE_COMMAND } from '../Commands'
|
||||||
|
import { $createRemoteImageNode } from './RemoteImageNode'
|
||||||
|
import { $wrapNodeInElement } from '@lexical/utils'
|
||||||
|
|
||||||
|
export function InsertRemoteImageDialog({ onClose }: { onClose: () => void }) {
|
||||||
|
const [url, setURL] = useState('')
|
||||||
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (url.length < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.dispatchCommand(INSERT_REMOTE_IMAGE_COMMAND, url)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextInput label="URL:" onChange={setURL} value={url} />
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClick}>Confirm</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RemoteImagePlugin() {
|
||||||
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerCommand<string>(
|
||||||
|
INSERT_REMOTE_IMAGE_COMMAND,
|
||||||
|
(payload) => {
|
||||||
|
const imageNode = $createRemoteImageNode(payload)
|
||||||
|
$insertNodes([imageNode])
|
||||||
|
if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) {
|
||||||
|
$wrapNodeInElement(imageNode, $createParagraphNode).selectEnd()
|
||||||
|
}
|
||||||
|
const newLineNode = $createParagraphNode()
|
||||||
|
$insertNodes([newLineNode])
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_NORMAL,
|
||||||
|
)
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin'
|
|||||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||||
import MobileToolbarPlugin from './Plugins/MobileToolbarPlugin/MobileToolbarPlugin'
|
import MobileToolbarPlugin from './Plugins/MobileToolbarPlugin/MobileToolbarPlugin'
|
||||||
import CodeOptionsPlugin from './Plugins/CodeOptionsPlugin/CodeOptions'
|
import CodeOptionsPlugin from './Plugins/CodeOptionsPlugin/CodeOptions'
|
||||||
|
import RemoteImagePlugin from './Plugins/RemoteImagePlugin/RemoteImagePlugin'
|
||||||
|
|
||||||
export const SuperNotePreviewCharLimit = 160
|
export const SuperNotePreviewCharLimit = 160
|
||||||
|
|
||||||
@@ -202,6 +203,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
|||||||
</SuperSearchContextProvider>
|
</SuperSearchContextProvider>
|
||||||
<MobileToolbarPlugin />
|
<MobileToolbarPlugin />
|
||||||
<CodeOptionsPlugin />
|
<CodeOptionsPlugin />
|
||||||
|
<RemoteImagePlugin />
|
||||||
</BlocksEditor>
|
</BlocksEditor>
|
||||||
</BlocksEditorComposer>
|
</BlocksEditorComposer>
|
||||||
</FilesControllerProvider>
|
</FilesControllerProvider>
|
||||||
|
|||||||
@@ -348,8 +348,11 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async uploadNewFile(fileOrHandle: File | FileSystemFileHandle): Promise<FileItem | undefined> {
|
public async uploadNewFile(
|
||||||
let toastId = ''
|
fileOrHandle: File | FileSystemFileHandle,
|
||||||
|
showToast = true,
|
||||||
|
): Promise<FileItem | undefined> {
|
||||||
|
let toastId: string | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const minimumChunkSize = this.application.files.minimumChunkSize()
|
const minimumChunkSize = this.application.files.minimumChunkSize()
|
||||||
@@ -381,20 +384,24 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
|
|
||||||
const initialProgress = operation.getProgress().percentComplete
|
const initialProgress = operation.getProgress().percentComplete
|
||||||
|
|
||||||
toastId = addToast({
|
if (showToast) {
|
||||||
type: ToastType.Progress,
|
toastId = addToast({
|
||||||
message: `Uploading file "${fileToUpload.name}" (${initialProgress}%)`,
|
type: ToastType.Progress,
|
||||||
progress: initialProgress,
|
message: `Uploading file "${fileToUpload.name}" (${initialProgress}%)`,
|
||||||
})
|
progress: initialProgress,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const onChunk: OnChunkCallbackNoProgress = async ({ data, index, isLast }) => {
|
const onChunk: OnChunkCallbackNoProgress = async ({ data, index, isLast }) => {
|
||||||
await this.application.files.pushBytesForUpload(operation, data, index, isLast)
|
await this.application.files.pushBytesForUpload(operation, data, index, isLast)
|
||||||
|
|
||||||
const percentComplete = Math.round(operation.getProgress().percentComplete)
|
const percentComplete = Math.round(operation.getProgress().percentComplete)
|
||||||
updateToast(toastId, {
|
if (toastId) {
|
||||||
message: `Uploading file "${fileToUpload.name}" (${percentComplete}%)`,
|
updateToast(toastId, {
|
||||||
progress: percentComplete,
|
message: `Uploading file "${fileToUpload.name}" (${percentComplete}%)`,
|
||||||
})
|
progress: percentComplete,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileResult = await this.reader.readFile(fileToUpload, minimumChunkSize, onChunk)
|
const fileResult = await this.reader.readFile(fileToUpload, minimumChunkSize, onChunk)
|
||||||
@@ -414,30 +421,34 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
throw new Error('Unable to close upload session')
|
throw new Error('Unable to close upload session')
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissToast(toastId)
|
if (toastId) {
|
||||||
addToast({
|
dismissToast(toastId)
|
||||||
type: ToastType.Success,
|
}
|
||||||
message: `Uploaded file "${uploadedFile.name}"`,
|
if (showToast) {
|
||||||
actions: [
|
addToast({
|
||||||
{
|
type: ToastType.Success,
|
||||||
label: 'Open',
|
message: `Uploaded file "${uploadedFile.name}"`,
|
||||||
handler: (toastId) => {
|
actions: [
|
||||||
void this.handleFileAction({
|
{
|
||||||
type: FileItemActionType.PreviewFile,
|
label: 'Open',
|
||||||
payload: { file: uploadedFile },
|
handler: (toastId) => {
|
||||||
})
|
void this.handleFileAction({
|
||||||
dismissToast(toastId)
|
type: FileItemActionType.PreviewFile,
|
||||||
|
payload: { file: uploadedFile },
|
||||||
|
})
|
||||||
|
dismissToast(toastId)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
autoClose: true,
|
||||||
autoClose: true,
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
return uploadedFile
|
return uploadedFile
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
if (toastId.length > 0) {
|
if (toastId) {
|
||||||
dismissToast(toastId)
|
dismissToast(toastId)
|
||||||
}
|
}
|
||||||
addToast({
|
addToast({
|
||||||
|
|||||||
Reference in New Issue
Block a user