7d1a60b9ea
* feat(library): separate background texture for library and reader (#4743) The library and reader shared a single background texture, so a reader backdrop with borders or other reading-oriented decoration looked wrong on the bookshelf. Let users set them independently. - Add device-local libraryBackground{TextureId,Opacity,Size} to SystemSettings. Each field falls back to the reader/global value when unset (getLibraryViewSettings), so an existing bookshelf looks unchanged until the user picks a library texture, then decouples per-field. No migration needed; the selection stays per-device like the reader's, while imported images keep syncing via the texture kind. - Make the Color panel's Background Image picker context-aware: opened from the library it edits the library texture, opened while reading it edits the reader texture. A sublabel states which page it applies to. - Apply the library texture at boot and on every library mount, so returning from a textured book restores the bookshelf background. - useBackgroundTexture now unmounts on 'none' instead of early-returning, since library and reader share one style element: switching a page to None must clear a texture the other page mounted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n: translate library/reader background texture labels (#4743) Add translations for the two new context sublabels ("Applies to the Library" / "Applies to the Reader") across all 33 locales, anchored to each locale's existing Library and reading terminology. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
3.8 KiB
TypeScript
97 lines
3.8 KiB
TypeScript
import { ViewSettings } from '@/types/book';
|
|
import { SystemSettings } from '@/types/settings';
|
|
import { EnvConfigType } from '@/services/environment';
|
|
import { useBookDataStore } from '@/store/bookDataStore';
|
|
import { useReaderStore } from '@/store/readerStore';
|
|
import { useSettingsStore } from '@/store/settingsStore';
|
|
import { getStyles } from '@/utils/style';
|
|
|
|
/**
|
|
* Resolve the effective background texture for the library page (issue #4743).
|
|
* The library texture is stored separately from the reader's, but each field
|
|
* falls back to the reader/global value when unset — so the bookshelf inherits
|
|
* the current look until the user explicitly picks a library texture, then
|
|
* decouples per-field. Returns a `ViewSettings` so it can be handed straight to
|
|
* `useBackgroundTexture().applyBackgroundTexture`.
|
|
*/
|
|
export const getLibraryViewSettings = (settings: SystemSettings): ViewSettings => {
|
|
// globalViewSettings can be absent on the very first renders — the store
|
|
// starts as `{} as SystemSettings` until appService.loadSettings() runs — so
|
|
// every read is optional and falls back to a no-texture default.
|
|
const globalViewSettings = settings.globalViewSettings;
|
|
return {
|
|
...globalViewSettings,
|
|
backgroundTextureId:
|
|
settings.libraryBackgroundTextureId ?? globalViewSettings?.backgroundTextureId ?? 'none',
|
|
backgroundOpacity:
|
|
settings.libraryBackgroundOpacity ?? globalViewSettings?.backgroundOpacity ?? 0.6,
|
|
backgroundSize: settings.libraryBackgroundSize ?? globalViewSettings?.backgroundSize ?? 'cover',
|
|
};
|
|
};
|
|
|
|
export const saveViewSettings = async <K extends keyof ViewSettings>(
|
|
envConfig: EnvConfigType,
|
|
bookKey: string,
|
|
key: K,
|
|
value: ViewSettings[K],
|
|
skipGlobal = false,
|
|
applyStyles = true,
|
|
) => {
|
|
const { settings, setSettings, saveSettings } = useSettingsStore.getState();
|
|
const { bookKeys, getView, getViewState, getViewSettings, setViewSettings } =
|
|
useReaderStore.getState();
|
|
const { getConfig, saveConfig } = useBookDataStore.getState();
|
|
|
|
const applyViewSettings = async (bookKey: string) => {
|
|
const viewSettings = getViewSettings(bookKey);
|
|
const viewState = getViewState(bookKey);
|
|
if (bookKey && viewSettings && viewSettings[key] !== value) {
|
|
viewSettings[key] = value;
|
|
setViewSettings(bookKey, viewSettings);
|
|
if (applyStyles) {
|
|
const view = getView(bookKey);
|
|
view?.renderer.setStyles?.(getStyles(viewSettings));
|
|
}
|
|
const config = getConfig(bookKey);
|
|
if (viewState?.isPrimary && config) {
|
|
await saveConfig(envConfig, bookKey, config, settings);
|
|
}
|
|
}
|
|
};
|
|
|
|
const isSettingsGlobal = getViewSettings(bookKey)?.isGlobal ?? true;
|
|
if (isSettingsGlobal && !skipGlobal) {
|
|
// Build a NEW settings object (and a NEW globalViewSettings) so the
|
|
// settingsStore subscriber that gates replica push fires — it compares
|
|
// `state.settings !== prev.settings`, so an in-place mutation followed
|
|
// by setSettings(same_ref) silently bypasses the publish path and
|
|
// whitelisted writes (userStylesheet, userUIStylesheet) only ship
|
|
// on the next unrelated setSettings call.
|
|
const nextSettings: SystemSettings = {
|
|
...settings,
|
|
globalViewSettings: { ...settings.globalViewSettings, [key]: value },
|
|
};
|
|
setSettings(nextSettings);
|
|
|
|
for (const bookKey of bookKeys) {
|
|
await applyViewSettings(bookKey);
|
|
}
|
|
await saveSettings(envConfig, nextSettings);
|
|
} else if (bookKey) {
|
|
await applyViewSettings(bookKey);
|
|
}
|
|
};
|
|
|
|
export const saveSysSettings = async <K extends keyof SystemSettings>(
|
|
envConfig: EnvConfigType,
|
|
key: K,
|
|
value: SystemSettings[K],
|
|
) => {
|
|
const { settings, setSettings, saveSettings } = useSettingsStore.getState();
|
|
if (settings[key] !== value) {
|
|
settings[key] = value;
|
|
setSettings(settings);
|
|
await saveSettings(envConfig, settings);
|
|
}
|
|
};
|