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/WebView**) Add setting to enable/disable WebView
|
||||||
- (**Settings/Sync**) Add sync settings
|
- (**Settings/Sync**) Add sync settings
|
||||||
- (**Extension**) Support installing external JARs
|
- (**Extension**) Support installing external JARs
|
||||||
|
- (**Reader**) Add auto background color setting
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"@vibrant/color": "4.0.4",
|
"@vibrant/color": "4.0.4",
|
||||||
"apollo-upload-client": "19.0.0",
|
"apollo-upload-client": "19.0.0",
|
||||||
"awaitable-component": "1.0.0",
|
"awaitable-component": "1.0.0",
|
||||||
|
"colorthief": "3.4.0",
|
||||||
"csstype": "3.2.3",
|
"csstype": "3.2.3",
|
||||||
"dayjs": "1.11.21",
|
"dayjs": "1.11.21",
|
||||||
"fast-average-color": "9.5.2",
|
"fast-average-color": "9.5.2",
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -71,6 +71,9 @@ importers:
|
|||||||
awaitable-component:
|
awaitable-component:
|
||||||
specifier: 1.0.0
|
specifier: 1.0.0
|
||||||
version: 1.0.0(react@19.2.6)
|
version: 1.0.0(react@19.2.6)
|
||||||
|
colorthief:
|
||||||
|
specifier: 3.4.0
|
||||||
|
version: 3.4.0
|
||||||
csstype:
|
csstype:
|
||||||
specifier: 3.2.3
|
specifier: 3.2.3
|
||||||
version: 3.2.3
|
version: 3.2.3
|
||||||
@@ -3238,6 +3241,15 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
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:
|
commander@10.0.1:
|
||||||
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -8668,6 +8680,8 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
colorthief@3.4.0: {}
|
||||||
|
|
||||||
commander@10.0.1: {}
|
commander@10.0.1: {}
|
||||||
|
|
||||||
commander@14.0.3: {}
|
commander@14.0.3: {}
|
||||||
|
|||||||
@@ -296,6 +296,9 @@ export const APP_METADATA: Record<
|
|||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
convert: convertToNumber, // ReaderBackgroundColor (enum)
|
convert: convertToNumber, // ReaderBackgroundColor (enum)
|
||||||
},
|
},
|
||||||
|
useAutoBackgroundColorContinuousMode: {
|
||||||
|
convert: convertToBoolean,
|
||||||
|
},
|
||||||
pageGap: {
|
pageGap: {
|
||||||
convert: convertToNumber,
|
convert: convertToNumber,
|
||||||
toConstrainedValue: (value: number) => coerceIn(value, PAGE_GAP.min, PAGE_GAP.max),
|
toConstrainedValue: (value: number) => coerceIn(value, PAGE_GAP.min, PAGE_GAP.max),
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export enum ReaderBackgroundColor {
|
|||||||
BLACK,
|
BLACK,
|
||||||
GRAY,
|
GRAY,
|
||||||
WHITE,
|
WHITE,
|
||||||
|
AUTO,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReaderFilterRGBA {
|
export interface ReaderFilterRGBA {
|
||||||
@@ -159,6 +160,7 @@ export interface IReaderSettingsGlobal {
|
|||||||
shouldShowPageNumber: boolean;
|
shouldShowPageNumber: boolean;
|
||||||
isStaticNav: boolean;
|
isStaticNav: boolean;
|
||||||
backgroundColor: ReaderBackgroundColor;
|
backgroundColor: ReaderBackgroundColor;
|
||||||
|
useAutoBackgroundColorContinuousMode: boolean;
|
||||||
hotkeys: Record<ReaderHotkey, string[]>;
|
hotkeys: Record<ReaderHotkey, string[]>;
|
||||||
imagePreLoadAmount: number;
|
imagePreLoadAmount: number;
|
||||||
shouldUseAutoWebtoonMode: boolean;
|
shouldUseAutoWebtoonMode: boolean;
|
||||||
@@ -321,6 +323,10 @@ export interface ReaderStatePages {
|
|||||||
setPageLoadStates: (
|
setPageLoadStates: (
|
||||||
set: ((prevStates: ReaderPageLoadState[]) => ReaderPageLoadState[]) | ReaderPageLoadState[],
|
set: ((prevStates: ReaderPageLoadState[]) => ReaderPageLoadState[]) | ReaderPageLoadState[],
|
||||||
) => void;
|
) => void;
|
||||||
|
pageBackgroundColors: ReaderPageBackgroundColor[];
|
||||||
|
setPageBackgroundColor: (
|
||||||
|
set: ((prev: ReaderPageBackgroundColor[]) => ReaderPageBackgroundColor[]) | ReaderPageBackgroundColor[],
|
||||||
|
) => void;
|
||||||
pages: PageData[];
|
pages: PageData[];
|
||||||
setPages: (pages: PageData[]) => void;
|
setPages: (pages: PageData[]) => void;
|
||||||
transitionPageMode: ReaderTransitionPageMode;
|
transitionPageMode: ReaderTransitionPageMode;
|
||||||
@@ -395,3 +401,5 @@ export type TReaderStateSettingsContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ReaderPageSpreadState = { url: string; isSpread: boolean };
|
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 { CHAPTER_READER_FIELDS } from '@/lib/graphql/chapter/ChapterFragments.ts';
|
||||||
import { isPageOfOutdatedPageLoadStates, isSpreadPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
import { isPageOfOutdatedPageLoadStates, isSpreadPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||||
import { coerceIn } from '@/lib/HelperFunctions.ts';
|
import { coerceIn } from '@/lib/HelperFunctions.ts';
|
||||||
|
|
||||||
import { DirectionOffset } from '@/base/Base.types.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 = (
|
export const getInitialReaderPageIndex = (
|
||||||
resumeMode: ReaderResumeMode,
|
resumeMode: ReaderResumeMode,
|
||||||
@@ -124,11 +133,135 @@ export const getChapterIdsForDownloadAhead = (
|
|||||||
.filter((id) => !Chapters.isDownloading(id));
|
.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 =
|
export const createUpdateReaderPageLoadState =
|
||||||
(
|
(
|
||||||
actualPages: ReaderStatePages['pages'],
|
actualPages: ReaderStatePages['pages'],
|
||||||
setPagesToSpreadState: React.Dispatch<React.SetStateAction<ReaderPageSpreadState[]>>,
|
setPagesToSpreadState: React.Dispatch<React.SetStateAction<ReaderPageSpreadState[]>>,
|
||||||
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
||||||
|
setPageBackgroundColors: ReaderStatePages['setPageBackgroundColor'],
|
||||||
readingMode: ReadingMode,
|
readingMode: ReadingMode,
|
||||||
) =>
|
) =>
|
||||||
(pagesIndex: number, url: string, isPrimary: boolean = true) => {
|
(pagesIndex: number, url: string, isPrimary: boolean = true) => {
|
||||||
@@ -139,9 +272,13 @@ export const createUpdateReaderPageLoadState =
|
|||||||
const page = actualPages[pagesIndex];
|
const page = actualPages[pagesIndex];
|
||||||
const { index } = isPrimary ? page.primary : page.secondary!;
|
const { index } = isPrimary ? page.primary : page.secondary!;
|
||||||
|
|
||||||
if (readingMode === ReadingMode.DOUBLE_PAGE) {
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
updatePageBackgroundColor(index, url, img, setPageBackgroundColors, readingMode).catch(
|
||||||
|
defaultPromiseErrorHandler('updatePageBackgroundColor'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (readingMode === ReadingMode.DOUBLE_PAGE) {
|
||||||
const isSpreadPageFlag = isSpreadPage(img);
|
const isSpreadPageFlag = isSpreadPage(img);
|
||||||
if (!isSpreadPageFlag) {
|
if (!isSpreadPageFlag) {
|
||||||
return;
|
return;
|
||||||
@@ -161,10 +298,10 @@ export const createUpdateReaderPageLoadState =
|
|||||||
|
|
||||||
return prevState.toSpliced(index, 1, { url, isSpread: isSpreadPageFlag });
|
return prevState.toSpliced(index, 1, { url, isSpread: isSpreadPageFlag });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
img.crossOrigin = 'anonymous';
|
img.crossOrigin = 'anonymous';
|
||||||
img.src = url;
|
img.src = url;
|
||||||
}
|
|
||||||
|
|
||||||
setPageLoadStates((statePageLoadStates) => {
|
setPageLoadStates((statePageLoadStates) => {
|
||||||
const pageLoadState = statePageLoadStates[index];
|
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 { TapZoneLayout } from '@/features/reader/tap-zones/TapZoneLayout.tsx';
|
||||||
import { ReaderRGBAFilter } from '@/features/reader/filters/ReaderRGBAFilter.tsx';
|
import { ReaderRGBAFilter } from '@/features/reader/filters/ReaderRGBAFilter.tsx';
|
||||||
import { ReaderViewer } from '@/features/reader/viewer/ReaderViewer.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 { ReaderHotkeys } from '@/features/reader/hotkeys/ReaderHotkeys.tsx';
|
||||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||||
import type { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
import type { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||||
@@ -39,16 +38,24 @@ import {
|
|||||||
getReaderOverlayStore,
|
getReaderOverlayStore,
|
||||||
getReaderStore,
|
getReaderStore,
|
||||||
useReaderChaptersStore,
|
useReaderChaptersStore,
|
||||||
|
useReaderPagesStore,
|
||||||
useReaderSettingsStore,
|
useReaderSettingsStore,
|
||||||
useReaderStore,
|
useReaderStore,
|
||||||
} from '@/features/reader/stores/ReaderStore.ts';
|
} from '@/features/reader/stores/ReaderStore.ts';
|
||||||
import { ReaderAutoScroll } from '@/features/reader/auto-scroll/ReaderAutoScroll.tsx';
|
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 = ({
|
const BaseReader = ({
|
||||||
setOverride,
|
setOverride,
|
||||||
readerNavBarWidth,
|
readerNavBarWidth,
|
||||||
}: Pick<NavbarContextType, 'setOverride' | 'readerNavBarWidth'>) => {
|
}: Pick<NavbarContextType, 'setOverride' | 'readerNavBarWidth'>) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const manga = useReaderStore('manga');
|
const manga = useReaderStore('manga');
|
||||||
const { mangaChapters, initialChapter, chapterForDuplicatesHandling, currentChapter } = useReaderChaptersStore(
|
const { mangaChapters, initialChapter, chapterForDuplicatesHandling, currentChapter } = useReaderChaptersStore(
|
||||||
'mangaChapters',
|
'mangaChapters',
|
||||||
@@ -60,22 +67,31 @@ const BaseReader = ({
|
|||||||
shouldSkipDupChapters,
|
shouldSkipDupChapters,
|
||||||
shouldSkipFilteredChapters,
|
shouldSkipFilteredChapters,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
readingMode,
|
|
||||||
tapZoneLayout,
|
|
||||||
tapZoneInvertMode,
|
|
||||||
shouldShowReadingModePreview,
|
shouldShowReadingModePreview,
|
||||||
shouldShowTapZoneLayoutPreview,
|
shouldShowTapZoneLayoutPreview,
|
||||||
|
useAutoBackgroundColorContinuousMode,
|
||||||
} = useReaderSettingsStore(
|
} = useReaderSettingsStore(
|
||||||
'shouldSkipDupChapters',
|
'shouldSkipDupChapters',
|
||||||
'shouldSkipFilteredChapters',
|
'shouldSkipFilteredChapters',
|
||||||
'backgroundColor',
|
'backgroundColor',
|
||||||
'readingMode',
|
|
||||||
'tapZoneLayout',
|
|
||||||
'tapZoneInvertMode',
|
|
||||||
'shouldShowReadingModePreview',
|
'shouldShowReadingModePreview',
|
||||||
'shouldShowTapZoneLayoutPreview',
|
'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);
|
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@@ -112,6 +128,30 @@ const BaseReader = ({
|
|||||||
defaultSettingsResponse.loading;
|
defaultSettingsResponse.loading;
|
||||||
const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error;
|
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(() => {
|
useEffect(() => {
|
||||||
getReaderStore().setManga(mangaResponse.data?.manga);
|
getReaderStore().setManga(mangaResponse.data?.manga);
|
||||||
}, [mangaResponse.data?.manga]);
|
}, [mangaResponse.data?.manga]);
|
||||||
@@ -234,10 +274,11 @@ const BaseReader = ({
|
|||||||
pr: safeAreaInset.right ? 'env(safe-area-inset-right)' : undefined,
|
pr: safeAreaInset.right ? 'env(safe-area-inset-right)' : undefined,
|
||||||
pl: safeAreaInset.left ? 'env(safe-area-inset-left)' : undefined,
|
pl: safeAreaInset.left ? 'env(safe-area-inset-left)' : undefined,
|
||||||
marginLeft: `${readerNavBarWidth}px`,
|
marginLeft: `${readerNavBarWidth}px`,
|
||||||
transition: (theme) =>
|
transition: `width 0.${theme.transitions.duration.shortest}s, margin-left 0.${theme.transitions.duration.shortest}s, background 1.${theme.transitions.duration.shortest}s`,
|
||||||
`width 0.${theme.transitions.duration.shortest}s, margin-left 0.${theme.transitions.duration.shortest}s`,
|
|
||||||
overflow: 'auto',
|
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} />
|
<ReaderViewer ref={scrollElementRef} />
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefine
|
|||||||
shouldShowPageNumber: undefined,
|
shouldShowPageNumber: undefined,
|
||||||
isStaticNav: undefined,
|
isStaticNav: undefined,
|
||||||
backgroundColor: undefined,
|
backgroundColor: undefined,
|
||||||
|
useAutoBackgroundColorContinuousMode: undefined,
|
||||||
hotkeys: undefined,
|
hotkeys: undefined,
|
||||||
imagePreLoadAmount: undefined,
|
imagePreLoadAmount: undefined,
|
||||||
shouldUseAutoWebtoonMode: undefined,
|
shouldUseAutoWebtoonMode: undefined,
|
||||||
@@ -180,6 +181,7 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
|||||||
readingMode: ReadingMode.SINGLE_PAGE,
|
readingMode: ReadingMode.SINGLE_PAGE,
|
||||||
exitMode: ReaderExitMode.PREVIOUS,
|
exitMode: ReaderExitMode.PREVIOUS,
|
||||||
backgroundColor: ReaderBackgroundColor.THEME,
|
backgroundColor: ReaderBackgroundColor.THEME,
|
||||||
|
useAutoBackgroundColorContinuousMode: false,
|
||||||
customFilter: {
|
customFilter: {
|
||||||
brightness: {
|
brightness: {
|
||||||
value: CUSTOM_FILTER.brightness.default,
|
value: CUSTOM_FILTER.brightness.default,
|
||||||
@@ -379,6 +381,8 @@ export const READER_BACKGROUND_TO_COLOR = {
|
|||||||
[ReaderBackgroundColor.BLACK]: 'common.black',
|
[ReaderBackgroundColor.BLACK]: 'common.black',
|
||||||
[ReaderBackgroundColor.GRAY]: 'grey.200',
|
[ReaderBackgroundColor.GRAY]: 'grey.200',
|
||||||
[ReaderBackgroundColor.WHITE]: 'common.white',
|
[ReaderBackgroundColor.WHITE]: 'common.white',
|
||||||
|
// Used as the fallback color
|
||||||
|
[ReaderBackgroundColor.AUTO]: 'background.default',
|
||||||
} as const satisfies Record<ReaderBackgroundColor, string>;
|
} as const satisfies Record<ReaderBackgroundColor, string>;
|
||||||
|
|
||||||
export const CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION: Record<
|
export const CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION: Record<
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ import type {
|
|||||||
ReaderPagerProps,
|
ReaderPagerProps,
|
||||||
SafeAreaInset,
|
SafeAreaInset,
|
||||||
} from '@/features/reader/Reader.types.ts';
|
} 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 type { MangaGenreInfo, MangaSourceNameInfo } from '@/features/manga/Manga.types.ts';
|
||||||
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
||||||
import { ReaderPagedPager } from '@/features/reader/viewer/pager/components/ReaderPagedPager.tsx';
|
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 { ReaderVerticalPager } from '@/features/reader/viewer/pager/components/ReaderVerticalPager.tsx';
|
||||||
import { ReaderHorizontalPager } from '@/features/reader/viewer/pager/components/ReaderHorizontalPager.tsx';
|
import { ReaderHorizontalPager } from '@/features/reader/viewer/pager/components/ReaderHorizontalPager.tsx';
|
||||||
import { ScrollDirection } from '@/base/Base.types.ts';
|
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 =>
|
export const isOffsetDoubleSpreadPagesEditable = (readingMode: IReaderSettings['readingMode']): boolean =>
|
||||||
readingMode === ReadingMode.DOUBLE_PAGE;
|
readingMode === ReadingMode.DOUBLE_PAGE;
|
||||||
@@ -117,3 +126,33 @@ export const getSafeAreaInsets = (
|
|||||||
...(direction === ScrollDirection.Y && safeAreaInset.bottom ? ['env(safe-area-inset-bottom)'] : []),
|
...(direction === ScrollDirection.Y && safeAreaInset.bottom ? ['env(safe-area-inset-bottom)'] : []),
|
||||||
...(direction === ScrollDirection.X && safeAreaInset.left ? ['env(safe-area-inset-left)'] : []),
|
...(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
|
<ReaderSettingBackgroundColor
|
||||||
backgroundColor={settings.backgroundColor}
|
backgroundColor={settings.backgroundColor}
|
||||||
updateSetting={(value) => updateSetting('backgroundColor', value)}
|
useAutoBackgroundColorContinuousMode={settings.useAutoBackgroundColorContinuousMode}
|
||||||
|
updateSetting={(...args) => updateSetting(...args)}
|
||||||
/>
|
/>
|
||||||
<ReaderSettingProgressBarType
|
<ReaderSettingProgressBarType
|
||||||
overlayMode={overlayMode}
|
overlayMode={overlayMode}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import type { ValueToDisplayData } from '@/base/Base.types.ts';
|
|||||||
import { ButtonSelectInput } from '@/base/components/inputs/ButtonSelectInput.tsx';
|
import { ButtonSelectInput } from '@/base/components/inputs/ButtonSelectInput.tsx';
|
||||||
import type { IReaderSettingsWithDefaultFlag } from '@/features/reader/Reader.types.ts';
|
import type { IReaderSettingsWithDefaultFlag } from '@/features/reader/Reader.types.ts';
|
||||||
import { ReaderBackgroundColor } 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> = {
|
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderBackgroundColor> = {
|
||||||
[ReaderBackgroundColor.THEME]: {
|
[ReaderBackgroundColor.THEME]: {
|
||||||
@@ -30,6 +32,10 @@ const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderBackgroundColor> = {
|
|||||||
title: msg`White`,
|
title: msg`White`,
|
||||||
icon: null,
|
icon: null,
|
||||||
},
|
},
|
||||||
|
[ReaderBackgroundColor.AUTO]: {
|
||||||
|
title: msg`Auto`,
|
||||||
|
icon: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const READER_BACKGROUND_COLOR_VALUES = Object.values(ReaderBackgroundColor).filter(
|
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 = ({
|
export const ReaderSettingBackgroundColor = ({
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
|
useAutoBackgroundColorContinuousMode,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
}: Pick<IReaderSettingsWithDefaultFlag, 'backgroundColor'> & {
|
}: Pick<IReaderSettingsWithDefaultFlag, 'backgroundColor' | 'useAutoBackgroundColorContinuousMode'> & {
|
||||||
updateSetting: (color: ReaderBackgroundColor) => void;
|
updateSetting: (...args: Parameters<typeof ReaderService.updateSetting>) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<ButtonSelectInput
|
<ButtonSelectInput
|
||||||
label={t`Background color`}
|
label={t`Background color`}
|
||||||
value={backgroundColor}
|
value={backgroundColor}
|
||||||
values={READER_BACKGROUND_COLOR_VALUES}
|
values={READER_BACKGROUND_COLOR_VALUES}
|
||||||
setValue={updateSetting}
|
setValue={(value) => updateSetting('backgroundColor', value)}
|
||||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
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'
|
| 'setPageUrls'
|
||||||
| 'setPageSpreadStates'
|
| 'setPageSpreadStates'
|
||||||
| 'setPageLoadStates'
|
| 'setPageLoadStates'
|
||||||
|
| 'setPageBackgroundColor'
|
||||||
| 'setPages'
|
| 'setPages'
|
||||||
| 'setTransitionPageMode'
|
| 'setTransitionPageMode'
|
||||||
| 'setRetryFailedPagesKeyPrefix'
|
| 'setRetryFailedPagesKeyPrefix'
|
||||||
@@ -36,6 +37,7 @@ export const READER_DEFAULT_PAGES_STATE: Omit<
|
|||||||
pageUrls: [],
|
pageUrls: [],
|
||||||
pageSpreadStates: [{ url: '', isSpread: false }],
|
pageSpreadStates: [{ url: '', isSpread: false }],
|
||||||
pageLoadStates: [{ url: '', loaded: false }],
|
pageLoadStates: [{ url: '', loaded: false }],
|
||||||
|
pageBackgroundColors: [{ url: '', color: undefined }],
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
name: '1',
|
name: '1',
|
||||||
@@ -114,6 +116,19 @@ export const createReaderPagesStoreSlice = <T extends ReaderPagesStoreSlice>(
|
|||||||
undefined,
|
undefined,
|
||||||
createActionName('setPageLoadStates'),
|
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) =>
|
setPages: (pages) =>
|
||||||
set(
|
set(
|
||||||
(draft) => {
|
(draft) => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import Box from '@mui/material/Box';
|
|||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import type {
|
import type {
|
||||||
IReaderSettings,
|
IReaderSettings,
|
||||||
|
ReaderPageBackgroundColor,
|
||||||
ReaderPagerProps,
|
ReaderPagerProps,
|
||||||
ReaderPageSpreadState,
|
ReaderPageSpreadState,
|
||||||
ReaderResumeMode,
|
ReaderResumeMode,
|
||||||
@@ -124,6 +125,9 @@ const BaseReaderChapterViewer = ({
|
|||||||
const [pagesToSpreadState, setPagesToSpreadState] = useState<ReaderPageSpreadState[]>(
|
const [pagesToSpreadState, setPagesToSpreadState] = useState<ReaderPageSpreadState[]>(
|
||||||
READER_DEFAULT_PAGES_STATE.pageSpreadStates,
|
READER_DEFAULT_PAGES_STATE.pageSpreadStates,
|
||||||
);
|
);
|
||||||
|
const [pageBackgroundColors, setPageBackgroundColors] = useState<ReaderPageBackgroundColor[]>(
|
||||||
|
READER_DEFAULT_PAGES_STATE.pageBackgroundColors,
|
||||||
|
);
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const isCurrentChapterRef = useRef(isCurrentChapter);
|
const isCurrentChapterRef = useRef(isCurrentChapter);
|
||||||
@@ -197,6 +201,13 @@ const BaseReaderChapterViewer = ({
|
|||||||
|
|
||||||
setPageLoadStates(value);
|
setPageLoadStates(value);
|
||||||
},
|
},
|
||||||
|
(value) => {
|
||||||
|
if (isCurrentChapterRef.current) {
|
||||||
|
getReaderPagesStore().setPageBackgroundColor(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPageBackgroundColors(value);
|
||||||
|
},
|
||||||
readingMode,
|
readingMode,
|
||||||
),
|
),
|
||||||
[actualPages, readingMode],
|
[actualPages, readingMode],
|
||||||
@@ -251,6 +262,7 @@ const BaseReaderChapterViewer = ({
|
|||||||
actualPages,
|
actualPages,
|
||||||
pageLoadStates,
|
pageLoadStates,
|
||||||
pagesToSpreadState,
|
pagesToSpreadState,
|
||||||
|
pageBackgroundColors,
|
||||||
arePagesFetched,
|
arePagesFetched,
|
||||||
setArePagesFetched,
|
setArePagesFetched,
|
||||||
(value) => updateState(value, noOp, getReaderChaptersStore().setReaderStateChapters),
|
(value) => updateState(value, noOp, getReaderChaptersStore().setReaderStateChapters),
|
||||||
@@ -265,6 +277,12 @@ const BaseReaderChapterViewer = ({
|
|||||||
setPagesToSpreadState,
|
setPagesToSpreadState,
|
||||||
getReaderPagesStore().setPageSpreadStates.bind(getReaderPagesStore()),
|
getReaderPagesStore().setPageSpreadStates.bind(getReaderPagesStore()),
|
||||||
),
|
),
|
||||||
|
(value) =>
|
||||||
|
updateState(
|
||||||
|
value,
|
||||||
|
setPageBackgroundColors,
|
||||||
|
getReaderPagesStore().setPageBackgroundColor.bind(getReaderPagesStore()),
|
||||||
|
),
|
||||||
(value) => updateState(value, noOp, getReaderPagesStore().setCurrentPageIndex.bind(getReaderPagesStore())),
|
(value) => updateState(value, noOp, getReaderPagesStore().setCurrentPageIndex.bind(getReaderPagesStore())),
|
||||||
(value) => {
|
(value) => {
|
||||||
if ((isInitialChapter && !arePagesFetched) || scrollIntoView) {
|
if ((isInitialChapter && !arePagesFetched) || scrollIntoView) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { ComponentProps } from 'react';
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
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 { ReaderTransitionPageMode, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||||
import { isTransitionPageVisible } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
import { isTransitionPageVisible } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||||
import { useBackButton } from '@/base/hooks/useBackButton.ts';
|
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 type { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||||
import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx';
|
import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx';
|
||||||
import { getValueFromObject, noOp } from '@/lib/HelperFunctions.ts';
|
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 type { ChapterIdInfo, ChapterNameInfo, ChapterScanlatorInfo } from '@/features/chapter/Chapter.types.ts';
|
||||||
import {
|
import {
|
||||||
useReaderChaptersStore,
|
useReaderChaptersStore,
|
||||||
@@ -37,6 +36,7 @@ import {
|
|||||||
useReaderSettingsStore,
|
useReaderSettingsStore,
|
||||||
useReaderStore,
|
useReaderStore,
|
||||||
} from '@/features/reader/stores/ReaderStore.ts';
|
} from '@/features/reader/stores/ReaderStore.ts';
|
||||||
|
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||||
|
|
||||||
const ChapterInfo = ({
|
const ChapterInfo = ({
|
||||||
title,
|
title,
|
||||||
@@ -47,12 +47,12 @@ const ChapterInfo = ({
|
|||||||
title: string;
|
title: string;
|
||||||
name?: ChapterNameInfo['name'];
|
name?: ChapterNameInfo['name'];
|
||||||
scanlator?: ChapterScanlatorInfo['scanlator'];
|
scanlator?: ChapterScanlatorInfo['scanlator'];
|
||||||
backgroundColor: IReaderSettings['backgroundColor'];
|
backgroundColor: ReaderBackgroundColor;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const contrastText = theme.palette.getContrastText(
|
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);
|
const disabledText = theme.alpha(contrastText, 0.5);
|
||||||
|
|
||||||
@@ -131,21 +131,22 @@ const BaseReaderTransitionPage = ({
|
|||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
backgroundColor: READER_BACKGROUND_TO_COLOR[backgroundColor],
|
||||||
...applyStyles(!isContinuousReadingMode(readingMode), {
|
...applyStyles(!isContinuousReadingMode(readingMode), {
|
||||||
width: '100%',
|
width: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
||||||
height: '100%',
|
height: `calc(100vh - ${scrollbar.xSize}px)`,
|
||||||
}),
|
}),
|
||||||
...applyStyles(isContinuousReadingMode(readingMode), {
|
...applyStyles(isContinuousReadingMode(readingMode), {
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||||
left: 0,
|
left: 0,
|
||||||
maxWidth: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
width: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
||||||
minHeight: `calc(100vh - ${scrollbar.xSize}px)`,
|
height: `calc(100vh - ${scrollbar.xSize}px)`,
|
||||||
}),
|
}),
|
||||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||||
top: 0,
|
top: 0,
|
||||||
minWidth: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
width: `calc(100vw - ${scrollbar.ySize}px - ${readerNavBarWidth}px)`,
|
||||||
maxHeight: `calc(100vh - ${scrollbar.xSize}px)`,
|
height: `calc(100vh - ${scrollbar.xSize}px)`,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -9,7 +9,12 @@
|
|||||||
import { useLayoutEffect, useRef } from 'react';
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
import { getInitialReaderPageIndex } from '@/features/reader/Reader.utils.ts';
|
import { getInitialReaderPageIndex } from '@/features/reader/Reader.utils.ts';
|
||||||
import { createPagesData } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
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 { ReaderTransitionPageMode } from '@/features/reader/Reader.types.ts';
|
||||||
import type { requestManager } from '@/lib/requests/RequestManager.ts';
|
import type { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import type { TChapterReader } from '@/features/chapter/Chapter.types.ts';
|
import type { TChapterReader } from '@/features/chapter/Chapter.types.ts';
|
||||||
@@ -25,6 +30,7 @@ export const useReaderSetPagesState = (
|
|||||||
pages: ReaderStatePages['pages'],
|
pages: ReaderStatePages['pages'],
|
||||||
pageLoadStates: ReaderStatePages['pageLoadStates'],
|
pageLoadStates: ReaderStatePages['pageLoadStates'],
|
||||||
pagesToSpreadState: ReaderPageSpreadState[],
|
pagesToSpreadState: ReaderPageSpreadState[],
|
||||||
|
pageBackgroundColors: ReaderPageBackgroundColor[],
|
||||||
arePagesFetched: boolean,
|
arePagesFetched: boolean,
|
||||||
setArePagesFetched: (fetched: boolean) => void,
|
setArePagesFetched: (fetched: boolean) => void,
|
||||||
setReaderStateChapters: ReaderChaptersStoreSlice['chapters']['setReaderStateChapters'],
|
setReaderStateChapters: ReaderChaptersStoreSlice['chapters']['setReaderStateChapters'],
|
||||||
@@ -33,6 +39,7 @@ export const useReaderSetPagesState = (
|
|||||||
setPageUrls: ReaderStatePages['setPageUrls'],
|
setPageUrls: ReaderStatePages['setPageUrls'],
|
||||||
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
||||||
setPagesToSpreadState: (state: ReaderPageSpreadState[]) => void,
|
setPagesToSpreadState: (state: ReaderPageSpreadState[]) => void,
|
||||||
|
setPageBackgroundColors: (state: ReaderPageBackgroundColor[]) => void,
|
||||||
setCurrentPageIndex: ReaderStatePages['setCurrentPageIndex'],
|
setCurrentPageIndex: ReaderStatePages['setCurrentPageIndex'],
|
||||||
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
||||||
setTransitionPageMode: ReaderStatePages['setTransitionPageMode'],
|
setTransitionPageMode: ReaderStatePages['setTransitionPageMode'],
|
||||||
@@ -62,10 +69,12 @@ export const useReaderSetPagesState = (
|
|||||||
setPageUrls(newPages);
|
setPageUrls(newPages);
|
||||||
setPageLoadStates(newPageData.map(({ primary: { url } }) => ({ url, loaded: false })));
|
setPageLoadStates(newPageData.map(({ primary: { url } }) => ({ url, loaded: false })));
|
||||||
setPagesToSpreadState(newPageData.map(({ primary: { url } }) => ({ url, isSpread: false })));
|
setPagesToSpreadState(newPageData.map(({ primary: { url } }) => ({ url, isSpread: false })));
|
||||||
|
setPageBackgroundColors(newPageData.map(({ primary: { url } }) => ({ url, color: undefined })));
|
||||||
} else {
|
} else {
|
||||||
setPages(pages);
|
setPages(pages);
|
||||||
setPageLoadStates(pageLoadStates);
|
setPageLoadStates(pageLoadStates);
|
||||||
setPagesToSpreadState(pagesToSpreadState);
|
setPagesToSpreadState(pagesToSpreadState);
|
||||||
|
setPageBackgroundColors(pageBackgroundColors);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotalPages(pagesPayload.pages.length);
|
setTotalPages(pagesPayload.pages.length);
|
||||||
|
|||||||
@@ -648,7 +648,5 @@ export const getScrollToXForReadingDirection = (
|
|||||||
return getOptionForDirection(-element.scrollWidth, 0, themeDirectionForReadingDirection);
|
return getOptionForDirection(-element.scrollWidth, 0, themeDirectionForReadingDirection);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPageOfOutdatedPageLoadStates = (
|
export const isPageOfOutdatedPageLoadStates = (url: string, state: { url: string } | undefined): boolean =>
|
||||||
url: string,
|
state === undefined || state.url !== url;
|
||||||
pageLoadState: ReaderStatePages['pageLoadStates'][number] | undefined,
|
|
||||||
): boolean => pageLoadState === undefined || pageLoadState.url !== url;
|
|
||||||
|
|||||||
@@ -499,6 +499,7 @@ msgstr "Author"
|
|||||||
|
|
||||||
#: src/features/reader/overlay/progress-bar/settings/components/ReaderSettingProgressBarPosition.tsx
|
#: src/features/reader/overlay/progress-bar/settings/components/ReaderSettingProgressBarPosition.tsx
|
||||||
#: src/features/reader/overlay/settings/ReaderSettingOverlayMode.tsx
|
#: src/features/reader/overlay/settings/ReaderSettingOverlayMode.tsx
|
||||||
|
#: src/features/reader/settings/general/components/ReaderSettingBackgroundColor.tsx
|
||||||
msgid "Auto"
|
msgid "Auto"
|
||||||
msgstr "Auto"
|
msgstr "Auto"
|
||||||
|
|
||||||
@@ -1578,6 +1579,10 @@ msgstr "Electron"
|
|||||||
msgid "Electron path"
|
msgid "Electron path"
|
||||||
msgstr "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
|
#: src/features/settings/screens/ServerSettings.tsx
|
||||||
msgid "Enable debug logs"
|
msgid "Enable debug logs"
|
||||||
msgstr "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