Introduce fujifilm simulations dropdown

This commit is contained in:
Sam Becker 2023-10-27 20:48:02 -07:00
parent fdc35beff1
commit 00bffcf4fc
5 changed files with 176 additions and 119 deletions

View File

@ -11,6 +11,8 @@ export default function FieldSetWithStatus({
note, note,
value, value,
onChange, onChange,
selectOptions,
selectOptionsDefaultLabel,
placeholder, placeholder,
loading, loading,
required, required,
@ -23,6 +25,8 @@ export default function FieldSetWithStatus({
note?: string note?: string
value: string value: string
onChange?: (value: string) => void onChange?: (value: string) => void
selectOptions?: { value: string, label: string }[]
selectOptionsDefaultLabel?: string
placeholder?: string placeholder?: string
loading?: boolean loading?: boolean
required?: boolean required?: boolean
@ -52,21 +56,43 @@ export default function FieldSetWithStatus({
<Spinner /> <Spinner />
</span>} </span>}
</label> </label>
<input {selectOptions
ref={inputRef} ? <select
id={id} id={id}
name={id} name={id}
value={value} value={value}
checked={type === 'checkbox' ? value === 'true' : undefined} onChange={e => onChange?.(e.target.value)}
placeholder={placeholder} className={cc(
onChange={e => onChange?.(type === 'checkbox' 'w-full',
? e.target.value === 'true' ? 'false' : 'true' // Use special class because `select` can't be readonly
: e.target.value)} readOnly || pending && 'disabled-select',
type={type} )}
autoComplete="off" >
readOnly={readOnly || pending} {selectOptionsDefaultLabel &&
className={cc(type === 'text' && 'w-full')} <option value="">{selectOptionsDefaultLabel}</option>}
/> {selectOptions.map(({ value: optionValue, label: optionLabel }) =>
<option
key={optionValue}
value={optionValue}
>
{optionLabel}
</option>)}
</select>
: <input
ref={inputRef}
id={id}
name={id}
value={value}
checked={type === 'checkbox' ? value === 'true' : undefined}
placeholder={placeholder}
onChange={e => 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')}
/>}
</div> </div>
); );
}; };

View File

@ -96,6 +96,8 @@ export default function PhotoForm({
label, label,
note, note,
required, required,
options,
optionsDefaultLabel,
readOnly, readOnly,
hideIfEmpty, hideIfEmpty,
hideBasedOnCamera, hideBasedOnCamera,
@ -113,6 +115,8 @@ export default function PhotoForm({
note={note} note={note}
value={formData[key] ?? ''} value={formData[key] ?? ''}
onChange={value => setFormData({ ...formData, [key]: value })} onChange={value => setFormData({ ...formData, [key]: value })}
selectOptions={options}
selectOptionsDefaultLabel={optionsDefaultLabel}
required={required} required={required}
readOnly={readOnly} readOnly={readOnly}
placeholder={loadingMessage && !formData[key] placeholder={loadingMessage && !formData[key]

View File

@ -8,7 +8,10 @@ import { getOffsetFromExif } from '@/utility/exif';
import { toFixedNumber } from '@/utility/number'; import { toFixedNumber } from '@/utility/number';
import { convertStringToArray } from '@/utility/string'; import { convertStringToArray } from '@/utility/string';
import { generateNanoid } from '@/utility/nanoid'; import { generateNanoid } from '@/utility/nanoid';
import { FujifilmSimulation } from '@/vendors/fujifilm'; import {
FILM_SIMULATION_FORM_INPUT_OPTIONS,
FujifilmSimulation,
} from '@/vendors/fujifilm';
export type PhotoFormData = Record<keyof PhotoDbInsert, string>; export type PhotoFormData = Record<keyof PhotoDbInsert, string>;
@ -22,14 +25,20 @@ type FormMeta = {
hideBasedOnCamera?: (make?: string, mode?: string) => boolean hideBasedOnCamera?: (make?: string, mode?: string) => boolean
loadingMessage?: string loadingMessage?: string
checkbox?: boolean checkbox?: boolean
options?: { value: string, label: string }[]
optionsDefaultLabel?: string
}; };
const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = { const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = {
title: { label: 'title' }, title: { label: 'title' },
tags: { label: 'tags', note: 'comma-separated values' }, tags: { label: 'tags', note: 'comma-separated values' },
id: { label: 'id', readOnly: true, hideIfEmpty: true }, id: { label: 'id', readOnly: true, hideIfEmpty: true },
// eslint-disable-next-line max-len blurData: {
blurData: { label: 'blur data', readOnly: true, required: true, loadingMessage: 'Generating blur data ...' }, label: 'blur data',
readOnly: true,
required: true,
loadingMessage: 'Generating blur data ...',
},
url: { label: 'url', readOnly: true }, url: { label: 'url', readOnly: true },
extension: { label: 'extension', readOnly: true }, extension: { label: 'extension', readOnly: true },
aspectRatio: { label: 'aspect ratio', readOnly: true }, aspectRatio: { label: 'aspect ratio', readOnly: true },
@ -37,7 +46,8 @@ const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = {
model: { label: 'camera model' }, model: { label: 'camera model' },
filmSimulation: { filmSimulation: {
label: 'fujifilm simulation', label: 'fujifilm simulation',
readOnly: true, options: FILM_SIMULATION_FORM_INPUT_OPTIONS,
optionsDefaultLabel: 'Unknown',
hideBasedOnCamera: make => make !== 'FUJIFILM', hideBasedOnCamera: make => make !== 'FUJIFILM',
}, },
focalLength: { label: 'focal length' }, focalLength: { label: 'focal length' },

View File

@ -18,22 +18,32 @@
tracking-wider tracking-wider
} }
button, .button, button, .button,
input[type=text], input[type=email], input[type=password] { input[type=text], input[type=email], input[type=password], select {
@apply @apply
px-2 py-1.5 px-2.5 py-2
border rounded-md border rounded-md
bg-white dark:bg-black bg-white dark:bg-black
border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700
font-mono text-base leading-none font-mono text-base leading-tight
min-h-[2.25rem] min-h-[2.25rem]
} }
input[type=text], input[type=email], input[type=password] { input[type=text], input[type=email], input[type=password], select {
@apply @apply
text-[1rem] /* Prevent iOS auto-zoom behavior */ text-[1rem] /* Prevent iOS auto-zoom behavior */
min-w-[20rem] read-only:cursor-default min-w-[20rem] read-only:cursor-default
}
input[type=text], input[type=email], input[type=password] {
@apply
read-only:bg-gray-100 read-only:bg-gray-100
dark:read-only:bg-gray-900 dark:read-only:text-gray-400 dark:read-only:bg-gray-900 dark:read-only:text-gray-400
} }
/* Required for readonly behavior on <select /> */
.disabled-select {
@apply
bg-gray-100
dark:bg-gray-900 dark:text-gray-400
pointer-events-none
}
input[type=file] { input[type=file] {
@apply @apply
block font-mono w-full text-gray-500 dark:text-gray-400 block font-mono w-full text-gray-500 dark:text-gray-400

View File

@ -1,5 +1,5 @@
// MakerNote tag IDs and values referenced from: // 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'; import type { ExifData } from 'ts-exif-parser';
@ -13,32 +13,32 @@ const TAG_ID_SATURATION = 0x1003;
const TAG_ID_FILM_MODE = 0x1401; const TAG_ID_FILM_MODE = 0x1401;
type FujifilmSimulationFromSaturation = type FujifilmSimulationFromSaturation =
'Monochrome' | 'monochrome' |
'Monochrome + Ye' | 'monochrome-ye' |
'Monochrome + R' | 'monochrome-r' |
'Monochrome + G' | 'monochrome-g' |
'Sepia' | 'sepia' |
'Acros' | 'acros' |
'Acros + Ye' | 'acros-ye' |
'Acros + R' | 'acros-r' |
'Acros + G'; 'acros-g';
type FujifilmMode = type FujifilmMode =
'Provia' | 'provia' |
'Portrait' | 'portrait' |
'Portrait Saturation' | 'portrait-saturation' |
'Portrait Skin Tone' | 'portrait-skin-tone' |
'Portrait Sharpness' | 'portrait-sharpness' |
'Portrait Ex' | 'portrait-ex' |
'Velvia' | 'velvia' |
'Pro Neg. Std' | 'pro-neg-std' |
'Pro Neg. Hi' | 'pro-neg-hi' |
'Classic Chrome' | 'classic-chrome' |
'Eterna' | 'eterna' |
'Classic Neg.' | 'classic-neg' |
'Eterna Bleach Bypass' | 'eterna-bleach-bypass' |
'Nostalgic Neg.' | 'nostalgic-neg' |
'Reala'; 'reala';
export type FujifilmSimulation = export type FujifilmSimulation =
FujifilmSimulationFromSaturation | FujifilmSimulationFromSaturation |
@ -47,6 +47,84 @@ export type FujifilmSimulation =
export const isExifForFujifilm = (data: ExifData) => export const isExifForFujifilm = (data: ExifData) =>
data.tags?.Make === MAKE_FUJIFILM; 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<FujifilmSimulation, string> = {
'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 = ( const parseFujifilmMakerNote = (
bytes: Buffer, bytes: Buffer,
valueForTag: (tag: number, value: number) => void 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<FujifilmSimulation, string> = {
'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 = ( export const getFujifilmSimulationFromMakerNote = (
bytes: Buffer, bytes: Buffer,
): FujifilmSimulation | undefined => { ): FujifilmSimulation | undefined => {