From c3b3fe4367ddd846f7eb2f925b826a346805dc5d Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 16 Mar 2025 21:28:12 -0500 Subject: [PATCH] Add initial iPhone lens text formatting support --- __tests__/lens.test.ts | 14 ++--- src/admin/AdminAppMenu.tsx | 3 +- src/app/paths.ts | 2 +- src/components/cmdk/CommandKClient.tsx | 17 +++--- src/components/icons/IconLock.tsx | 10 +++- src/components/primitives/EntityLink.tsx | 3 + src/components/primitives/LabeledIcon.tsx | 5 +- src/lens/PhotoLens.tsx | 4 +- src/lens/apple.ts | 71 +++++++++++++++++++++++ src/lens/index.ts | 33 +---------- src/lens/meta.ts | 4 +- src/photo/PhotoLarge.tsx | 1 + src/photo/PhotoLink.tsx | 8 +-- src/photo/PhotoPrevNext.tsx | 63 ++++++-------------- 14 files changed, 132 insertions(+), 106 deletions(-) create mode 100644 src/lens/apple.ts diff --git a/__tests__/lens.test.ts b/__tests__/lens.test.ts index 5b392cae..d9a8a302 100644 --- a/__tests__/lens.test.ts +++ b/__tests__/lens.test.ts @@ -2,15 +2,15 @@ import { formatLensText, Lens } from '@/lens'; const IPHONE_15_PRO_FRONT: Lens = { make: 'Apple', model: 'iPhone 15 Pro front TrueDepth camera 2.69mm f/1.9' }; -const IPHONE_15_PRO_BACK_WIDE: Lens = { make: 'Apple', model: 'iPhone 15 Pro front TrueDepth camera 2.69mm f/1.9' }; -const IPHONE_15_PRO_BACK_MAIN: Lens = { make: 'Apple', model: 'iPhone 15 Pro front TrueDepth camera 2.69mm f/1.9' }; -const IPHONE_15_PRO_BACK_TELEPHOTO: Lens = { make: 'Apple', model: 'iPhone 15 Pro front TrueDepth camera 2.69mm f/1.9' }; +const IPHONE_15_PRO_BACK_WIDE: Lens = { make: 'Apple', model: 'iPhone 15 Pro back triple camera 6.765mm f/2.2' }; +const IPHONE_15_PRO_BACK_MAIN: Lens = { make: 'Apple', model: 'iPhone 15 Pro back triple camera 6.765mm f/1.78' }; +const IPHONE_15_PRO_BACK_TELEPHOTO: Lens = { make: 'Apple', model: 'iPhone 15 Pro back triple camera 6.765mm f/2.8' }; describe('Lens', () => { it('correctly formats iPhone lenses', () => { - expect(formatLensText(IPHONE_15_PRO_FRONT)).toBe('Front Camera'); - expect(formatLensText(IPHONE_15_PRO_BACK_WIDE)).toBe('Wide Camera'); - expect(formatLensText(IPHONE_15_PRO_BACK_MAIN)).toBe('Main Camera'); - expect(formatLensText(IPHONE_15_PRO_BACK_TELEPHOTO)).toBe('Telephoto Camera'); + expect(formatLensText(IPHONE_15_PRO_FRONT)).toBe('15 Pro front'); + expect(formatLensText(IPHONE_15_PRO_BACK_WIDE)).toBe('15 Pro Wide (6.765mm)'); + expect(formatLensText(IPHONE_15_PRO_BACK_MAIN)).toBe('15 Pro Main (6.765mm)'); + expect(formatLensText(IPHONE_15_PRO_BACK_TELEPHOTO)).toBe('15 Pro Telephoto (6.765mm)'); }); }); diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index 0ea73823..68bc42aa 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -161,7 +161,8 @@ export default function AdminAppMenu({ header={
Admin menu
} diff --git a/src/app/paths.ts b/src/app/paths.ts index d4f85a7c..f73abe29 100644 --- a/src/app/paths.ts +++ b/src/app/paths.ts @@ -113,9 +113,9 @@ const getPhotoId = (photoOrPhotoId: PhotoOrPhotoId) => export const pathForPhoto = ({ photo, - tag, camera, lens, + tag, simulation, focal, recipe, diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx index b84898b0..9da4215f 100644 --- a/src/components/cmdk/CommandKClient.tsx +++ b/src/components/cmdk/CommandKClient.tsx @@ -37,12 +37,12 @@ import { useDebounce } from 'use-debounce'; import Spinner from '../Spinner'; import { usePathname, useRouter } from 'next/navigation'; import { useTheme } from 'next-themes'; -import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi'; +import { BiDesktop, BiLockAlt, BiMoon, BiSun } from 'react-icons/bi'; import { IoInvertModeSharp } from 'react-icons/io5'; import { useAppState } from '@/state/AppState'; import { searchPhotosAction } from '@/photo/actions'; import { RiToolsFill } from 'react-icons/ri'; -import { BiLockAlt, BiSolidUser } from 'react-icons/bi'; +import { BiSolidUser } from 'react-icons/bi'; import { HiDocumentText } from 'react-icons/hi'; import { signOutAction } from '@/auth/actions'; import { getKeywordsForPhoto, titleForPhoto } from '@/photo'; @@ -69,6 +69,7 @@ import IconPhoto from '../icons/IconPhoto'; import IconRecipe from '../icons/IconRecipe'; import IconFocalLength from '../icons/IconFocalLength'; import IconFilmSimulation from '../icons/IconFilmSimulation'; +import IconLock from '../icons/IconLock'; const DIALOG_TITLE = 'Global Command-K Menu'; const DIALOG_DESCRIPTION = 'For searching photos, views, and settings'; @@ -424,20 +425,20 @@ export default function CommandKClient({ if (isUserSignedIn) { adminSection.items.push({ label: 'Manage Photos', - annotation: , + annotation: , path: PATH_ADMIN_PHOTOS, }); if (uploadsCount) { adminSection.items.push({ label: 'Manage Uploads', - annotation: , + annotation: , path: PATH_ADMIN_UPLOADS, }); } if (tagsCount) { adminSection.items.push({ label: 'Manage Tags', - annotation: , + annotation: , path: PATH_ADMIN_TAGS, }); } @@ -448,17 +449,17 @@ export default function CommandKClient({ } , keywords: ['app insights'], - annotation: , + annotation: , path: PATH_ADMIN_INSIGHTS, }, { label: 'App Config', - annotation: , + annotation: , path: PATH_ADMIN_CONFIGURATION, }, { label: selectedPhotoIds === undefined ? 'Select Multiple Photos' : 'Exit Select Multiple Photos', - annotation: , + annotation: , path: selectedPhotoIds === undefined ? PATH_GRID_INFERRED : undefined, diff --git a/src/components/icons/IconLock.tsx b/src/components/icons/IconLock.tsx index 86aeeb86..05a07c23 100644 --- a/src/components/icons/IconLock.tsx +++ b/src/components/icons/IconLock.tsx @@ -1,6 +1,12 @@ import { IconBaseProps } from 'react-icons'; +import { BiLockAlt } from 'react-icons/bi'; import { FiLock } from 'react-icons/fi'; -export default function IconLock(props: IconBaseProps) { - return ; +export default function IconLock({ + narrow, + ...props +}: IconBaseProps & { narrow?: boolean }) { + return narrow + ? + : ; } diff --git a/src/components/primitives/EntityLink.tsx b/src/components/primitives/EntityLink.tsx index 9fbfa094..8e9a38f6 100644 --- a/src/components/primitives/EntityLink.tsx +++ b/src/components/primitives/EntityLink.tsx @@ -31,6 +31,7 @@ export default function EntityLink({ truncate = true, className, classNameIcon, + uppercase, debug, }: { icon: ReactNode @@ -44,6 +45,7 @@ export default function EntityLink({ truncate?: boolean className?: string classNameIcon?: string + uppercase?: boolean debug?: boolean } & EntityLinkExternalProps) { const classForContrast = () => { @@ -81,6 +83,7 @@ export default function EntityLink({ prefetch, title, type, + uppercase, className: clsx( classForContrast(), href && !badged && 'hover:text-gray-900 dark:hover:text-gray-100', diff --git a/src/components/primitives/LabeledIcon.tsx b/src/components/primitives/LabeledIcon.tsx index 9ab702e5..e0d15f4a 100644 --- a/src/components/primitives/LabeledIcon.tsx +++ b/src/components/primitives/LabeledIcon.tsx @@ -15,6 +15,7 @@ export default function LabeledIcon({ classNameIcon, children, iconWide, + uppercase = true, debug, }: { icon?: ReactNode, @@ -23,6 +24,7 @@ export default function LabeledIcon({ classNameIcon?: string, children: ReactNode, iconWide?:boolean, + uppercase?: boolean, debug?: boolean, }) { return ( @@ -44,7 +46,8 @@ export default function LabeledIcon({ } {children && type !== 'icon-only' && {children} diff --git a/src/lens/PhotoLens.tsx b/src/lens/PhotoLens.tsx index 604bdb44..1e462f0a 100644 --- a/src/lens/PhotoLens.tsx +++ b/src/lens/PhotoLens.tsx @@ -13,13 +13,15 @@ export default function PhotoLens({ prefetch, countOnHover, className, + shortText, }: { lens: Lens countOnHover?: number + shortText?: boolean } & EntityLinkExternalProps) { return ( + make?.toLocaleLowerCase() === LENS_MAKE_APPLE; + +export const isLensApple = ({ make }: Lens) => + isLensMakeApple(make); + +export const formatAppleLensText = ( + model: string, + includePhoneName?: boolean, +) => { + const [ + _, + phoneName, + side, + focalLength, + aperture, + ] = (/iPhone ([0-9a-z]{1,3}(?: (?:Pro|Max))*).*?(back|front).*?([0-9\.]+)mm.*?f\/([0-9\.]+)/gi.exec(model) ?? []); + + const format = (lensName: string, includeFocalLength = true) => { + let result = ''; + if (includePhoneName) { + result += `${phoneName} `; + } + result += lensName; + if (!includePhoneName) { + result += ' Camera'; + } + if (includeFocalLength && focalLength) { + result += ` (${focalLength}mm)`; + } + return result; + }; + + if (side === 'front') { + return format('front', false); + } else if (side === 'back') { + switch (phoneName) { + case '13 Pro': + switch (aperture) { + case '1.8': return format('Wide'); + case '1.5': return format('Main'); + case '2.8': return format('Telephoto'); + } + case '14 Pro': + switch (aperture) { + case '2.2': return format('Wide'); + case '1.78': return format('Main'); + case '2.8': return format('Telephoto'); + } + case '15 Pro': + switch (aperture) { + case '2.2': return format('Wide'); + case '1.78': return format('Main'); + case '2.8': return format('Telephoto'); + } + case '16 Pro': + switch (aperture) { + case '2.2': return format('Wide'); + case '1.78': return format('Main'); + case '2.8': return format('Telephoto'); + } + } + } + + return model; +}; \ No newline at end of file diff --git a/src/lens/index.ts b/src/lens/index.ts index 7aa7182f..9e36c191 100644 --- a/src/lens/index.ts +++ b/src/lens/index.ts @@ -1,10 +1,9 @@ import { Photo } from '@/photo'; import { parameterize } from '@/utility/string'; +import { formatAppleLensText, isLensMakeApple } from './apple'; const LENS_PLACEHOLDER: Lens = { make: 'Lens', model: 'Model' }; -const LENS_MAKE_APPLE = 'apple'; - export type Lens = { make: string model: string @@ -58,41 +57,13 @@ export const lensFromPhoto = ( ? { make: photo.lensMake, model: photo.lensModel } : fallback ?? LENS_PLACEHOLDER; -const isLensMakeApple = (make?: string) => - make?.toLocaleLowerCase() === LENS_MAKE_APPLE; - -export const isLensApple = ({ make }: Lens) => - isLensMakeApple(make); - -const formatAppleLensText = ( - model: string, - includePhoneName?: boolean, -) => { - if (model.includes('15 Pro')) { - const phoneName = '15 Pro'; - if (model.includes('front')) { return includePhoneName - ? `${phoneName}: Front Camera` - : 'Front Camera'; } - if (model.includes('f/2.2')) { return includePhoneName - ? `${phoneName}: Wide Camera` - : 'Wide Camera'; } - if (model.includes('f/1.78')) { return includePhoneName - ? `${phoneName}: Main Camera` - : 'Main Camera'; } - if (model.includes('f/2.8')) { return includePhoneName - ? `${phoneName}: Telephoto Camera` - : 'Telephoto Camera'; } - } - return model; -}; - export const formatLensText = ( { make, model: modelRaw }: Lens, length: 'long' | // Unmodified make and model 'medium' | // Make and model, with modifiers removed 'short' // Model only - = 'short', + = 'medium', ) => { // Capture simple make without modifiers like 'Corporation' or 'Company' const makeSimple = make.match(/^(\S+)/)?.[1]; diff --git a/src/lens/meta.ts b/src/lens/meta.ts index 696cb87b..6b7036ac 100644 --- a/src/lens/meta.ts +++ b/src/lens/meta.ts @@ -19,7 +19,7 @@ export const titleForLens = ( photos: Photo[], explicitCount?: number, ) => [ - 'Shot on', + 'Lens:', formatLensText(lensFromPhoto(photos[0], lens)), photoQuantityText(explicitCount ?? photos.length), ].join(' '); @@ -29,7 +29,7 @@ export const shareTextForLens = ( photos: Photo[], ) => [ - 'Photos shot on', + 'Lens:', formatLensText(lensFromPhoto(photos[0], lens)), ].join(' '); diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 1efdeb09..9cffd626 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -295,6 +295,7 @@ export default function PhotoLarge({ lens={lens} contrast="medium" prefetch={prefetchRelatedLinks} + shortText />} } {showRecipeContent && recipeTitle && diff --git a/src/photo/PhotoLink.tsx b/src/photo/PhotoLink.tsx index aa5d23a5..e0701cdd 100644 --- a/src/photo/PhotoLink.tsx +++ b/src/photo/PhotoLink.tsx @@ -11,16 +11,12 @@ import { clsx } from 'clsx/lite'; export default function PhotoLink({ photo, - tag, - camera, - simulation, - focal, - recipe, scroll, prefetch, nextPhotoAnimation, className, children, + ...categories }: { photo?: Photo scroll?: boolean @@ -34,7 +30,7 @@ export default function PhotoLink({ return ( photo ? { if (nextPhotoAnimation) { diff --git a/src/photo/PhotoPrevNext.tsx b/src/photo/PhotoPrevNext.tsx index f8a943b2..502ced35 100644 --- a/src/photo/PhotoPrevNext.tsx +++ b/src/photo/PhotoPrevNext.tsx @@ -24,11 +24,7 @@ export default function PhotoPrevNext({ photo, photos = [], className, - tag, - camera, - simulation, - focal, - recipe, + ...categories }: { photo?: Photo photos?: Photo[] @@ -44,42 +40,30 @@ export default function PhotoPrevNext({ const previousPhoto = photo ? getPreviousPhoto(photo, photos) : undefined; const nextPhoto = photo ? getNextPhoto(photo, photos) : undefined; + const pathPrevious = previousPhoto + ? pathForPhoto({ photo: previousPhoto, ...categories }) + : undefined; + + const pathNext = nextPhoto + ? pathForPhoto({ photo: nextPhoto, ...categories }) + : undefined; + useEffect(() => { if (shouldRespondToKeyboardCommands) { const onKeyUp = (e: KeyboardEvent) => { switch (e.key.toUpperCase()) { case 'ARROWLEFT': case 'J': - if (previousPhoto) { + if (pathPrevious) { setNextPhotoAnimation?.(ANIMATION_RIGHT); - router.push( - pathForPhoto({ - photo: previousPhoto, - tag, - camera, - simulation, - focal, - recipe, - }), - { scroll: false }, - ); + router.push(pathPrevious, { scroll: false }); } break; case 'ARROWRIGHT': case 'L': - if (nextPhoto) { + if (pathNext) { setNextPhotoAnimation?.(ANIMATION_LEFT); - router.push( - pathForPhoto({ - photo: nextPhoto, - tag, - camera, - simulation, - focal, - recipe, - }), - { scroll: false }, - ); + router.push(pathNext, { scroll: false }); } break; }; @@ -91,13 +75,8 @@ export default function PhotoPrevNext({ router, shouldRespondToKeyboardCommands, setNextPhotoAnimation, - previousPhoto, - nextPhoto, - tag, - camera, - simulation, - focal, - recipe, + pathPrevious, + pathNext, ]); return ( @@ -111,14 +90,10 @@ export default function PhotoPrevNext({ 'items-center sm:items-start', )}> @@ -131,14 +106,10 @@ export default function PhotoPrevNext({ /