Make reader width configurable (#565)
Signed-off-by: Chance Zibolski <chance.zibolski@gmail.com>
This commit is contained in:
@@ -23,7 +23,7 @@ import ListItem from '@mui/material/ListItem';
|
|||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ChapterOffset, IReaderSettings, TChapter, TManga } from '@/typings';
|
import { AllowedMetadataValueTypes, ChapterOffset, IReaderSettings, TChapter, TManga } from '@/typings';
|
||||||
import { ReaderSettingsOptions } from '@/components/reader/ReaderSettingsOptions';
|
import { ReaderSettingsOptions } from '@/components/reader/ReaderSettingsOptions';
|
||||||
|
|
||||||
const Root = styled('div')(({ theme }) => ({
|
const Root = styled('div')(({ theme }) => ({
|
||||||
@@ -112,7 +112,7 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
settings: IReaderSettings;
|
settings: IReaderSettings;
|
||||||
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
|
setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes) => void;
|
||||||
manga: TManga;
|
manga: TManga;
|
||||||
chapter: TChapter;
|
chapter: TChapter;
|
||||||
curPage: number;
|
curPage: number;
|
||||||
@@ -141,7 +141,7 @@ export function ReaderNavBar(props: IProps) {
|
|||||||
|
|
||||||
const disableChapterNavButtons = retrievingNextChapter;
|
const disableChapterNavButtons = retrievingNextChapter;
|
||||||
|
|
||||||
const updateSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
const updateSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes) => {
|
||||||
// prevent closing the navBar when updating the "staticNav" setting
|
// prevent closing the navBar when updating the "staticNav" setting
|
||||||
setUpdateDrawerOnRender(key !== 'staticNav');
|
setUpdateDrawerOnRender(key !== 'staticNav');
|
||||||
setSettingValue(key, value);
|
setSettingValue(key, value);
|
||||||
@@ -269,6 +269,7 @@ export function ReaderNavBar(props: IProps) {
|
|||||||
fitPageToWindow={settings.fitPageToWindow}
|
fitPageToWindow={settings.fitPageToWindow}
|
||||||
readerType={settings.readerType}
|
readerType={settings.readerType}
|
||||||
offsetFirstPage={settings.offsetFirstPage}
|
offsetFirstPage={settings.offsetFirstPage}
|
||||||
|
readerWidth={settings.readerWidth}
|
||||||
/>
|
/>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
<Divider sx={{ my: 1, mx: 2 }} />
|
<Divider sx={{ my: 1, mx: 2 }} />
|
||||||
|
|||||||
@@ -8,9 +8,12 @@
|
|||||||
|
|
||||||
import { useState, useEffect, forwardRef, useRef } from 'react';
|
import { useState, useEffect, forwardRef, useRef } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { IReaderSettings } from '@/typings';
|
import { IReaderSettings, ReaderType } from '@/typings';
|
||||||
import { SpinnerImage } from '@/components/util/SpinnerImage';
|
import { SpinnerImage } from '@/components/util/SpinnerImage';
|
||||||
|
|
||||||
|
export const isFillsPageReaderType = (readerType: ReaderType): boolean =>
|
||||||
|
['DoubleRTL', 'DoubleLTR', 'ContinuesHorizontalLTR', 'ContinuesHorizontalRTL'].includes(readerType);
|
||||||
|
|
||||||
function imageStyle(settings: IReaderSettings): any {
|
function imageStyle(settings: IReaderSettings): any {
|
||||||
const [dimensions, setDimensions] = useState({
|
const [dimensions, setDimensions] = useState({
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
@@ -29,13 +32,7 @@ function imageStyle(settings: IReaderSettings): any {
|
|||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
if (
|
if (settings.fitPageToWindow || isFillsPageReaderType(settings.readerType)) {
|
||||||
settings.fitPageToWindow ||
|
|
||||||
settings.readerType === 'DoubleLTR' ||
|
|
||||||
settings.readerType === 'DoubleRTL' ||
|
|
||||||
settings.readerType === 'ContinuesHorizontalLTR' ||
|
|
||||||
settings.readerType === 'ContinuesHorizontalRTL'
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
marginLeft: '7px',
|
marginLeft: '7px',
|
||||||
@@ -51,9 +48,11 @@ function imageStyle(settings: IReaderSettings): any {
|
|||||||
return {
|
return {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
|
marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
|
||||||
minWidth: '50vw',
|
minWidth: '10vw',
|
||||||
width: dimensions.width < dimensions.height ? '100vw' : '100%',
|
width: dimensions.width < dimensions.height ? '100vw' : `${settings.readerWidth}%`,
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import { List, ListItem, ListItemText, Switch } from '@mui/material';
|
|||||||
import Select from '@mui/material/Select';
|
import Select from '@mui/material/Select';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IReaderSettings } from '@/typings';
|
import { AllowedMetadataValueTypes, IReaderSettings } from '@/typings';
|
||||||
|
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||||
|
import { isFillsPageReaderType } from '@/components/reader/Page.tsx';
|
||||||
|
|
||||||
interface IProps extends IReaderSettings {
|
interface IProps extends IReaderSettings {
|
||||||
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
|
setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReaderSettingsOptions({
|
export function ReaderSettingsOptions({
|
||||||
@@ -25,15 +27,10 @@ export function ReaderSettingsOptions({
|
|||||||
setSettingValue,
|
setSettingValue,
|
||||||
fitPageToWindow,
|
fitPageToWindow,
|
||||||
offsetFirstPage,
|
offsetFirstPage,
|
||||||
|
readerWidth,
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fitPageToWindowEligible = [
|
const fitPageToWindowEligible = !isFillsPageReaderType(readerType);
|
||||||
'ContinuesVertical',
|
|
||||||
'Webtoon',
|
|
||||||
'SingleVertical',
|
|
||||||
'SingleRTL',
|
|
||||||
'SingleLTR',
|
|
||||||
].includes(readerType);
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -68,7 +65,7 @@ export function ReaderSettingsOptions({
|
|||||||
onChange={(e) => setSettingValue('skipDupChapters', e.target.checked)}
|
onChange={(e) => setSettingValue('skipDupChapters', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{fitPageToWindowEligible ? (
|
{fitPageToWindowEligible && (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary={t('reader.settings.label.fit_page_to_window')} />
|
<ListItemText primary={t('reader.settings.label.fit_page_to_window')} />
|
||||||
<Switch
|
<Switch
|
||||||
@@ -77,8 +74,8 @@ export function ReaderSettingsOptions({
|
|||||||
onChange={(e) => setSettingValue('fitPageToWindow', e.target.checked)}
|
onChange={(e) => setSettingValue('fitPageToWindow', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
)}
|
||||||
{readerType === 'DoubleLTR' || readerType === 'DoubleRTL' ? (
|
{(readerType === 'DoubleLTR' || readerType === 'DoubleRTL') && (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary={t('reader.settings.label.offset_first_page')} />
|
<ListItemText primary={t('reader.settings.label.offset_first_page')} />
|
||||||
<Switch
|
<Switch
|
||||||
@@ -87,7 +84,22 @@ export function ReaderSettingsOptions({
|
|||||||
onChange={(e) => setSettingValue('offsetFirstPage', e.target.checked)}
|
onChange={(e) => setSettingValue('offsetFirstPage', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
)}
|
||||||
|
{fitPageToWindowEligible && !fitPageToWindow && (
|
||||||
|
<NumberSetting
|
||||||
|
settingTitle={t('reader.settings.label.reader_width')}
|
||||||
|
dialogTitle={t('reader.settings.label.reader_width')}
|
||||||
|
settingValue={`${readerWidth}%`}
|
||||||
|
value={readerWidth}
|
||||||
|
minValue={10}
|
||||||
|
maxValue={100}
|
||||||
|
defaultValue={100}
|
||||||
|
valueUnit="%"
|
||||||
|
showSlider
|
||||||
|
handleUpdate={(width: number) => setSettingValue('readerWidth', width)}
|
||||||
|
listItemTextSx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary={t('reader.settings.label.reader_type')} />
|
<ListItemText primary={t('reader.settings.label.reader_type')} />
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Dialog from '@mui/material/Dialog';
|
|||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import { InputAdornment, ListItemText, Stack, Typography } from '@mui/material';
|
import { InputAdornment, ListItemText, Stack, SxProps, Typography, Theme } from '@mui/material';
|
||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
@@ -38,6 +38,7 @@ type BaseProps = {
|
|||||||
handleUpdate: (value: number) => void;
|
handleUpdate: (value: number) => void;
|
||||||
showSlider?: never;
|
showSlider?: never;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
listItemTextSx?: SxProps<Theme>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsWithSlider = Omit<BaseProps, 'defaultValue' | 'minValue' | 'maxValue' | 'showSlider'> &
|
type PropsWithSlider = Omit<BaseProps, 'defaultValue' | 'minValue' | 'maxValue' | 'showSlider'> &
|
||||||
@@ -61,6 +62,7 @@ export const NumberSetting = ({
|
|||||||
handleUpdate,
|
handleUpdate,
|
||||||
showSlider,
|
showSlider,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
listItemTextSx: sx,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -107,6 +109,7 @@ export const NumberSetting = ({
|
|||||||
<ListItemText
|
<ListItemText
|
||||||
primary={settingTitle}
|
primary={settingTitle}
|
||||||
secondary={settingValue ?? t('global.label.loading')}
|
secondary={settingValue ?? t('global.label.loading')}
|
||||||
|
sx={sx}
|
||||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
@@ -150,7 +153,7 @@ export const NumberSetting = ({
|
|||||||
}}
|
}}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
inputProps: { min: minValue, max: maxValue, step: stepSize },
|
inputProps: { min: minValue, max: maxValue, step: stepSize },
|
||||||
startAdornment: <InputAdornment position="start">{valueUnit}</InputAdornment>,
|
endAdornment: <InputAdornment position="end">{valueUnit}</InputAdornment>,
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
value={dialogValue}
|
value={dialogValue}
|
||||||
|
|||||||
@@ -607,7 +607,8 @@
|
|||||||
"reader_type": "Reader type",
|
"reader_type": "Reader type",
|
||||||
"show_page_number": "Show page number",
|
"show_page_number": "Show page number",
|
||||||
"skip_dup_chapters": "Skip duplicate chapters",
|
"skip_dup_chapters": "Skip duplicate chapters",
|
||||||
"static_navigation": "Static navigation"
|
"static_navigation": "Static navigation",
|
||||||
|
"reader_width": "Reader width"
|
||||||
},
|
},
|
||||||
"reader_type": {
|
"reader_type": {
|
||||||
"label": {
|
"label": {
|
||||||
|
|||||||
@@ -11,7 +11,15 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'r
|
|||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ChapterOffset, IReaderSettings, ReaderType, TChapter, TManga, TranslationKey } from '@/typings';
|
import {
|
||||||
|
AllowedMetadataValueTypes,
|
||||||
|
ChapterOffset,
|
||||||
|
IReaderSettings,
|
||||||
|
ReaderType,
|
||||||
|
TChapter,
|
||||||
|
TManga,
|
||||||
|
TranslationKey,
|
||||||
|
} from '@/typings';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import {
|
import {
|
||||||
checkAndHandleMissingStoredReaderSettings,
|
checkAndHandleMissingStoredReaderSettings,
|
||||||
@@ -227,7 +235,7 @@ export function Reader() {
|
|||||||
.response.catch();
|
.response.catch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
const setSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes) => {
|
||||||
setSettings({ ...settings, [key]: value });
|
setSettings({ ...settings, [key]: value });
|
||||||
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
||||||
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useContext, useEffect } from 'react';
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IReaderSettings } from '@/typings';
|
import { AllowedMetadataValueTypes, IReaderSettings } from '@/typings';
|
||||||
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata';
|
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata';
|
||||||
import {
|
import {
|
||||||
checkAndHandleMissingStoredReaderSettings,
|
checkAndHandleMissingStoredReaderSettings,
|
||||||
@@ -34,7 +34,7 @@ export function DefaultReaderSettings() {
|
|||||||
|
|
||||||
useSetDefaultBackTo('settings');
|
useSetDefaultBackTo('settings');
|
||||||
|
|
||||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
const setSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes) => {
|
||||||
requestUpdateServerMetadata(convertToGqlMeta(metadata)! ?? {}, [[key, value]]).catch(() =>
|
requestUpdateServerMetadata(convertToGqlMeta(metadata)! ?? {}, [[key, value]]).catch(() =>
|
||||||
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
||||||
);
|
);
|
||||||
@@ -71,6 +71,7 @@ export function DefaultReaderSettings() {
|
|||||||
fitPageToWindow={settings.fitPageToWindow}
|
fitPageToWindow={settings.fitPageToWindow}
|
||||||
readerType={settings.readerType}
|
readerType={settings.readerType}
|
||||||
offsetFirstPage={settings.offsetFirstPage}
|
offsetFirstPage={settings.offsetFirstPage}
|
||||||
|
readerWidth={settings.readerWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ export interface IReaderSettings {
|
|||||||
fitPageToWindow: boolean;
|
fitPageToWindow: boolean;
|
||||||
readerType: ReaderType;
|
readerType: ReaderType;
|
||||||
offsetFirstPage: boolean;
|
offsetFirstPage: boolean;
|
||||||
|
readerWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ChapterOffset {
|
export enum ChapterOffset {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const getDefaultSettings = (): IReaderSettings => ({
|
|||||||
fitPageToWindow: false,
|
fitPageToWindow: false,
|
||||||
readerType: 'ContinuesVertical',
|
readerType: 'ContinuesVertical',
|
||||||
offsetFirstPage: false,
|
offsetFirstPage: false,
|
||||||
|
readerWidth: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getReaderSettingsWithDefaultValueFallback = <DefaultSettings extends IReaderSettings | UndefinedReaderSettings>(
|
const getReaderSettingsWithDefaultValueFallback = <DefaultSettings extends IReaderSettings | UndefinedReaderSettings>(
|
||||||
@@ -84,6 +85,7 @@ export const checkAndHandleMissingStoredReaderSettings = async (
|
|||||||
fitPageToWindow: undefined,
|
fitPageToWindow: undefined,
|
||||||
readerType: undefined,
|
readerType: undefined,
|
||||||
offsetFirstPage: undefined,
|
offsetFirstPage: undefined,
|
||||||
|
readerWidth: undefined,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user