From f0812c2ff02fbd22266c342053a75f2a72b45667 Mon Sep 17 00:00:00 2001 From: Thespacemanfil Date: Fri, 24 Apr 2026 00:12:56 +0100 Subject: [PATCH 1/6] Numpad controls --- src/App.tsx | 12 ++ src/components/panel/SettingsPanel.tsx | 216 +++++++++++++++++++++- src/components/ui/AppProperties.tsx | 12 ++ src/hooks/useKeyboardShortcuts.tsx | 245 ++++++++++++++++++++++++- src/utils/adjustments.tsx | 15 ++ 5 files changed, 492 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1fee88963..c752ccb0e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3165,6 +3165,13 @@ function App() { denoiseModalState.isOpen || negativeModalState.isOpen; + const handleNumpadExport = useCallback(() => { + if (selectedImage) { + setMultiSelectedPaths([selectedImage.path]); + setIsLibraryExportPanelVisible(true); + } + }, [selectedImage]); + useKeyboardShortcuts({ isModalOpen: isAnyModalOpen, osPlatform, @@ -3215,6 +3222,11 @@ function App() { originalSize, brushSettings: brushSettings, setBrushSettings: setBrushSettings, + numpadSettings: appSettings?.numpadSettings, + setNumpadSettings: (settings) => handleSettingsChange({ ...appSettings, numpadSettings: settings }), + handleExportCurrent: handleNumpadExport, + adjustments, + setAdjustments, }); useEffect(() => { diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx index bb9adc993..2da4d38d0 100644 --- a/src/components/panel/SettingsPanel.tsx +++ b/src/components/panel/SettingsPanel.tsx @@ -29,7 +29,7 @@ import Switch from '../ui/Switch'; import Input from '../ui/Input'; import Slider from '../ui/Slider'; import { ThemeProps, THEMES, DEFAULT_THEME_ID } from '../../utils/themes'; -import { Invokes } from '../ui/AppProperties'; +import { Invokes, NumpadSettings } from '../ui/AppProperties'; import Text from '../ui/Text'; import { TextColors, TextVariants, TextWeights } from '../../types/typography'; import { useOsPlatform } from '../../hooks/useOsPlatform'; @@ -334,6 +334,19 @@ export default function SettingsPanel({ const [tempLensMaker, setTempLensMaker] = useState(''); const [tempLensModel, setTempLensModel] = useState(''); + const [numpadSettings, setNumpadSettings] = useState( + appSettings?.numpadSettings || { + enabled: false, + mode: 'digital', + enterKeyMode: 'next', + stepSizes: { + exposure: 0.1, + contrast: 2, + rgbCmy: 1, + }, + } + ); + const osPlatform = useOsPlatform(); const [processingSettings, setProcessingSettings] = useState({ editorPreviewResolution: appSettings?.editorPreviewResolution || 1920, @@ -1858,6 +1871,207 @@ export default function SettingsPanel({ + +
+ + Numpad + + + Enable numpad shortcuts for rapid color and density adjustments, inspired by traditional film scanner workflows. + + +
+ + { + const newSettings = { ...numpadSettings, enabled: checked }; + setNumpadSettings(newSettings); + onSettingsChange({ ...appSettings, numpadSettings: newSettings }); + }} + /> + + + {numpadSettings.enabled && ( + <> + +
+ {[ + { id: 'digital', label: 'Digital' }, + { id: 'film', label: 'Film' }, + ].map((mode) => ( + + ))} +
+
+ + +
+ {[ + { id: 'next', label: 'Next Image' }, + { id: 'instant-export', label: 'Instant Export' }, + { id: 'skip-move', label: 'Skip & Move' }, + ].map((enterMode) => ( + + ))} +
+
+ +
+ Step Sizes + + + { + const newSettings = { + ...numpadSettings, + stepSizes: { ...numpadSettings.stepSizes, exposure: parseFloat(e.target.value) }, + }; + setNumpadSettings(newSettings); + onSettingsChange({ ...appSettings, numpadSettings: newSettings }); + }} + step={0.01} + value={numpadSettings.stepSizes.exposure} + /> + + + + { + const newSettings = { + ...numpadSettings, + stepSizes: { ...numpadSettings.stepSizes, contrast: parseFloat(e.target.value) }, + }; + setNumpadSettings(newSettings); + onSettingsChange({ ...appSettings, numpadSettings: newSettings }); + }} + step={0.5} + value={numpadSettings.stepSizes.contrast} + /> + + + + { + const newSettings = { + ...numpadSettings, + stepSizes: { ...numpadSettings.stepSizes, rgbCmy: parseFloat(e.target.value) }, + }; + setNumpadSettings(newSettings); + onSettingsChange({ ...appSettings, numpadSettings: newSettings }); + }} + step={0.1} + value={numpadSettings.stepSizes.rgbCmy} + /> + +
+ +
+ + Key Mapping + +
+ + + + + + + +
+
+ + )} +
+
)} diff --git a/src/components/ui/AppProperties.tsx b/src/components/ui/AppProperties.tsx index cd464b76f..5313b48ef 100644 --- a/src/components/ui/AppProperties.tsx +++ b/src/components/ui/AppProperties.tsx @@ -145,6 +145,17 @@ export enum ThumbnailAspectRatio { Contain = 'contain', } +export interface NumpadSettings { + enabled: boolean; + mode: 'digital' | 'film'; + enterKeyMode: 'next' | 'instant-export' | 'skip-move'; + stepSizes: { + exposure: number; + contrast: number; + rgbCmy: number; + }; +} + export interface AppSettings { aiConnectorAddress?: string; decorations?: any; @@ -181,6 +192,7 @@ export interface AppSettings { waveformHeight?: number; activeWaveformChannel?: string; useWgpuRenderer?: boolean; + numpadSettings?: NumpadSettings; } export interface BrushSettings { diff --git a/src/hooks/useKeyboardShortcuts.tsx b/src/hooks/useKeyboardShortcuts.tsx index 606c58204..4b2e86d0d 100644 --- a/src/hooks/useKeyboardShortcuts.tsx +++ b/src/hooks/useKeyboardShortcuts.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; -import { ImageFile, Panel, SelectedImage } from '../components/ui/AppProperties'; +import { ImageFile, Panel, SelectedImage, NumpadSettings } from '../components/ui/AppProperties'; import { BrushSettings } from '../components/ui/AppProperties'; +import { Adjustments, INITIAL_SP3000_OFFSETS } from '../utils/adjustments'; interface KeyboardShortcutsProps { activeAiPatchContainerId?: string | null; @@ -51,8 +52,13 @@ interface KeyboardShortcutsProps { displaySize?: { width: number; height: number }; baseRenderSize?: { width: number; height: number }; originalSize?: { width: number; height: number }; - brushSettings: BrushSettings | null; - setBrushSettings: (settings: BrushSettings) => void; + brushSettings: BrushSettings | null; + setBrushSettings: (settings: BrushSettings) => void; + numpadSettings?: NumpadSettings; + setNumpadSettings?: (settings: NumpadSettings) => void; + handleExportCurrent?: () => void; + adjustments: Adjustments; + setAdjustments: (adjustments: Partial | ((prev: Adjustments) => Adjustments)) => void; } export const useKeyboardShortcuts = ({ @@ -104,8 +110,13 @@ export const useKeyboardShortcuts = ({ displaySize, baseRenderSize, originalSize, - brushSettings, - setBrushSettings, + brushSettings, + setBrushSettings, + numpadSettings, + setNumpadSettings, + handleExportCurrent, + adjustments, + setAdjustments, }: KeyboardShortcutsProps) => { useEffect(() => { const handleKeyDown = (event: any) => { @@ -364,6 +375,221 @@ export const useKeyboardShortcuts = ({ } } + // Numpad shortcuts for SP-3000 style adjustments + if (numpadSettings?.enabled && selectedImage) { + const stepSizes = numpadSettings.stepSizes; + const mode = numpadSettings.mode; + + // Handle numpad keys + if (code.startsWith('Numpad') && !isCtrl) { + event.preventDefault(); + + switch (code) { + case 'Numpad7': + // - Yellow / + Blue + if (mode === 'digital') { + setAdjustments((prev: Adjustments) => ({ + ...prev, + temperature: (prev.temperature || 0) - stepSizes.rgbCmy, + })); + } else { + setAdjustments((prev: Adjustments) => ({ + ...prev, + sp3000ColorOffsets: { + ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), + yellowBlue: (prev.sp3000ColorOffsets?.yellowBlue || 0) - stepSizes.rgbCmy, + }, + })); + } + break; + case 'Numpad4': + // + Yellow / - Blue + if (mode === 'digital') { + setAdjustments((prev: Adjustments) => ({ + ...prev, + temperature: (prev.temperature || 0) + stepSizes.rgbCmy, + })); + } else { + setAdjustments((prev: Adjustments) => ({ + ...prev, + sp3000ColorOffsets: { + ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), + yellowBlue: (prev.sp3000ColorOffsets?.yellowBlue || 0) + stepSizes.rgbCmy, + }, + })); + } + break; + case 'Numpad8': + // - Magenta / + Green + if (mode === 'digital') { + setAdjustments((prev: Adjustments) => ({ + ...prev, + tint: (prev.tint || 0) - stepSizes.rgbCmy, + })); + } else { + setAdjustments((prev: Adjustments) => ({ + ...prev, + sp3000ColorOffsets: { + ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), + magentaGreen: (prev.sp3000ColorOffsets?.magentaGreen || 0) - stepSizes.rgbCmy, + }, + })); + } + break; + case 'Numpad5': + // + Magenta / - Green + if (mode === 'digital') { + setAdjustments((prev: Adjustments) => ({ + ...prev, + tint: (prev.tint || 0) + stepSizes.rgbCmy, + })); + } else { + setAdjustments((prev: Adjustments) => ({ + ...prev, + sp3000ColorOffsets: { + ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), + magentaGreen: (prev.sp3000ColorOffsets?.magentaGreen || 0) + stepSizes.rgbCmy, + }, + })); + } + break; + case 'Numpad9': + // - Cyan / + Red (Film mode only) + if (mode === 'film') { + setAdjustments((prev: Adjustments) => ({ + ...prev, + sp3000ColorOffsets: { + ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), + cyanRed: (prev.sp3000ColorOffsets?.cyanRed || 0) - stepSizes.rgbCmy, + }, + })); + } + break; + case 'Numpad6': + // + Cyan / - Red (Film mode only) + if (mode === 'film') { + setAdjustments((prev: Adjustments) => ({ + ...prev, + sp3000ColorOffsets: { + ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), + cyanRed: (prev.sp3000ColorOffsets?.cyanRed || 0) + stepSizes.rgbCmy, + }, + })); + } + break; + case 'Numpad1': + // - Density (brighten) + if (mode === 'digital') { + setAdjustments((prev: Adjustments) => ({ + ...prev, + exposure: (prev.exposure || 0) + stepSizes.exposure, + })); + } else { + setAdjustments((prev: Adjustments) => ({ + ...prev, + exposure: (prev.exposure || 0) + stepSizes.exposure, + blacks: (prev.blacks || 0) + 2, + })); + } + break; + case 'NumpadDecimal': + // + Density (darken) + if (mode === 'digital') { + setAdjustments((prev: Adjustments) => ({ + ...prev, + exposure: (prev.exposure || 0) - stepSizes.exposure, + })); + } else { + setAdjustments((prev: Adjustments) => ({ + ...prev, + exposure: (prev.exposure || 0) - stepSizes.exposure, + blacks: (prev.blacks || 0) - 2, + })); + } + break; + case 'Numpad2': + // - Contrast (soften) + setAdjustments((prev: Adjustments) => ({ + ...prev, + contrast: (prev.contrast || 0) - stepSizes.contrast, + })); + break; + case 'Numpad3': + // + Contrast (harden) + setAdjustments((prev: Adjustments) => ({ + ...prev, + contrast: (prev.contrast || 0) + stepSizes.contrast, + })); + break; + case 'Numpad0': + // Reset SP-3000 offsets + setAdjustments((prev: Adjustments) => ({ + ...prev, + sp3000ColorOffsets: { ...INITIAL_SP3000_OFFSETS }, + })); + break; + case 'NumpadEnter': + // Handle based on enterKeyMode + switch (numpadSettings.enterKeyMode) { + case 'next': + // Move to next image + const nextIndex = sortedImageList.findIndex( + (img: ImageFile) => img.path === selectedImage.path + ); + if (nextIndex !== -1) { + let newIndex = nextIndex + 1; + if (newIndex >= sortedImageList.length) { + newIndex = 0; + } + const nextImage = sortedImageList[newIndex]; + if (nextImage) { + handleImageSelect(nextImage.path); + } + } + break; + case 'instant-export': + // Export and move to next + if (handleExportCurrent) { + handleExportCurrent(); + const exportIndex = sortedImageList.findIndex( + (img: ImageFile) => img.path === selectedImage.path + ); + if (exportIndex !== -1) { + let newIndex = exportIndex + 1; + if (newIndex >= sortedImageList.length) { + newIndex = 0; + } + const nextImage = sortedImageList[newIndex]; + if (nextImage) { + handleImageSelect(nextImage.path); + } + } + } + break; + case 'skip-move': + // Just move to next (revert metadata changes) + const skipIndex = sortedImageList.findIndex( + (img: ImageFile) => img.path === selectedImage.path + ); + if (skipIndex !== -1) { + let newIndex = skipIndex + 1; + if (newIndex >= sortedImageList.length) { + newIndex = 0; + } + const nextImage = sortedImageList[newIndex]; + if (nextImage) { + handleImageSelect(nextImage.path); + } + } + break; + } + break; + default: + break; + } + } + } + if (isCtrl) { const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1; const currentPercent = @@ -502,7 +728,12 @@ export const useKeyboardShortcuts = ({ displaySize, baseRenderSize, originalSize, - brushSettings, - setBrushSettings, + brushSettings, + setBrushSettings, + numpadSettings, + setNumpadSettings, + handleExportCurrent, + adjustments, + setAdjustments, ]); }; diff --git a/src/utils/adjustments.tsx b/src/utils/adjustments.tsx index 0ca4e3571..38aa77ad7 100644 --- a/src/utils/adjustments.tsx +++ b/src/utils/adjustments.tsx @@ -124,6 +124,12 @@ export interface ColorCalibration { blueSaturation: number; } +export interface Sp3000ColorOffsets { + cyanRed: number; + magentaGreen: number; + yellowBlue: number; +} + export interface Adjustments { [index: string]: any; aiPatches: Array; @@ -185,6 +191,7 @@ export interface Adjustments { shadows: number; sharpness: number; showClipping: boolean; + sp3000ColorOffsets?: Sp3000ColorOffsets; structure: number; temperature: number; tint: number; @@ -346,6 +353,12 @@ const INITIAL_COLOR_CALIBRATION: ColorCalibration = { blueSaturation: 0, }; +export const INITIAL_SP3000_OFFSETS: Sp3000ColorOffsets = { + cyanRed: 0, + magentaGreen: 0, + yellowBlue: 0, +}; + export const INITIAL_MASK_ADJUSTMENTS: MaskAdjustments = { blacks: 0, brightness: 0, @@ -496,6 +509,7 @@ export const INITIAL_ADJUSTMENTS: Adjustments = { shadows: 0, sharpness: 0, showClipping: false, + sp3000ColorOffsets: { ...INITIAL_SP3000_OFFSETS }, structure: 0, temperature: 0, tint: 0, @@ -590,6 +604,7 @@ export const normalizeLoadedAdjustments = (loadedAdjustments: Adjustments): any colorGrading: { ...INITIAL_ADJUSTMENTS.colorGrading, ...(loadedAdjustments.colorGrading || {}) }, hsl: { ...INITIAL_ADJUSTMENTS.hsl, ...(loadedAdjustments.hsl || {}) }, curves: { ...INITIAL_ADJUSTMENTS.curves, ...(loadedAdjustments.curves || {}) }, + sp3000ColorOffsets: { ...INITIAL_ADJUSTMENTS.sp3000ColorOffsets, ...(loadedAdjustments.sp3000ColorOffsets || {}) }, masks: normalizedMasks, aiPatches: normalizedAiPatches, sectionVisibility: { From 443acb1c6c7d8274acdbf03d526b5daed493f32f Mon Sep 17 00:00:00 2001 From: Thespacemanfil Date: Fri, 24 Apr 2026 00:44:52 +0100 Subject: [PATCH 2/6] Final fixes and reference changes Fixed a few issues and changed all but one reference to the Sp-3000 to film instead --- src/components/panel/SettingsPanel.tsx | 12 +++- src/hooks/useKeyboardShortcuts.tsx | 82 ++++++++++++++------------ src/utils/adjustments.tsx | 10 ++-- 3 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx index 2da4d38d0..ba918f8d6 100644 --- a/src/components/panel/SettingsPanel.tsx +++ b/src/components/panel/SettingsPanel.tsx @@ -1877,7 +1877,7 @@ export default function SettingsPanel({ Numpad - Enable numpad shortcuts for rapid color and density adjustments, inspired by traditional film scanner workflows. + Enable numpad shortcuts for rapid color and density adjustments, inspired by the Fuji Frontier SP-3000 scanner workflow.
@@ -1938,6 +1938,14 @@ export default function SettingsPanel({
+ {numpadSettings.mode === 'film' && ( +
+ + ⚠️ Film mode is experimental. RGB/CMY color offsets are not yet applied to image rendering. Use Digital mode for full functionality. + +
+ )} + diff --git a/src/hooks/useKeyboardShortcuts.tsx b/src/hooks/useKeyboardShortcuts.tsx index 4b2e86d0d..143163e29 100644 --- a/src/hooks/useKeyboardShortcuts.tsx +++ b/src/hooks/useKeyboardShortcuts.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { ImageFile, Panel, SelectedImage, NumpadSettings } from '../components/ui/AppProperties'; import { BrushSettings } from '../components/ui/AppProperties'; -import { Adjustments, INITIAL_SP3000_OFFSETS } from '../utils/adjustments'; +import { Adjustments, INITIAL_FILM_OFFSETS } from '../utils/adjustments'; interface KeyboardShortcutsProps { activeAiPatchContainerId?: string | null; @@ -253,7 +253,7 @@ export const useKeyboardShortcuts = ({ } } } else { - if ((key === 'enter' || key === ' ') && !isCtrl) { + if ((key === 'enter' || key === ' ') && !isCtrl && code !== 'NumpadEnter') { event.preventDefault(); if (libraryActivePath) { handleImageSelect(libraryActivePath); @@ -325,7 +325,7 @@ export const useKeyboardShortcuts = ({ } } - if (code.startsWith('Digit') && !isCtrl) { + if (code.startsWith('Digit') && !isCtrl && !code.startsWith('Numpad')) { event.preventDefault(); const keyNum = parseInt(code.replace('Digit', ''), 10); @@ -341,7 +341,7 @@ export const useKeyboardShortcuts = ({ handleRate(keyNum); } } - } else if (['0', '1', '2', '3', '4', '5'].includes(key) && !isCtrl) { + } else if (['0', '1', '2', '3', '4', '5'].includes(key) && !isCtrl && !code.startsWith('Numpad')) { event.preventDefault(); handleRate(parseInt(key, 10)); } @@ -375,7 +375,7 @@ export const useKeyboardShortcuts = ({ } } - // Numpad shortcuts for SP-3000 style adjustments + // Numpad shortcuts for film-style adjustments if (numpadSettings?.enabled && selectedImage) { const stepSizes = numpadSettings.stepSizes; const mode = numpadSettings.mode; @@ -395,9 +395,9 @@ export const useKeyboardShortcuts = ({ } else { setAdjustments((prev: Adjustments) => ({ ...prev, - sp3000ColorOffsets: { - ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), - yellowBlue: (prev.sp3000ColorOffsets?.yellowBlue || 0) - stepSizes.rgbCmy, + filmColorOffsets: { + ...(prev.filmColorOffsets || INITIAL_FILM_OFFSETS), + yellowBlue: (prev.filmColorOffsets?.yellowBlue || 0) - stepSizes.rgbCmy, }, })); } @@ -412,9 +412,9 @@ export const useKeyboardShortcuts = ({ } else { setAdjustments((prev: Adjustments) => ({ ...prev, - sp3000ColorOffsets: { - ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), - yellowBlue: (prev.sp3000ColorOffsets?.yellowBlue || 0) + stepSizes.rgbCmy, + filmColorOffsets: { + ...(prev.filmColorOffsets || INITIAL_FILM_OFFSETS), + yellowBlue: (prev.filmColorOffsets?.yellowBlue || 0) + stepSizes.rgbCmy, }, })); } @@ -429,9 +429,9 @@ export const useKeyboardShortcuts = ({ } else { setAdjustments((prev: Adjustments) => ({ ...prev, - sp3000ColorOffsets: { - ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), - magentaGreen: (prev.sp3000ColorOffsets?.magentaGreen || 0) - stepSizes.rgbCmy, + filmColorOffsets: { + ...(prev.filmColorOffsets || INITIAL_FILM_OFFSETS), + magentaGreen: (prev.filmColorOffsets?.magentaGreen || 0) - stepSizes.rgbCmy, }, })); } @@ -446,9 +446,9 @@ export const useKeyboardShortcuts = ({ } else { setAdjustments((prev: Adjustments) => ({ ...prev, - sp3000ColorOffsets: { - ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), - magentaGreen: (prev.sp3000ColorOffsets?.magentaGreen || 0) + stepSizes.rgbCmy, + filmColorOffsets: { + ...(prev.filmColorOffsets || INITIAL_FILM_OFFSETS), + magentaGreen: (prev.filmColorOffsets?.magentaGreen || 0) + stepSizes.rgbCmy, }, })); } @@ -458,9 +458,9 @@ export const useKeyboardShortcuts = ({ if (mode === 'film') { setAdjustments((prev: Adjustments) => ({ ...prev, - sp3000ColorOffsets: { - ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), - cyanRed: (prev.sp3000ColorOffsets?.cyanRed || 0) - stepSizes.rgbCmy, + filmColorOffsets: { + ...(prev.filmColorOffsets || INITIAL_FILM_OFFSETS), + cyanRed: (prev.filmColorOffsets?.cyanRed || 0) - stepSizes.rgbCmy, }, })); } @@ -470,9 +470,9 @@ export const useKeyboardShortcuts = ({ if (mode === 'film') { setAdjustments((prev: Adjustments) => ({ ...prev, - sp3000ColorOffsets: { - ...(prev.sp3000ColorOffsets || INITIAL_SP3000_OFFSETS), - cyanRed: (prev.sp3000ColorOffsets?.cyanRed || 0) + stepSizes.rgbCmy, + filmColorOffsets: { + ...(prev.filmColorOffsets || INITIAL_FILM_OFFSETS), + cyanRed: (prev.filmColorOffsets?.cyanRed || 0) + stepSizes.rgbCmy, }, })); } @@ -482,12 +482,12 @@ export const useKeyboardShortcuts = ({ if (mode === 'digital') { setAdjustments((prev: Adjustments) => ({ ...prev, - exposure: (prev.exposure || 0) + stepSizes.exposure, + exposure: Math.max(-5, Math.min(5, (prev.exposure || 0) + stepSizes.exposure)), })); } else { setAdjustments((prev: Adjustments) => ({ ...prev, - exposure: (prev.exposure || 0) + stepSizes.exposure, + exposure: Math.max(-5, Math.min(5, (prev.exposure || 0) + stepSizes.exposure)), blacks: (prev.blacks || 0) + 2, })); } @@ -497,12 +497,12 @@ export const useKeyboardShortcuts = ({ if (mode === 'digital') { setAdjustments((prev: Adjustments) => ({ ...prev, - exposure: (prev.exposure || 0) - stepSizes.exposure, + exposure: Math.max(-5, Math.min(5, (prev.exposure || 0) - stepSizes.exposure)), })); } else { setAdjustments((prev: Adjustments) => ({ ...prev, - exposure: (prev.exposure || 0) - stepSizes.exposure, + exposure: Math.max(-5, Math.min(5, (prev.exposure || 0) - stepSizes.exposure)), blacks: (prev.blacks || 0) - 2, })); } @@ -511,22 +511,30 @@ export const useKeyboardShortcuts = ({ // - Contrast (soften) setAdjustments((prev: Adjustments) => ({ ...prev, - contrast: (prev.contrast || 0) - stepSizes.contrast, + contrast: Math.max(-100, Math.min(100, (prev.contrast || 0) - stepSizes.contrast)), })); break; case 'Numpad3': // + Contrast (harden) setAdjustments((prev: Adjustments) => ({ ...prev, - contrast: (prev.contrast || 0) + stepSizes.contrast, + contrast: Math.max(-100, Math.min(100, (prev.contrast || 0) + stepSizes.contrast)), })); break; case 'Numpad0': - // Reset SP-3000 offsets - setAdjustments((prev: Adjustments) => ({ - ...prev, - sp3000ColorOffsets: { ...INITIAL_SP3000_OFFSETS }, - })); + // Reset color adjustments + if (mode === 'digital') { + setAdjustments((prev: Adjustments) => ({ + ...prev, + temperature: 0, + tint: 0, + })); + } else { + setAdjustments((prev: Adjustments) => ({ + ...prev, + filmColorOffsets: { ...INITIAL_FILM_OFFSETS }, + })); + } break; case 'NumpadEnter': // Handle based on enterKeyMode @@ -551,11 +559,11 @@ export const useKeyboardShortcuts = ({ // Export and move to next if (handleExportCurrent) { handleExportCurrent(); - const exportIndex = sortedImageList.findIndex( + const currentIndex = sortedImageList.findIndex( (img: ImageFile) => img.path === selectedImage.path ); - if (exportIndex !== -1) { - let newIndex = exportIndex + 1; + if (currentIndex !== -1) { + let newIndex = currentIndex + 1; if (newIndex >= sortedImageList.length) { newIndex = 0; } diff --git a/src/utils/adjustments.tsx b/src/utils/adjustments.tsx index 38aa77ad7..5a0ba5fa5 100644 --- a/src/utils/adjustments.tsx +++ b/src/utils/adjustments.tsx @@ -124,7 +124,7 @@ export interface ColorCalibration { blueSaturation: number; } -export interface Sp3000ColorOffsets { +export interface FilmColorOffsets { cyanRed: number; magentaGreen: number; yellowBlue: number; @@ -191,7 +191,7 @@ export interface Adjustments { shadows: number; sharpness: number; showClipping: boolean; - sp3000ColorOffsets?: Sp3000ColorOffsets; + filmColorOffsets?: FilmColorOffsets; structure: number; temperature: number; tint: number; @@ -353,7 +353,7 @@ const INITIAL_COLOR_CALIBRATION: ColorCalibration = { blueSaturation: 0, }; -export const INITIAL_SP3000_OFFSETS: Sp3000ColorOffsets = { +export const INITIAL_FILM_OFFSETS: FilmColorOffsets = { cyanRed: 0, magentaGreen: 0, yellowBlue: 0, @@ -509,7 +509,7 @@ export const INITIAL_ADJUSTMENTS: Adjustments = { shadows: 0, sharpness: 0, showClipping: false, - sp3000ColorOffsets: { ...INITIAL_SP3000_OFFSETS }, + filmColorOffsets: { ...INITIAL_FILM_OFFSETS }, structure: 0, temperature: 0, tint: 0, @@ -604,7 +604,7 @@ export const normalizeLoadedAdjustments = (loadedAdjustments: Adjustments): any colorGrading: { ...INITIAL_ADJUSTMENTS.colorGrading, ...(loadedAdjustments.colorGrading || {}) }, hsl: { ...INITIAL_ADJUSTMENTS.hsl, ...(loadedAdjustments.hsl || {}) }, curves: { ...INITIAL_ADJUSTMENTS.curves, ...(loadedAdjustments.curves || {}) }, - sp3000ColorOffsets: { ...INITIAL_ADJUSTMENTS.sp3000ColorOffsets, ...(loadedAdjustments.sp3000ColorOffsets || {}) }, + filmColorOffsets: { ...INITIAL_ADJUSTMENTS.filmColorOffsets, ...(loadedAdjustments.filmColorOffsets || {}) }, masks: normalizedMasks, aiPatches: normalizedAiPatches, sectionVisibility: { From 1d12af347fb96593ee94139ff424405b955db756 Mon Sep 17 00:00:00 2001 From: Thespacemanfil Date: Fri, 24 Apr 2026 00:50:35 +0100 Subject: [PATCH 3/6] Settings visibility updated Ensure film mode only settings are not visible when digital mode is selected --- src/components/panel/SettingsPanel.tsx | 62 ++++++++++++++------------ 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx index ba918f8d6..17189c82c 100644 --- a/src/components/panel/SettingsPanel.tsx +++ b/src/components/panel/SettingsPanel.tsx @@ -2036,27 +2036,29 @@ export default function SettingsPanel({ /> - - { - const newSettings = { - ...numpadSettings, - stepSizes: { ...numpadSettings.stepSizes, rgbCmy: parseFloat(e.target.value) }, - }; - setNumpadSettings(newSettings); - onSettingsChange({ ...appSettings, numpadSettings: newSettings }); - }} - step={0.1} - value={numpadSettings.stepSizes.rgbCmy} - /> - + {numpadSettings.mode === 'film' && ( + + { + const newSettings = { + ...numpadSettings, + stepSizes: { ...numpadSettings.stepSizes, rgbCmy: parseFloat(e.target.value) }, + }; + setNumpadSettings(newSettings); + onSettingsChange({ ...appSettings, numpadSettings: newSettings }); + }} + step={0.1} + value={numpadSettings.stepSizes.rgbCmy} + /> + + )}
@@ -2064,15 +2066,17 @@ export default function SettingsPanel({ Key Mapping
- - - - + {numpadSettings.mode === 'film' && } + {numpadSettings.mode === 'film' && } + {numpadSettings.mode === 'film' && ( + + )} + - +
From 8515805a7cd4de453cb70702012dbbfb27c2d3b2 Mon Sep 17 00:00:00 2001 From: Thespacemanfil Date: Fri, 24 Apr 2026 00:53:12 +0100 Subject: [PATCH 4/6] Mode visibility fix --- src/components/panel/SettingsPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx index 17189c82c..b2300d4bc 100644 --- a/src/components/panel/SettingsPanel.tsx +++ b/src/components/panel/SettingsPanel.tsx @@ -1993,7 +1993,7 @@ export default function SettingsPanel({ Step Sizes Date: Fri, 24 Apr 2026 01:17:06 +0100 Subject: [PATCH 5/6] -Final- Settings visibility adjustment --- src/components/panel/SettingsPanel.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx index b2300d4bc..58efda1bb 100644 --- a/src/components/panel/SettingsPanel.tsx +++ b/src/components/panel/SettingsPanel.tsx @@ -1878,6 +1878,7 @@ export default function SettingsPanel({ Enable numpad shortcuts for rapid color and density adjustments, inspired by the Fuji Frontier SP-3000 scanner workflow. + {numpadSettings.mode === 'film' && ' Film mode provides RGB/CMY color control similar to traditional film scanners.'}
@@ -1901,7 +1902,7 @@ export default function SettingsPanel({ <>
{[ From 4ba39426c18ab1011b00c6172531444435caead2 Mon Sep 17 00:00:00 2001 From: Thespacemanfil Date: Fri, 24 Apr 2026 01:31:02 +0100 Subject: [PATCH 6/6] Instant export fix --- src/App.tsx | 57 +++++++++++++++++++++- src/components/panel/SettingsPanel.tsx | 66 ++++++++++++++++++++++++++ src/components/ui/AppProperties.tsx | 2 + src/hooks/useKeyboardShortcuts.tsx | 37 +++++++++------ 4 files changed, 146 insertions(+), 16 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c752ccb0e..f6cd7d663 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -92,7 +92,7 @@ import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import GlobalTooltip from './components/ui/GlobalTooltip'; import { THEMES, DEFAULT_THEME_ID, ThemeProps } from './utils/themes'; import { SubMask, ToolType } from './components/panel/right/Masks'; -import { ExportState, IMPORT_TIMEOUT, ImportState, Status } from './components/ui/ExportImportProperties'; +import { ExportState, IMPORT_TIMEOUT, ImportState, Status, ExportSettings, FILE_FORMATS } from './components/ui/ExportImportProperties'; import { AppSettings, BrushSettings, @@ -3172,6 +3172,60 @@ function App() { } }, [selectedImage]); + const handleInstantExport = useCallback(async (): Promise => { + if (!selectedImage) return false; + + const numpadSettings = appSettings?.numpadSettings; + if (!numpadSettings?.defaultExportPresetId || !numpadSettings?.defaultExportPath) { + toast.error('Please configure default export preset and path in settings'); + return false; + } + + const preset = appSettings?.exportPresets?.find((p) => p.id === numpadSettings.defaultExportPresetId); + if (!preset) { + toast.error('Selected export preset not found'); + return false; + } + + try { + const exportSettings: ExportSettings = { + filenameTemplate: preset.filenameTemplate, + jpegQuality: preset.jpegQuality, + keepMetadata: preset.keepMetadata, + preserveTimestamps: preset.preserveTimestamps, + resize: preset.enableResize ? { mode: preset.resizeMode, value: preset.resizeValue, dontEnlarge: preset.dontEnlarge } : null, + stripGps: preset.stripGps, + exportMasks: preset.exportMasks ?? false, + watermark: + preset.enableWatermark && preset.watermarkPath + ? { + path: preset.watermarkPath, + anchor: preset.watermarkAnchor as any, + scale: preset.watermarkScale, + spacing: preset.watermarkSpacing, + opacity: preset.watermarkOpacity, + } + : null, + preserveFolders: preset.preserveFolders ?? false, + }; + + await invoke(Invokes.BatchExportImages, { + exportSettings, + outputFolder: numpadSettings.defaultExportPath, + outputFormat: FILE_FORMATS.find((f: any) => f.id === preset.fileFormat)?.extensions[0], + paths: [selectedImage.path], + baseOriginFolder: rootPath, + }); + + toast.success('Export successful'); + return true; + } catch (error) { + console.error('Error exporting image:', error); + toast.error(typeof error === 'string' ? error : 'Export failed'); + return false; + } + }, [selectedImage, appSettings, rootPath]); + useKeyboardShortcuts({ isModalOpen: isAnyModalOpen, osPlatform, @@ -3225,6 +3279,7 @@ function App() { numpadSettings: appSettings?.numpadSettings, setNumpadSettings: (settings) => handleSettingsChange({ ...appSettings, numpadSettings: settings }), handleExportCurrent: handleNumpadExport, + handleInstantExport, adjustments, setAdjustments, }); diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx index 58efda1bb..2eb6ef292 100644 --- a/src/components/panel/SettingsPanel.tsx +++ b/src/components/panel/SettingsPanel.tsx @@ -30,6 +30,7 @@ import Input from '../ui/Input'; import Slider from '../ui/Slider'; import { ThemeProps, THEMES, DEFAULT_THEME_ID } from '../../utils/themes'; import { Invokes, NumpadSettings } from '../ui/AppProperties'; +import { ExportPreset } from '../ui/ExportImportProperties'; import Text from '../ui/Text'; import { TextColors, TextVariants, TextWeights } from '../../types/typography'; import { useOsPlatform } from '../../hooks/useOsPlatform'; @@ -1990,6 +1991,71 @@ export default function SettingsPanel({
+ {numpadSettings.enterKeyMode === 'instant-export' && ( + <> + +
+ +
+
+ + +
+
+ {numpadSettings.defaultExportPath || 'Not set'} +
+ +
+
+ + )} +
Step Sizes diff --git a/src/components/ui/AppProperties.tsx b/src/components/ui/AppProperties.tsx index 5313b48ef..95b313c47 100644 --- a/src/components/ui/AppProperties.tsx +++ b/src/components/ui/AppProperties.tsx @@ -154,6 +154,8 @@ export interface NumpadSettings { contrast: number; rgbCmy: number; }; + defaultExportPresetId?: string; + defaultExportPath?: string; } export interface AppSettings { diff --git a/src/hooks/useKeyboardShortcuts.tsx b/src/hooks/useKeyboardShortcuts.tsx index 143163e29..e07d6d80a 100644 --- a/src/hooks/useKeyboardShortcuts.tsx +++ b/src/hooks/useKeyboardShortcuts.tsx @@ -57,6 +57,7 @@ interface KeyboardShortcutsProps { numpadSettings?: NumpadSettings; setNumpadSettings?: (settings: NumpadSettings) => void; handleExportCurrent?: () => void; + handleInstantExport?: () => Promise; adjustments: Adjustments; setAdjustments: (adjustments: Partial | ((prev: Adjustments) => Adjustments)) => void; } @@ -115,6 +116,7 @@ export const useKeyboardShortcuts = ({ numpadSettings, setNumpadSettings, handleExportCurrent, + handleInstantExport, adjustments, setAdjustments, }: KeyboardShortcutsProps) => { @@ -556,22 +558,27 @@ export const useKeyboardShortcuts = ({ } break; case 'instant-export': - // Export and move to next - if (handleExportCurrent) { - handleExportCurrent(); - const currentIndex = sortedImageList.findIndex( - (img: ImageFile) => img.path === selectedImage.path - ); - if (currentIndex !== -1) { - let newIndex = currentIndex + 1; - if (newIndex >= sortedImageList.length) { - newIndex = 0; + // Export and move to next after completion + if (handleInstantExport) { + (async () => { + const exportSuccess = await handleInstantExport(); + if (exportSuccess) { + const currentIndex = sortedImageList.findIndex( + (img: ImageFile) => img.path === selectedImage.path + ); + if (currentIndex !== -1) { + let newIndex = currentIndex + 1; + if (newIndex >= sortedImageList.length) { + newIndex = 0; + } + const nextImage = sortedImageList[newIndex]; + if (nextImage) { + handleImageSelect(nextImage.path); + } + } } - const nextImage = sortedImageList[newIndex]; - if (nextImage) { - handleImageSelect(nextImage.path); - } - } + // If export failed, stay on current image (error shown in handleInstantExport) + })(); } break; case 'skip-move':