···991010local XRPCClient = require("xrpc")
11111212-local Paperbnd = WidgetContainer:extend {
1313- name = "paperbnd",
1414- is_doc_only = false,
1515-}
1212+local Paperbnd = WidgetContainer:extend({
1313+ name = "paperbnd",
1414+ is_doc_only = false,
1515+})
16161717function Paperbnd:init()
1818- self.ui.menu:registerToMainMenu(self)
1818+ self.ui.menu:registerToMainMenu(self)
19192020- -- Load settings
2121- self.settings = LuaSettings:open(DataStorage:getSettingsDir() .. "/paperbnd.lua")
2222- self.handle = self.settings:readSetting("handle")
2323- self.app_password = self.settings:readSetting("app_password")
2424- self.pds_url = self.settings:readSetting("pds_url")
2525- self.access_token = self.settings:readSetting("access_token")
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.auto_sync_enabled = self.settings:readSetting("auto_sync_enabled")
3030- if self.auto_sync_enabled == nil then
3131- self.auto_sync_enabled = false -- Default OFF for safety
3232- end
2020+ -- Load settings
2121+ self.settings = LuaSettings:open(DataStorage:getSettingsDir() .. "/paperbnd.lua")
2222+ self.handle = self.settings:readSetting("handle")
2323+ self.app_password = self.settings:readSetting("app_password")
2424+ self.pds_url = self.settings:readSetting("pds_url")
2525+ self.access_token = self.settings:readSetting("access_token")
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.auto_sync_enabled = self.settings:readSetting("auto_sync_enabled")
3030+ if self.auto_sync_enabled == nil then
3131+ self.auto_sync_enabled = false -- Default OFF for safety
3232+ end
33333434- -- Auto-sync state tracking
3535- self.auto_sync_task = nil -- Scheduled sync task reference
3636- self.last_synced_page = nil -- Prevent duplicate syncs
3737- self.auto_sync_delay = 10.0 -- 10 second debounce delay
3434+ -- Auto-sync state tracking
3535+ self.auto_sync_task = nil -- Scheduled sync task reference
3636+ self.last_synced_page = nil -- Prevent duplicate syncs
3737+ self.auto_sync_delay = 10.0 -- 10 second debounce delay
38383939- -- Initialize XRPC client
4040- self.xrpc = XRPCClient:new()
4141- if self.pds_url then
4242- self.xrpc:setPDS(self.pds_url)
4343- end
4444- if self.access_token then
4545- self.xrpc:setAuth(self.access_token, self.refresh_token)
4646- end
4747- if self.handle and self.app_password then
4848- self.xrpc:setCredentials(self.handle, self.app_password)
4949- end
3939+ -- Initialize XRPC client
4040+ self.xrpc = XRPCClient:new()
4141+ if self.pds_url then
4242+ self.xrpc:setPDS(self.pds_url)
4343+ end
4444+ if self.access_token then
4545+ self.xrpc:setAuth(self.access_token, self.refresh_token)
4646+ end
4747+ if self.handle and self.app_password then
4848+ self.xrpc:setCredentials(self.handle, self.app_password)
4949+ end
50505151- -- Set up callback for credential updates
5252- self.xrpc:setCredentialsCallback(function(access_token, refresh_token)
5353- self:onTokenRefresh(access_token, refresh_token)
5454- end)
5151+ -- Set up callback for credential updates
5252+ self.xrpc:setCredentialsCallback(function(access_token, refresh_token)
5353+ self:onTokenRefresh(access_token, refresh_token)
5454+ end)
55555656- -- Validate session at startup if we have credentials
5757- if self:isAuthenticated() then
5858- self:validateSessionAtStartup()
5959- end
5656+ -- Validate session at startup if we have credentials
5757+ if self:isAuthenticated() then
5858+ self:validateSessionAtStartup()
5959+ end
6060end
61616262function Paperbnd:saveSettings()
6363- self.settings:saveSetting("handle", self.handle)
6464- self.settings:saveSetting("app_password", self.app_password)
6565- self.settings:saveSetting("pds_url", self.pds_url)
6666- self.settings:saveSetting("access_token", self.access_token)
6767- self.settings:saveSetting("refresh_token", self.refresh_token)
6868- self.settings:saveSetting("did", self.did)
6969- self.settings:saveSetting("document_mappings", self.document_mappings)
7070- self.settings:saveSetting("auto_sync_enabled", self.auto_sync_enabled)
7171- self.settings:flush()
6363+ self.settings:saveSetting("handle", self.handle)
6464+ self.settings:saveSetting("app_password", self.app_password)
6565+ self.settings:saveSetting("pds_url", self.pds_url)
6666+ self.settings:saveSetting("access_token", self.access_token)
6767+ self.settings:saveSetting("refresh_token", self.refresh_token)
6868+ self.settings:saveSetting("did", self.did)
6969+ self.settings:saveSetting("document_mappings", self.document_mappings)
7070+ self.settings:saveSetting("auto_sync_enabled", self.auto_sync_enabled)
7171+ self.settings:flush()
7272end
73737474function Paperbnd:onTokenRefresh(access_token, refresh_token)
7575- self.access_token = access_token
7676- self.refresh_token = refresh_token
7777- self:saveSettings()
7575+ self.access_token = access_token
7676+ self.refresh_token = refresh_token
7777+ self:saveSettings()
7878end
79798080function Paperbnd:validateSessionAtStartup()
8181- -- Validate session asynchronously to avoid blocking plugin startup
8282- -- This will trigger automatic renewal if the session is expired
8383- -- Session validation errors are intentionally ignored at startup
8484- -- renewSession will be called automatically on the next authenticated request
8585- self.xrpc:validateSession()
8181+ -- Validate session asynchronously to avoid blocking plugin startup
8282+ -- This will trigger automatic renewal if the session is expired
8383+ -- Session validation errors are intentionally ignored at startup
8484+ -- renewSession will be called automatically on the next authenticated request
8585+ self.xrpc:validateSession()
8686end
87878888function Paperbnd:addToMainMenu(menu_items)
8989- menu_items.paperbnd = {
9090- text = _("Paperbnd"),
9191- sub_item_table = {
9292- {
9393- text = _("Set credentials"),
9494- keep_menu_open = true,
9595- callback = function()
9696- self:setCredentials()
9797- end,
9898- },
9999- {
100100- text = _("Link current book"),
101101- enabled_func = function()
102102- return self:isAuthenticated() and self.ui.document ~= nil
103103- end,
104104- callback = function()
105105- self:linkCurrentBook()
106106- end,
107107- },
108108- {
109109- text = _("Sync progress now"),
110110- enabled_func = function()
111111- return self:isAuthenticated() and self:isCurrentBookLinked()
112112- end,
113113- callback = function()
114114- self:syncProgress()
115115- end,
116116- },
117117- {
118118- text = _("Auto-sync on page turn"),
119119- checked_func = function()
120120- return self.auto_sync_enabled
121121- end,
122122- callback = function()
123123- self.auto_sync_enabled = not self.auto_sync_enabled
124124- self:saveSettings()
8989+ menu_items.paperbnd = {
9090+ text = _("Paperbnd"),
9191+ sub_item_table = {
9292+ {
9393+ text = _("Set credentials"),
9494+ keep_menu_open = true,
9595+ callback = function()
9696+ self:setCredentials()
9797+ end,
9898+ },
9999+ {
100100+ text = _("Link current book"),
101101+ enabled_func = function()
102102+ return self:isAuthenticated() and self.ui.document ~= nil
103103+ end,
104104+ callback = function()
105105+ self:linkCurrentBook()
106106+ end,
107107+ },
108108+ {
109109+ text = _("Sync progress now"),
110110+ enabled_func = function()
111111+ return self:isAuthenticated() and self:isCurrentBookLinked()
112112+ end,
113113+ callback = function()
114114+ self:syncProgress()
115115+ end,
116116+ },
117117+ {
118118+ text = _("Auto-sync on page turn"),
119119+ checked_func = function()
120120+ return self.auto_sync_enabled
121121+ end,
122122+ callback = function()
123123+ self.auto_sync_enabled = not self.auto_sync_enabled
124124+ self:saveSettings()
125125126126- -- Cancel pending sync when disabling
127127- if not self.auto_sync_enabled and self.auto_sync_task then
128128- UIManager:unschedule(self.auto_sync_task)
129129- self.auto_sync_task = nil
130130- self.last_synced_page = nil
131131- end
126126+ -- Cancel pending sync when disabling
127127+ if not self.auto_sync_enabled and self.auto_sync_task then
128128+ UIManager:unschedule(self.auto_sync_task)
129129+ self.auto_sync_task = nil
130130+ self.last_synced_page = nil
131131+ end
132132133133- local status = self.auto_sync_enabled and _("enabled") or _("disabled")
134134- UIManager:show(InfoMessage:new {
135135- text = T(_("Auto-sync %1"), status),
136136- timeout = 2,
137137- })
138138- end,
139139- },
140140- {
141141- text = _("Unlink current book"),
142142- enabled_func = function()
143143- return self:isCurrentBookLinked()
144144- end,
145145- callback = function()
146146- self:unlinkCurrentBook()
147147- end,
148148- },
149149- },
150150- }
133133+ local status = self.auto_sync_enabled and _("enabled") or _("disabled")
134134+ UIManager:show(InfoMessage:new({
135135+ text = T(_("Auto-sync %1"), status),
136136+ timeout = 2,
137137+ }))
138138+ end,
139139+ },
140140+ {
141141+ text = _("Unlink current book"),
142142+ enabled_func = function()
143143+ return self:isCurrentBookLinked()
144144+ end,
145145+ callback = function()
146146+ self:unlinkCurrentBook()
147147+ end,
148148+ },
149149+ },
150150+ }
151151end
152152153153function Paperbnd:isAuthenticated()
154154- return self.handle ~= nil and self.access_token ~= nil and self.did ~= nil
154154+ return self.handle ~= nil and self.access_token ~= nil and self.did ~= nil
155155end
156156157157function Paperbnd:getCurrentDocumentPath()
158158- if self.ui.document then
159159- return self.ui.document.file
160160- end
161161- return nil
158158+ if self.ui.document then
159159+ return self.ui.document.file
160160+ end
161161+ return nil
162162end
163163164164function Paperbnd:isCurrentBookLinked()
165165- local doc_path = self:getCurrentDocumentPath()
166166- return doc_path ~= nil and self.document_mappings[doc_path] ~= nil
165165+ local doc_path = self:getCurrentDocumentPath()
166166+ return doc_path ~= nil and self.document_mappings[doc_path] ~= nil
167167end
168168169169function Paperbnd:setCredentials()
170170- local handle_input
171171- handle_input = InputDialog:new {
172172- title = _("Enter your handle"),
173173- input = self.handle or "",
174174- buttons = {
175175- {
176176- {
177177- text = _("Cancel"),
178178- callback = function()
179179- UIManager:close(handle_input)
180180- end,
181181- },
182182- {
183183- text = _("Next"),
184184- is_enter_default = true,
185185- callback = function()
186186- local handle = handle_input:getInputText()
187187- UIManager:close(handle_input)
170170+ local handle_input
171171+ handle_input = InputDialog:new({
172172+ title = _("Enter your handle"),
173173+ input = self.handle or "",
174174+ buttons = {
175175+ {
176176+ {
177177+ text = _("Cancel"),
178178+ callback = function()
179179+ UIManager:close(handle_input)
180180+ end,
181181+ },
182182+ {
183183+ text = _("Next"),
184184+ is_enter_default = true,
185185+ callback = function()
186186+ local handle = handle_input:getInputText()
187187+ UIManager:close(handle_input)
188188189189- if handle and handle ~= "" then
190190- self:setAppPassword(handle)
191191- end
192192- end,
193193- },
194194- },
195195- },
196196- }
197197- UIManager:show(handle_input)
198198- handle_input:onShowKeyboard()
189189+ if handle and handle ~= "" then
190190+ self:setAppPassword(handle)
191191+ end
192192+ end,
193193+ },
194194+ },
195195+ },
196196+ })
197197+ UIManager:show(handle_input)
198198+ handle_input:onShowKeyboard()
199199end
200200201201function Paperbnd:setAppPassword(handle)
202202- local password_input
203203- password_input = InputDialog:new {
204204- title = _("Enter your app password"),
205205- input = self.app_password or "",
206206- text_type = "password",
207207- buttons = {
208208- {
209209- {
210210- text = _("Cancel"),
211211- callback = function()
212212- UIManager:close(password_input)
213213- end,
214214- },
215215- {
216216- text = _("Login"),
217217- is_enter_default = true,
218218- callback = function()
219219- local password = password_input:getInputText()
220220- UIManager:close(password_input)
202202+ local password_input
203203+ password_input = InputDialog:new({
204204+ title = _("Enter your app password"),
205205+ input = self.app_password or "",
206206+ text_type = "password",
207207+ buttons = {
208208+ {
209209+ {
210210+ text = _("Cancel"),
211211+ callback = function()
212212+ UIManager:close(password_input)
213213+ end,
214214+ },
215215+ {
216216+ text = _("Login"),
217217+ is_enter_default = true,
218218+ callback = function()
219219+ local password = password_input:getInputText()
220220+ UIManager:close(password_input)
221221222222- if password and password ~= "" then
223223- self:authenticate(handle, password)
224224- end
225225- end,
226226- },
227227- },
228228- },
229229- }
230230- UIManager:show(password_input)
231231- password_input:onShowKeyboard()
222222+ if password and password ~= "" then
223223+ self:authenticate(handle, password)
224224+ end
225225+ end,
226226+ },
227227+ },
228228+ },
229229+ })
230230+ UIManager:show(password_input)
231231+ password_input:onShowKeyboard()
232232end
233233234234function Paperbnd:authenticate(handle, password)
235235- UIManager:show(InfoMessage:new {
236236- text = _("Authenticating..."),
237237- timeout = 1,
238238- })
235235+ UIManager:show(InfoMessage:new({
236236+ text = _("Authenticating..."),
237237+ timeout = 1,
238238+ }))
239239240240- -- Resolve PDS
241241- local pds_url, pds_err = self.xrpc:resolvePDS(handle)
242242- if pds_err then
243243- UIManager:show(InfoMessage:new {
244244- text = T(_("Failed to resolve PDS: %1"), pds_err),
245245- })
246246- return
247247- end
240240+ -- Resolve PDS
241241+ local pds_url, pds_err = self.xrpc:resolvePDS(handle)
242242+ if pds_err then
243243+ UIManager:show(InfoMessage:new({
244244+ text = T(_("Failed to resolve PDS: %1"), pds_err),
245245+ }))
246246+ return
247247+ end
248248249249- self.pds_url = pds_url
250250- self.xrpc:setPDS(pds_url)
249249+ self.pds_url = pds_url
250250+ self.xrpc:setPDS(pds_url)
251251252252- -- Create session
253253- local session, session_err = self.xrpc:createSession(handle, password)
254254- if session_err then
255255- UIManager:show(InfoMessage:new {
256256- text = T(_("Authentication failed: %1"), session_err),
257257- })
258258- return
259259- end
252252+ -- Create session
253253+ local session, session_err = self.xrpc:createSession(handle, password)
254254+ if session_err then
255255+ UIManager:show(InfoMessage:new({
256256+ text = T(_("Authentication failed: %1"), session_err),
257257+ }))
258258+ return
259259+ end
260260261261- self.handle = handle
262262- self.app_password = password
263263- self.access_token = session.accessJwt
264264- self.refresh_token = session.refreshJwt
265265- self.did = session.did
261261+ self.handle = handle
262262+ self.app_password = password
263263+ self.access_token = session.accessJwt
264264+ self.refresh_token = session.refreshJwt
265265+ self.did = session.did
266266267267- -- Store credentials in XRPC client for automatic session renewal
268268- self.xrpc:setCredentials(handle, password)
267267+ -- Store credentials in XRPC client for automatic session renewal
268268+ self.xrpc:setCredentials(handle, password)
269269270270- self:saveSettings()
270270+ self:saveSettings()
271271272272- UIManager:show(InfoMessage:new {
273273- text = _("Authentication successful!"),
274274- })
272272+ UIManager:show(InfoMessage:new({
273273+ text = _("Authentication successful!"),
274274+ }))
275275end
276276277277function Paperbnd:linkCurrentBook()
278278- if not self:isAuthenticated() then
279279- UIManager:show(InfoMessage:new {
280280- text = _("Please set credentials first"),
281281- })
282282- return
283283- end
278278+ if not self:isAuthenticated() then
279279+ UIManager:show(InfoMessage:new({
280280+ text = _("Please set credentials first"),
281281+ }))
282282+ return
283283+ end
284284285285- UIManager:show(InfoMessage:new {
286286- text = _("Fetching your books..."),
287287- timeout = 1,
288288- })
285285+ UIManager:show(InfoMessage:new({
286286+ text = _("Fetching your books..."),
287287+ timeout = 1,
288288+ }))
289289290290- -- Fetch list items
291291- local response, err = self.xrpc:listRecords(
292292- self.did,
293293- "social.popfeed.feed.listItem",
294294- 100,
295295- nil
296296- )
290290+ -- Fetch list items
291291+ local response, err = self.xrpc:listRecords(self.did, "social.popfeed.feed.listItem", 100, nil)
297292298298- if err then
299299- UIManager:show(InfoMessage:new {
300300- text = T(_("Failed to fetch books: %1"), err),
301301- })
302302- return
303303- end
293293+ if err then
294294+ UIManager:show(InfoMessage:new({
295295+ text = T(_("Failed to fetch books: %1"), err),
296296+ }))
297297+ return
298298+ end
304299300300+ if not response.records or #response.records == 0 then
301301+ UIManager:show(InfoMessage:new({
302302+ text = _("No books found in your account"),
303303+ }))
304304+ return
305305+ end
305306306306- if not response.records or #response.records == 0 then
307307- UIManager:show(InfoMessage:new {
308308- text = _("No books found in your account"),
309309- })
310310- return
311311- end
307307+ -- Filter for books only
308308+ local books = {}
309309+ for _, record in ipairs(response.records) do
310310+ if
311311+ record.value
312312+ and record.value.creativeWorkType == "book"
313313+ and record.value.listType == "currently_reading_books"
314314+ then
315315+ table.insert(books, {
316316+ title = record.value.title or "Unknown",
317317+ author = record.value.mainCredit or "Unknown",
318318+ rkey = record.uri:match("([^/]+)$"),
319319+ record = record.value,
320320+ })
321321+ end
322322+ end
312323313313- -- Filter for books only
314314- local books = {}
315315- for _, record in ipairs(response.records) do
316316- if record.value and record.value.creativeWorkType == "book"
317317- and record.value.listType == "currently_reading_books" then
318318- table.insert(books, {
319319- title = record.value.title or "Unknown",
320320- author = record.value.mainCredit or "Unknown",
321321- rkey = record.uri:match("([^/]+)$"),
322322- record = record.value,
323323- })
324324- end
325325- end
324324+ if #books == 0 then
325325+ UIManager:show(InfoMessage:new({
326326+ text = _("No books found in your account"),
327327+ }))
328328+ return
329329+ end
326330327327-328328- if #books == 0 then
329329- UIManager:show(InfoMessage:new {
330330- text = _("No books found in your account"),
331331- })
332332- return
333333- end
334334-335335- -- Show selection dialog
336336- self:showBookSelectionDialog(books)
331331+ -- Show selection dialog
332332+ self:showBookSelectionDialog(books)
337333end
338334339335function Paperbnd:showBookSelectionDialog(books)
340340- local Menu = require("ui/widget/menu")
341341- local Screen = require("device").screen
336336+ local Menu = require("ui/widget/menu")
337337+ local Screen = require("device").screen
342338343343- local items = {}
344344- for _, book in ipairs(books) do
345345- table.insert(items, {
346346- text = book.title,
347347- subtitle = book.author,
348348- book = book,
349349- })
350350- end
339339+ local items = {}
340340+ for _, book in ipairs(books) do
341341+ table.insert(items, {
342342+ text = book.title,
343343+ subtitle = book.author,
344344+ book = book,
345345+ })
346346+ end
351347352352- local book_menu = Menu:new {
353353- title = _("Select a book"),
354354- item_table = items,
355355- width = Screen:getWidth() - Screen:scaleBySize(100),
356356- height = Screen:getHeight() - Screen:scaleBySize(100),
357357- fullscreen = true,
358358- single_line = false,
359359- onMenuSelect = function(_, item)
360360- self:confirmLinkBook(item.book)
361361- end,
362362- }
348348+ local book_menu = Menu:new({
349349+ title = _("Select a book"),
350350+ item_table = items,
351351+ width = Screen:getWidth() - Screen:scaleBySize(100),
352352+ height = Screen:getHeight() - Screen:scaleBySize(100),
353353+ fullscreen = true,
354354+ single_line = false,
355355+ onMenuSelect = function(_, item)
356356+ self:confirmLinkBook(item.book)
357357+ end,
358358+ })
363359364364- UIManager:show(book_menu)
360360+ UIManager:show(book_menu)
365361end
366362367363function Paperbnd:confirmLinkBook(book)
368368- local doc_path = self:getCurrentDocumentPath()
369369- if not doc_path then
370370- return
371371- end
364364+ local doc_path = self:getCurrentDocumentPath()
365365+ if not doc_path then
366366+ return
367367+ end
372368373373- self.document_mappings[doc_path] = {
374374- rkey = book.rkey,
375375- title = book.title,
376376- author = book.author,
377377- }
369369+ self.document_mappings[doc_path] = {
370370+ rkey = book.rkey,
371371+ title = book.title,
372372+ author = book.author,
373373+ }
378374379379- self:saveSettings()
375375+ self:saveSettings()
380376381381- UIManager:show(InfoMessage:new {
382382- text = T(_("Linked to: %1"), book.title),
383383- })
377377+ UIManager:show(InfoMessage:new({
378378+ text = T(_("Linked to: %1"), book.title),
379379+ }))
384380end
385381386382function Paperbnd:unlinkCurrentBook()
387387- local doc_path = self:getCurrentDocumentPath()
388388- if doc_path and self.document_mappings[doc_path] then
389389- local book_title = self.document_mappings[doc_path].title
390390- self.document_mappings[doc_path] = nil
391391- self:saveSettings()
383383+ local doc_path = self:getCurrentDocumentPath()
384384+ if doc_path and self.document_mappings[doc_path] then
385385+ local book_title = self.document_mappings[doc_path].title
386386+ self.document_mappings[doc_path] = nil
387387+ self:saveSettings()
392388393393- UIManager:show(InfoMessage:new {
394394- text = T(_("Unlinked: %1"), book_title),
395395- })
396396- end
389389+ UIManager:show(InfoMessage:new({
390390+ text = T(_("Unlinked: %1"), book_title),
391391+ }))
392392+ end
397393end
398394399395function Paperbnd:syncProgress()
400400- if not self:isAuthenticated() then
401401- return
402402- end
396396+ if not self:isAuthenticated() then
397397+ return
398398+ end
403399404404- local doc_path = self:getCurrentDocumentPath()
405405- if not doc_path or not self.document_mappings[doc_path] then
406406- return
407407- end
400400+ local doc_path = self:getCurrentDocumentPath()
401401+ if not doc_path or not self.document_mappings[doc_path] then
402402+ return
403403+ end
408404409409- local mapping = self.document_mappings[doc_path]
405405+ local mapping = self.document_mappings[doc_path]
410406411411- -- Get document statistics
412412- local pages = self.ui.document:getPageCount()
413413- local current_page = self.ui.paging and self.ui.paging.current_page or 1
414414- local percent = math.floor((current_page / pages) * 100)
407407+ -- Get document statistics
408408+ local pages = self.ui.document:getPageCount()
409409+ local current_page = self.ui.paging and self.ui.paging.current_page or 1
410410+ local percent = math.floor((current_page / pages) * 100)
415411416416- -- Fetch current record
417417- local record_response, err = self.xrpc:getRecord(
418418- self.did,
419419- "social.popfeed.feed.listItem",
420420- mapping.rkey
421421- )
412412+ -- Fetch current record
413413+ local record_response, err = self.xrpc:getRecord(self.did, "social.popfeed.feed.listItem", mapping.rkey)
422414423423- if err then
424424- UIManager:show(InfoMessage:new {
425425- text = T(_("Failed to fetch record: %1"), err),
426426- })
427427- return
428428- end
415415+ if err then
416416+ UIManager:show(InfoMessage:new({
417417+ text = T(_("Failed to fetch record: %1"), err),
418418+ }))
419419+ return
420420+ end
429421430430- local record = record_response.value
422422+ local record = record_response.value
431423432432- -- Update bookProgress
433433- record.bookProgress = {
434434- status = "in_progress",
435435- percent = percent,
436436- currentPage = current_page,
437437- totalPages = pages,
438438- updatedAt = os.date("!%Y-%m-%dT%H:%M:%S.000Z"),
439439- }
424424+ -- Update bookProgress
425425+ record.bookProgress = {
426426+ status = "in_progress",
427427+ percent = percent,
428428+ currentPage = current_page,
429429+ totalPages = pages,
430430+ updatedAt = os.date("!%Y-%m-%dT%H:%M:%S.000Z"),
431431+ }
440432441441- -- If not already in currently_reading, update listType
442442- -- if record.listType ~= "currently_reading_books" then
443443- -- record.listType = "currently_reading_books"
444444- -- TODO: Also update the listUri to point to currently_reading list
445445- -- end
433433+ -- If not already in currently_reading, update listType
434434+ -- if record.listType ~= "currently_reading_books" then
435435+ -- record.listType = "currently_reading_books"
436436+ -- TODO: Also update the listUri to point to currently_reading list
437437+ -- end
446438447447- -- Put updated record
448448- local _, put_err = self.xrpc:putRecord(
449449- self.did,
450450- "social.popfeed.feed.listItem",
451451- mapping.rkey,
452452- record
453453- )
439439+ -- Put updated record
440440+ local _, put_err = self.xrpc:putRecord(self.did, "social.popfeed.feed.listItem", mapping.rkey, record)
454441455455- if put_err then
456456- UIManager:show(InfoMessage:new {
457457- text = T(_("Failed to sync progress: %1"), put_err),
458458- })
459459- return
460460- end
442442+ if put_err then
443443+ UIManager:show(InfoMessage:new({
444444+ text = T(_("Failed to sync progress: %1"), put_err),
445445+ }))
446446+ return
447447+ end
461448462462- UIManager:show(InfoMessage:new {
463463- text = T(_("Synced: %1% (%2/%3)"), percent, current_page, pages),
464464- timeout = 2,
465465- })
449449+ UIManager:show(InfoMessage:new({
450450+ text = T(_("Synced: %1% (%2/%3)"), percent, current_page, pages),
451451+ timeout = 2,
452452+ }))
466453end
467454468455function Paperbnd:scheduleDebouncedSync(pageno)
469469- -- Only schedule if auto-sync enabled and authenticated/linked
470470- if not self.auto_sync_enabled then
471471- return
472472- end
456456+ -- Only schedule if auto-sync enabled and authenticated/linked
457457+ if not self.auto_sync_enabled then
458458+ return
459459+ end
473460474474- if not self:isAuthenticated() or not self:isCurrentBookLinked() then
475475- return
476476- end
461461+ if not self:isAuthenticated() or not self:isCurrentBookLinked() then
462462+ return
463463+ end
477464478478- -- Cancel any existing scheduled sync
479479- if self.auto_sync_task then
480480- UIManager:unschedule(self.auto_sync_task)
481481- self.auto_sync_task = nil
482482- end
465465+ -- Cancel any existing scheduled sync
466466+ if self.auto_sync_task then
467467+ UIManager:unschedule(self.auto_sync_task)
468468+ self.auto_sync_task = nil
469469+ end
483470484484- -- Create closure-based task for this scheduling
485485- self.auto_sync_task = function()
486486- self:performAutoSync(pageno)
487487- end
471471+ -- Create closure-based task for this scheduling
472472+ self.auto_sync_task = function()
473473+ self:performAutoSync(pageno)
474474+ end
488475489489- -- Schedule sync after debounce delay
490490- UIManager:scheduleIn(self.auto_sync_delay, self.auto_sync_task)
476476+ -- Schedule sync after debounce delay
477477+ UIManager:scheduleIn(self.auto_sync_delay, self.auto_sync_task)
491478end
492479493480function Paperbnd:performAutoSync(pageno)
494494- -- Clear task reference
495495- self.auto_sync_task = nil
481481+ -- Clear task reference
482482+ self.auto_sync_task = nil
496483497497- -- Check if page actually changed since last sync
498498- if self.last_synced_page == pageno then
499499- return
500500- end
484484+ -- Check if page actually changed since last sync
485485+ if self.last_synced_page == pageno then
486486+ return
487487+ end
501488502502- -- Double-check auth and linking
503503- if not self:isAuthenticated() or not self:isCurrentBookLinked() then
504504- return
505505- end
489489+ -- Double-check auth and linking
490490+ if not self:isAuthenticated() or not self:isCurrentBookLinked() then
491491+ return
492492+ end
506493507507- -- Perform sync (reuses existing logic)
508508- self:syncProgress()
494494+ -- Perform sync (reuses existing logic)
495495+ self:syncProgress()
509496510510- -- Track synced page
511511- self.last_synced_page = pageno
497497+ -- Track synced page
498498+ self.last_synced_page = pageno
512499end
513500514501function Paperbnd:onPageUpdate(pageno)
515515- -- Fires on every page turn - debounce to avoid excessive syncing
516516- self:scheduleDebouncedSync(pageno)
502502+ -- Fires on every page turn - debounce to avoid excessive syncing
503503+ self:scheduleDebouncedSync(pageno)
517504518518- -- Return false to allow event propagation
519519- return false
505505+ -- Return false to allow event propagation
506506+ return false
520507end
521508522509-- Hook into document close to sync progress
523510function Paperbnd:onCloseDocument()
524524- -- Cancel any pending auto-sync
525525- if self.auto_sync_task then
526526- UIManager:unschedule(self.auto_sync_task)
527527- self.auto_sync_task = nil
528528- end
511511+ -- Cancel any pending auto-sync
512512+ if self.auto_sync_task then
513513+ UIManager:unschedule(self.auto_sync_task)
514514+ self.auto_sync_task = nil
515515+ end
529516530530- -- Clear page tracking for next document
531531- self.last_synced_page = nil
517517+ -- Clear page tracking for next document
518518+ self.last_synced_page = nil
532519533533- -- Always sync on close (original behavior)
534534- if self:isAuthenticated() and self:isCurrentBookLinked() then
535535- self:syncProgress()
536536- end
520520+ -- Always sync on close (original behavior)
521521+ if self:isAuthenticated() and self:isCurrentBookLinked() then
522522+ self:syncProgress()
523523+ end
537524end
538525539526return Paperbnd
+241-241
xrpc.lua
···66local rapidjson = require("rapidjson")
7788local XRPCClient = {
99- pds_url = nil,
1010- handle = nil,
1111- app_password = nil,
1212- access_token = nil,
1313- refresh_token = nil,
1414- timeout = 30,
1515- on_credentials_updated = nil, -- Callback for when credentials are refreshed
99+ pds_url = nil,
1010+ handle = nil,
1111+ app_password = nil,
1212+ access_token = nil,
1313+ refresh_token = nil,
1414+ timeout = 30,
1515+ on_credentials_updated = nil, -- Callback for when credentials are refreshed
1616}
17171818function XRPCClient:new(o)
1919- o = o or {}
2020- setmetatable(o, self)
2121- self.__index = self
2222- return o
1919+ o = o or {}
2020+ setmetatable(o, self)
2121+ self.__index = self
2222+ return o
2323end
24242525function XRPCClient:setPDS(pds_url)
2626- self.pds_url = pds_url
2626+ self.pds_url = pds_url
2727end
28282929function XRPCClient:setAuth(access_token, refresh_token)
3030- self.access_token = access_token
3131- if refresh_token then
3232- self.refresh_token = refresh_token
3333- end
3030+ self.access_token = access_token
3131+ if refresh_token then
3232+ self.refresh_token = refresh_token
3333+ end
3434end
35353636function XRPCClient:setCredentials(handle, app_password)
3737- self.handle = handle
3838- self.app_password = app_password
3737+ self.handle = handle
3838+ self.app_password = app_password
3939end
40404141function XRPCClient:setCredentialsCallback(callback)
4242- self.on_credentials_updated = callback
4242+ self.on_credentials_updated = callback
4343end
44444545-- Notify that credentials were updated
4646function XRPCClient:notifyCredentialsUpdated(access_token, refresh_token)
4747- if self.on_credentials_updated then
4848- self.on_credentials_updated(access_token, refresh_token)
4949- end
4747+ if self.on_credentials_updated then
4848+ self.on_credentials_updated(access_token, refresh_token)
4949+ end
5050end
51515252function XRPCClient:call(opts)
5353- opts = opts or {}
5454- local method = opts.method
5555- local params = opts.params
5656- local body = opts.body
5757- local skip_renewal = opts.skip_renewal or false
5858- local pds = opts.pds or self.pds_url
5353+ opts = opts or {}
5454+ local method = opts.method
5555+ local params = opts.params
5656+ local body = opts.body
5757+ local skip_renewal = opts.skip_renewal or false
5858+ local pds = opts.pds or self.pds_url
59596060- if not pds then
6161- return nil, "PDS URL not set"
6262- end
6060+ if not pds then
6161+ return nil, "PDS URL not set"
6262+ end
63636464- local endpoint = pds .. "/xrpc/" .. method
6464+ local endpoint = pds .. "/xrpc/" .. method
65656666- -- Add query parameters if provided
6767- if params and next(params) then
6868- local query_parts = {}
6969- for k, v in pairs(params) do
7070- table.insert(query_parts, k .. "=" .. url.escape(tostring(v)))
7171- end
7272- endpoint = endpoint .. "?" .. table.concat(query_parts, "&")
7373- end
6666+ -- Add query parameters if provided
6767+ if params and next(params) then
6868+ local query_parts = {}
6969+ for k, v in pairs(params) do
7070+ table.insert(query_parts, k .. "=" .. url.escape(tostring(v)))
7171+ end
7272+ endpoint = endpoint .. "?" .. table.concat(query_parts, "&")
7373+ end
74747575- local request_body = nil
7676- local source = nil
7777- local is_post = body ~= nil
7878- local headers = {
7979- ["Accept"] = "application/json",
8080- }
7575+ local request_body = nil
7676+ local source = nil
7777+ local is_post = body ~= nil
7878+ local headers = {
7979+ ["Accept"] = "application/json",
8080+ }
81818282- -- Handle request body (POST only)
8383- if is_post then
8484- headers["Content-Type"] = "application/json"
8282+ -- Handle request body (POST only)
8383+ if is_post then
8484+ headers["Content-Type"] = "application/json"
85858686- -- Add authorization header for POST requests if token is set
8787- if self.access_token then
8888- headers["Authorization"] = "Bearer " .. self.access_token
8989- end
8686+ -- Add authorization header for POST requests if token is set
8787+ if self.access_token then
8888+ headers["Authorization"] = "Bearer " .. self.access_token
8989+ end
90909191- local encoded, err = rapidjson.encode(body)
9292- if err then
9393- return nil, "Failed to encode JSON: " .. err
9494- end
9595- request_body = encoded
9696- headers["Content-Length"] = tostring(#request_body)
9797- source = ltn12.source.string(request_body)
9898- end
9191+ local encoded, err = rapidjson.encode(body)
9292+ if err then
9393+ return nil, "Failed to encode JSON: " .. err
9494+ end
9595+ request_body = encoded
9696+ headers["Content-Length"] = tostring(#request_body)
9797+ source = ltn12.source.string(request_body)
9898+ end
9999100100- local sink = {}
101101- local request = {
102102- url = endpoint,
103103- method = is_post and "POST" or "GET",
104104- sink = ltn12.sink.table(sink),
105105- source = source,
106106- headers = headers,
107107- }
100100+ local sink = {}
101101+ local request = {
102102+ url = endpoint,
103103+ method = is_post and "POST" or "GET",
104104+ sink = ltn12.sink.table(sink),
105105+ source = source,
106106+ headers = headers,
107107+ }
108108109109- socketutil:set_timeout(self.timeout, self.timeout)
110110- local code, response_headers = socket.skip(1, http.request(request))
111111- socketutil:reset_timeout()
109109+ socketutil:set_timeout(self.timeout, self.timeout)
110110+ local code, response_headers = socket.skip(1, http.request(request))
111111+ socketutil:reset_timeout()
112112113113- if not code then
114114- return nil, "HTTP request failed: " .. tostring(response_headers)
115115- end
113113+ if not code then
114114+ return nil, "HTTP request failed: " .. tostring(response_headers)
115115+ end
116116117117- local response_body = table.concat(sink)
117117+ local response_body = table.concat(sink)
118118119119- -- Handle authentication errors with automatic session renewal
120120- local is_auth_error = code == 401 or
121121- (code == 400 and response_body and string.find(string.lower(response_body), "expired"))
119119+ -- Handle authentication errors with automatic session renewal
120120+ local is_auth_error = code == 401
121121+ or (code == 400 and response_body and string.find(string.lower(response_body), "expired"))
122122123123- if is_auth_error and not skip_renewal then
124124- -- Try to renew session (refresh token -> app password fallback)
125125- local renewed, renew_err = self:renewSession()
123123+ if is_auth_error and not skip_renewal then
124124+ -- Try to renew session (refresh token -> app password fallback)
125125+ local renewed, renew_err = self:renewSession()
126126127127- if renewed then
128128- -- Session renewed successfully, retry the original request
129129- -- Rebuild request with new authorization header
130130- if is_post and self.access_token then
131131- headers["Authorization"] = "Bearer " .. self.access_token
132132- end
127127+ if renewed then
128128+ -- Session renewed successfully, retry the original request
129129+ -- Rebuild request with new authorization header
130130+ if is_post and self.access_token then
131131+ headers["Authorization"] = "Bearer " .. self.access_token
132132+ end
133133134134- sink = {}
135135- request.sink = ltn12.sink.table(sink)
136136- if is_post then
137137- request.source = ltn12.source.string(request_body)
138138- end
139139- request.headers = headers
134134+ sink = {}
135135+ request.sink = ltn12.sink.table(sink)
136136+ if is_post then
137137+ request.source = ltn12.source.string(request_body)
138138+ end
139139+ request.headers = headers
140140141141- socketutil:set_timeout(self.timeout, self.timeout)
142142- code, response_headers = socket.skip(1, http.request(request))
143143- socketutil:reset_timeout()
141141+ socketutil:set_timeout(self.timeout, self.timeout)
142142+ code, response_headers = socket.skip(1, http.request(request))
143143+ socketutil:reset_timeout()
144144145145- if not code then
146146- return nil, "HTTP request failed after session renewal: " .. tostring(response_headers)
147147- end
145145+ if not code then
146146+ return nil, "HTTP request failed after session renewal: " .. tostring(response_headers)
147147+ end
148148149149- response_body = table.concat(sink)
150150- else
151151- -- Could not renew session
152152- return nil, "Session expired and renewal failed: " .. (renew_err or "unknown error")
153153- end
154154- end
149149+ response_body = table.concat(sink)
150150+ else
151151+ -- Could not renew session
152152+ return nil, "Session expired and renewal failed: " .. (renew_err or "unknown error")
153153+ end
154154+ end
155155156156- -- Handle non-200 responses
157157- if code ~= 200 then
158158- local error_msg
159159- if response_body and response_body ~= "" then
160160- local error_data = rapidjson.decode(response_body)
161161- if error_data and error_data.message then
162162- error_msg = error_data.message
163163- else
164164- error_msg = response_body
165165- end
166166- else
167167- error_msg = "HTTP " .. code
168168- end
156156+ -- Handle non-200 responses
157157+ if code ~= 200 then
158158+ local error_msg
159159+ if response_body and response_body ~= "" then
160160+ local error_data = rapidjson.decode(response_body)
161161+ if error_data and error_data.message then
162162+ error_msg = error_data.message
163163+ else
164164+ error_msg = response_body
165165+ end
166166+ else
167167+ error_msg = "HTTP " .. code
168168+ end
169169170170- return nil, error_msg
171171- end
170170+ return nil, error_msg
171171+ end
172172173173- -- Decode response
174174- if response_body and response_body ~= "" then
175175- local decoded, err = rapidjson.decode(response_body)
176176- if err then
177177- return nil, "Failed to decode JSON response: " .. err
178178- end
179179- return decoded, nil
180180- end
173173+ -- Decode response
174174+ if response_body and response_body ~= "" then
175175+ local decoded, err = rapidjson.decode(response_body)
176176+ if err then
177177+ return nil, "Failed to decode JSON response: " .. err
178178+ end
179179+ return decoded, nil
180180+ end
181181182182- return {}, nil
182182+ return {}, nil
183183end
184184185185-- Resolve PDS from handle using Slingshot service
186186function XRPCClient:resolvePDS(handle)
187187- local response, err = self:call({
188188- method = "com.bad-example.identity.resolveMiniDoc",
189189- params = { identifier = handle },
190190- pds = "https://slingshot.microcosm.blue",
191191- })
187187+ local response, err = self:call({
188188+ method = "com.bad-example.identity.resolveMiniDoc",
189189+ params = { identifier = handle },
190190+ pds = "https://slingshot.microcosm.blue",
191191+ })
192192193193- if err or not response then
194194- return nil, "Failed to resolve PDS for handle: " .. err
195195- end
193193+ if err or not response then
194194+ return nil, "Failed to resolve PDS for handle: " .. err
195195+ end
196196197197- if not response.pds then
198198- return nil, "Invalid response from Slingshot service"
199199- end
197197+ if not response.pds then
198198+ return nil, "Invalid response from Slingshot service"
199199+ end
200200201201- return response.pds, nil
201201+ return response.pds, nil
202202end
203203204204-- Create session (login)
205205function XRPCClient:createSession(identifier, password)
206206- local response, err = self:call({
207207- method = "com.atproto.server.createSession",
208208- body = {
209209- identifier = identifier,
210210- password = password,
211211- },
212212- skip_renewal = true, -- Don't auto-renew during initial login
213213- })
206206+ local response, err = self:call({
207207+ method = "com.atproto.server.createSession",
208208+ body = {
209209+ identifier = identifier,
210210+ password = password,
211211+ },
212212+ skip_renewal = true, -- Don't auto-renew during initial login
213213+ })
214214215215- if err or not response then
216216- return nil, err
217217- end
215215+ if err or not response then
216216+ return nil, err
217217+ end
218218219219- if response.accessJwt then
220220- self:setAuth(response.accessJwt, response.refreshJwt)
221221- -- Note: We don't call notifyCredentialsUpdated here since the caller
222222- -- of createSession is expected to handle storing credentials
223223- end
219219+ if response.accessJwt then
220220+ self:setAuth(response.accessJwt, response.refreshJwt)
221221+ -- Note: We don't call notifyCredentialsUpdated here since the caller
222222+ -- of createSession is expected to handle storing credentials
223223+ end
224224225225- return response, nil
225225+ return response, nil
226226end
227227228228-- Validate current session by making a test request
229229function XRPCClient:validateSession()
230230- if not self.access_token then
231231- return false, "No access token"
232232- end
230230+ if not self.access_token then
231231+ return false, "No access token"
232232+ end
233233234234- -- Make a lightweight request to check if session is valid
235235- local response, err = self:call({
236236- method = "com.atproto.server.getSession",
237237- body = {},
238238- skip_renewal = true, -- Don't auto-renew during validation
239239- })
234234+ -- Make a lightweight request to check if session is valid
235235+ local response, err = self:call({
236236+ method = "com.atproto.server.getSession",
237237+ body = {},
238238+ skip_renewal = true, -- Don't auto-renew during validation
239239+ })
240240241241- return response ~= nil, err
241241+ return response ~= nil, err
242242end
243243244244-- Refresh session using refresh token
245245function XRPCClient:refreshSession()
246246- if not self.refresh_token then
247247- return nil, "No refresh token available"
248248- end
246246+ if not self.refresh_token then
247247+ return nil, "No refresh token available"
248248+ end
249249250250- -- Temporarily store the current access token
251251- local old_access_token = self.access_token
250250+ -- Temporarily store the current access token
251251+ local old_access_token = self.access_token
252252253253- -- Use refresh token for this request
254254- self.access_token = self.refresh_token
253253+ -- Use refresh token for this request
254254+ self.access_token = self.refresh_token
255255256256- local response, err = self:call({
257257- method = "com.atproto.server.refreshSession",
258258- body = {},
259259- skip_renewal = true, -- Don't recurse during refresh
260260- })
256256+ local response, err = self:call({
257257+ method = "com.atproto.server.refreshSession",
258258+ body = {},
259259+ skip_renewal = true, -- Don't recurse during refresh
260260+ })
261261262262- if err or not response then
263263- -- Restore old access token on failure
264264- self.access_token = old_access_token
265265- return nil, err
266266- end
262262+ if err or not response then
263263+ -- Restore old access token on failure
264264+ self.access_token = old_access_token
265265+ return nil, err
266266+ end
267267268268- -- Update tokens with new values
269269- if response.accessJwt then
270270- self:setAuth(response.accessJwt, response.refreshJwt)
271271- self:notifyCredentialsUpdated(response.accessJwt, response.refreshJwt)
272272- end
268268+ -- Update tokens with new values
269269+ if response.accessJwt then
270270+ self:setAuth(response.accessJwt, response.refreshJwt)
271271+ self:notifyCredentialsUpdated(response.accessJwt, response.refreshJwt)
272272+ end
273273274274- return response, nil
274274+ return response, nil
275275end
276276277277-- Renew session with multi-level fallback
278278-- 1. Try to use refresh token
279279-- 2. If refresh token expired, create new session with app password
280280function XRPCClient:renewSession()
281281- -- First, try to refresh with refresh token
282282- local response, err = self:refreshSession()
281281+ -- First, try to refresh with refresh token
282282+ local response, err = self:refreshSession()
283283284284- if response then
285285- return true, nil
286286- end
284284+ if response then
285285+ return true, nil
286286+ end
287287288288- -- If refresh failed and we have app password, try to create new session
289289- if self.handle and self.app_password then
290290- local session, session_err = self:createSession(self.handle, self.app_password)
288288+ -- If refresh failed and we have app password, try to create new session
289289+ if self.handle and self.app_password then
290290+ local session, session_err = self:createSession(self.handle, self.app_password)
291291292292- if session then
293293- -- New session created successfully, tokens already set by createSession
294294- self:notifyCredentialsUpdated(session.accessJwt, session.refreshJwt)
295295- return true, nil
296296- end
292292+ if session then
293293+ -- New session created successfully, tokens already set by createSession
294294+ self:notifyCredentialsUpdated(session.accessJwt, session.refreshJwt)
295295+ return true, nil
296296+ end
297297298298- return false, "Failed to refresh session and create new session: " .. (session_err or "unknown error")
299299- end
298298+ return false, "Failed to refresh session and create new session: " .. (session_err or "unknown error")
299299+ end
300300301301- return false,
302302- "Failed to refresh session: " .. (err or "unknown error") .. ", and no app password available for fallback"
301301+ return false,
302302+ "Failed to refresh session: " .. (err or "unknown error") .. ", and no app password available for fallback"
303303end
304304305305-- List records from a collection
306306function XRPCClient:listRecords(repo, collection, limit, cursor)
307307- local params = {
308308- repo = repo,
309309- collection = collection,
310310- }
307307+ local params = {
308308+ repo = repo,
309309+ collection = collection,
310310+ }
311311312312- if limit then
313313- params.limit = limit
314314- end
312312+ if limit then
313313+ params.limit = limit
314314+ end
315315316316- if cursor then
317317- params.cursor = cursor
318318- end
316316+ if cursor then
317317+ params.cursor = cursor
318318+ end
319319320320- return self:call({
321321- method = "com.atproto.repo.listRecords",
322322- params = params,
323323- })
320320+ return self:call({
321321+ method = "com.atproto.repo.listRecords",
322322+ params = params,
323323+ })
324324end
325325326326-- Get a single record
327327function XRPCClient:getRecord(repo, collection, rkey)
328328- local params = {
329329- repo = repo,
330330- collection = collection,
331331- rkey = rkey,
332332- }
328328+ local params = {
329329+ repo = repo,
330330+ collection = collection,
331331+ rkey = rkey,
332332+ }
333333334334- return self:call({
335335- method = "com.atproto.repo.getRecord",
336336- params = params,
337337- })
334334+ return self:call({
335335+ method = "com.atproto.repo.getRecord",
336336+ params = params,
337337+ })
338338end
339339340340-- Put (create or update) a record
341341function XRPCClient:putRecord(repo, collection, rkey, record)
342342- return self:call({
343343- method = "com.atproto.repo.putRecord",
344344- body = {
345345- repo = repo,
346346- collection = collection,
347347- rkey = rkey,
348348- record = record,
349349- },
350350- })
342342+ return self:call({
343343+ method = "com.atproto.repo.putRecord",
344344+ body = {
345345+ repo = repo,
346346+ collection = collection,
347347+ rkey = rkey,
348348+ record = record,
349349+ },
350350+ })
351351end
352352353353return XRPCClient