From 2ec6250dddd5a7d0b1b8131281235dc7bb8797fc Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Tue, 5 Mar 2024 16:59:30 +0800 Subject: [PATCH] feat: support uploading local image (#4820) * feat: support uploading local image * fix: code review * fix: add hover style to empty image block --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 79 +++++++++++++ frontend/appflowy_tauri/src-tauri/Cargo.toml | 2 +- .../appflowy_tauri/src-tauri/tauri.conf.json | 11 +- .../application/document/document.types.ts | 1 + .../_shared/image_upload/LocalImage.tsx | 60 ++++++++++ .../_shared/image_upload/UploadImage.tsx | 94 +++++++++++++++- .../_shared/image_upload/UploadTabs.tsx | 31 +++--- .../components/_shared/image_upload/index.ts | 1 + .../_shared/view_title/cover/CoverPopover.tsx | 23 +++- .../_shared/view_title/cover/ViewCover.tsx | 22 +++- .../components/blocks/image/ImageEmpty.tsx | 2 +- .../components/blocks/image/ImageRender.tsx | 104 ++++++++++-------- .../components/blocks/image/ImageResizer.tsx | 6 +- .../components/blocks/image/UploadPopover.tsx | 19 +++- .../blocks/math_equation/MathEquation.tsx | 2 +- .../components/editor/editor.scss | 5 + .../src/appflowy_app/utils/mui.ts | 4 + .../src/appflowy_app/utils/upload_image.ts | 9 ++ frontend/resources/translations/en.json | 5 +- 19 files changed, 398 insertions(+), 82 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 7e91aa05a4..0e2f08cfbe 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -3816,6 +3816,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -4956,6 +4967,30 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + [[package]] name = "ring" version = "0.16.20" @@ -5896,6 +5931,7 @@ dependencies = [ "rand 0.8.5", "raw-window-handle", "regex", + "rfd", "semver", "serde", "serde_json", @@ -7050,6 +7086,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + [[package]] name = "windows" version = "0.39.0" @@ -7205,6 +7254,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + [[package]] name = "windows_aarch64_msvc" version = "0.39.0" @@ -7229,6 +7284,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + [[package]] name = "windows_i686_gnu" version = "0.39.0" @@ -7253,6 +7314,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + [[package]] name = "windows_i686_msvc" version = "0.39.0" @@ -7277,6 +7344,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + [[package]] name = "windows_x86_64_gnu" version = "0.39.0" @@ -7319,6 +7392,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + [[package]] name = "windows_x86_64_msvc" version = "0.39.0" diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 5276a9bc03..9307f3528a 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -34,7 +34,7 @@ lru = "0.12.0" [dependencies] serde_json.workspace = true serde.workspace = true -tauri = { version = "1.5", features = [ +tauri = { version = "1.5", features = [ "dialog-all", "clipboard-all", "fs-all", "shell-open", diff --git a/frontend/appflowy_tauri/src-tauri/tauri.conf.json b/frontend/appflowy_tauri/src-tauri/tauri.conf.json index 8c8e36b10a..899bbeeb41 100644 --- a/frontend/appflowy_tauri/src-tauri/tauri.conf.json +++ b/frontend/appflowy_tauri/src-tauri/tauri.conf.json @@ -20,8 +20,7 @@ "fs": { "all": true, "scope": [ - "$APPLOCALDATA/**", - "$APPLOCALDATA/images/*" + "$APPLOCALDATA/**" ], "readFile": true, "writeFile": true, @@ -37,6 +36,14 @@ "all": true, "writeText": true, "readText": true + }, + "dialog": { + "all": true, + "ask": true, + "confirm": true, + "message": true, + "open": true, + "save": true } }, "bundle": { diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts index d3af2903cf..bdc4b23600 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts @@ -111,6 +111,7 @@ export interface MathEquationNode extends Element { } export enum ImageType { + Local = 0, Internal = 1, External = 2, } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx new file mode 100644 index 0000000000..bf29f68a2e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx @@ -0,0 +1,60 @@ +import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; +import { CircularProgress } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { ErrorOutline } from '@mui/icons-material'; + +export const LocalImage = forwardRef< + HTMLImageElement, + { + renderErrorNode?: () => React.ReactElement | null; + } & React.ImgHTMLAttributes +>((localImageProps, ref) => { + const { src, renderErrorNode, ...props } = localImageProps; + const imageRef = useRef(null); + const { t } = useTranslation(); + const [imageURL, setImageURL] = useState(''); + const [loading, setLoading] = useState(true); + const [isError, setIsError] = useState(false); + const loadLocalImage = useCallback(async () => { + if (!src) return; + setLoading(true); + setIsError(false); + const { readBinaryFile, BaseDirectory } = await import('@tauri-apps/api/fs'); + + try { + const buffer = await readBinaryFile(src, { dir: BaseDirectory.AppLocalData }); + const blob = new Blob([buffer]); + + setImageURL(URL.createObjectURL(blob)); + } catch (e) { + setIsError(true); + } + + setLoading(false); + }, [src]); + + useEffect(() => { + void loadLocalImage(); + }, [loadLocalImage]); + + if (loading) { + return ( +
+ + {t('editor.loading')}... +
+ ); + } + + if (isError) { + if (renderErrorNode) return renderErrorNode(); + return ( +
+ +
{t('editor.imageLoadFailed')}
+
+ ); + } + + return {'local; +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx index 7f3d828149..a6b66a4c1f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx @@ -1,7 +1,95 @@ -import React from 'react'; +import React, { useCallback } from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import CloudUploadIcon from '@mui/icons-material/CloudUploadOutlined'; +import { notify } from '$app/components/_shared/notify'; +import { isTauri } from '$app/utils/env'; +import { getFileName, IMAGE_DIR, ALLOWED_IMAGE_EXTENSIONS, MAX_IMAGE_SIZE } from '$app/utils/upload_image'; -export function UploadImage() { - return
; +export function UploadImage({ onDone }: { onDone?: (url: string) => void }) { + const { t } = useTranslation(); + + const checkTauriFile = useCallback( + async (url: string) => { + const { readBinaryFile } = await import('@tauri-apps/api/fs'); + + const buffer = await readBinaryFile(url); + const blob = new Blob([buffer]); + + if (blob.size > MAX_IMAGE_SIZE) { + notify.error(t('document.imageBlock.error.invalidImageSize')); + return false; + } + + return true; + }, + [t] + ); + + const uploadTauriLocalImage = useCallback( + async (url: string) => { + const { copyFile, BaseDirectory, exists, createDir } = await import('@tauri-apps/api/fs'); + + const checked = await checkTauriFile(url); + + if (!checked) return; + + try { + const existDir = await exists(IMAGE_DIR, { dir: BaseDirectory.AppLocalData }); + + if (!existDir) { + await createDir(IMAGE_DIR, { dir: BaseDirectory.AppLocalData }); + } + + const filename = getFileName(url); + + await copyFile(url, `${IMAGE_DIR}/${filename}`, { dir: BaseDirectory.AppLocalData }); + const newUrl = `${IMAGE_DIR}/${filename}`; + + onDone?.(newUrl); + } catch (e) { + notify.error(t('document.plugins.image.imageUploadFailed')); + } + }, + [checkTauriFile, onDone, t] + ); + + const handleClickUpload = useCallback(async () => { + if (!isTauri()) return; + const { open } = await import('@tauri-apps/api/dialog'); + + const url = await open({ + multiple: false, + directory: false, + filters: [ + { + name: 'Image', + extensions: ALLOWED_IMAGE_EXTENSIONS, + }, + ], + }); + + if (!url || typeof url !== 'string') return; + + await uploadTauriLocalImage(url); + }, [uploadTauriLocalImage]); + + return ( +
+ +
+ ); } export default UploadImage; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx index 65eb58b7ae..fb65c709ce 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx @@ -25,10 +25,12 @@ export function UploadTabs({ tabOptions, popoverProps, containerStyle, + extra, }: { containerStyle?: React.CSSProperties; tabOptions: TabOption[]; popoverProps?: PopoverProps; + extra?: React.ReactNode; }) { const [tabValue, setTabValue] = useState(() => { return tabOptions[0].key; @@ -82,20 +84,23 @@ export function UploadTabs({ }} >
- - {tabOptions.map((tab) => { - const { key, label } = tab; +
+ + {tabOptions.map((tab) => { + const { key, label } = tab; - return ; - })} - + return ; + })} + + {extra} +
void; onUpdateCover?: (cover?: PageCover) => void; + onRemoveCover?: () => void; }) { const { t } = useTranslation(); const tabOptions: TabOption[] = useMemo(() => { @@ -46,6 +49,19 @@ function CoverPopover({ }); }, }, + { + label: t('button.upload'), + key: TAB_KEY.UPLOAD, + Component: UploadImage, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Image, + cover_selection: value, + image_type: ImageType.Local, + }); + onClose(); + }, + }, { label: t('document.imageBlock.embedLink.label'), key: TAB_KEY.EMBED_LINK, @@ -84,6 +100,11 @@ function CoverPopover({ }} containerStyle={{ width: 433, maxHeight: 300 }} tabOptions={tabOptions} + extra={ + + } /> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx index a86eef6ca5..f207e07886 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx @@ -4,9 +4,15 @@ import { renderColor } from '$app/utils/color'; import ViewCoverActions from '$app/components/_shared/view_title/cover/ViewCoverActions'; import CoverPopover from '$app/components/_shared/view_title/cover/CoverPopover'; import DefaultImage from '$app/assets/images/default_cover.jpg'; +import { ImageType } from '$app/application/document/document.types'; +import { LocalImage } from '$app/components/_shared/image_upload'; export function ViewCover({ cover, onUpdateCover }: { cover: PageCover; onUpdateCover?: (cover?: PageCover) => void }) { - const { cover_selection_type: type, cover_selection: value = '' } = useMemo(() => cover || {}, [cover]); + const { + cover_selection_type: type, + cover_selection: value = '', + image_type: source, + } = useMemo(() => cover || {}, [cover]); const [showAction, setShowAction] = useState(false); const actionRef = useRef(null); const [showPopover, setShowPopover] = useState(false); @@ -44,9 +50,16 @@ export function ViewCover({ cover, onUpdateCover }: { cover: PageCover; onUpdate }} className={'relative flex h-[255px] w-full'} > - {type === CoverType.Asset ? renderCoverImage(DefaultImage) : null} - {type === CoverType.Color ? renderCoverColor(value) : null} - {type === CoverType.Image ? renderCoverImage(value) : null} + {source === ImageType.Local ? ( + + ) : ( + <> + {type === CoverType.Asset ? renderCoverImage(DefaultImage) : null} + {type === CoverType.Color ? renderCoverColor(value) : null} + {type === CoverType.Image ? renderCoverImage(value) : null} + + )} + setShowPopover(false)} anchorEl={actionRef.current} onUpdateCover={onUpdateCover} + onRemoveCover={handleRemoveCover} /> )}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageEmpty.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageEmpty.tsx index df3584e922..e0b649939e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageEmpty.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageEmpty.tsx @@ -39,7 +39,7 @@ function ImageEmpty({ <>
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx index 01e4df6c5c..ceab0c2d64 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { ImageNode } from '$app/application/document/document.types'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ImageNode, ImageType } from '$app/application/document/document.types'; import { useTranslation } from 'react-i18next'; import { CircularProgress } from '@mui/material'; import { ErrorOutline } from '@mui/icons-material'; @@ -7,6 +7,7 @@ import ImageResizer from '$app/components/editor/components/blocks/image/ImageRe import { CustomEditor } from '$app/components/editor/command'; import { useSlateStatic } from 'slate-react'; import ImageActions from '$app/components/editor/components/blocks/image/ImageActions'; +import { LocalImage } from '$app/components/_shared/image_upload'; function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) { const [loading, setLoading] = useState(true); @@ -14,7 +15,7 @@ function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) const imgRef = useRef(null); const editor = useSlateStatic(); - const { url, width: imageWidth } = node.data; + const { url = '', width: imageWidth, image_type: source } = node.data; const { t } = useTranslation(); const blockId = node.blockId; @@ -35,56 +36,71 @@ function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) setInitialWidth(imgRef.current.offsetWidth); } }, [hasError, initialWidth, loading]); + const imageProps: React.ImgHTMLAttributes = useMemo(() => { + return { + style: { width: loading || hasError ? '0' : imageWidth ?? '100%', opacity: selected ? 0.8 : 1 }, + className: 'object-cover', + ref: imgRef, + src: url, + draggable: false, + onLoad: () => { + setHasError(false); + setLoading(false); + }, + onError: () => { + setHasError(true); + setLoading(false); + }, + }; + }, [url, imageWidth, loading, hasError, selected]); + + const renderErrorNode = useCallback(() => { + return ( +
+ +
{t('editor.imageLoadFailed')}
+
+ ); + }, [t]); + + if (!url) return null; return ( - <> -
{ - setShowActions(true); - }} - onMouseLeave={() => { - setShowActions(false); - }} - className={'relative'} - > - { - setHasError(false); - setLoading(false); - }} - onError={() => { +
{ + setShowActions(true); + }} + onMouseLeave={() => { + setShowActions(false); + }} + className={`relative min-h-[48px] ${hasError || (loading && source !== ImageType.Local) ? 'w-full' : ''}`} + > + {source === ImageType.Local ? ( + { setHasError(true); - setLoading(false); + return null; }} - src={url} - alt={`image-${blockId}`} - className={'object-cover'} - style={{ width: loading || hasError ? '0' : imageWidth ?? '100%', opacity: selected ? 0.8 : 1 }} + loading={'lazy'} /> - {initialWidth && } - {showActions && } -
+ ) : ( + {`image-${blockId}`} + )} - {loading && ( -
+ {initialWidth && } + {showActions && } + {hasError ? ( + renderErrorNode() + ) : loading && source !== ImageType.Local ? ( +
{t('editor.loading')}
- )} - {hasError && ( -
- -
{t('editor.imageLoadFailed')}
-
- )} - + ) : null} +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx index a2164202a6..ff241937de 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx @@ -29,18 +29,16 @@ function ImageResizer({ width, onWidthChange }: { width: number; onWidthChange: const onResizeStart = useCallback( (e: React.MouseEvent) => { startX.current = e.clientX; + originalWidth.current = width; document.addEventListener('mousemove', onResize); document.addEventListener('mouseup', onResizeEnd); }, - [onResize, onResizeEnd] + [onResize, onResizeEnd, width] ); return (
{ - originalWidth.current = width; - }} style={{ right: '2px', }} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx index b6d882ec7b..0aff9fb0cc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx @@ -3,7 +3,7 @@ import { PopoverOrigin } from '@mui/material/Popover/Popover'; import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; import { useTranslation } from 'react-i18next'; -import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY } from '$app/components/_shared/image_upload'; +import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY, UploadImage } from '$app/components/_shared/image_upload'; import { CustomEditor } from '$app/components/editor/command'; import { useSlateStatic } from 'slate-react'; import { ImageNode, ImageType } from '$app/application/document/document.types'; @@ -48,11 +48,18 @@ function UploadPopover({ const tabOptions: TabOption[] = useMemo(() => { return [ - // { - // label: t('button.upload'), - // key: TAB_KEY.UPLOAD, - // Component: UploadImage, - // }, + { + label: t('button.upload'), + key: TAB_KEY.UPLOAD, + Component: UploadImage, + onDone: (link: string) => { + CustomEditor.setImageBlockData(editor, node, { + url: link, + image_type: ImageType.Local, + }); + onClose(); + }, + }, { label: t('document.imageBlock.embedLink.label'), key: TAB_KEY.EMBED_LINK, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx index 542eb977d9..ee441be624 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx @@ -54,7 +54,7 @@ export const MathEquation = memo( >
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss index b555b978c9..36adff3880 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss @@ -155,6 +155,11 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { ::selection { @apply bg-transparent; } + &:hover { + .container-bg { + background: var(--content-blue-100) !important; + } + } } .mention-inline { diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts index 91105bdf55..badd5c60a3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts @@ -54,6 +54,10 @@ export const getDesignTokens = (isDark: boolean): ThemeOptions => { boxShadow: 'var(--shadow)', }, }, + outlinedInherit: { + color: 'var(--text-title)', + borderColor: 'var(--line-divider)', + }, }, }, MuiButtonBase: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts new file mode 100644 index 0000000000..7d92eddd91 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts @@ -0,0 +1,9 @@ +export const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB +export const ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png']; +export const IMAGE_DIR = 'images'; + +export function getFileName(url: string) { + const [...parts] = url.split('/'); + + return parts.pop() ?? url; +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index b4badec242..fc537dfb91 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -915,8 +915,9 @@ "error": { "invalidImage": "Invalid image", "invalidImageSize": "Image size must be less than 5MB", - "invalidImageFormat": "Image format is not supported. Supported formats: JPEG, PNG, GIF, SVG", - "invalidImageUrl": "Invalid image URL" + "invalidImageFormat": "Image format is not supported. Supported formats: JPEG, PNG, JPG", + "invalidImageUrl": "Invalid image URL", + "noImage": "No such file or directory" }, "embedLink": { "label": "Embed link",