Files
readest/apps/readest-app/src/hooks/useTheme.ts
T
Huang Xin b71b246601 feat(settings): add TTS settings tab and highlight opacity, closes #3661 (#3712)
Add a new TTS tab in settings with media metadata update frequency control
(sentence/paragraph/chapter) to reduce Bluetooth notification spam, and move
TTS highlight settings from Color tab to the new TTS tab. Also add a highlight
opacity setting with live preview in the Color tab.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 06:35:26 +02:00

149 lines
5.3 KiB
TypeScript

import { useCallback, useEffect, useRef } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useSafeAreaInsets } from './useSafeAreaInsets';
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;
appThemeColor?: keyof Palette;
};
export const useTheme = ({
systemUIVisible = true,
appThemeColor = 'base-100',
}: UseThemeProps = {}) => {
const { appService } = useEnv();
const { settings } = useSettingsStore();
const isEink = settings?.globalViewSettings?.isEink;
const isColorEink = settings?.globalViewSettings?.isColorEink;
const isBwEink = isEink && !isColorEink;
const highlightOpacity = settings?.globalViewSettings?.highlightOpacity ?? 0.4;
const {
themeColor,
isDarkMode,
showSystemUI,
dismissSystemUI,
updateAppTheme,
setStatusBarHeight,
systemUIAlwaysHidden,
setSystemUIAlwaysHidden,
} = useThemeStore();
const { onUpdateInsets } = useSafeAreaInsets();
const useFallbackColors = useRef(false);
useEffect(() => {
updateAppTheme(appThemeColor);
if (appService?.isAndroidApp) {
getStatusBarHeight().then((res) => {
if (res.height && res.height > 0) {
setStatusBarHeight(res.height / window.devicePixelRatio);
}
});
handleSystemUIVisibility(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService?.isAndroidApp]);
const handleSystemUIVisibility = useCallback(
(updateInsets = false) => {
if (!appService?.isMobileApp) return;
const visible = !!(systemUIVisible && !systemUIAlwaysHidden);
if (visible) {
showSystemUI();
} else {
dismissSystemUI();
}
setSystemUIVisibility({ visible, darkMode: isDarkMode }).then(() => {
if (updateInsets) {
onUpdateInsets();
}
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[appService, isDarkMode, systemUIVisible],
);
useEffect(() => {
if (appService?.isMobileApp) {
handleSystemUIVisibility();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleSystemUIVisibility]);
useEffect(() => {
if (!appService?.isMobileApp) return;
handleSystemUIVisibility();
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
handleSystemUIVisibility();
}
};
const handleOrientationChange = () => {
if (appService?.isIOSApp && getOSPlatform() === 'ios') {
// FIXME: This is a workaround for iPhone apps where the system UI is not visible in landscape mode
// when the app is in fullscreen mode until we find a better solution to override the prefersStatusBarHidden
// in the ViewController. Note that screen.orientation.type is not abailable in iOS before 16.4.
const systemUIAlwaysHidden = screen.orientation?.type.includes('landscape');
setSystemUIAlwaysHidden(systemUIAlwaysHidden);
handleSystemUIVisibility();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
screen.orientation?.addEventListener('change', handleOrientationChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
screen.orientation?.removeEventListener('change', handleOrientationChange);
};
// 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, undefined, useFallbackColors.current);
});
localStorage.setItem('customThemes', JSON.stringify(customThemes));
}, [settings.globalReadSettings?.customThemes]);
useEffect(() => {
const colorScheme = isDarkMode ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', `${themeColor}-${colorScheme}`);
document.documentElement.style.setProperty('color-scheme', colorScheme);
document.documentElement.style.setProperty('--scroll-bg-opacity', isBwEink ? '1.0' : '0.5');
document.documentElement.style.setProperty(
'--overlayer-highlight-opacity',
isBwEink ? '1.0' : String(highlightOpacity),
);
document.documentElement.style.setProperty(
'--overlayer-highlight-blend-mode',
isBwEink ? 'difference' : isDarkMode ? 'screen' : 'multiply',
);
document.documentElement.style.setProperty(
'--bg-texture-blend-mode',
isDarkMode ? 'lighten' : 'multiply',
);
}, [themeColor, isDarkMode, isBwEink, highlightOpacity]);
};