forked from akai/readest
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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<BookFormat> = 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;
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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<BookFormat> = 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<BookSearchConfig>;
|
||||
viewSettings?: Partial<ViewSettings>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<XPointer> => {
|
||||
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;
|
||||
};
|
||||
|
||||
+126
-40
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user