diff --git a/libertas-frontend/src/components/albums/add-media-to-album-dialog.tsx b/libertas-frontend/src/components/albums/add-media-to-album-dialog.tsx
index e2cf664..f4ac399 100644
--- a/libertas-frontend/src/components/albums/add-media-to-album-dialog.tsx
+++ b/libertas-frontend/src/components/albums/add-media-to-album-dialog.tsx
@@ -31,7 +31,7 @@ export function AddMediaToAlbumDialog({ albumId }: AddMediaToAlbumDialogProps) {
fetchNextPage,
hasNextPage,
isFetchingNextPage,
- } = useGetMediaList();
+ } = useGetMediaList(1, 20);
const { mutate: addMedia, isPending: isAdding } = useAddMediaToAlbum(albumId);
diff --git a/libertas-frontend/src/components/media/face-overlay.tsx b/libertas-frontend/src/components/media/face-overlay.tsx
new file mode 100644
index 0000000..65e7c11
--- /dev/null
+++ b/libertas-frontend/src/components/media/face-overlay.tsx
@@ -0,0 +1,56 @@
+import type { FaceRegion } from "@/domain/types";
+import { cn } from "@/lib/utils";
+
+type FaceOverlayProps = {
+ faces: FaceRegion[];
+ hoveredFaceId: string | null;
+ imageWidth: number;
+ imageHeight: number;
+ className?: string;
+ onFaceClick?: (face: FaceRegion) => void;
+};
+
+export function FaceOverlay({
+ faces,
+ hoveredFaceId,
+ imageWidth,
+ imageHeight,
+ className,
+ onFaceClick,
+}: FaceOverlayProps) {
+ if (!imageWidth || !imageHeight) return null;
+
+ return (
+
+ {faces.map((face) => {
+ const isHovered = face.id === hoveredFaceId;
+
+ // Calculate percentages
+ const left = (face.x_min / imageWidth) * 100;
+ const top = (face.y_min / imageHeight) * 100;
+ const width = ((face.x_max - face.x_min) / imageWidth) * 100;
+ const height = ((face.y_max - face.y_min) / imageHeight) * 100;
+
+ return (
+
onFaceClick?.(face)}
+ title={face.person_id ? "Assigned Person" : "Unknown Person"}
+ />
+ );
+ })}
+
+ );
+}
diff --git a/libertas-frontend/src/components/media/media-details-sidebar.tsx b/libertas-frontend/src/components/media/media-details-sidebar.tsx
index 946ff51..10e18be 100644
--- a/libertas-frontend/src/components/media/media-details-sidebar.tsx
+++ b/libertas-frontend/src/components/media/media-details-sidebar.tsx
@@ -1,6 +1,5 @@
-import type { Media, MediaMetadata } from "@/domain/types";
+import type { FaceRegion, Media, MediaMetadata } from "@/domain/types";
import { useGetMediaDetails } from "@/features/media/use-media";
-import { useListMediaFaces } from "@/features/faces/use-faces";
import { useListMediaTags } from "@/features/tags/use-tags";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
@@ -17,6 +16,10 @@ import { Separator } from "../ui/separator";
type MediaDetailsSidebarProps = {
media: Media;
+ faces: FaceRegion[] | undefined;
+ isLoadingFaces: boolean;
+ onHoverFace: (faceId: string | null) => void;
+ onFaceClick: (face: FaceRegion) => void;
};
function findMeta(
@@ -28,14 +31,17 @@ function findMeta(
const manualTags = new Set(["DateTimeOriginal", "Make", "Model"]);
-export function MediaDetailsSidebar({ media }: MediaDetailsSidebarProps) {
+export function MediaDetailsSidebar({
+ media,
+ faces,
+ isLoadingFaces,
+ onHoverFace,
+ onFaceClick
+}: MediaDetailsSidebarProps) {
const { data: details, isLoading: isLoadingDetails } = useGetMediaDetails(
media.id
);
const { data: tags, isLoading: isLoadingTags } = useListMediaTags(media.id);
- const { data: faces, isLoading: isLoadingFaces } = useListMediaFaces(
- media.id
- );
const displayDate = media.date_taken
? format(parseISO(media.date_taken), "MMMM d, yyyy 'at' h:mm a")
@@ -53,8 +59,6 @@ export function MediaDetailsSidebar({ media }: MediaDetailsSidebarProps) {
)
.sort((a, b) => a.tag_name.localeCompare(b.tag_name));
- console.log("Other Metadata:", details);
-
return (
@@ -70,7 +74,7 @@ export function MediaDetailsSidebar({ media }: MediaDetailsSidebarProps) {
defaultValue={["details", "tags", "people"]}
className="w-full"
>
- {/* --- People Section (Unchanged) --- */}
+ {/* --- People Section --- */}
People
@@ -78,7 +82,13 @@ export function MediaDetailsSidebar({ media }: MediaDetailsSidebarProps) {
{faces && faces.length > 0 && (
{faces.map((face) => (
-
+
onHoverFace(face.id)}
+ onMouseLeave={() => onHoverFace(null)}
+ onClick={() => onFaceClick(face)}
+ />
))}
)}
diff --git a/libertas-frontend/src/components/media/media-viewer.tsx b/libertas-frontend/src/components/media/media-viewer.tsx
index a02fcc5..a989d4c 100644
--- a/libertas-frontend/src/components/media/media-viewer.tsx
+++ b/libertas-frontend/src/components/media/media-viewer.tsx
@@ -1,5 +1,5 @@
import { Dialog, DialogContent } from "@/components/ui/dialog";
-import { type Media } from "@/domain/types";
+import { type Media, type FaceRegion } from "@/domain/types";
import { AuthenticatedImage } from "./authenticated-image";
import { Skeleton } from "../ui/skeleton";
import {
@@ -8,6 +8,10 @@ import {
ResizableHandle,
} from "@/components/ui/resizable";
import { MediaDetailsSidebar } from "./media-details-sidebar";
+import { useListMediaFaces } from "@/features/faces/use-faces";
+import { useState, useRef } from "react";
+import { FaceOverlay } from "./face-overlay";
+import { PersonAssignmentDialog } from "@/components/people/person-assignment-dialog";
type MediaViewerProps = {
media: Media | null;
@@ -16,21 +20,51 @@ type MediaViewerProps = {
export function MediaViewer({ media, onOpenChange }: MediaViewerProps) {
const isOpen = media !== null;
+ const { data: faces, isLoading: isLoadingFaces } = useListMediaFaces(media?.id ?? "");
+ const [hoveredFaceId, setHoveredFaceId] = useState(null);
+ const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null);
+ const imageRef = useRef(null);
+
+ // Assignment dialog state
+ const [assignmentDialogOpen, setAssignmentDialogOpen] = useState(false);
+ const [selectedFace, setSelectedFace] = useState(null);
+
+ const handleImageLoad = (e: React.SyntheticEvent) => {
+ const img = e.currentTarget;
+ setImageDimensions({ width: img.naturalWidth, height: img.naturalHeight });
+ };
+
+ const handleFaceClick = (face: FaceRegion) => {
+ setSelectedFace(face);
+ setAssignmentDialogOpen(true);
+ };
return (