From b9cba9b14b45266d45e4dd584afc5c784b56d1b1 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 25 Oct 2023 12:38:19 -0500 Subject: [PATCH 01/13] Parse and store fujifilm simulations --- .vscode/settings.json | 7 + .../admin/uploads/[uploadPath]/page.tsx | 34 ++++- src/photo/PhotoForm.tsx | 6 +- src/photo/form.ts | 13 +- src/utility/fujifilm.ts | 120 ++++++++++++++++++ 5 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 src/utility/fujifilm.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 6eb8f0ac..91af0e49 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,24 +1,31 @@ { "cSpell.words": [ "ABCDEFGHIJKLMNOP", + "Acros", "ARROWLEFT", "ARROWRIGHT", + "Astia", "camelcase", + "Eterna", "exif", + "exiftool", "ghijklmnopqrstuv", "hgetall", "hset", "Lightbox", "nanoids", "nextjs", + "Provia", "qaub", "QRSTUVWXYZ", + "Reala", "skippable", "sonner", "thephotoblog", "trpc", "unnest", "UsKSGcbt", + "Velvia", "WRHGZC", "wxyz", "zadd", diff --git a/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx index a4ea30e5..2dc78856 100644 --- a/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx @@ -1,9 +1,14 @@ import PhotoForm from '@/photo/PhotoForm'; -import { ExifParserFactory } from 'ts-exif-parser'; +import { ExifData, ExifParserFactory } from 'ts-exif-parser'; import { convertExifToFormData } from '@/photo/form'; import AdminChildPage from '@/components/AdminChildPage'; import { getExtensionFromBlobUrl, getIdFromBlobUrl } from '@/services/blob'; import { PATH_ADMIN_UPLOADS } from '@/site/paths'; +import { + FujifilmSimulation, + getFujifilmSimulationFromMakerNote, + isExifForFujifilm, +} from '@/utility/fujifilm'; interface Params { params: { uploadPath: string } @@ -19,12 +24,27 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { .then(res => res.arrayBuffer()) : undefined; - let data; + let exifDataForm: ExifData | undefined; + let filmSimulation: FujifilmSimulation | undefined; if (fileBytes) { - data = ExifParserFactory - .create(Buffer.from(fileBytes)) - .parse(); + const parser = ExifParserFactory.create(Buffer.from(fileBytes)); + + // Data for form + parser.enableBinaryFields(false); + exifDataForm = parser.parse(); + + // Capture film simulation for Fujifilm cameras + if (isExifForFujifilm(exifDataForm)) { + // Parse exif data again with binary fields + // in order to access MakerNote tag + parser.enableBinaryFields(true); + const exifDataBinary = parser.parse(); + const makerNote = exifDataBinary.tags?.MakerNote; + if (Buffer.isBuffer(makerNote)) { + filmSimulation = getFujifilmSimulationFromMakerNote(makerNote); + } + } } return ( @@ -33,12 +53,12 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { backLabel="Uploads" breadcrumb={getIdFromBlobUrl(url)} > - {data + {exifDataForm ? : null} diff --git a/src/photo/PhotoForm.tsx b/src/photo/PhotoForm.tsx index b62d1c41..6f07a4ae 100644 --- a/src/photo/PhotoForm.tsx +++ b/src/photo/PhotoForm.tsx @@ -98,10 +98,14 @@ export default function PhotoForm({ required, readOnly, hideIfEmpty, + hideBasedOnCamera, loadingMessage, checkbox, }]) => - (!hideIfEmpty || formData[key]) && + ( + (!hideIfEmpty || formData[key]) && + !hideBasedOnCamera?.(formData.make) + ) && ; @@ -18,6 +19,7 @@ type FormMeta = { readOnly?: boolean hideIfEmpty?: boolean hideTemporarily?: boolean + hideBasedOnCamera?: (make?: string, mode?: string) => boolean loadingMessage?: string checkbox?: boolean }; @@ -33,6 +35,11 @@ const FORM_METADATA: Record = { aspectRatio: { label: 'aspect ratio', readOnly: true }, make: { label: 'camera make' }, model: { label: 'camera model' }, + filmSimulation: { + label: 'fujifilm simulation', + readOnly: true, + hideBasedOnCamera: make => make !== 'FUJIFILM', + }, focalLength: { label: 'focal length' }, focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' }, fNumber: { label: 'aperture' }, @@ -42,7 +49,6 @@ const FORM_METADATA: Record = { locationName: { label: 'location name', hideTemporarily: true }, latitude: { label: 'latitude' }, longitude: { label: 'longitude' }, - filmSimulation: { label: 'film simulation', hideTemporarily: true }, priorityOrder: { label: 'priority order' }, takenAt: { label: 'taken at' }, takenAtNaive: { label: 'taken at (naive)' }, @@ -77,7 +83,8 @@ export const convertPhotoToFormData = ( }; export const convertExifToFormData = ( - data: ExifData + data: ExifData, + fujifilmSimulation?: FujifilmSimulation, ): Record => ({ aspectRatio: ( (data.imageSize?.width ?? 3.0) / @@ -93,7 +100,7 @@ export const convertExifToFormData = ( exposureCompensation: data.tags?.ExposureCompensation?.toString(), latitude: data.tags?.GPSLatitude?.toString(), longitude: data.tags?.GPSLongitude?.toString(), - filmSimulation: undefined, + filmSimulation: fujifilmSimulation, takenAt: data.tags?.DateTimeOriginal ? convertTimestampWithOffsetToPostgresString( data.tags?.DateTimeOriginal, diff --git a/src/utility/fujifilm.ts b/src/utility/fujifilm.ts new file mode 100644 index 00000000..b02bdf0e --- /dev/null +++ b/src/utility/fujifilm.ts @@ -0,0 +1,120 @@ +// MakerNote tag IDs and values referenced from: +// exiftool/lib/Image/ExifTool/Fujifilm.pm + +import type { ExifData } from 'ts-exif-parser'; + +const BYTE_INDEX_FIRST_TAG = 14; +const BYTES_PER_TAG = 12; +const BYTE_OFFSET_FOR_INT_VALUE = 8; + +const TAG_ID_SATURATION = 0x1003; +const TAG_ID_FILM_MODE = 0x1401; + +type FujifilmSimulationFromSaturation = + 'Monochrome' | + 'Monochrome + Ye Filter' | + 'Monochrome + R Filter' | + 'Monochrome + G Filter' | + 'Sepia' | + 'Acros' | + 'Acros + Ye Filter' | + 'Acros + R Filter' | + 'Acros + G Filter'; + +type FujifilmMode = + 'Provia / Standard' | + 'Astia / Soft' | + 'Velvia / Vivid' | + 'Studio Portrait' | + 'Pro Neg. Std' | + 'Pro Neg. Hi' | + 'Classic Chrome' | + 'Eterna / Cinema' | + 'Classic Neg.' | + 'Eterna Bleach Bypass' | + 'Nostalgic Neg.' | + 'Reala Ace'; + +export type FujifilmSimulation = + FujifilmSimulationFromSaturation | + FujifilmMode; + +export const isExifForFujifilm = (data: ExifData) => + data.tags?.Make === 'FUJIFILM'; + +const parseFujifilmMakerNote = ( + bytes: Buffer, + valueForTag: (tag: number, value: number) => void +) => { + for ( + let i = BYTE_INDEX_FIRST_TAG; + i + BYTES_PER_TAG < bytes.length; + i += BYTES_PER_TAG + ) { + const tag = bytes.readUInt16LE(i); + const value = bytes.readUInt16LE(i + BYTE_OFFSET_FOR_INT_VALUE); + valueForTag(tag, value); + } +}; + +const getFujifilmSimulationFromSaturation = ( + value?: number, +): FujifilmSimulationFromSaturation | undefined => { + switch (value) { + case 0x300: return 'Monochrome'; + case 0x301: return 'Monochrome + R Filter'; + case 0x302: return 'Monochrome + Ye Filter'; + case 0x303: return 'Monochrome + G Filter'; + case 0x310: return 'Sepia'; + case 0x500: return 'Acros'; + case 0x501: return 'Acros + R Filter'; + case 0x502: return 'Acros + Ye Filter'; + case 0x503: return 'Acros + G Filter'; + } +}; + +const getFujifilmMode = ( + value?: number, +): FujifilmMode | undefined => { + switch (value) { + case 0x000: return 'Provia / Standard'; + case 0x100: + case 0x110: + case 0x120: + case 0x130: return 'Astia / Soft'; + case 0x200: + case 0x400: return 'Velvia / Vivid'; + case 0x300: return 'Studio Portrait'; + case 0x500: return 'Pro Neg. Std'; + case 0x501: return 'Pro Neg. Hi'; + case 0x600: return 'Classic Chrome'; + case 0x700: return 'Eterna / Cinema'; + case 0x800: return 'Classic Neg.'; + case 0x900: return 'Eterna Bleach Bypass'; + case 0xa00: return 'Nostalgic Neg.'; + case 0xb00: return 'Reala Ace'; + } +}; + +export const getFujifilmSimulationFromMakerNote = ( + bytes: Buffer, +): FujifilmSimulation | undefined => { + let filmModeFromSaturation: FujifilmSimulationFromSaturation | undefined; + let filmMode: FujifilmMode | undefined; + + parseFujifilmMakerNote( + bytes, + (tag, value) => { + switch (tag) { + case TAG_ID_SATURATION: + filmModeFromSaturation = getFujifilmSimulationFromSaturation(value); + break; + case TAG_ID_FILM_MODE: + filmMode = getFujifilmMode(value); + break; + } + }, + ); + + return filmModeFromSaturation ?? filmMode; +}; From a3f13ce5ccd057dc3cb8811df5d4da89dc9bf341 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 25 Oct 2023 12:49:16 -0500 Subject: [PATCH 02/13] Update film simulation capitalization --- src/utility/fujifilm.ts | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/utility/fujifilm.ts b/src/utility/fujifilm.ts index b02bdf0e..dd605c67 100644 --- a/src/utility/fujifilm.ts +++ b/src/utility/fujifilm.ts @@ -16,24 +16,23 @@ type FujifilmSimulationFromSaturation = 'Monochrome + R Filter' | 'Monochrome + G Filter' | 'Sepia' | - 'Acros' | - 'Acros + Ye Filter' | - 'Acros + R Filter' | - 'Acros + G Filter'; + 'ACROS' | + 'ACROS + Ye Filter' | + 'ACROS + R Filter' | + 'ACROS + G Filter'; type FujifilmMode = 'Provia / Standard' | - 'Astia / Soft' | + 'ASTIA / Soft' | 'Velvia / Vivid' | - 'Studio Portrait' | 'Pro Neg. Std' | 'Pro Neg. Hi' | 'Classic Chrome' | - 'Eterna / Cinema' | + 'ETERNA / Cinema' | 'Classic Neg.' | - 'Eterna Bleach Bypass' | + 'ETERNA Bleach Bypass' | 'Nostalgic Neg.' | - 'Reala Ace'; + 'REALA ACE'; export type FujifilmSimulation = FujifilmSimulationFromSaturation | @@ -66,10 +65,10 @@ const getFujifilmSimulationFromSaturation = ( case 0x302: return 'Monochrome + Ye Filter'; case 0x303: return 'Monochrome + G Filter'; case 0x310: return 'Sepia'; - case 0x500: return 'Acros'; - case 0x501: return 'Acros + R Filter'; - case 0x502: return 'Acros + Ye Filter'; - case 0x503: return 'Acros + G Filter'; + case 0x500: return 'ACROS'; + case 0x501: return 'ACROS + R Filter'; + case 0x502: return 'ACROS + Ye Filter'; + case 0x503: return 'ACROS + G Filter'; } }; @@ -81,18 +80,17 @@ const getFujifilmMode = ( case 0x100: case 0x110: case 0x120: - case 0x130: return 'Astia / Soft'; + case 0x130: return 'ASTIA / Soft'; case 0x200: case 0x400: return 'Velvia / Vivid'; - case 0x300: return 'Studio Portrait'; case 0x500: return 'Pro Neg. Std'; case 0x501: return 'Pro Neg. Hi'; case 0x600: return 'Classic Chrome'; - case 0x700: return 'Eterna / Cinema'; + case 0x700: return 'ETERNA / Cinema'; case 0x800: return 'Classic Neg.'; - case 0x900: return 'Eterna Bleach Bypass'; + case 0x900: return 'ETERNA Bleach Bypass'; case 0xa00: return 'Nostalgic Neg.'; - case 0xb00: return 'Reala Ace'; + case 0xb00: return 'REALA ACE'; } }; From 07ec113542266d76ddf855add720cdeaff250c81 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 25 Oct 2023 12:50:37 -0500 Subject: [PATCH 03/13] Use ASTIA for 0x300 (previously Studio Portrait) --- src/utility/fujifilm.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utility/fujifilm.ts b/src/utility/fujifilm.ts index dd605c67..e4be7ed4 100644 --- a/src/utility/fujifilm.ts +++ b/src/utility/fujifilm.ts @@ -80,7 +80,8 @@ const getFujifilmMode = ( case 0x100: case 0x110: case 0x120: - case 0x130: return 'ASTIA / Soft'; + case 0x130: + case 0x300: return 'ASTIA / Soft'; case 0x200: case 0x400: return 'Velvia / Vivid'; case 0x500: return 'Pro Neg. Std'; From 7509b96c20fa4662f9d87665ea54697846451dfa Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 25 Oct 2023 13:25:22 -0500 Subject: [PATCH 04/13] Make Fujifilm simulations more robust --- src/utility/fujifilm.ts | 100 +++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/src/utility/fujifilm.ts b/src/utility/fujifilm.ts index e4be7ed4..f88f55c1 100644 --- a/src/utility/fujifilm.ts +++ b/src/utility/fujifilm.ts @@ -3,6 +3,8 @@ import type { ExifData } from 'ts-exif-parser'; +const MAKE_FUJIFILM = 'FUJIFILM'; + const BYTE_INDEX_FIRST_TAG = 14; const BYTES_PER_TAG = 12; const BYTE_OFFSET_FOR_INT_VALUE = 8; @@ -12,34 +14,38 @@ const TAG_ID_FILM_MODE = 0x1401; type FujifilmSimulationFromSaturation = 'Monochrome' | - 'Monochrome + Ye Filter' | - 'Monochrome + R Filter' | - 'Monochrome + G Filter' | + 'Monochrome + Ye' | + 'Monochrome + R' | + 'Monochrome + G' | 'Sepia' | - 'ACROS' | - 'ACROS + Ye Filter' | - 'ACROS + R Filter' | - 'ACROS + G Filter'; + 'Acros' | + 'Acros + Ye' | + 'Acros + R' | + 'Acros + G'; type FujifilmMode = - 'Provia / Standard' | - 'ASTIA / Soft' | - 'Velvia / Vivid' | + 'Provia' | + 'Portrait' | + 'Portrait Saturation' | + 'Portrait Skin Tone' | + 'Portrait Sharpness' | + 'Portrait Ex' | + 'Velvia' | 'Pro Neg. Std' | 'Pro Neg. Hi' | 'Classic Chrome' | - 'ETERNA / Cinema' | + 'Eterna' | 'Classic Neg.' | - 'ETERNA Bleach Bypass' | + 'Eterna Bleach Bypass' | 'Nostalgic Neg.' | - 'REALA ACE'; + 'Reala'; export type FujifilmSimulation = FujifilmSimulationFromSaturation | FujifilmMode; export const isExifForFujifilm = (data: ExifData) => - data.tags?.Make === 'FUJIFILM'; + data.tags?.Make === MAKE_FUJIFILM; const parseFujifilmMakerNote = ( bytes: Buffer, @@ -61,14 +67,14 @@ const getFujifilmSimulationFromSaturation = ( ): FujifilmSimulationFromSaturation | undefined => { switch (value) { case 0x300: return 'Monochrome'; - case 0x301: return 'Monochrome + R Filter'; - case 0x302: return 'Monochrome + Ye Filter'; - case 0x303: return 'Monochrome + G Filter'; + case 0x301: return 'Monochrome + R'; + case 0x302: return 'Monochrome + Ye'; + case 0x303: return 'Monochrome + G'; case 0x310: return 'Sepia'; - case 0x500: return 'ACROS'; - case 0x501: return 'ACROS + R Filter'; - case 0x502: return 'ACROS + Ye Filter'; - case 0x503: return 'ACROS + G Filter'; + case 0x500: return 'Acros'; + case 0x501: return 'Acros + R'; + case 0x502: return 'Acros + Ye'; + case 0x503: return 'Acros + G'; } }; @@ -76,25 +82,57 @@ const getFujifilmMode = ( value?: number, ): FujifilmMode | undefined => { switch (value) { - case 0x000: return 'Provia / Standard'; - case 0x100: - case 0x110: - case 0x120: - case 0x130: - case 0x300: return 'ASTIA / Soft'; + case 0x000: return 'Provia'; + case 0x100: return 'Portrait'; + case 0x110: return 'Portrait Saturation'; + case 0x120: return 'Portrait Skin Tone'; + case 0x130: return 'Portrait Sharpness'; + case 0x300: return 'Portrait Ex'; case 0x200: - case 0x400: return 'Velvia / Vivid'; + case 0x400: return 'Velvia'; case 0x500: return 'Pro Neg. Std'; case 0x501: return 'Pro Neg. Hi'; case 0x600: return 'Classic Chrome'; - case 0x700: return 'ETERNA / Cinema'; + case 0x700: return 'Eterna'; case 0x800: return 'Classic Neg.'; - case 0x900: return 'ETERNA Bleach Bypass'; + case 0x900: return 'Eterna Bleach Bypass'; case 0xa00: return 'Nostalgic Neg.'; - case 0xb00: return 'REALA ACE'; + case 0xb00: return 'Reala'; } }; +const LABEL_FOR_FILM_SIMULATION: Record = { + 'Monochrome': 'Monochrome', + 'Monochrome + Ye': 'Monochrome + Yellow Filter', + 'Monochrome + R': 'Monochrome + Red Filter', + 'Monochrome + G': 'Monochrome + Green Filter', + 'Sepia': 'Sepia', + 'Acros': 'ACROS', + 'Acros + Ye': 'ACROS + Yellow Filter', + 'Acros + R': 'ACROS + Red Filter', + 'Acros + G': 'ACROS + Green Filter', + 'Provia': 'PROVIA / Standard', + 'Portrait': 'Studio Portrait', + 'Portrait Saturation': 'Studio Portrait + Enhanced Saturation', + 'Portrait Skin Tone': 'ASTIA / Soft', + 'Portrait Sharpness': 'Studio Portrait + Enhanced Sharpness', + 'Portrait Ex': 'Studio Portrait Ex', + 'Velvia': 'Velvia / Vivid', + 'Pro Neg. Std': 'PRO Neg. Std', + 'Pro Neg. Hi': 'PRO Neg. Hi', + 'Classic Chrome': 'Classic Chrome', + 'Eterna': 'ETERNA / Cinema', + 'Classic Neg.': 'Classic Neg.', + 'Eterna Bleach Bypass': 'ETERNA Bleach Bypass', + 'Nostalgic Neg.': 'Nostalgic Neg.', + 'Reala': 'REALA ACE', +}; + +export const getLabelForFilmSimulation = ( + simulation: FujifilmSimulation +): string => + LABEL_FOR_FILM_SIMULATION[simulation]; + export const getFujifilmSimulationFromMakerNote = ( bytes: Buffer, ): FujifilmSimulation | undefined => { From fdc35beff1ccc416707097f948ce926ab48ca84f Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 27 Oct 2023 15:00:37 -0700 Subject: [PATCH 05/13] Add placeholder film simulation icon --- .../admin/uploads/[uploadPath]/page.tsx | 2 +- src/photo/PhotoLarge.tsx | 16 ++-- src/photo/form.ts | 2 +- .../fujifilm/PhotoFujifilmSimulation.tsx | 77 +++++++++++++++++++ .../fujifilm.ts => vendors/fujifilm/index.ts} | 0 5 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 src/vendors/fujifilm/PhotoFujifilmSimulation.tsx rename src/{utility/fujifilm.ts => vendors/fujifilm/index.ts} (100%) diff --git a/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx index 2dc78856..a908701f 100644 --- a/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx @@ -8,7 +8,7 @@ import { FujifilmSimulation, getFujifilmSimulationFromMakerNote, isExifForFujifilm, -} from '@/utility/fujifilm'; +} from '@/vendors/fujifilm'; interface Params { params: { uploadPath: string } diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index ce16d094..4847acc0 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -8,6 +8,8 @@ import PhotoTags from '@/tag/PhotoTags'; import ShareButton from '@/components/ShareButton'; import PhotoCamera from '../camera/PhotoCamera'; import { Camera, cameraFromPhoto } from '@/camera'; +import PhotoFujifilmSimulation from + '@/vendors/fujifilm/PhotoFujifilmSimulation'; export default function PhotoLarge({ photo, @@ -73,11 +75,15 @@ export default function PhotoLarge({ } {showCamera && photoHasCameraData(photo) && - } +
+ +   + +
} )} {renderMiniGrid(<> {photoHasExifData(photo) && diff --git a/src/photo/form.ts b/src/photo/form.ts index 012e2249..0e055799 100644 --- a/src/photo/form.ts +++ b/src/photo/form.ts @@ -8,7 +8,7 @@ import { getOffsetFromExif } from '@/utility/exif'; import { toFixedNumber } from '@/utility/number'; import { convertStringToArray } from '@/utility/string'; import { generateNanoid } from '@/utility/nanoid'; -import { FujifilmSimulation } from '@/utility/fujifilm'; +import { FujifilmSimulation } from '@/vendors/fujifilm'; export type PhotoFormData = Record; diff --git a/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx b/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx new file mode 100644 index 00000000..d075ff0f --- /dev/null +++ b/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx @@ -0,0 +1,77 @@ +/* eslint-disable max-len */ +import { FujifilmSimulation } from '@/vendors/fujifilm'; + +export default function PhotoFilmSimulation({ + simulation: _simulation, +}: { + simulation: FujifilmSimulation; +}) { + return ( + + + + + + + + + + + + + ); +} diff --git a/src/utility/fujifilm.ts b/src/vendors/fujifilm/index.ts similarity index 100% rename from src/utility/fujifilm.ts rename to src/vendors/fujifilm/index.ts From 00bffcf4fcf055dbb3c2dc146daa4b704bc482d7 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 27 Oct 2023 20:48:02 -0700 Subject: [PATCH 06/13] Introduce fujifilm simulations dropdown --- src/components/FieldSetWithStatus.tsx | 56 ++++++-- src/photo/PhotoForm.tsx | 4 + src/photo/form.ts | 18 ++- src/site/globals.css | 18 ++- src/vendors/fujifilm/index.ts | 199 +++++++++++++------------- 5 files changed, 176 insertions(+), 119 deletions(-) diff --git a/src/components/FieldSetWithStatus.tsx b/src/components/FieldSetWithStatus.tsx index affe5150..c518e37d 100644 --- a/src/components/FieldSetWithStatus.tsx +++ b/src/components/FieldSetWithStatus.tsx @@ -11,6 +11,8 @@ export default function FieldSetWithStatus({ note, value, onChange, + selectOptions, + selectOptionsDefaultLabel, placeholder, loading, required, @@ -23,6 +25,8 @@ export default function FieldSetWithStatus({ note?: string value: string onChange?: (value: string) => void + selectOptions?: { value: string, label: string }[] + selectOptionsDefaultLabel?: string placeholder?: string loading?: boolean required?: boolean @@ -52,21 +56,43 @@ export default function FieldSetWithStatus({ } - onChange?.(type === 'checkbox' - ? e.target.value === 'true' ? 'false' : 'true' - : e.target.value)} - type={type} - autoComplete="off" - readOnly={readOnly || pending} - className={cc(type === 'text' && 'w-full')} - /> + {selectOptions + ? + : onChange?.(type === 'checkbox' + ? e.target.value === 'true' ? 'false' : 'true' + : e.target.value)} + type={type} + autoComplete="off" + readOnly={readOnly || pending} + className={cc(type === 'text' && 'w-full')} + />} ); }; diff --git a/src/photo/PhotoForm.tsx b/src/photo/PhotoForm.tsx index 6f07a4ae..9f680de0 100644 --- a/src/photo/PhotoForm.tsx +++ b/src/photo/PhotoForm.tsx @@ -96,6 +96,8 @@ export default function PhotoForm({ label, note, required, + options, + optionsDefaultLabel, readOnly, hideIfEmpty, hideBasedOnCamera, @@ -113,6 +115,8 @@ export default function PhotoForm({ note={note} value={formData[key] ?? ''} onChange={value => setFormData({ ...formData, [key]: value })} + selectOptions={options} + selectOptionsDefaultLabel={optionsDefaultLabel} required={required} readOnly={readOnly} placeholder={loadingMessage && !formData[key] diff --git a/src/photo/form.ts b/src/photo/form.ts index 0e055799..a8c8b140 100644 --- a/src/photo/form.ts +++ b/src/photo/form.ts @@ -8,7 +8,10 @@ import { getOffsetFromExif } from '@/utility/exif'; import { toFixedNumber } from '@/utility/number'; import { convertStringToArray } from '@/utility/string'; import { generateNanoid } from '@/utility/nanoid'; -import { FujifilmSimulation } from '@/vendors/fujifilm'; +import { + FILM_SIMULATION_FORM_INPUT_OPTIONS, + FujifilmSimulation, +} from '@/vendors/fujifilm'; export type PhotoFormData = Record; @@ -22,14 +25,20 @@ type FormMeta = { hideBasedOnCamera?: (make?: string, mode?: string) => boolean loadingMessage?: string checkbox?: boolean + options?: { value: string, label: string }[] + optionsDefaultLabel?: string }; const FORM_METADATA: Record = { title: { label: 'title' }, tags: { label: 'tags', note: 'comma-separated values' }, id: { label: 'id', readOnly: true, hideIfEmpty: true }, - // eslint-disable-next-line max-len - blurData: { label: 'blur data', readOnly: true, required: true, loadingMessage: 'Generating blur data ...' }, + blurData: { + label: 'blur data', + readOnly: true, + required: true, + loadingMessage: 'Generating blur data ...', + }, url: { label: 'url', readOnly: true }, extension: { label: 'extension', readOnly: true }, aspectRatio: { label: 'aspect ratio', readOnly: true }, @@ -37,7 +46,8 @@ const FORM_METADATA: Record = { model: { label: 'camera model' }, filmSimulation: { label: 'fujifilm simulation', - readOnly: true, + options: FILM_SIMULATION_FORM_INPUT_OPTIONS, + optionsDefaultLabel: 'Unknown', hideBasedOnCamera: make => make !== 'FUJIFILM', }, focalLength: { label: 'focal length' }, diff --git a/src/site/globals.css b/src/site/globals.css index 5cfbf843..5118a39d 100644 --- a/src/site/globals.css +++ b/src/site/globals.css @@ -18,22 +18,32 @@ tracking-wider } button, .button, - input[type=text], input[type=email], input[type=password] { + input[type=text], input[type=email], input[type=password], select { @apply - px-2 py-1.5 + px-2.5 py-2 border rounded-md bg-white dark:bg-black border-gray-200 dark:border-gray-700 - font-mono text-base leading-none + font-mono text-base leading-tight min-h-[2.25rem] } - input[type=text], input[type=email], input[type=password] { + input[type=text], input[type=email], input[type=password], select { @apply text-[1rem] /* Prevent iOS auto-zoom behavior */ min-w-[20rem] read-only:cursor-default + } + input[type=text], input[type=email], input[type=password] { + @apply read-only:bg-gray-100 dark:read-only:bg-gray-900 dark:read-only:text-gray-400 } + /* Required for readonly behavior on