Compare commits

...

7 Commits

13 changed files with 92 additions and 22 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.86",
"version": "0.9.87",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev --turbopack",
+10
View File
@@ -1,5 +1,15 @@
{
"releases": {
"0.9.87": {
"date": "2025-10-16",
"notes": [
"Reader: Improved chapter detection with better chapter title recognition",
"Reader: Enhanced compatibility for parsing more footnote formats",
"Theme: Fixed auto theme switching in new reader windows",
"TTS: Improved scrolling accuracy to highlighted text during read-aloud",
"TTS: Fixed crash issues on certain Android systems"
]
},
"0.9.86": {
"date": "2025-10-14",
"notes": [
@@ -161,10 +161,47 @@ class MediaPlaybackService : MediaBrowserServiceCompat() {
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setSmallIcon(R.drawable.notification_icon)
addAction(
android.R.drawable.ic_media_previous,
"Previous",
MediaButtonReceiver.buildMediaButtonPendingIntent(
this@MediaPlaybackService,
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
)
if (playbackState == PlaybackStateCompat.STATE_PLAYING) {
addAction(
android.R.drawable.ic_media_pause,
"Pause",
MediaButtonReceiver.buildMediaButtonPendingIntent(
this@MediaPlaybackService,
PlaybackStateCompat.ACTION_PAUSE
)
)
} else {
addAction(
android.R.drawable.ic_media_play,
"Play",
MediaButtonReceiver.buildMediaButtonPendingIntent(
this@MediaPlaybackService,
PlaybackStateCompat.ACTION_PLAY
)
)
}
addAction(
android.R.drawable.ic_media_next,
"Next",
MediaButtonReceiver.buildMediaButtonPendingIntent(
this@MediaPlaybackService,
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
)
setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession?.sessionToken)
.setShowActionsInCompactView(0)
.setShowActionsInCompactView(0, 1, 2)
)
}
return builder.build()
+2 -2
View File
@@ -17,8 +17,8 @@
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://translate.toil.cc https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.netlify.app",
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://chinese-fonts-cdn.netlify.app",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.netlify.app https://cdnjs.cloudflare.com",
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://chinese-fonts-cdn.netlify.app https://cdnjs.cloudflare.com",
"frame-src": "'self' blob: asset: http://asset.localhost https://*.stripe.com",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com https://*.stripe.com"
},
@@ -68,8 +68,8 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
}
: {
top: `${topInset}px`,
insetInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
width: `calc(100% - ${contentInsets.left + contentInsets.right}px)`,
paddingInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
width: '100%',
}
}
>
@@ -11,7 +11,7 @@ import { BookSearchConfig, BookSearchResult } from '@/types/book';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { debounce } from '@/utils/debounce';
import { isCJKStr } from '@/utils/lang';
import { createRejecttFilter } from '@/utils/node';
import { createRejectFilter } from '@/utils/node';
import Dropdown from '@/components/Dropdown';
import SearchOptions from './SearchOptions';
@@ -127,7 +127,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
...searchConfig,
index,
query: term,
acceptNode: createRejecttFilter({
acceptNode: createRejectFilter({
tags: primaryLang.startsWith('ja') ? ['rt'] : [],
}),
});
@@ -208,6 +208,30 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
viewSettings.ttsLocation = cfi || '';
setViewSettings(bookKey, viewSettings);
}
if (!view.renderer.scrolled) {
view.renderer.scrollToAnchor(range);
} else {
const rect = range.getBoundingClientRect();
const { start, size, viewSize, sideProp } = view.renderer;
const positionStart = rect[sideProp === 'height' ? 'y' : 'x'] + viewSettings.marginTopPx;
const positionEnd = rect[sideProp === 'height' ? 'height' : 'width'] + positionStart;
const offsetStart = view.book.dir === 'rtl' ? viewSize - positionStart : positionStart;
const offsetEnd = view.book.dir === 'rtl' ? viewSize - positionEnd : positionEnd;
const showHeader = viewSettings.showHeader;
const showFooter = viewSettings.showFooter;
const showBarsOnScroll = viewSettings.showBarsOnScroll;
const headerScrollOverlap = showHeader && showBarsOnScroll ? 44 : 0;
const footerScrollOverlap = showFooter && showBarsOnScroll ? 44 : 0;
const scrollingOverlap = viewSettings.scrollingOverlap;
const endInNextView = offsetEnd > start + size - footerScrollOverlap - scrollingOverlap;
const startInPrevView = offsetStart < start + headerScrollOverlap + scrollingOverlap;
if (endInNextView || startInPrevView) {
const scrollTo = offsetStart - headerScrollOverlap - scrollingOverlap;
view.renderer.scrollToAnchor(scrollTo / viewSize);
}
}
};
ttsController.addEventListener('tts-speak-mark', handleSpeakMark);
@@ -3,7 +3,7 @@ import { AppService } from '@/types/system';
import { parseSSMLMarks } from '@/utils/ssml';
import { Overlayer } from 'foliate-js/overlayer.js';
import { TTSGranularity, TTSHighlightOptions, TTSMark, TTSVoice } from './types';
import { createRejecttFilter } from '@/utils/node';
import { createRejectFilter } from '@/utils/node';
import { WebSpeechClient } from './WebSpeechClient';
import { NativeTTSClient } from './NativeTTSClient';
import { EdgeTTSClient } from './EdgeTTSClient';
@@ -85,13 +85,6 @@ export class TTSController extends EventTarget {
const { style, color } = options;
overlayer?.remove(HIGHLIGHT_KEY);
overlayer?.add(HIGHLIGHT_KEY, range, Overlayer[style], { color });
const rect = range.getBoundingClientRect();
const { start, size, viewSize, sideProp } = this.view.renderer;
const position = rect[sideProp === 'height' ? 'y' : 'x'] + 88;
const offset = this.view.book.dir === 'rtl' ? viewSize - position : position;
if (!this.view.renderer.scrolled || offset < start || offset > start + size) {
this.view.renderer.scrollToAnchor(range);
}
};
}
@@ -109,7 +102,7 @@ export class TTSController extends EventTarget {
const highlightOptions: TTSHighlightOptions = { style: 'highlight', color: 'gray' };
await this.view.initTTS(
granularity,
createRejecttFilter({
createRejectFilter({
tags: ['rt', 'sup'],
contents: [{ tag: 'a', content: /^\d+$/ }],
}),
+6 -1
View File
@@ -141,7 +141,12 @@ export const loadDataTheme = () => {
const themeMode = localStorage.getItem('themeMode');
const themeColor = localStorage.getItem('themeColor');
if (themeMode && themeColor) {
document.documentElement.setAttribute('data-theme', `${themeColor}-${themeMode}`);
const systemIsDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode);
document.documentElement.setAttribute(
'data-theme',
`${themeColor}-${isDarkMode ? 'dark' : 'light'}`,
);
}
};
+1 -1
View File
@@ -33,7 +33,7 @@ const getAdditionalBasicFontLinks = () => `
const getAdditionalCJKFontLinks = () => `
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/misans-webfont@1.0.4/misans-l3/misans-l3/result.min.css" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cn-fontsource-lxgw-wen-kai-gb-screen@1.0.6/font.min.css" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lxgw-wenkai-screen-web/1.520.0/lxgwwenkaigbscreen/result.css" crossorigin="anonymous" />
<link rel='stylesheet' href='https://chinese-fonts-cdn.netlify.app/packages/hwmct/dist/%E6%B1%87%E6%96%87%E6%98%8E%E6%9C%9D%E4%BD%93/result.css' crossorigin="anonymous" />
<link rel='stylesheet' href='https://chinese-fonts-cdn.netlify.app/packages/jhlst/dist/%E4%BA%AC%E8%8F%AF%E8%80%81%E5%AE%8B%E4%BD%93v2_002/result.css' crossorigin="anonymous" />
<link rel='stylesheet' href='https://chinese-fonts-cdn.netlify.app/packages/syst/dist/SourceHanSerifCN/result.css' crossorigin="anonymous" />
+1 -1
View File
@@ -1,4 +1,4 @@
export const createRejecttFilter = ({
export const createRejectFilter = ({
tags = [],
classes = [],
attributes = [],
+2 -1
View File
@@ -115,9 +115,10 @@ export class TxtToEpubConverter {
[
String.raw`第[零〇一二三四五六七八九十0-9][零〇一二三四五六七八九十百千万0-9]*(?:[章卷节回讲篇封本册部话])(?:[::、  \(\)0-9]*[^\n-]{0,24})(?!\S)`,
String.raw`(?:楔子|前言|简介|引言|序言|序章|总论|概论|后记)(?:[:  ][^\n-]{0,24})?(?!\S)`,
String.raw`chapter[\s.]*[0-9]+(?:[:.  ]+[^\n-]{0,50})?(?!\S)`,
].join('|') +
')',
'gu',
'gui',
),
);
chapterRegexps.push(