diff --git a/src/components/albums/info/AlbumImages.tsx b/src/components/albums/info/AlbumImages.tsx index f7b1b3e..9240e0d 100644 --- a/src/components/albums/info/AlbumImages.tsx +++ b/src/components/albums/info/AlbumImages.tsx @@ -1,110 +1,84 @@ import "yet-another-react-lightbox/styles.css"; -import { listAlbumAssets } from '@/handlers/api/album.handler'; -import { useConfig } from '@/contexts/ConfigContext'; -import { IAlbum } from '@/types/album' -import { IAsset } from '@/types/asset' -import React, { useEffect, useMemo, useRef, useState } from 'react' -import { ExternalLink, Hourglass } from 'lucide-react'; -import { useRouter } from 'next/router'; -import Lightbox from 'yet-another-react-lightbox'; -import Captions from 'yet-another-react-lightbox/plugins/captions'; -import LazyImage from '@/components/ui/lazy-image'; +import { listAlbumAssets } from "@/handlers/api/album.handler"; +import { useConfig } from "@/contexts/ConfigContext"; +import { IAlbum } from "@/types/album"; +import { IAsset } from "@/types/asset"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { Hourglass } from "lucide-react"; +import { useRouter } from "next/router"; import { Button } from "@/components/ui/button"; -import Link from "next/link"; +import AssetGrid from "@/components/shared/AssetGrid"; +import PhotoSelectionContext, { + IPhotoSelectionContext, +} from "@/contexts/PhotoSelectionContext"; interface AlbumImagesProps { - album: IAlbum - + album: IAlbum; } interface IAssetFilter { - faceId?: string - page: number + faceId?: string; + page: number; } export default function AlbumImages({ album }: AlbumImagesProps) { const { exImmichUrl } = useConfig(); const router = useRouter(); const { faceId } = router.query as { faceId: string }; - const [assets, setAssets] = useState([]) + const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [filters, setFilters] = useState({ faceId, - page: 1 - }) - const [hasMore, setHasMore] = useState(true) + page: 1, + }); + const [hasMore, setHasMore] = useState(true); - const [index, setIndex] = useState(-1); + // Initialize context state + const [contextState, setContextState] = useState({ + selectedIds: [], + assets: [], + config: {}, + updateContext: (newConfig: Partial) => { + setContextState((prevState) => ({ + ...prevState, + ...newConfig, + // No deep merge needed for config here as it's simple + config: newConfig.config + ? { ...prevState.config, ...newConfig.config } + : prevState.config, + })); + }, + }); const fetchAssets = async () => { setLoading(true); return listAlbumAssets(album.id, filters) - .then((assets) => { + .then((newAssets) => { if (filters.page === 1) { - setAssets(assets) + setAssets(newAssets); } else { - setAssets([...assets, ...assets]) + setAssets((prevAssets) => [...prevAssets, ...newAssets]); } - setHasMore(assets.length >= 100) + setHasMore(newAssets.length === 100); }) .catch(setErrorMessage) .finally(() => setLoading(false)); }; - const images = useMemo(() => { - return assets.map((p) => ({ - ...p, - src: p.url as string, - original: p.previewUrl as string, - width: p.exifImageWidth as number, - height: p.exifImageHeight as number, - // isSelected: selectedIds.includes(p.id), - tags: [ - { - title: "Immich Link", - value: ( - - Open in Immich - - ), - }, - ], - })); - }, [assets]); - - const slides = useMemo( - () => - images.map(({ original, width, height }) => ({ - src: original, - width, - height, - })), - [images] - ); - - const handleClick = (idx: number) => setIndex(idx); - - // const handleSelect = (_idx: number, asset: IAsset) => { - // const isPresent = selectedIds.includes(asset.id); - // if (isPresent) { - // updateContext({ - // selectedIds: selectedIds.filter((id) => id !== asset.id), - // }); - // } else { - // updateContext({ selectedIds: [...selectedIds, asset.id] }); - // } - // }; - useEffect(() => { - fetchAssets(); + if (album.id) { + fetchAssets(); + } }, [album.id, filters]); useEffect(() => { setFilters({ faceId, - page: 1 - }) - }, [faceId]) + page: 1, + }); + setAssets([]); + setHasMore(true); + }, [faceId]); if (loading && filters.page === 1) return ( @@ -114,53 +88,36 @@ export default function AlbumImages({ album }: AlbumImagesProps) { ); - return ( -
- = 0} - index={index} - close={() => setIndex(-1)} - /> -
- {images.map((image) => ( -
- handleClick(images.indexOf(image))} - /> - - - Open In Immich - -
- ))} + if (errorMessage) { + return ( +
+ Error loading images: {errorMessage}
- {hasMore && } -
+ ); + } + + return ( + +
+ + {hasMore && ( + + )} +
+
); } diff --git a/src/pages/albums/[albumId].tsx b/src/pages/albums/[albumId].tsx index 3b40c0d..13574e8 100644 --- a/src/pages/albums/[albumId].tsx +++ b/src/pages/albums/[albumId].tsx @@ -66,7 +66,6 @@ export default function AlbumListPage() { } const renderContent = () => { - if (loading) { return } diff --git a/src/pages/api/albums/[id]/assets.ts b/src/pages/api/albums/[id]/assets.ts index 8ed26f8..4d5f79f 100644 --- a/src/pages/api/albums/[id]/assets.ts +++ b/src/pages/api/albums/[id]/assets.ts @@ -7,6 +7,8 @@ import { and, eq, isNotNull } from "drizzle-orm"; import { assets } from "@/schema/assets.schema"; import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema"; import { assetFaces, exif, person } from "@/schema"; +import { isFlipped } from "@/helpers/asset.helper"; +import { ASSET_VIDEO_PATH } from "@/config/routes"; export default async function handler( req: NextApiRequest, @@ -41,6 +43,7 @@ export default async function handler( exifImageHeight: exif.exifImageHeight, ownerId: assets.ownerId, dateTimeOriginal: exif.dateTimeOriginal, + orientation: exif.orientation, }) .from(albumsAssetsAssets) .leftJoin(assets, eq(albumsAssetsAssets.assetsId, assets.id)) @@ -56,5 +59,18 @@ export default async function handler( .limit(100) .offset(100 * (page - 1)); - res.status(200).json(dbAssets); + const cleanedAssets = dbAssets.map((asset) => { + return { + ...asset, + exifImageHeight: isFlipped(asset?.orientation) + ? asset?.exifImageWidth + : asset?.exifImageHeight, + exifImageWidth: isFlipped(asset?.orientation) + ? asset?.exifImageHeight + : asset?.exifImageWidth, + orientation: asset?.orientation, + downloadUrl: asset?.id ? ASSET_VIDEO_PATH(asset.id) : null, + }; + }); + res.status(200).json(cleanedAssets); } \ No newline at end of file diff --git a/src/pages/api/find/search.ts b/src/pages/api/find/search.ts index 534e931..2bf858a 100644 --- a/src/pages/api/find/search.ts +++ b/src/pages/api/find/search.ts @@ -1,70 +1,82 @@ -import { db } from '@/config/db'; -import { ENV } from '@/config/environment'; -import { ASSET_VIDEO_PATH } from '@/config/routes'; -import { getCurrentUser } from '@/handlers/serverUtils/user.utils'; -import { cleanUpAsset, isFlipped } from '@/helpers/asset.helper'; -import { parseFindQuery } from '@/helpers/gemini.helper'; -import { person } from '@/schema'; -import { Person } from '@/schema/person.schema'; -import { inArray } from 'drizzle-orm'; -import { NextApiRequest, NextApiResponse } from 'next'; +import { db } from "@/config/db"; +import { ENV } from "@/config/environment"; +import { ASSET_VIDEO_PATH } from "@/config/routes"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { cleanUpAsset, isFlipped } from "@/helpers/asset.helper"; +import { parseFindQuery } from "@/helpers/gemini.helper"; +import { person } from "@/schema"; +import { Person } from "@/schema/person.schema"; +import { inArray } from "drizzle-orm"; +import { NextApiRequest, NextApiResponse } from "next"; -export default async function search(req: NextApiRequest, res: NextApiResponse) { +export default async function search( + req: NextApiRequest, + res: NextApiResponse +) { try { const { query } = req.body; - const currentUser = await getCurrentUser(req); + const currentUser = await getCurrentUser(req); const parsedQuery = await parseFindQuery(query as string); const { personIds } = parsedQuery; + + let dbPeople: Person[] = []; + if (personIds) { + dbPeople = await db + .select() + .from(person) + .where(inArray(person.id, personIds)); + } + const url = ENV.IMMICH_URL + "/api/search/smart"; - let dbPeople: Person[] = []; - if (personIds) { - dbPeople = await db.select().from(person).where( - inArray(person.id, personIds) - ); - } - - return fetch(url, { - method: 'POST', - body: JSON.stringify({...parsedQuery, withExif: true }), - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${currentUser?.accessToken}` - } - }).then(response => { - if (response.status !== 200) { - return res.json({ - assets: [], - filters: parsedQuery, - error: "Failed to fetch assets", - errorData: response.json() - }); - } - return response.json(); - }) - .then(data => { - const items = data.assets.items.map((item: any) => { - return { - ...item, - exifImageHeight: isFlipped(item?.exifInfo?.orientation) ? item?.exifInfo?.exifImageWidth : item?.exifInfo?.exifImageHeight, - exifImageWidth: isFlipped(item?.exifInfo?.orientation) ? item?.exifInfo?.exifImageHeight : item?.exifInfo?.exifImageWidth, - orientation: item?.exifInfo?.orientation, - downloadUrl: ASSET_VIDEO_PATH(item.id) - } - }).map(cleanUpAsset) - res.status(200).json({ - assets: items, - filters: { - ...parsedQuery, - personIds: dbPeople.map((person) => person.name) + return fetch(url, { + method: "POST", + body: JSON.stringify({ ...parsedQuery, withExif: true }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${currentUser?.accessToken}`, }, - }); - }).catch(err => { - res.status(500).json({ message: err.message }); - }); + }) + .then((response) => { + if (response.status !== 200) { + return res.json({ + assets: [], + filters: parsedQuery, + error: "Failed to fetch assets", + errorData: response.json(), + }); + } + return response.json(); + }) + .then((data) => { + const items = data.assets.items + .map((item: any) => { + return { + ...item, + exifImageHeight: isFlipped(item?.exifInfo?.orientation) + ? item?.exifInfo?.exifImageWidth + : item?.exifInfo?.exifImageHeight, + exifImageWidth: isFlipped(item?.exifInfo?.orientation) + ? item?.exifInfo?.exifImageHeight + : item?.exifInfo?.exifImageWidth, + orientation: item?.exifInfo?.orientation, + downloadUrl: ASSET_VIDEO_PATH(item.id), + }; + }) + .map(cleanUpAsset); + res.status(200).json({ + assets: items, + filters: { + ...parsedQuery, + personIds: dbPeople.map((person) => person.name), + }, + }); + }) + .catch((err) => { + res.status(500).json({ message: err.message }); + }); } catch (err: any) { console.error(err); res.status(500).json({ message: err.message || "Failed to parse query" }); } - -} \ No newline at end of file +} diff --git a/src/pages/find/index.tsx b/src/pages/find/index.tsx index 2ba8c8a..07fe30b 100644 --- a/src/pages/find/index.tsx +++ b/src/pages/find/index.tsx @@ -72,17 +72,7 @@ export default function FindPage() { const updateContext = contextState.updateContext; - // Derive slides from contextState.assets - const slides = useMemo( - () => - contextState.assets.map((asset) => ({ - src: ASSET_PREVIEW_PATH(asset.id), - width: 1000, - height: 1000, - })), - [contextState.assets] - ); - + const appliedFilters: { label: string; value: string;