Shrink client bundle
This commit is contained in:
parent
47a53a2398
commit
7dd07aac6e
@ -1 +1 @@
|
||||
export { GET, POST } from '@/auth';
|
||||
export { GET, POST } from '@/auth/server';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { auth } from '@/auth';
|
||||
import { auth } from '@/auth/server';
|
||||
import {
|
||||
awsS3Client,
|
||||
awsS3PutObjectCommandForKey,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { auth } from '@/auth';
|
||||
import { auth } from '@/auth/server';
|
||||
import { revalidateAdminPaths, revalidatePhotosKey } from '@/photo/cache';
|
||||
import {
|
||||
ACCEPTED_PHOTO_FILE_TYPES,
|
||||
|
||||
@ -9,8 +9,8 @@ import { cache } from 'react';
|
||||
import { getPhotos, getPhotosMeta } from '@/photo/db/query';
|
||||
import { GRID_HOMEPAGE_ENABLED } from '@/app/config';
|
||||
import { getDataForCategories } from '@/category/data';
|
||||
import PhotoGridPage from '@/photo/PhotoGridPage';
|
||||
import PhotoFeedPage from '@/photo/PhotoFeedPage';
|
||||
import PhotoGridPage from '@/photo/PhotoGridPage';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
export const maxDuration = 60;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { auth } from '@/auth';
|
||||
import { auth } from '@/auth/server';
|
||||
import SignInForm from '@/auth/SignInForm';
|
||||
import { PATH_ADMIN, PATH_ROOT } from '@/app/paths';
|
||||
import { clsx } from 'clsx/lite';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { auth } from './src/auth';
|
||||
import { auth } from './src/auth/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import {
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from 'react-icons/bi';
|
||||
import { HiOutlineCog } from 'react-icons/hi';
|
||||
import ChecklistGroup from '@/components/ChecklistGroup';
|
||||
import { ConfigChecklistStatus } from '../app/config';
|
||||
import { AppConfiguration } from '../app/config';
|
||||
import StatusIcon from '@/components/StatusIcon';
|
||||
import { labelForStorage } from '@/platforms/storage';
|
||||
import { HiSparkles } from 'react-icons/hi';
|
||||
@ -109,7 +109,7 @@ export default function AdminAppConfigurationClient({
|
||||
// Component props
|
||||
simplifiedView,
|
||||
isAnalyzingConfiguration,
|
||||
}: ConfigChecklistStatus &
|
||||
}: AppConfiguration &
|
||||
Partial<Awaited<ReturnType<typeof testConnectionsAction>>> & {
|
||||
simplifiedView?: boolean
|
||||
isAnalyzingConfiguration?: boolean
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { runAuthenticatedAdminServerAction } from '@/auth';
|
||||
import { runAuthenticatedAdminServerAction } from '@/auth/server';
|
||||
import { testRedisConnection } from '@/platforms/redis';
|
||||
import { testOpenAiConnection } from '@/platforms/openai';
|
||||
import { testDatabaseConnection } from '@/platforms/postgres';
|
||||
|
||||
@ -317,9 +317,7 @@ export default function AdminAppInsightsClient({
|
||||
/>}
|
||||
content="Speed up page load times"
|
||||
expandContent={<>
|
||||
Improve load times by enabling static optimization
|
||||
{' '}
|
||||
on:
|
||||
Improve load times by enabling static optimization:
|
||||
<div className="flex flex-col gap-y-4 mt-3">
|
||||
{renderLabeledEnvVar(
|
||||
'Photo pages',
|
||||
|
||||
@ -399,11 +399,11 @@ export const APP_CONFIGURATION = {
|
||||
commitUrl: VERCEL_GIT_COMMIT_URL,
|
||||
};
|
||||
|
||||
export type ConfigChecklistStatus = typeof APP_CONFIGURATION;
|
||||
|
||||
export const IS_SITE_READY =
|
||||
APP_CONFIGURATION.hasDatabase &&
|
||||
APP_CONFIGURATION.hasStorageProvider &&
|
||||
APP_CONFIGURATION.hasAuthSecret &&
|
||||
APP_CONFIGURATION.hasAdminUser;
|
||||
|
||||
export type AppConfiguration = typeof APP_CONFIGURATION;
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} from '@/auth/server';
|
||||
import type { Session } from 'next-auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
generateAuthSecret,
|
||||
KEY_CALLBACK_URL,
|
||||
KEY_CREDENTIALS_CALLBACK_ROUTE_ERROR_URL,
|
||||
KEY_CREDENTIALS_SIGN_IN_ERROR,
|
||||
KEY_CREDENTIALS_SIGN_IN_ERROR_URL,
|
||||
KEY_CREDENTIALS_SUCCESS,
|
||||
auth,
|
||||
generateAuthSecret,
|
||||
signIn,
|
||||
signOut,
|
||||
} from '@/auth';
|
||||
import type { Session } from 'next-auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
} from '.';
|
||||
|
||||
export const signInAction = async (
|
||||
_prevState: string | undefined,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { cache } from 'react';
|
||||
import { auth } from '@/auth';
|
||||
import { auth } from '@/auth/server';
|
||||
|
||||
export const authCachedSafe = cache(() => auth().catch(() => null));
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import { deleteCookie, getCookie, storeCookie } from '@/utility/cookie';
|
||||
|
||||
const KEY_AUTH_EMAIL = 'authjs.email';
|
||||
|
||||
export const storeAuthEmailCookie = (email: string) =>
|
||||
storeCookie(KEY_AUTH_EMAIL, email);
|
||||
|
||||
export const clearAuthEmailCookie = () =>
|
||||
deleteCookie(KEY_AUTH_EMAIL);
|
||||
|
||||
export const hasAuthEmailCookie = () =>
|
||||
Boolean(getCookie(KEY_AUTH_EMAIL));
|
||||
@ -1,6 +1,4 @@
|
||||
import { isPathProtected } from '@/app/paths';
|
||||
import NextAuth, { User } from 'next-auth';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
import { deleteCookie, getCookie, storeCookie } from '@/utility/cookie';
|
||||
|
||||
export const KEY_CREDENTIALS_SIGN_IN_ERROR = 'CredentialsSignin';
|
||||
export const KEY_CREDENTIALS_SIGN_IN_ERROR_URL =
|
||||
@ -10,53 +8,16 @@ export const KEY_CREDENTIALS_CALLBACK_ROUTE_ERROR_URL =
|
||||
export const KEY_CREDENTIALS_SUCCESS = 'success';
|
||||
export const KEY_CALLBACK_URL = 'callbackUrl';
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
signIn,
|
||||
signOut,
|
||||
auth,
|
||||
} = NextAuth({
|
||||
providers: [
|
||||
Credentials({
|
||||
async authorize({ email, password }) {
|
||||
if (
|
||||
process.env.ADMIN_EMAIL && process.env.ADMIN_EMAIL === email &&
|
||||
process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD === password
|
||||
) {
|
||||
const user: User = { email, name: 'Admin User' };
|
||||
return user;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
authorized({ auth, request }) {
|
||||
const { pathname } = request.nextUrl;
|
||||
const KEY_AUTH_EMAIL = 'authjs.email';
|
||||
|
||||
const isUrlProtected = isPathProtected(pathname);
|
||||
const isUserLoggedIn = !!auth?.user;
|
||||
const isRequestAuthorized = !isUrlProtected || isUserLoggedIn;
|
||||
export const storeAuthEmailCookie = (email: string) =>
|
||||
storeCookie(KEY_AUTH_EMAIL, email);
|
||||
|
||||
return isRequestAuthorized;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: '/sign-in',
|
||||
},
|
||||
});
|
||||
export const clearAuthEmailCookie = () =>
|
||||
deleteCookie(KEY_AUTH_EMAIL);
|
||||
|
||||
export const runAuthenticatedAdminServerAction = async <T>(
|
||||
callback: () => T,
|
||||
): Promise<T> => {
|
||||
const session = await auth();
|
||||
if (session?.user) {
|
||||
return callback();
|
||||
} else {
|
||||
throw new Error('Unauthorized server action request');
|
||||
}
|
||||
};
|
||||
export const hasAuthEmailCookie = () =>
|
||||
Boolean(getCookie(KEY_AUTH_EMAIL));
|
||||
|
||||
export const generateAuthSecret = () => fetch(
|
||||
'https://generate-secret.vercel.app/32',
|
||||
|
||||
51
src/auth/server.ts
Normal file
51
src/auth/server.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { isPathProtected } from '@/app/paths';
|
||||
import NextAuth, { User } from 'next-auth';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
signIn,
|
||||
signOut,
|
||||
auth,
|
||||
} = NextAuth({
|
||||
providers: [
|
||||
Credentials({
|
||||
async authorize({ email, password }) {
|
||||
if (
|
||||
process.env.ADMIN_EMAIL && process.env.ADMIN_EMAIL === email &&
|
||||
process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD === password
|
||||
) {
|
||||
const user: User = { email, name: 'Admin User' };
|
||||
return user;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
authorized({ auth, request }) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
const isUrlProtected = isPathProtected(pathname);
|
||||
const isUserLoggedIn = !!auth?.user;
|
||||
const isRequestAuthorized = !isUrlProtected || isUserLoggedIn;
|
||||
|
||||
return isRequestAuthorized;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: '/sign-in',
|
||||
},
|
||||
});
|
||||
|
||||
export const runAuthenticatedAdminServerAction = async <T>(
|
||||
callback: () => T,
|
||||
): Promise<T> => {
|
||||
const session = await auth();
|
||||
if (session?.user) {
|
||||
return callback();
|
||||
} else {
|
||||
throw new Error('Unauthorized server action request');
|
||||
}
|
||||
};
|
||||
@ -12,8 +12,11 @@ import {
|
||||
FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS,
|
||||
labelForFujifilmSimulation,
|
||||
} from '@/platforms/fujifilm/simulation';
|
||||
import { deparameterize, formatCount } from '@/utility/string';
|
||||
import { formatCountDescriptive } from '@/utility/string';
|
||||
import {
|
||||
deparameterize,
|
||||
formatCount,
|
||||
formatCountDescriptive,
|
||||
} from '@/utility/string';
|
||||
import { AnnotatedTag } from '@/photo/form';
|
||||
import PhotoFilmIcon from './PhotoFilmIcon';
|
||||
|
||||
|
||||
@ -1,58 +1,24 @@
|
||||
'use client';
|
||||
import { ComponentProps } from 'react';
|
||||
import PhotoGridPageClient from './PhotoGridPageClient';
|
||||
import {
|
||||
htmlHasBrParagraphBreaks,
|
||||
safelyParseFormattedHtml,
|
||||
} from '@/utility/html';
|
||||
import { PAGE_ABOUT } from '@/app/config';
|
||||
|
||||
import { Photo } from '.';
|
||||
import { PATH_GRID_INFERRED } from '@/app/paths';
|
||||
import PhotoGridSidebar from './PhotoGridSidebar';
|
||||
import PhotoGridContainer from './PhotoGridContainer';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import clsx from 'clsx/lite';
|
||||
import { PhotoSetCategories } from '@/category';
|
||||
import useElementHeight from '@/utility/useElementHeight';
|
||||
import MaskedScroll from '@/components/MaskedScroll';
|
||||
export default function PhotoGridPage(
|
||||
props: ComponentProps<typeof PhotoGridPageClient>,
|
||||
) {
|
||||
const aboutTextSafelyParsedHtml = PAGE_ABOUT
|
||||
? safelyParseFormattedHtml(PAGE_ABOUT)
|
||||
: undefined;
|
||||
const aboutTextHasBrParagraphBreaks = PAGE_ABOUT
|
||||
? htmlHasBrParagraphBreaks(PAGE_ABOUT)
|
||||
: false;
|
||||
|
||||
export default function PhotoGridPage({
|
||||
photos,
|
||||
photosCount,
|
||||
...categories
|
||||
}: PhotoSetCategories & {
|
||||
photos: Photo[]
|
||||
photosCount: number
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { setSelectedPhotoIds } = useAppState();
|
||||
|
||||
useEffect(
|
||||
() => () => setSelectedPhotoIds?.(undefined),
|
||||
[setSelectedPhotoIds],
|
||||
);
|
||||
|
||||
const containerHeight = useElementHeight(ref);
|
||||
|
||||
return (
|
||||
<PhotoGridContainer
|
||||
cacheKey={`page-${PATH_GRID_INFERRED}`}
|
||||
photos={photos}
|
||||
count={photosCount}
|
||||
sidebar={
|
||||
<MaskedScroll
|
||||
className={clsx(
|
||||
'sticky top-0 -mb-5 -mt-5',
|
||||
'max-h-screen py-4',
|
||||
)}
|
||||
fadeHeight={36}
|
||||
hideScrollbar
|
||||
>
|
||||
<PhotoGridSidebar {...{
|
||||
...categories,
|
||||
photosCount,
|
||||
containerHeight,
|
||||
}}
|
||||
/>
|
||||
</MaskedScroll>
|
||||
}
|
||||
canSelect
|
||||
/>
|
||||
);
|
||||
return <PhotoGridPageClient {...{
|
||||
...props,
|
||||
aboutTextSafelyParsedHtml,
|
||||
aboutTextHasBrParagraphBreaks,
|
||||
}} />;
|
||||
}
|
||||
|
||||
56
src/photo/PhotoGridPageClient.tsx
Normal file
56
src/photo/PhotoGridPageClient.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { Photo } from '.';
|
||||
import { PATH_GRID_INFERRED } from '@/app/paths';
|
||||
import PhotoGridSidebar from './PhotoGridSidebar';
|
||||
import PhotoGridContainer from './PhotoGridContainer';
|
||||
import { ComponentProps, useEffect, useRef } from 'react';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import clsx from 'clsx/lite';
|
||||
import useElementHeight from '@/utility/useElementHeight';
|
||||
import MaskedScroll from '@/components/MaskedScroll';
|
||||
|
||||
export default function PhotoGridPageClient({
|
||||
photos,
|
||||
photosCount,
|
||||
...categories
|
||||
}: ComponentProps<typeof PhotoGridSidebar> & {
|
||||
photos: Photo[]
|
||||
photosCount: number
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { setSelectedPhotoIds } = useAppState();
|
||||
|
||||
useEffect(
|
||||
() => () => setSelectedPhotoIds?.(undefined),
|
||||
[setSelectedPhotoIds],
|
||||
);
|
||||
|
||||
const containerHeight = useElementHeight(ref);
|
||||
|
||||
return (
|
||||
<PhotoGridContainer
|
||||
cacheKey={`page-${PATH_GRID_INFERRED}`}
|
||||
photos={photos}
|
||||
count={photosCount}
|
||||
sidebar={
|
||||
<MaskedScroll
|
||||
className={clsx(
|
||||
'sticky top-0 -mb-5 -mt-5',
|
||||
'max-h-screen py-4',
|
||||
)}
|
||||
fadeHeight={36}
|
||||
hideScrollbar
|
||||
>
|
||||
<PhotoGridSidebar {...{
|
||||
...categories,
|
||||
photosCount,
|
||||
containerHeight,
|
||||
}} />
|
||||
</MaskedScroll>
|
||||
}
|
||||
canSelect
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -10,11 +10,7 @@ import FavsTag from '../tag/FavsTag';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import HiddenTag from '@/tag/HiddenTag';
|
||||
import { CATEGORY_VISIBILITY, PAGE_ABOUT } from '@/app/config';
|
||||
import {
|
||||
htmlHasBrParagraphBreaks,
|
||||
safelyParseFormattedHtml,
|
||||
} from '@/utility/html';
|
||||
import { CATEGORY_VISIBILITY } from '@/app/config';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||
import IconCamera from '@/components/icons/IconCamera';
|
||||
@ -38,11 +34,15 @@ export default function PhotoGridSidebar({
|
||||
photosCount,
|
||||
photosDateRange,
|
||||
containerHeight,
|
||||
aboutTextSafelyParsedHtml,
|
||||
aboutTextHasBrParagraphBreaks,
|
||||
...categories
|
||||
}: PhotoSetCategories & {
|
||||
photosCount: number
|
||||
photosDateRange?: PhotoDateRange
|
||||
containerHeight?: number
|
||||
aboutTextSafelyParsedHtml?: string
|
||||
aboutTextHasBrParagraphBreaks?: boolean
|
||||
}) {
|
||||
const {
|
||||
cameras,
|
||||
@ -245,16 +245,16 @@ export default function PhotoGridSidebar({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{PAGE_ABOUT && <HeaderList
|
||||
{aboutTextSafelyParsedHtml && <HeaderList
|
||||
items={[<p
|
||||
key="about"
|
||||
ref={aboutRef}
|
||||
className={clsx(
|
||||
'max-w-60 normal-case text-dim',
|
||||
htmlHasBrParagraphBreaks(PAGE_ABOUT) && 'pb-2',
|
||||
aboutTextHasBrParagraphBreaks && 'pb-2',
|
||||
)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: safelyParseFormattedHtml(PAGE_ABOUT),
|
||||
__html: aboutTextSafelyParsedHtml,
|
||||
}}
|
||||
/>]}
|
||||
/>}
|
||||
|
||||
@ -46,7 +46,7 @@ import {
|
||||
} from './server';
|
||||
import { TAG_FAVS, isTagFavs } from '@/tag';
|
||||
import { convertPhotoToPhotoDbInsert, Photo } from '.';
|
||||
import { runAuthenticatedAdminServerAction } from '@/auth';
|
||||
import { runAuthenticatedAdminServerAction } from '@/auth/server';
|
||||
import { AiImageQuery, getAiImageQuery } from './ai';
|
||||
import { streamOpenAiImageQuery } from '@/platforms/openai';
|
||||
import {
|
||||
|
||||
@ -1,22 +1,13 @@
|
||||
import type { ExifData } from 'ts-exif-parser';
|
||||
import { DEFAULT_ASPECT_RATIO, Photo, PhotoDbInsert, PhotoExif } from '..';
|
||||
import { DEFAULT_ASPECT_RATIO, Photo, PhotoDbInsert } from '..';
|
||||
import {
|
||||
convertTimestampToNaivePostgresString,
|
||||
convertTimestampWithOffsetToPostgresString,
|
||||
generateLocalNaivePostgresString,
|
||||
generateLocalPostgresString,
|
||||
validationMessageNaivePostgresDateString,
|
||||
validationMessagePostgresDateString,
|
||||
} from '@/utility/date';
|
||||
import {
|
||||
convertApertureValueToFNumber,
|
||||
getAspectRatioFromExif,
|
||||
getOffsetFromExif,
|
||||
} from '@/utility/exif';
|
||||
import { roundToNumber } from '@/utility/number';
|
||||
import { convertStringToArray, parameterize } from '@/utility/string';
|
||||
import { generateNanoid } from '@/utility/nanoid';
|
||||
import { GEO_PRIVACY_ENABLED } from '@/app/config';
|
||||
import { TAG_FAVS, getValidationMessageForTags } from '@/tag';
|
||||
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
@ -272,46 +263,6 @@ export const convertPhotoToFormData = (photo: Photo): PhotoFormData => {
|
||||
} as PhotoFormData);
|
||||
};
|
||||
|
||||
// CREATE FORM DATA: FROM EXIF
|
||||
|
||||
export const convertExifToFormData = (
|
||||
data: ExifData,
|
||||
film?: FujifilmSimulation,
|
||||
recipeData?: FujifilmRecipe,
|
||||
): Omit<
|
||||
Record<keyof PhotoExif, string | undefined>,
|
||||
'takenAt' | 'takenAtNaive'
|
||||
> => ({
|
||||
aspectRatio: getAspectRatioFromExif(data).toString(),
|
||||
make: data.tags?.Make,
|
||||
model: data.tags?.Model,
|
||||
focalLength: data.tags?.FocalLength?.toString(),
|
||||
focalLengthIn35MmFormat: data.tags?.FocalLengthIn35mmFormat?.toString(),
|
||||
lensMake: data.tags?.LensMake,
|
||||
lensModel: data.tags?.LensModel,
|
||||
fNumber: (
|
||||
data.tags?.FNumber?.toString() ||
|
||||
convertApertureValueToFNumber(data.tags?.ApertureValue)
|
||||
),
|
||||
iso: data.tags?.ISO?.toString() || data.tags?.ISOSpeed?.toString(),
|
||||
exposureTime: data.tags?.ExposureTime?.toString(),
|
||||
exposureCompensation: data.tags?.ExposureCompensation?.toString(),
|
||||
latitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLatitude?.toString() : undefined,
|
||||
longitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
|
||||
film,
|
||||
recipeData: JSON.stringify(recipeData),
|
||||
...data.tags?.DateTimeOriginal && {
|
||||
takenAt: convertTimestampWithOffsetToPostgresString(
|
||||
data.tags.DateTimeOriginal,
|
||||
getOffsetFromExif(data),
|
||||
),
|
||||
takenAtNaive:
|
||||
convertTimestampToNaivePostgresString(data.tags.DateTimeOriginal),
|
||||
},
|
||||
});
|
||||
|
||||
// PREPARE FORM FOR DB INSERT
|
||||
|
||||
export const convertFormDataToPhotoDbInsert = (
|
||||
|
||||
50
src/photo/form/server.ts
Normal file
50
src/photo/form/server.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import {
|
||||
convertApertureValueToFNumber,
|
||||
getAspectRatioFromExif,
|
||||
} from '@/utility/exif';
|
||||
import { GEO_PRIVACY_ENABLED } from '@/app/config';
|
||||
import { convertTimestampWithOffsetToPostgresString } from '@/utility/date';
|
||||
import { convertTimestampToNaivePostgresString } from '@/utility/date';
|
||||
import { getOffsetFromExif } from '@/utility/exif';
|
||||
import { PhotoExif } from '..';
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
|
||||
import type { ExifData } from 'ts-exif-parser';
|
||||
|
||||
export const convertExifToFormData = (
|
||||
data: ExifData,
|
||||
film?: FujifilmSimulation,
|
||||
recipeData?: FujifilmRecipe,
|
||||
): Omit<
|
||||
Record<keyof PhotoExif, string | undefined>,
|
||||
'takenAt' | 'takenAtNaive'
|
||||
> => ({
|
||||
aspectRatio: getAspectRatioFromExif(data).toString(),
|
||||
make: data.tags?.Make,
|
||||
model: data.tags?.Model,
|
||||
focalLength: data.tags?.FocalLength?.toString(),
|
||||
focalLengthIn35MmFormat: data.tags?.FocalLengthIn35mmFormat?.toString(),
|
||||
lensMake: data.tags?.LensMake,
|
||||
lensModel: data.tags?.LensModel,
|
||||
fNumber: (
|
||||
data.tags?.FNumber?.toString() ||
|
||||
convertApertureValueToFNumber(data.tags?.ApertureValue)
|
||||
),
|
||||
iso: data.tags?.ISO?.toString() || data.tags?.ISOSpeed?.toString(),
|
||||
exposureTime: data.tags?.ExposureTime?.toString(),
|
||||
exposureCompensation: data.tags?.ExposureCompensation?.toString(),
|
||||
latitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLatitude?.toString() : undefined,
|
||||
longitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
|
||||
film,
|
||||
recipeData: JSON.stringify(recipeData),
|
||||
...data.tags?.DateTimeOriginal && {
|
||||
takenAt: convertTimestampWithOffsetToPostgresString(
|
||||
data.tags.DateTimeOriginal,
|
||||
getOffsetFromExif(data),
|
||||
),
|
||||
takenAtNaive:
|
||||
convertTimestampToNaivePostgresString(data.tags.DateTimeOriginal),
|
||||
},
|
||||
});
|
||||
@ -2,10 +2,7 @@ import {
|
||||
getExtensionFromStorageUrl,
|
||||
getIdFromStorageUrl,
|
||||
} from '@/platforms/storage';
|
||||
import {
|
||||
convertExifToFormData,
|
||||
convertFormDataToPhotoDbInsert,
|
||||
} from '@/photo/form';
|
||||
import { convertFormDataToPhotoDbInsert } from '@/photo/form';
|
||||
import {
|
||||
FujifilmSimulation,
|
||||
getFujifilmSimulationFromMakerNote,
|
||||
@ -17,7 +14,7 @@ import {
|
||||
GEO_PRIVACY_ENABLED,
|
||||
PRESERVE_ORIGINAL_UPLOADS,
|
||||
} from '@/app/config';
|
||||
import { isExifForFujifilm } from '@/platforms/fujifilm';
|
||||
import { isExifForFujifilm } from '@/platforms/fujifilm/server';
|
||||
import {
|
||||
FujifilmRecipe,
|
||||
getFujifilmRecipeFromMakerNote,
|
||||
@ -27,6 +24,8 @@ import {
|
||||
updateAllMatchingRecipeTitles,
|
||||
} from './db/query';
|
||||
import { PhotoDbInsert } from '.';
|
||||
import { convertExifToFormData } from './form/server';
|
||||
|
||||
const IMAGE_WIDTH_RESIZE = 200;
|
||||
const IMAGE_WIDTH_BLUR = 200;
|
||||
|
||||
|
||||
@ -1,84 +1,4 @@
|
||||
// MakerNote tag IDs and values referenced from:
|
||||
// - github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/FujiFilm.pm
|
||||
// - exiftool.org/TagNames/FujiFilm.html
|
||||
|
||||
import type { ExifData } from 'ts-exif-parser';
|
||||
|
||||
export const MAKE_FUJIFILM = 'FUJIFILM';
|
||||
|
||||
// Makernote Offsets
|
||||
const BYTE_OFFSET_TAG_COUNT = 12;
|
||||
const BYTE_OFFSET_FIRST_TAG = 14;
|
||||
|
||||
// Tag Offsets
|
||||
const BYTE_OFFSET_TAG_TYPE = 2;
|
||||
const BYTE_OFFSET_TAG_SIZE = 4;
|
||||
const BYTE_OFFSET_TAG_VALUE = 8;
|
||||
|
||||
// Tag Sizes
|
||||
const BYTES_PER_TAG = 12;
|
||||
const BYTES_PER_TAG_VALUE = 4;
|
||||
|
||||
export const isExifForFujifilm = (data: ExifData) =>
|
||||
data.tags?.Make?.toLocaleUpperCase() === MAKE_FUJIFILM;
|
||||
|
||||
export const isMakeFujifilm = (make?: string) =>
|
||||
make?.toLocaleUpperCase() === MAKE_FUJIFILM;
|
||||
|
||||
export const parseFujifilmMakerNote = (
|
||||
bytes: Buffer,
|
||||
sendTagNumbers: (tagId: number, numbers: number[]) => void,
|
||||
) => {
|
||||
const tagCount = bytes.readUint16LE(BYTE_OFFSET_TAG_COUNT);
|
||||
|
||||
for (let i = 0; i < tagCount; i++) {
|
||||
const index = BYTE_OFFSET_FIRST_TAG + i * BYTES_PER_TAG;
|
||||
|
||||
if (index + BYTES_PER_TAG < bytes.length) {
|
||||
const tagId = bytes.readUInt16LE(index);
|
||||
const tagType = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_TYPE);
|
||||
const tagValueSize = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_SIZE);
|
||||
|
||||
const sendNumbersForDataType = (
|
||||
parseNumberAtOffset: (offset: number) => number,
|
||||
sizeInBytes: number,
|
||||
) => {
|
||||
let values: number[] = [];
|
||||
if (tagValueSize * sizeInBytes <= BYTES_PER_TAG_VALUE) {
|
||||
// Retrieve values if they fit in tag block
|
||||
values = Array.from({ length: tagValueSize }, (_, i) =>
|
||||
parseNumberAtOffset(
|
||||
index + BYTE_OFFSET_TAG_VALUE + i * sizeInBytes,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Retrieve outside values if they don't fit in tag block
|
||||
const offset = bytes.readUint16LE(index + BYTE_OFFSET_TAG_VALUE);
|
||||
for (let i = 0; i < tagValueSize; i++) {
|
||||
values.push(parseNumberAtOffset(offset + i * sizeInBytes));
|
||||
}
|
||||
}
|
||||
sendTagNumbers(tagId, values);
|
||||
};
|
||||
|
||||
switch (tagType) {
|
||||
// Int8 (UInt8 read as Int8 according to spec)
|
||||
case 1:
|
||||
sendNumbersForDataType(offset => bytes.readInt8(offset), 1);
|
||||
break;
|
||||
// UInt16
|
||||
case 3:
|
||||
sendNumbersForDataType(offset => bytes.readUInt16LE(offset), 2);
|
||||
break;
|
||||
// UInt32
|
||||
case 4:
|
||||
sendNumbersForDataType(offset => bytes.readUInt32LE(offset), 4);
|
||||
break;
|
||||
// Int32
|
||||
case 9:
|
||||
sendNumbersForDataType(offset => bytes.readInt32LE(offset), 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { parseFujifilmMakerNote } from '.';
|
||||
import { parseFujifilmMakerNote } from './server';
|
||||
|
||||
const TAG_ID_DYNAMIC_RANGE = 0x1400;
|
||||
const TAG_ID_DYNAMIC_RANGE_SETTING = 0x1402;
|
||||
|
||||
80
src/platforms/fujifilm/server.ts
Normal file
80
src/platforms/fujifilm/server.ts
Normal file
@ -0,0 +1,80 @@
|
||||
// MakerNote tag IDs and values referenced from:
|
||||
// - github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/FujiFilm.pm
|
||||
// - exiftool.org/TagNames/FujiFilm.html
|
||||
|
||||
import type { ExifData } from 'ts-exif-parser';
|
||||
import { MAKE_FUJIFILM } from '.';
|
||||
|
||||
// Makernote Offsets
|
||||
const BYTE_OFFSET_TAG_COUNT = 12;
|
||||
const BYTE_OFFSET_FIRST_TAG = 14;
|
||||
|
||||
// Tag Offsets
|
||||
const BYTE_OFFSET_TAG_TYPE = 2;
|
||||
const BYTE_OFFSET_TAG_SIZE = 4;
|
||||
const BYTE_OFFSET_TAG_VALUE = 8;
|
||||
|
||||
// Tag Sizes
|
||||
const BYTES_PER_TAG = 12;
|
||||
const BYTES_PER_TAG_VALUE = 4;
|
||||
|
||||
export const isExifForFujifilm = (data: ExifData) =>
|
||||
data.tags?.Make?.toLocaleUpperCase() === MAKE_FUJIFILM;
|
||||
|
||||
export const parseFujifilmMakerNote = (
|
||||
bytes: Buffer,
|
||||
sendTagNumbers: (tagId: number, numbers: number[]) => void,
|
||||
) => {
|
||||
const tagCount = bytes.readUint16LE(BYTE_OFFSET_TAG_COUNT);
|
||||
|
||||
for (let i = 0; i < tagCount; i++) {
|
||||
const index = BYTE_OFFSET_FIRST_TAG + i * BYTES_PER_TAG;
|
||||
|
||||
if (index + BYTES_PER_TAG < bytes.length) {
|
||||
const tagId = bytes.readUInt16LE(index);
|
||||
const tagType = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_TYPE);
|
||||
const tagValueSize = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_SIZE);
|
||||
|
||||
const sendNumbersForDataType = (
|
||||
parseNumberAtOffset: (offset: number) => number,
|
||||
sizeInBytes: number,
|
||||
) => {
|
||||
let values: number[] = [];
|
||||
if (tagValueSize * sizeInBytes <= BYTES_PER_TAG_VALUE) {
|
||||
// Retrieve values if they fit in tag block
|
||||
values = Array.from({ length: tagValueSize }, (_, i) =>
|
||||
parseNumberAtOffset(
|
||||
index + BYTE_OFFSET_TAG_VALUE + i * sizeInBytes,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Retrieve outside values if they don't fit in tag block
|
||||
const offset = bytes.readUint16LE(index + BYTE_OFFSET_TAG_VALUE);
|
||||
for (let i = 0; i < tagValueSize; i++) {
|
||||
values.push(parseNumberAtOffset(offset + i * sizeInBytes));
|
||||
}
|
||||
}
|
||||
sendTagNumbers(tagId, values);
|
||||
};
|
||||
|
||||
switch (tagType) {
|
||||
// Int8 (UInt8 read as Int8 according to spec)
|
||||
case 1:
|
||||
sendNumbersForDataType(offset => bytes.readInt8(offset), 1);
|
||||
break;
|
||||
// UInt16
|
||||
case 3:
|
||||
sendNumbersForDataType(offset => bytes.readUInt16LE(offset), 2);
|
||||
break;
|
||||
// UInt32
|
||||
case 4:
|
||||
sendNumbersForDataType(offset => bytes.readUInt32LE(offset), 4);
|
||||
break;
|
||||
// Int32
|
||||
case 9:
|
||||
sendNumbersForDataType(offset => bytes.readInt32LE(offset), 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { parseFujifilmMakerNote } from '.';
|
||||
import { parseFujifilmMakerNote } from './server';
|
||||
|
||||
const TAG_ID_SATURATION = 0x1003;
|
||||
const TAG_ID_FILM_MODE = 0x1401;
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
storeAuthEmailCookie,
|
||||
clearAuthEmailCookie,
|
||||
hasAuthEmailCookie,
|
||||
} from '@/auth/client';
|
||||
} from '@/auth';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { isPathAdmin, PATH_ROOT } from '@/app/paths';
|
||||
import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload';
|
||||
|
||||
@ -9,4 +9,4 @@ export const safelyParseFormattedHtml = (text: string) =>
|
||||
|
||||
// Matches two or more <br> or <br /> tags in a row
|
||||
export const htmlHasBrParagraphBreaks = (text: string) =>
|
||||
text.match(/(<br\s*\/?>){2}/i);
|
||||
/(<br\s*\/?>){2}/i.test(text);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user