diff --git a/src/admin/AdminPhotosTable.tsx b/src/admin/AdminPhotosTable.tsx index a8db1804..36a969a5 100644 --- a/src/admin/AdminPhotosTable.tsx +++ b/src/admin/AdminPhotosTable.tsx @@ -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({ )}> diff --git a/src/admin/AdminPhotosTableInfinite.tsx b/src/admin/AdminPhotosTableInfinite.tsx index ad7f61fc..33b8b793 100644 --- a/src/admin/AdminPhotosTableInfinite.tsx +++ b/src/admin/AdminPhotosTableInfinite.tsx @@ -7,9 +7,11 @@ import AdminPhotosTable from './AdminPhotosTable'; export default function AdminPhotosTableInfinite({ initialOffset, itemsPerPage, + hasAiTextGeneration, }: { initialOffset: number itemsPerPage: number + hasAiTextGeneration?: boolean }) { return ( } ); diff --git a/src/admin/ClearCacheButton.tsx b/src/admin/ClearCacheButton.tsx index e00b9a6b..32d07050 100644 --- a/src/admin/ClearCacheButton.tsx +++ b/src/admin/ClearCacheButton.tsx @@ -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 ( -
+ } onFormSubmit={invalidateSwr} diff --git a/src/admin/PhotoSyncButton.tsx b/src/admin/PhotoSyncButton.tsx index ccfbcdaf..61a1eb31 100644 --- a/src/admin/PhotoSyncButton.tsx +++ b/src/admin/PhotoSyncButton.tsx @@ -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) { - 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 ( {photoId && - } + } {photoUrl && } } onFormSubmitToastMessage={shouldToast ? photoTitle - ? `"${photoTitle}" EXIF data synced` - : 'EXIF data synced' + ? `"${photoTitle}" data synced` + : 'Data synced' : undefined} onFormSubmit={onFormSubmit} > - {includeLabel ? 'EXIF' : null} + {label} ); diff --git a/src/app/admin/photos/page.tsx b/src/app/admin/photos/page.tsx index 66609ca0..542c0d65 100644 --- a/src/app/admin/photos/page.tsx +++ b/src/app/admin/photos/page.tsx @@ -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() { /> }
- + {photosCount > photos.length && }
} diff --git a/src/middleware.ts b/src/middleware.ts index 43386d1e..ac78095a 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -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$|$).*)'], }; diff --git a/src/photo/PhotoEditPageClient.tsx b/src/photo/PhotoEditPageClient.tsx index 34303e8a..8b7fc05f 100644 --- a/src/photo/PhotoEditPageClient.tsx +++ b/src/photo/PhotoEditPageClient.tsx @@ -70,6 +70,7 @@ export default function PhotoEditPageClient({ } } diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 82236f2d..b9ba4144 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -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 (