feat: add file view (#1064)
This commit is contained in:
@@ -19,7 +19,7 @@ import PremiumModalProvider from '@/Hooks/usePremiumModal'
|
|||||||
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
||||||
import TagsContextMenuWrapper from '@/Components/Tags/TagContextMenu'
|
import TagsContextMenuWrapper from '@/Components/Tags/TagContextMenu'
|
||||||
import { ToastContainer } from '@standardnotes/stylekit'
|
import { ToastContainer } from '@standardnotes/stylekit'
|
||||||
import FilePreviewModalWrapper from '@/Components/Files/FilePreviewModal'
|
import FilePreviewModalWrapper from '@/Components/FilePreview/FilePreviewModal'
|
||||||
import ContentListView from '@/Components/ContentListView/ContentListView'
|
import ContentListView from '@/Components/ContentListView/ContentListView'
|
||||||
import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu'
|
import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu'
|
||||||
import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper'
|
import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper'
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
|
||||||
import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
|
import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||||
import VisuallyHidden from '@reach/visually-hidden'
|
import VisuallyHidden from '@reach/visually-hidden'
|
||||||
@@ -14,20 +13,33 @@ import AttachedFilesPopover from './AttachedFilesPopover'
|
|||||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||||
import { PopoverTabs } from './PopoverTabs'
|
import { PopoverTabs } from './PopoverTabs'
|
||||||
import { isHandlingFileDrag } from '@/Utils/DragTypeCheck'
|
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 = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
viewControllerManager: ViewControllerManager
|
featuresController: FeaturesController
|
||||||
|
filePreviewModalController: FilePreviewModalController
|
||||||
|
filesController: FilesController
|
||||||
|
navigationController: NavigationController
|
||||||
|
notesController: NotesController
|
||||||
onClickPreprocessing?: () => Promise<void>
|
onClickPreprocessing?: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachedFilesButton: FunctionComponent<Props> = ({
|
const AttachedFilesButton: FunctionComponent<Props> = ({
|
||||||
application,
|
application,
|
||||||
viewControllerManager,
|
featuresController,
|
||||||
|
filesController,
|
||||||
|
filePreviewModalController,
|
||||||
|
navigationController,
|
||||||
|
notesController,
|
||||||
onClickPreprocessing,
|
onClickPreprocessing,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const premiumModal = usePremiumModal()
|
const premiumModal = usePremiumModal()
|
||||||
const note: SNNote | undefined = viewControllerManager.notesController.firstSelectedNote
|
const note: SNNote | undefined = notesController.firstSelectedNote
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [position, setPosition] = useState({
|
const [position, setPosition] = useState({
|
||||||
@@ -41,14 +53,16 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
|||||||
const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen)
|
const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (viewControllerManager.filePreviewModalController.isOpen) {
|
if (filePreviewModalController.isOpen) {
|
||||||
keepMenuOpen(true)
|
keepMenuOpen(true)
|
||||||
} else {
|
} else {
|
||||||
keepMenuOpen(false)
|
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<FileItem[]>([])
|
const [allFiles, setAllFiles] = useState<FileItem[]>([])
|
||||||
const [attachedFiles, setAttachedFiles] = useState<FileItem[]>([])
|
const [attachedFiles, setAttachedFiles] = useState<FileItem[]>([])
|
||||||
const attachedFilesCount = attachedFiles.length
|
const attachedFilesCount = attachedFiles.length
|
||||||
@@ -92,10 +106,10 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
|||||||
}, [onClickPreprocessing, open])
|
}, [onClickPreprocessing, open])
|
||||||
|
|
||||||
const prospectivelyShowFilesPremiumModal = useCallback(() => {
|
const prospectivelyShowFilesPremiumModal = useCallback(() => {
|
||||||
if (!viewControllerManager.featuresController.hasFiles) {
|
if (!featuresController.hasFiles) {
|
||||||
premiumModal.activate('Files')
|
premiumModal.activate('Files')
|
||||||
}
|
}
|
||||||
}, [viewControllerManager.featuresController.hasFiles, premiumModal])
|
}, [featuresController.hasFiles, premiumModal])
|
||||||
|
|
||||||
const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => {
|
const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => {
|
||||||
prospectivelyShowFilesPremiumModal()
|
prospectivelyShowFilesPremiumModal()
|
||||||
@@ -192,7 +206,7 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
|||||||
|
|
||||||
setIsDraggingFiles(false)
|
setIsDraggingFiles(false)
|
||||||
|
|
||||||
if (!viewControllerManager.featuresController.hasFiles) {
|
if (!featuresController.hasFiles) {
|
||||||
prospectivelyShowFilesPremiumModal()
|
prospectivelyShowFilesPremiumModal()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -207,7 +221,7 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedFiles = await viewControllerManager.filesController.uploadNewFile(fileOrHandle)
|
const uploadedFiles = await filesController.uploadNewFile(fileOrHandle)
|
||||||
|
|
||||||
if (!uploadedFiles) {
|
if (!uploadedFiles) {
|
||||||
return
|
return
|
||||||
@@ -225,8 +239,8 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
viewControllerManager.filesController,
|
filesController,
|
||||||
viewControllerManager.featuresController.hasFiles,
|
featuresController.hasFiles,
|
||||||
attachFileToNote,
|
attachFileToNote,
|
||||||
currentTab,
|
currentTab,
|
||||||
application,
|
application,
|
||||||
@@ -283,13 +297,14 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
|||||||
{open && (
|
{open && (
|
||||||
<AttachedFilesPopover
|
<AttachedFilesPopover
|
||||||
application={application}
|
application={application}
|
||||||
filesController={viewControllerManager.filesController}
|
filesController={filesController}
|
||||||
attachedFiles={attachedFiles}
|
attachedFiles={attachedFiles}
|
||||||
allFiles={allFiles}
|
allFiles={allFiles}
|
||||||
closeOnBlur={closeOnBlur}
|
closeOnBlur={closeOnBlur}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
isDraggingFiles={isDraggingFiles}
|
isDraggingFiles={isDraggingFiles}
|
||||||
setCurrentTab={setCurrentTab}
|
setCurrentTab={setCurrentTab}
|
||||||
|
attachedTabDisabled={navigationController.isInFilesView}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type Props = {
|
|||||||
currentTab: PopoverTabs
|
currentTab: PopoverTabs
|
||||||
isDraggingFiles: boolean
|
isDraggingFiles: boolean
|
||||||
setCurrentTab: Dispatch<SetStateAction<PopoverTabs>>
|
setCurrentTab: Dispatch<SetStateAction<PopoverTabs>>
|
||||||
|
attachedTabDisabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachedFilesPopover: FunctionComponent<Props> = ({
|
const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||||
@@ -31,6 +32,7 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
|||||||
currentTab,
|
currentTab,
|
||||||
isDraggingFiles,
|
isDraggingFiles,
|
||||||
setCurrentTab,
|
setCurrentTab,
|
||||||
|
attachedTabDisabled,
|
||||||
}) => {
|
}) => {
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -81,11 +83,12 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
|||||||
id={PopoverTabs.AttachedFiles}
|
id={PopoverTabs.AttachedFiles}
|
||||||
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
|
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'
|
currentTab === PopoverTabs.AttachedFiles ? 'color-info font-medium shadow-bottom' : 'color-text'
|
||||||
}`}
|
} ${attachedTabDisabled ? 'color-neutral cursor-not-allowed' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentTab(PopoverTabs.AttachedFiles)
|
setCurrentTab(PopoverTabs.AttachedFiles)
|
||||||
}}
|
}}
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
|
disabled={attachedTabDisabled}
|
||||||
>
|
>
|
||||||
Attached
|
Attached
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ const ContentList: FunctionComponent<Props> = ({
|
|||||||
<ContentListItem
|
<ContentListItem
|
||||||
key={item.uuid}
|
key={item.uuid}
|
||||||
application={application}
|
application={application}
|
||||||
viewControllerManager={viewControllerManager}
|
|
||||||
item={item}
|
item={item}
|
||||||
selected={!!selectedItems[item.uuid]}
|
selected={!!selectedItems[item.uuid]}
|
||||||
hideDate={hideDate}
|
hideDate={hideDate}
|
||||||
@@ -72,6 +71,10 @@ const ContentList: FunctionComponent<Props> = ({
|
|||||||
hideTags={hideTags}
|
hideTags={hideTags}
|
||||||
hideIcon={hideEditorIcon}
|
hideIcon={hideEditorIcon}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
|
filesController={viewControllerManager.filesController}
|
||||||
|
selectionController={viewControllerManager.selectionController}
|
||||||
|
navigationController={viewControllerManager.navigationController}
|
||||||
|
notesController={viewControllerManager.notesController}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTag = props.viewControllerManager.navigationController.selected
|
const selectedTag = props.navigationController.selected
|
||||||
if (!selectedTag) {
|
if (!selectedTag) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
|||||||
|
|
||||||
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||||
application,
|
application,
|
||||||
viewControllerManager,
|
filesController,
|
||||||
|
selectionController,
|
||||||
hideDate,
|
hideDate,
|
||||||
hideIcon,
|
hideIcon,
|
||||||
hideTags,
|
hideTags,
|
||||||
@@ -21,40 +22,28 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const openFileContextMenu = useCallback(
|
const openFileContextMenu = useCallback(
|
||||||
(posX: number, posY: number) => {
|
(posX: number, posY: number) => {
|
||||||
viewControllerManager.filesController.setFileContextMenuLocation({
|
filesController.setFileContextMenuLocation({
|
||||||
x: posX,
|
x: posX,
|
||||||
y: posY,
|
y: posY,
|
||||||
})
|
})
|
||||||
viewControllerManager.filesController.setShowFileContextMenu(true)
|
filesController.setShowFileContextMenu(true)
|
||||||
},
|
},
|
||||||
[viewControllerManager.filesController],
|
[filesController],
|
||||||
)
|
)
|
||||||
|
|
||||||
const openContextMenu = useCallback(
|
const openContextMenu = useCallback(
|
||||||
async (posX: number, posY: number) => {
|
async (posX: number, posY: number) => {
|
||||||
const { didSelect } = await viewControllerManager.selectionController.selectItem(item.uuid)
|
const { didSelect } = await selectionController.selectItem(item.uuid)
|
||||||
if (didSelect) {
|
if (didSelect) {
|
||||||
openFileContextMenu(posX, posY)
|
openFileContextMenu(posX, posY)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[viewControllerManager.selectionController, item.uuid, openFileContextMenu],
|
[selectionController, item.uuid, openFileContextMenu],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
void viewControllerManager.selectionController.selectItem(item.uuid, true).then(({ didSelect }) => {
|
void selectionController.selectItem(item.uuid, true)
|
||||||
if (didSelect && viewControllerManager.selectionController.selectedItemsCount < 2) {
|
}, [item.uuid, selectionController])
|
||||||
viewControllerManager.filePreviewModalController.activate(
|
|
||||||
item as FileItem,
|
|
||||||
viewControllerManager.filesController.allFiles,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [
|
|
||||||
viewControllerManager.filePreviewModalController,
|
|
||||||
viewControllerManager.filesController.allFiles,
|
|
||||||
viewControllerManager.selectionController,
|
|
||||||
item,
|
|
||||||
])
|
|
||||||
|
|
||||||
const IconComponent = () =>
|
const IconComponent = () =>
|
||||||
getFileIconComponent(
|
getFileIconComponent(
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
|||||||
|
|
||||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||||
application,
|
application,
|
||||||
viewControllerManager,
|
notesController,
|
||||||
|
selectionController,
|
||||||
hideDate,
|
hideDate,
|
||||||
hideIcon,
|
hideIcon,
|
||||||
hideTags,
|
hideTags,
|
||||||
@@ -27,16 +28,16 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0
|
const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0
|
||||||
|
|
||||||
const openNoteContextMenu = (posX: number, posY: number) => {
|
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||||
viewControllerManager.notesController.setContextMenuClickLocation({
|
notesController.setContextMenuClickLocation({
|
||||||
x: posX,
|
x: posX,
|
||||||
y: posY,
|
y: posY,
|
||||||
})
|
})
|
||||||
viewControllerManager.notesController.reloadContextMenuLayout()
|
notesController.reloadContextMenuLayout()
|
||||||
viewControllerManager.notesController.setContextMenuOpen(true)
|
notesController.setContextMenuOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openContextMenu = async (posX: number, posY: number) => {
|
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) {
|
if (didSelect) {
|
||||||
openNoteContextMenu(posX, posY)
|
openNoteContextMenu(posX, posY)
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
id={item.uuid}
|
id={item.uuid}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void viewControllerManager.selectionController.selectItem(item.uuid, true)
|
void selectionController.selectItem(item.uuid, true)
|
||||||
}}
|
}}
|
||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
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 { SortableItem } from '@standardnotes/snjs'
|
||||||
import { ListableContentItem } from './ListableContentItem'
|
import { ListableContentItem } from './ListableContentItem'
|
||||||
|
|
||||||
export type AbstractListItemProps = {
|
export type AbstractListItemProps = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
viewControllerManager: ViewControllerManager
|
filesController: FilesController
|
||||||
|
selectionController: SelectedItemsController
|
||||||
|
navigationController: NavigationController
|
||||||
|
notesController: NotesController
|
||||||
hideDate: boolean
|
hideDate: boolean
|
||||||
hideIcon: boolean
|
hideIcon: boolean
|
||||||
hideTags: boolean
|
hideTags: boolean
|
||||||
|
|||||||
@@ -134,6 +134,13 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
<Icon type="trash" className="mr-2 color-danger" />
|
<Icon type="trash" className="mr-2 color-danger" />
|
||||||
<span className="color-danger">Delete permanently</span>
|
<span className="color-danger">Delete permanently</span>
|
||||||
</button>
|
</button>
|
||||||
|
{selectedFiles.length === 1 && (
|
||||||
|
<div className="px-3 pt-1.5 pb-0.5 text-xs color-neutral font-medium">
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold">File ID:</span> {selectedFiles[0].uuid}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { FileItem } from '@standardnotes/snjs'
|
||||||
|
import { MutableRefObject } from 'react'
|
||||||
|
|
||||||
|
export const createObjectURLWithRef = (
|
||||||
|
type: FileItem['mimeType'],
|
||||||
|
bytes: Uint8Array,
|
||||||
|
ref: MutableRefObject<string | undefined>,
|
||||||
|
) => {
|
||||||
|
const objectURL = URL.createObjectURL(
|
||||||
|
new Blob([bytes], {
|
||||||
|
type,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
ref.current = objectURL
|
||||||
|
|
||||||
|
return objectURL
|
||||||
|
}
|
||||||
@@ -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<Uint8Array>()
|
||||||
|
|
||||||
|
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 ? (
|
||||||
|
<div className="flex flex-col justify-center items-center flex-grow">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="sk-spinner w-5 h-5 spinner-info mr-3"></div>
|
||||||
|
<div className="text-base font-semibold">{downloadProgress}%</div>
|
||||||
|
</div>
|
||||||
|
<span className="mt-3">Loading file...</span>
|
||||||
|
</div>
|
||||||
|
) : downloadedBytes ? (
|
||||||
|
<PreviewComponent file={file} bytes={downloadedBytes} />
|
||||||
|
) : (
|
||||||
|
<FilePreviewError
|
||||||
|
file={file}
|
||||||
|
filesController={application.getViewControllerManager().filesController}
|
||||||
|
tryAgainCallback={() => {
|
||||||
|
setDownloadedBytes(undefined)
|
||||||
|
}}
|
||||||
|
isFilePreviewable={isFilePreviewable}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilePreview
|
||||||
@@ -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 (
|
||||||
|
<div className="flex flex-col justify-center items-center flex-grow">
|
||||||
|
<NoPreviewIllustration className="w-30 h-30 mb-4" />
|
||||||
|
<div className="font-bold text-base mb-2">This file can't be previewed.</div>
|
||||||
|
{isFilePreviewable ? (
|
||||||
|
<>
|
||||||
|
<div className="text-sm text-center color-passive-0 mb-4 max-w-35ch">
|
||||||
|
There was an error loading the file. Try again, or download the file and open it using another application.
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
className="mr-3"
|
||||||
|
onClick={() => {
|
||||||
|
tryAgainCallback()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="normal"
|
||||||
|
onClick={() => {
|
||||||
|
filesController.downloadFile(file).catch(console.error)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-sm text-center color-passive-0 mb-4 max-w-35ch">
|
||||||
|
To view this file, download it and open it using another application.
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
filesController.downloadFile(file).catch(console.error)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilePreviewError
|
||||||
@@ -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<Props> = observer(({ application, viewControllerManager }) => {
|
||||||
|
const { currentFile, setCurrentFile, otherFiles, dismiss } = viewControllerManager.filePreviewModalController
|
||||||
|
|
||||||
|
if (!currentFile) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [showFileInfoPanel, setShowFileInfoPanel] = useState(false)
|
||||||
|
const closeButtonRef = useRef<HTMLButtonElement>(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 (
|
||||||
|
<DialogOverlay
|
||||||
|
className="sn-component"
|
||||||
|
aria-label="File preview modal"
|
||||||
|
onDismiss={dismiss}
|
||||||
|
initialFocusRef={closeButtonRef}
|
||||||
|
dangerouslyBypassScrollLock
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
aria-label="File preview modal"
|
||||||
|
className="flex flex-col rounded shadow-overlay"
|
||||||
|
style={{
|
||||||
|
width: '90%',
|
||||||
|
maxWidth: '90%',
|
||||||
|
minHeight: '90%',
|
||||||
|
background: 'var(--modal-background-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-shrink-0 justify-between items-center min-h-6 px-4 py-3 border-0 border-b-1 border-solid border-main focus:shadow-none"
|
||||||
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
|
onKeyDown={keyDownHandler}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-6 h-6">{IconComponent}</div>
|
||||||
|
<span className="ml-3 font-medium">{currentFile.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
className="flex p-1.5 mr-4 bg-transparent hover:bg-contrast border-solid border-main border-1 cursor-pointer rounded"
|
||||||
|
onClick={() => setShowFileInfoPanel((show) => !show)}
|
||||||
|
>
|
||||||
|
<Icon type="info" className="color-neutral" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
ref={closeButtonRef}
|
||||||
|
onClick={dismiss}
|
||||||
|
aria-label="Close modal"
|
||||||
|
className="flex p-1 bg-transparent hover:bg-contrast border-0 cursor-pointer rounded"
|
||||||
|
>
|
||||||
|
<Icon type="close" className="color-neutral" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-grow min-h-0">
|
||||||
|
<div className="flex flex-grow items-center justify-center relative max-w-full">
|
||||||
|
<FilePreview file={currentFile} application={application} key={currentFile.uuid} />
|
||||||
|
</div>
|
||||||
|
{showFileInfoPanel && <FilePreviewInfoPanel file={currentFile} />}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogOverlay>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
FilePreviewModal.displayName = 'FilePreviewModal'
|
||||||
|
|
||||||
|
const FilePreviewModalWrapper: FunctionComponent<Props> = ({ application, viewControllerManager }) => {
|
||||||
|
return viewControllerManager.filePreviewModalController.isOpen ? (
|
||||||
|
<FilePreviewModal application={application} viewControllerManager={viewControllerManager} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(FilePreviewModalWrapper)
|
||||||
@@ -12,7 +12,7 @@ const ImagePreview: FunctionComponent<Props> = ({ objectUrl }) => {
|
|||||||
const [imageZoomPercent, setImageZoomPercent] = useState(100)
|
const [imageZoomPercent, setImageZoomPercent] = useState(100)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center w-full h-full">
|
<div className="flex items-center justify-center w-full h-full min-h-0">
|
||||||
<div className="flex items-center justify-center w-full h-full relative overflow-auto">
|
<div className="flex items-center justify-center w-full h-full relative overflow-auto">
|
||||||
<img
|
<img
|
||||||
src={objectUrl}
|
src={objectUrl}
|
||||||
@@ -26,6 +26,7 @@ const ImagePreview: FunctionComponent<Props> = ({ objectUrl }) => {
|
|||||||
: {
|
: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
left: 0,
|
||||||
margin: 'auto',
|
margin: 'auto',
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
@@ -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<Props> = ({ file, bytes }) => {
|
||||||
|
const objectUrlRef = useRef<string>()
|
||||||
|
|
||||||
|
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 <ImagePreview objectUrl={objectUrl} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.mimeType.startsWith('video/')) {
|
||||||
|
return <video className="w-full h-full" src={objectUrl} controls autoPlay />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.mimeType.startsWith('audio/')) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<audio src={objectUrl} controls />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PreviewableTextFileTypes.includes(file.mimeType)) {
|
||||||
|
return <TextPreview bytes={bytes} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <object className="w-full h-full" data={objectUrl} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreviewComponent
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
bytes: Uint8Array
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextPreview = ({ bytes }: Props) => {
|
||||||
|
const text = useMemo(() => {
|
||||||
|
const textDecoder = new TextDecoder()
|
||||||
|
return textDecoder.decode(bytes)
|
||||||
|
}, [bytes])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
autoComplete="off"
|
||||||
|
className="w-full h-full flex-grow font-editor focus:shadow-none focus:outline-none"
|
||||||
|
dir="auto"
|
||||||
|
id={ElementIds.FileTextPreview}
|
||||||
|
defaultValue={text}
|
||||||
|
readOnly={true}
|
||||||
|
></textarea>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextPreview
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
|
export const PreviewableTextFileTypes = ['text/plain', 'application/json']
|
||||||
|
|
||||||
export const isFileTypePreviewable = (fileType: string) => {
|
export const isFileTypePreviewable = (fileType: string) => {
|
||||||
const isImage = fileType.startsWith('image/')
|
const isImage = fileType.startsWith('image/')
|
||||||
const isVideo = fileType.startsWith('video/')
|
const isVideo = fileType.startsWith('video/')
|
||||||
const isAudio = fileType.startsWith('audio/')
|
const isAudio = fileType.startsWith('audio/')
|
||||||
const isPdf = fileType === 'application/pdf'
|
const isPdf = fileType === 'application/pdf'
|
||||||
|
const isText = PreviewableTextFileTypes.includes(fileType)
|
||||||
|
|
||||||
if (isImage || isVideo || isAudio || isPdf) {
|
if (isImage || isVideo || isAudio || isText || isPdf) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
36
app/assets/javascripts/Components/FileView/FileView.tsx
Normal file
36
app/assets/javascripts/Components/FileView/FileView.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
||||||
|
import FileViewWithoutProtection from './FileViewWithoutProtection'
|
||||||
|
import { FileViewProps } from './FileViewProps'
|
||||||
|
|
||||||
|
const FileView = ({ application, viewControllerManager, file }: FileViewProps) => {
|
||||||
|
const [shouldShowProtectedOverlay, setShouldShowProtectedOverlay] = useState(
|
||||||
|
file.protected && !application.hasProtectionSources(),
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShouldShowProtectedOverlay(viewControllerManager.filesController.showProtectedOverlay)
|
||||||
|
}, [viewControllerManager.filesController.showProtectedOverlay])
|
||||||
|
|
||||||
|
const dismissProtectedWarning = useCallback(() => {
|
||||||
|
void viewControllerManager.filesController.toggleFileProtection(file)
|
||||||
|
}, [file, viewControllerManager.filesController])
|
||||||
|
|
||||||
|
return shouldShowProtectedOverlay ? (
|
||||||
|
<div aria-label="Note" className="section editor sn-component">
|
||||||
|
<div className="h-full flex justify-center items-center">
|
||||||
|
<ProtectedItemOverlay
|
||||||
|
viewControllerManager={viewControllerManager}
|
||||||
|
hasProtectionSources={application.hasProtectionSources()}
|
||||||
|
onViewItem={dismissProtectedWarning}
|
||||||
|
itemType={'note'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<FileViewWithoutProtection application={application} viewControllerManager={viewControllerManager} file={file} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(FileView)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||||
|
import { FileItem } from '@standardnotes/snjs/dist/@types'
|
||||||
|
|
||||||
|
export type FileViewProps = {
|
||||||
|
application: WebApplication
|
||||||
|
viewControllerManager: ViewControllerManager
|
||||||
|
file: FileItem
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { ChangeEventHandler, FormEventHandler, useCallback, useEffect, useState } from 'react'
|
||||||
|
import AttachedFilesButton from '@/Components/AttachedFilesPopover/AttachedFilesButton'
|
||||||
|
import FileOptionsPanel from '@/Components/FileContextMenu/FileOptionsPanel'
|
||||||
|
import FilePreview from '@/Components/FilePreview/FilePreview'
|
||||||
|
import { FileViewProps } from './FileViewProps'
|
||||||
|
|
||||||
|
const FileViewWithoutProtection = ({ application, viewControllerManager, file }: FileViewProps) => {
|
||||||
|
const [name, setName] = useState(file.name)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setName(file.name)
|
||||||
|
}, [file.name])
|
||||||
|
|
||||||
|
const onTitleChange: ChangeEventHandler<HTMLInputElement> = useCallback(async (event) => {
|
||||||
|
setName(event.target.value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onFormSubmit: FormEventHandler = useCallback(
|
||||||
|
async (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
await application.items.renameFile(file, name)
|
||||||
|
|
||||||
|
void application.sync.sync()
|
||||||
|
},
|
||||||
|
[application.items, application.sync, file, name],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sn-component section editor" aria-label="File">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="content-title-bar section-title-bar w-full" id="file-title-bar">
|
||||||
|
<div className="flex items-center justify-between h-8">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<form onSubmit={onFormSubmit} className="title overflow-auto">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
id={ElementIds.FileTitleEditor}
|
||||||
|
onChange={onTitleChange}
|
||||||
|
onFocus={(event) => {
|
||||||
|
event.target.select()
|
||||||
|
}}
|
||||||
|
spellCheck={false}
|
||||||
|
value={name}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-3">
|
||||||
|
<AttachedFilesButton
|
||||||
|
application={application}
|
||||||
|
featuresController={viewControllerManager.featuresController}
|
||||||
|
filePreviewModalController={viewControllerManager.filePreviewModalController}
|
||||||
|
filesController={viewControllerManager.filesController}
|
||||||
|
navigationController={viewControllerManager.navigationController}
|
||||||
|
notesController={viewControllerManager.notesController}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FileOptionsPanel
|
||||||
|
filesController={viewControllerManager.filesController}
|
||||||
|
selectionController={viewControllerManager.selectionController}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FilePreview file={file} application={application} key={file.uuid} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(FileViewWithoutProtection)
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
|
||||||
import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
|
|
||||||
import { DialogContent, DialogOverlay } from '@reach/dialog'
|
|
||||||
import { addToast, ToastType } from '@standardnotes/stylekit'
|
|
||||||
import { NoPreviewIllustration } from '@standardnotes/icons'
|
|
||||||
import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import { getFileIconComponent } from '@/Components/AttachedFilesPopover/getFileIconComponent'
|
|
||||||
import Button from '@/Components/Button/Button'
|
|
||||||
import Icon from '@/Components/Icon/Icon'
|
|
||||||
import FilePreviewInfoPanel from './FilePreviewInfoPanel'
|
|
||||||
import { isFileTypePreviewable } from './isFilePreviewable'
|
|
||||||
import PreviewComponent from './PreviewComponent'
|
|
||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
|
||||||
import { KeyboardKey } from '@/Services/IOService'
|
|
||||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
|
||||||
import { observer } from 'mobx-react-lite'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
application: WebApplication
|
|
||||||
viewControllerManager: ViewControllerManager
|
|
||||||
}
|
|
||||||
|
|
||||||
const FilePreviewModal: FunctionComponent<Props> = observer(({ application, viewControllerManager }) => {
|
|
||||||
const { currentFile, setCurrentFile, otherFiles, dismiss } = viewControllerManager.filePreviewModalController
|
|
||||||
|
|
||||||
if (!currentFile) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const [objectUrl, setObjectUrl] = useState<string>()
|
|
||||||
const [isFilePreviewable, setIsFilePreviewable] = useState(false)
|
|
||||||
const [isLoadingFile, setIsLoadingFile] = useState(true)
|
|
||||||
const [fileDownloadProgress, setFileDownloadProgress] = useState(0)
|
|
||||||
const [showFileInfoPanel, setShowFileInfoPanel] = useState(false)
|
|
||||||
const currentFileIdRef = useRef<string>()
|
|
||||||
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
|
||||||
|
|
||||||
const getObjectUrl = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const chunks: Uint8Array[] = []
|
|
||||||
setFileDownloadProgress(0)
|
|
||||||
await application.files.downloadFile(currentFile, async (decryptedChunk, progress) => {
|
|
||||||
chunks.push(decryptedChunk)
|
|
||||||
if (progress) {
|
|
||||||
setFileDownloadProgress(Math.round(progress.percentComplete))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const finalDecryptedBytes = concatenateUint8Arrays(chunks)
|
|
||||||
setObjectUrl(
|
|
||||||
URL.createObjectURL(
|
|
||||||
new Blob([finalDecryptedBytes], {
|
|
||||||
type: currentFile.mimeType,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
setIsLoadingFile(false)
|
|
||||||
}
|
|
||||||
}, [application.files, currentFile])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsLoadingFile(true)
|
|
||||||
}, [currentFile.uuid])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isPreviewable = isFileTypePreviewable(currentFile.mimeType)
|
|
||||||
setIsFilePreviewable(isPreviewable)
|
|
||||||
|
|
||||||
if (!isPreviewable) {
|
|
||||||
setObjectUrl('')
|
|
||||||
setIsLoadingFile(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentFileIdRef.current !== currentFile.uuid && isPreviewable) {
|
|
||||||
getObjectUrl().catch(console.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentFileIdRef.current = currentFile.uuid
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (objectUrl) {
|
|
||||||
URL.revokeObjectURL(objectUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [currentFile, getObjectUrl, objectUrl])
|
|
||||||
|
|
||||||
const keyDownHandler: KeyboardEventHandler = useCallback(
|
|
||||||
(event) => {
|
|
||||||
if (event.key !== KeyboardKey.Left && event.key !== KeyboardKey.Right) {
|
|
||||||
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 (
|
|
||||||
<DialogOverlay
|
|
||||||
className="sn-component"
|
|
||||||
aria-label="File preview modal"
|
|
||||||
onDismiss={dismiss}
|
|
||||||
initialFocusRef={closeButtonRef}
|
|
||||||
dangerouslyBypassScrollLock
|
|
||||||
>
|
|
||||||
<DialogContent
|
|
||||||
aria-label="File preview modal"
|
|
||||||
className="flex flex-col rounded shadow-overlay"
|
|
||||||
style={{
|
|
||||||
width: '90%',
|
|
||||||
maxWidth: '90%',
|
|
||||||
minHeight: '90%',
|
|
||||||
background: 'var(--modal-background-color)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex flex-shrink-0 justify-between items-center min-h-6 px-4 py-3 border-0 border-b-1 border-solid border-main focus:shadow-none"
|
|
||||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
|
||||||
onKeyDown={keyDownHandler}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="w-6 h-6">{IconComponent}</div>
|
|
||||||
<span className="ml-3 font-medium">{currentFile.name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<button
|
|
||||||
className="flex p-1.5 mr-4 bg-transparent hover:bg-contrast border-solid border-main border-1 cursor-pointer rounded"
|
|
||||||
onClick={() => setShowFileInfoPanel((show) => !show)}
|
|
||||||
>
|
|
||||||
<Icon type="info" className="color-neutral" />
|
|
||||||
</button>
|
|
||||||
{objectUrl && (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
className="mr-4"
|
|
||||||
onClick={() => {
|
|
||||||
application.getArchiveService().downloadData(objectUrl, currentFile.name)
|
|
||||||
addToast({
|
|
||||||
type: ToastType.Success,
|
|
||||||
message: 'Successfully downloaded file',
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
ref={closeButtonRef}
|
|
||||||
onClick={dismiss}
|
|
||||||
aria-label="Close modal"
|
|
||||||
className="flex p-1 bg-transparent hover:bg-contrast border-0 cursor-pointer rounded"
|
|
||||||
>
|
|
||||||
<Icon type="close" className="color-neutral" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-grow min-h-0">
|
|
||||||
<div className="flex flex-grow items-center justify-center relative max-w-full">
|
|
||||||
{isLoadingFile ? (
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="sk-spinner w-5 h-5 spinner-info mr-3"></div>
|
|
||||||
<div className="text-base font-semibold">{fileDownloadProgress}%</div>
|
|
||||||
</div>
|
|
||||||
<span className="mt-3">Loading file...</span>
|
|
||||||
</div>
|
|
||||||
) : objectUrl ? (
|
|
||||||
<PreviewComponent file={currentFile} objectUrl={objectUrl} />
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<NoPreviewIllustration className="w-30 h-30 mb-4" />
|
|
||||||
<div className="font-bold text-base mb-2">This file can't be previewed.</div>
|
|
||||||
{isFilePreviewable ? (
|
|
||||||
<>
|
|
||||||
<div className="text-sm text-center color-passive-0 mb-4 max-w-35ch">
|
|
||||||
There was an error loading the file. Try again, or download the file and open it using another
|
|
||||||
application.
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
className="mr-3"
|
|
||||||
onClick={() => {
|
|
||||||
getObjectUrl().catch(console.error)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="normal"
|
|
||||||
onClick={() => {
|
|
||||||
application
|
|
||||||
.getViewControllerManager()
|
|
||||||
.filesController.downloadFile(currentFile)
|
|
||||||
.catch(console.error)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="text-sm text-center color-passive-0 mb-4 max-w-35ch">
|
|
||||||
To view this file, download it and open it using another application.
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => {
|
|
||||||
application
|
|
||||||
.getViewControllerManager()
|
|
||||||
.filesController.downloadFile(currentFile)
|
|
||||||
.catch(console.error)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showFileInfoPanel && <FilePreviewInfoPanel file={currentFile} />}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</DialogOverlay>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
FilePreviewModal.displayName = 'FilePreviewModal'
|
|
||||||
|
|
||||||
const FilePreviewModalWrapper: FunctionComponent<Props> = ({ application, viewControllerManager }) => {
|
|
||||||
return viewControllerManager.filePreviewModalController.isOpen ? (
|
|
||||||
<FilePreviewModal application={application} viewControllerManager={viewControllerManager} />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default observer(FilePreviewModalWrapper)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { FileItem } from '@standardnotes/snjs'
|
|
||||||
import { FunctionComponent } from 'react'
|
|
||||||
import ImagePreview from './ImagePreview'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
file: FileItem
|
|
||||||
objectUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const PreviewComponent: FunctionComponent<Props> = ({ file, objectUrl }) => {
|
|
||||||
if (file.mimeType.startsWith('image/')) {
|
|
||||||
return <ImagePreview objectUrl={objectUrl} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.mimeType.startsWith('video/')) {
|
|
||||||
return <video className="w-full h-full" src={objectUrl} controls autoPlay />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.mimeType.startsWith('audio/')) {
|
|
||||||
return <audio src={objectUrl} controls />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <object className="w-full h-full" data={objectUrl} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PreviewComponent
|
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { NoteViewController } from '@standardnotes/snjs'
|
import { FileItem, NoteViewController } from '@standardnotes/snjs'
|
||||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
||||||
import NoteView from '@/Components/NoteView/NoteView'
|
import NoteView from '@/Components/NoteView/NoteView'
|
||||||
import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles'
|
import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles'
|
||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
|
import FileView from '@/Components/FileView/FileView'
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
showMultipleSelectedNotes: boolean
|
showMultipleSelectedNotes: boolean
|
||||||
showMultipleSelectedFiles: boolean
|
showMultipleSelectedFiles: boolean
|
||||||
controllers: NoteViewController[]
|
controllers: NoteViewController[]
|
||||||
|
selectedFile: FileItem | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -25,6 +27,7 @@ class NoteGroupView extends PureComponent<Props, State> {
|
|||||||
showMultipleSelectedNotes: false,
|
showMultipleSelectedNotes: false,
|
||||||
showMultipleSelectedFiles: false,
|
showMultipleSelectedFiles: false,
|
||||||
controllers: [],
|
controllers: [],
|
||||||
|
selectedFile: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +59,14 @@ class NoteGroupView extends PureComponent<Props, State> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.autorun(() => {
|
||||||
|
if (this.viewControllerManager && this.viewControllerManager.selectionController) {
|
||||||
|
this.setState({
|
||||||
|
selectedFile: this.viewControllerManager.selectionController.selectedFiles[0],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override deinit() {
|
override deinit() {
|
||||||
@@ -66,6 +77,9 @@ class NoteGroupView extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
|
const shouldNotShowMultipleSelectedItems =
|
||||||
|
!this.state.showMultipleSelectedNotes && !this.state.showMultipleSelectedFiles
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={ElementIds.EditorColumn} className="h-full app-column app-column-third">
|
<div id={ElementIds.EditorColumn} className="h-full app-column app-column-third">
|
||||||
{this.state.showMultipleSelectedNotes && (
|
{this.state.showMultipleSelectedNotes && (
|
||||||
@@ -79,13 +93,21 @@ class NoteGroupView extends PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!this.state.showMultipleSelectedNotes && (
|
{shouldNotShowMultipleSelectedItems && this.state.controllers.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{this.state.controllers.map((controller) => {
|
{this.state.controllers.map((controller) => {
|
||||||
return <NoteView key={controller.note.uuid} application={this.application} controller={controller} />
|
return <NoteView key={controller.note.uuid} application={this.application} controller={controller} />
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shouldNotShowMultipleSelectedItems && this.state.controllers.length < 1 && this.state.selectedFile && (
|
||||||
|
<FileView
|
||||||
|
application={this.application}
|
||||||
|
viewControllerManager={this.viewControllerManager}
|
||||||
|
file={this.state.selectedFile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { KeyboardModifier, KeyboardKey } from '@/Services/IOService'
|
|||||||
import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Constants/Strings'
|
import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Constants/Strings'
|
||||||
import { confirmDialog } from '@/Services/AlertService'
|
import { confirmDialog } from '@/Services/AlertService'
|
||||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||||
import ProtectedNoteOverlay from '@/Components/ProtectedNoteOverlay/ProtectedNoteOverlay'
|
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
||||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||||
import NoteTagsContainer from '@/Components/NoteTags/NoteTagsContainer'
|
import NoteTagsContainer from '@/Components/NoteTags/NoteTagsContainer'
|
||||||
@@ -871,10 +871,11 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
<div aria-label="Note" className="section editor sn-component">
|
<div aria-label="Note" className="section editor sn-component">
|
||||||
{this.state.showProtectedWarning && (
|
{this.state.showProtectedWarning && (
|
||||||
<div className="h-full flex justify-center items-center">
|
<div className="h-full flex justify-center items-center">
|
||||||
<ProtectedNoteOverlay
|
<ProtectedItemOverlay
|
||||||
viewControllerManager={this.viewControllerManager}
|
viewControllerManager={this.viewControllerManager}
|
||||||
hasProtectionSources={this.application.hasProtectionSources()}
|
hasProtectionSources={this.application.hasProtectionSources()}
|
||||||
onViewNote={this.dismissProtectedWarning}
|
onViewItem={this.dismissProtectedWarning}
|
||||||
|
itemType={'note'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -906,7 +907,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{this.note && (
|
{this.note && (
|
||||||
<div id="editor-title-bar" className="section-title-bar w-full">
|
<div id="editor-title-bar" className="content-title-bar section-title-bar w-full">
|
||||||
<div className="flex items-center justify-between h-8">
|
<div className="flex items-center justify-between h-8">
|
||||||
<div className={(this.state.noteLocked ? 'locked' : '') + ' flex-grow'}>
|
<div className={(this.state.noteLocked ? 'locked' : '') + ' flex-grow'}>
|
||||||
<div className="title overflow-auto">
|
<div className="title overflow-auto">
|
||||||
@@ -943,8 +944,12 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
<div className="mr-3">
|
<div className="mr-3">
|
||||||
<AttachedFilesButton
|
<AttachedFilesButton
|
||||||
application={this.application}
|
application={this.application}
|
||||||
viewControllerManager={this.viewControllerManager}
|
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
|
featuresController={this.viewControllerManager.featuresController}
|
||||||
|
filePreviewModalController={this.viewControllerManager.filePreviewModalController}
|
||||||
|
filesController={this.viewControllerManager.filesController}
|
||||||
|
navigationController={this.viewControllerManager.navigationController}
|
||||||
|
notesController={this.viewControllerManager.notesController}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-3">
|
<div className="mr-3">
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
viewControllerManager: ViewControllerManager
|
viewControllerManager: ViewControllerManager
|
||||||
onViewNote: () => void
|
onViewItem: () => void
|
||||||
hasProtectionSources: boolean
|
hasProtectionSources: boolean
|
||||||
|
itemType: 'note' | 'file'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProtectedNoteOverlay = ({ viewControllerManager, onViewNote, hasProtectionSources }: Props) => {
|
const ProtectedItemOverlay = ({ viewControllerManager, onViewItem, hasProtectionSources, itemType }: Props) => {
|
||||||
const instructionText = hasProtectionSources
|
const instructionText = hasProtectionSources
|
||||||
? 'Authenticate to view this note.'
|
? `Authenticate to view this ${itemType}.`
|
||||||
: 'Add a passcode or create an account to require authentication to view this note.'
|
: `Add a passcode or create an account to require authentication to view this ${itemType}.`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center text-center max-w-md">
|
<div className="flex flex-col items-center justify-center text-center max-w-md">
|
||||||
<h1 className="text-2xl m-0 w-full">This note is protected</h1>
|
<h1 className="text-2xl m-0 w-full">This {itemType} is protected</h1>
|
||||||
<p className="text-lg mt-2 w-full">{instructionText}</p>
|
<p className="text-lg mt-2 w-full">{instructionText}</p>
|
||||||
<div className="mt-4 flex gap-3">
|
<div className="mt-4 flex gap-3">
|
||||||
{!hasProtectionSources && (
|
{!hasProtectionSources && (
|
||||||
@@ -26,12 +27,12 @@ const ProtectedNoteOverlay = ({ viewControllerManager, onViewNote, hasProtection
|
|||||||
Open account menu
|
Open account menu
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button className="sn-button small outlined normal-focus-brightness" onClick={onViewNote}>
|
<button className="sn-button small outlined normal-focus-brightness" onClick={onViewItem}>
|
||||||
{hasProtectionSources ? 'Authenticate' : 'View Note'}
|
{hasProtectionSources ? 'Authenticate' : `View ${itemType}`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProtectedNoteOverlay
|
export default ProtectedItemOverlay
|
||||||
@@ -22,3 +22,6 @@ export const TAG_FOLDERS_FEATURE_TOOLTIP = 'A Plus or Pro plan is required to en
|
|||||||
export const SMART_TAGS_FEATURE_NAME = 'Smart Tags'
|
export const SMART_TAGS_FEATURE_NAME = 'Smart Tags'
|
||||||
|
|
||||||
export const PLAIN_EDITOR_NAME = 'Plain Text'
|
export const PLAIN_EDITOR_NAME = 'Plain Text'
|
||||||
|
|
||||||
|
export const SYNC_TIMEOUT_DEBOUNCE = 350
|
||||||
|
export const SYNC_TIMEOUT_NO_DEBOUNCE = 100
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export const ElementIds = {
|
export const ElementIds = {
|
||||||
NoteTextEditor: 'note-text-editor',
|
NoteTextEditor: 'note-text-editor',
|
||||||
NoteTitleEditor: 'note-title-editor',
|
NoteTitleEditor: 'note-title-editor',
|
||||||
|
FileTitleEditor: 'file-title-editor',
|
||||||
|
FileTextPreview: 'file-text-preview',
|
||||||
EditorContent: 'editor-content',
|
EditorContent: 'editor-content',
|
||||||
EditorColumn: 'editor-column',
|
EditorColumn: 'editor-column',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class FilesController extends AbstractViewController {
|
|||||||
allFiles: FileItem[] = []
|
allFiles: FileItem[] = []
|
||||||
attachedFiles: FileItem[] = []
|
attachedFiles: FileItem[] = []
|
||||||
showFileContextMenu = false
|
showFileContextMenu = false
|
||||||
|
showProtectedOverlay = false
|
||||||
fileContextMenuLocation: FileContextMenuLocation = { x: 0, y: 0 }
|
fileContextMenuLocation: FileContextMenuLocation = { x: 0, y: 0 }
|
||||||
|
|
||||||
override deinit(): void {
|
override deinit(): void {
|
||||||
@@ -52,9 +53,12 @@ export class FilesController extends AbstractViewController {
|
|||||||
showFileContextMenu: observable,
|
showFileContextMenu: observable,
|
||||||
fileContextMenuLocation: observable,
|
fileContextMenuLocation: observable,
|
||||||
|
|
||||||
|
showProtectedOverlay: observable,
|
||||||
|
|
||||||
reloadAllFiles: action,
|
reloadAllFiles: action,
|
||||||
reloadAttachedFiles: action,
|
reloadAttachedFiles: action,
|
||||||
setShowFileContextMenu: action,
|
setShowFileContextMenu: action,
|
||||||
|
setShowProtectedOverlay: action,
|
||||||
setFileContextMenuLocation: action,
|
setFileContextMenuLocation: action,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,6 +83,10 @@ export class FilesController extends AbstractViewController {
|
|||||||
this.showFileContextMenu = enabled
|
this.showFileContextMenu = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setShowProtectedOverlay = (enabled: boolean) => {
|
||||||
|
this.showProtectedOverlay = enabled
|
||||||
|
}
|
||||||
|
|
||||||
setFileContextMenuLocation = (location: FileContextMenuLocation) => {
|
setFileContextMenuLocation = (location: FileContextMenuLocation) => {
|
||||||
this.fileContextMenuLocation = location
|
this.fileContextMenuLocation = location
|
||||||
}
|
}
|
||||||
@@ -404,9 +412,15 @@ export class FilesController extends AbstractViewController {
|
|||||||
|
|
||||||
setProtectionForFiles = async (protect: boolean, files: FileItem[]) => {
|
setProtectionForFiles = async (protect: boolean, files: FileItem[]) => {
|
||||||
if (protect) {
|
if (protect) {
|
||||||
await this.application.mutator.protectItems(files)
|
const protectedItems = await this.application.mutator.protectItems(files)
|
||||||
|
if (protectedItems) {
|
||||||
|
this.setShowProtectedOverlay(true)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.application.mutator.unprotectItems(files, ChallengeReason.UnprotectFile)
|
const unprotectedItems = await this.application.mutator.unprotectItems(files, ChallengeReason.UnprotectFile)
|
||||||
|
if (unprotectedItems) {
|
||||||
|
this.setShowProtectedOverlay(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export class NavigationController extends AbstractViewController {
|
|||||||
setContextMenuClickLocation: action,
|
setContextMenuClickLocation: action,
|
||||||
setContextMenuPosition: action,
|
setContextMenuPosition: action,
|
||||||
setContextMenuMaxHeight: action,
|
setContextMenuMaxHeight: action,
|
||||||
|
|
||||||
|
isInFilesView: computed,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
@@ -257,6 +259,10 @@ export class NavigationController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isInFilesView(): boolean {
|
||||||
|
return this.selectedUuid === SystemViewId.Files
|
||||||
|
}
|
||||||
|
|
||||||
public get allLocalRootTags(): SNTag[] {
|
public get allLocalRootTags(): SNTag[] {
|
||||||
if (this.editing_ instanceof SNTag && this.application.items.isTemplateItem(this.editing_)) {
|
if (this.editing_ instanceof SNTag && this.application.items.isTemplateItem(this.editing_)) {
|
||||||
return [this.editing_, ...this.rootTags]
|
return [this.editing_, ...this.rootTags]
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
.app-column-container {
|
.app-column-container {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: row;
|
grid-template-columns: auto auto 2fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-column-first {
|
.app-column-first {
|
||||||
width: 220px;
|
width: 220px;
|
||||||
flex-shrink: 0.2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-column-second {
|
.app-column-second {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
flex-shrink: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-column-third {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 0.3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-column {
|
.app-column {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ $heading-height: 75px;
|
|||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#editor-title-bar {
|
.content-title-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
@@ -108,19 +108,21 @@ $heading-height: 75px;
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.editable {
|
.editor-content .editable,
|
||||||
overflow-y: scroll;
|
#editor-content .editable,
|
||||||
width: 100%;
|
#file-text-preview {
|
||||||
background-color: var(--editor-pane-editor-background-color);
|
overflow-y: scroll;
|
||||||
color: var(--editor-pane-editor-foreground-color);
|
width: 100%;
|
||||||
|
background-color: var(--editor-pane-editor-background-color);
|
||||||
|
color: var(--editor-pane-editor-foreground-color);
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
font-size: var(--sn-stylekit-font-size-editor);
|
font-size: var(--sn-stylekit-font-size-editor);
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#editor-pane-component-stack {
|
#editor-pane-component-stack {
|
||||||
|
|||||||
@@ -62,6 +62,10 @@
|
|||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pb-0\.5 {
|
||||||
|
padding-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pb-1 {
|
.pb-1 {
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user