feat: immersive UI in reader page on iOS and Android, closes #886 and closes #864 (#911)

This commit is contained in:
Huang Xin
2025-04-19 22:43:39 +08:00
committed by GitHub
parent 759779a98d
commit cf66665096
32 changed files with 327 additions and 60 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ yarn-error.log*
/private_keys
# plists
*.plist
Entitlements*.plist
# local confs
tauri.*.conf.json
+6
View File
@@ -6,6 +6,12 @@
<false/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIHomeIndicatorAutoHidden</key>
<true/>
<key>UIRequiresFullScreen</key>
<true/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
@@ -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
)
}
}
}
@@ -2,8 +2,12 @@
<!-- Base application theme. -->
<style name="Theme.readest" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:fitsSystemWindows">false</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
</resources>
@@ -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)
}
}
@@ -4,6 +4,7 @@ const COMMANDS: &[&str] = &[
"copy_uri_to_path",
"use_background_audio",
"install_package",
"set_system_ui_visibility",
];
fn main() {
@@ -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()
}
}
}
@@ -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"]
@@ -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.
<tr>
<td>
`native-bridge:allow-set-system-ui-visibility`
</td>
<td>
Enables the set_system_ui_visibility command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-set-system-ui-visibility`
</td>
<td>
Denies the set_system_ui_visibility command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-use-background-audio`
</td>
@@ -6,4 +6,5 @@ permissions = [
"allow-copy-uri-to-path",
"allow-use-background-audio",
"allow-install-package",
"allow-set-system-ui-visibility",
]
@@ -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`"
}
]
}
@@ -43,3 +43,11 @@ pub(crate) async fn install_package<R: Runtime>(
) -> Result<InstallPackageResponse> {
app.native_bridge().install_package(payload)
}
#[command]
pub(crate) async fn set_system_ui_visibility<R: Runtime>(
app: AppHandle<R>,
payload: SetSystemUIVisibilityRequest,
) -> Result<SetSystemUIVisibilityResponse> {
app.native_bridge().set_system_ui_visibility(payload)
}
@@ -36,4 +36,11 @@ impl<R: Runtime> NativeBridge<R> {
) -> crate::Result<InstallPackageResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn set_system_ui_visibility(
&self,
_payload: SetSystemUIVisibilityRequest,
) -> crate::Result<SetSystemUIVisibilityResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
}
@@ -42,6 +42,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::copy_uri_to_path,
commands::use_background_audio,
commands::install_package,
commands::set_system_ui_visibility,
])
.setup(|app, api| {
#[cfg(mobile)]
@@ -66,3 +66,14 @@ impl<R: Runtime> NativeBridge<R> {
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn set_system_ui_visibility(
&self,
payload: SetSystemUIVisibilityRequest,
) -> crate::Result<SetSystemUIVisibilityResponse> {
self.0
.run_mobile_plugin("set_system_ui_visibility", payload)
.map_err(Into::into)
}
}
@@ -44,3 +44,17 @@ pub struct InstallPackageResponse {
pub success: bool,
pub error: Option<String>,
}
#[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<String>,
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { useTheme } from '@/hooks/useTheme';
export default function AuthErrorPage() {
const router = useRouter();
useTheme();
useTheme({ systemUIVisible: false });
useEffect(() => {
const timer = setTimeout(() => {
+3
View File
@@ -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<HTMLDivElement>(null);
useTheme({ systemUIVisible: false });
const getTauriRedirectTo = (isOAuth: boolean) => {
if (process.env.NODE_ENV === 'production' || appService?.isMobile || USE_APPLE_SIGN_IN) {
if (appService?.isMobile) {
@@ -92,10 +92,12 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
<div
ref={headerRef}
className={clsx(
'titlebar z-10 flex h-[52px] w-full items-center py-2 pr-4 sm:h-[48px] sm:pr-6',
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
'titlebar bg-base-200 z-10 flex h-[52px] w-full items-center py-2 pr-4 sm:h-[48px] sm:pr-6',
isTrafficLightVisible ? 'pl-16' : 'pl-0 sm:pl-2',
)}
style={{
marginTop: appService?.hasSafeAreaInset ? 'max(env(safe-area-inset-top), 24px)' : '',
}}
>
<div className='flex w-full items-center justify-between space-x-6 sm:space-x-12'>
<div className='exclude-title-bar-mousedown relative flex w-full items-center pl-4'>
+8 -10
View File
@@ -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
<div
ref={containerRef}
className={clsx(
'scroll-container drop-zone mt-[48px] flex-grow overflow-y-auto px-4 sm:px-2',
appService?.hasSafeAreaInset && 'mt-[calc(52px+env(safe-area-inset-top))]',
'scroll-container drop-zone flex-grow overflow-y-auto px-4 sm:px-2',
appService?.hasSafeAreaInset && 'pt-[52px]',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
isDragging && 'drag-over',
)}
style={{
marginTop: appService?.hasSafeAreaInset
? 'max(env(safe-area-inset-top), 24px)'
: '48px',
}}
>
<DropIndicator />
<Bookshelf
@@ -42,7 +42,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
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<HeaderBarProps> = ({
'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)' : '',
}}
/>
<div
ref={headerRef}
className={clsx(
`header-bar bg-base-100 absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
`shadow-xs transition-opacity duration-300`,
`shadow-xs transition-[opacity,margin-top] duration-300`,
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
appService?.hasRoundedWindow && 'rounded-window-top-right',
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-top-left',
isHoveredAnim && 'hover-bar-anim',
isVisible ? 'pointer-events-auto visible' : 'pointer-events-none opacity-0',
isDropdownOpen && 'header-bar-pinned',
)}
style={{
marginTop: systemUIVisible
? 'max(env(safe-area-inset-top), 24px)'
: 'env(safe-area-inset-top)',
}}
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
>
<div className='sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4'>
@@ -139,7 +146,10 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
showMaximize={
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
}
onClose={() => onCloseBook(bookKey)}
onClose={() => {
setHoveredBookKey(null);
onCloseBook(bookKey);
}}
/>
</div>
</div>
@@ -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 && (
+4 -1
View File
@@ -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 (
<Suspense
fallback={
+1 -1
View File
@@ -34,7 +34,7 @@ const ProfilePage = () => {
const headerRef = useRef<HTMLDivElement>(null);
useTheme();
useTheme({ systemUIVisible: false });
useEffect(() => {
if (!user || !token) return;
@@ -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);
}, []);
@@ -62,7 +62,10 @@ export const usePullToRefresh = (ref: React.RefObject<HTMLDivElement>, 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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
+20 -3
View File
@@ -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) => {
@@ -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<ReaderStore>((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) =>
-1
View File
@@ -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;
+22
View File
@@ -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<CopyURIResponse> {
const result = await invoke<CopyURIResponse>('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<SetSystemUIVisibilityResponse> {
const result = await invoke<SetSystemUIVisibilityResponse>(
'plugin:native-bridge|set_system_ui_visibility',
{
payload: request,
},
);
return result;
}
+4 -2
View File
@@ -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)')