Files
afilmory/apps/web/src/hooks/usePhotoViewer.ts
Innei 67a948ddeb feat: introduce @photo-gallery/data package for centralized photo management
- Added a new package `@photo-gallery/data` to manage photo data and metadata.
- Updated various components to utilize the new photoLoader from the data package.
- Adjusted dependencies in `pnpm-lock.yaml` and `package.json` files to include the new package.
- Refactored imports across the application to streamline photo data access.

Signed-off-by: Innei <tukon479@gmail.com>
2025-06-08 20:46:19 +08:00

122 lines
3.3 KiB
TypeScript

import { photoLoader } from '@photo-gallery/data'
import { atom, useAtom, useAtomValue } from 'jotai'
import { useCallback, useEffect, useMemo } from 'react'
import { useLocation, useNavigate } from 'react-router'
import { gallerySettingAtom } from '~/atoms/app'
const openAtom = atom(false)
const currentIndexAtom = atom(0)
const triggerElementAtom = atom<HTMLElement | null>(null)
const data = photoLoader.getPhotos()
export const usePhotos = () => {
const { sortOrder, selectedTags } = useAtomValue(gallerySettingAtom)
const masonryItems = useMemo(() => {
// 首先根据 tags 筛选
let filteredPhotos = data
if (selectedTags.length > 0) {
filteredPhotos = data.filter((photo) =>
selectedTags.some((tag) => photo.tags.includes(tag)),
)
}
// 然后排序
const sortedPhotos = filteredPhotos.toSorted((a, b) => {
let aDateStr = ''
let bDateStr = ''
if (a.exif && a.exif.Photo && a.exif.Photo.DateTimeOriginal) {
aDateStr = a.exif.Photo.DateTimeOriginal as unknown as string
} else {
aDateStr = a.lastModified
}
if (b.exif && b.exif.Photo && b.exif.Photo.DateTimeOriginal) {
bDateStr = b.exif.Photo.DateTimeOriginal as unknown as string
} else {
bDateStr = b.lastModified
}
return sortOrder === 'asc'
? aDateStr.localeCompare(bDateStr)
: bDateStr.localeCompare(aDateStr)
})
return sortedPhotos
}, [sortOrder, selectedTags])
return masonryItems
}
export const usePhotoViewer = () => {
const photos = usePhotos()
const [isOpen, setIsOpen] = useAtom(openAtom)
const [currentIndex, setCurrentIndex] = useAtom(currentIndexAtom)
const [triggerElement, setTriggerElement] = useAtom(triggerElementAtom)
const navigate = useNavigate()
const openViewer = useCallback(
(index: number, element?: HTMLElement) => {
setCurrentIndex(index)
setTriggerElement(element || null)
setIsOpen(true)
// 防止背景滚动
document.body.style.overflow = 'hidden'
},
[setCurrentIndex, setIsOpen, setTriggerElement],
)
const closeViewer = useCallback(() => {
setIsOpen(false)
setTriggerElement(null)
// 恢复背景滚动
document.body.style.overflow = ''
}, [setIsOpen, setTriggerElement])
const goToNext = useCallback(() => {
if (currentIndex < photos.length - 1) {
setCurrentIndex(currentIndex + 1)
}
}, [currentIndex, photos.length, setCurrentIndex])
const location = useLocation()
useEffect(() => {
if (!isOpen) {
const timer = setTimeout(() => {
navigate('/')
}, 500)
return () => clearTimeout(timer)
}
const targetPathname = `/${photos[currentIndex].id}`
if (location.pathname !== targetPathname) {
navigate(targetPathname)
}
}, [currentIndex, isOpen, location.pathname, navigate, photos])
const goToPrevious = useCallback(() => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1)
}
}, [currentIndex, setCurrentIndex])
const goToIndex = useCallback(
(index: number) => {
if (index >= 0 && index < photos.length) {
setCurrentIndex(index)
}
},
[photos.length, setCurrentIndex],
)
return {
isOpen,
currentIndex,
triggerElement,
openViewer,
closeViewer,
goToNext,
goToPrevious,
goToIndex,
}
}