Add comprehensive photo data syncing
This commit is contained in:
parent
ce2a5a213f
commit
3021018dc0
@ -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
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>}
|
||||
|
||||
@ -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$|$).*)'],
|
||||
};
|
||||
|
||||
@ -70,6 +70,7 @@ export default function PhotoEditPageClient({
|
||||
<AiButton {...{ aiContent, shouldConfirm: hasTextContent }} />}
|
||||
<PhotoSyncButton
|
||||
action={action}
|
||||
label="EXIF"
|
||||
formData={{ photoUrl: photo.url }}
|
||||
/>
|
||||
</div>}
|
||||
|
||||
@ -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 (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user