From cf666650963af4435cf4c734642fbf448d2bd619 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 19 Apr 2025 22:43:39 +0800 Subject: [PATCH] feat: immersive UI in reader page on iOS and Android, closes #886 and closes #864 (#911) --- apps/readest-app/.gitignore | 2 +- apps/readest-app/src-tauri/Info.plist | 6 ++ .../com/bilingify/readest/MainActivity.kt | 22 ------ .../app/src/main/res/values/themes.xml | 8 +- .../src/main/java/NativeBridgePlugin.kt | 79 +++++++++++++++++++ .../tauri-plugin-native-bridge/build.rs | 1 + .../ios/Sources/NativeBridgePlugin.swift | 31 +++++++- .../commands/set_system_ui_visibility.toml | 13 +++ .../permissions/autogenerated/reference.md | 27 +++++++ .../permissions/default.toml | 1 + .../permissions/schemas/schema.json | 16 +++- .../src/commands.rs | 8 ++ .../tauri-plugin-native-bridge/src/desktop.rs | 7 ++ .../tauri-plugin-native-bridge/src/lib.rs | 1 + .../tauri-plugin-native-bridge/src/mobile.rs | 11 +++ .../tauri-plugin-native-bridge/src/models.rs | 14 ++++ apps/readest-app/src/app/auth/error/page.tsx | 2 +- apps/readest-app/src/app/auth/page.tsx | 3 + .../app/library/components/LibraryHeader.tsx | 6 +- apps/readest-app/src/app/library/page.tsx | 18 ++--- .../src/app/reader/components/HeaderBar.tsx | 18 ++++- .../src/app/reader/components/Reader.tsx | 20 ++++- apps/readest-app/src/app/updater/page.tsx | 5 +- apps/readest-app/src/app/user/page.tsx | 2 +- .../src/components/UpdaterWindow.tsx | 3 - .../readest-app/src/hooks/usePullToRefresh.ts | 3 + apps/readest-app/src/hooks/useTheme.ts | 23 +++++- apps/readest-app/src/store/readerStore.ts | 6 ++ apps/readest-app/src/styles/globals.css | 1 - apps/readest-app/src/utils/bridge.ts | 22 ++++++ apps/readest-app/src/utils/style.ts | 6 +- packages/tauri | 2 +- 32 files changed, 327 insertions(+), 60 deletions(-) create mode 100644 apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/set_system_ui_visibility.toml diff --git a/apps/readest-app/.gitignore b/apps/readest-app/.gitignore index 7c508dda..b296d45b 100644 --- a/apps/readest-app/.gitignore +++ b/apps/readest-app/.gitignore @@ -32,7 +32,7 @@ yarn-error.log* /private_keys # plists -*.plist +Entitlements*.plist # local confs tauri.*.conf.json diff --git a/apps/readest-app/src-tauri/Info.plist b/apps/readest-app/src-tauri/Info.plist index 3809d268..b01f21a9 100644 --- a/apps/readest-app/src-tauri/Info.plist +++ b/apps/readest-app/src-tauri/Info.plist @@ -6,6 +6,12 @@ LSSupportsOpeningDocumentsInPlace + UIViewControllerBasedStatusBarAppearance + + UIHomeIndicatorAutoHidden + + UIRequiresFullScreen + CFBundleDocumentTypes diff --git a/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt b/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt index 97272de0..a10754ae 100644 --- a/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt +++ b/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt @@ -1,33 +1,11 @@ package com.bilingify.readest -import android.os.Build import android.os.Bundle -import android.view.View -import android.view.WindowInsets -import android.view.WindowInsetsController import androidx.activity.enableEdgeToEdge class MainActivity : TauriActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - - hideSystemUI() - } - - private fun hideSystemUI() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - window.decorView.windowInsetsController?.let { controller -> - controller.hide(WindowInsets.Type.systemBars()) - controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } - } else { - @Suppress("DEPRECATION") - window.decorView.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - or View.SYSTEM_UI_FLAG_FULLSCREEN - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - ) - } } } diff --git a/apps/readest-app/src-tauri/gen/android/app/src/main/res/values/themes.xml b/apps/readest-app/src-tauri/gen/android/app/src/main/res/values/themes.xml index 448c59cd..ae6b1e58 100644 --- a/apps/readest-app/src-tauri/gen/android/app/src/main/res/values/themes.xml +++ b/apps/readest-app/src-tauri/gen/android/app/src/main/res/values/themes.xml @@ -2,8 +2,12 @@ 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 5fe13240..698f9304 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 @@ -4,6 +4,12 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.util.Log +import android.os.Build +import android.view.View +import android.view.WindowInsets +import android.view.WindowManager +import android.view.WindowInsetsController +import android.graphics.Color import android.webkit.WebView import androidx.core.content.FileProvider import androidx.browser.customtabs.CustomTabsIntent @@ -31,6 +37,12 @@ class InstallPackageRequestArgs { var path: String? = null } +@InvokeArg +class SetSystemUIVisibilityRequestArgs { + var visible: Boolean? = false + var darkMode: Boolean? = false +} + @TauriPlugin class NativeBridgePlugin(private val activity: Activity): Plugin(activity) { private val implementation = NativeBridge() @@ -145,4 +157,71 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) { } invoke.resolve(ret) } + + @Command + fun set_system_ui_visibility(invoke: Invoke) { + val args = invoke.parseArgs(SetSystemUIVisibilityRequestArgs::class.java) + val visible = args.visible ?: false + var isDarkMode = args.darkMode ?: false + val ret = JSObject() + try { + val window = activity.window + val decorView = window.decorView + if (!visible) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + window.attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false) + val controller = window.insetsController + if (controller != null) { + controller.systemBarsBehavior = + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + + if (isDarkMode) { + controller.setSystemBarsAppearance( + 0, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + ) + } else { + controller.setSystemBarsAppearance( + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + ) + } + if (visible) { + controller.show(WindowInsets.Type.systemBars()) + } else { + controller.hide(WindowInsets.Type.systemBars()) + } + } + window.statusBarColor = Color.TRANSPARENT + window.navigationBarColor = Color.TRANSPARENT + } else { + @Suppress("DEPRECATION") + decorView.systemUiVisibility = when { + visible && !isDarkMode -> View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + visible -> View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + else -> View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_FULLSCREEN + } + window.statusBarColor = Color.TRANSPARENT + window.navigationBarColor = Color.TRANSPARENT + } + ret.put("success", true) + } catch (e: Exception) { + ret.put("success", false) + ret.put("error", e.message) + } + invoke.resolve(ret) + } } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs index da470368..043c05f1 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs @@ -4,6 +4,7 @@ const COMMANDS: &[&str] = &[ "copy_uri_to_path", "use_background_audio", "install_package", + "set_system_ui_visibility", ]; fn main() { diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift index 40d427c3..0f953129 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift @@ -14,6 +14,11 @@ class UseBackgroundAudioRequestArgs: Decodable { let enabled: Bool } +class SetSystemUIVisibilityRequestArgs: Decodable { + let visible: Bool + let darkMode: Bool +} + class NativeBridgePlugin: Plugin { private var authSession: ASWebAuthenticationSession? @@ -66,6 +71,30 @@ class NativeBridgePlugin: Plugin { let started = authSession?.start() ?? false Logger.info("Auth session start result: \(started)") } + + @objc public func set_system_ui_visibility(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(SetSystemUIVisibilityRequestArgs.self) + let visible = args.visible + let darkMode = args.darkMode + + DispatchQueue.main.async { + UIApplication.shared.isIdleTimerDisabled = !visible + UIApplication.shared.setStatusBarHidden(!visible, with: .none) + + let windows = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + + let keyWindow = windows.first(where: { $0.isKeyWindow }) ?? windows.first + if let keyWindow = keyWindow { + keyWindow.overrideUserInterfaceStyle = darkMode ? .dark : .light + keyWindow.layoutIfNeeded() + } else { + print("No key window found") + } + } + invoke.resolve(["success": true]) + } } @_cdecl("init_plugin_native_bridge") @@ -78,4 +107,4 @@ extension NativeBridgePlugin: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { return UIApplication.shared.windows.first ?? UIWindow() } -} \ No newline at end of file +} diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/set_system_ui_visibility.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/set_system_ui_visibility.toml new file mode 100644 index 00000000..7b1b5427 --- /dev/null +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/set_system_ui_visibility.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-set-system-ui-visibility" +description = "Enables the set_system_ui_visibility command without any pre-configured scope." +commands.allow = ["set_system_ui_visibility"] + +[[permission]] +identifier = "deny-set-system-ui-visibility" +description = "Denies the set_system_ui_visibility command without any pre-configured scope." +commands.deny = ["set_system_ui_visibility"] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md index 78689406..1a0202e8 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md @@ -9,6 +9,7 @@ Default permissions for the plugin - `allow-copy-uri-to-path` - `allow-use-background-audio` - `allow-install-package` +- `allow-set-system-ui-visibility` ## Permission Table @@ -126,6 +127,32 @@ Denies the install_package command without any pre-configured scope. +`native-bridge:allow-set-system-ui-visibility` + + + + +Enables the set_system_ui_visibility command without any pre-configured scope. + + + + + + + +`native-bridge:deny-set-system-ui-visibility` + + + + +Denies the set_system_ui_visibility command without any pre-configured scope. + + + + + + + `native-bridge:allow-use-background-audio` diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml index f26dd027..8d5aa057 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml @@ -6,4 +6,5 @@ permissions = [ "allow-copy-uri-to-path", "allow-use-background-audio", "allow-install-package", + "allow-set-system-ui-visibility", ] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json index 7eb0bcb7..06e5927c 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json @@ -342,6 +342,18 @@ "const": "deny-install-package", "markdownDescription": "Denies the install_package command without any pre-configured scope." }, + { + "description": "Enables the set_system_ui_visibility command without any pre-configured scope.", + "type": "string", + "const": "allow-set-system-ui-visibility", + "markdownDescription": "Enables the set_system_ui_visibility command without any pre-configured scope." + }, + { + "description": "Denies the set_system_ui_visibility command without any pre-configured scope.", + "type": "string", + "const": "deny-set-system-ui-visibility", + "markdownDescription": "Denies the set_system_ui_visibility command without any pre-configured scope." + }, { "description": "Enables the use_background_audio command without any pre-configured scope.", "type": "string", @@ -355,10 +367,10 @@ "markdownDescription": "Denies the use_background_audio command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`", + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`" + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`" } ] } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs index 4f21bafc..1c1aecad 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs @@ -43,3 +43,11 @@ pub(crate) async fn install_package( ) -> Result { app.native_bridge().install_package(payload) } + +#[command] +pub(crate) async fn set_system_ui_visibility( + app: AppHandle, + payload: SetSystemUIVisibilityRequest, +) -> Result { + app.native_bridge().set_system_ui_visibility(payload) +} diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs index c2836023..5ddd7619 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs @@ -36,4 +36,11 @@ impl NativeBridge { ) -> crate::Result { Err(crate::Error::UnsupportedPlatformError) } + + pub fn set_system_ui_visibility( + &self, + _payload: SetSystemUIVisibilityRequest, + ) -> crate::Result { + Err(crate::Error::UnsupportedPlatformError) + } } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs index 9bbb6b45..8bb00a1d 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs @@ -42,6 +42,7 @@ pub fn init() -> TauriPlugin { commands::copy_uri_to_path, commands::use_background_audio, commands::install_package, + commands::set_system_ui_visibility, ]) .setup(|app, api| { #[cfg(mobile)] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs index 9ded15c6..94a4a32f 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs @@ -66,3 +66,14 @@ impl NativeBridge { .map_err(Into::into) } } + +impl NativeBridge { + pub fn set_system_ui_visibility( + &self, + payload: SetSystemUIVisibilityRequest, + ) -> crate::Result { + self.0 + .run_mobile_plugin("set_system_ui_visibility", payload) + .map_err(Into::into) + } +} diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs index fe539884..283717e3 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs @@ -44,3 +44,17 @@ pub struct InstallPackageResponse { pub success: bool, pub error: Option, } + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SetSystemUIVisibilityRequest { + pub visible: bool, + pub dark_mode: bool, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SetSystemUIVisibilityResponse { + pub success: bool, + pub error: Option, +} diff --git a/apps/readest-app/src/app/auth/error/page.tsx b/apps/readest-app/src/app/auth/error/page.tsx index a2dee522..2a0f3db8 100644 --- a/apps/readest-app/src/app/auth/error/page.tsx +++ b/apps/readest-app/src/app/auth/error/page.tsx @@ -6,7 +6,7 @@ import { useTheme } from '@/hooks/useTheme'; export default function AuthErrorPage() { const router = useRouter(); - useTheme(); + useTheme({ systemUIVisible: false }); useEffect(() => { const timer = setTimeout(() => { diff --git a/apps/readest-app/src/app/auth/page.tsx b/apps/readest-app/src/app/auth/page.tsx index 852a1c78..cc6db434 100644 --- a/apps/readest-app/src/app/auth/page.tsx +++ b/apps/readest-app/src/app/auth/page.tsx @@ -13,6 +13,7 @@ import { IoArrowBack } from 'react-icons/io5'; import { useAuth } from '@/context/AuthContext'; import { supabase } from '@/utils/supabase'; import { useEnv } from '@/context/EnvContext'; +import { useTheme } from '@/hooks/useTheme'; import { useThemeStore } from '@/store/themeStore'; import { useSettingsStore } from '@/store/settingsStore'; import { useTranslation } from '@/hooks/useTranslation'; @@ -74,6 +75,8 @@ export default function AuthPage() { const headerRef = useRef(null); + useTheme({ systemUIVisible: false }); + const getTauriRedirectTo = (isOAuth: boolean) => { if (process.env.NODE_ENV === 'production' || appService?.isMobile || USE_APPLE_SIGN_IN) { if (appService?.isMobile) { diff --git a/apps/readest-app/src/app/library/components/LibraryHeader.tsx b/apps/readest-app/src/app/library/components/LibraryHeader.tsx index 9b6daadd..852ad2e6 100644 --- a/apps/readest-app/src/app/library/components/LibraryHeader.tsx +++ b/apps/readest-app/src/app/library/components/LibraryHeader.tsx @@ -92,10 +92,12 @@ const LibraryHeader: React.FC = ({
diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index b4c6ceec..bc4dc1da 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -21,7 +21,6 @@ import { getCurrentWebview } from '@tauri-apps/api/webview'; import { useEnv } from '@/context/EnvContext'; import { useAuth } from '@/context/AuthContext'; -import { useThemeStore } from '@/store/themeStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useLibraryStore } from '@/store/libraryStore'; import { useSettingsStore } from '@/store/settingsStore'; @@ -60,8 +59,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP setCheckOpenWithBooks, } = useLibraryStore(); const _ = useTranslation(); - useTheme(); - const { updateAppTheme } = useThemeStore(); + useTheme({ systemUIVisible: true, appThemeColor: 'base-200' }); const { settings, setSettings, saveSettings } = useSettingsStore(); const [loading, setLoading] = useState(false); const isInitiating = useRef(false); @@ -94,11 +92,6 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP }, }); - useEffect(() => { - updateAppTheme('base-200'); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useEffect(() => { const doCheckAppUpdates = async () => { if (appService?.hasUpdater && settings.autoCheckUpdates) { @@ -533,11 +526,16 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
= ({ cleanupTrafficLightListeners, } = useTrafficLightStore(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const { bookKeys, hoveredBookKey, setHoveredBookKey } = useReaderStore(); + const { bookKeys, hoveredBookKey, systemUIVisible, setHoveredBookKey } = useReaderStore(); const { isSideBarVisible } = useSidebarStore(); const iconSize16 = useResponsiveSize(16); @@ -89,20 +89,27 @@ const HeaderBar: React.FC = ({ 'bg-base-100 absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]', isVisible ? 'visible' : 'hidden', )} + style={{ + height: systemUIVisible ? 'max(env(safe-area-inset-top), 24px)' : '', + }} />
!appService?.isMobile && setHoveredBookKey('')} >
@@ -139,7 +146,10 @@ const HeaderBar: React.FC = ({ showMaximize={ bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web' } - onClose={() => onCloseBook(bookKey)} + onClose={() => { + setHoveredBookKey(null); + onCloseBook(bookKey); + }} />
diff --git a/apps/readest-app/src/app/reader/components/Reader.tsx b/apps/readest-app/src/app/reader/components/Reader.tsx index 3aa19523..7a863d3b 100644 --- a/apps/readest-app/src/app/reader/components/Reader.tsx +++ b/apps/readest-app/src/app/reader/components/Reader.tsx @@ -7,10 +7,12 @@ import { useEffect, Suspense, useRef } from 'react'; import { useEnv } from '@/context/EnvContext'; import { useTheme } from '@/hooks/useTheme'; import { useThemeStore } from '@/store/themeStore'; +import { useReaderStore } from '@/store/readerStore'; import { useLibraryStore } from '@/store/libraryStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { useSettingsStore } from '@/store/settingsStore'; import { useScreenWakeLock } from '@/hooks/useScreenWakeLock'; +import { setSystemUIVisibility } from '@/utils/bridge'; import { AboutWindow } from '@/components/AboutWindow'; import { UpdaterWindow } from '@/components/UpdaterWindow'; import { Toast } from '@/components/Toast'; @@ -19,16 +21,16 @@ import ReaderContent from './ReaderContent'; const Reader: React.FC<{ ids?: string }> = ({ ids }) => { const { envConfig, appService } = useEnv(); const { settings, setSettings } = useSettingsStore(); + const { isDarkMode } = useThemeStore(); + const { hoveredBookKey, showSystemUI, dismissSystemUI } = useReaderStore(); const { isSideBarVisible } = useSidebarStore(); const { getVisibleLibrary, setLibrary } = useLibraryStore(); const isInitiating = useRef(false); - const { updateAppTheme } = useThemeStore(); - useTheme(); + useTheme({ systemUIVisible: false, appThemeColor: 'base-100' }); useScreenWakeLock(settings.screenWakeLock); useEffect(() => { - updateAppTheme('base-100'); if (isInitiating.current) return; isInitiating.current = true; const initLibrary = async () => { @@ -42,6 +44,18 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (!appService?.isMobile) return; + const systemUIVisible = !!hoveredBookKey; + setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode }); + if (systemUIVisible) { + showSystemUI(); + } else { + dismissSystemUI(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hoveredBookKey]); + return ( getVisibleLibrary().length > 0 && settings.globalReadSettings && ( diff --git a/apps/readest-app/src/app/updater/page.tsx b/apps/readest-app/src/app/updater/page.tsx index c36914b2..31a868d3 100644 --- a/apps/readest-app/src/app/updater/page.tsx +++ b/apps/readest-app/src/app/updater/page.tsx @@ -1,10 +1,13 @@ 'use client'; import { Suspense } from 'react'; -import Spinner from '@/components/Spinner'; import { UpdaterContent } from '@/components/UpdaterWindow'; +import { useTheme } from '@/hooks/useTheme'; +import Spinner from '@/components/Spinner'; const UpdaterPage = () => { + useTheme(); + return ( { const headerRef = useRef(null); - useTheme(); + useTheme({ systemUIVisible: false }); useEffect(() => { if (!user || !token) return; diff --git a/apps/readest-app/src/components/UpdaterWindow.tsx b/apps/readest-app/src/components/UpdaterWindow.tsx index 280f36bc..6869de40 100644 --- a/apps/readest-app/src/components/UpdaterWindow.tsx +++ b/apps/readest-app/src/components/UpdaterWindow.tsx @@ -9,7 +9,6 @@ import { relaunch } from '@tauri-apps/plugin-process'; import { fetch } from '@tauri-apps/plugin-http'; import { useTranslation } from '@/hooks/useTranslation'; import { useSearchParams } from 'next/navigation'; -import { useTheme } from '@/hooks/useTheme'; import { tauriDownload } from '@/utils/transfer'; import { READEST_UPDATER_FILE, READEST_CHANGELOG_FILE } from '@/services/constants'; import packageJson from '../../package.json'; @@ -201,8 +200,6 @@ export const UpdaterContent = ({ version }: { version?: string }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [update]); - useTheme(); - useEffect(() => { setIsMounted(true); }, []); diff --git a/apps/readest-app/src/hooks/usePullToRefresh.ts b/apps/readest-app/src/hooks/usePullToRefresh.ts index 50376693..b09099ac 100644 --- a/apps/readest-app/src/hooks/usePullToRefresh.ts +++ b/apps/readest-app/src/hooks/usePullToRefresh.ts @@ -62,7 +62,10 @@ export const usePullToRefresh = (ref: React.RefObject, onTrigger return; } + const headerbar = document.querySelector('.titlebar'); const pullIndicator = document.createElement('div'); + const headerBottom = headerbar?.getBoundingClientRect().bottom || 0; + pullIndicator.style.top = `${headerBottom + 20}px`; pullIndicator.className = 'pull-indicator text-gray-500'; pullIndicator.innerHTML = ` diff --git a/apps/readest-app/src/hooks/useTheme.ts b/apps/readest-app/src/hooks/useTheme.ts index 5592260e..4fc72bbc 100644 --- a/apps/readest-app/src/hooks/useTheme.ts +++ b/apps/readest-app/src/hooks/useTheme.ts @@ -1,11 +1,28 @@ import { useEffect } from 'react'; import { useThemeStore } from '@/store/themeStore'; import { useSettingsStore } from '@/store/settingsStore'; -import { applyCustomTheme } from '@/styles/themes'; +import { applyCustomTheme, Palette } from '@/styles/themes'; +import { setSystemUIVisibility } from '@/utils/bridge'; -export const useTheme = () => { +type UseThemeProps = { + systemUIVisible?: boolean; + appThemeColor?: keyof Palette; +}; + +export const useTheme = ({ + systemUIVisible = true, + appThemeColor = 'base-100', +}: UseThemeProps = {}) => { const { settings } = useSettingsStore(); - const { themeColor, isDarkMode } = useThemeStore(); + const { themeColor, isDarkMode, updateAppTheme } = useThemeStore(); + + useEffect(() => { + updateAppTheme(appThemeColor); + console.log('useTheme systemUIVisible', systemUIVisible); + setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDarkMode]); + useEffect(() => { const customThemes = settings.globalReadSettings?.customThemes ?? []; customThemes.forEach((customTheme) => { diff --git a/apps/readest-app/src/store/readerStore.ts b/apps/readest-app/src/store/readerStore.ts index c82ffb68..f19f7422 100644 --- a/apps/readest-app/src/store/readerStore.ts +++ b/apps/readest-app/src/store/readerStore.ts @@ -29,9 +29,12 @@ interface ReaderStore { viewStates: { [key: string]: ViewState }; bookKeys: string[]; hoveredBookKey: string | null; + systemUIVisible: boolean; setBookKeys: (keys: string[]) => void; setHoveredBookKey: (key: string | null) => void; setBookmarkRibbonVisibility: (key: string, visible: boolean) => void; + showSystemUI: () => void; + dismissSystemUI: () => void; setProgress: ( key: string, @@ -63,8 +66,11 @@ export const useReaderStore = create((set, get) => ({ viewStates: {}, bookKeys: [], hoveredBookKey: null, + systemUIVisible: false, setBookKeys: (keys: string[]) => set({ bookKeys: keys }), setHoveredBookKey: (key: string | null) => set({ hoveredBookKey: key }), + showSystemUI: () => set({ systemUIVisible: true }), + dismissSystemUI: () => set({ systemUIVisible: false }), getView: (key: string | null) => (key && get().viewStates[key]?.view) || null, setView: (key: string, view) => diff --git a/apps/readest-app/src/styles/globals.css b/apps/readest-app/src/styles/globals.css index d358ec3d..d607690d 100644 --- a/apps/readest-app/src/styles/globals.css +++ b/apps/readest-app/src/styles/globals.css @@ -220,7 +220,6 @@ foliate-view { .pull-indicator { position: fixed; - top: calc(64px + env(safe-area-inset-top)); width: 100%; background-color: transparent; display: flex; diff --git a/apps/readest-app/src/utils/bridge.ts b/apps/readest-app/src/utils/bridge.ts index 82e76867..99055d80 100644 --- a/apps/readest-app/src/utils/bridge.ts +++ b/apps/readest-app/src/utils/bridge.ts @@ -23,6 +23,16 @@ export interface InstallPackageResponse { error?: string; } +export interface SetSystemUIVisibilityRequest { + visible: boolean; + darkMode: boolean; +} + +export interface SetSystemUIVisibilityResponse { + success: boolean; + error?: string; +} + export async function copyURIToPath(request: CopyURIRequest): Promise { const result = await invoke('plugin:native-bridge|copy_uri_to_path', { payload: request, @@ -45,3 +55,15 @@ export async function installPackage( }); return result; } + +export async function setSystemUIVisibility( + request: SetSystemUIVisibilityRequest, +): Promise { + const result = await invoke( + 'plugin:native-bridge|set_system_ui_visibility', + { + payload: request, + }, + ); + return result; +} diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts index f9b638e5..4b89ad79 100644 --- a/apps/readest-app/src/utils/style.ts +++ b/apps/readest-app/src/utils/style.ts @@ -419,6 +419,8 @@ export const mountAdditionalFonts = (document: Document) => { }; export const transformStylesheet = (css: string) => { + const isMobile = ['ios', 'android'].includes(getOSPlatform()); + const fontScale = isMobile ? 1.25 : 1; // replace absolute font sizes with rem units // replace hardcoded colors return css @@ -431,11 +433,11 @@ export const transformStylesheet = (css: string) => { .replace(/font-size\s*:\s*xx-large/gi, 'font-size: 2rem') .replace(/font-size\s*:\s*xxx-large/gi, 'font-size: 3rem') .replace(/font-size\s*:\s*(\d+(?:\.\d+)?)px/gi, (_, px) => { - const rem = parseFloat(px) / 16; + const rem = parseFloat(px) / fontScale / 16; return `font-size: ${rem}rem`; }) .replace(/font-size\s*:\s*(\d+(?:\.\d+)?)pt/gi, (_, pt) => { - const rem = parseFloat(pt) / 12; + const rem = parseFloat(pt) / fontScale / 12; return `font-size: ${rem}rem`; }) .replace(/[\s;]color\s*:\s*#000000/gi, 'color: var(--theme-fg-color)') diff --git a/packages/tauri b/packages/tauri index b1548268..29264c82 160000 --- a/packages/tauri +++ b/packages/tauri @@ -1 +1 @@ -Subproject commit b154826881f3aea8086b0bf4d5fdc005efd3debe +Subproject commit 29264c8244bbc75cc5360c787498a7a9637ddd9a