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 */
+ .disabled-select {
+ @apply
+ bg-gray-100
+ dark:bg-gray-900 dark:text-gray-400
+ pointer-events-none
+ }
input[type=file] {
@apply
block font-mono w-full text-gray-500 dark:text-gray-400
diff --git a/src/vendors/fujifilm/index.ts b/src/vendors/fujifilm/index.ts
index f88f55c1..650c4c58 100644
--- a/src/vendors/fujifilm/index.ts
+++ b/src/vendors/fujifilm/index.ts
@@ -1,5 +1,5 @@
// MakerNote tag IDs and values referenced from:
-// exiftool/lib/Image/ExifTool/Fujifilm.pm
+// github.com/exiftool/exiftool/lib/Image/ExifTool/Fujifilm.pm
import type { ExifData } from 'ts-exif-parser';
@@ -13,32 +13,32 @@ const TAG_ID_SATURATION = 0x1003;
const TAG_ID_FILM_MODE = 0x1401;
type FujifilmSimulationFromSaturation =
- 'Monochrome' |
- 'Monochrome + Ye' |
- 'Monochrome + R' |
- 'Monochrome + G' |
- 'Sepia' |
- 'Acros' |
- 'Acros + Ye' |
- 'Acros + R' |
- 'Acros + G';
+ 'monochrome' |
+ 'monochrome-ye' |
+ 'monochrome-r' |
+ 'monochrome-g' |
+ 'sepia' |
+ 'acros' |
+ 'acros-ye' |
+ 'acros-r' |
+ 'acros-g';
type FujifilmMode =
- 'Provia' |
- 'Portrait' |
- 'Portrait Saturation' |
- 'Portrait Skin Tone' |
- 'Portrait Sharpness' |
- 'Portrait Ex' |
- 'Velvia' |
- 'Pro Neg. Std' |
- 'Pro Neg. Hi' |
- 'Classic Chrome' |
- 'Eterna' |
- 'Classic Neg.' |
- 'Eterna Bleach Bypass' |
- 'Nostalgic Neg.' |
- 'Reala';
+ 'provia' |
+ 'portrait' |
+ 'portrait-saturation' |
+ 'portrait-skin-tone' |
+ 'portrait-sharpness' |
+ 'portrait-ex' |
+ 'velvia' |
+ 'pro-neg-std' |
+ 'pro-neg-hi' |
+ 'classic-chrome' |
+ 'eterna' |
+ 'classic-neg' |
+ 'eterna-bleach-bypass' |
+ 'nostalgic-neg' |
+ 'reala';
export type FujifilmSimulation =
FujifilmSimulationFromSaturation |
@@ -47,6 +47,84 @@ export type FujifilmSimulation =
export const isExifForFujifilm = (data: ExifData) =>
data.tags?.Make === MAKE_FUJIFILM;
+const getFujifilmSimulationFromSaturation = (
+ value?: number,
+): FujifilmSimulationFromSaturation | undefined => {
+ switch (value) {
+ case 0x300: return 'monochrome';
+ 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';
+ case 0x502: return 'acros-ye';
+ case 0x503: return 'acros-g';
+ }
+};
+
+const getFujifilmMode = (
+ value?: number,
+): FujifilmMode | undefined => {
+ switch (value) {
+ 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';
+ case 0x500: return 'pro-neg-std';
+ case 0x501: return 'pro-neg-hi';
+ case 0x600: return 'classic-chrome';
+ case 0x700: return 'eterna';
+ case 0x800: return 'classic-neg';
+ case 0x900: return 'eterna-bleach-bypass';
+ case 0xa00: return 'nostalgic-neg';
+ case 0xb00: return 'reala';
+ }
+};
+
+const FILM_SIMULATION_LABELS: 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 FILM_SIMULATION_FORM_INPUT_OPTIONS = Object
+ .entries(FILM_SIMULATION_LABELS)
+ .map(([value, label]) => (
+ { value, label } as { value: FujifilmSimulation, label: string }
+ ))
+ .sort((a, b) => a.label.localeCompare(b.label));
+
+export const getLabelForFilmSimulation = (
+ simulation: FujifilmSimulation
+): string =>
+ FILM_SIMULATION_LABELS[simulation];
+
const parseFujifilmMakerNote = (
bytes: Buffer,
valueForTag: (tag: number, value: number) => void
@@ -62,77 +140,6 @@ const parseFujifilmMakerNote = (
}
};
-const getFujifilmSimulationFromSaturation = (
- value?: number,
-): FujifilmSimulationFromSaturation | undefined => {
- switch (value) {
- case 0x300: return 'Monochrome';
- 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';
- case 0x502: return 'Acros + Ye';
- case 0x503: return 'Acros + G';
- }
-};
-
-const getFujifilmMode = (
- value?: number,
-): FujifilmMode | undefined => {
- switch (value) {
- 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';
- case 0x500: return 'Pro Neg. Std';
- case 0x501: return 'Pro Neg. Hi';
- case 0x600: return 'Classic Chrome';
- case 0x700: return 'Eterna';
- case 0x800: return 'Classic Neg.';
- case 0x900: return 'Eterna Bleach Bypass';
- case 0xa00: return 'Nostalgic Neg.';
- 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 => {