diff --git a/frontend/src/components/MetadataSettingsPanel.tsx b/frontend/src/components/MetadataSettingsPanel.tsx index 463a92d..3ab018d 100644 --- a/frontend/src/components/MetadataSettingsPanel.tsx +++ b/frontend/src/components/MetadataSettingsPanel.tsx @@ -17,7 +17,7 @@ const formatFocalLength = (val: string): string => { if (/^\d+(\.\d+)?(-\d+(\.\d+)?)?$/.test(trimmed)) { return `${trimmed}mm`; } - return val; + return trimmed; }; const formatAperture = (val: string): string => { @@ -26,7 +26,7 @@ const formatAperture = (val: string): string => { if (match) { return `f/${match[2]}`; } - return val; + return trimmed; }; const formatShutterSpeed = (val: string): string => { @@ -34,7 +34,7 @@ const formatShutterSpeed = (val: string): string => { if (/^\d+(\/\d+)?$/.test(trimmed) || /^\d+\.\d+$/.test(trimmed)) { return `${trimmed}s`; } - return val; + return trimmed; }; const formatISO = (val: string): string => { @@ -43,7 +43,7 @@ const formatISO = (val: string): string => { if (match) { return `ISO${match[2]}`; } - return val; + return trimmed; }; const formatTemp = (val: string): string => { @@ -51,7 +51,7 @@ const formatTemp = (val: string): string => { if (/^-?\d+(\.\d+)?$/.test(trimmed)) { return `${trimmed}℃`; } - return val; + return trimmed; }; const formatTime = (val: string): string => { @@ -59,7 +59,7 @@ const formatTime = (val: string): string => { if (/^\d+(\.\d+)?$/.test(trimmed)) { return `${trimmed}min`; } - return val; + return trimmed; }; export interface MetadataSettingsPanelProps { @@ -95,23 +95,58 @@ export const MetadataSettingsPanel = ({ // Compute suggestion lists based on current inputs const { availableFilms, availableDevelopers, availableDilutions, availableTemps, availableTimes } = useMemo(() => { + const parseValue = (val: string): number => { + if (val.toLowerCase() === 'stock') return 0; + // Handle dilutions like "1+4" or "1:4" -> returns 4 + const dilutionMatch = val.match(/^\d+[+:](\d+(\.\d+)?)$/); + if (dilutionMatch) return parseFloat(dilutionMatch[1]); + // Handle time like "6:45" -> 6.75 + const timeMatch = val.match(/^(\d+):(\d{2})$/); + if (timeMatch) return parseInt(timeMatch[1], 10) + parseInt(timeMatch[2], 10) / 60; + return parseFloat(val); + }; + + const customSort = (a: string, b: string) => { + const numA = parseValue(a); + const numB = parseValue(b); + const aIsNum = !isNaN(numA); + const bIsNum = !isNaN(numB); + + if (aIsNum && bIsNum) { + if (numA !== numB) return numA - numB; + } else if (aIsNum && !bIsNum) { + return -1; // Numbers come before strings + } else if (!aIsNum && bIsNum) { + return 1; // Strings come after numbers + } + return a.localeCompare(b); + }; + const availableFilms = Array.from(new Set(recipes.map(r => r.film))).filter(Boolean).sort(); - // Filter recipes matching the selected film - const matchingRecipes = recipes.filter(r => !exif.film || r.film === exif.film); + // Filter recipes matching the selected film (Fallback to all if invalid) + const isFilmValid = !!exif.film && recipes.some(r => r.film === exif.film); + const matchingRecipes = isFilmValid ? recipes.filter(r => r.film === exif.film) : recipes; const availableDevelopers = Array.from(new Set(matchingRecipes.map(r => r.developer))).filter(Boolean).sort(); // Filter matching developer - const matchingRecipesForDev = matchingRecipes.filter(r => !exif.developer || r.developer === exif.developer); - const availableDilutions = Array.from(new Set(matchingRecipesForDev.map(r => r.dilution))).filter(Boolean).sort(); + const isDevValid = !!exif.developer && matchingRecipes.some(r => r.developer === exif.developer); + const matchingRecipesForDev = isDevValid ? matchingRecipes.filter(r => r.developer === exif.developer) : matchingRecipes; + const availableDilutions = Array.from(new Set(matchingRecipesForDev.map(r => r.dilution))).filter(Boolean).sort(customSort); // Filter matching dilution - const matchingRecipesForDilution = matchingRecipesForDev.filter(r => !exif.dilution || r.dilution === exif.dilution); - const availableTemps = Array.from(new Set(matchingRecipesForDilution.map(r => r.temp))).filter(Boolean).sort(); - const availableTimes = Array.from(new Set(matchingRecipesForDilution.map(r => r.time))).filter(Boolean).sort(); + const isDilutionValid = !!exif.dilution && matchingRecipesForDev.some(r => r.dilution === exif.dilution); + const matchingRecipesForDilution = isDilutionValid ? matchingRecipesForDev.filter(r => r.dilution === exif.dilution) : matchingRecipesForDev; + + const availableTemps = Array.from(new Set(matchingRecipesForDilution.map(r => formatTemp(r.temp)))).filter(Boolean).sort(customSort); + + // Filter matching temperature to narrow down times + const isTempValid = !!exif.temperature && matchingRecipesForDilution.some(r => formatTemp(r.temp) === formatTemp(exif.temperature)); + const matchingRecipesForTemp = isTempValid ? matchingRecipesForDilution.filter(r => formatTemp(r.temp) === formatTemp(exif.temperature)) : matchingRecipesForDilution; + const availableTimes = Array.from(new Set(matchingRecipesForTemp.map(r => formatTime(r.time)))).filter(Boolean).sort(customSort); return { availableFilms, availableDevelopers, availableDilutions, availableTemps, availableTimes }; - }, [recipes, exif.film, exif.developer, exif.dilution]); + }, [recipes, exif.film, exif.developer, exif.dilution, exif.temperature]); return (
diff --git a/frontend/src/components/ToggleInput.tsx b/frontend/src/components/ToggleInput.tsx index 0f6807d..5b55cc5 100644 --- a/frontend/src/components/ToggleInput.tsx +++ b/frontend/src/components/ToggleInput.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, FocusEvent } from 'react'; +import { ChangeEvent, FocusEvent, MouseEvent, useState } from 'react'; const EyeIcon = ({ visible }: { visible: boolean }) => ( visible ? ( @@ -20,40 +20,75 @@ export interface ToggleInputProps { onBlur?: (e: FocusEvent) => void; } -export const ToggleInput = ({ label, id, value, onChange, visible, onToggleVisibility, hideInput, suggestions, onBlur }: ToggleInputProps) => ( -
-
- - +export const ToggleInput = ({ label, id, value, onChange, visible, onToggleVisibility, hideInput, suggestions, onBlur }: ToggleInputProps) => { + // Temporary override to show all datalist options on click. + // null = no override (use parent's value), "" = cleared for datalist display. + const [tempValue, setTempValue] = useState(null); + const hasSuggestions = suggestions && suggestions.length > 0; + + const handleMouseDown = (e: MouseEvent) => { + if (hasSuggestions && document.activeElement !== e.currentTarget) { + setTempValue(""); + } + }; + + const handleChange = (e: ChangeEvent) => { + setTempValue(null); + onChange(e); + }; + + const handleBlur = (e: FocusEvent) => { + if (tempValue !== null) { + // Synchronously restore the real value to the DOM node before calling onBlur. + // This ensures that if the parent's onBlur reads e.currentTarget.value, + // they get the actual value instead of the temporary cleared string. + // The display will be restored automatically by setTempValue(null) below. + e.currentTarget.value = value; + } + if (onBlur) { + onBlur(e); + } + setTempValue(null); + }; + + const displayValue = tempValue !== null ? tempValue : value; + + return ( +
+
+ + +
+ {!hideInput && ( + <> + + {hasSuggestions && ( + + {suggestions.map((s) => ( + + )} + + )}
- {!hideInput && ( - <> - 0 ? `${id}-datalist` : undefined} - /> - {suggestions && suggestions.length > 0 && ( - - {suggestions.map((s) => ( - - )} - - )} -
-); + ); +};