diff --git a/apps/readest-app/src/__tests__/utils/style-get-styles.test.ts b/apps/readest-app/src/__tests__/utils/style-get-styles.test.ts index eb752073..81683870 100644 --- a/apps/readest-app/src/__tests__/utils/style-get-styles.test.ts +++ b/apps/readest-app/src/__tests__/utils/style-get-styles.test.ts @@ -571,6 +571,22 @@ describe('getColorStyles branches (via getStyles)', () => { expect(css).not.toMatch(/body\.pbg\s*\{[^}]*background-color:[^}]*!important/); }); + it('overrides inline white backgrounds in dark mode when overrideColor is false', () => { + const vs = makeViewSettings({ overrideColor: false }); + const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' }); + const css = getStyles(vs, theme); + expect(css).toContain('background-color: #1a1a1a !important'); + expect(css).toContain('background-color: #fff"]'); + expect(css).toContain('body.theme-dark'); + }); + + it('does not add inline white background overrides in light mode', () => { + const vs = makeViewSettings({ overrideColor: false }); + const theme = makeThemeCode({ isDarkMode: false }); + const css = getStyles(vs, theme); + expect(css).not.toContain('background-color: #fff"]'); + }); + it('applies dark mode link color lightblue when overrideColor is false', () => { const vs = makeViewSettings({ overrideColor: false }); const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' }); diff --git a/apps/readest-app/src/__tests__/utils/style.test.ts b/apps/readest-app/src/__tests__/utils/style.test.ts index ee8ecd2f..3ea71ffb 100644 --- a/apps/readest-app/src/__tests__/utils/style.test.ts +++ b/apps/readest-app/src/__tests__/utils/style.test.ts @@ -313,6 +313,27 @@ describe('transformStylesheet', () => { expect(result).toBe(css); }); }); + + describe('dark mode light backgrounds', () => { + it('rewrites white backgrounds in EPUB CSS to theme bg', () => { + localStorage.setItem('themeMode', 'dark'); + localStorage.setItem('themeColor', 'default'); + localStorage.setItem('systemIsDarkMode', 'false'); + const css = '.callout { background-color: #ffffff; padding: 1em; }'; + const result = transformStylesheet(css, VW, VH, VERTICAL); + expect(result).toMatch(/background-color:\s*#[0-9a-f]{6}/i); + expect(result).not.toContain('background-color: #ffffff'); + localStorage.removeItem('themeMode'); + }); + + it('leaves dark backgrounds unchanged in dark mode', () => { + localStorage.setItem('themeMode', 'dark'); + const css = '.panel { background-color: #222; }'; + const result = transformStylesheet(css, VW, VH, VERTICAL); + expect(result).toContain('background-color: #222'); + localStorage.removeItem('themeMode'); + }); + }); }); describe('getFootnoteStyles', () => { diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts index 99f2da43..8f4356bd 100644 --- a/apps/readest-app/src/utils/style.ts +++ b/apps/readest-app/src/utils/style.ts @@ -108,6 +108,54 @@ const getFontStyles = ( return fontStyles; }; +/** True for #fff, #f5f5f5, rgb(255,…), etc. Used when rewriting EPUB CSS in dark mode. */ +const isLightCssColor = (value: string): boolean => { + const v = value.trim().toLowerCase(); + if (v === 'white') return true; + const hex = v.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (hex) { + const h = hex[1]!; + const expand = + h.length === 3 + ? h + .split('') + .map((c) => c + c) + .join('') + : h; + const r = parseInt(expand.slice(0, 2), 16); + const g = parseInt(expand.slice(2, 4), 16); + const b = parseInt(expand.slice(4, 6), 16); + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance > 0.85; + } + const rgb = v.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/); + if (rgb?.[1] != null && rgb[2] != null && rgb[3] != null) { + const r = parseInt(rgb[1], 10); + const g = parseInt(rgb[2], 10); + const b = parseInt(rgb[3], 10); + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance > 0.85; + } + return false; +}; + +const getDarkModeLightBackgroundOverrides = (bg: string) => ` + /* Callout boxes often use inline white/light backgrounds while html/body set dark fg. */ + *[style*="background-color: #fff"], *[style*="background-color:#fff"], + *[style*="background-color: #ffffff"], *[style*="background-color:#ffffff"], + *[style*="background-color: white"], *[style*="background-color:white"], + *[style*="background: #fff"], *[style*="background:#fff"], + *[style*="background: #ffffff"], *[style*="background:#ffffff"], + *[style*="background: white"], *[style*="background:white"], + *[style*="background-color: rgb(255"], *[style*="background-color:rgb(255"], + *[style*="background: rgb(255"], *[style*="background:rgb(255"] { + background-color: ${bg} !important; + } + body.theme-dark { + background-color: ${bg} !important; + } +`; + const getEinkSelectionStyles = () => { return ` ::selection { @@ -222,6 +270,7 @@ const getColorStyles = ( *[style*="color:#000"], *[style*="color:#000000"], *[style*="color:black"] { color: ${fg} !important; } + ${isDarkMode && !overrideColor ? getDarkModeLightBackgroundOverrides(bg) : ''} /* for the Gutenberg eBooks */ #pg-header * { color: inherit !important; @@ -975,6 +1024,22 @@ export const transformStylesheet = (css: string, vw: number, vh: number, vertica .replace(/([\s;])color\s*:\s*#000000/gi, '$1color: var(--theme-fg-color)') .replace(/([\s;])color\s*:\s*#000/gi, '$1color: var(--theme-fg-color)') .replace(/([\s;])color\s*:\s*rgb\(0,\s*0,\s*0\)/gi, '$1color: var(--theme-fg-color)'); + + const { isDarkMode, bg } = getThemeCode(); + if (isDarkMode) { + css = css.replace(ruleRegex, (match, selector, block) => { + const rewritten = block.replace( + /background(-color)?\s*:\s*([^;!}]+)(\s*!important)?(?=\s*[;!}])/gi, + (decl: string, _prop: string, value: string, important?: string) => { + const raw = value.trim().split(/\s+/)[0] ?? ''; + if (!isLightCssColor(raw)) return decl; + return `background-color: ${bg}${important ?? ''}`; + }, + ); + return rewritten === block ? match : selector + rewritten; + }); + } + return css; };