feat: preview next/prev files using arrow keys (#1004)
This commit is contained in:
@@ -60,23 +60,29 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen)
|
const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen)
|
||||||
|
|
||||||
const [attachedFilesCount, setAttachedFilesCount] = useState(
|
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles)
|
||||||
note ? application.items.getFilesForNote(note).length : 0,
|
const [allFiles, setAllFiles] = useState<SNFile[]>([])
|
||||||
)
|
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([])
|
||||||
|
const attachedFilesCount = attachedFiles.length
|
||||||
const reloadAttachedFilesCount = useCallback(() => {
|
|
||||||
setAttachedFilesCount(note ? application.items.getFilesForNote(note).length : 0)
|
|
||||||
}, [application.items, note])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unregisterFileStream = application.streamItems(ContentType.File, () => {
|
const unregisterFileStream = application.streamItems(ContentType.File, () => {
|
||||||
reloadAttachedFilesCount()
|
setAllFiles(
|
||||||
|
application.items
|
||||||
|
.getItems<SNFile>(ContentType.File)
|
||||||
|
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1)),
|
||||||
|
)
|
||||||
|
setAttachedFiles(
|
||||||
|
application.items
|
||||||
|
.getFilesForNote(note)
|
||||||
|
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unregisterFileStream()
|
unregisterFileStream()
|
||||||
}
|
}
|
||||||
}, [application, reloadAttachedFilesCount])
|
}, [application, note])
|
||||||
|
|
||||||
const toggleAttachedFilesMenu = useCallback(async () => {
|
const toggleAttachedFilesMenu = useCallback(async () => {
|
||||||
if (!appState.features.isEntitledToFiles) {
|
if (!appState.features.isEntitledToFiles) {
|
||||||
@@ -213,7 +219,10 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
await renameFile(file, action.payload.name)
|
await renameFile(file, action.payload.name)
|
||||||
break
|
break
|
||||||
case PopoverFileItemActionType.PreviewFile:
|
case PopoverFileItemActionType.PreviewFile:
|
||||||
filePreviewModal.activate(file)
|
filePreviewModal.activate(
|
||||||
|
file,
|
||||||
|
currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles,
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +237,6 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [isDraggingFiles, setIsDraggingFiles] = useState(false)
|
const [isDraggingFiles, setIsDraggingFiles] = useState(false)
|
||||||
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles)
|
|
||||||
const dragCounter = useRef(0)
|
const dragCounter = useRef(0)
|
||||||
|
|
||||||
const handleDrag = (event: DragEvent) => {
|
const handleDrag = (event: DragEvent) => {
|
||||||
@@ -373,12 +381,13 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
<AttachedFilesPopover
|
<AttachedFilesPopover
|
||||||
application={application}
|
application={application}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
note={note}
|
attachedFiles={attachedFiles}
|
||||||
handleFileAction={handleFileAction}
|
allFiles={allFiles}
|
||||||
currentTab={currentTab}
|
|
||||||
closeOnBlur={closeOnBlur}
|
closeOnBlur={closeOnBlur}
|
||||||
setCurrentTab={setCurrentTab}
|
currentTab={currentTab}
|
||||||
|
handleFileAction={handleFileAction}
|
||||||
isDraggingFiles={isDraggingFiles}
|
isDraggingFiles={isDraggingFiles}
|
||||||
|
setCurrentTab={setCurrentTab}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||||
import { WebApplication } from '@/UIModels/Application'
|
import { WebApplication } from '@/UIModels/Application'
|
||||||
import { AppState } from '@/UIModels/AppState'
|
import { AppState } from '@/UIModels/AppState'
|
||||||
import { ContentType, SNFile, SNNote } from '@standardnotes/snjs'
|
import { SNFile } from '@standardnotes/snjs'
|
||||||
import { FilesIllustration } from '@standardnotes/stylekit'
|
import { FilesIllustration } from '@standardnotes/stylekit'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent } from 'preact'
|
import { FunctionComponent } from 'preact'
|
||||||
import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks'
|
import { StateUpdater, useRef, useState } from 'preact/hooks'
|
||||||
import { Button } from '@/Components/Button/Button'
|
import { Button } from '@/Components/Button/Button'
|
||||||
import { Icon } from '@/Components/Icon'
|
import { Icon } from '@/Components/Icon'
|
||||||
import { PopoverFileItem } from './PopoverFileItem'
|
import { PopoverFileItem } from './PopoverFileItem'
|
||||||
@@ -19,11 +19,12 @@ export enum PopoverTabs {
|
|||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
appState: AppState
|
appState: AppState
|
||||||
currentTab: PopoverTabs
|
allFiles: SNFile[]
|
||||||
|
attachedFiles: SNFile[]
|
||||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||||
|
currentTab: PopoverTabs
|
||||||
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>
|
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>
|
||||||
isDraggingFiles: boolean
|
isDraggingFiles: boolean
|
||||||
note: SNNote
|
|
||||||
setCurrentTab: StateUpdater<PopoverTabs>
|
setCurrentTab: StateUpdater<PopoverTabs>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,15 +32,14 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
({
|
({
|
||||||
application,
|
application,
|
||||||
appState,
|
appState,
|
||||||
currentTab,
|
allFiles,
|
||||||
|
attachedFiles,
|
||||||
closeOnBlur,
|
closeOnBlur,
|
||||||
|
currentTab,
|
||||||
handleFileAction,
|
handleFileAction,
|
||||||
isDraggingFiles,
|
isDraggingFiles,
|
||||||
note,
|
|
||||||
setCurrentTab,
|
setCurrentTab,
|
||||||
}) => {
|
}) => {
|
||||||
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([])
|
|
||||||
const [allFiles, setAllFiles] = useState<SNFile[]>([])
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
@@ -52,26 +52,6 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
)
|
)
|
||||||
: filesList
|
: filesList
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unregisterFileStream = application.streamItems(ContentType.File, () => {
|
|
||||||
setAttachedFiles(
|
|
||||||
application.items
|
|
||||||
.getFilesForNote(note)
|
|
||||||
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1)),
|
|
||||||
)
|
|
||||||
|
|
||||||
setAllFiles(
|
|
||||||
application.items
|
|
||||||
.getItems(ContentType.File)
|
|
||||||
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1)) as SNFile[],
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unregisterFileStream()
|
|
||||||
}
|
|
||||||
}, [application, note])
|
|
||||||
|
|
||||||
const handleAttachFilesClick = async () => {
|
const handleAttachFilesClick = async () => {
|
||||||
const uploadedFiles = await appState.files.uploadNewFile()
|
const uploadedFiles = await appState.files.uploadNewFile()
|
||||||
if (!uploadedFiles) {
|
if (!uploadedFiles) {
|
||||||
|
|||||||
@@ -11,18 +11,30 @@ import { Icon } from '@/Components/Icon'
|
|||||||
import { FilePreviewInfoPanel } from './FilePreviewInfoPanel'
|
import { FilePreviewInfoPanel } from './FilePreviewInfoPanel'
|
||||||
import { isFileTypePreviewable } from './isFilePreviewable'
|
import { isFileTypePreviewable } from './isFilePreviewable'
|
||||||
import { PreviewComponent } from './PreviewComponent'
|
import { PreviewComponent } from './PreviewComponent'
|
||||||
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||||
|
import { useFilePreviewModal } from './FilePreviewModalProvider'
|
||||||
|
import { KeyboardKey } from '@/Services/IOService'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
|
files: SNFile[]
|
||||||
file: SNFile
|
file: SNFile
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilePreviewModal: FunctionComponent<Props> = ({ application, file, onDismiss }) => {
|
export const FilePreviewModal: FunctionComponent<Props> = ({
|
||||||
|
application,
|
||||||
|
files,
|
||||||
|
file,
|
||||||
|
onDismiss,
|
||||||
|
}) => {
|
||||||
|
const context = useFilePreviewModal()
|
||||||
|
|
||||||
const [objectUrl, setObjectUrl] = useState<string>()
|
const [objectUrl, setObjectUrl] = useState<string>()
|
||||||
const [isFilePreviewable, setIsFilePreviewable] = useState(false)
|
const [isFilePreviewable, setIsFilePreviewable] = useState(false)
|
||||||
const [isLoadingFile, setIsLoadingFile] = useState(true)
|
const [isLoadingFile, setIsLoadingFile] = useState(true)
|
||||||
const [showFileInfoPanel, setShowFileInfoPanel] = useState(false)
|
const [showFileInfoPanel, setShowFileInfoPanel] = useState(false)
|
||||||
|
const currentFileIdRef = useRef<string>()
|
||||||
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
const getObjectUrl = useCallback(async () => {
|
const getObjectUrl = useCallback(async () => {
|
||||||
@@ -46,6 +58,10 @@ export const FilePreviewModal: FunctionComponent<Props> = ({ application, file,
|
|||||||
}
|
}
|
||||||
}, [application.files, file])
|
}, [application.files, file])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoadingFile(true)
|
||||||
|
}, [file.uuid])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isPreviewable = isFileTypePreviewable(file.mimeType)
|
const isPreviewable = isFileTypePreviewable(file.mimeType)
|
||||||
setIsFilePreviewable(isPreviewable)
|
setIsFilePreviewable(isPreviewable)
|
||||||
@@ -54,16 +70,48 @@ export const FilePreviewModal: FunctionComponent<Props> = ({ application, file,
|
|||||||
setIsLoadingFile(false)
|
setIsLoadingFile(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!objectUrl && isPreviewable) {
|
if (currentFileIdRef.current !== file.uuid && isPreviewable) {
|
||||||
getObjectUrl().catch(console.error)
|
getObjectUrl().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentFileIdRef.current = file.uuid
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (objectUrl) {
|
if (objectUrl) {
|
||||||
URL.revokeObjectURL(objectUrl)
|
URL.revokeObjectURL(objectUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [file.mimeType, getObjectUrl, objectUrl])
|
}, [file, getObjectUrl, objectUrl])
|
||||||
|
|
||||||
|
const keyDownHandler = (event: KeyboardEvent) => {
|
||||||
|
if (event.key !== KeyboardKey.Left && event.key !== KeyboardKey.Right) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const currentFileIndex = files.findIndex((fileFromArray) => fileFromArray.uuid === file.uuid)
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case KeyboardKey.Left: {
|
||||||
|
const previousFileIndex =
|
||||||
|
currentFileIndex - 1 >= 0 ? currentFileIndex - 1 : files.length - 1
|
||||||
|
const previousFile = files[previousFileIndex]
|
||||||
|
if (previousFile) {
|
||||||
|
context.setCurrentFile(previousFile)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case KeyboardKey.Right: {
|
||||||
|
const nextFileIndex = currentFileIndex + 1 < files.length ? currentFileIndex + 1 : 0
|
||||||
|
const nextFile = files[nextFileIndex]
|
||||||
|
if (nextFile) {
|
||||||
|
context.setCurrentFile(nextFile)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogOverlay
|
<DialogOverlay
|
||||||
@@ -81,7 +129,11 @@ export const FilePreviewModal: FunctionComponent<Props> = ({ application, file,
|
|||||||
background: 'var(--sn-stylekit-background-color)',
|
background: 'var(--sn-stylekit-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">
|
<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="flex items-center">
|
||||||
<div className="w-6 h-6">
|
<div className="w-6 h-6">
|
||||||
{getFileIconComponent(
|
{getFileIconComponent(
|
||||||
@@ -121,10 +173,10 @@ export const FilePreviewModal: FunctionComponent<Props> = ({ application, file,
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow min-h-0">
|
<div className="flex flex-grow min-h-0">
|
||||||
<div className="flex flex-grow items-center justify-center relative max-w-full">
|
<div className="flex flex-grow items-center justify-center relative max-w-full">
|
||||||
{objectUrl ? (
|
{isLoadingFile ? (
|
||||||
<PreviewComponent file={file} objectUrl={objectUrl} />
|
|
||||||
) : isLoadingFile ? (
|
|
||||||
<div className="sk-spinner w-5 h-5 spinner-info"></div>
|
<div className="sk-spinner w-5 h-5 spinner-info"></div>
|
||||||
|
) : objectUrl ? (
|
||||||
|
<PreviewComponent file={file} objectUrl={objectUrl} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<NoPreviewIllustration className="w-30 h-30 mb-4" />
|
<NoPreviewIllustration className="w-30 h-30 mb-4" />
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import { createContext, FunctionComponent } from 'preact'
|
|||||||
import { useContext, useState } from 'preact/hooks'
|
import { useContext, useState } from 'preact/hooks'
|
||||||
import { FilePreviewModal } from './FilePreviewModal'
|
import { FilePreviewModal } from './FilePreviewModal'
|
||||||
|
|
||||||
|
type FilePreviewActivateFunction = (file: SNFile, files: SNFile[]) => void
|
||||||
|
|
||||||
type FilePreviewModalContextData = {
|
type FilePreviewModalContextData = {
|
||||||
activate: (file: SNFile) => void
|
activate: FilePreviewActivateFunction
|
||||||
|
setCurrentFile: (file: SNFile) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilePreviewModalContext = createContext<FilePreviewModalContextData | null>(null)
|
const FilePreviewModalContext = createContext<FilePreviewModalContextData | null>(null)
|
||||||
@@ -24,10 +27,12 @@ export const FilePreviewModalProvider: FunctionComponent<{
|
|||||||
application: WebApplication
|
application: WebApplication
|
||||||
}> = ({ application, children }) => {
|
}> = ({ application, children }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [file, setFile] = useState<SNFile>()
|
const [currentFile, setCurrentFile] = useState<SNFile>()
|
||||||
|
const [files, setFiles] = useState<SNFile[]>([])
|
||||||
|
|
||||||
const activate = (file: SNFile) => {
|
const activate: FilePreviewActivateFunction = (file, files) => {
|
||||||
setFile(file)
|
setCurrentFile(file)
|
||||||
|
setFiles(files)
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +42,15 @@ export const FilePreviewModalProvider: FunctionComponent<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isOpen && file && (
|
<FilePreviewModalContext.Provider value={{ activate, setCurrentFile }}>
|
||||||
<FilePreviewModal application={application} file={file} onDismiss={close} />
|
{isOpen && currentFile && (
|
||||||
)}
|
<FilePreviewModal
|
||||||
<FilePreviewModalContext.Provider value={{ activate }}>
|
application={application}
|
||||||
|
files={files}
|
||||||
|
file={currentFile}
|
||||||
|
onDismiss={close}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</FilePreviewModalContext.Provider>
|
</FilePreviewModalContext.Provider>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export enum KeyboardKey {
|
|||||||
Backspace = 'Backspace',
|
Backspace = 'Backspace',
|
||||||
Up = 'ArrowUp',
|
Up = 'ArrowUp',
|
||||||
Down = 'ArrowDown',
|
Down = 'ArrowDown',
|
||||||
|
Left = 'ArrowLeft',
|
||||||
|
Right = 'ArrowRight',
|
||||||
Enter = 'Enter',
|
Enter = 'Enter',
|
||||||
Escape = 'Escape',
|
Escape = 'Escape',
|
||||||
Home = 'Home',
|
Home = 'Home',
|
||||||
|
|||||||
Reference in New Issue
Block a user