From cdd274e5ca6e4b379cf0bb28841b9ac412b17849 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 1 Oct 2025 01:14:35 +0800 Subject: [PATCH] compat: add support for WebView down to version 92, closes #2139 (#2145) --- apps/readest-app/src/app/auth/page.tsx | 2 +- apps/readest-app/src/app/error.tsx | 4 +- .../src/app/library/components/Bookshelf.tsx | 25 +++++--- .../app/library/components/BookshelfItem.tsx | 2 +- .../src/app/library/components/GroupItem.tsx | 7 +- apps/readest-app/src/app/library/page.tsx | 2 +- .../src/app/reader/components/Reader.tsx | 2 +- .../app/reader/components/ReaderContent.tsx | 5 +- apps/readest-app/src/app/user/page.tsx | 2 +- .../src/components/AboutWindow.tsx | 4 +- apps/readest-app/src/globals.d.ts | 7 ++ apps/readest-app/src/hooks/useTheme.ts | 23 ++++++- apps/readest-app/src/styles/themes.ts | 64 +++++++++++++++---- apps/readest-app/src/utils/color.ts | 4 ++ apps/readest-app/src/utils/ua.ts | 8 ++- 15 files changed, 118 insertions(+), 43 deletions(-) create mode 100644 apps/readest-app/src/globals.d.ts diff --git a/apps/readest-app/src/app/auth/page.tsx b/apps/readest-app/src/app/auth/page.tsx index c5700991..55e3a126 100644 --- a/apps/readest-app/src/app/auth/page.tsx +++ b/apps/readest-app/src/app/auth/page.tsx @@ -345,7 +345,7 @@ export default function AuthPage() {
diff --git a/apps/readest-app/src/app/error.tsx b/apps/readest-app/src/app/error.tsx index 4314a0b4..85c85f47 100644 --- a/apps/readest-app/src/app/error.tsx +++ b/apps/readest-app/src/app/error.tsx @@ -4,7 +4,7 @@ import posthog from 'posthog-js'; import { useEffect, useState } from 'react'; import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; -import { parseWebViewVersion } from '@/utils/ua'; +import { parseWebViewInfo } from '@/utils/ua'; import { handleGlobalError } from '@/utils/error'; interface ErrorPageProps { @@ -18,7 +18,7 @@ export default function Error({ error, reset }: ErrorPageProps) { const [browserInfo, setBrowserInfo] = useState(''); useEffect(() => { - setBrowserInfo(parseWebViewVersion(appService)); + setBrowserInfo(parseWebViewInfo(appService)); posthog.captureException(error); handleGlobalError(error); }, [appService, error]); diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index a16f2729..975f8c98 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -353,16 +353,21 @@ const Bookshelf: React.FC = ({ /> ))} {viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && ( - +
+ +
)}
{loading && ( diff --git a/apps/readest-app/src/app/library/components/BookshelfItem.tsx b/apps/readest-app/src/app/library/components/BookshelfItem.tsx index 27fd22a4..322ebd7a 100644 --- a/apps/readest-app/src/app/library/components/BookshelfItem.tsx +++ b/apps/readest-app/src/app/library/components/BookshelfItem.tsx @@ -342,7 +342,7 @@ const BookshelfItem: React.FC = ({ onKeyDown={handleKeyDown} {...handlers} > -
+
{'format' in item ? ( = ({ mode, group, isSelectMode, groupS }; return ( -
+
diff --git a/apps/readest-app/src/app/reader/components/Reader.tsx b/apps/readest-app/src/app/reader/components/Reader.tsx index 38fb13e3..44ced651 100644 --- a/apps/readest-app/src/app/reader/components/Reader.tsx +++ b/apps/readest-app/src/app/reader/components/Reader.tsx @@ -142,7 +142,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
diff --git a/apps/readest-app/src/app/reader/components/ReaderContent.tsx b/apps/readest-app/src/app/reader/components/ReaderContent.tsx index 0eb7b743..34d6f7b1 100644 --- a/apps/readest-app/src/app/reader/components/ReaderContent.tsx +++ b/apps/readest-app/src/app/reader/components/ReaderContent.tsx @@ -1,6 +1,5 @@ 'use client'; -import clsx from 'clsx'; import React, { useEffect, useRef, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; @@ -185,7 +184,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i setTimeout(() => setLoading(true), 200); return ( loading && ( -
+
) @@ -193,7 +192,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i } return ( -
+
diff --git a/apps/readest-app/src/app/user/page.tsx b/apps/readest-app/src/app/user/page.tsx index a6f7f53d..163ca221 100644 --- a/apps/readest-app/src/app/user/page.tsx +++ b/apps/readest-app/src/app/user/page.tsx @@ -341,7 +341,7 @@ const ProfilePage = () => {
diff --git a/apps/readest-app/src/components/AboutWindow.tsx b/apps/readest-app/src/components/AboutWindow.tsx index f8095170..b171f1f6 100644 --- a/apps/readest-app/src/components/AboutWindow.tsx +++ b/apps/readest-app/src/components/AboutWindow.tsx @@ -3,7 +3,7 @@ import Image from 'next/image'; import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater'; -import { parseWebViewVersion } from '@/utils/ua'; +import { parseWebViewInfo } from '@/utils/ua'; import { getAppVersion } from '@/utils/version'; import SupportLinks from './SupportLinks'; import LegalLinks from './LegalLinks'; @@ -30,7 +30,7 @@ export const AboutWindow = () => { const [isOpen, setIsOpen] = useState(false); useEffect(() => { - setBrowserInfo(parseWebViewVersion(appService)); + setBrowserInfo(parseWebViewInfo(appService)); const handleCustomEvent = (event: CustomEvent) => { setIsOpen(event.detail.visible); diff --git a/apps/readest-app/src/globals.d.ts b/apps/readest-app/src/globals.d.ts new file mode 100644 index 00000000..1dbd085f --- /dev/null +++ b/apps/readest-app/src/globals.d.ts @@ -0,0 +1,7 @@ +declare module '*.css' { + interface IClassNames { + [className: string]: string; + } + const classNames: IClassNames; + export default classNames; +} diff --git a/apps/readest-app/src/hooks/useTheme.ts b/apps/readest-app/src/hooks/useTheme.ts index 3b7698c3..c9162b78 100644 --- a/apps/readest-app/src/hooks/useTheme.ts +++ b/apps/readest-app/src/hooks/useTheme.ts @@ -1,10 +1,11 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { useEnv } from '@/context/EnvContext'; import { useThemeStore } from '@/store/themeStore'; import { useSettingsStore } from '@/store/settingsStore'; -import { applyCustomTheme, Palette } from '@/styles/themes'; +import { themes, applyCustomTheme, Palette } from '@/styles/themes'; import { getStatusBarHeight, setSystemUIVisibility } from '@/utils/bridge'; import { getOSPlatform } from '@/utils/misc'; +import { parseWebViewVersion } from '@/utils/ua'; type UseThemeProps = { systemUIVisible?: boolean; @@ -28,6 +29,8 @@ export const useTheme = ({ setSystemUIAlwaysHidden, } = useThemeStore(); + const useFallbackColors = useRef(false); + useEffect(() => { updateAppTheme(appThemeColor); if (appService?.isAndroidApp) { @@ -88,10 +91,24 @@ export const useTheme = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [handleSystemUIVisibility]); + useEffect(() => { + if (!appService?.isAndroidApp) return; + const webViewVersion = parseWebViewVersion(appService); + // OKLCH color model is supported in Chromium 111+ + useFallbackColors.current = webViewVersion < 111; + }, [appService]); + + useEffect(() => { + if (!themeColor || !themes.find((t) => t.name === themeColor)) return; + if (useFallbackColors.current) { + applyCustomTheme(undefined, themeColor, true); + } + }, [themeColor]); + useEffect(() => { const customThemes = settings.globalReadSettings?.customThemes ?? []; customThemes.forEach((customTheme) => { - applyCustomTheme(customTheme); + applyCustomTheme(customTheme, undefined, useFallbackColors.current); }); localStorage.setItem('customThemes', JSON.stringify(customThemes)); }, [settings.globalReadSettings?.customThemes]); diff --git a/apps/readest-app/src/styles/themes.ts b/apps/readest-app/src/styles/themes.ts index 44eae235..52d0fca0 100644 --- a/apps/readest-app/src/styles/themes.ts +++ b/apps/readest-app/src/styles/themes.ts @@ -1,6 +1,6 @@ import tinycolor from 'tinycolor2'; import { stubTranslation as _ } from '../utils/misc'; -import { getContrastOklch, hexToOklch } from '../utils/color'; +import { getContrastHex, getContrastOklch, hexToOklch } from '../utils/color'; export type BaseColor = { bg: string; @@ -160,8 +160,8 @@ export const themes = [ }, ] as Theme[]; -const generateCustomThemeVariables = (palette: Palette): string => { - return ` +const generateCustomThemeVariables = (palette: Palette, fallbackIncluded = false): string => { + const colors = ` --b1: ${hexToOklch(palette['base-100'])}; --b2: ${hexToOklch(palette['base-200'])}; --b3: ${hexToOklch(palette['base-300'])}; @@ -188,22 +188,64 @@ const generateCustomThemeVariables = (palette: Palette): string => { --er: 70.9% 0.184 22; --erc: 100% 0 0; `; + + const fallbackColors = ` + --fallback-b1: ${palette['base-100']}; + --fallback-b2: ${palette['base-200']}; + --fallback-b3: ${palette['base-300']}; + --fallback-bc: ${palette['base-content']}; + + --fallback-p: ${palette.primary}; + --fallback-pc: ${getContrastHex(palette.primary)}; + + --fallback-s: ${palette.secondary}; + --fallback-sc: ${getContrastHex(palette.secondary)}; + + --fallback-a: ${palette.accent}; + --fallback-ac: ${getContrastHex(palette.accent)}; + + --fallback-n: ${palette.neutral}; + --fallback-nc: ${palette['neutral-content']}; + + --fallback-in: #ff0000; + --fallback-inc: #ffffff; + --fallback-su: #00ff00; + --fallback-suc: #000000; + --fallback-wa: #ffff00; + --fallback-wac: #000000; + --fallback-er: #ff8000; + --fallback-erc: #000000; + `; + + return colors + (fallbackIncluded ? fallbackColors : ''); }; -export const applyCustomTheme = (customTheme: CustomTheme) => { - const lightPalette = generateLightPalette(customTheme.colors.light); - const darkPalette = generateDarkPalette(customTheme.colors.dark); +export const applyCustomTheme = ( + customTheme?: CustomTheme, + themeName?: string, + fallbackIncluded = false, +) => { + if (!customTheme && !themeName) return; - const lightThemeName = `${customTheme.name}-light`; - const darkThemeName = `${customTheme.name}-dark`; + const lightThemeName = customTheme ? `${customTheme.name}-light` : `${themeName}-light`; + const darkThemeName = customTheme ? `${customTheme.name}-dark` : `${themeName}-dark`; + + console.log('themeName', themeName); + const lightPalette = customTheme + ? generateLightPalette(customTheme.colors.light) + : (themes.find((t) => t.name === themeName) || themes[0]!).colors.light; + + const darkPalette = customTheme + ? generateDarkPalette(customTheme.colors.dark) + : (themes.find((t) => t.name === themeName) || themes[0]!).colors.dark; const css = ` [data-theme="${lightThemeName}"] { - ${generateCustomThemeVariables(lightPalette)} + ${generateCustomThemeVariables(lightPalette, fallbackIncluded)} } [data-theme="${darkThemeName}"] { - ${generateCustomThemeVariables(darkPalette)} + ${generateCustomThemeVariables(darkPalette, fallbackIncluded)} } :root { @@ -213,7 +255,7 @@ export const applyCustomTheme = (customTheme: CustomTheme) => { `; const styleElement = document.createElement('style'); - styleElement.id = `theme-${lightThemeName}-styles`; + styleElement.id = `theme-${customTheme ? customTheme.name : themeName}-styles`; styleElement.textContent = css; const existingStyle = document.getElementById(styleElement.id); diff --git a/apps/readest-app/src/utils/color.ts b/apps/readest-app/src/utils/color.ts index c7211939..53d06a9f 100644 --- a/apps/readest-app/src/utils/color.ts +++ b/apps/readest-app/src/utils/color.ts @@ -38,3 +38,7 @@ export function hexToOklch(hexColor: string): string { export const getContrastOklch = (hexColor: string): string => { return tinycolor(hexColor).isDark() ? '100% 0 0' : '0% 0 0'; }; + +export const getContrastHex = (hex: string): string => { + return tinycolor(hex).isDark() ? '#FFFFFF' : '#000000'; +}; diff --git a/apps/readest-app/src/utils/ua.ts b/apps/readest-app/src/utils/ua.ts index 8918ec7c..0906c4a6 100644 --- a/apps/readest-app/src/utils/ua.ts +++ b/apps/readest-app/src/utils/ua.ts @@ -1,6 +1,6 @@ import { AppService } from '@/types/system'; -export const parseWebViewVersion = (appService: AppService | null): string => { +export const parseWebViewInfo = (appService: AppService | null): string => { const ua = navigator.userAgent; if (appService?.isAndroidApp) { @@ -63,3 +63,9 @@ export const parseWebViewVersion = (appService: AppService | null): string => { return 'Unknown'; } }; + +export const parseWebViewVersion = (appService: AppService | null): number => { + const webViewInfo = parseWebViewInfo(appService); + const versionMatch = webViewInfo.match(/([0-9]+)\./); + return versionMatch ? parseFloat(versionMatch[1]!) : 0; +};