Compare commits

...

10 Commits

Author SHA1 Message Date
Huang Xin 721e70d027 release: version 0.9.93 (#2486) 2025-11-20 06:33:58 +01:00
Huang Xin 3c1a7eac86 fix(library): fixed occasional freeze when navigating back to the library (#2485)
Closes #2482.
2025-11-20 06:15:49 +01:00
Huang Xin f8e21dbd4c fix(sync): fixed sync issues affecting new accounts, closes #2481 (#2483) 2025-11-20 04:28:41 +01:00
Huang Xin d16a35d26f fix(layout): constrain image height within table cells in paginated mode (#2480)
Closes #2472.
2025-11-19 18:06:39 +01:00
Huang Xin f881caf794 chore(pwa): config workbox to skip precaching next.js internal files (#2478) 2025-11-19 14:38:04 +01:00
54wedge eb6fa276de fix(txt): register style.css to content.opf (#2476)
* fix(txt): register style.css to content.opf

* refactor: code styling

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2025-11-19 13:59:04 +01:00
Huang Xin e987c8b37a chore(pwa): get rid of public headers requests (#2477) 2025-11-19 13:31:21 +01:00
Huang Xin 26d76e27ac fix(txt): more tolerant encoding detection for utf-8 (#2475) 2025-11-19 07:47:10 +01:00
Huang Xin 975549fca0 fix(font): avoid overriding monospace fonts for code blocks, closes #1805 (#2474) 2025-11-19 06:57:45 +01:00
Huang Xin 9a6bed52dd fix(sync): propagate book delete status to other devices (#2473) 2025-11-19 05:47:59 +01:00
13 changed files with 106 additions and 28 deletions
+28
View File
@@ -50,6 +50,15 @@ const nextConfig = {
},
],
},
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
};
@@ -57,6 +66,8 @@ const nextConfig = {
const withPWA = withPWAInit({
dest: 'public',
disable: isDev || appPlatform !== 'web',
cacheStartUrl: false,
dynamicStartUrl: false,
cacheOnFrontEndNav: true,
aggressiveFrontEndNavCaching: true,
reloadOnOnline: true,
@@ -66,6 +77,23 @@ const withPWA = withPWAInit({
},
workboxOptions: {
disableDevLogs: true,
manifestTransforms: [
(manifestEntries) => {
const manifest = manifestEntries.filter((entry) => {
const url = entry.url;
return (
!url.includes('dynamic-css-manifest.json') &&
!url.includes('middleware-manifest.json') &&
!url.includes('react-loadable-manifest.json') &&
!url.includes('build-manifest.json') &&
!url.includes('_buildManifest.js') &&
!url.includes('_ssgManifest.js') &&
!url.includes('_headers')
);
});
return { manifest };
},
],
},
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.92",
"version": "0.9.93",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev --turbopack",
-2
View File
@@ -1,2 +0,0 @@
/_next/static/*
Cache-Control: public,max-age=31536000,immutable
+10
View File
@@ -1,5 +1,15 @@
{
"releases": {
"0.9.93": {
"date": "2025-11-20",
"notes": [
"Sync: Improved reliability for new accounts so your books sync correctly from the start",
"Library: Fixed an occasional issue where returning to the library could get stuck",
"Layout: Improved image display in paginated mode so images won't exceed the available height",
"TXT: Improved performance and stability when opening and parsing TXT files",
"Fonts: Fixed an issue where monospace fonts were not applied correctly in code blocks"
]
},
"0.9.92": {
"date": "2025-11-18",
"notes": [
@@ -99,7 +99,7 @@ export const useBooksSync = () => {
oldBook.coverImageUrl = await appService?.generateCoverImageUrl(oldBook);
}
const mergedBook =
matchingBook.updatedAt > oldBook.updatedAt
matchingBook.updatedAt >= oldBook.updatedAt
? { ...oldBook, ...matchingBook, syncedAt: Date.now() }
: { ...matchingBook, ...oldBook, syncedAt: Date.now() };
return mergedBook;
@@ -143,6 +143,10 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
},
});
useEffect(() => {
sessionStorage.setItem('lastLibraryParams', searchParams?.toString() || '');
}, [searchParams]);
useEffect(() => {
const doCheckAppUpdates = async () => {
if (appService?.hasUpdater && settings.autoCheckUpdates) {
@@ -23,6 +23,7 @@ import { useKOSync } from '../hooks/useKOSync';
import {
applyFixedlayoutStyles,
applyImageStyle,
applyScrollModeClass,
applyTableStyle,
applyThemeModeClass,
applyTranslationStyle,
@@ -193,6 +194,7 @@ const FoliateViewer: React.FC<{
applyImageStyle(detail.doc);
applyTableStyle(detail.doc);
applyThemeModeClass(detail.doc, isDarkMode);
applyScrollModeClass(detail.doc, viewSettings.scrolled || false);
keepTextAlignment(detail.doc);
removeTabIndex(detail.doc);
@@ -414,10 +416,17 @@ const FoliateViewer: React.FC<{
applyFixedlayoutStyles(doc, viewSettings);
}
applyThemeModeClass(doc, isDarkMode);
applyScrollModeClass(doc, viewSettings.scrolled || false);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode, isDarkMode, viewSettings?.overrideColor, viewSettings?.invertImgColorInDark]);
}, [
themeCode,
isDarkMode,
viewSettings?.scrolled,
viewSettings?.overrideColor,
viewSettings?.invertImgColorInDark,
]);
useEffect(() => {
const mountCustomFonts = async () => {
+1 -12
View File
@@ -2,9 +2,8 @@
import '@/utils/polyfill';
import i18n from '@/i18n/i18n';
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { IconContext } from 'react-icons';
import { usePathname } from 'next/navigation';
import { AuthProvider } from '@/context/AuthContext';
import { useEnv } from '@/context/EnvContext';
import { CSPostHogProvider } from '@/context/PHContext';
@@ -28,16 +27,6 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
const iconSize = useDefaultIconSize();
useSafeAreaInsets(); // Initialize safe area insets
const pathname = usePathname();
const prevPathnameRef = useRef<string | null>(null);
useEffect(() => {
if (prevPathnameRef.current !== null) {
sessionStorage.setItem('lastPath', prevPathnameRef.current);
}
prevPathnameRef.current = pathname;
}, [pathname]);
useEffect(() => {
const handlerLanguageChanged = (lng: string) => {
document.documentElement.lang = lng;
+3 -2
View File
@@ -108,8 +108,9 @@ export function useSync(bookKey?: string) {
const result = await syncClient.pullChanges(since, type, bookId, metaHash);
setSyncResult({ ...syncResult, [type]: result[type] });
const records = result[type];
if (!records?.length) return;
const maxTime = computeMaxTimestamp(records);
if (since > 0 && !records?.length) return;
// For since = 0, we set lastSyncedAt to now if no records returned
const maxTime = records?.length ? computeMaxTimestamp(records) : Date.now();
setLastSyncedAt(maxTime);
// due to closures in React hooks the settings might be stale
+4 -4
View File
@@ -82,10 +82,10 @@ export const navigateToLibrary = (
navOptions?: { scroll?: boolean },
navBack?: boolean,
) => {
const lastPath = typeof window !== 'undefined' ? sessionStorage.getItem('lastPath') : null;
if (navBack && lastPath === '/library') {
router.back();
return;
const lastLibraryParams =
typeof window !== 'undefined' ? sessionStorage.getItem('lastLibraryParams') : null;
if (navBack && lastLibraryParams) {
queryParams = lastLibraryParams;
}
router.replace(`/library${queryParams ? `?${queryParams}` : ''}`, navOptions);
+15 -4
View File
@@ -100,7 +100,7 @@ const getFontStyles = (
pre, code, kbd {
font-family: var(--monospace);
}
body *:not(pre):not(code):not(kbd):not(pre *):not(code *):not(kbd *) {
body *:not(pre, code, kbd, .code):not(pre *, code *, kbd *, .code *) {
${overrideFont ? 'font-family: revert !important;' : ''}
}
`;
@@ -364,6 +364,10 @@ const getLayoutStyles = (
max-width: 100% !important;
}
body.paginated-mode td:has(img), body.paginated-mode td :has(img) {
max-height: calc(var(--available-height) * 0.8 * 1px);
}
/* some epubs set insane inline-block for p */
p {
display: block;
@@ -654,6 +658,11 @@ export const applyThemeModeClass = (document: Document, isDarkMode: boolean) =>
document.body.classList.add(isDarkMode ? 'theme-dark' : 'theme-light');
};
export const applyScrollModeClass = (document: Document, isScrollMode: boolean) => {
document.body.classList.remove('scroll-mode', 'paginated-mode');
document.body.classList.add(isScrollMode ? 'scroll-mode' : 'paginated-mode');
};
export const applyImageStyle = (document: Document) => {
document.querySelectorAll('img').forEach((img) => {
const parent = img.parentNode;
@@ -708,9 +717,11 @@ export const applyTableStyle = (document: Document) => {
}
}
const scale = `calc(min(1, var(--available-width) / ${totalTableWidth}))`;
table.style.transformOrigin = 'left top';
table.style.transform = `scale(${scale})`;
if (totalTableWidth > 0) {
const scale = `calc(min(1, var(--available-width) / ${totalTableWidth}))`;
table.style.transformOrigin = 'left top';
table.style.transform = `scale(${scale})`;
}
});
};
+28
View File
@@ -55,6 +55,7 @@ export class TxtToEpubConverter {
const fileContent = await txtFile.arrayBuffer();
const detectedEncoding = this.detectEncoding(fileContent) || 'utf-8';
console.log(`Detected encoding: ${detectedEncoding}`);
const decoder = new TextDecoder(detectedEncoding);
const txtContent = decoder.decode(fileContent).trim();
@@ -384,6 +385,7 @@ export class TxtToEpubConverter {
}
const tocManifest = `<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>`;
const styleManifest = `<item id="css" href="style.css" media-type="text/css"/>`;
// Add content.opf file
const contentOpf = `<?xml version="1.0" encoding="UTF-8"?>
@@ -397,6 +399,7 @@ export class TxtToEpubConverter {
<manifest>
${manifest}
${tocManifest}
${styleManifest}
</manifest>
<spine toc="ncx">
${spine}
@@ -413,6 +416,31 @@ export class TxtToEpubConverter {
new TextDecoder('utf-8', { fatal: true }).decode(buffer);
return 'utf-8';
} catch {
const uint8Array = new Uint8Array(buffer);
// Try tolerant UTF-8 detection - check if most of it is valid UTF-8
let validBytes = 0;
let checkedBytes = 0;
const sampleSize = Math.min(uint8Array.length, 10000);
for (let i = 0; i < sampleSize; i++) {
try {
new TextDecoder('utf-8', { fatal: true }).decode(uint8Array.slice(i, i + 100));
validBytes += 100;
checkedBytes += 100;
i += 99;
} catch {
checkedBytes++;
}
}
const validPercentage = (validBytes / checkedBytes) * 100;
console.log(`UTF-8 validity: ${validPercentage.toFixed(2)}%`);
// If more than 80% is valid UTF-8, consider it UTF-8 with some corruption
if (validPercentage > 80) {
console.log('Treating as UTF-8 despite some invalid sequences');
return 'utf-8';
}
// If UTF-8 decoding fails, try to detect other encodings
}