@@ -14,8 +14,11 @@ import { useTheme } from './src/theme';
1414import { hardwareService , modelManager , authService , ragService , remoteServerManager } from './src/services' ;
1515import logger from './src/utils/logger' ;
1616import { useAppStore , useAuthStore , useRemoteServerStore } from './src/stores' ;
17+ import { hydrateDownloadStore } from './src/services/downloadHydration' ;
18+ import { useDownloads } from './src/hooks/useDownloads' ;
1719import { LockScreen } from './src/screens' ;
1820import { useAppState } from './src/hooks/useAppState' ;
21+ import { useDownloadStore } from './src/stores/downloadStore' ;
1922
2023LogBox . ignoreAllLogs ( ) ; // Suppress all logs
2124
@@ -28,12 +31,12 @@ const ensureRemoteServerStoreHydrated = async () => {
2831} ;
2932
3033function App ( ) {
34+ useDownloads ( ) ;
3135 const [ isInitializing , setIsInitializing ] = useState ( true ) ;
3236 const setDeviceInfo = useAppStore ( ( s ) => s . setDeviceInfo ) ;
3337 const setModelRecommendation = useAppStore ( ( s ) => s . setModelRecommendation ) ;
3438 const setDownloadedModels = useAppStore ( ( s ) => s . setDownloadedModels ) ;
3539 const setDownloadedImageModels = useAppStore ( ( s ) => s . setDownloadedImageModels ) ;
36- const clearImageModelDownloading = useAppStore ( ( s ) => s . clearImageModelDownloading ) ;
3740
3841 const { colors, isDark } = useTheme ( ) ;
3942
@@ -44,6 +47,27 @@ function App() {
4447 setLastBackgroundTime,
4548 } = useAuthStore ( ) ;
4649
50+ const reattachTextDownloadRecovery = useCallback ( async ( ) => {
51+ const restoredIds = await modelManager . restoreInProgressDownloads ( ) ;
52+ modelManager . startBackgroundDownloadPolling ( ) ;
53+ restoredIds . forEach ( ( downloadId ) => {
54+ modelManager . watchDownload (
55+ downloadId ,
56+ async ( ) => {
57+ const models = await modelManager . getDownloadedModels ( ) ;
58+ setDownloadedModels ( models ) ;
59+ useDownloadStore . getState ( ) . remove (
60+ useDownloadStore . getState ( ) . downloadIdIndex [ downloadId ] ?? '' ,
61+ ) ;
62+ } ,
63+ ( error : Error ) => {
64+ logger . error ( '[App] Restored text download failed:' , error ) ;
65+ useDownloadStore . getState ( ) . setStatus ( downloadId , 'failed' , { message : error . message } ) ;
66+ } ,
67+ ) ;
68+ } ) ;
69+ } , [ setDownloadedModels ] ) ;
70+
4771 // Handle app state changes for auto-lock
4872 useAppState ( {
4973 onBackground : useCallback ( ( ) => {
@@ -53,29 +77,39 @@ function App() {
5377 }
5478 } , [ authEnabled , setLastBackgroundTime , setLocked ] ) ,
5579 onForeground : useCallback ( ( ) => {
56- // Lock is already set when going to background
57- // Nothing additional needed here
58- } , [ ] ) ,
80+ // Rebuild the unified store before reattaching JS listeners so restored
81+ // progress events map onto current download entries instead of racing hydration.
82+ hydrateDownloadStore ( )
83+ . catch ( ( error ) => {
84+ logger . error ( '[App] Failed to hydrate download store on foreground:' , error ) ;
85+ } )
86+ . finally ( ( ) => {
87+ reattachTextDownloadRecovery ( ) . catch ( ( error ) => {
88+ logger . error ( '[App] Failed to restore text downloads on foreground:' , error ) ;
89+ } ) ;
90+ } ) ;
91+ } , [ reattachTextDownloadRecovery ] ) ,
5992 } ) ;
6093
61- useEffect ( ( ) => {
62- initializeApp ( ) ;
63-
64- } , [ ] ) ;
65-
66- const ensureAppStoreHydrated = async ( ) => {
94+ const ensureAppStoreHydrated = useCallback ( async ( ) => {
6795 const persistApi = useAppStore . persist ;
6896 if ( ! persistApi ?. hasHydrated || ! persistApi . rehydrate ) return ;
6997 if ( ! persistApi . hasHydrated ( ) ) {
7098 await persistApi . rehydrate ( ) ;
7199 }
72- } ;
100+ } , [ ] ) ;
73101
74- const initializeApp = async ( ) => {
102+ const initializeApp = useCallback ( async ( ) => {
75103 try {
76104 // Ensure persisted download metadata is loaded before restore logic reads it.
77105 await ensureAppStoreHydrated ( ) ;
78106
107+ // Hydrate download store from SQLite before any screen mounts.
108+ await hydrateDownloadStore ( ) . catch ( ( error ) => {
109+ logger . error ( '[App] Failed to hydrate download store during startup:' , error ) ;
110+ } ) ;
111+ await reattachTextDownloadRecovery ( ) ;
112+
79113 // Phase 1: Quick initialization - get app ready to show UI
80114 // Initialize hardware detection
81115 const deviceInfo = await hardwareService . getDeviceInfo ( ) ;
@@ -90,84 +124,25 @@ function App() {
90124 // Clean up any mmproj files that were incorrectly added as standalone models
91125 await modelManager . cleanupMMProjEntries ( ) ;
92126
93- // Wire up background download metadata persistence
94- const {
95- setBackgroundDownload,
96- activeBackgroundDownloads,
97- addDownloadedModel,
98- setDownloadProgress,
99- } = useAppStore . getState ( ) ;
100- modelManager . setBackgroundDownloadMetadataCallback ( ( downloadId , info ) => {
101- setBackgroundDownload ( downloadId , info ) ;
127+ // Reconcile image model directories that finished extracting on disk but
128+ // whose AsyncStorage registration was lost to an app kill. Runs before
129+ // refreshModelLists so the recovered models are included in the initial
130+ // setDownloadedImageModels call. activeModelIds guards against touching
131+ // directories that are currently being downloaded/extracted.
132+ const activeImageModelIds = new Set (
133+ Object . values ( useDownloadStore . getState ( ) . downloads )
134+ . filter ( e => e . modelType === 'image' )
135+ . map ( e => e . modelId . replace ( 'image:' , '' ) ) ,
136+ ) ;
137+ await modelManager . reconcileFinishedImageDownloads ( activeImageModelIds ) . catch ( ( error ) => {
138+ logger . error ( '[App] Image model reconciliation failed:' , error ) ;
102139 } ) ;
103140
104- // Recover any background downloads that completed while app was dead
105- try {
106- const recoveredModels = await modelManager . syncBackgroundDownloads (
107- activeBackgroundDownloads ,
108- ( downloadId ) => setBackgroundDownload ( downloadId , null )
109- ) ;
110- for ( const model of recoveredModels ) {
111- addDownloadedModel ( model ) ;
112- logger . log ( '[App] Recovered background download:' , model . name ) ;
113- }
114- } catch ( err ) {
115- logger . error ( '[App] Failed to sync background downloads:' , err ) ;
116- }
117-
118- // Recover completed image downloads (zip unzip / multifile finalization)
119- try {
120- const recoveredImageModels = await modelManager . syncCompletedImageDownloads (
121- activeBackgroundDownloads ,
122- ( downloadId ) => setBackgroundDownload ( downloadId , null ) ,
123- ) ;
124- for ( const model of recoveredImageModels ) {
125- logger . log ( '[App] Recovered image download:' , model . name ) ;
126- }
127- } catch ( err ) {
128- logger . error ( '[App] Failed to sync completed image downloads:' , err ) ;
129- }
130-
131- // Re-wire event listeners for downloads that were still running when the
132- // app was killed (running/pending status in Android DownloadManager).
133- try {
134- const restoredDownloadIds = await modelManager . restoreInProgressDownloads (
135- activeBackgroundDownloads ,
136- ( progress ) => {
137- const key = `${ progress . modelId } /${ progress . fileName } ` ;
138- setDownloadProgress ( key , {
139- progress : progress . progress ,
140- bytesDownloaded : progress . bytesDownloaded ,
141- totalBytes : progress . totalBytes ,
142- } ) ;
143- } ,
144- ) ;
145- for ( const downloadId of restoredDownloadIds ) {
146- const metadata = activeBackgroundDownloads [ downloadId ] ;
147- const progressKey = metadata ? `${ metadata . modelId } /${ metadata . fileName } ` : null ;
148- modelManager . watchDownload (
149- downloadId ,
150- ( model ) => {
151- if ( progressKey ) setDownloadProgress ( progressKey , null ) ;
152- addDownloadedModel ( model ) ;
153- logger . log ( '[App] Restored in-progress download completed:' , model . name ) ;
154- } ,
155- ( error ) => {
156- if ( progressKey ) setDownloadProgress ( progressKey , null ) ;
157- logger . error ( '[App] Restored in-progress download failed:' , error ) ;
158- } ,
159- ) ;
160- }
161- } catch ( err ) {
162- logger . error ( '[App] Failed to restore in-progress downloads:' , err ) ;
163- }
164-
165- // Clear any stale imageModelDownloading entries — if the app was killed
166- // mid-download these would be persisted as "downloading" forever.
167- clearImageModelDownloading ( ) ;
168-
169141 // Scan for any models that may have been downloaded externally or
170- // when app was killed before JS callback fired
142+ // while the app was killed. hydrateDownloadStore (called on cold start
143+ // and foreground resume) repopulates in-flight downloads directly
144+ // from the native Room DB, replacing the old metadata-callback +
145+ // syncBackgroundDownloads recovery path.
171146 const { textModels, imageModels } = await modelManager . refreshModelLists ( ) ;
172147 setDownloadedModels ( textModels ) ;
173148 setDownloadedImageModels ( imageModels ) ;
@@ -200,7 +175,20 @@ function App() {
200175 logger . error ( '[App] Error initializing app:' , error ) ;
201176 setIsInitializing ( false ) ;
202177 }
203- } ;
178+ } , [
179+ authEnabled ,
180+ ensureAppStoreHydrated ,
181+ reattachTextDownloadRecovery ,
182+ setDeviceInfo ,
183+ setDownloadedImageModels ,
184+ setDownloadedModels ,
185+ setLocked ,
186+ setModelRecommendation ,
187+ ] ) ;
188+
189+ useEffect ( ( ) => {
190+ initializeApp ( ) ;
191+ } , [ initializeApp ] ) ;
204192
205193 const handleUnlock = useCallback ( ( ) => {
206194 setLocked ( false ) ;
0 commit comments