···2626 self.refresh_token = self.settings:readSetting("refresh_token")
2727 self.did = self.settings:readSetting("did")
2828 self.document_mappings = self.settings:readSetting("document_mappings") or {}
2929+ self.offline_queue = self.settings:readSetting("offline_queue") or {}
2930 self.auto_sync_enabled = self.settings:readSetting("auto_sync_enabled")
3031 if self.auto_sync_enabled == nil then
3132 self.auto_sync_enabled = false -- Default OFF for safety
···5657 -- Validate session at startup if we have credentials
5758 if self:isAuthenticated() then
5859 self:validateSessionAtStartup()
6060+6161+ -- Try to flush any queued syncs from previous sessions
6262+ self:flushOfflineQueue()
5963 end
6064end
6165···6771 self.settings:saveSetting("refresh_token", self.refresh_token)
6872 self.settings:saveSetting("did", self.did)
6973 self.settings:saveSetting("document_mappings", self.document_mappings)
7474+ self.settings:saveSetting("offline_queue", self.offline_queue)
7075 self.settings:saveSetting("auto_sync_enabled", self.auto_sync_enabled)
7176 self.settings:flush()
7277end
···155160 {
156161 text_func = function()
157162 local doc_path = self:getCurrentDocumentPath()
163163+164164+ -- Check if there's a queued sync for this document
165165+ if doc_path and self.offline_queue[doc_path] then
166166+ local queued = self.offline_queue[doc_path]
167167+ return T(_("Last synced: Queued - offline (%1%)"), queued.percent)
168168+ end
169169+158170 if
159171 doc_path
160172 and self.document_mappings[doc_path]
···475487 return
476488 end
477489478478- local mapping = self.document_mappings[doc_path]
490490+ -- Cancel any pending auto-sync for current document (manual sync takes priority)
491491+ if self.auto_sync_task then
492492+ UIManager:unschedule(self.auto_sync_task)
493493+ self.auto_sync_task = nil
494494+ end
479495480496 -- Get document statistics
481497 local pages = self.ui.document:getPageCount()
···491507 end
492508 local percent = math.floor((current_page / pages) * 100)
493509510510+ -- Try to sync
511511+ local success = self:attemptSync(doc_path, percent, current_page, pages)
512512+513513+ if success then
514514+ -- Show success notification (silent for auto-sync)
515515+ if not use_notification then
516516+ -- Use InfoMessage for manual sync and document close
517517+ UIManager:show(InfoMessage:new({
518518+ text = T(_("Synced: %1% (%2/%3)"), percent, current_page, pages),
519519+ timeout = 2,
520520+ }))
521521+ end
522522+523523+ -- On success, try to flush any queued syncs
524524+ self:flushOfflineQueue()
525525+ else
526526+ -- Queue this sync for later (silent)
527527+ self:queueOfflineSync(doc_path, percent, current_page, pages)
528528+ end
529529+end
530530+531531+-- Helper function to detect network errors
532532+function Paperbnd:isNetworkError(error_msg)
533533+ if not error_msg then
534534+ return false
535535+ end
536536+ local error_lower = string.lower(error_msg)
537537+ return string.find(error_lower, "connection") ~= nil
538538+ or string.find(error_lower, "timeout") ~= nil
539539+ or string.find(error_lower, "unreachable") ~= nil
540540+ or string.find(error_lower, "network") ~= nil
541541+ or string.find(error_lower, "no address associated") ~= nil
542542+end
543543+544544+-- Attempt to sync progress, returns true on success, false on network error
545545+function Paperbnd:attemptSync(doc_path, percent, current_page, pages)
546546+ local mapping = self.document_mappings[doc_path]
547547+ if not mapping then
548548+ return false
549549+ end
550550+494551 -- Fetch current record
495552 local record_response, err = self.xrpc:getRecord(self.did, "social.popfeed.feed.listItem", mapping.rkey)
496553497554 if err then
498498- UIManager:show(InfoMessage:new({
499499- text = T(_("Failed to fetch record: %1"), err),
500500- }))
501501- return
555555+ if self:isNetworkError(err) then
556556+ -- Network error - return false to trigger queuing
557557+ return false
558558+ else
559559+ -- Other error (auth, data, etc.) - show error and return false
560560+ UIManager:show(InfoMessage:new({
561561+ text = T(_("Failed to fetch record: %1"), err),
562562+ }))
563563+ return false
564564+ end
502565 end
503566504567 local record = record_response.value
···512575 updatedAt = os.date("!%Y-%m-%dT%H:%M:%S.000Z"),
513576 }
514577515515- -- If not already in currently_reading, update listType
516516- -- if record.listType ~= "currently_reading_books" then
517517- -- record.listType = "currently_reading_books"
518518- -- TODO: Also update the listUri to point to currently_reading list
519519- -- end
520520-521578 -- Put updated record
522579 local _res, put_err = self.xrpc:putRecord(self.did, "social.popfeed.feed.listItem", mapping.rkey, record)
523580524581 if put_err then
525525- UIManager:show(InfoMessage:new({
526526- text = T(_("Failed to sync progress: %1"), put_err),
527527- }))
528528- return
582582+ if self:isNetworkError(put_err) then
583583+ -- Network error - return false to trigger queuing
584584+ return false
585585+ else
586586+ -- Other error - show error and return false
587587+ UIManager:show(InfoMessage:new({
588588+ text = T(_("Failed to sync progress: %1"), put_err),
589589+ }))
590590+ return false
591591+ end
529592 end
530593531531- -- Record last sync info for status display (per-document)
594594+ -- Success! Update last sync info
532595 self.document_mappings[doc_path].last_sync_time = os.time()
533596 self.document_mappings[doc_path].last_sync_percent = percent
534597 self:saveSettings()
535598536536- -- Show success notification (silent for auto-sync)
537537- if not use_notification then
538538- -- Use InfoMessage for manual sync and document close
599599+ return true
600600+end
601601+602602+-- Queue a sync for offline processing
603603+function Paperbnd:queueOfflineSync(doc_path, percent, current_page, pages)
604604+ local mapping = self.document_mappings[doc_path]
605605+ if not mapping then
606606+ return
607607+ end
608608+609609+ -- Cancel any pending auto-sync (this queued sync is newer)
610610+ if self.auto_sync_task then
611611+ UIManager:unschedule(self.auto_sync_task)
612612+ self.auto_sync_task = nil
613613+ end
614614+615615+ -- Store or update queue entry (latest only per document)
616616+ self.offline_queue[doc_path] = {
617617+ percent = percent,
618618+ current_page = current_page,
619619+ total_pages = pages,
620620+ timestamp = os.time(),
621621+ rkey = mapping.rkey,
622622+ title = mapping.title,
623623+ author = mapping.author,
624624+ }
625625+626626+ self:saveSettings()
627627+end
628628+629629+-- Flush all queued syncs (called after successful sync)
630630+function Paperbnd:flushOfflineQueue()
631631+ if not self:isAuthenticated() then
632632+ return
633633+ end
634634+635635+ local queue_count = 0
636636+ for _ in pairs(self.offline_queue) do
637637+ queue_count = queue_count + 1
638638+ end
639639+640640+ if queue_count == 0 then
641641+ return
642642+ end
643643+644644+ local flushed_count = 0
645645+ local failed_paths = {}
646646+ local current_doc_path = self:getCurrentDocumentPath()
647647+648648+ -- Try to sync each queued item
649649+ for doc_path, queue_entry in pairs(self.offline_queue) do
650650+ local percent, current_page, pages
651651+652652+ -- If this is the current document, use latest progress instead of queued
653653+ if doc_path == current_doc_path and self.ui.document then
654654+ pages = self.ui.document:getPageCount()
655655+ if self.ui.paging then
656656+ current_page = self.ui.paging.current_page
657657+ elseif self.ui.rolling then
658658+ current_page = self.ui.rolling.current_page
659659+ else
660660+ current_page = 1
661661+ end
662662+ percent = math.floor((current_page / pages) * 100)
663663+ else
664664+ -- Use queued progress for other documents
665665+ percent = queue_entry.percent
666666+ current_page = queue_entry.current_page
667667+ pages = queue_entry.total_pages
668668+ end
669669+670670+ local success = self:attemptSync(doc_path, percent, current_page, pages)
671671+ if success then
672672+ -- Remove from queue on success
673673+ self.offline_queue[doc_path] = nil
674674+ flushed_count = flushed_count + 1
675675+ else
676676+ -- Keep in queue on failure
677677+ table.insert(failed_paths, doc_path)
678678+ end
679679+ end
680680+681681+ -- Save updated queue
682682+ if flushed_count > 0 then
683683+ self:saveSettings()
684684+685685+ -- Show notification for flushed syncs
539686 UIManager:show(InfoMessage:new({
540540- text = T(_("Synced: %1% (%2/%3)"), percent, current_page, pages),
687687+ text = T(_("Flushed %1 queued sync(s)"), flushed_count),
541688 timeout = 2,
542689 }))
543690 end
544544- -- Auto-sync is completely silent (use_notification = true)
545691end
546692547693function Paperbnd:scheduleDebouncedSync(pageno)