1- import { useState , useCallback } from 'react' ;
1+ import { useState , useCallback , useEffect , useRef } from 'react' ;
22import JSZip from 'jszip' ;
33import { PhotoUpload } from './components/PhotoUpload' ;
44import { PaddingSettingsPanel } from './components/PaddingSettingsPanel' ;
@@ -67,7 +67,12 @@ export default function App() {
6767 const [ isProcessed , setIsProcessed ] = useState ( false ) ;
6868 const [ progress , setProgress ] = useState ( 0 ) ;
6969 const [ theme , setTheme ] = useDarkMode ( ) ;
70- const [ settingsChangedSinceProcess , setSettingsChangedSinceProcess ] = useState ( false ) ;
70+
71+ // Ref that always holds the latest handleProcess so the debounce timer
72+ // never captures a stale closure.
73+ const handleProcessRef = useRef < ( ) => void > ( ( ) => { } ) ;
74+ const autoProcessTimerRef = useRef < ReturnType < typeof setTimeout > | undefined > ( undefined ) ;
75+ const isInitialMountRef = useRef ( true ) ;
7176
7277 const THEME_OPTIONS : { value : Theme ; icon : typeof Sun ; label : string } [ ] = [
7378 { value : 'light' , icon : Sun , label : 'Light' } ,
@@ -116,7 +121,6 @@ export default function App() {
116121 if ( photos . length === 0 ) return ;
117122 setIsProcessing ( true ) ;
118123 setProgress ( 0 ) ;
119- setSettingsChangedSinceProcess ( false ) ;
120124
121125 // Determine target aspect ratio
122126 let target : number ;
@@ -138,6 +142,26 @@ export default function App() {
138142 setIsProcessed ( true ) ;
139143 } ;
140144
145+ // Keep the ref pointing at the latest handleProcess so the debounce
146+ // timer always calls the version that closes over fresh state.
147+ useEffect ( ( ) => {
148+ handleProcessRef . current = handleProcess ;
149+ } ) ;
150+
151+ // Auto-process when settings change (debounced), provided photos are loaded.
152+ // photos.length is intentionally omitted from deps to avoid triggering on
153+ // photo additions/removals; handleProcess already guards against empty photos.
154+ useEffect ( ( ) => {
155+ if ( isInitialMountRef . current ) {
156+ isInitialMountRef . current = false ;
157+ return ;
158+ }
159+ if ( photos . length === 0 ) return ;
160+ clearTimeout ( autoProcessTimerRef . current ) ;
161+ autoProcessTimerRef . current = setTimeout ( ( ) => handleProcessRef . current ( ) , 400 ) ;
162+ return ( ) => clearTimeout ( autoProcessTimerRef . current ) ;
163+ } , [ settings ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
164+
141165 const handleDownloadAll = async ( ) => {
142166 const processedPhotos = photos . filter ( p => p . paddedDataUrl ) ;
143167 if ( processedPhotos . length === 0 ) return ;
@@ -281,7 +305,6 @@ export default function App() {
281305 defaultSettings = { DEFAULT_SETTINGS }
282306 onChange = { s => {
283307 setSettings ( s ) ;
284- if ( isProcessed ) setSettingsChangedSinceProcess ( true ) ;
285308 setIsProcessed ( false ) ;
286309 } }
287310 />
@@ -305,11 +328,6 @@ export default function App() {
305328 </ >
306329 ) }
307330 </ button >
308- { settingsChangedSinceProcess && (
309- < p className = "text-xs text-amber-600 dark:text-amber-400 text-center mt-2" >
310- Settings changed — re-process to apply
311- </ p >
312- ) }
313331 </ div >
314332 </ div >
315333 </ div >
0 commit comments