feat: add an option to invert image color in dark mode (#1223)

This commit is contained in:
Huang Xin
2025-05-22 16:38:09 +08:00
committed by GitHub
parent 32dfadf182
commit e238b49fbf
25 changed files with 91 additions and 39 deletions
@@ -324,5 +324,6 @@
"Translated by {{provider}}.": "ترجم بواسطة {{provider}}.",
"DeepL": "ديب إل",
"Google Translate": "ترجمة جوجل",
"Azure Translator": "مترجم أزور"
"Azure Translator": "مترجم أزور",
"Invert Image In Dark Mode": "عكس الصورة في الوضع المظلم"
}
@@ -320,5 +320,6 @@
"Translated by {{provider}}.": "Übersetzt von {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Übersetzer",
"Azure Translator": "Azure Übersetzer"
"Azure Translator": "Azure Übersetzer",
"Invert Image In Dark Mode": "Invertiere Bild im Dunkeln"
}
@@ -320,5 +320,6 @@
"Translated by {{provider}}.": "Μεταφρασμένο από {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Μετάφραση Google",
"Azure Translator": "Μεταφραστής Azure"
"Azure Translator": "Μεταφραστής Azure",
"Invert Image In Dark Mode": "Αντιστροφή σε σκούρο"
}
@@ -321,5 +321,6 @@
"Translated by {{provider}}.": "Traducido por {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Azure Translator": "Azure Translator"
"Azure Translator": "Azure Translator",
"Invert Image In Dark Mode": "Invertir imagen en oscuro"
}
@@ -321,5 +321,6 @@
"Translated by {{provider}}.": "Traduit par {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Traduction",
"Azure Translator": "Traducteur Azure"
"Azure Translator": "Traducteur Azure",
"Invert Image In Dark Mode": "Inverser l'image en mode sombre"
}
@@ -320,5 +320,6 @@
"Translated by {{provider}}.": "{{provider}} द्वारा अनुवादित।",
"DeepL": "डीपएल",
"Google Translate": "गूगल अनुवाद",
"Azure Translator": "एज़्योर अनुवादक"
"Azure Translator": "एज़्योर अनुवादक",
"Invert Image In Dark Mode": "अंधेरे में छवि को उलटें"
}
@@ -319,5 +319,6 @@
"Translated by {{provider}}.": "Terjemahan oleh {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Azure Translator": "Azure Translator"
"Azure Translator": "Azure Translator",
"Invert Image In Dark Mode": "Inversi Gambar di Gelap"
}
@@ -321,5 +321,6 @@
"Translated by {{provider}}.": "Tradotto da {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Azure Translator": "Traduttore Azure"
"Azure Translator": "Traduttore Azure",
"Invert Image In Dark Mode": "Inverti immagine in scuro"
}
@@ -319,5 +319,6 @@
"Translated by {{provider}}.": "{{provider}}によって翻訳されました。",
"DeepL": "DeepL",
"Google Translate": "Google翻訳",
"Azure Translator": "Azure翻訳"
"Azure Translator": "Azure翻訳",
"Invert Image In Dark Mode": "ダークモードで画像を反転"
}
@@ -319,5 +319,6 @@
"Translated by {{provider}}.": "{{provider}}에 의해 번역됨.",
"DeepL": "DeepL",
"Google Translate": "구글 번역",
"Azure Translator": "Azure 번역기"
"Azure Translator": "Azure 번역기",
"Invert Image In Dark Mode": "어두운 곳에서 이미지 반전"
}
@@ -320,5 +320,6 @@
"Translated by {{provider}}.": "Vertaald door {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Vertalen",
"Azure Translator": "Azure Vertaler"
"Azure Translator": "Azure Vertaler",
"Invert Image In Dark Mode": "Inverteer afbeelding in het donker"
}
@@ -322,5 +322,6 @@
"Translated by {{provider}}.": "Przetłumaczone przez {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Tłumacz Google",
"Azure Translator": "Azure Translator"
"Azure Translator": "Azure Translator",
"Invert Image In Dark Mode": "Odwróć obraz w ciemności"
}
@@ -321,5 +321,6 @@
"Translated by {{provider}}.": "Traduzido por {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Tradutor",
"Azure Translator": "Tradutor Azure"
"Azure Translator": "Tradutor Azure",
"Invert Image In Dark Mode": "Inverter Imagem no Escuro"
}
@@ -322,5 +322,6 @@
"Translated by {{provider}}.": "Переведено {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Azure Translator": "Azure Translator"
"Azure Translator": "Azure Translator",
"Invert Image In Dark Mode": "Инвертировать в тёмной теме"
}
@@ -320,5 +320,6 @@
"Translated by {{provider}}.": "{{provider}} tarafından çevrildi.",
"DeepL": "DeepL",
"Google Translate": "Google Çeviri",
"Azure Translator": "Azure Çevirmen"
"Azure Translator": "Azure Çevirmen",
"Invert Image In Dark Mode": "Karartılmış Görüntüyü Ters Çevir"
}
@@ -322,5 +322,6 @@
"Translated by {{provider}}.": "Перекладено {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Azure Translator": "Azure Translator"
"Azure Translator": "Azure Translator",
"Invert Image In Dark Mode": "Інвертувати зображення в темряві"
}
@@ -319,5 +319,6 @@
"Translated by {{provider}}.": "Được dịch bởi {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Dịch",
"Azure Translator": "Azure Translator"
"Azure Translator": "Azure Translator",
"Invert Image In Dark Mode": "Đảo ảnh chế độ tối"
}
@@ -319,5 +319,6 @@
"Translated by {{provider}}.": "由 {{provider}} 提供翻译",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Azure Translator": "Azure Translator"
"Azure Translator": "Azure Translator",
"Invert Image In Dark Mode": "深色主题下图片反色"
}
@@ -319,5 +319,6 @@
"Translated by {{provider}}.": "由 {{provider}} 提供翻譯",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Azure Translator": "Azure Translator"
"Azure Translator": "Azure Translator",
"Invert Image In Dark Mode": "深色主題下圖片反相"
}
@@ -33,9 +33,12 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const { themeMode, setThemeMode } = useThemeStore();
const { themeMode, isDarkMode, setThemeMode } = useThemeStore();
const [isScrolledMode, setScrolledMode] = useState(viewSettings!.scrolled);
const [zoomLevel, setZoomLevel] = useState(viewSettings!.zoomLevel!);
const [invertImgColorInDark, setInvertImgColorInDark] = useState(
viewSettings!.invertImgColorInDark,
);
const zoomIn = () => setZoomLevel((prev) => Math.min(prev + ZOOM_STEP, MAX_ZOOM_LEVEL));
const zoomOut = () => setZoomLevel((prev) => Math.max(prev - ZOOM_STEP, MIN_ZOOM_LEVEL));
@@ -76,6 +79,12 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [zoomLevel]);
useEffect(() => {
if (invertImgColorInDark === viewSettings.invertImgColorInDark) return;
saveViewSettings(envConfig, bookKey, 'invertImgColorInDark', invertImgColorInDark, true, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [invertImgColorInDark]);
return (
<div
tabIndex={0}
@@ -135,6 +144,12 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
Icon={themeMode === 'dark' ? BiMoon : themeMode === 'light' ? BiSun : TbSunMoon}
onClick={cycleThemeMode}
/>
<MenuItem
label={_('Invert Image In Dark Mode')}
disabled={!isDarkMode}
Icon={invertImgColorInDark ? MdCheck : undefined}
onClick={() => setInvertImgColorInDark(!invertImgColorInDark)}
/>
</div>
);
};
@@ -13,24 +13,38 @@ import {
themes,
} from '@/styles/themes';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import ThemeEditor from './ThemeEditor';
const ColorPanel: React.FC<{ bookKey: string }> = ({}) => {
const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { themeMode, themeColor, isDarkMode, setThemeMode, setThemeColor, saveCustomTheme } =
useThemeStore();
const { envConfig } = useEnv();
const { settings, setSettings } = useSettingsStore();
const { getViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const [invertImgColorInDark, setInvertImgColorInDark] = useState(
viewSettings.invertImgColorInDark,
);
const iconSize16 = useResponsiveSize(16);
const iconSize24 = useResponsiveSize(24);
const [editTheme, setEditTheme] = useState<CustomTheme | null>(null);
const [customThems, setCustomThemes] = useState<Theme[]>([]);
const [showCustomThemeEditor, setShowCustomThemeEditor] = useState(false);
useEffect(() => {
if (invertImgColorInDark === viewSettings.invertImgColorInDark) return;
saveViewSettings(envConfig, bookKey, 'invertImgColorInDark', invertImgColorInDark);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [invertImgColorInDark]);
useEffect(() => {
const customThemes = settings.globalReadSettings.customThemes ?? [];
setCustomThemes(
@@ -112,6 +126,17 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({}) => {
</div>
</div>
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Invert Image In Dark Mode')}</h2>
<input
type='checkbox'
className='toggle'
checked={invertImgColorInDark}
disabled={!isDarkMode}
onChange={() => setInvertImgColorInDark(!invertImgColorInDark)}
/>
</div>
<div>
<h2 className='mb-2 font-medium'>{_('Theme Color')}</h2>
<div className='grid grid-cols-3 gap-4'>
@@ -162,6 +162,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}, [writingMode]);
useEffect(() => {
if (overrideLayout === viewSettings.overrideLayout) return;
saveViewSettings(envConfig, bookKey, 'overrideLayout', overrideLayout);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [overrideLayout]);
+1 -1
View File
@@ -118,7 +118,7 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
textIndent: 0,
fullJustification: true,
hyphenation: true,
invert: false,
invertImgColorInDark: false,
theme: 'light',
overrideFont: false,
overrideLayout: false,
+1 -1
View File
@@ -99,7 +99,7 @@ export interface BookStyle {
textIndent: number;
fullJustification: boolean;
hyphenation: boolean;
invert: boolean;
invertImgColorInDark: boolean;
theme: string;
overrideFont: boolean;
overrideLayout: boolean;
+7 -15
View File
@@ -29,7 +29,7 @@ const getFontStyles = (
overrideFont: boolean,
themeCode: ThemeCode,
) => {
const { fg, primary } = themeCode;
const { fg, primary, isDarkMode } = themeCode;
const lastSerifFonts = ['Georgia', 'Times New Roman'];
const serifFonts = [
serif,
@@ -87,15 +87,9 @@ const getFontStyles = (
${overrideFont ? 'font-family: revert !important;' : ''}
}
a:any-link, a:any-link * {
${overrideFont ? `color: ${primary};` : ''}
${overrideFont ? `color: ${primary};` : isDarkMode ? `color: lightblue;` : ''}
text-decoration: none;
}
/* https://github.com/whatwg/html/issues/5426 */
@media (prefers-color-scheme: dark) {
a:any-link, a:any-link * {
${overrideFont ? `color: ${primary};` : `color: lightblue;`}
}
}
/* override inline hardcoded text color */
*[style*="color: rgb(0,0,0)"], *[style*="color: rgb(0, 0, 0)"],
*[style*="color: #000"], *[style*="color: #000000"], *[style*="color: black"],
@@ -188,6 +182,7 @@ const getLayoutStyles = (
zoomLevel: number,
writingMode: string,
vertical: boolean,
invertImgColorInDark: boolean,
themeCode: ThemeCode,
) => {
const { bg, fg, primary, isDarkMode } = themeCode;
@@ -293,11 +288,13 @@ const getLayoutStyles = (
color: unset;
}
img {
${isDarkMode && invertImgColorInDark ? 'filter: invert(100%);' : ''}
}
/* inline images without dimension */
p img, span img, sup img {
height: 1em;
mix-blend-mode: ${isDarkMode ? 'screen' : 'multiply'};
${isDarkMode ? 'filter: invert(100%);' : ''}
}
p:has(img), span:has(img) {
background-color: ${bg};
@@ -345,12 +342,6 @@ export const getFootnoteStyles = () => `
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
a:any-link, a:any-link * {
color: unset;
}
}
ol {
margin: 0;
padding: 0;
@@ -423,6 +414,7 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
viewSettings.zoomLevel! / 100.0,
viewSettings.writingMode!,
viewSettings.vertical!,
viewSettings.invertImgColorInDark!,
themeCode,
);
// scale the font size on-the-fly so that we can sync the same font size on different devices