feat: implement add media to album functionality with dialog and API integration
This commit is contained in:
@@ -0,0 +1,144 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogFooter,
|
||||||
|
DialogClose,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useGetMediaList } from "@/features/media/use-media";
|
||||||
|
import { useAddMediaToAlbum } from "@/features/albums/use-albums";
|
||||||
|
import { AuthenticatedImage } from "@/components/media/authenticated-image";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type AddMediaToAlbumDialogProps = {
|
||||||
|
albumId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AddMediaToAlbumDialog({ albumId }: AddMediaToAlbumDialogProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [selectedMediaIds, setSelectedMediaIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
} = useGetMediaList();
|
||||||
|
|
||||||
|
const { mutate: addMedia, isPending: isAdding } = useAddMediaToAlbum();
|
||||||
|
|
||||||
|
const toggleSelection = (mediaId: string) => {
|
||||||
|
setSelectedMediaIds((prev) =>
|
||||||
|
prev.includes(mediaId)
|
||||||
|
? prev.filter((id) => id !== mediaId)
|
||||||
|
: [...prev, mediaId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
addMedia(
|
||||||
|
{
|
||||||
|
albumId,
|
||||||
|
payload: { media_ids: selectedMediaIds },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectedMediaIds([]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setIsOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setSelectedMediaIds([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Plus size={18} className="mr-2" />
|
||||||
|
Add Photos
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-5xl h-[80vh] flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add Photos to Album</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select photos from your library to add to this album.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto pr-2">
|
||||||
|
{isLoading && <p>Loading photos...</p>}
|
||||||
|
{error && <p>Error loading photos: {error.message}</p>}
|
||||||
|
{data && (
|
||||||
|
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 gap-2">
|
||||||
|
{data.pages.map((page) =>
|
||||||
|
page.data.map((media) => {
|
||||||
|
const isSelected = selectedMediaIds.includes(media.id);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={media.id}
|
||||||
|
className={cn(
|
||||||
|
"aspect-square bg-gray-200 rounded-md overflow-hidden cursor-pointer transition-all",
|
||||||
|
isSelected
|
||||||
|
? "ring-4 ring-blue-500 ring-offset-2 opacity-100"
|
||||||
|
: "opacity-80 hover:opacity-100"
|
||||||
|
)}
|
||||||
|
onClick={() => toggleSelection(media.id)}
|
||||||
|
>
|
||||||
|
<AuthenticatedImage
|
||||||
|
src={media.thumbnail_url ?? media.file_url}
|
||||||
|
alt={media.original_filename}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasNextPage && (
|
||||||
|
<div className="flex justify-center mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
disabled={isFetchingNextPage}
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? "Loading more..." : "Load More"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="outline">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={selectedMediaIds.length === 0 || isAdding}
|
||||||
|
>
|
||||||
|
{isAdding ? "Adding..." : `Add ${selectedMediaIds.length} Photo(s)`}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,7 +28,6 @@ export function AlbumCard({ album }: AlbumCardProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="p-4 pt-0">
|
<CardFooter className="p-4 pt-0">
|
||||||
{/* TODO: Show photo count */}
|
{/* TODO: Show photo count */}
|
||||||
<p className="text-sm text-gray-600">0 items</p>
|
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { createAlbum, getAlbums } from '@/services/album-service'
|
import { addMediaToAlbum, createAlbum, getAlbum, getAlbumMedia, getAlbums, type AddMediaToAlbumPayload } from '@/services/album-service'
|
||||||
|
|
||||||
|
const ALBUMS_KEY = ["albums"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query hook to fetch a list of all albums.
|
* Query hook to fetch a list of all albums.
|
||||||
@@ -28,3 +30,48 @@ export const useCreateAlbum = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useGetAlbumMedia = (albumId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [ALBUMS_KEY, albumId, "media"],
|
||||||
|
queryFn: () => getAlbumMedia(albumId),
|
||||||
|
enabled: !!albumId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutation hook to add media to an album.
|
||||||
|
*/
|
||||||
|
export const useAddMediaToAlbum = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
albumId,
|
||||||
|
payload,
|
||||||
|
}: {
|
||||||
|
albumId: string;
|
||||||
|
payload: AddMediaToAlbumPayload;
|
||||||
|
}) => addMediaToAlbum(albumId, payload),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [ALBUMS_KEY, variables.albumId, "media"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to add media to album:", error);
|
||||||
|
// TODO: Add user-facing toast
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query hook to fetch a single album by ID.
|
||||||
|
*/
|
||||||
|
export const useGetAlbum = (albumId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [ALBUMS_KEY, albumId],
|
||||||
|
queryFn: () => getAlbum(albumId),
|
||||||
|
enabled: !!albumId,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as LoginRouteImport } from './routes/login'
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
import { Route as AboutRouteImport } from './routes/about'
|
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as PeopleIndexRouteImport } from './routes/people/index'
|
import { Route as PeopleIndexRouteImport } from './routes/people/index'
|
||||||
import { Route as MediaIndexRouteImport } from './routes/media/index'
|
import { Route as MediaIndexRouteImport } from './routes/media/index'
|
||||||
@@ -22,11 +21,6 @@ const LoginRoute = LoginRouteImport.update({
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AboutRoute = AboutRouteImport.update({
|
|
||||||
id: '/about',
|
|
||||||
path: '/about',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -55,7 +49,6 @@ const AlbumsAlbumIdRoute = AlbumsAlbumIdRouteImport.update({
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/albums/$albumId': typeof AlbumsAlbumIdRoute
|
'/albums/$albumId': typeof AlbumsAlbumIdRoute
|
||||||
'/albums': typeof AlbumsIndexRoute
|
'/albums': typeof AlbumsIndexRoute
|
||||||
@@ -64,7 +57,6 @@ export interface FileRoutesByFullPath {
|
|||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/albums/$albumId': typeof AlbumsAlbumIdRoute
|
'/albums/$albumId': typeof AlbumsAlbumIdRoute
|
||||||
'/albums': typeof AlbumsIndexRoute
|
'/albums': typeof AlbumsIndexRoute
|
||||||
@@ -74,7 +66,6 @@ export interface FileRoutesByTo {
|
|||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/albums/$albumId': typeof AlbumsAlbumIdRoute
|
'/albums/$albumId': typeof AlbumsAlbumIdRoute
|
||||||
'/albums/': typeof AlbumsIndexRoute
|
'/albums/': typeof AlbumsIndexRoute
|
||||||
@@ -85,25 +76,16 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/about'
|
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/albums/$albumId'
|
| '/albums/$albumId'
|
||||||
| '/albums'
|
| '/albums'
|
||||||
| '/media'
|
| '/media'
|
||||||
| '/people'
|
| '/people'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to: '/' | '/login' | '/albums/$albumId' | '/albums' | '/media' | '/people'
|
||||||
| '/'
|
|
||||||
| '/about'
|
|
||||||
| '/login'
|
|
||||||
| '/albums/$albumId'
|
|
||||||
| '/albums'
|
|
||||||
| '/media'
|
|
||||||
| '/people'
|
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/about'
|
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/albums/$albumId'
|
| '/albums/$albumId'
|
||||||
| '/albums/'
|
| '/albums/'
|
||||||
@@ -113,7 +95,6 @@ export interface FileRouteTypes {
|
|||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
AboutRoute: typeof AboutRoute
|
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
AlbumsAlbumIdRoute: typeof AlbumsAlbumIdRoute
|
AlbumsAlbumIdRoute: typeof AlbumsAlbumIdRoute
|
||||||
AlbumsIndexRoute: typeof AlbumsIndexRoute
|
AlbumsIndexRoute: typeof AlbumsIndexRoute
|
||||||
@@ -130,13 +111,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof LoginRouteImport
|
preLoaderRoute: typeof LoginRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/about': {
|
|
||||||
id: '/about'
|
|
||||||
path: '/about'
|
|
||||||
fullPath: '/about'
|
|
||||||
preLoaderRoute: typeof AboutRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -177,7 +151,6 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
AboutRoute: AboutRoute,
|
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
AlbumsAlbumIdRoute: AlbumsAlbumIdRoute,
|
AlbumsAlbumIdRoute: AlbumsAlbumIdRoute,
|
||||||
AlbumsIndexRoute: AlbumsIndexRoute,
|
AlbumsIndexRoute: AlbumsIndexRoute,
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/about")({
|
|
||||||
component: AboutComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function AboutComponent() {
|
|
||||||
return (
|
|
||||||
<div className="p-2">
|
|
||||||
<h3>About</h3>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
|
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 type { Media } from "@/domain/types";
|
||||||
|
import { useGetAlbum, useGetAlbumMedia } from "@/features/albums/use-albums";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/albums/$albumId")({
|
export const Route = createFileRoute("/albums/$albumId")({
|
||||||
component: AlbumDetailPage,
|
component: AlbumDetailPage,
|
||||||
@@ -6,14 +12,51 @@ export const Route = createFileRoute("/albums/$albumId")({
|
|||||||
|
|
||||||
function AlbumDetailPage() {
|
function AlbumDetailPage() {
|
||||||
const { albumId } = Route.useParams();
|
const { albumId } = Route.useParams();
|
||||||
|
const { data: album, isLoading: isLoadingAlbum } = useGetAlbum(albumId);
|
||||||
|
const { data: media, isLoading: isLoadingMedia } = useGetAlbumMedia(albumId);
|
||||||
|
const [selectedMedia, setSelectedMedia] = useState<Media | null>(null);
|
||||||
|
|
||||||
|
const isLoading = isLoadingAlbum || isLoadingMedia;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-6">
|
||||||
<h1 className="text-3xl font-bold">Album: {albumId}</h1>
|
<div className="flex items-center justify-between">
|
||||||
<p className="mt-4">
|
<h1 className="text-3xl font-bold truncate">
|
||||||
This page will show the details and photos for a single album.
|
{album?.name ?? "Loading album..."}
|
||||||
</p>
|
</h1>
|
||||||
{/* TODO: Fetch album details and display media grid */}
|
<AddMediaToAlbumDialog albumId={albumId} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && <p>Loading photos...</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) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
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>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{media && media.length === 0 && <p>This album is empty.</p>}
|
||||||
|
|
||||||
|
<MediaViewer
|
||||||
|
media={selectedMedia}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setSelectedMedia(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Album } from "@/domain/types"
|
import type { Album, Media } from "@/domain/types"
|
||||||
import apiClient from "@/services/api-client"
|
import apiClient from "@/services/api-client"
|
||||||
|
|
||||||
export type CreateAlbumPayload = {
|
export type CreateAlbumPayload = {
|
||||||
@@ -24,3 +24,46 @@ export const createAlbum = async (
|
|||||||
const { data } = await apiClient.post('/albums', payload)
|
const { data } = await apiClient.post('/albums', payload)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all media for a specific album.
|
||||||
|
*/
|
||||||
|
export const getAlbumMedia = async (albumId: string): Promise<Media[]> => {
|
||||||
|
const { data } = await apiClient.get(`/albums/${albumId}/media`);
|
||||||
|
|
||||||
|
|
||||||
|
const prefix = import.meta.env.VITE_PREFIX_PATH || apiClient.defaults.baseURL;
|
||||||
|
|
||||||
|
|
||||||
|
const processedMedia = data.map((media: Media) => ({
|
||||||
|
...media,
|
||||||
|
file_url: `${prefix}${media.file_url}`,
|
||||||
|
thumbnail_url: media.thumbnail_url
|
||||||
|
? `${prefix}${media.thumbnail_url}`
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return processedMedia;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddMediaToAlbumPayload = {
|
||||||
|
media_ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a list of media IDs to a specific album.
|
||||||
|
*/
|
||||||
|
export const addMediaToAlbum = async (
|
||||||
|
albumId: string,
|
||||||
|
payload: AddMediaToAlbumPayload,
|
||||||
|
): Promise<void> => {
|
||||||
|
await apiClient.post(`/albums/${albumId}/media`, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a single album by its ID.
|
||||||
|
*/
|
||||||
|
export const getAlbum = async (albumId: string): Promise<Album> => {
|
||||||
|
const { data } = await apiClient.get(`/albums/${albumId}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -13,8 +13,8 @@ use crate::{
|
|||||||
error::ApiError,
|
error::ApiError,
|
||||||
middleware::auth::UserId,
|
middleware::auth::UserId,
|
||||||
schema::{
|
schema::{
|
||||||
AddMediaToAlbumRequest, AlbumResponse, CreateAlbumRequest, SetThumbnailRequest,
|
AddMediaToAlbumRequest, AlbumResponse, CreateAlbumRequest, MediaResponse,
|
||||||
ShareAlbumRequest, UpdateAlbumRequest,
|
SetThumbnailRequest, ShareAlbumRequest, UpdateAlbumRequest,
|
||||||
},
|
},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
@@ -133,6 +133,21 @@ async fn set_album_thumbnail(
|
|||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_media_for_album(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(album_id): Path<Uuid>,
|
||||||
|
) -> Result<Json<Vec<MediaResponse>>, ApiError> {
|
||||||
|
let media_list = state
|
||||||
|
.album_service
|
||||||
|
.get_album_media(album_id, user_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response = media_list.into_iter().map(MediaResponse::from).collect();
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn album_routes() -> Router<AppState> {
|
pub fn album_routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", post(create_album).get(list_user_albums))
|
.route("/", post(create_album).get(list_user_albums))
|
||||||
@@ -143,6 +158,9 @@ pub fn album_routes() -> Router<AppState> {
|
|||||||
.delete(delete_album),
|
.delete(delete_album),
|
||||||
)
|
)
|
||||||
.route("/{id}/thumbnail", put(set_album_thumbnail))
|
.route("/{id}/thumbnail", put(set_album_thumbnail))
|
||||||
.route("/{id}/media", post(add_media_to_album))
|
.route(
|
||||||
|
"/{id}/media",
|
||||||
|
post(add_media_to_album).get(get_media_for_album),
|
||||||
|
)
|
||||||
.route("/{id}/share", post(share_album))
|
.route("/{id}/share", post(share_album))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use chrono::Utc;
|
|||||||
use libertas_core::{
|
use libertas_core::{
|
||||||
authz::{self, Permission},
|
authz::{self, Permission},
|
||||||
error::{CoreError, CoreResult},
|
error::{CoreError, CoreResult},
|
||||||
models::{Album, PublicAlbumBundle},
|
models::{Album, Media, PublicAlbumBundle},
|
||||||
repositories::{AlbumRepository, AlbumShareRepository},
|
repositories::{AlbumRepository, AlbumShareRepository},
|
||||||
schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData, UpdateAlbumData},
|
schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData, UpdateAlbumData},
|
||||||
services::{AlbumService, AuthorizationService},
|
services::{AlbumService, AuthorizationService},
|
||||||
@@ -188,4 +188,14 @@ impl AlbumService for AlbumServiceImpl {
|
|||||||
.set_thumbnail_media_id(album_id, media_id)
|
.set_thumbnail_media_id(album_id, media_id)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_album_media(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<Vec<Media>> {
|
||||||
|
self.auth_service
|
||||||
|
.check_permission(Some(user_id), Permission::ViewAlbum(album_id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let media = self.album_repo.list_media_by_album_id(album_id).await?;
|
||||||
|
|
||||||
|
Ok(media)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ pub trait AlbumService: Send + Sync {
|
|||||||
media_id: Uuid,
|
media_id: Uuid,
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> CoreResult<()>;
|
) -> CoreResult<()>;
|
||||||
|
async fn get_album_media(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<Vec<Media>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
Reference in New Issue
Block a user