1- import { useEffect , useState } from 'react'
1+ import { useEffect , useState , useRef , useCallback } from 'react'
22import { Play } from 'lucide-react'
33import { useAppStore , useProjectConfig , useProjectResults , SavedProject } from './stores/appStore'
44import { optimize , depthToPressure } from './lib/optimizer'
@@ -26,7 +26,10 @@ function App() {
2626 getActiveProject,
2727 newProject,
2828 addRecentFile,
29- getSelectedMaterialKeys
29+ getSelectedMaterialKeys,
30+ autoSaveEnabled,
31+ projects,
32+ activeProjectId
3033 } = useAppStore ( )
3134
3235 const config = useProjectConfig ( )
@@ -35,6 +38,7 @@ function App() {
3538 const [ isResizing , setIsResizing ] = useState ( false )
3639 const [ statusMessage , setStatusMessage ] = useState ( '' )
3740 const [ viewerWidth , setViewerWidth ] = useState ( 450 ) // Default wider viewer
41+ const autoSaveTimerRef = useRef < number | null > ( null )
3842
3943 // Handle sidebar resize
4044 useEffect ( ( ) => {
@@ -64,14 +68,15 @@ function App() {
6468 }
6569 } , [ isResizing , setSidebarWidth ] )
6670
67- // Save config handler
68- const handleSaveConfig = async ( ) => {
71+ // Save config handler - saves to existing path if available
72+ const handleSaveConfig = useCallback ( async ( ) => {
73+ const project = getActiveProject ( )
6974 const projectData = exportProject ( )
7075 if ( ! projectData ) return
7176
7277 if ( ! window . electronAPI ) {
7378 // Fallback for browser - download as file
74- const fileName = `${ projectData . name . replace ( / [ ^ a - z 0 - 9 ] / gi, '-' ) } .buoy.json `
79+ const fileName = `${ projectData . name . replace ( / [ ^ a - z 0 - 9 ] / gi, '-' ) } .tube `
7580 const blob = new Blob ( [ JSON . stringify ( projectData , null , 2 ) ] , { type : 'application/json' } )
7681 const url = URL . createObjectURL ( blob )
7782 const a = document . createElement ( 'a' )
@@ -85,21 +90,83 @@ function App() {
8590 return
8691 }
8792
88- const result = await window . electronAPI . saveConfig ( JSON . stringify ( projectData , null , 2 ) )
93+ // If project has existing path, save directly to that path
94+ if ( project ?. filePath ) {
95+ const result = await window . electronAPI . saveToPath ( JSON . stringify ( projectData , null , 2 ) , project . filePath )
96+ if ( result . success ) {
97+ markProjectSaved ( project . filePath )
98+ setStatusMessage ( `Saved to ${ project . filePath } ` )
99+ setTimeout ( ( ) => setStatusMessage ( '' ) , 3000 )
100+ }
101+ } else {
102+ // Otherwise show save dialog
103+ const result = await window . electronAPI . saveConfig ( JSON . stringify ( projectData , null , 2 ) , `${ projectData . name } .tube` )
104+ if ( result . success && result . filePath ) {
105+ markProjectSaved ( result . filePath )
106+ setStatusMessage ( `Saved to ${ result . filePath } ` )
107+ setTimeout ( ( ) => setStatusMessage ( '' ) , 3000 )
108+ }
109+ }
110+ } , [ exportProject , markProjectSaved , getActiveProject ] )
111+
112+ // Save As... handler - always shows dialog
113+ const handleSaveConfigAs = useCallback ( async ( ) => {
114+ const projectData = exportProject ( )
115+ if ( ! projectData ) return
116+
117+ if ( ! window . electronAPI ) {
118+ // Fallback for browser - same as regular save
119+ const fileName = `${ projectData . name . replace ( / [ ^ a - z 0 - 9 ] / gi, '-' ) } .tube`
120+ const blob = new Blob ( [ JSON . stringify ( projectData , null , 2 ) ] , { type : 'application/json' } )
121+ const url = URL . createObjectURL ( blob )
122+ const a = document . createElement ( 'a' )
123+ a . href = url
124+ a . download = fileName
125+ a . click ( )
126+ URL . revokeObjectURL ( url )
127+ markProjectSaved ( fileName )
128+ setStatusMessage ( 'Project downloaded' )
129+ setTimeout ( ( ) => setStatusMessage ( '' ) , 3000 )
130+ return
131+ }
132+
133+ const result = await window . electronAPI . saveConfig ( JSON . stringify ( projectData , null , 2 ) , `${ projectData . name } .tube` )
89134 if ( result . success && result . filePath ) {
90135 markProjectSaved ( result . filePath )
91136 setStatusMessage ( `Saved to ${ result . filePath } ` )
92137 setTimeout ( ( ) => setStatusMessage ( '' ) , 3000 )
93138 }
94- }
139+ } , [ exportProject , markProjectSaved ] )
140+
141+ // Autosave - only saves if project has existing filePath (has been saved before)
142+ const autoSave = useCallback ( async ( ) => {
143+ const project = getActiveProject ( )
144+ if ( ! project || ! project . modified || ! project . filePath ) return
145+ if ( ! window . electronAPI ) return
146+
147+ const projectData = exportProject ( )
148+ if ( ! projectData ) return
149+
150+ // Save directly to existing path
151+ try {
152+ const result = await window . electronAPI . saveToPath ( JSON . stringify ( projectData , null , 2 ) , project . filePath )
153+ if ( result . success ) {
154+ markProjectSaved ( project . filePath )
155+ setStatusMessage ( 'Autosaved' )
156+ setTimeout ( ( ) => setStatusMessage ( '' ) , 2000 )
157+ }
158+ } catch {
159+ // Silent fail for autosave
160+ }
161+ } , [ getActiveProject , exportProject , markProjectSaved ] )
95162
96163 // Load config handler
97164 const handleLoadConfig = async ( ) => {
98165 if ( ! window . electronAPI ) {
99166 // Fallback for browser - use file input
100167 const input = document . createElement ( 'input' )
101168 input . type = 'file'
102- input . accept = '.json,.buoy .json'
169+ input . accept = '.tube, .json'
103170 input . onchange = async ( e ) => {
104171 const file = ( e . target as HTMLInputElement ) . files ?. [ 0 ]
105172 if ( file ) {
@@ -191,7 +258,7 @@ function App() {
191258 const url = URL . createObjectURL ( blob )
192259 const a = document . createElement ( 'a' )
193260 a . href = url
194- a . download = 'buoyancy -results.csv'
261+ a . download = 'tubes -results.csv'
195262 a . click ( )
196263 URL . revokeObjectURL ( url )
197264 setStatusMessage ( 'Results downloaded' )
@@ -230,6 +297,30 @@ function App() {
230297 return cleanup
231298 } , [ toggleSidebar ] )
232299
300+ // Autosave effect - debounced save when project is modified
301+ useEffect ( ( ) => {
302+ const project = projects . find ( p => p . id === activeProjectId )
303+ if ( ! autoSaveEnabled || ! project ?. modified || ! project ?. filePath ) {
304+ return
305+ }
306+
307+ // Clear any existing timer
308+ if ( autoSaveTimerRef . current ) {
309+ clearTimeout ( autoSaveTimerRef . current )
310+ }
311+
312+ // Set a new timer for autosave (2 second debounce)
313+ autoSaveTimerRef . current = window . setTimeout ( ( ) => {
314+ autoSave ( )
315+ } , 2000 )
316+
317+ return ( ) => {
318+ if ( autoSaveTimerRef . current ) {
319+ clearTimeout ( autoSaveTimerRef . current )
320+ }
321+ }
322+ } , [ autoSaveEnabled , projects , activeProjectId , autoSave ] )
323+
233324 // Keyboard shortcuts
234325 useEffect ( ( ) => {
235326 const handleKeyDown = ( e : KeyboardEvent ) => {
@@ -245,7 +336,11 @@ function App() {
245336 break
246337 case 's' :
247338 e . preventDefault ( )
248- handleSaveConfig ( )
339+ if ( e . shiftKey ) {
340+ handleSaveConfigAs ( )
341+ } else {
342+ handleSaveConfig ( )
343+ }
249344 break
250345 case 'e' :
251346 e . preventDefault ( )
@@ -261,7 +356,7 @@ function App() {
261356
262357 window . addEventListener ( 'keydown' , handleKeyDown )
263358 return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown )
264- } , [ toggleSidebar , results ] )
359+ } , [ toggleSidebar , results , handleSaveConfig , handleSaveConfigAs , newProject ] )
265360
266361 // Run optimization
267362 const runOptimization = ( ) => {
@@ -287,7 +382,10 @@ function App() {
287382 diameterStepMm : config . diameterStepMm ,
288383 lengthStepMm : config . lengthStepMm ,
289384 waterDensity : config . waterDensity ,
290- box : config . box
385+ box : config . box ,
386+ forcedWallThicknessMm : config . forcedWallThicknessMm ,
387+ forcedEndcapThicknessMm : config . forcedEndcapThicknessMm ,
388+ endcapConstraint : config . endcapConstraint
291389 } )
292390
293391 const selectedCount = getSelectedMaterialKeys ( ) . length
@@ -304,6 +402,7 @@ function App() {
304402 onNew = { newProject }
305403 onOpen = { handleLoadConfig }
306404 onSave = { handleSaveConfig }
405+ onSaveAs = { handleSaveConfigAs }
307406 onExport = { handleExportResults }
308407 onToggleSidebar = { toggleSidebar }
309408 onOpenRecent = { handleOpenRecent }
@@ -330,7 +429,7 @@ function App() {
330429 { /* Toolbar */ }
331430 < div className = "h-10 bg-vsc-bg-dark border-b border-vsc-border flex items-center justify-between px-4 flex-shrink-0" >
332431 < div className = "text-sm text-vsc-fg-dim" >
333- { statusMessage || 'Optimize cylinder dimensions for maximum buoyancy ' }
432+ { statusMessage || 'Everything we currently know about tubes ' }
334433 </ div >
335434 < button
336435 onClick = { runOptimization }
0 commit comments