fix(eink): remove scroll animation in eink mode and optimize eink detection (#3822)

* fix(eink): remove scroll animation in eink mode

* fix(android): fix startup ANR on e-ink devices from getprop subprocesses
This commit is contained in:
Huang Xin 2026-04-11 00:22:06 +08:00 committed by GitHub
parent 3df75a67f9
commit ab7da981da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 51 additions and 25 deletions

21
Cargo.lock generated
View file

@ -27,6 +27,7 @@ dependencies = [
"rand 0.8.5",
"read-progress-stream",
"reqwest 0.12.28",
"rsproperties",
"serde",
"serde_json",
"tauri",
@ -5080,6 +5081,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "pretty-hex"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a65843dfefbafd3c879c683306959a6de478443ffe9c9adf02f5976432402d7"
[[package]]
name = "prettyplease"
version = "0.2.37"
@ -5835,6 +5842,20 @@ dependencies = [
"byteorder",
]
[[package]]
name = "rsproperties"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98d55abde44982a00d67a84809166ea8f5c143abc86818f555485f19766cfe4"
dependencies = [
"log",
"pretty-hex",
"rustix 1.1.4",
"thiserror 2.0.18",
"zerocopy",
"zerocopy-derive",
]
[[package]]
name = "rust-ini"
version = "0.21.3"

View file

@ -78,3 +78,6 @@ tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"
tauri-plugin-window-state = "2"
discord-rich-presence = "1.0.0"
[target.'cfg(target_os = "android")'.dependencies]
rsproperties = "0.3"

View file

@ -1,4 +1,4 @@
use std::process::Command;
use std::sync::OnceLock;
/// Known e-ink device manufacturers and brands (case-insensitive matching)
const EINK_MANUFACTURERS: &[&str] = &[
@ -45,28 +45,21 @@ const EINK_MODELS: &[&str] = &[
"max lumi",
];
/// Get Android system property using getprop command
fn get_system_property(prop: &str) -> Option<String> {
Command::new("getprop")
.arg(prop)
.output()
rsproperties::get::<String>(prop)
.ok()
.and_then(|output| {
if output.status.success() {
let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
if value.is_empty() {
None
} else {
Some(value)
}
} else {
None
}
})
.filter(|s| !s.is_empty())
}
/// Check if the current Android device is an e-ink device
/// Check if the current Android device is an e-ink device.
///
/// The result is cached on first call so subsequent calls are free.
pub fn is_eink_device() -> bool {
static IS_EINK: OnceLock<bool> = OnceLock::new();
*IS_EINK.get_or_init(detect_eink_device)
}
fn detect_eink_device() -> bool {
// Get device manufacturer and model
let manufacturer = get_system_property("ro.product.manufacturer")
.or_else(|| get_system_property("ro.product.brand"))

View file

@ -43,9 +43,10 @@ const TOCView: React.FC<{
bookKey: string;
toc: TOCItem[];
}> = ({ bookKey, toc }) => {
const { getView, getProgress } = useReaderStore();
const { getView, getViewSettings, getProgress } = useReaderStore();
const { sideBarBookKey, isSideBarVisible } = useSidebarStore();
const progress = getProgress(bookKey);
const isEink = !!getViewSettings(bookKey)?.isEink;
const [expandedItems, setExpandedItems] = useState<Set<string>>(() =>
computeExpandedSet(toc, progress?.sectionHref),
@ -131,14 +132,18 @@ const TOCView: React.FC<{
const timer = setTimeout(() => {
const idx = flatItems.findIndex((f) => f.item.href === activeHref);
if (idx !== -1) {
// Eink displays ghost previous frames during smooth JS scroll
// animations; force an instant jump to avoid the artifact. A CSS-only
// fix is impossible because scrollTo({ behavior: 'smooth' }) overrides
// CSS scroll-behavior and is not a CSS transition.
const distance = Math.abs(idx - visibleCenterRef.current);
const behavior = distance > 16 ? 'auto' : 'smooth';
const behavior = isEink || distance > 16 ? 'auto' : 'smooth';
virtuosoRef.current?.scrollToIndex({ index: idx, align: 'center', behavior });
}
pendingScrollRef.current = false;
}, 100);
return () => clearTimeout(timer);
}, [flatItems, activeHref, isSideBarVisible]);
}, [flatItems, activeHref, isSideBarVisible, isEink]);
return (
<div ref={containerRef} className='toc-list rounded' role='tree'>

View file

@ -20,7 +20,11 @@ const useScrollToItem = (
const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
if (!isVisible) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Eink displays ghost previous frames during smooth JS scroll animations;
// force an instant jump. scrollIntoView({ behavior: 'smooth' }) overrides
// CSS scroll-behavior, so a CSS-only fix via useEinkMode is impossible.
const isEink = document.documentElement.getAttribute('data-eink') === 'true';
element.scrollIntoView({ behavior: isEink ? 'auto' : 'smooth', block: 'center' });
}
if (isCurrent) {

View file

@ -39,13 +39,13 @@ export function getDefaultViewSettings(ctx: Context): ViewSettings {
...DEFAULT_BOOK_STYLE,
...DEFAULT_BOOK_FONT,
...DEFAULT_BOOK_LANGUAGE,
...(ctx.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
...(ctx.isEink ? DEFAULT_EINK_VIEW_SETTINGS : {}),
...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
...DEFAULT_VIEW_CONFIG,
...DEFAULT_TTS_CONFIG,
...DEFAULT_SCREEN_CONFIG,
...DEFAULT_ANNOTATOR_CONFIG,
...(ctx.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
...(ctx.isEink ? DEFAULT_EINK_VIEW_SETTINGS : {}),
...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
...{ ...DEFAULT_TRANSLATOR_CONFIG, translateTargetLang: getTargetLang() },
};
}