From 011ad18a0226f55a6595b884948c3fb6a7ef7faf Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 13 Apr 2026 18:04:41 +0800 Subject: [PATCH] fix(android): use stable safe area insets to avoid unnecessary layout shift, closes #3670 (#3859) --- .../android/src/main/java/NativeBridgePlugin.kt | 1 - .../src/app/reader/components/ImageViewer.tsx | 9 +++++---- .../src/app/reader/components/TableViewer.tsx | 9 +++++---- apps/readest-app/src/hooks/useSafeAreaInsets.ts | 3 +-- apps/readest-app/src/utils/style.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt index c4a2dcb0..ce1b4cb8 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt @@ -442,7 +442,6 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) { if (windowInsets != null) { val insets = windowInsets.getInsets( - WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() ) val density = activity.resources.displayMetrics.density diff --git a/apps/readest-app/src/app/reader/components/ImageViewer.tsx b/apps/readest-app/src/app/reader/components/ImageViewer.tsx index 91ee9de5..31b118e6 100644 --- a/apps/readest-app/src/app/reader/components/ImageViewer.tsx +++ b/apps/readest-app/src/app/reader/components/ImageViewer.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx'; import React, { useState, useRef, useEffect } from 'react'; import { IoChevronBack, IoChevronForward } from 'react-icons/io5'; import { useTranslation } from '@/hooks/useTranslation'; +import { useKeyDownActions } from '@/hooks/useKeyDownActions'; import { Insets } from '@/types/misc'; import ZoomControls from './ZoomControls'; @@ -39,6 +40,9 @@ const ImageViewer: React.FC = ({ const imageRef = useRef(null); const zoomLabelTimeoutRef = useRef | null>(null); + // Escape (desktop) and Android Back key → close the viewer. + useKeyDownActions({ onCancel: onClose }); + const hideZoomLabelAfterDelay = () => { if (zoomLabelTimeoutRef.current) { clearTimeout(zoomLabelTimeoutRef.current); @@ -79,10 +83,7 @@ const ImageViewer: React.FC = ({ const handleKeyDown = (e: React.KeyboardEvent) => { e.stopPropagation(); - if (e.key === 'Escape') { - onClose(); - return; - } + // Escape is handled by useKeyDownActions (also covers Android Back key). // Arrow key navigation if (e.key === 'ArrowLeft' && onPrevious) { diff --git a/apps/readest-app/src/app/reader/components/TableViewer.tsx b/apps/readest-app/src/app/reader/components/TableViewer.tsx index f4e15246..e72b8d88 100644 --- a/apps/readest-app/src/app/reader/components/TableViewer.tsx +++ b/apps/readest-app/src/app/reader/components/TableViewer.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx'; import React, { useState, useRef, useEffect } from 'react'; import { useTranslation } from '@/hooks/useTranslation'; +import { useKeyDownActions } from '@/hooks/useKeyDownActions'; import { Insets } from '@/types/misc'; import ZoomControls from './ZoomControls'; @@ -27,6 +28,9 @@ const TableViewer: React.FC = ({ gridInsets, html, isDarkMode, const contentRef = useRef(null); const zoomLabelTimeoutRef = useRef | null>(null); + // Escape (desktop) and Android Back key → close the viewer. + useKeyDownActions({ onCancel: onClose }); + const hideZoomLabelAfterDelay = () => { if (zoomLabelTimeoutRef.current) { clearTimeout(zoomLabelTimeoutRef.current); @@ -63,10 +67,7 @@ const TableViewer: React.FC = ({ gridInsets, html, isDarkMode, const handleKeyDown = (e: React.KeyboardEvent) => { e.stopPropagation(); - if (e.key === 'Escape') { - onClose(); - return; - } + // Escape is handled by useKeyDownActions (also covers Android Back key). const isCtrlOrCmd = e.ctrlKey || e.metaKey; diff --git a/apps/readest-app/src/hooks/useSafeAreaInsets.ts b/apps/readest-app/src/hooks/useSafeAreaInsets.ts index 7286747d..b11673ed 100644 --- a/apps/readest-app/src/hooks/useSafeAreaInsets.ts +++ b/apps/readest-app/src/hooks/useSafeAreaInsets.ts @@ -34,11 +34,10 @@ export const useSafeAreaInsets = () => { const rootStyles = getComputedStyle(document.documentElement); const hasCustomProperties = rootStyles.getPropertyValue('--safe-area-inset-top'); - const isWebView139 = /Chrome\/139/.test(navigator.userAgent); if (appService.isIOSApp && getOSPlatform() === 'macos') { // for iPadOS use zero insets updateInsets({ top: 0, right: 0, bottom: 0, left: 0 }); - } else if ((appService.isAndroidApp && isWebView139) || appService.isIOSApp) { + } else if (appService.isAndroidApp || appService.isIOSApp) { // safe-area-inset-* values in css are always 0px in some versions of webview 139 // due to https://issues.chromium.org/issues/40699457 getSafeAreaInsets().then((response) => { diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts index 0cc9565c..f1646b87 100644 --- a/apps/readest-app/src/utils/style.ts +++ b/apps/readest-app/src/utils/style.ts @@ -776,7 +776,7 @@ export const transformStylesheet = (css: string, vw: number, vh: number, vertica if (pxWidth > vw && !/max-width\s*:/.test(block)) { block = block.replace( /}$/, - ' max-width: calc(var(--available-width) * 1px); box-sizing: border-box; }', + ' width: 100%; max-width: calc(var(--available-width) * 1px); box-sizing: border-box; }', ); return selector + block; }