- Added hooks for listing, creating, updating, deleting, sharing, and merging people. - Introduced a new route for person details and media. - Implemented clustering faces functionality. - Created services for person-related API interactions. feat: Introduce tag management functionality - Added hooks for listing, adding, and removing tags from media. - Created services for tag-related API interactions. feat: Enhance user authentication handling - Added a hook to fetch current user details. - Updated auth storage to manage user state more effectively. feat: Update album management features - Enhanced album service to return created album details. - Updated API handlers to return album responses upon creation. - Modified album repository to return created album. feat: Implement media management improvements - Added media details fetching and processing of media URLs. - Enhanced media upload functionality to return processed media. feat: Introduce face management features - Added services for listing faces for media and assigning faces to persons. fix: Update API client to clear authentication state on 401 errors.
110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import { AddMediaToAlbumDialog } from "@/components/albums/add-media-to-album-dialog";
|
|
import { AuthenticatedImage } from "@/components/media/authenticated-image";
|
|
import { MediaViewer } from "@/components/media/media-viewer";
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuTrigger,
|
|
} from "@/components/ui/context-menu";
|
|
import type { Media } from "@/domain/types";
|
|
import {
|
|
useGetAlbum,
|
|
useGetAlbumMedia,
|
|
useRemoveMediaFromAlbum,
|
|
} from "@/features/albums/use-albums";
|
|
import { createFileRoute } from "@tanstack/react-router";
|
|
import { Eye, Trash2 } from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
export const Route = createFileRoute("/albums/$albumId")({
|
|
component: AlbumDetailPage,
|
|
});
|
|
|
|
function AlbumDetailPage() {
|
|
const { albumId } = Route.useParams();
|
|
const {
|
|
data: album,
|
|
isLoading: isLoadingAlbum,
|
|
error: albumError,
|
|
} = useGetAlbum(albumId);
|
|
const {
|
|
data: media,
|
|
isLoading: isLoadingMedia,
|
|
error: mediaError,
|
|
} = useGetAlbumMedia(albumId);
|
|
const [selectedMedia, setSelectedMedia] = useState<Media | null>(null);
|
|
|
|
const { mutate: removeMedia, isPending: isRemoving } =
|
|
useRemoveMediaFromAlbum(albumId);
|
|
|
|
const isLoading = isLoadingAlbum || isLoadingMedia;
|
|
const error = albumError || mediaError;
|
|
|
|
const handleRemoveMedia = (mediaId: string) => {
|
|
removeMedia({
|
|
media_ids: [mediaId],
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-3xl font-bold truncate">
|
|
{album?.name ?? "Loading album..."}
|
|
</h1>
|
|
<AddMediaToAlbumDialog albumId={albumId} />
|
|
</div>
|
|
|
|
{isLoading && <p>Loading photos...</p>}
|
|
{error && <p>Error loading photos: {error.message}</p>}
|
|
|
|
{media && media.length > 0 && (
|
|
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
|
|
{media.map((m) => (
|
|
<ContextMenu key={m.id}>
|
|
<ContextMenuTrigger>
|
|
<div
|
|
className="aspect-square bg-gray-200 rounded-md overflow-hidden cursor-pointer hover:opacity-80 transition-opacity"
|
|
onClick={() => setSelectedMedia(m)}
|
|
>
|
|
<AuthenticatedImage
|
|
src={m.thumbnail_url ?? m.file_url}
|
|
alt={m.original_filename}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuContent>
|
|
<ContextMenuItem onSelect={() => setSelectedMedia(m)}>
|
|
<Eye className="mr-2 h-4 w-4" />
|
|
View
|
|
</ContextMenuItem>
|
|
<ContextMenuItem
|
|
className="text-destructive focus:text-destructive"
|
|
onSelect={() => handleRemoveMedia(m.id)}
|
|
disabled={isRemoving}
|
|
>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Remove from Album
|
|
</ContextMenuItem>
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{media && media.length === 0 && <p>This album is empty.</p>}
|
|
|
|
<MediaViewer
|
|
media={selectedMedia}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setSelectedMedia(null);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|