Introduce fujifilm simulations dropdown
This commit is contained in:
parent
fdc35beff1
commit
00bffcf4fc
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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' },
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
199
src/vendors/fujifilm/index.ts
vendored
199
src/vendors/fujifilm/index.ts
vendored
@ -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 => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user