Add "safe area inset" setting to reader

This commit is contained in:
schroda
2026-05-08 01:39:48 +02:00
parent 6d3eb4266b
commit d1e70c2609
15 changed files with 153 additions and 40 deletions

View File

@@ -8,7 +8,7 @@
import deepmerge from '@mui/utils/deepmerge';
import type { AppMetadataKeys, IMetadataMigration } from '@/features/metadata/Metadata.types.ts';
import type { IReaderSettings, ReaderCustomFilter } from '@/features/reader/Reader.types.ts';
import type { IReaderSettings, ReaderCustomFilter, SafeAreaInset } from '@/features/reader/Reader.types.ts';
import { ProgressBarPosition, ReaderPageScaleMode, ReadingMode } from '@/features/reader/Reader.types.ts';
import {
AUTO_SCROLL_SPEED,
@@ -394,6 +394,9 @@ export const APP_METADATA: Record<
return locale;
},
},
safeAreaInset: {
convert: convertToObject<SafeAreaInset>,
},
} as const;
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA);

View File

@@ -136,6 +136,13 @@ export enum ReaderScrollAmount {
LARGE = 95,
}
export interface SafeAreaInset {
top: boolean;
right: boolean;
bottom: boolean;
left: boolean;
}
export interface IReaderSettingsGlobal {
overlayMode: ReaderOverlayMode;
exitMode: ReaderExitMode;
@@ -169,6 +176,7 @@ export interface IReaderSettingsGlobal {
scrollAmount: ReaderScrollAmount;
shouldUseInfiniteScroll: boolean;
shouldShowTransitionPage: boolean;
safeAreaInset: SafeAreaInset;
}
export interface IReaderSettingsManga {

View File

@@ -36,6 +36,7 @@ const BaseReaderPageNumber = ({
shouldShowPageNumber: state.shouldShowPageNumber,
progressBarType: state.progressBarType,
}));
const safeAreaInset = useReaderSettingsStore((state) => state.safeAreaInset);
const isMaximized = useReaderProgressBarStore('isMaximized');
const pageName = useMemo(() => {
@@ -68,7 +69,8 @@ const BaseReaderPageNumber = ({
position: 'fixed',
left: readerNavBarWidth,
right: 0,
bottom: (theme) => `calc(${theme.spacing(1)} + max(${scrollbar.xSize}px, env(safe-area-inset-bottom)))`,
bottom: (theme) =>
`calc(${theme.spacing(1)} + max(${scrollbar.xSize}px, ${safeAreaInset.bottom ? 'env(safe-area-inset-bottom)' : '0px'}))`,
alignItems: 'center',
transition: (theme) => `left 0.${theme.transitions.duration.shortest}s`,
}}

View File

@@ -74,7 +74,9 @@ const BaseReader = ({
'tapZoneInvertMode',
'shouldShowReadingModePreview',
'shouldShowTapZoneLayoutPreview',
'safeAreaInset',
);
const safeAreaInset = useReaderSettingsStore((state) => state.safeAreaInset);
const scrollElementRef = useRef<HTMLDivElement | null>(null);
@@ -228,10 +230,10 @@ const BaseReader = ({
maxWidth: `calc(100vw - ${readerNavBarWidth}px)`,
width: `calc(100vw - ${readerNavBarWidth}px)`,
height: `100vh`,
pt: 'env(safe-area-inset-top)',
pb: 'env(safe-area-inset-bottom)',
pr: 'env(safe-area-inset-right)',
pl: 'env(safe-area-inset-left)',
pt: safeAreaInset.top ? 'env(safe-area-inset-top)' : undefined,
pb: safeAreaInset.bottom ? 'env(safe-area-inset-bottom)' : undefined,
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`,

View File

@@ -155,6 +155,7 @@ const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefine
scrollAmount: undefined,
shouldUseInfiniteScroll: undefined,
shouldShowTransitionPage: undefined,
safeAreaInset: undefined,
};
export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT);
@@ -242,6 +243,12 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
scrollAmount: ReaderScrollAmount.LARGE,
shouldUseInfiniteScroll: true,
shouldShowTransitionPage: true,
safeAreaInset: {
top: true,
right: true,
bottom: true,
left: true,
},
};
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {

View File

@@ -84,6 +84,7 @@ const convertSettingsToMetadata = (
readerWidth: JSON.stringify(settings.readerWidth),
hotkeys: JSON.stringify(settings.hotkeys),
autoScroll: JSON.stringify(settings.autoScroll),
safeAreaInset: JSON.stringify(settings.autoScroll),
});
export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag(

View File

@@ -16,6 +16,7 @@ import { ProgressBarType, ReaderOverlayMode } from '@/features/reader/Reader.typ
import { ReaderSettingOverlayMode } from '@/features/reader/overlay/settings/ReaderSettingOverlayMode.tsx';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { ReaderSettingBackgroundColor } from '@/features/reader/settings/general/components/ReaderSettingBackgroundColor.tsx';
import { ReaderSettingSafeAreaInset } from '@/features/reader/settings/general/components/ReaderSettingSafeAreaInset.tsx';
export const ReaderGeneralSettings = ({
overlayMode,
@@ -59,6 +60,10 @@ export const ReaderGeneralSettings = ({
onChange={(_, checked) => updateSetting('shouldShowPageNumber', checked)}
/>
)}
<ReaderSettingSafeAreaInset
safeAreaInset={settings.safeAreaInset}
updateSetting={(value) => updateSetting('safeAreaInset', value)}
/>
</Stack>
);
};

View File

@@ -0,0 +1,63 @@
/*
* 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/.
*/
import { useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
import type { ValueToDisplayData } from '@/base/Base.types.ts';
import { ButtonSelectInput } from '@/base/components/inputs/ButtonSelectInput.tsx';
import type { IReaderSettingsWithDefaultFlag, SafeAreaInset } from '@/features/reader/Reader.types.ts';
import { DEFAULT_READER_SETTINGS } from '@/features/reader/settings/ReaderSettings.constants.tsx';
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<keyof SafeAreaInset> = {
top: {
title: msg`Top`,
icon: null,
},
right: {
title: msg`Right`,
icon: null,
},
bottom: {
title: msg`Bottom`,
icon: null,
},
left: {
title: msg`Left`,
icon: null,
},
};
const READER_SAFE_AREA_INSET_VALUES = Object.keys(DEFAULT_READER_SETTINGS.safeAreaInset) as (keyof SafeAreaInset)[];
export const ReaderSettingSafeAreaInset = ({
safeAreaInset,
updateSetting,
}: Pick<IReaderSettingsWithDefaultFlag, 'safeAreaInset'> & {
updateSetting: (safeAreaInset: SafeAreaInset) => void;
}) => {
const { t } = useLingui();
return (
<ButtonSelectInput
label={t`Apply safe area padding`}
description={t`Prevents content from overlapping device cutouts and screen edges`}
value={Object.entries(safeAreaInset)
.filter(([, enabled]) => enabled)
.map(([key]) => key as keyof SafeAreaInset)}
values={READER_SAFE_AREA_INSET_VALUES}
setValue={(values) =>
updateSetting(
Object.fromEntries(
READER_SAFE_AREA_INSET_VALUES.map((key) => [key, values.includes(key)]),
) as unknown as SafeAreaInset,
)
}
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
/>
);
};

View File

@@ -93,7 +93,7 @@ export const ReaderSettingTapZoneInvertMode = ({
const tapZonesInvertOption = convertTapZoneInvertModeToOption(tapZoneInvertMode.value);
return (
<ButtonSelectInput
<ButtonSelectInput<TapZonesInvertOption>
{...buttonSelectInputProps}
label={t`Invert tap zones`}
value={tapZoneInvertMode.isDefault ? undefined : tapZonesInvertOption}