feat: preview next/prev files using arrow keys (#1004)

This commit is contained in:
Aman Harwara
2022-04-27 22:53:42 +05:30
committed by GitHub
parent e7fb9b67f8
commit 96be0d578d
5 changed files with 111 additions and 58 deletions

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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" />

View File

@@ -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>
</> </>

View File

@@ -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',