From 92024ec7cab69a49a1bdf081c2e74afd5056e679 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Mon, 6 Jun 2022 21:30:51 +0530 Subject: [PATCH] feat: add file view (#1064) --- .../ApplicationView/ApplicationView.tsx | 2 +- .../AttachedFilesButton.tsx | 43 ++- .../AttachedFilesPopover.tsx | 5 +- .../ContentListView/ContentList.tsx | 5 +- .../ContentListView/ContentListItem.tsx | 2 +- .../ContentListView/FileListItem.tsx | 29 +- .../ContentListView/NoteListItem.tsx | 13 +- .../Types/AbstractListItemProps.ts | 10 +- .../FileContextMenu/FileMenuOptions.tsx | 7 + .../FilePreview/CreateObjectURLWithRef.tsx | 18 ++ .../Components/FilePreview/FilePreview.tsx | 81 ++++++ .../FilePreview/FilePreviewError.tsx | 62 ++++ .../FilePreviewInfoPanel.tsx | 0 .../FilePreview/FilePreviewModal.tsx | 134 +++++++++ .../{Files => FilePreview}/ImagePreview.tsx | 3 +- .../FilePreview/PreviewComponent.tsx | 54 ++++ .../Components/FilePreview/TextPreview.tsx | 26 ++ .../isFilePreviewable.ts | 5 +- .../Components/FileView/FileView.tsx | 36 +++ .../Components/FileView/FileViewProps.tsx | 9 + .../FileView/FileViewWithoutProtection.tsx | 75 +++++ .../Components/Files/FilePreviewModal.tsx | 270 ------------------ .../Components/Files/PreviewComponent.tsx | 26 -- .../NoteGroupView/NoteGroupView.tsx | 26 +- .../Components/NoteView/NoteView.tsx | 15 +- .../ProtectedItemOverlay.tsx} | 17 +- app/assets/javascripts/Constants/Constants.ts | 3 + .../javascripts/Constants/ElementIDs.ts | 2 + .../Controllers/FilesController.ts | 18 +- .../Navigation/NavigationController.ts | 6 + app/assets/stylesheets/_columns.scss | 11 +- app/assets/stylesheets/_editor.scss | 26 +- app/assets/stylesheets/utils/_padding.scss | 4 + 33 files changed, 661 insertions(+), 382 deletions(-) create mode 100644 app/assets/javascripts/Components/FilePreview/CreateObjectURLWithRef.tsx create mode 100644 app/assets/javascripts/Components/FilePreview/FilePreview.tsx create mode 100644 app/assets/javascripts/Components/FilePreview/FilePreviewError.tsx rename app/assets/javascripts/Components/{Files => FilePreview}/FilePreviewInfoPanel.tsx (100%) create mode 100644 app/assets/javascripts/Components/FilePreview/FilePreviewModal.tsx rename app/assets/javascripts/Components/{Files => FilePreview}/ImagePreview.tsx (98%) create mode 100644 app/assets/javascripts/Components/FilePreview/PreviewComponent.tsx create mode 100644 app/assets/javascripts/Components/FilePreview/TextPreview.tsx rename app/assets/javascripts/Components/{Files => FilePreview}/isFilePreviewable.ts (59%) create mode 100644 app/assets/javascripts/Components/FileView/FileView.tsx create mode 100644 app/assets/javascripts/Components/FileView/FileViewProps.tsx create mode 100644 app/assets/javascripts/Components/FileView/FileViewWithoutProtection.tsx delete mode 100644 app/assets/javascripts/Components/Files/FilePreviewModal.tsx delete mode 100644 app/assets/javascripts/Components/Files/PreviewComponent.tsx rename app/assets/javascripts/Components/{ProtectedNoteOverlay/ProtectedNoteOverlay.tsx => ProtectedItemOverlay/ProtectedItemOverlay.tsx} (60%) diff --git a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx index 50a2005ca..cd3d0086e 100644 --- a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -19,7 +19,7 @@ import PremiumModalProvider from '@/Hooks/usePremiumModal' import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal' import TagsContextMenuWrapper from '@/Components/Tags/TagContextMenu' import { ToastContainer } from '@standardnotes/stylekit' -import FilePreviewModalWrapper from '@/Components/Files/FilePreviewModal' +import FilePreviewModalWrapper from '@/Components/FilePreview/FilePreviewModal' import ContentListView from '@/Components/ContentListView/ContentListView' import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu' import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper' diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx index 20f49327e..3b95c318a 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx @@ -1,5 +1,4 @@ import { WebApplication } from '@/Application/Application' -import { ViewControllerManager } from '@/Services/ViewControllerManager' import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import VisuallyHidden from '@reach/visually-hidden' @@ -14,20 +13,33 @@ import AttachedFilesPopover from './AttachedFilesPopover' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { PopoverTabs } from './PopoverTabs' import { isHandlingFileDrag } from '@/Utils/DragTypeCheck' +import { NotesController } from '@/Controllers/NotesController' +import { FilePreviewModalController } from '@/Controllers/FilePreviewModalController' +import { NavigationController } from '@/Controllers/Navigation/NavigationController' +import { FeaturesController } from '@/Controllers/FeaturesController' +import { FilesController } from '@/Controllers/FilesController' type Props = { application: WebApplication - viewControllerManager: ViewControllerManager + featuresController: FeaturesController + filePreviewModalController: FilePreviewModalController + filesController: FilesController + navigationController: NavigationController + notesController: NotesController onClickPreprocessing?: () => Promise } const AttachedFilesButton: FunctionComponent = ({ application, - viewControllerManager, + featuresController, + filesController, + filePreviewModalController, + navigationController, + notesController, onClickPreprocessing, }: Props) => { const premiumModal = usePremiumModal() - const note: SNNote | undefined = viewControllerManager.notesController.firstSelectedNote + const note: SNNote | undefined = notesController.firstSelectedNote const [open, setOpen] = useState(false) const [position, setPosition] = useState({ @@ -41,14 +53,16 @@ const AttachedFilesButton: FunctionComponent = ({ const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen) useEffect(() => { - if (viewControllerManager.filePreviewModalController.isOpen) { + if (filePreviewModalController.isOpen) { keepMenuOpen(true) } else { keepMenuOpen(false) } - }, [viewControllerManager.filePreviewModalController.isOpen, keepMenuOpen]) + }, [filePreviewModalController.isOpen, keepMenuOpen]) - const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles) + const [currentTab, setCurrentTab] = useState( + navigationController.isInFilesView ? PopoverTabs.AllFiles : PopoverTabs.AttachedFiles, + ) const [allFiles, setAllFiles] = useState([]) const [attachedFiles, setAttachedFiles] = useState([]) const attachedFilesCount = attachedFiles.length @@ -92,10 +106,10 @@ const AttachedFilesButton: FunctionComponent = ({ }, [onClickPreprocessing, open]) const prospectivelyShowFilesPremiumModal = useCallback(() => { - if (!viewControllerManager.featuresController.hasFiles) { + if (!featuresController.hasFiles) { premiumModal.activate('Files') } - }, [viewControllerManager.featuresController.hasFiles, premiumModal]) + }, [featuresController.hasFiles, premiumModal]) const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => { prospectivelyShowFilesPremiumModal() @@ -192,7 +206,7 @@ const AttachedFilesButton: FunctionComponent = ({ setIsDraggingFiles(false) - if (!viewControllerManager.featuresController.hasFiles) { + if (!featuresController.hasFiles) { prospectivelyShowFilesPremiumModal() return } @@ -207,7 +221,7 @@ const AttachedFilesButton: FunctionComponent = ({ return } - const uploadedFiles = await viewControllerManager.filesController.uploadNewFile(fileOrHandle) + const uploadedFiles = await filesController.uploadNewFile(fileOrHandle) if (!uploadedFiles) { return @@ -225,8 +239,8 @@ const AttachedFilesButton: FunctionComponent = ({ } }, [ - viewControllerManager.filesController, - viewControllerManager.featuresController.hasFiles, + filesController, + featuresController.hasFiles, attachFileToNote, currentTab, application, @@ -283,13 +297,14 @@ const AttachedFilesButton: FunctionComponent = ({ {open && ( )} diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx index a827a0fc3..5c005c338 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx @@ -20,6 +20,7 @@ type Props = { currentTab: PopoverTabs isDraggingFiles: boolean setCurrentTab: Dispatch> + attachedTabDisabled: boolean } const AttachedFilesPopover: FunctionComponent = ({ @@ -31,6 +32,7 @@ const AttachedFilesPopover: FunctionComponent = ({ currentTab, isDraggingFiles, setCurrentTab, + attachedTabDisabled, }) => { const [searchQuery, setSearchQuery] = useState('') const searchInputRef = useRef(null) @@ -81,11 +83,12 @@ const AttachedFilesPopover: FunctionComponent = ({ id={PopoverTabs.AttachedFiles} className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${ currentTab === PopoverTabs.AttachedFiles ? 'color-info font-medium shadow-bottom' : 'color-text' - }`} + } ${attachedTabDisabled ? 'color-neutral cursor-not-allowed' : ''}`} onClick={() => { setCurrentTab(PopoverTabs.AttachedFiles) }} onBlur={closeOnBlur} + disabled={attachedTabDisabled} > Attached diff --git a/app/assets/javascripts/Components/ContentListView/ContentList.tsx b/app/assets/javascripts/Components/ContentListView/ContentList.tsx index 8e5709e75..45d20aa20 100644 --- a/app/assets/javascripts/Components/ContentListView/ContentList.tsx +++ b/app/assets/javascripts/Components/ContentListView/ContentList.tsx @@ -64,7 +64,6 @@ const ContentList: FunctionComponent = ({ = ({ hideTags={hideTags} hideIcon={hideEditorIcon} sortBy={sortBy} + filesController={viewControllerManager.filesController} + selectionController={viewControllerManager.selectionController} + navigationController={viewControllerManager.navigationController} + notesController={viewControllerManager.notesController} /> ))} diff --git a/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx b/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx index ab4ebcbaf..ce3b64556 100644 --- a/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx +++ b/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx @@ -10,7 +10,7 @@ const ContentListItem: FunctionComponent = (props) => { return [] } - const selectedTag = props.viewControllerManager.navigationController.selected + const selectedTag = props.navigationController.selected if (!selectedTag) { return [] } diff --git a/app/assets/javascripts/Components/ContentListView/FileListItem.tsx b/app/assets/javascripts/Components/ContentListView/FileListItem.tsx index b1c32c664..b99d8cadf 100644 --- a/app/assets/javascripts/Components/ContentListView/FileListItem.tsx +++ b/app/assets/javascripts/Components/ContentListView/FileListItem.tsx @@ -10,7 +10,8 @@ import { DisplayableListItemProps } from './Types/DisplayableListItemProps' const FileListItem: FunctionComponent = ({ application, - viewControllerManager, + filesController, + selectionController, hideDate, hideIcon, hideTags, @@ -21,40 +22,28 @@ const FileListItem: FunctionComponent = ({ }) => { const openFileContextMenu = useCallback( (posX: number, posY: number) => { - viewControllerManager.filesController.setFileContextMenuLocation({ + filesController.setFileContextMenuLocation({ x: posX, y: posY, }) - viewControllerManager.filesController.setShowFileContextMenu(true) + filesController.setShowFileContextMenu(true) }, - [viewControllerManager.filesController], + [filesController], ) const openContextMenu = useCallback( async (posX: number, posY: number) => { - const { didSelect } = await viewControllerManager.selectionController.selectItem(item.uuid) + const { didSelect } = await selectionController.selectItem(item.uuid) if (didSelect) { openFileContextMenu(posX, posY) } }, - [viewControllerManager.selectionController, item.uuid, openFileContextMenu], + [selectionController, item.uuid, openFileContextMenu], ) const onClick = useCallback(() => { - void viewControllerManager.selectionController.selectItem(item.uuid, true).then(({ didSelect }) => { - if (didSelect && viewControllerManager.selectionController.selectedItemsCount < 2) { - viewControllerManager.filePreviewModalController.activate( - item as FileItem, - viewControllerManager.filesController.allFiles, - ) - } - }) - }, [ - viewControllerManager.filePreviewModalController, - viewControllerManager.filesController.allFiles, - viewControllerManager.selectionController, - item, - ]) + void selectionController.selectItem(item.uuid, true) + }, [item.uuid, selectionController]) const IconComponent = () => getFileIconComponent( diff --git a/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx b/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx index 7672fe7e8..f55029e00 100644 --- a/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx +++ b/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx @@ -11,7 +11,8 @@ import { DisplayableListItemProps } from './Types/DisplayableListItemProps' const NoteListItem: FunctionComponent = ({ application, - viewControllerManager, + notesController, + selectionController, hideDate, hideIcon, hideTags, @@ -27,16 +28,16 @@ const NoteListItem: FunctionComponent = ({ const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0 const openNoteContextMenu = (posX: number, posY: number) => { - viewControllerManager.notesController.setContextMenuClickLocation({ + notesController.setContextMenuClickLocation({ x: posX, y: posY, }) - viewControllerManager.notesController.reloadContextMenuLayout() - viewControllerManager.notesController.setContextMenuOpen(true) + notesController.reloadContextMenuLayout() + notesController.setContextMenuOpen(true) } const openContextMenu = async (posX: number, posY: number) => { - const { didSelect } = await viewControllerManager.selectionController.selectItem(item.uuid, true) + const { didSelect } = await selectionController.selectItem(item.uuid, true) if (didSelect) { openNoteContextMenu(posX, posY) } @@ -49,7 +50,7 @@ const NoteListItem: FunctionComponent = ({ }`} id={item.uuid} onClick={() => { - void viewControllerManager.selectionController.selectItem(item.uuid, true) + void selectionController.selectItem(item.uuid, true) }} onContextMenu={(event) => { event.preventDefault() diff --git a/app/assets/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts b/app/assets/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts index 8b1b2ded4..8b294d03c 100644 --- a/app/assets/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts +++ b/app/assets/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts @@ -1,11 +1,17 @@ import { WebApplication } from '@/Application/Application' -import { ViewControllerManager } from '@/Services/ViewControllerManager' +import { FilesController } from '@/Controllers/FilesController' +import { NavigationController } from '@/Controllers/Navigation/NavigationController' +import { NotesController } from '@/Controllers/NotesController' +import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { SortableItem } from '@standardnotes/snjs' import { ListableContentItem } from './ListableContentItem' export type AbstractListItemProps = { application: WebApplication - viewControllerManager: ViewControllerManager + filesController: FilesController + selectionController: SelectedItemsController + navigationController: NavigationController + notesController: NotesController hideDate: boolean hideIcon: boolean hideTags: boolean diff --git a/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx b/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx index d129174ce..828f25a0a 100644 --- a/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx +++ b/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx @@ -134,6 +134,13 @@ const FileMenuOptions: FunctionComponent = ({ Delete permanently + {selectedFiles.length === 1 && ( +
+
+ File ID: {selectedFiles[0].uuid} +
+
+ )} ) } diff --git a/app/assets/javascripts/Components/FilePreview/CreateObjectURLWithRef.tsx b/app/assets/javascripts/Components/FilePreview/CreateObjectURLWithRef.tsx new file mode 100644 index 000000000..6eae4de1a --- /dev/null +++ b/app/assets/javascripts/Components/FilePreview/CreateObjectURLWithRef.tsx @@ -0,0 +1,18 @@ +import { FileItem } from '@standardnotes/snjs' +import { MutableRefObject } from 'react' + +export const createObjectURLWithRef = ( + type: FileItem['mimeType'], + bytes: Uint8Array, + ref: MutableRefObject, +) => { + const objectURL = URL.createObjectURL( + new Blob([bytes], { + type, + }), + ) + + ref.current = objectURL + + return objectURL +} diff --git a/app/assets/javascripts/Components/FilePreview/FilePreview.tsx b/app/assets/javascripts/Components/FilePreview/FilePreview.tsx new file mode 100644 index 000000000..bdadb3b49 --- /dev/null +++ b/app/assets/javascripts/Components/FilePreview/FilePreview.tsx @@ -0,0 +1,81 @@ +import { WebApplication } from '@/Application/Application' +import { concatenateUint8Arrays } from '@/Utils' +import { FileItem } from '@standardnotes/snjs' +import { useEffect, useMemo, useState } from 'react' +import FilePreviewError from './FilePreviewError' +import { isFileTypePreviewable } from './isFilePreviewable' +import PreviewComponent from './PreviewComponent' + +type Props = { + application: WebApplication + file: FileItem +} + +const FilePreview = ({ file, application }: Props) => { + const isFilePreviewable = useMemo(() => { + return isFileTypePreviewable(file.mimeType) + }, [file.mimeType]) + + const [isDownloading, setIsDownloading] = useState(true) + const [downloadProgress, setDownloadProgress] = useState(0) + const [downloadedBytes, setDownloadedBytes] = useState() + + useEffect(() => { + if (!isFilePreviewable) { + setIsDownloading(false) + setDownloadProgress(0) + setDownloadedBytes(undefined) + return + } + + const downloadFileForPreview = async () => { + if (downloadedBytes) { + return + } + + setIsDownloading(true) + + try { + const chunks: Uint8Array[] = [] + setDownloadProgress(0) + await application.files.downloadFile(file, async (decryptedChunk, progress) => { + chunks.push(decryptedChunk) + if (progress) { + setDownloadProgress(Math.round(progress.percentComplete)) + } + }) + const finalDecryptedBytes = concatenateUint8Arrays(chunks) + setDownloadedBytes(finalDecryptedBytes) + } catch (error) { + console.error(error) + } finally { + setIsDownloading(false) + } + } + + void downloadFileForPreview() + }, [application.files, downloadedBytes, file, isFilePreviewable]) + + return isDownloading ? ( +
+
+
+
{downloadProgress}%
+
+ Loading file... +
+ ) : downloadedBytes ? ( + + ) : ( + { + setDownloadedBytes(undefined) + }} + isFilePreviewable={isFilePreviewable} + /> + ) +} + +export default FilePreview diff --git a/app/assets/javascripts/Components/FilePreview/FilePreviewError.tsx b/app/assets/javascripts/Components/FilePreview/FilePreviewError.tsx new file mode 100644 index 000000000..fbb41a70e --- /dev/null +++ b/app/assets/javascripts/Components/FilePreview/FilePreviewError.tsx @@ -0,0 +1,62 @@ +import { FilesController } from '@/Controllers/FilesController' +import { NoPreviewIllustration } from '@standardnotes/icons' +import { FileItem } from '@standardnotes/snjs' +import Button from '../Button/Button' + +type Props = { + file: FileItem + filesController: FilesController + isFilePreviewable: boolean + tryAgainCallback: () => void +} + +const FilePreviewError = ({ file, filesController, isFilePreviewable, tryAgainCallback }: Props) => { + return ( +
+ +
This file can't be previewed.
+ {isFilePreviewable ? ( + <> +
+ There was an error loading the file. Try again, or download the file and open it using another application. +
+
+ + +
+ + ) : ( + <> +
+ To view this file, download it and open it using another application. +
+ + + )} +
+ ) +} + +export default FilePreviewError diff --git a/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx b/app/assets/javascripts/Components/FilePreview/FilePreviewInfoPanel.tsx similarity index 100% rename from app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx rename to app/assets/javascripts/Components/FilePreview/FilePreviewInfoPanel.tsx diff --git a/app/assets/javascripts/Components/FilePreview/FilePreviewModal.tsx b/app/assets/javascripts/Components/FilePreview/FilePreviewModal.tsx new file mode 100644 index 000000000..f2aa1ef08 --- /dev/null +++ b/app/assets/javascripts/Components/FilePreview/FilePreviewModal.tsx @@ -0,0 +1,134 @@ +import { WebApplication } from '@/Application/Application' +import { DialogContent, DialogOverlay } from '@reach/dialog' +import { FunctionComponent, KeyboardEventHandler, useCallback, useMemo, useRef, useState } from 'react' +import { getFileIconComponent } from '@/Components/AttachedFilesPopover/getFileIconComponent' +import Icon from '@/Components/Icon/Icon' +import FilePreviewInfoPanel from './FilePreviewInfoPanel' +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' +import { KeyboardKey } from '@/Services/IOService' +import { ViewControllerManager } from '@/Services/ViewControllerManager' +import { observer } from 'mobx-react-lite' +import FilePreview from './FilePreview' + +type Props = { + application: WebApplication + viewControllerManager: ViewControllerManager +} + +const FilePreviewModal: FunctionComponent = observer(({ application, viewControllerManager }) => { + const { currentFile, setCurrentFile, otherFiles, dismiss } = viewControllerManager.filePreviewModalController + + if (!currentFile) { + return null + } + + const [showFileInfoPanel, setShowFileInfoPanel] = useState(false) + const closeButtonRef = useRef(null) + + const keyDownHandler: KeyboardEventHandler = useCallback( + (event) => { + const hasNotPressedLeftOrRightKeys = event.key !== KeyboardKey.Left && event.key !== KeyboardKey.Right + + if (hasNotPressedLeftOrRightKeys) { + return + } + + event.preventDefault() + + const currentFileIndex = otherFiles.findIndex((fileFromArray) => fileFromArray.uuid === currentFile.uuid) + + switch (event.key) { + case KeyboardKey.Left: { + const previousFileIndex = currentFileIndex - 1 >= 0 ? currentFileIndex - 1 : otherFiles.length - 1 + const previousFile = otherFiles[previousFileIndex] + if (previousFile) { + setCurrentFile(previousFile) + } + break + } + case KeyboardKey.Right: { + const nextFileIndex = currentFileIndex + 1 < otherFiles.length ? currentFileIndex + 1 : 0 + const nextFile = otherFiles[nextFileIndex] + if (nextFile) { + setCurrentFile(nextFile) + } + break + } + } + }, + [currentFile.uuid, otherFiles, setCurrentFile], + ) + + const IconComponent = useMemo( + () => + getFileIconComponent( + application.iconsController.getIconForFileType(currentFile.mimeType), + 'w-6 h-6 flex-shrink-0', + ), + [application.iconsController, currentFile.mimeType], + ) + + return ( + + +
+
+
{IconComponent}
+ {currentFile.name} +
+
+ + +
+
+
+
+ +
+ {showFileInfoPanel && } +
+
+
+ ) +}) + +FilePreviewModal.displayName = 'FilePreviewModal' + +const FilePreviewModalWrapper: FunctionComponent = ({ application, viewControllerManager }) => { + return viewControllerManager.filePreviewModalController.isOpen ? ( + + ) : null +} + +export default observer(FilePreviewModalWrapper) diff --git a/app/assets/javascripts/Components/Files/ImagePreview.tsx b/app/assets/javascripts/Components/FilePreview/ImagePreview.tsx similarity index 98% rename from app/assets/javascripts/Components/Files/ImagePreview.tsx rename to app/assets/javascripts/Components/FilePreview/ImagePreview.tsx index 6d4e4fb82..677c26a59 100644 --- a/app/assets/javascripts/Components/Files/ImagePreview.tsx +++ b/app/assets/javascripts/Components/FilePreview/ImagePreview.tsx @@ -12,7 +12,7 @@ const ImagePreview: FunctionComponent = ({ objectUrl }) => { const [imageZoomPercent, setImageZoomPercent] = useState(100) return ( -
+
= ({ objectUrl }) => { : { position: 'absolute', top: 0, + left: 0, margin: 'auto', }), }} diff --git a/app/assets/javascripts/Components/FilePreview/PreviewComponent.tsx b/app/assets/javascripts/Components/FilePreview/PreviewComponent.tsx new file mode 100644 index 000000000..632247cde --- /dev/null +++ b/app/assets/javascripts/Components/FilePreview/PreviewComponent.tsx @@ -0,0 +1,54 @@ +import { FileItem } from '@standardnotes/snjs' +import { FunctionComponent, useEffect, useMemo, useRef } from 'react' +import { createObjectURLWithRef } from './CreateObjectURLWithRef' +import ImagePreview from './ImagePreview' +import { PreviewableTextFileTypes } from './isFilePreviewable' +import TextPreview from './TextPreview' + +type Props = { + file: FileItem + bytes: Uint8Array +} + +const PreviewComponent: FunctionComponent = ({ file, bytes }) => { + const objectUrlRef = useRef() + + const objectUrl = useMemo(() => { + return createObjectURLWithRef(file.mimeType, bytes, objectUrlRef) + }, [bytes, file.mimeType]) + + useEffect(() => { + const objectUrl = objectUrlRef.current + + return () => { + if (objectUrl) { + URL.revokeObjectURL(objectUrl) + objectUrlRef.current = '' + } + } + }, []) + + if (file.mimeType.startsWith('image/')) { + return + } + + if (file.mimeType.startsWith('video/')) { + return