diff --git a/apps/readest-app/src/app/library/components/KOSyncSettings.tsx b/apps/readest-app/src/app/library/components/KOSyncSettings.tsx index 368ea531..c3f21689 100644 --- a/apps/readest-app/src/app/library/components/KOSyncSettings.tsx +++ b/apps/readest-app/src/app/library/components/KOSyncSettings.tsx @@ -108,6 +108,7 @@ export const KOSyncSettingsWindow: React.FC = () => { [settings.koreaderSyncUserkey], ); + // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedSaveDeviceName = useCallback( debounce((newDeviceName: string) => { const newSettings = { ...settings, koreaderSyncDeviceName: newDeviceName }; diff --git a/apps/readest-app/src/app/reader/hooks/useKOSync.ts b/apps/readest-app/src/app/reader/hooks/useKOSync.ts index acace5a7..92aabea4 100644 --- a/apps/readest-app/src/app/reader/hooks/useKOSync.ts +++ b/apps/readest-app/src/app/reader/hooks/useKOSync.ts @@ -7,14 +7,12 @@ import { useReaderStore } from '@/store/readerStore'; import { useBookDataStore } from '@/store/bookDataStore'; import { useTranslation } from '@/hooks/useTranslation'; import { KOSyncClient, KoSyncProgress } from '@/services/sync/KOSyncClient'; -import { Book, BookFormat } from '@/types/book'; +import { Book, FIXED_LAYOUT_FORMATS } from '@/types/book'; import { BookDoc } from '@/libs/document'; import { debounce } from '@/utils/debounce'; import { eventDispatcher } from '@/utils/event'; import { getCFIFromXPointer, XCFI } from '@/utils/xcfi'; -const PAGINATED_FORMATS: Set = new Set(['PDF', 'CBZ']); - type SyncState = 'idle' | 'checking' | 'conflict' | 'synced' | 'error'; export interface SyncDetails { @@ -62,7 +60,7 @@ export const useKOSync = (bookKey: string) => { let progressStr: string; let percentage: number; - if (PAGINATED_FORMATS.has(currentBook.format)) { + if (FIXED_LAYOUT_FORMATS.has(currentBook.format)) { const page = (currentProgress.section?.current ?? 0) + 1; const totalPages = currentProgress.section?.total ?? 0; progressStr = page.toString(); @@ -223,12 +221,12 @@ export const useKOSync = (bookKey: string) => { const localTimestamp = bookData?.config?.updatedAt || book.updatedAt; const remoteIsNewer = remote.timestamp * 1000 > localTimestamp; - const localIdentifier = PAGINATED_FORMATS.has(book.format) + const localIdentifier = FIXED_LAYOUT_FORMATS.has(book.format) ? progress.section?.current.toString() : progress.location; const isLocalCFI = localIdentifier?.startsWith('epubcfi'); - const remoteIdentifier = PAGINATED_FORMATS.has(book.format) + const remoteIdentifier = FIXED_LAYOUT_FORMATS.has(book.format) ? (parseInt(remote.progress, 10) - 1).toString() : remote.progress.startsWith('epubcfi') ? remote.progress @@ -265,7 +263,7 @@ export const useKOSync = (bookKey: string) => { const applyRemoteProgress = async () => { const view = getView(bookKey); if (view && remote.progress) { - if (PAGINATED_FORMATS.has(book.format)) { + if (FIXED_LAYOUT_FORMATS.has(book.format)) { const pageToGo = parseInt(remote.progress, 10); if (!isNaN(pageToGo)) view.select(pageToGo - 1); } else { @@ -312,7 +310,7 @@ export const useKOSync = (bookKey: string) => { let remotePreview = ''; const remotePercentage = remote.percentage || 0; - if (PAGINATED_FORMATS.has(book.format)) { + if (FIXED_LAYOUT_FORMATS.has(book.format)) { const localPageInfo = progress.section; const localPercentage = localPageInfo && localPageInfo.total > 0 @@ -427,7 +425,7 @@ export const useKOSync = (bookKey: string) => { const bookDoc = conflictDetails?.bookDoc; if (view && remote?.progress && currentBook) { - if (PAGINATED_FORMATS.has(currentBook.format)) { + if (FIXED_LAYOUT_FORMATS.has(currentBook.format)) { const localTotalPages = getProgress(bookKey)?.section?.total ?? 0; const remotePage = parseInt(remote.progress, 10); const remotePercentage = remote.percentage || 0; diff --git a/apps/readest-app/src/app/reader/hooks/useProgressSync.ts b/apps/readest-app/src/app/reader/hooks/useProgressSync.ts index 721ef4b7..84a48ee4 100644 --- a/apps/readest-app/src/app/reader/hooks/useProgressSync.ts +++ b/apps/readest-app/src/app/reader/hooks/useProgressSync.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { useAuth } from '@/context/AuthContext'; import { useSync } from '@/hooks/useSync'; -import { BookConfig } from '@/types/book'; +import { BookConfig, FIXED_LAYOUT_FORMATS } from '@/types/book'; import { useBookDataStore } from '@/store/bookDataStore'; import { useReaderStore } from '@/store/readerStore'; import { useSettingsStore } from '@/store/settingsStore'; @@ -11,15 +11,15 @@ import { CFI } from '@/libs/document'; import { debounce } from '@/utils/debounce'; import { eventDispatcher } from '@/utils/event'; import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants'; +import { getCFIFromXPointer, getXPointerFromCFI } from '@/utils/xcfi'; export const useProgressSync = (bookKey: string) => { const _ = useTranslation(); - const { getConfig, setConfig } = useBookDataStore(); + const { getConfig, setConfig, getBookData } = useBookDataStore(); const { getView, getProgress } = useReaderStore(); const { settings } = useSettingsStore(); const { syncedConfigs, syncConfigs } = useSync(bookKey); const { user } = useAuth(); - const view = getView(bookKey); const config = getConfig(bookKey); const progress = getProgress(bookKey); @@ -29,24 +29,38 @@ export const useProgressSync = (bookKey: string) => { const pushConfig = (bookKey: string, config: BookConfig | null) => { if (!config || !user) return; const bookHash = bookKey.split('-')[0]!; - const newConfig = { bookHash, ...config }; + const newConfig = { ...config, bookHash }; const compressedConfig = JSON.parse( serializeConfig(newConfig, settings.globalViewSettings, DEFAULT_BOOK_SEARCH_CONFIG), ); delete compressedConfig.booknotes; syncConfigs([compressedConfig], bookHash, 'push'); }; + const pullConfig = (bookKey: string) => { if (!user) return; const bookHash = bookKey.split('-')[0]!; syncConfigs([], bookHash, 'pull'); }; - const syncConfig = () => { + + const syncConfig = async () => { if (!configPulled.current) { pullConfig(bookKey); } else { const config = getConfig(bookKey); - if (config && config.progress && config.progress[0] > 0) { + const view = getView(bookKey); + const book = getBookData(bookKey)?.book; + if (config && view && book && config.progress && config.progress[0] > 0) { + try { + const content = view.renderer.getContents()[0]; + if (content && !FIXED_LAYOUT_FORMATS.has(book.format)) { + const { doc, index } = content; + const xpointerResult = await getXPointerFromCFI(config.location!, doc, index || 0); + config.xpointer = xpointerResult.xpointer; + } + } catch (error) { + console.error('Failed to convert CFI to XPointer', error); + } pushConfig(bookKey, config); } } @@ -91,28 +105,51 @@ export const useProgressSync = (bookKey: string) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [progress]); - // Pull: proccess the pulled progress - useEffect(() => { - if (!configPulled.current && syncedConfigs) { - configPulled.current = true; - const syncedConfig = syncedConfigs.filter((c) => c.bookHash === bookKey.split('-')[0])[0]; - if (syncedConfig) { - const configCFI = config?.location; - const syncedCFI = syncedConfig.location; - setConfig(bookKey, syncedConfig); - if (syncedCFI && configCFI) { - if (CFI.compare(configCFI, syncedCFI) < 0) { - if (view) { - view.goTo(syncedCFI); - eventDispatcher.dispatch('hint', { - bookKey, - message: _('Reading Progress Synced'), - }); - } + const applyRemoteProgress = useCallback(async () => { + if (!syncedConfigs || syncedConfigs.length === 0) return; + const syncedConfig = syncedConfigs.filter((c) => c.bookHash === bookKey.split('-')[0])[0]; + if (syncedConfig) { + const configCFI = config?.location; + let remoteCFILocation = syncedConfig.location; + const xPointer = syncedConfig.xpointer; + const bookData = getBookData(bookKey); + const view = getView(bookKey); + if (xPointer && view && bookData && bookData.bookDoc) { + const content = view.renderer.getContents()[0]; + const candidateCFI = await getCFIFromXPointer( + xPointer, + content?.doc, + content?.index, + bookData.bookDoc, + ); + if (CFI.compare(remoteCFILocation, candidateCFI) < 0) { + remoteCFILocation = candidateCFI; + } + } + setConfig(bookKey, syncedConfig); + if (remoteCFILocation && configCFI) { + if (CFI.compare(configCFI, remoteCFILocation) < 0) { + if (view) { + view.goTo(remoteCFILocation); + eventDispatcher.dispatch('hint', { + bookKey, + message: _('Reading Progress Synced'), + }); } } } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [syncedConfigs]); + }, [syncedConfigs, config?.location]); + + // Pull: proccess the pulled progress + useEffect(() => { + if (!configPulled.current && syncedConfigs) { + configPulled.current = true; + applyRemoteProgress().catch((error) => { + console.error('Failed to apply remote progress', error); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [applyRemoteProgress]); }; diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index be874201..f2daec21 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -5,6 +5,8 @@ export type BookNoteType = 'bookmark' | 'annotation' | 'excerpt'; export type HighlightStyle = 'highlight' | 'underline' | 'squiggly'; export type HighlightColor = 'red' | 'yellow' | 'green' | 'blue' | 'violet'; +export const FIXED_LAYOUT_FORMATS: Set = new Set(['PDF', 'CBZ']); + export interface Book { // if Book is a remote book we just lazy load the book content via url url?: string; @@ -221,7 +223,8 @@ export interface BookSearchResult { export interface BookConfig { bookHash?: string; progress?: [number, number]; // [current pagenum, total pagenum], 1-based page number - location?: string; + location?: string; // CFI of the current location + xpointer?: string; // XPointer of the current location (for Koreader interoperability) booknotes?: BookNote[]; searchConfig?: Partial; viewSettings?: Partial; diff --git a/apps/readest-app/src/types/records.ts b/apps/readest-app/src/types/records.ts index 9c248c50..20090a90 100644 --- a/apps/readest-app/src/types/records.ts +++ b/apps/readest-app/src/types/records.ts @@ -21,6 +21,7 @@ export interface DBBookConfig { user_id: string; book_hash: string; location?: string; + xpointer?: string; progress?: string; search_config?: string; view_settings?: string; diff --git a/apps/readest-app/src/utils/transform.ts b/apps/readest-app/src/utils/transform.ts index d68b8fd3..7a71ea00 100644 --- a/apps/readest-app/src/utils/transform.ts +++ b/apps/readest-app/src/utils/transform.ts @@ -11,13 +11,14 @@ import { DBBookConfig, DBBook, DBBookNote } from '@/types/records'; import { sanitizeString } from './sanitize'; export const transformBookConfigToDB = (bookConfig: unknown, userId: string): DBBookConfig => { - const { bookHash, progress, location, searchConfig, viewSettings, updatedAt } = + const { bookHash, progress, location, xpointer, searchConfig, viewSettings, updatedAt } = bookConfig as BookConfig; return { user_id: userId, book_hash: bookHash!, location: location, + xpointer: xpointer, progress: progress && JSON.stringify(progress), search_config: searchConfig && JSON.stringify(searchConfig), view_settings: viewSettings && JSON.stringify(viewSettings), @@ -26,10 +27,12 @@ export const transformBookConfigToDB = (bookConfig: unknown, userId: string): DB }; export const transformBookConfigFromDB = (dbBookConfig: DBBookConfig): BookConfig => { - const { book_hash, progress, location, search_config, view_settings, updated_at } = dbBookConfig; + const { book_hash, progress, location, xpointer, search_config, view_settings, updated_at } = + dbBookConfig; return { bookHash: book_hash, location, + xpointer, progress: progress && JSON.parse(progress), searchConfig: search_config && JSON.parse(search_config), viewSettings: view_settings && JSON.parse(view_settings), diff --git a/apps/readest-app/src/utils/xcfi.ts b/apps/readest-app/src/utils/xcfi.ts index 055c869c..1888852a 100644 --- a/apps/readest-app/src/utils/xcfi.ts +++ b/apps/readest-app/src/utils/xcfi.ts @@ -502,13 +502,13 @@ export class XCFI { export const getCFIFromXPointer = async ( xpointer: string, - doc: Document, - index: number, + doc?: Document, + index?: number, bookDoc?: BookDoc, ) => { const xSpineIndex = XCFI.extractSpineIndex(xpointer); let converter: XCFI; - if (index === xSpineIndex) { + if (index === xSpineIndex && doc) { converter = new XCFI(doc, index || 0); } else { const doc = await bookDoc?.sections?.[xSpineIndex]?.createDocument(); @@ -519,3 +519,23 @@ export const getCFIFromXPointer = async ( const cfi = converter.xPointerToCFI(xpointer); return cfi; }; + +export const getXPointerFromCFI = async ( + cfi: string, + doc?: Document, + index?: number, + bookDoc?: BookDoc, +): Promise => { + const xSpineIndex = XCFI.extractSpineIndex(cfi); + let converter: XCFI; + if (index === xSpineIndex && doc) { + converter = new XCFI(doc, index || 0); + } else { + const doc = await bookDoc?.sections?.[xSpineIndex]?.createDocument(); + if (!doc) throw new Error('Failed to load document for CFI conversion.'); + converter = new XCFI(doc, xSpineIndex || 0); + } + + const xpointer = converter.cfiToXPointer(cfi); + return xpointer; +}; diff --git a/apps/readest.koplugin/main.lua b/apps/readest.koplugin/main.lua index fcbdadb9..40d77982 100644 --- a/apps/readest.koplugin/main.lua +++ b/apps/readest.koplugin/main.lua @@ -1,10 +1,10 @@ local Device = require("device") local Event = require("ui/event") local InfoMessage = require("ui/widget/infomessage") +local WidgetContainer = require("ui/widget/container/widgetcontainer") local MultiInputDialog = require("ui/widget/multiinputdialog") local NetworkMgr = require("ui/network/manager") local UIManager = require("ui/uimanager") -local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") local time = require("ui/time") local util = require("util") @@ -19,7 +19,7 @@ local ReadestSync = WidgetContainer:new{ settings = nil, } -local API_CALL_DEBOUNCE_DELAY = time.s(30) +local API_CALL_DEBOUNCE_DELAY = 30 local SUPABAE_ANON_KEY_BASE64 = "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6WlNJc0luSmxaaUk2SW5aaWMzbDRablZ6YW1weFpIaHJhbkZzZVhOaklpd2ljbTlzWlNJNkltRnViMjRpTENKcFlYUWlPakUzTXpReE1qTTJOekVzSW1WNGNDSTZNakEwT1RZNU9UWTNNWDAuM1U1VXFhb3VfMVNnclZlMWVvOXJBcGMwdUtqcWhwUWRVWGh2d1VIbVVmZw==" ReadestSync.default_settings = { @@ -27,6 +27,7 @@ ReadestSync.default_settings = { supabase_anon_key = sha2.base64_to_bin(SUPABAE_ANON_KEY_BASE64), auto_sync = false, user_email = nil, + user_name = nil, user_id = nil, access_token = nil, refresh_token = nil, @@ -47,6 +48,29 @@ function ReadestSync:onDispatcherRegisterActions() -- end +function ReadestSync:needsLogin() + return not self.settings.access_token or not self.settings.expires_at + or self.settings.expires_at < os.time() + 60 +end + +function ReadestSync:tryRefreshToken() + if self.settings.refresh_token and self.settings.expires_at + and self.settings.expires_at < os.time() - self.settings.expires_in / 2 then + local client = self:getSupabaseAuthClient() + client:refresh_token(self.settings.refresh_token, function(success, response) + if success then + self.settings.access_token = response.access_token + self.settings.refresh_token = response.refresh_token + self.settings.expires_at = response.expires_at + self.settings.expires_in = response.expires_in + G_reader_settings:writeSetting("readest_sync", self.settings) + else + logger.err("ReadestSync: Token refresh failed:", response or "Unknown error") + end + end) + end +end + function ReadestSync:addToMainMenu(menu_items) menu_items.readest_sync = { sorting_hint = "tools", @@ -54,17 +78,17 @@ function ReadestSync:addToMainMenu(menu_items) sub_item_table = { { text_func = function() - return self.settings.access_token and (_("Logout")) - or _("Login with Readest Account") + return self:needsLogin() and _("Log in Readest Account") + or _("Log out as ") .. (self.settings.user_name or "") end, callback_func = function() - if self.settings.access_token then + if self:needsLogin() then return function(menu) - self:logout(menu) + self:login(menu) end else return function(menu) - self:login(menu) + self:logout(menu) end end end, @@ -200,10 +224,12 @@ function ReadestSync:doLogin(email, password, menu) if success then self.settings.user_email = email self.settings.user_id = response.user.id + self.settings.user_name = response.user.user_metadata.user_name or email self.settings.access_token = response.access_token self.settings.refresh_token = response.refresh_token self.settings.expires_at = response.expires_at self.settings.expires_in = response.expires_in + G_reader_settings:writeSetting("readest_sync", self.settings) if menu then menu:updateItems() @@ -235,6 +261,7 @@ function ReadestSync:logout(menu) self.settings.refresh_token = nil self.settings.expires_at = nil self.settings.expires_in = nil + G_reader_settings:writeSetting("readest_sync", self.settings) if menu then menu:updateItems() @@ -250,26 +277,78 @@ function ReadestSync:getDocumentIdentifier() return self.ui.doc_settings:readSetting("partial_md5_checksum") end +function ReadestSync:showSyncedMessage() + UIManager:show(InfoMessage:new{ + text = _("Progress has been synchronized."), + timeout = 3, + }) +end + function ReadestSync:applyBookConfig(config) logger.dbg("ReadestSync: Applying book config:", config) - local location_xp = config.location_xp + local xpointer = config.xpointer local progress = config.progress + local has_pages = self.ui.document.info.has_pages -- Check if it's the bracket format: [page,total_pages] local progress_pattern = "^%[(%d+),(%d+)%]$" - local page, total_pages = progress:match(progress_pattern) - if location_xp then - -- TODO - return - end - if page and total_pages then - local percentage = tonumber(page) / tonumber(total_pages) - local current_page = self.ui.document:getCurrentPage() - local page_count = self.ui.document:getPageCount() - if page_count > 0 and current_page / page_count < percentage then + if has_pages and progress then + local page, total_pages = progress:match(progress_pattern) + local current_page = self.ui:getCurrentPage() + local new_page = tonumber(page) + if new_page > current_page then self.ui.link:addCurrentLocationToStack() - self.ui:handleEvent(Event:new("GotoPercent", percentage * 100)) + self.ui:handleEvent(Event:new("GotoPage", new_page)) + self:showSyncedMessage() end end + if not has_pages and xpointer then + local last_xpointer = self.ui.rolling:getLastProgress() + local working_xpointer = xpointer + local cmp_result = self.document:compareXPointers(last_xpointer, working_xpointer) + -- FIXME: Crengine is not very good at comparing XPointers, so we need to reduce the path + while cmp_result == nil and working_xpointer do + local last_slash_pos = working_xpointer:match("^.*()/") + if last_slash_pos and last_slash_pos > 1 then + working_xpointer = working_xpointer:sub(1, last_slash_pos - 1) + cmp_result = self.document:compareXPointers(last_xpointer, working_xpointer) + else + break + end + end + if cmp_result > 0 then + self.ui.link:addCurrentLocationToStack() + self.ui:handleEvent(Event:new("GotoXPointer", working_xpointer)) + self:showSyncedMessage() + end + end +end + +function ReadestSync:getCurrentBookConfig() + local book_hash = self:getDocumentIdentifier() + if not book_hash then + UIManager:show(InfoMessage:new{ + text = _("Cannot identify the current book"), + timeout = 2, + }) + return nil + end + + local config = { + bookHash = book_hash, + progress = "", + xpointer = "", + updatedAt = os.time() * 1000, + } + + local current_page = self.ui:getCurrentPage() + local page_count = self.ui.document:getPageCount() + config.progress = {current_page, page_count} + + if not self.ui.document.info.has_pages then + config.xpointer = self.ui.rolling:getLastProgress() + end + + return config end function ReadestSync:pushBookConfig(interactive) @@ -283,15 +362,12 @@ function ReadestSync:pushBookConfig(interactive) return end - local now = UIManager:getElapsedTimeSinceBoot() + local now = os.time() if not interactive and now - self.last_sync_timestamp <= API_CALL_DEBOUNCE_DELAY then logger.dbg("ReadestSync: Debouncing push request") return end - local book_hash = self:getDocumentIdentifier() - if not book_hash then return end - local config = self:getCurrentBookConfig() if not config then return end @@ -299,26 +375,19 @@ function ReadestSync:pushBookConfig(interactive) return end - -- Use Supabase REST API to upsert book config - local url = self.settings.supabase_url .. "/rest/v1/book_configs" - local payload = { - user_id = self.user_id, - hash = document_id, - config = config, - updated_at = os.date("!%Y-%m-%dT%H:%M:%SZ") - } - local client = self:getReadestSyncClient() if not client then if interactive then UIManager:show(InfoMessage:new{ - text = _("Please configure Supabase settings first"), + text = _("Please configure Readest settings first"), timeout = 3, }) end return end + self:tryRefreshToken() + if interactive then UIManager:show(InfoMessage:new{ text = _("Pushing book config..."), @@ -326,10 +395,15 @@ function ReadestSync:pushBookConfig(interactive) }) end + local payload = { + books = {}, + notes = {}, + configs = { config } + } + client:pushChanges( - config, + payload, function(success, response) - logger.dbg("ReadestSync: Push result:", success, response) if interactive then if success then UIManager:show(InfoMessage:new{ @@ -344,7 +418,7 @@ function ReadestSync:pushBookConfig(interactive) end end if success then - self.last_sync_timestamp = time.now() + self.last_sync_timestamp = os.time() end end ) @@ -373,13 +447,15 @@ function ReadestSync:pullBookConfig(interactive) if not client then if interactive then UIManager:show(InfoMessage:new{ - text = _("Please configure Supabase settings first"), + text = _("Please configure Readest settings first"), timeout = 3, }) end return end + self:tryRefreshToken() + if interactive then UIManager:show(InfoMessage:new{ text = _("Pulling book config..."), @@ -394,8 +470,17 @@ function ReadestSync:pullBookConfig(interactive) book = book_hash, }, function(success, response) - logger.dbg("ReadestSync: Pull result:", success, response) if not success then + if response and response.error == "Not authenticated" then + if interactive then + UIManager:show(InfoMessage:new{ + text = _("Authentication failed, please login again"), + timeout = 2, + }) + end + self:logout() + return + end if interactive then UIManager:show(InfoMessage:new{ text = _("Failed to pull book config"), @@ -448,8 +533,9 @@ end function ReadestSync:onPageUpdate(page) if self.settings.auto_sync and self.settings.access_token and page then - -- Schedule a delayed push to avoid too frequent updates - UIManager:unschedule(self.delayed_push_task) + if self.delayed_push_task then + UIManager:unschedule(self.delayed_push_task) + end self.delayed_push_task = function() self:pushBookConfig(false) end diff --git a/apps/readest.koplugin/readest-sync-api.json b/apps/readest.koplugin/readest-sync-api.json index af6a0347..0cf8b8d5 100644 --- a/apps/readest.koplugin/readest-sync-api.json +++ b/apps/readest.koplugin/readest-sync-api.json @@ -1,33 +1,19 @@ { - "base_url": "http://localhost:3000/api", - "name": "readest-sync-api", - "methods": { - "pullChanges": { - "path": "/sync", - "method": "GET", - "required_params": [ - "since", - "type", - "book" - ], - "expected_status": [ - 200, - 400, - 401 - ] - }, - "pushChanges": { - "path": "/sync", - "method": "POST", - "required_params": [], - "payload": [], - "expected_status": [ - 200, - 201, - 400, - 401, - 422 - ] + "base_url": "http://web.readest.com/api", + "name": "readest-sync-api", + "methods": { + "pullChanges": { + "path": "/sync", + "method": "GET", + "required_params": ["since", "type", "book"], + "expected_status": [200, 400, 401, 403] + }, + "pushChanges": { + "path": "/sync", + "method": "POST", + "required_params": ["books", "notes", "configs"], + "payload": ["books", "notes", "configs"], + "expected_status": [200, 201, 400, 401, 403] + } } - } -} \ No newline at end of file +} diff --git a/apps/readest.koplugin/readestsync.lua b/apps/readest.koplugin/readestsync.lua index 8dc6facf..0ae89585 100644 --- a/apps/readest.koplugin/readestsync.lua +++ b/apps/readest.koplugin/readestsync.lua @@ -96,23 +96,28 @@ function ReadestSyncClient:pullChanges(params, callback) socketutil:reset_timeout() end -function ReadestSyncClient:pushChanges(changes) +function ReadestSyncClient:pushChanges(changes, callback) self.client:reset_middlewares() self.client:enable("Format.JSON") self.client:enable("ReadestHeaders", {}) self.client:enable("ReadestAuth", {}) socketutil:set_timeout(SYNC_TIMEOUTS[1], SYNC_TIMEOUTS[2]) - local ok, res = pcall(function() - return self.client:pushChanges(changes or {}) + local co = coroutine.create(function() + local ok, res = pcall(function() + return self.client:pushChanges(changes or {}) + end) + if ok then + callback(res.status == 200, res.body) + else + logger.dbg("ReadestSyncClient:pushChanges failure:", res) + callback(false, res.body) + end end) + self.client:enable("AsyncHTTP", {thread = co}) + coroutine.resume(co) + if UIManager.looper then UIManager:setInputTimeout() end socketutil:reset_timeout() - if ok then - return res.status == 200 or res.status == 201, res.body - else - logger.dbg("ReadestSyncClient:pushChanges failure:", res) - return false, res.body - end end return ReadestSyncClient \ No newline at end of file diff --git a/apps/readest.koplugin/supabase-auth-api.json b/apps/readest.koplugin/supabase-auth-api.json index 9ca02934..60f94bb8 100644 --- a/apps/readest.koplugin/supabase-auth-api.json +++ b/apps/readest.koplugin/supabase-auth-api.json @@ -1,57 +1,33 @@ { - "base_url": "https://readest.supabase.co/auth/v1/", - "name": "supabase-auth-api", - "methods": { - "sign_in_password": { - "path": "/token?grant_type=password", - "method": "POST", - "required_params": [ - "email", - "password" - ], - "payload": [ - "email", - "password" - ], - "expected_status": [ - 200, - 400, - 401 - ] - }, - "refresh_token": { - "path": "/token?grant_type=refresh_token", - "method": "POST", - "required_params": [ - "refresh_token" - ], - "payload": [ - "refresh_token" - ], - "expected_status": [ - 200, - 400, - 401 - ] - }, - "sign_out": { - "path": "/logout", - "method": "POST", - "required_params": [], - "payload": [], - "expected_status": [ - 204, - 401 - ] - }, - "get_user": { - "path": "/user", - "method": "GET", - "required_params": [], - "expected_status": [ - 200, - 401 - ] + "base_url": "https://readest.supabase.co/auth/v1/", + "name": "supabase-auth-api", + "methods": { + "sign_in_password": { + "path": "/token?grant_type=password", + "method": "POST", + "required_params": ["email", "password"], + "payload": ["email", "password"], + "expected_status": [200, 400, 401] + }, + "refresh_token": { + "path": "/token?grant_type=refresh_token", + "method": "POST", + "required_params": ["refresh_token"], + "payload": ["refresh_token"], + "expected_status": [200, 400, 401, 403] + }, + "sign_out": { + "path": "/logout", + "method": "POST", + "required_params": [], + "payload": [], + "expected_status": [204, 401] + }, + "get_user": { + "path": "/user", + "method": "GET", + "required_params": [], + "expected_status": [200, 401] + } } - } -} \ No newline at end of file +}