Compare commits

...

6 Commits

18 changed files with 229 additions and 96 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.85",
"version": "0.9.86",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev --turbopack",
+10
View File
@@ -1,5 +1,15 @@
{
"releases": {
"0.9.86": {
"date": "2025-10-14",
"notes": [
"Reader: Added support for per-book background image customization",
"Reader: Added option to override book language metadata for improved hyphenation accuracy",
"Sync: Readest sync in KOReader can now be triggered using gesture controls",
"Sync: Fixed a crash in KOReader that occurred when syncing progress without an open book",
"Sync: Improved upload performance with concurrent upload support"
]
},
"0.9.85": {
"date": "2025-10-14",
"notes": [
@@ -31,11 +31,12 @@ interface BookshelfProps {
isSelectAll: boolean;
isSelectNone: boolean;
handleImportBooks: () => void;
handleBookUpload: (book: Book) => Promise<boolean>;
handleBookDownload: (book: Book) => Promise<boolean>;
handleBookDelete: (book: Book) => Promise<boolean>;
handleBookUpload: (book: Book, syncBooks?: boolean) => Promise<boolean>;
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
handleSetSelectMode: (selectMode: boolean) => void;
handleShowDetailsBook: (book: Book) => void;
handlePushLibrary: () => Promise<void>;
booksTransferProgress: { [key: string]: number | null };
}
@@ -50,6 +51,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
handleBookDelete,
handleSetSelectMode,
handleShowDetailsBook,
handlePushLibrary,
booksTransferProgress,
}) => {
const _ = useTranslation();
@@ -217,9 +219,14 @@ const Bookshelf: React.FC<BookshelfProps> = ({
};
const confirmDelete = async () => {
for (const book of getBooksToDelete()) {
handleBookDelete(book);
const books = getBooksToDelete();
const concurrency = 4;
for (let i = 0; i < books.length; i += concurrency) {
const batch = books.slice(i, i + concurrency);
await Promise.all(batch.map((book) => handleBookDelete(book, false)));
}
handlePushLibrary();
setSelectedBooks([]);
setShowDeleteAlert(false);
setShowSelectModeActions(true);
@@ -77,9 +77,9 @@ interface BookshelfItemProps {
transferProgress: number | null;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
toggleSelection: (hash: string) => void;
handleBookUpload: (book: Book) => Promise<boolean>;
handleBookDownload: (book: Book) => Promise<boolean>;
handleBookDelete: (book: Book) => Promise<boolean>;
handleBookUpload: (book: Book, syncBooks?: boolean) => Promise<boolean>;
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
handleSetSelectMode: (selectMode: boolean) => void;
handleShowDetailsBook: (book: Book) => void;
}
+44 -18
View File
@@ -419,41 +419,56 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const importBooks = async (files: SelectedFile[]) => {
setLoading(true);
const failedFiles = [];
const { library } = useLibraryStore.getState();
const failedImports: Array<{ filename: string; errorMessage: string }> = [];
const errorMap: [string, string][] = [
['No chapters detected.', _('No chapters detected.')],
['Failed to parse EPUB.', _('Failed to parse the EPUB file.')],
['Unsupported format.', _('This book format is not supported.')],
];
const { library } = useLibraryStore.getState();
for (const selectedFile of files) {
const processFile = async (selectedFile: SelectedFile) => {
const file = selectedFile.file || selectedFile.path;
if (!file) continue;
if (!file) return;
try {
const book = await appService?.importBook(file, library);
setLibrary([...library]);
if (user && book && !book.uploadedAt && settings.autoUpload) {
console.log('Uploading book:', book.title);
handleBookUpload(book);
handleBookUpload(book, false);
}
} catch (error) {
const filename = typeof file === 'string' ? file : file.name;
const baseFilename = getFilename(filename);
failedFiles.push(baseFilename);
const errorMessage =
error instanceof Error
? errorMap.find(([substring]) => error.message.includes(substring))?.[1] || ''
: '';
eventDispatcher.dispatch('toast', {
message:
_('Failed to import book(s): {{filenames}}', {
filenames: listFormater(false).format(failedFiles),
}) + (errorMessage ? `\n${errorMessage}` : ''),
type: 'error',
});
failedImports.push({ filename: baseFilename, errorMessage });
console.error('Failed to import book:', filename, error);
}
};
const concurrency = 4;
for (let i = 0; i < files.length; i += concurrency) {
const batch = files.slice(i, i + concurrency);
await Promise.all(batch.map(processFile));
}
pushLibrary();
if (failedImports.length > 0) {
const filenames = failedImports.map((f) => f.filename);
const errorMessage = failedImports.find((f) => f.errorMessage)?.errorMessage || '';
eventDispatcher.dispatch('toast', {
message:
_('Failed to import book(s): {{filenames}}', {
filenames: listFormater(false).format(filenames),
}) + (errorMessage ? `\n${errorMessage}` : ''),
type: 'error',
});
}
setLibrary([...library]);
appService?.saveLibraryBooks(library);
setLoading(false);
};
@@ -468,13 +483,18 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}, 500);
const handleBookUpload = useCallback(
async (book: Book) => {
async (book: Book, syncBooks = true) => {
try {
await appService?.uploadBook(book, (progress) => {
updateBookTransferProgress(book.hash, progress);
});
setBooksTransferProgress((prev) => {
const updated = { ...prev };
delete updated[book.hash];
return updated;
});
await updateBook(envConfig, book);
pushLibrary();
if (syncBooks) pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
@@ -484,6 +504,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
});
return true;
} catch (err) {
setBooksTransferProgress((prev) => {
const updated = { ...prev };
delete updated[book.hash];
return updated;
});
if (err instanceof Error) {
if (err.message.includes('Not authenticated') && settings.keepLogin) {
settings.keepLogin = false;
@@ -541,7 +566,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
);
const handleBookDelete = (deleteAction: DeleteAction) => {
return async (book: Book) => {
return async (book: Book, syncBooks = true) => {
const deletionMessages = {
both: _('Book deleted: {{title}}', { title: book.title }),
cloud: _('Deleted cloud backup of the book: {{title}}', { title: book.title }),
@@ -555,7 +580,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
try {
await appService?.deleteBook(book, deleteAction);
await updateBook(envConfig, book);
pushLibrary();
if (syncBooks) pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
@@ -709,6 +734,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
handleSetSelectMode={handleSetSelectMode}
handleShowDetailsBook={handleShowDetailsBook}
booksTransferProgress={booksTransferProgress}
handlePushLibrary={pushLibrary}
/>
</div>
</OverlayScrollbarsComponent>
@@ -15,6 +15,7 @@ import { usePagination } from '../hooks/usePagination';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useProgressSync } from '../hooks/useProgressSync';
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
import { useAutoFocus } from '@/hooks/useAutoFocus';
import { useTranslation } from '@/hooks/useTranslation';
import { useKOSync } from '../hooks/useKOSync';
@@ -42,6 +43,7 @@ import { getMaxInlineSize } from '@/utils/config';
import { getDirFromUILanguage } from '@/utils/rtl';
import { isCJKLang } from '@/utils/lang';
import { isTauriAppPlatform } from '@/services/environment';
import { TransformContext } from '@/services/transformers/types';
import { transformContent } from '@/services/transformService';
import { lockScreenOrientation } from '@/utils/bridge';
import { useTextTranslation } from '../hooks/useTextTranslation';
@@ -73,6 +75,7 @@ const FoliateViewer: React.FC<{
const { getViewState, getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { getBookData } = useBookDataStore();
const { applyBackgroundTexture } = useBackgroundTexture();
const viewState = getViewState(bookKey);
const viewSettings = getViewSettings(bookKey);
@@ -116,15 +119,17 @@ const FoliateViewer: React.FC<{
detail.data = Promise.resolve(detail.data)
.then((data) => {
const viewSettings = getViewSettings(bookKey);
const bookData = getBookData(bookKey);
if (viewSettings && detail.type === 'text/css')
return transformStylesheet(width, height, data);
if (viewSettings && detail.type === 'application/xhtml+xml') {
if (viewSettings && bookData && detail.type === 'application/xhtml+xml') {
const ctx = {
bookKey,
viewSettings,
primaryLanguage: bookData.book?.primaryLanguage,
content: data,
transformers: ['punctuation', 'footnote', 'language'],
};
transformers: ['punctuation', 'footnote', 'whitespace', 'language'],
} as TransformContext;
return Promise.resolve(transformContent(ctx));
}
return data;
@@ -145,14 +150,19 @@ const FoliateViewer: React.FC<{
const writingDir = viewRef.current?.renderer.setStyles && getDirection(detail.doc);
const viewSettings = getViewSettings(bookKey)!;
const bookData = getBookData(bookKey)!;
viewSettings.vertical =
const newVertical =
writingDir?.vertical || viewSettings.writingMode.includes('vertical') || false;
viewSettings.rtl =
const newRtl =
writingDir?.rtl ||
getDirFromUILanguage() === 'rtl' ||
viewSettings.writingMode.includes('rl') ||
false;
setViewSettings(bookKey, { ...viewSettings });
if (viewSettings.vertical !== newVertical || viewSettings.rtl !== newRtl) {
viewSettings.vertical = newVertical;
viewSettings.rtl = newRtl;
setViewSettings(bookKey, { ...viewSettings });
}
mountAdditionalFonts(detail.doc, isCJKLang(bookData.book?.primaryLanguage));
@@ -395,6 +405,17 @@ const FoliateViewer: React.FC<{
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings.customFonts, envConfig]);
useEffect(() => {
if (!viewSettings) return;
applyBackgroundTexture(envConfig, viewSettings);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
viewSettings?.backgroundTextureId,
viewSettings?.backgroundOpacity,
viewSettings?.backgroundSize,
applyBackgroundTexture,
]);
useEffect(() => {
if (viewRef.current && viewRef.current.renderer) {
doubleClickDisabled.current = !!viewSettings?.disableDoubleClick;
@@ -142,7 +142,11 @@ export const useProgressSync = (bookKey: string) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Object.entries(syncedConfig).filter(([_, value]) => value !== null && value !== undefined),
);
setConfig(bookKey, { ...config, ...filteredSyncedConfig });
if (syncedConfig.updatedAt >= config.updatedAt) {
setConfig(bookKey, { ...config, ...filteredSyncedConfig });
} else {
setConfig(bookKey, { ...filteredSyncedConfig, ...config });
}
if (remoteCFILocation && configCFI) {
if (CFI.compare(configCFI, remoteCFILocation) < 0) {
if (view) {
+4 -18
View File
@@ -12,14 +12,14 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useDeviceControlStore } from '@/store/deviceStore';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
import { useCustomTextureStore } from '@/store/customTextureStore';
import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
import { getLocale } from '@/utils/misc';
const Providers = ({ children }: { children: React.ReactNode }) => {
const { envConfig, appService } = useEnv();
const { applyUILanguage } = useSettingsStore();
const { setScreenBrightness } = useDeviceControlStore();
const { addTexture, applyTexture } = useCustomTextureStore();
const { applyBackgroundTexture } = useBackgroundTexture();
const iconSize = useDefaultIconSize();
useSafeAreaInsets(); // Initialize safe area insets
@@ -47,24 +47,10 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
if (appService.hasScreenBrightness && brightness >= 0) {
setScreenBrightness(brightness / 100);
}
const backgroundTextureId = globalViewSettings.backgroundTextureId;
const backgroundOpacity = globalViewSettings.backgroundOpacity;
const backgroundSize = globalViewSettings.backgroundSize;
if (backgroundTextureId && backgroundTextureId !== 'none') {
const customTexture = settings.customTextures.find((t) => t.id === backgroundTextureId);
if (customTexture) {
addTexture(customTexture.path);
}
applyTexture(envConfig, backgroundTextureId);
document.documentElement.style.setProperty(
'--bg-texture-opacity',
backgroundOpacity.toString(),
);
document.documentElement.style.setProperty('--bg-texture-size', backgroundSize);
}
applyBackgroundTexture(envConfig, globalViewSettings);
});
}
}, [envConfig, appService, applyUILanguage, addTexture, applyTexture, setScreenBrightness]);
}, [envConfig, appService, applyUILanguage, setScreenBrightness, applyBackgroundTexture]);
// Make sure appService is available in all children components
if (!appService) return;
@@ -142,10 +142,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
const applyBackgroundTexture = () => {
applyTexture(envConfig, selectedTextureId);
document.documentElement.style.setProperty(
'--bg-texture-opacity',
backgroundOpacity.toString(),
);
document.documentElement.style.setProperty('--bg-texture-opacity', `${backgroundOpacity}`);
document.documentElement.style.setProperty('--bg-texture-size', backgroundSize);
};
@@ -0,0 +1,31 @@
import { useCallback } from 'react';
import { useCustomTextureStore } from '@/store/customTextureStore';
import { useSettingsStore } from '@/store/settingsStore';
import { EnvConfigType } from '@/services/environment';
import { ViewSettings } from '@/types/book';
export const useBackgroundTexture = () => {
const applyBackgroundTexture = useCallback(
(envConfig: EnvConfigType, viewSettings: ViewSettings) => {
const textureId = viewSettings.backgroundTextureId;
const textureOpacity = viewSettings.backgroundOpacity;
const textureSize = viewSettings.backgroundSize;
if (!textureId || textureId === 'none') return;
document.documentElement.style.setProperty('--bg-texture-opacity', `${textureOpacity}`);
document.documentElement.style.setProperty('--bg-texture-size', textureSize);
const settings = useSettingsStore.getState().settings;
const customTexture = settings.customTextures?.find((t) => t.id === textureId);
if (customTexture) {
useCustomTextureStore.getState().addTexture(customTexture.path);
}
useCustomTextureStore.getState().applyTexture(envConfig, textureId);
},
[],
);
return { applyBackgroundTexture };
};
+1 -1
View File
@@ -155,7 +155,7 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
overrideColor: false,
backgroundTextureId: 'none',
backgroundOpacity: 0.6,
backgroundSize: 'auto',
backgroundSize: 'cover',
codeHighlighting: false,
codeLanguage: 'auto-detect',
userStylesheet: '',
@@ -2,10 +2,12 @@ import type { Transformer } from './types';
import { footnoteTransformer } from './footnote';
import { languageTransformer } from './language';
import { punctuationTransformer } from './punctuation';
import { whitespaceTransformer } from './whitespace';
export const availableTransformers: Transformer[] = [
punctuationTransformer,
footnoteTransformer,
languageTransformer,
whitespaceTransformer,
// Add more transformers here
];
@@ -1,10 +1,11 @@
import { detectLanguage, isValidLang } from '@/utils/lang';
import { detectLanguage, isSameLang, isValidLang } from '@/utils/lang';
import type { Transformer } from './types';
export const languageTransformer: Transformer = {
name: 'language',
transform: async (ctx) => {
const primaryLanguage = ctx.primaryLanguage;
let result = ctx.content;
const attrsMatch = result.match(/<html\b([^>]*)>/i);
if (attrsMatch) {
@@ -13,9 +14,10 @@ export const languageTransformer: Transformer = {
const xmlLangRegex = / xml:lang="([^"]*)"/i;
const xmlLangMatch = attrs.match(xmlLangRegex);
const langMatch = attrs.match(langRegex);
if (!isValidLang(langMatch?.[1] || xmlLangMatch?.[1])) {
const docLang = langMatch?.[1] || xmlLangMatch?.[1];
if (!isValidLang(docLang) || !isSameLang(docLang, primaryLanguage)) {
const mainContent = result.replace(/<[^>]+>/g, ' ');
const lang = detectLanguage(mainContent);
const lang = isValidLang(primaryLanguage) ? primaryLanguage : detectLanguage(mainContent);
const newLangAttr = ` lang="${lang}"`;
const newXmlLangAttr = ` xml:lang="${lang}"`;
attrs = langMatch ? attrs.replace(langRegex, newLangAttr) : attrs + newLangAttr;
@@ -3,6 +3,7 @@ import { ViewSettings } from '@/types/book';
export type TransformContext = {
bookKey: string;
viewSettings: ViewSettings;
primaryLanguage?: string;
content: string;
transformers: string[];
reversePunctuationTransform?: boolean;
@@ -0,0 +1,21 @@
import type { Transformer } from './types';
export const whitespaceTransformer: Transformer = {
name: 'whitespace',
transform: async (ctx) => {
const viewSettings = ctx.viewSettings;
if (viewSettings.overrideLayout) {
const cleaned = ctx.content
// Replace &nbsp; but skip literal "&amp;nbsp;"
.replace(/(?<!&amp;)&nbsp;/g, ' ')
// Replace literal non-breaking space characters (U+00A0) with normal spaces
.replace(/\u00A0/g, ' ')
// Collapse consecutive spaces into one
.replace(/ {2,}/g, ' ');
return cleaned;
} else {
return ctx.content;
}
},
};
+1 -2
View File
@@ -288,10 +288,9 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
const oldConfig = bookData.config;
const newConfig = {
...bookData.config,
updatedAt: Date.now(),
progress,
location,
};
} as BookConfig;
useBookDataStore.setState((state) => ({
booksData: {
+8 -4
View File
@@ -59,7 +59,7 @@ const createTextureCSS = (texture: BackgroundTexture) => {
body::before, .sidebar-container::before, .notebook-container::before,
.foliate-viewer::before {
content: "";
position: fixed;
position: absolute;
top: 0;
left: 0;
right: 0;
@@ -68,10 +68,14 @@ const createTextureCSS = (texture: BackgroundTexture) => {
z-index: 0;
background-image: url("${texture.blobUrl || texture.url}");
background-repeat: repeat;
background-size: var(--bg-texture-size, auto);
mix-blend-mode: var(--bg-texture-blend-mode, normal);
background-size: var(--bg-texture-size, cover);
mix-blend-mode: var(--bg-texture-blend-mode, multiply);
opacity: var(--bg-texture-opacity, 0.6);
}`;
}
body::before {
height: 100vh;
}
`;
return css;
};
+54 -32
View File
@@ -1,5 +1,6 @@
local Device = require("device")
local Event = require("ui/event")
local Dispatcher = require("dispatcher")
local InfoMessage = require("ui/widget/infomessage")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local MultiInputDialog = require("ui/widget/multiinputdialog")
@@ -40,35 +41,25 @@ function ReadestSync:init()
self.last_sync_timestamp = 0
self.settings = G_reader_settings:readSetting("readest_sync", self.default_settings)
self:onDispatcherRegisterActions()
self.ui.menu:registerToMainMenu(self)
end
function ReadestSync:onDispatcherRegisterActions()
--
Dispatcher:registerAction("readest_sync_set_autosync",
{ category="string", event="ReadestSyncToggleAutoSync", title=_("Set auto progress sync"), reader=true,
args={true, false}, toggle={_("on"), _("off")},})
Dispatcher:registerAction("readest_sync_toggle_autosync", { category="none", event="ReadestSyncToggleAutoSync", title=_("Toggle auto readest sync"), reader=true,})
Dispatcher:registerAction("readest_sync_push_progress", { category="none", event="ReadestSyncPushProgress", title=_("Push readest progress from this device"), reader=true,})
Dispatcher:registerAction("readest_sync_pull_progress", { category="none", event="ReadestSyncPullProgress", title=_("Pull readest progress from other devices"), reader=true, separator=true,})
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:saveSetting("readest_sync", self.settings)
else
logger.err("ReadestSync: Token refresh failed:", response or "Unknown error")
end
function ReadestSync:onReaderReady()
if self.settings.auto_sync and self.settings.access_token then
UIManager:nextTick(function()
self:pullBookConfig(false)
end)
end
self:onDispatcherRegisterActions()
end
function ReadestSync:addToMainMenu(menu_items)
@@ -98,17 +89,14 @@ function ReadestSync:addToMainMenu(menu_items)
text = _("Auto sync book configs"),
checked_func = function() return self.settings.auto_sync end,
callback = function()
self.settings.auto_sync = not self.settings.auto_sync
if self.settings.auto_sync then
self:pullBookConfig(false)
end
self:onReadestSyncToggleAutoSync()
end,
separator = true,
},
{
text = _("Push book config now"),
enabled_func = function()
return self.settings.access_token ~= nil
return self.settings.access_token ~= nil and self.ui.document ~= nil
end,
callback = function()
self:pushBookConfig(true)
@@ -117,7 +105,7 @@ function ReadestSync:addToMainMenu(menu_items)
{
text = _("Pull book config now"),
enabled_func = function()
return self.settings.access_token ~= nil
return self.settings.access_token ~= nil and self.ui.document ~= nil
end,
callback = function()
self:pullBookConfig(true)
@@ -127,6 +115,29 @@ function ReadestSync:addToMainMenu(menu_items)
}
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:saveSetting("readest_sync", self.settings)
else
logger.err("ReadestSync: Token refresh failed:", response or "Unknown error")
end
end)
end
end
function ReadestSync:getSupabaseAuthClient()
if not self.settings.supabase_url or not self.settings.supabase_anon_key then
return nil
@@ -596,12 +607,23 @@ function ReadestSync:pullBookConfig(interactive)
)
end
function ReadestSync:onReaderReady()
if self.settings.auto_sync and self.settings.access_token then
UIManager:nextTick(function()
self:pullBookConfig(false)
end)
function ReadestSync:onReadestSyncToggleAutoSync(toggle)
if toggle == self.settings.auto_sync then
return true
end
self.settings.auto_sync = not self.settings.auto_sync
G_reader_settings:saveSetting("readest_sync", self.settings)
if self.settings.auto_sync and self.ui.document then
self:pullBookConfig(false)
end
end
function ReadestSync:onReadestSyncPushProgress()
self:pushBookConfig(true)
end
function ReadestSync:onReadestSyncPullProgress()
self:pullBookConfig(true)
end
function ReadestSync:onCloseDocument()