feat: add support for per-book background image settings in addition to global settings, also closes #2223 (#2225)

This commit is contained in:
Huang Xin
2025-10-14 14:54:26 +08:00
committed by GitHub
parent 9a991a31ba
commit 60ddb17547
8 changed files with 71 additions and 30 deletions
@@ -15,6 +15,7 @@ import { usePagination } from '../hooks/usePagination';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useProgressSync } from '../hooks/useProgressSync';
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
import { useAutoFocus } from '@/hooks/useAutoFocus';
import { useTranslation } from '@/hooks/useTranslation';
import { useKOSync } from '../hooks/useKOSync';
@@ -73,6 +74,7 @@ const FoliateViewer: React.FC<{
const { getViewState, getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { getBookData } = useBookDataStore();
const { applyBackgroundTexture } = useBackgroundTexture();
const viewState = getViewState(bookKey);
const viewSettings = getViewSettings(bookKey);
@@ -145,14 +147,19 @@ const FoliateViewer: React.FC<{
const writingDir = viewRef.current?.renderer.setStyles && getDirection(detail.doc);
const viewSettings = getViewSettings(bookKey)!;
const bookData = getBookData(bookKey)!;
viewSettings.vertical =
const newVertical =
writingDir?.vertical || viewSettings.writingMode.includes('vertical') || false;
viewSettings.rtl =
const newRtl =
writingDir?.rtl ||
getDirFromUILanguage() === 'rtl' ||
viewSettings.writingMode.includes('rl') ||
false;
if (viewSettings.vertical !== newVertical || viewSettings.rtl !== newRtl) {
viewSettings.vertical = newVertical;
viewSettings.rtl = newRtl;
setViewSettings(bookKey, { ...viewSettings });
}
mountAdditionalFonts(detail.doc, isCJKLang(bookData.book?.primaryLanguage));
@@ -395,6 +402,16 @@ const FoliateViewer: React.FC<{
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings.customFonts, envConfig]);
useEffect(() => {
if (!viewSettings) return;
applyBackgroundTexture(envConfig, viewSettings);
}, [
viewSettings?.backgroundTextureId,
viewSettings?.backgroundOpacity,
viewSettings?.backgroundSize,
applyBackgroundTexture,
]);
useEffect(() => {
if (viewRef.current && viewRef.current.renderer) {
doubleClickDisabled.current = !!viewSettings?.disableDoubleClick;
@@ -142,7 +142,11 @@ export const useProgressSync = (bookKey: string) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Object.entries(syncedConfig).filter(([_, value]) => value !== null && value !== undefined),
);
if (syncedConfig.updatedAt >= config.updatedAt) {
setConfig(bookKey, { ...config, ...filteredSyncedConfig });
} else {
setConfig(bookKey, { ...filteredSyncedConfig, ...config });
}
if (remoteCFILocation && configCFI) {
if (CFI.compare(configCFI, remoteCFILocation) < 0) {
if (view) {
+4 -15
View File
@@ -12,14 +12,14 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useDeviceControlStore } from '@/store/deviceStore';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
import { useCustomTextureStore } from '@/store/customTextureStore';
import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
import { getLocale } from '@/utils/misc';
const Providers = ({ children }: { children: React.ReactNode }) => {
const { envConfig, appService } = useEnv();
const { applyUILanguage } = useSettingsStore();
const { setScreenBrightness } = useDeviceControlStore();
const { addTexture, applyTexture } = useCustomTextureStore();
const { applyBackgroundTexture } = useBackgroundTexture();
const iconSize = useDefaultIconSize();
useSafeAreaInsets(); // Initialize safe area insets
@@ -47,21 +47,10 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
if (appService.hasScreenBrightness && brightness >= 0) {
setScreenBrightness(brightness / 100);
}
const textureId = globalViewSettings.backgroundTextureId;
const textureOpacity = globalViewSettings.backgroundOpacity;
const textureSize = globalViewSettings.backgroundSize;
if (textureId && textureId !== 'none') {
document.documentElement.style.setProperty('--bg-texture-opacity', `${textureOpacity}`);
document.documentElement.style.setProperty('--bg-texture-size', textureSize);
const customTexture = settings.customTextures?.find((t) => t.id === textureId);
if (customTexture) {
addTexture(customTexture.path);
}
applyTexture(envConfig, textureId);
}
applyBackgroundTexture(envConfig, globalViewSettings);
});
}
}, [envConfig, appService, applyUILanguage, addTexture, applyTexture, setScreenBrightness]);
}, [envConfig, appService, applyUILanguage, setScreenBrightness, applyBackgroundTexture]);
// Make sure appService is available in all children components
if (!appService) return;
@@ -142,10 +142,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
const applyBackgroundTexture = () => {
applyTexture(envConfig, selectedTextureId);
document.documentElement.style.setProperty(
'--bg-texture-opacity',
backgroundOpacity.toString(),
);
document.documentElement.style.setProperty('--bg-texture-opacity', `${backgroundOpacity}`);
document.documentElement.style.setProperty('--bg-texture-size', backgroundSize);
};
@@ -0,0 +1,31 @@
import { useCallback } from 'react';
import { useCustomTextureStore } from '@/store/customTextureStore';
import { useSettingsStore } from '@/store/settingsStore';
import { EnvConfigType } from '@/services/environment';
import { ViewSettings } from '@/types/book';
export const useBackgroundTexture = () => {
const applyBackgroundTexture = useCallback(
(envConfig: EnvConfigType, viewSettings: ViewSettings) => {
const textureId = viewSettings.backgroundTextureId;
const textureOpacity = viewSettings.backgroundOpacity;
const textureSize = viewSettings.backgroundSize;
if (!textureId || textureId === 'none') return;
document.documentElement.style.setProperty('--bg-texture-opacity', `${textureOpacity}`);
document.documentElement.style.setProperty('--bg-texture-size', textureSize);
const settings = useSettingsStore.getState().settings;
const customTexture = settings.customTextures?.find((t) => t.id === textureId);
if (customTexture) {
useCustomTextureStore.getState().addTexture(customTexture.path);
}
useCustomTextureStore.getState().applyTexture(envConfig, textureId);
},
[],
);
return { applyBackgroundTexture };
};
+1 -1
View File
@@ -155,7 +155,7 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
overrideColor: false,
backgroundTextureId: 'none',
backgroundOpacity: 0.6,
backgroundSize: 'auto',
backgroundSize: 'cover',
codeHighlighting: false,
codeLanguage: 'auto-detect',
userStylesheet: '',
+1 -2
View File
@@ -288,10 +288,9 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
const oldConfig = bookData.config;
const newConfig = {
...bookData.config,
updatedAt: Date.now(),
progress,
location,
};
} as BookConfig;
useBookDataStore.setState((state) => ({
booksData: {
+8 -4
View File
@@ -59,7 +59,7 @@ const createTextureCSS = (texture: BackgroundTexture) => {
body::before, .sidebar-container::before, .notebook-container::before,
.foliate-viewer::before {
content: "";
position: fixed;
position: absolute;
top: 0;
left: 0;
right: 0;
@@ -68,10 +68,14 @@ const createTextureCSS = (texture: BackgroundTexture) => {
z-index: 0;
background-image: url("${texture.blobUrl || texture.url}");
background-repeat: repeat;
background-size: var(--bg-texture-size, auto);
mix-blend-mode: var(--bg-texture-blend-mode, normal);
background-size: var(--bg-texture-size, cover);
mix-blend-mode: var(--bg-texture-blend-mode, multiply);
opacity: var(--bg-texture-opacity, 0.6);
}`;
}
body::before {
height: 100vh;
}
`;
return css;
};