Add comprehensive photo data syncing

This commit is contained in:
Sam Becker 2024-05-29 23:30:26 -05:00
parent ce2a5a213f
commit 3021018dc0
8 changed files with 90 additions and 47 deletions

View File

@ -14,7 +14,7 @@ import EditButton from './EditButton';
import DeleteButton from './DeleteButton';
import {
deletePhotoFormAction,
syncPhotoExifDataAction,
syncPhotoAction,
} from '@/photo/actions';
import { useAppState } from '@/state/AppState';
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
@ -24,10 +24,12 @@ export default function AdminPhotosTable({
photos,
onLastPhotoVisible,
revalidatePhoto,
hasAiTextGeneration,
}: {
photos: Photo[],
onLastPhotoVisible?: () => void
revalidatePhoto?: RevalidatePhoto
hasAiTextGeneration?: boolean
}) {
const { invalidateSwr } = useAppState();
@ -82,11 +84,11 @@ export default function AdminPhotosTable({
)}>
<EditButton path={pathForAdminPhotoEdit(photo)} />
<PhotoSyncButton
action={syncPhotoExifDataAction}
action={syncPhotoAction}
photoTitle={titleForPhoto(photo)}
formData={{ photoId: photo.id }}
onFormSubmit={invalidateSwr}
includeLabel={false}
hasAiTextGeneration={hasAiTextGeneration}
shouldConfirm
shouldToast
/>

View File

@ -7,9 +7,11 @@ import AdminPhotosTable from './AdminPhotosTable';
export default function AdminPhotosTableInfinite({
initialOffset,
itemsPerPage,
hasAiTextGeneration,
}: {
initialOffset: number
itemsPerPage: number
hasAiTextGeneration?: boolean
}) {
return (
<InfinitePhotoScroll
@ -24,6 +26,7 @@ export default function AdminPhotosTableInfinite({
photos={photos}
onLastPhotoVisible={onLastPhotoVisible}
revalidatePhoto={revalidatePhoto}
hasAiTextGeneration={hasAiTextGeneration}
/>}
</InfinitePhotoScroll>
);

View File

@ -1,7 +1,7 @@
'use client';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { syncCacheAction } from '@/photo/actions';
import { clearCacheAction } from '@/photo/actions';
import { useAppState } from '@/state/AppState';
import { BiTrash } from 'react-icons/bi';
@ -9,7 +9,7 @@ export default function ClearCacheButton() {
const { invalidateSwr } = useAppState();
return (
<form action={syncCacheAction}>
<form action={clearCacheAction}>
<SubmitButtonWithStatus
icon={<BiTrash size={16} />}
onFormSubmit={invalidateSwr}

View File

@ -6,35 +6,38 @@ import { ComponentProps } from 'react';
export default function PhotoSyncButton({
action,
includeLabel = true,
label,
onFormSubmit,
formData: { photoId, photoUrl } = {},
photoTitle,
hasAiTextGeneration,
shouldConfirm,
shouldToast,
}: {
action: (formData: FormData) => void
includeLabel?: boolean
label?: string
formData?: {
photoId?: string
photoUrl?: string
}
photoTitle?: string
hasAiTextGeneration?: boolean
shouldConfirm?: boolean
shouldToast?: boolean
} & ComponentProps<typeof SubmitButtonWithStatus>) {
const confirmText =
'Are you sure you want to overwrite EXIF data ' + (photoTitle
? `for "${photoTitle}" from source file? `
: 'from source file? '
) + 'This action cannot be undone.';
const confirmText = ['Overwrite'];
if (photoTitle) { confirmText.push(`"${photoTitle}"`); }
confirmText.push('data from original file?');
if (hasAiTextGeneration) { confirmText.push(
'This will also auto-generate AI text for undefined fields.'); }
confirmText.push('This action cannot be undone.');
return (
<FormWithConfirm
action={action}
confirmText={shouldConfirm ? confirmText : undefined}
confirmText={shouldConfirm ? confirmText.join(' ') : undefined}
>
{photoId &&
<input name="id" value={photoId} hidden readOnly />}
<input name="photoId" value={photoId} hidden readOnly />}
{photoUrl &&
<input name="photoUrl" value={photoUrl} hidden readOnly />}
<SubmitButtonWithStatus
@ -42,16 +45,16 @@ export default function PhotoSyncButton({
icon={<IconGrSync
className={clsx(
'translate-y-[0.5px] translate-x-[0.5px]',
includeLabel && 'sm:translate-x-[-0.5px]',
label && 'sm:translate-x-[-0.5px]',
)} />}
onFormSubmitToastMessage={shouldToast
? photoTitle
? `"${photoTitle}" EXIF data synced`
: 'EXIF data synced'
? `"${photoTitle}" data synced`
: 'Data synced'
: undefined}
onFormSubmit={onFormSubmit}
>
{includeLabel ? 'EXIF' : null}
{label}
</SubmitButtonWithStatus>
</FormWithConfirm>
);

View File

@ -2,7 +2,7 @@ import PhotoUpload from '@/photo/PhotoUpload';
import { clsx } from 'clsx/lite';
import SiteGrid from '@/components/SiteGrid';
import AdminUploadsTable from '@/admin/AdminUploadsTable';
import { PRO_MODE_ENABLED } from '@/site/config';
import { AI_TEXT_GENERATION_ENABLED, PRO_MODE_ENABLED } from '@/site/config';
import { getStoragePhotoUrlsNoStore } from '@/services/storage/cache';
import { getPhotos } from '@/photo/db/query';
import { revalidatePath } from 'next/cache';
@ -58,11 +58,15 @@ export default async function AdminPhotosPage() {
/>
</div>}
<div className="space-y-4">
<AdminPhotosTable photos={photos} />
<AdminPhotosTable
photos={photos}
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
/>
{photosCount > photos.length &&
<AdminPhotosTableInfinite
initialOffset={INFINITE_SCROLL_INITIAL_ADMIN_PHOTOS}
itemsPerPage={INFINITE_SCROLL_MULTIPLE_ADMIN_PHOTOS}
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
/>}
</div>
</div>}

View File

@ -40,14 +40,13 @@ export default function middleware(req: NextRequest, res:NextResponse) {
}
export const config = {
/* Excludes:
- /api + /api/auth*
- /_next/static*
- /_next/image*
- /favicon.ico + /favicons/*
- /grid
- / (root)
*/
// Excludes:
// - /api + /api/auth*
// - /_next/static*
// - /_next/image*
// - /favicon.ico + /favicons/*
// - /grid
// - / (root)
// eslint-disable-next-line max-len
matcher: ['/((?!api$|api/auth|_next/static|_next/image|favicon.ico$|favicons/|grid$|$).*)'],
};

View File

@ -70,6 +70,7 @@ export default function PhotoEditPageClient({
<AiButton {...{ aiContent, shouldConfirm: hasTextContent }} />}
<PhotoSyncButton
action={action}
label="EXIF"
formData={{ photoUrl: photo.url }}
/>
</div>}

View File

@ -279,31 +279,62 @@ export const getExifDataAction = async (
return {};
});
// Accessed from admin photo table
// will update blur data
export const syncPhotoExifDataAction = async (formData: FormData) =>
// Accessed from admin photo table, will:
// - update EXIF data
// - anonymize storage url if necessary
// - update blur data (or destroy if blur is disabled)
// - generate AI text data, if enabled, and auto-generated fields are empty
export const syncPhotoAction = async (formData: FormData) =>
runAuthenticatedAdminServerAction(async () => {
const photoId = formData.get('id') as string;
if (photoId) {
const photo = await getPhoto(photoId);
if (photo) {
const { photoFormExif } = await extractImageDataFromBlobPath(
photo.url, {
generateBlurData: BLUR_ENABLED,
});
if (photoFormExif) {
const photoFormDbInsert = convertFormDataToPhotoDbInsert({
...convertPhotoToFormData(photo),
...photoFormExif,
});
await updatePhoto(photoFormDbInsert);
revalidatePhotosKey();
const photoId = formData.get('photoId') as string | undefined;
const photo = await getPhoto(photoId ?? '', true);
if (photo) {
const {
photoFormExif,
imageResizedBase64,
} = await extractImageDataFromBlobPath(photo.url, {
includeInitialPhotoFields: false,
generateBlurData: BLUR_ENABLED,
generateResizedImage: AI_TEXT_GENERATION_ENABLED,
});
if (photoFormExif) {
if (photo.url.includes(photo.id)) {
// Anonymize storage url on update if necessary by
// re-running image upload transfer logic
const url = await convertUploadToPhoto(photo.url);
if (url) { photo.url = url; }
}
const {
title: atTitle,
caption: aiCaption,
tags: aiTags,
semanticDescription: aiSemanticDescription,
} = await generateAiImageQueries(
imageResizedBase64,
AI_TEXT_AUTO_GENERATED_FIELDS
);
const photoFormDbInsert = convertFormDataToPhotoDbInsert({
...convertPhotoToFormData(photo),
...photoFormExif,
...!BLUR_ENABLED && { blurData: undefined },
...!photo.title && { title: atTitle },
...!photo.caption && { caption: aiCaption },
...photo.tags.length === 0 && { tags: aiTags },
...!photo.semanticDescription &&
{ semanticDescription: aiSemanticDescription },
});
await updatePhoto(photoFormDbInsert);
revalidateAllKeysAndPaths();
}
}
});
export const syncCacheAction = async () =>
export const clearCacheAction = async () =>
runAuthenticatedAdminServerAction(revalidateAllKeysAndPaths);
export const streamAiImageQueryAction = async (