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;
+};