@@ -228,17 +228,16 @@ const CollectionAPI = {
228228 }
229229} ;
230230window . CollectionAPI = CollectionAPI ;
231- import { saveBookOffline , removeOfflineBook , db } from './db.js' ;
232231
233232// Example click handler for your custom "Save for Offline" icon
234233async function handleDownloadToggle ( bookCard , bookData ) {
235- const isAlreadyDownloaded = await db . downloadedBooks . get ( bookData . id ) ;
234+ const isAlreadyDownloaded = await window . db . downloadedBooks . get ( bookData . id ) ;
236235
237236 if ( isAlreadyDownloaded ) {
238- const success = await removeOfflineBook ( bookData . id ) ;
237+ const success = await window . removeOfflineBook ( bookData . id ) ;
239238 if ( success ) bookCard . classList . remove ( 'is-downloaded' ) ;
240239 } else {
241- const success = await saveBookOffline ( bookData ) ;
240+ const success = await window . saveBookOffline ( bookData ) ;
242241 if ( success ) bookCard . classList . add ( 'is-downloaded' ) ;
243242 }
244243}
@@ -1864,6 +1863,10 @@ class LibraryManager {
18641863
18651864 // 4. Sync with backend if available (Full Refresh)
18661865 await this . syncWithBackend ( ) ;
1866+ if ( navigator . onLine ) {
1867+ await this . flushPendingLibraryMutations ( ) ;
1868+ }
1869+ await this . updateSyncStatus ( ) ;
18671870 }
18681871
18691872 getUser ( ) {
@@ -1883,6 +1886,194 @@ class LibraryManager {
18831886 return new Headers ( headers ) ;
18841887 }
18851888
1889+ async _getPendingSyncCount ( ) {
1890+ const user = this . getUser ( ) ;
1891+ if ( ! user || ! window . db ?. syncQueue ) return 0 ;
1892+ return await window . db . syncQueue . where ( 'userId' ) . equals ( user . id ) . count ( ) ;
1893+ }
1894+
1895+ async updateSyncStatus ( ) {
1896+ const statusEl = document . getElementById ( 'library-sync-status' ) ;
1897+ if ( ! statusEl ) return ;
1898+
1899+ const pendingCount = await this . _getPendingSyncCount ( ) ;
1900+ statusEl . hidden = false ;
1901+ statusEl . textContent = pendingCount > 0
1902+ ? `${ pendingCount } pending sync${ pendingCount === 1 ? '' : 's' } `
1903+ : 'Synced' ;
1904+ statusEl . dataset . state = pendingCount > 0 ? 'pending' : 'synced' ;
1905+ }
1906+
1907+ async _queueMutation ( action , book , extra = { } ) {
1908+ if ( typeof window . enqueueLibraryMutation !== 'function' ) return ;
1909+
1910+ const user = this . getUser ( ) ;
1911+ if ( ! user || ! window . db ?. syncQueue ) return ;
1912+
1913+ const snapshot = JSON . parse ( JSON . stringify ( book ) ) ;
1914+
1915+ const existingMutations = ( await window . db . syncQueue . where ( 'userId' ) . equals ( user . id ) . toArray ( ) )
1916+ . filter ( ( mutation ) => mutation . bookId === snapshot . id ) ;
1917+
1918+ if ( action === 'remove' ) {
1919+ await Promise . all ( existingMutations . map ( ( mutation ) => window . db . syncQueue . delete ( mutation . id ) ) ) ;
1920+ await this . updateSyncStatus ( ) ;
1921+ return ;
1922+ }
1923+
1924+ const shelf = extra . shelf || this . findBookShelf ( snapshot . id ) || null ;
1925+ const mergedMutation = {
1926+ userId : user . id ,
1927+ action,
1928+ bookId : snapshot . id ,
1929+ db_id : snapshot . db_id || null ,
1930+ shelf,
1931+ payload : extra ,
1932+ book : snapshot
1933+ } ;
1934+
1935+ const pendingAdd = existingMutations . find ( ( mutation ) => mutation . action === 'add' ) ;
1936+ if ( pendingAdd && ( action === 'move' || action === 'update' ) ) {
1937+ await window . db . syncQueue . put ( {
1938+ ...pendingAdd ,
1939+ db_id : mergedMutation . db_id || pendingAdd . db_id || null ,
1940+ shelf : action === 'move' ? extra . toShelf || pendingAdd . shelf : pendingAdd . shelf ,
1941+ payload : {
1942+ ...( pendingAdd . payload || { } ) ,
1943+ ...extra
1944+ } ,
1945+ book : snapshot ,
1946+ createdAt : pendingAdd . createdAt || new Date ( ) . toISOString ( )
1947+ } ) ;
1948+ await this . updateSyncStatus ( ) ;
1949+ return ;
1950+ }
1951+
1952+ if ( action === 'add' ) {
1953+ await Promise . all ( existingMutations . map ( ( mutation ) => window . db . syncQueue . delete ( mutation . id ) ) ) ;
1954+ }
1955+
1956+ await window . enqueueLibraryMutation ( mergedMutation ) ;
1957+ await this . updateSyncStatus ( ) ;
1958+ }
1959+
1960+ async _applyQueuedMutation ( mutation ) {
1961+ const user = this . getUser ( ) ;
1962+ if ( ! user ) return ;
1963+
1964+ const localBookResult = this . findBookInShelf ( mutation . bookId ) ;
1965+ const localBook = localBookResult ?. book || mutation . book ;
1966+ const dbId = localBook ?. db_id || mutation . db_id ;
1967+
1968+ if ( mutation . action === 'add' ) {
1969+ if ( ! localBook ) return ;
1970+
1971+ const payload = {
1972+ user_id : user . id ,
1973+ google_books_id : localBook . id ,
1974+ title : localBook . volumeInfo ?. title || localBook . title || '' ,
1975+ authors : localBook . volumeInfo ?. authors ? localBook . volumeInfo . authors . join ( ', ' ) : '' ,
1976+ thumbnail : localBook . volumeInfo ?. imageLinks ?. thumbnail || '' ,
1977+ shelf_type : mutation . shelf || mutation . payload ?. shelf || 'want'
1978+ } ;
1979+
1980+ const res = await fetch ( `${ this . apiBase } /library` , {
1981+ method : 'POST' ,
1982+ headers : this . getAuthHeaders ( ) ,
1983+ credentials : 'include' ,
1984+ body : JSON . stringify ( payload )
1985+ } ) ;
1986+
1987+ if ( ! res . ok ) {
1988+ const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
1989+ throw new Error ( data . error || `HTTP ${ res . status } ` ) ;
1990+ }
1991+
1992+ const data = await res . json ( ) ;
1993+ if ( localBook ) {
1994+ localBook . db_id = data . item . id ;
1995+ localBook . version = data . item . version ;
1996+ this . saveLocally ( ) ;
1997+ }
1998+ return ;
1999+ }
2000+
2001+ if ( mutation . action === 'remove' ) {
2002+ if ( ! dbId ) return ;
2003+
2004+ const res = await fetch ( `${ this . apiBase } /library/${ dbId } ` , {
2005+ method : 'DELETE' ,
2006+ headers : this . getAuthHeaders ( ) ,
2007+ credentials : 'include'
2008+ } ) ;
2009+
2010+ if ( ! res . ok ) {
2011+ const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
2012+ throw new Error ( data . error || `HTTP ${ res . status } ` ) ;
2013+ }
2014+ return ;
2015+ }
2016+
2017+ if ( mutation . action === 'move' || mutation . action === 'update' ) {
2018+ if ( ! dbId || ! localBook ) return ;
2019+
2020+ const body = mutation . action === 'move'
2021+ ? {
2022+ shelf_type : mutation . payload ?. toShelf ,
2023+ progress : localBook . progress ,
2024+ version : localBook . version
2025+ }
2026+ : {
2027+ ...mutation . payload ?. updates ,
2028+ version : localBook . version
2029+ } ;
2030+
2031+ const res = await fetch ( `${ this . apiBase } /library/${ dbId } ` , {
2032+ method : 'PUT' ,
2033+ headers : this . getAuthHeaders ( ) ,
2034+ credentials : 'include' ,
2035+ body : JSON . stringify ( body )
2036+ } ) ;
2037+
2038+ if ( ! res . ok ) {
2039+ const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
2040+ throw new Error ( data . error || `HTTP ${ res . status } ` ) ;
2041+ }
2042+
2043+ const data = await res . json ( ) ;
2044+ localBook . version = data . item . version ;
2045+ this . saveLocally ( ) ;
2046+ }
2047+ }
2048+
2049+ async flushPendingLibraryMutations ( ) {
2050+ const user = this . getUser ( ) ;
2051+ if ( ! user || ! window . db ?. syncQueue ) {
2052+ await this . updateSyncStatus ( ) ;
2053+ return 0 ;
2054+ }
2055+
2056+ const pendingMutations = await window . db . syncQueue . where ( 'userId' ) . equals ( user . id ) . sortBy ( 'createdAt' ) ;
2057+ if ( pendingMutations . length === 0 ) {
2058+ await this . updateSyncStatus ( ) ;
2059+ return 0 ;
2060+ }
2061+
2062+ let processed = 0 ;
2063+ for ( const mutation of pendingMutations ) {
2064+ await this . _applyQueuedMutation ( mutation ) ;
2065+ await window . db . syncQueue . delete ( mutation . id ) ;
2066+ processed += 1 ;
2067+ }
2068+
2069+ if ( processed > 0 ) {
2070+ await this . syncWithBackend ( ) ;
2071+ }
2072+
2073+ await this . updateSyncStatus ( ) ;
2074+ return processed ;
2075+ }
2076+
18862077 async syncWithBackend ( ) {
18872078 const user = this . getUser ( ) ;
18882079 if ( ! user ) return ;
@@ -1975,6 +2166,7 @@ class LibraryManager {
19752166 this . renderShelf ( 'finished' , 'shelf-finished' ) ;
19762167 }
19772168 }
2169+ await this . updateSyncStatus ( ) ;
19782170 }
19792171 } catch ( e ) {
19802172 console . error ( "Sync failed" , e ) ;
@@ -2032,6 +2224,7 @@ class LibraryManager {
20322224
20332225 // After upload, pull fresh state from backend to get the new DB IDs and versions
20342226 await this . syncWithBackend ( ) ;
2227+ await this . updateSyncStatus ( ) ;
20352228 } else {
20362229 const data = await res . json ( ) ;
20372230 console . error ( "Backend refused sync" , data ) ;
@@ -2201,10 +2394,12 @@ class LibraryManager {
22012394 enrichedBook . db_id = data . item . id ;
22022395 enrichedBook . version = data . item . version ;
22032396 this . saveLocally ( ) ;
2397+ await this . updateSyncStatus ( ) ;
22042398 }
22052399 } catch ( e ) {
22062400 console . error ( "Failed to save to backend" , e ) ;
2207- showToast ( "Saved locally (Sync failed)" , "info" ) ;
2401+ await this . _queueMutation ( 'add' , enrichedBook , { shelf } ) ;
2402+ showToast ( "Saved locally; sync queued" , "info" ) ;
22082403 }
22092404 }
22102405 }
@@ -2250,6 +2445,7 @@ class LibraryManager {
22502445 const data = await res . json ( ) ;
22512446 book . version = data . item . version ;
22522447 this . saveLocally ( ) ;
2448+ await this . updateSyncStatus ( ) ;
22532449 } else if ( res . status === 409 ) {
22542450 const data = await res . json ( ) ;
22552451 showToast ( "Conflict detected! Syncing with server..." , "error" ) ;
@@ -2261,7 +2457,8 @@ class LibraryManager {
22612457 }
22622458 } catch ( e ) {
22632459 console . error ( "Failed to update backend" , e ) ;
2264- showToast ( "Saved locally (Sync failed)" , "info" ) ;
2460+ await this . _queueMutation ( 'update' , book , { updates } ) ;
2461+ showToast ( "Saved locally; sync queued" , "info" ) ;
22652462 }
22662463 }
22672464 }
@@ -2325,9 +2522,11 @@ class LibraryManager {
23252522 headers : this . getAuthHeaders ( ) ,
23262523 credentials : 'include'
23272524 } ) ;
2525+ await this . updateSyncStatus ( ) ;
23282526 } catch ( e ) {
23292527 console . error ( "Failed to delete from backend" , e ) ;
2330- showToast ( "Removed locally (Backend sync failed)" , "info" ) ;
2528+ await this . _queueMutation ( 'remove' , book , { shelf } ) ;
2529+ showToast ( "Removed locally; sync queued" , "info" ) ;
23312530 }
23322531 } else if ( user ) {
23332532 // Fallback: If we don't have db_id locally (maybe added before login logic),
@@ -2379,6 +2578,7 @@ class LibraryManager {
23792578 const data = await res . json ( ) ;
23802579 book . version = data . item . version ;
23812580 this . saveLocally ( ) ;
2581+ await this . updateSyncStatus ( ) ;
23822582 } else if ( res . status === 409 ) {
23832583 showToast ( "Conflict detected! Syncing with server..." , "error" ) ;
23842584 await this . syncWithBackend ( ) ;
@@ -2389,10 +2589,12 @@ class LibraryManager {
23892589 }
23902590 } catch ( e ) {
23912591 console . error ( "Failed to update backend during move" , e ) ;
2392- showToast ( "Moved locally (Sync failed)" , "info" ) ;
2592+ await this . _queueMutation ( 'move' , book , { fromShelf, toShelf } ) ;
2593+ showToast ( "Moved locally; sync queued" , "info" ) ;
23932594 }
23942595 }
23952596
2597+ await this . updateSyncStatus ( ) ;
23962598 return true ;
23972599 }
23982600
0 commit comments