Add auto reader background color
This commit is contained in:
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- (**Settings/WebView**) Add setting to enable/disable WebView
|
||||
- (**Settings/Sync**) Add sync settings
|
||||
- (**Extension**) Support installing external JARs
|
||||
- (**Reader**) Add auto background color setting
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"@vibrant/color": "4.0.4",
|
||||
"apollo-upload-client": "19.0.0",
|
||||
"awaitable-component": "1.0.0",
|
||||
"colorthief": "3.4.0",
|
||||
"csstype": "3.2.3",
|
||||
"dayjs": "1.11.21",
|
||||
"fast-average-color": "9.5.2",
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -71,6 +71,9 @@ importers:
|
||||
awaitable-component:
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0(react@19.2.6)
|
||||
colorthief:
|
||||
specifier: 3.4.0
|
||||
version: 3.4.0
|
||||
csstype:
|
||||
specifier: 3.2.3
|
||||
version: 3.2.3
|
||||
@@ -3238,6 +3241,15 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
colorthief@3.4.0:
|
||||
resolution: {integrity: sha512-mwXc0OHLsb1on2JtVD2go31GTyHwBLtkRruggz6H/bY2vI1mL8v8Mvam9qIWiI4khnsbQXoBs8M+EofnlcQitQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
sharp: '>=0.33.0'
|
||||
peerDependenciesMeta:
|
||||
sharp:
|
||||
optional: true
|
||||
|
||||
commander@10.0.1:
|
||||
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -8668,6 +8680,8 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
colorthief@3.4.0: {}
|
||||
|
||||
commander@10.0.1: {}
|
||||
|
||||
commander@14.0.3: {}
|
||||
|
||||
@@ -296,6 +296,9 @@ export const APP_METADATA: Record<
|
||||
backgroundColor: {
|
||||
convert: convertToNumber, // ReaderBackgroundColor (enum)
|
||||
},
|
||||
useAutoBackgroundColorContinuousMode: {
|
||||
convert: convertToBoolean,
|
||||
},
|
||||
pageGap: {
|
||||
convert: convertToNumber,
|
||||
toConstrainedValue: (value: number) => coerceIn(value, PAGE_GAP.min, PAGE_GAP.max),
|
||||
|
||||
@@ -69,6 +69,7 @@ export enum ReaderBackgroundColor {
|
||||
BLACK,
|
||||
GRAY,
|
||||
WHITE,
|
||||
AUTO,
|
||||
}
|
||||
|
||||
export interface ReaderFilterRGBA {
|
||||
@@ -159,6 +160,7 @@ export interface IReaderSettingsGlobal {
|
||||
shouldShowPageNumber: boolean;
|
||||
isStaticNav: boolean;
|
||||
backgroundColor: ReaderBackgroundColor;
|
||||
useAutoBackgroundColorContinuousMode: boolean;
|
||||
hotkeys: Record<ReaderHotkey, string[]>;
|
||||
imagePreLoadAmount: number;
|
||||
shouldUseAutoWebtoonMode: boolean;
|
||||
@@ -321,6 +323,10 @@ export interface ReaderStatePages {
|
||||
setPageLoadStates: (
|
||||
set: ((prevStates: ReaderPageLoadState[]) => ReaderPageLoadState[]) | ReaderPageLoadState[],
|
||||
) => void;
|
||||
pageBackgroundColors: ReaderPageBackgroundColor[];
|
||||
setPageBackgroundColor: (
|
||||
set: ((prev: ReaderPageBackgroundColor[]) => ReaderPageBackgroundColor[]) | ReaderPageBackgroundColor[],
|
||||
) => void;
|
||||
pages: PageData[];
|
||||
setPages: (pages: PageData[]) => void;
|
||||
transitionPageMode: ReaderTransitionPageMode;
|
||||
@@ -395,3 +401,5 @@ export type TReaderStateSettingsContext = {
|
||||
};
|
||||
|
||||
export type ReaderPageSpreadState = { url: string; isSpread: boolean };
|
||||
|
||||
export type ReaderPageBackgroundColor = { url: string; color: string | undefined };
|
||||
|
||||
@@ -14,8 +14,17 @@ import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { CHAPTER_READER_FIELDS } from '@/lib/graphql/chapter/ChapterFragments.ts';
|
||||
import { isPageOfOutdatedPageLoadStates, isSpreadPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { coerceIn } from '@/lib/HelperFunctions.ts';
|
||||
|
||||
import { DirectionOffset } from '@/base/Base.types.ts';
|
||||
import * as ColorThief from 'colorthief';
|
||||
import groupBy from 'lodash/fp/groupBy';
|
||||
import mapValues from 'lodash/fp/mapValues';
|
||||
import maxBy from 'lodash/fp/maxBy';
|
||||
import sumBy from 'lodash/fp/sumBy';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import {
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
|
||||
export const getInitialReaderPageIndex = (
|
||||
resumeMode: ReaderResumeMode,
|
||||
@@ -124,11 +133,135 @@ export const getChapterIdsForDownloadAhead = (
|
||||
.filter((id) => !Chapters.isDownloading(id));
|
||||
};
|
||||
|
||||
const getPageBackgroundColor = (
|
||||
[top, right, bottom, left]: [
|
||||
Top: ColorThief.Color[] | null,
|
||||
Right: ColorThief.Color[] | null,
|
||||
Bottom: ColorThief.Color[] | null,
|
||||
Left: ColorThief.Color[] | null,
|
||||
],
|
||||
readingMode: ReadingMode,
|
||||
): string => {
|
||||
const isContinuousReadingModeFlag = isContinuousReadingMode(readingMode);
|
||||
const isContinuousVerticalReadingModeFlag = isContinuousVerticalReadingMode(readingMode);
|
||||
|
||||
const borderColorPalettes = (() => {
|
||||
if (isContinuousVerticalReadingModeFlag) {
|
||||
return [right, left];
|
||||
}
|
||||
|
||||
if (isContinuousReadingModeFlag) {
|
||||
return [top, bottom];
|
||||
}
|
||||
|
||||
return [top, right, bottom, left];
|
||||
})();
|
||||
|
||||
const blackThreshold = 10;
|
||||
const whiteThreshold = 246;
|
||||
|
||||
const considerBlack = (color: ColorThief.Color | null): boolean => {
|
||||
if (!color) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { r, g, b } = color.rgb();
|
||||
return r <= blackThreshold && g <= blackThreshold && b <= blackThreshold;
|
||||
};
|
||||
|
||||
const considerWhite = (color: ColorThief.Color | null): boolean => {
|
||||
if (!color) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { r, g, b } = color.rgb();
|
||||
return r >= whiteThreshold && g >= whiteThreshold && b >= whiteThreshold;
|
||||
};
|
||||
|
||||
const getHexValue = (color: ColorThief.Color | null) => {
|
||||
if (considerBlack(color)) {
|
||||
return '#000000';
|
||||
}
|
||||
|
||||
if (considerWhite(color)) {
|
||||
return '#ffffff';
|
||||
}
|
||||
|
||||
return color!.hex();
|
||||
};
|
||||
const colorsByHexValue = groupBy((color) => getHexValue(color), borderColorPalettes.flat().filter(Boolean));
|
||||
const proportionByHexValue = mapValues(
|
||||
(colors) =>
|
||||
sumBy((color) => {
|
||||
const fillsWholeBorder = color!.proportion >= 0.97;
|
||||
const multiplier = fillsWholeBorder ? borderColorPalettes.length : 1;
|
||||
|
||||
return color!.proportion * multiplier;
|
||||
}, colors),
|
||||
colorsByHexValue,
|
||||
);
|
||||
const [hexValue] = maxBy(([_hex, proportion]) => proportion, Object.entries(proportionByHexValue))!;
|
||||
|
||||
return hexValue;
|
||||
};
|
||||
|
||||
const updatePageBackgroundColor = async (
|
||||
index: number,
|
||||
url: string,
|
||||
img: HTMLImageElement,
|
||||
setPageBackgroundColors: ReaderStatePages['setPageBackgroundColor'],
|
||||
readingMode: ReadingMode,
|
||||
): Promise<void> => {
|
||||
const canvas = new OffscreenCanvas(img.width, img.height);
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const yOffset = 5;
|
||||
const xOffset = Math.trunc(img.width * 0.0275);
|
||||
const borderSampleSize = 1;
|
||||
|
||||
const options = { ignoreWhite: false };
|
||||
|
||||
const borderColorPalettes = await (async () => {
|
||||
try {
|
||||
return await Promise.all([
|
||||
ColorThief.getPalette(ctx.getImageData(0, yOffset, img.width, borderSampleSize), options),
|
||||
ColorThief.getPalette(ctx.getImageData(img.width - xOffset, 0, borderSampleSize, img.height), options),
|
||||
ColorThief.getPalette(ctx.getImageData(0, img.height - yOffset, img.width, borderSampleSize), options),
|
||||
ColorThief.getPalette(ctx.getImageData(xOffset, 0, borderSampleSize, img.height), options),
|
||||
]);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!borderColorPalettes) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPageBackgroundColors((prevState) => {
|
||||
const pageBackgroundColor = prevState[index];
|
||||
|
||||
if (isPageOfOutdatedPageLoadStates(url, pageBackgroundColor)) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
if (pageBackgroundColor.color) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const color = getPageBackgroundColor(borderColorPalettes, readingMode);
|
||||
|
||||
return prevState.toSpliced(index, 1, { url, color });
|
||||
});
|
||||
};
|
||||
|
||||
export const createUpdateReaderPageLoadState =
|
||||
(
|
||||
actualPages: ReaderStatePages['pages'],
|
||||
setPagesToSpreadState: React.Dispatch<React.SetStateAction<ReaderPageSpreadState[]>>,
|
||||
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
||||
setPageBackgroundColors: ReaderStatePages['setPageBackgroundColor'],
|
||||
readingMode: ReadingMode,
|
||||
) =>
|
||||
(pagesIndex: number, url: string, isPrimary: boolean = true) => {
|
||||
@@ -139,9 +272,13 @@ export const createUpdateReaderPageLoadState =
|
||||
const page = actualPages[pagesIndex];
|
||||
const { index } = isPrimary ? page.primary : page.secondary!;
|
||||
|
||||
if (readingMode === ReadingMode.DOUBLE_PAGE) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
updatePageBackgroundColor(index, url, img, setPageBackgroundColors, readingMode).catch(
|
||||
defaultPromiseErrorHandler('updatePageBackgroundColor'),
|
||||
);
|
||||
|
||||
if (readingMode === ReadingMode.DOUBLE_PAGE) {
|
||||
const isSpreadPageFlag = isSpreadPage(img);
|
||||
if (!isSpreadPageFlag) {
|
||||
return;
|
||||
@@ -161,10 +298,10 @@ export const createUpdateReaderPageLoadState =
|
||||
|
||||
return prevState.toSpliced(index, 1, { url, isSpread: isSpreadPageFlag });
|
||||
});
|
||||
}
|
||||
};
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
setPageLoadStates((statePageLoadStates) => {
|
||||
const pageLoadState = statePageLoadStates[index];
|
||||
|
||||
@@ -23,7 +23,6 @@ import { GET_CHAPTERS_READER } from '@/lib/graphql/chapter/ChapterQuery.ts';
|
||||
import { TapZoneLayout } from '@/features/reader/tap-zones/TapZoneLayout.tsx';
|
||||
import { ReaderRGBAFilter } from '@/features/reader/filters/ReaderRGBAFilter.tsx';
|
||||
import { ReaderViewer } from '@/features/reader/viewer/ReaderViewer.tsx';
|
||||
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { ReaderHotkeys } from '@/features/reader/hotkeys/ReaderHotkeys.tsx';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import type { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
@@ -39,16 +38,24 @@ import {
|
||||
getReaderOverlayStore,
|
||||
getReaderStore,
|
||||
useReaderChaptersStore,
|
||||
useReaderPagesStore,
|
||||
useReaderSettingsStore,
|
||||
useReaderStore,
|
||||
} from '@/features/reader/stores/ReaderStore.ts';
|
||||
import { ReaderAutoScroll } from '@/features/reader/auto-scroll/ReaderAutoScroll.tsx';
|
||||
import { getReaderBackgroundColor, isContinuousReadingMode } from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
import { getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
const BaseReader = ({
|
||||
setOverride,
|
||||
readerNavBarWidth,
|
||||
}: Pick<NavbarContextType, 'setOverride' | 'readerNavBarWidth'>) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
|
||||
const manga = useReaderStore('manga');
|
||||
const { mangaChapters, initialChapter, chapterForDuplicatesHandling, currentChapter } = useReaderChaptersStore(
|
||||
'mangaChapters',
|
||||
@@ -60,22 +67,31 @@ const BaseReader = ({
|
||||
shouldSkipDupChapters,
|
||||
shouldSkipFilteredChapters,
|
||||
backgroundColor,
|
||||
readingMode,
|
||||
tapZoneLayout,
|
||||
tapZoneInvertMode,
|
||||
shouldShowReadingModePreview,
|
||||
shouldShowTapZoneLayoutPreview,
|
||||
useAutoBackgroundColorContinuousMode,
|
||||
} = useReaderSettingsStore(
|
||||
'shouldSkipDupChapters',
|
||||
'shouldSkipFilteredChapters',
|
||||
'backgroundColor',
|
||||
'readingMode',
|
||||
'tapZoneLayout',
|
||||
'tapZoneInvertMode',
|
||||
'shouldShowReadingModePreview',
|
||||
'shouldShowTapZoneLayoutPreview',
|
||||
'useAutoBackgroundColorContinuousMode',
|
||||
);
|
||||
const safeAreaInset = useReaderSettingsStore((state) => state.safeAreaInset);
|
||||
const readingMode = useReaderSettingsStore('readingMode');
|
||||
const readingDirection = useReaderSettingsStore('readingDirection');
|
||||
const tapZoneLayout = useReaderSettingsStore('tapZoneLayout');
|
||||
const tapZoneInvertMode = useReaderSettingsStore('tapZoneInvertMode');
|
||||
const safeAreaInset = useReaderSettingsStore('safeAreaInset');
|
||||
const { invertColors, applySepia, applyGrayscale } = useReaderSettingsStore((state) => ({
|
||||
invertColors: state.customFilter.invert,
|
||||
applySepia: state.customFilter.sepia,
|
||||
applyGrayscale: state.customFilter.grayscale,
|
||||
}));
|
||||
|
||||
const currentPageIndex = useReaderPagesStore('currentPageIndex');
|
||||
const pages = useReaderPagesStore('pages');
|
||||
const pageBackgroundColors = useReaderPagesStore('pageBackgroundColors');
|
||||
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -112,6 +128,30 @@ const BaseReader = ({
|
||||
defaultSettingsResponse.loading;
|
||||
const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error;
|
||||
|
||||
const page = getPage(currentPageIndex, pages);
|
||||
const primaryPageBackground = getReaderBackgroundColor(
|
||||
backgroundColor,
|
||||
pageBackgroundColors[page.primary.index]?.color,
|
||||
isContinuousReadingMode(readingMode.value),
|
||||
useAutoBackgroundColorContinuousMode,
|
||||
theme,
|
||||
invertColors,
|
||||
applySepia,
|
||||
applyGrayscale,
|
||||
);
|
||||
const secondaryPageBackground = getReaderBackgroundColor(
|
||||
backgroundColor,
|
||||
pageBackgroundColors[page.secondary?.index ?? page.primary.index]?.color,
|
||||
isContinuousReadingMode(readingMode.value),
|
||||
useAutoBackgroundColorContinuousMode,
|
||||
theme,
|
||||
invertColors,
|
||||
applySepia,
|
||||
applyGrayscale,
|
||||
);
|
||||
|
||||
const direction = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection.value];
|
||||
|
||||
useEffect(() => {
|
||||
getReaderStore().setManga(mangaResponse.data?.manga);
|
||||
}, [mangaResponse.data?.manga]);
|
||||
@@ -234,10 +274,11 @@ const BaseReader = ({
|
||||
pr: safeAreaInset.right ? 'env(safe-area-inset-right)' : undefined,
|
||||
pl: safeAreaInset.left ? 'env(safe-area-inset-left)' : undefined,
|
||||
marginLeft: `${readerNavBarWidth}px`,
|
||||
transition: (theme) =>
|
||||
`width 0.${theme.transitions.duration.shortest}s, margin-left 0.${theme.transitions.duration.shortest}s`,
|
||||
transition: `width 0.${theme.transitions.duration.shortest}s, margin-left 0.${theme.transitions.duration.shortest}s, background 1.${theme.transitions.duration.shortest}s`,
|
||||
overflow: 'auto',
|
||||
backgroundColor: READER_BACKGROUND_TO_COLOR[backgroundColor],
|
||||
background: isContinuousReadingMode(readingMode.value)
|
||||
? primaryPageBackground
|
||||
: `linear-gradient(to right, ${getOptionForDirection(primaryPageBackground, secondaryPageBackground, direction)} 0 50%, ${getOptionForDirection(secondaryPageBackground, primaryPageBackground, direction)} 50% 100%)`,
|
||||
}}
|
||||
>
|
||||
<ReaderViewer ref={scrollElementRef} />
|
||||
|
||||
@@ -144,6 +144,7 @@ const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefine
|
||||
shouldShowPageNumber: undefined,
|
||||
isStaticNav: undefined,
|
||||
backgroundColor: undefined,
|
||||
useAutoBackgroundColorContinuousMode: undefined,
|
||||
hotkeys: undefined,
|
||||
imagePreLoadAmount: undefined,
|
||||
shouldUseAutoWebtoonMode: undefined,
|
||||
@@ -180,6 +181,7 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
||||
readingMode: ReadingMode.SINGLE_PAGE,
|
||||
exitMode: ReaderExitMode.PREVIOUS,
|
||||
backgroundColor: ReaderBackgroundColor.THEME,
|
||||
useAutoBackgroundColorContinuousMode: false,
|
||||
customFilter: {
|
||||
brightness: {
|
||||
value: CUSTOM_FILTER.brightness.default,
|
||||
@@ -379,6 +381,8 @@ export const READER_BACKGROUND_TO_COLOR = {
|
||||
[ReaderBackgroundColor.BLACK]: 'common.black',
|
||||
[ReaderBackgroundColor.GRAY]: 'grey.200',
|
||||
[ReaderBackgroundColor.WHITE]: 'common.white',
|
||||
// Used as the fallback color
|
||||
[ReaderBackgroundColor.AUTO]: 'background.default',
|
||||
} as const satisfies Record<ReaderBackgroundColor, string>;
|
||||
|
||||
export const CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION: Record<
|
||||
|
||||
@@ -14,7 +14,12 @@ import type {
|
||||
ReaderPagerProps,
|
||||
SafeAreaInset,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { ProgressBarPosition, ReaderPageScaleMode, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
import {
|
||||
ProgressBarPosition,
|
||||
ReaderBackgroundColor,
|
||||
ReaderPageScaleMode,
|
||||
ReadingMode,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import type { MangaGenreInfo, MangaSourceNameInfo } from '@/features/manga/Manga.types.ts';
|
||||
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
||||
import { ReaderPagedPager } from '@/features/reader/viewer/pager/components/ReaderPagedPager.tsx';
|
||||
@@ -22,6 +27,10 @@ import { ReaderDoublePagedPager } from '@/features/reader/viewer/pager/component
|
||||
import { ReaderVerticalPager } from '@/features/reader/viewer/pager/components/ReaderVerticalPager.tsx';
|
||||
import { ReaderHorizontalPager } from '@/features/reader/viewer/pager/components/ReaderHorizontalPager.tsx';
|
||||
import { ScrollDirection } from '@/base/Base.types.ts';
|
||||
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { getValueFromObject } from '@/lib/HelperFunctions.ts';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import { Colors } from '@/lib/Colors.ts';
|
||||
|
||||
export const isOffsetDoubleSpreadPagesEditable = (readingMode: IReaderSettings['readingMode']): boolean =>
|
||||
readingMode === ReadingMode.DOUBLE_PAGE;
|
||||
@@ -117,3 +126,33 @@ export const getSafeAreaInsets = (
|
||||
...(direction === ScrollDirection.Y && safeAreaInset.bottom ? ['env(safe-area-inset-bottom)'] : []),
|
||||
...(direction === ScrollDirection.X && safeAreaInset.left ? ['env(safe-area-inset-left)'] : []),
|
||||
];
|
||||
|
||||
export const getReaderBackgroundColor = (
|
||||
backgroundColor: ReaderBackgroundColor,
|
||||
pageBackgroundColor: string | undefined,
|
||||
isContinuousReadingModeFlag: boolean,
|
||||
useAutoBackgroundColorContinuousMode: boolean,
|
||||
theme: Theme,
|
||||
invertColors: boolean = false,
|
||||
applySepia: boolean = false,
|
||||
applyGrayscale: boolean = false,
|
||||
): string => {
|
||||
const applyFilters = (hex: string): string => {
|
||||
const maybeGray = applyGrayscale ? Colors.grayscaleHex(hex) : hex;
|
||||
const maybeSepia = applySepia ? Colors.sepiaHex(maybeGray) : maybeGray;
|
||||
const maybeInverted = invertColors ? Colors.invertColorHex(maybeSepia) : maybeSepia;
|
||||
|
||||
return maybeInverted;
|
||||
};
|
||||
|
||||
const isAuto = backgroundColor === ReaderBackgroundColor.AUTO;
|
||||
const isAutoColorUsable = isAuto && !!pageBackgroundColor;
|
||||
const isAutoColorAllowed = !isContinuousReadingModeFlag || useAutoBackgroundColorContinuousMode;
|
||||
|
||||
const useAutoColor = isAutoColorUsable && isAutoColorAllowed;
|
||||
if (useAutoColor) {
|
||||
return applyFilters(pageBackgroundColor);
|
||||
}
|
||||
|
||||
return getValueFromObject(theme.palette, READER_BACKGROUND_TO_COLOR[backgroundColor]);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,8 @@ export const ReaderGeneralSettings = ({
|
||||
/>
|
||||
<ReaderSettingBackgroundColor
|
||||
backgroundColor={settings.backgroundColor}
|
||||
updateSetting={(value) => updateSetting('backgroundColor', value)}
|
||||
useAutoBackgroundColorContinuousMode={settings.useAutoBackgroundColorContinuousMode}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
/>
|
||||
<ReaderSettingProgressBarType
|
||||
overlayMode={overlayMode}
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { ValueToDisplayData } from '@/base/Base.types.ts';
|
||||
import { ButtonSelectInput } from '@/base/components/inputs/ButtonSelectInput.tsx';
|
||||
import type { IReaderSettingsWithDefaultFlag } from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderBackgroundColor } from '@/features/reader/Reader.types.ts';
|
||||
import type { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderBackgroundColor> = {
|
||||
[ReaderBackgroundColor.THEME]: {
|
||||
@@ -30,6 +32,10 @@ const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderBackgroundColor> = {
|
||||
title: msg`White`,
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBackgroundColor.AUTO]: {
|
||||
title: msg`Auto`,
|
||||
icon: null,
|
||||
},
|
||||
};
|
||||
|
||||
const READER_BACKGROUND_COLOR_VALUES = Object.values(ReaderBackgroundColor).filter(
|
||||
@@ -38,19 +44,29 @@ const READER_BACKGROUND_COLOR_VALUES = Object.values(ReaderBackgroundColor).filt
|
||||
|
||||
export const ReaderSettingBackgroundColor = ({
|
||||
backgroundColor,
|
||||
useAutoBackgroundColorContinuousMode,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'backgroundColor'> & {
|
||||
updateSetting: (color: ReaderBackgroundColor) => void;
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'backgroundColor' | 'useAutoBackgroundColorContinuousMode'> & {
|
||||
updateSetting: (...args: Parameters<typeof ReaderService.updateSetting>) => void;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonSelectInput
|
||||
label={t`Background color`}
|
||||
value={backgroundColor}
|
||||
values={READER_BACKGROUND_COLOR_VALUES}
|
||||
setValue={updateSetting}
|
||||
setValue={(value) => updateSetting('backgroundColor', value)}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
{backgroundColor === ReaderBackgroundColor.AUTO && (
|
||||
<CheckboxInput
|
||||
label={t`Enable auto background color in continuous reading mode`}
|
||||
checked={useAutoBackgroundColorContinuousMode}
|
||||
onChange={(_, checked) => updateSetting('useAutoBackgroundColorContinuousMode', checked)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ export const READER_DEFAULT_PAGES_STATE: Omit<
|
||||
| 'setPageUrls'
|
||||
| 'setPageSpreadStates'
|
||||
| 'setPageLoadStates'
|
||||
| 'setPageBackgroundColor'
|
||||
| 'setPages'
|
||||
| 'setTransitionPageMode'
|
||||
| 'setRetryFailedPagesKeyPrefix'
|
||||
@@ -36,6 +37,7 @@ export const READER_DEFAULT_PAGES_STATE: Omit<
|
||||
pageUrls: [],
|
||||
pageSpreadStates: [{ url: '', isSpread: false }],
|
||||
pageLoadStates: [{ url: '', loaded: false }],
|
||||
pageBackgroundColors: [{ url: '', color: undefined }],
|
||||
pages: [
|
||||
{
|
||||
name: '1',
|
||||
@@ -114,6 +116,19 @@ export const createReaderPagesStoreSlice = <T extends ReaderPagesStoreSlice>(
|
||||
undefined,
|
||||
createActionName('setPageLoadStates'),
|
||||
),
|
||||
setPageBackgroundColor: (dominantColors) =>
|
||||
set(
|
||||
(draft) => {
|
||||
if (typeof dominantColors === 'function') {
|
||||
draft.pages.pageBackgroundColors = dominantColors(get().pages.pageBackgroundColors);
|
||||
return;
|
||||
}
|
||||
|
||||
draft.pages.pageBackgroundColors = dominantColors;
|
||||
},
|
||||
undefined,
|
||||
createActionName('setPageBackgroundColors'),
|
||||
),
|
||||
setPages: (pages) =>
|
||||
set(
|
||||
(draft) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import Box from '@mui/material/Box';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import type {
|
||||
IReaderSettings,
|
||||
ReaderPageBackgroundColor,
|
||||
ReaderPagerProps,
|
||||
ReaderPageSpreadState,
|
||||
ReaderResumeMode,
|
||||
@@ -124,6 +125,9 @@ const BaseReaderChapterViewer = ({
|
||||
const [pagesToSpreadState, setPagesToSpreadState] = useState<ReaderPageSpreadState[]>(
|
||||
READER_DEFAULT_PAGES_STATE.pageSpreadStates,
|
||||
);
|
||||
const [pageBackgroundColors, setPageBackgroundColors] = useState<ReaderPageBackgroundColor[]>(
|
||||
READER_DEFAULT_PAGES_STATE.pageBackgroundColors,
|
||||
);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isCurrentChapterRef = useRef(isCurrentChapter);
|
||||
@@ -197,6 +201,13 @@ const BaseReaderChapterViewer = ({
|
||||
|
||||
setPageLoadStates(value);
|
||||
},
|
||||
(value) => {
|
||||
if (isCurrentChapterRef.current) {
|
||||
getReaderPagesStore().setPageBackgroundColor(value);
|
||||
}
|
||||
|
||||
setPageBackgroundColors(value);
|
||||
},
|
||||
readingMode,
|
||||
),
|
||||
[actualPages, readingMode],
|
||||
@@ -251,6 +262,7 @@ const BaseReaderChapterViewer = ({
|
||||
actualPages,
|
||||
pageLoadStates,
|
||||
pagesToSpreadState,
|
||||
pageBackgroundColors,
|
||||
arePagesFetched,
|
||||
setArePagesFetched,
|
||||
(value) => updateState(value, noOp, getReaderChaptersStore().setReaderStateChapters),
|
||||
@@ -265,6 +277,12 @@ const BaseReaderChapterViewer = ({
|
||||
setPagesToSpreadState,
|
||||
getReaderPagesStore().setPageSpreadStates.bind(getReaderPagesStore()),
|
||||
),
|
||||
(value) =>
|
||||
updateState(
|
||||
value,
|
||||
setPageBackgroundColors,
|
||||
getReaderPagesStore().setPageBackgroundColor.bind(getReaderPagesStore()),
|
||||
),
|
||||
(value) => updateState(value, noOp, getReaderPagesStore().setCurrentPageIndex.bind(getReaderPagesStore())),
|
||||
(value) => {
|
||||
if ((isInitialChapter && !arePagesFetched) || scrollIntoView) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { ComponentProps } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import type { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import type { ReaderBackgroundColor } from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderTransitionPageMode, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
import { isTransitionPageVisible } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { useBackButton } from '@/base/hooks/useBackButton.ts';
|
||||
@@ -28,7 +28,6 @@ import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
import type { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx';
|
||||
import { getValueFromObject, noOp } from '@/lib/HelperFunctions.ts';
|
||||
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import type { ChapterIdInfo, ChapterNameInfo, ChapterScanlatorInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
import {
|
||||
useReaderChaptersStore,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
useReaderSettingsStore,
|
||||
useReaderStore,
|
||||
} from '@/features/reader/stores/ReaderStore.ts';
|
||||
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
|
||||
const ChapterInfo = ({
|
||||
title,
|
||||
@@ -47,12 +47,12 @@ const ChapterInfo = ({
|
||||
title: string;
|
||||
name?: ChapterNameInfo['name'];
|
||||
scanlator?: ChapterScanlatorInfo['scanlator'];
|
||||
backgroundColor: IReaderSettings['backgroundColor'];
|
||||
backgroundColor: ReaderBackgroundColor;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const contrastText = theme.palette.getContrastText(
|
||||
getValueFromObject(theme.palette, READER_BACKGROUND_TO_COLOR[backgroundColor]),
|
||||
getValueFromObject<string>(theme.palette, READER_BACKGROUND_TO_COLOR[backgroundColor]),
|
||||
);
|
||||
const disabledText = theme.alpha(contrastText, 0.5);
|
||||
|
||||
@@ -131,21 +131,22 @@ const BaseReaderTransitionPage = ({
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: READER_BACKGROUND_TO_COLOR[backgroundColor],
|
||||
...applyStyles(!isContinuousReadingMode(readingMode), {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
||||
height: `calc(100vh - ${scrollbar.xSize}px)`,
|
||||
}),
|
||||
...applyStyles(isContinuousReadingMode(readingMode), {
|
||||
position: 'sticky',
|
||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||
left: 0,
|
||||
maxWidth: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
||||
minHeight: `calc(100vh - ${scrollbar.xSize}px)`,
|
||||
width: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
||||
height: `calc(100vh - ${scrollbar.xSize}px)`,
|
||||
}),
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
top: 0,
|
||||
minWidth: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
||||
maxHeight: `calc(100vh - ${scrollbar.xSize}px)`,
|
||||
width: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
||||
height: `calc(100vh - ${scrollbar.xSize}px)`,
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
import { getInitialReaderPageIndex } from '@/features/reader/Reader.utils.ts';
|
||||
import { createPagesData } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import type { ReaderPageSpreadState, ReaderResumeMode, ReaderStatePages } from '@/features/reader/Reader.types.ts';
|
||||
import type {
|
||||
ReaderPageBackgroundColor,
|
||||
ReaderPageSpreadState,
|
||||
ReaderResumeMode,
|
||||
ReaderStatePages,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderTransitionPageMode } from '@/features/reader/Reader.types.ts';
|
||||
import type { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import type { TChapterReader } from '@/features/chapter/Chapter.types.ts';
|
||||
@@ -25,6 +30,7 @@ export const useReaderSetPagesState = (
|
||||
pages: ReaderStatePages['pages'],
|
||||
pageLoadStates: ReaderStatePages['pageLoadStates'],
|
||||
pagesToSpreadState: ReaderPageSpreadState[],
|
||||
pageBackgroundColors: ReaderPageBackgroundColor[],
|
||||
arePagesFetched: boolean,
|
||||
setArePagesFetched: (fetched: boolean) => void,
|
||||
setReaderStateChapters: ReaderChaptersStoreSlice['chapters']['setReaderStateChapters'],
|
||||
@@ -33,6 +39,7 @@ export const useReaderSetPagesState = (
|
||||
setPageUrls: ReaderStatePages['setPageUrls'],
|
||||
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
||||
setPagesToSpreadState: (state: ReaderPageSpreadState[]) => void,
|
||||
setPageBackgroundColors: (state: ReaderPageBackgroundColor[]) => void,
|
||||
setCurrentPageIndex: ReaderStatePages['setCurrentPageIndex'],
|
||||
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
||||
setTransitionPageMode: ReaderStatePages['setTransitionPageMode'],
|
||||
@@ -62,10 +69,12 @@ export const useReaderSetPagesState = (
|
||||
setPageUrls(newPages);
|
||||
setPageLoadStates(newPageData.map(({ primary: { url } }) => ({ url, loaded: false })));
|
||||
setPagesToSpreadState(newPageData.map(({ primary: { url } }) => ({ url, isSpread: false })));
|
||||
setPageBackgroundColors(newPageData.map(({ primary: { url } }) => ({ url, color: undefined })));
|
||||
} else {
|
||||
setPages(pages);
|
||||
setPageLoadStates(pageLoadStates);
|
||||
setPagesToSpreadState(pagesToSpreadState);
|
||||
setPageBackgroundColors(pageBackgroundColors);
|
||||
}
|
||||
|
||||
setTotalPages(pagesPayload.pages.length);
|
||||
|
||||
@@ -648,7 +648,5 @@ export const getScrollToXForReadingDirection = (
|
||||
return getOptionForDirection(-element.scrollWidth, 0, themeDirectionForReadingDirection);
|
||||
};
|
||||
|
||||
export const isPageOfOutdatedPageLoadStates = (
|
||||
url: string,
|
||||
pageLoadState: ReaderStatePages['pageLoadStates'][number] | undefined,
|
||||
): boolean => pageLoadState === undefined || pageLoadState.url !== url;
|
||||
export const isPageOfOutdatedPageLoadStates = (url: string, state: { url: string } | undefined): boolean =>
|
||||
state === undefined || state.url !== url;
|
||||
|
||||
@@ -499,6 +499,7 @@ msgstr "Author"
|
||||
|
||||
#: src/features/reader/overlay/progress-bar/settings/components/ReaderSettingProgressBarPosition.tsx
|
||||
#: src/features/reader/overlay/settings/ReaderSettingOverlayMode.tsx
|
||||
#: src/features/reader/settings/general/components/ReaderSettingBackgroundColor.tsx
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
@@ -1578,6 +1579,10 @@ msgstr "Electron"
|
||||
msgid "Electron path"
|
||||
msgstr "Electron path"
|
||||
|
||||
#: src/features/reader/settings/general/components/ReaderSettingBackgroundColor.tsx
|
||||
msgid "Enable auto background color in continuous reading mode"
|
||||
msgstr "Enable auto background color in continuous reading mode"
|
||||
|
||||
#: src/features/settings/screens/ServerSettings.tsx
|
||||
msgid "Enable debug logs"
|
||||
msgstr "Enable debug logs"
|
||||
|
||||
84
src/lib/Colors.ts
Normal file
84
src/lib/Colors.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
type Rgb = {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
};
|
||||
|
||||
export class Colors {
|
||||
static hexToRgb(hex: string): Rgb {
|
||||
let tmpHex = hex.replace(/^#/, '');
|
||||
|
||||
const isShorthand = tmpHex.length === 3; // #abc
|
||||
if (isShorthand) {
|
||||
tmpHex = hex
|
||||
.split('')
|
||||
.map((c) => c + c)
|
||||
.join('');
|
||||
}
|
||||
|
||||
return {
|
||||
r: parseInt(tmpHex.slice(0, 2), 16),
|
||||
g: parseInt(tmpHex.slice(2, 4), 16),
|
||||
b: parseInt(tmpHex.slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
static rgbToHex({ r, g, b }: Rgb): string {
|
||||
// oxlint-disable-next-line unicorn/consistent-function-scoping
|
||||
const toHex = (value: number) =>
|
||||
Math.round(Math.max(0, Math.min(255, value)))
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
|
||||
static grayScaleRgb({ r, g, b }: Rgb): Rgb {
|
||||
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
||||
|
||||
return {
|
||||
r: gray,
|
||||
g: gray,
|
||||
b: gray,
|
||||
};
|
||||
}
|
||||
|
||||
static grayscaleHex(hex: string): string {
|
||||
return Colors.rgbToHex(Colors.grayScaleRgb(Colors.hexToRgb(hex)));
|
||||
}
|
||||
|
||||
static sepiaRgb({ r, g, b }: Rgb, amount = 1): Rgb {
|
||||
const sepia = {
|
||||
r: r * 0.393 + g * 0.769 + b * 0.189,
|
||||
g: r * 0.349 + g * 0.686 + b * 0.168,
|
||||
b: r * 0.272 + g * 0.534 + b * 0.131,
|
||||
};
|
||||
|
||||
return {
|
||||
r: Math.min(255, r + (sepia.r - r) * amount),
|
||||
g: Math.min(255, g + (sepia.g - g) * amount),
|
||||
b: Math.min(255, b + (sepia.b - b) * amount),
|
||||
};
|
||||
}
|
||||
|
||||
static sepiaHex(hex: string): string {
|
||||
return Colors.rgbToHex(Colors.sepiaRgb(Colors.hexToRgb(hex)));
|
||||
}
|
||||
|
||||
static invertColorHex(hex: string): string {
|
||||
const clean = hex.replace('#', '');
|
||||
const r = 255 - parseInt(clean.substring(0, 2), 16);
|
||||
const g = 255 - parseInt(clean.substring(2, 4), 16);
|
||||
const b = 255 - parseInt(clean.substring(4, 6), 16);
|
||||
|
||||
return `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user