Simplify changing reader desktop quick settings

This commit is contained in:
schroda
2026-06-02 15:00:39 +02:00
parent 2964612da1
commit 78e2a99847
7 changed files with 179 additions and 18 deletions

View File

@@ -29,7 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- (**Migration**) Change migration match exclude/include icons - (**Migration**) Change migration match exclude/include icons
- (**Migration**) Show the exclude/include button only for an entry with a selected match - (**Migration**) Show the exclude/include button only for an entry with a selected match
- (**Source/Extension**) Rename language "All" to "Multi" - (**Source/Extension**) Rename language "All" to "Multi"
- - (**Reader**) Simplify changing settings in desktop sidebar
### Fixed ### Fixed

View File

@@ -6,12 +6,20 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import type { StackProps } from '@mui/material/Stack';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import type { ButtonProps } from '@mui/material/Button';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { Superscript } from '@/base/components/texts/Superscript.tsx'; import { Superscript } from '@/base/components/texts/Superscript.tsx';
import type { ValueToDisplayData } from '@/base/Base.types.ts'; import type { ValueToDisplayData } from '@/base/Base.types.ts';
import type { ReactNode, RefObject } from 'react';
import { useCallback, useState } from 'react';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import { assertIsDefined } from '@/base/Asserts.ts';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import { useElementSize } from '@mantine/hooks';
export interface SelectButtonBaseProps<Value extends string | number, MultiValue extends Value | Value[] = Value> { export interface SelectButtonBaseProps<Value extends string | number, MultiValue extends Value | Value[] = Value> {
value: MultiValue; value: MultiValue;
@@ -19,6 +27,9 @@ export interface SelectButtonBaseProps<Value extends string | number, MultiValue
values: Value[]; values: Value[];
setValue: (value: MultiValue) => void; setValue: (value: MultiValue) => void;
valueToDisplayData: ValueToDisplayData<Value>; valueToDisplayData: ValueToDisplayData<Value>;
isCollapsible?: MultiValue extends Value[] ? never : boolean;
tooltip?: MultiValue extends Value[] ? never : ReactNode;
defaultIcon?: MultiValue extends Value[] ? never : ReactNode;
} }
export interface SelectButtonDefaultableProps< export interface SelectButtonDefaultableProps<
@@ -33,7 +44,8 @@ export type SelectButtonProps<Value extends string | number, MultiValue extends
| (SelectButtonBaseProps<Value, MultiValue> & PropertiesNever<SelectButtonDefaultableProps<Value, MultiValue>>) | (SelectButtonBaseProps<Value, MultiValue> & PropertiesNever<SelectButtonDefaultableProps<Value, MultiValue>>)
| SelectButtonDefaultableProps<Value, MultiValue>; | SelectButtonDefaultableProps<Value, MultiValue>;
export const SelectButton = <Value extends string | number, MultiValue extends Value | Value[] = Value>({ const SelectButtonBase = <Value extends string | number, MultiValue extends Value | Value[] = Value>({
ref,
value, value,
values, values,
defaultValue, defaultValue,
@@ -41,15 +53,37 @@ export const SelectButton = <Value extends string | number, MultiValue extends V
valueToDisplayData, valueToDisplayData,
isDefaultable, isDefaultable,
onDefault, onDefault,
}: SelectButtonProps<Value, MultiValue>) => { slotProps,
}: SelectButtonProps<Value, MultiValue> & {
ref?: RefObject<HTMLDivElement>;
slotProps?: {
stack?: StackProps;
defaultButton?: ButtonProps & { hideText?: boolean };
button?: ButtonProps & { hideText?: boolean; tooltip?: (isDefault: boolean, title: string) => ReactNode };
};
}) => {
const { t } = useLingui(); const { t } = useLingui();
return ( return (
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}> <Stack
{...slotProps?.stack}
ref={ref}
sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1, ...slotProps?.stack?.sx }}
>
{isDefaultable && ( {isDefaultable && (
<Button key="default" onClick={onDefault} variant={value === undefined ? 'contained' : 'outlined'}> <CustomTooltip title={slotProps?.defaultButton?.hideText ? t`Default` : null}>
{t`Default`} <Button
</Button> onClick={onDefault}
variant={value === undefined ? 'contained' : 'outlined'}
{...slotProps?.defaultButton}
startIcon={!slotProps?.defaultButton?.hideText && slotProps?.defaultButton?.startIcon}
endIcon={!slotProps?.defaultButton?.hideText && slotProps?.defaultButton?.endIcon}
>
{!slotProps?.defaultButton?.hideText
? t`Default`
: (slotProps?.defaultButton?.startIcon ?? slotProps?.defaultButton?.endIcon)}
</Button>
</CustomTooltip>
)} )}
{values.map((displayValue) => { {values.map((displayValue) => {
const isDefault = value === undefined && displayValue === defaultValue; const isDefault = value === undefined && displayValue === defaultValue;
@@ -74,13 +108,27 @@ export const SelectButton = <Value extends string | number, MultiValue extends V
: t(valueToDisplayData[displayValue].title); : t(valueToDisplayData[displayValue].title);
return ( return (
<CustomTooltip key={displayValue} title={isDefault ? t`Active setting` : ''}> <CustomTooltip
key={displayValue}
title={slotProps?.button?.tooltip?.(isDefault, text) ?? (isDefault ? t`Active setting` : '')}
>
<Button <Button
onClick={() => setValue(newValue)}
variant={isSelected ? 'contained' : 'outlined'} variant={isSelected ? 'contained' : 'outlined'}
startIcon={valueToDisplayData[displayValue].icon} startIcon={!slotProps?.button?.hideText && valueToDisplayData[displayValue].icon}
{...slotProps?.button}
onClick={() => setValue(newValue)}
> >
{isDefault ? <Superscript superscript="*" text={text} /> : text} {(() => {
if (slotProps?.button?.hideText) {
return isDefault ? (
<Superscript superscript="*" text={valueToDisplayData[displayValue].icon} />
) : (
valueToDisplayData[displayValue].icon
);
}
return isDefault ? <Superscript superscript="*" text={text} /> : text;
})()}
</Button> </Button>
</CustomTooltip> </CustomTooltip>
); );
@@ -88,3 +136,108 @@ export const SelectButton = <Value extends string | number, MultiValue extends V
</Stack> </Stack>
); );
}; };
const SelectButtonCollapsible = <Value extends string | number, MultiValue extends Value | Value[] = Value>(
props: SelectButtonProps<Value, MultiValue>,
) => {
const { tooltip, value, valueToDisplayData, defaultValue, onDefault, setValue, defaultIcon } = props;
const { t } = useLingui();
const { ref, height } = useElementSize();
const [isExpanded, setIsExpanded] = useState(false);
const finalOnDefault = useCallback(() => {
setIsExpanded(false);
onDefault?.();
}, [onDefault]);
const finalSetValue = useCallback(
(...args: Parameters<typeof setValue>) => {
setIsExpanded(false);
setValue(...args);
},
[setValue],
);
if (!isExpanded) {
return (
<CustomTooltip title={tooltip}>
<Button
ref={ref}
onClick={() => setIsExpanded(true)}
variant="contained"
size="large"
startIcon={defaultIcon}
sx={{ justifyContent: 'start', textTransform: 'unset', flexGrow: 1 }}
>
{(() => {
const currentValue = (value as Value) ?? defaultValue;
assertIsDefined(currentValue);
const { title } = valueToDisplayData[currentValue];
return (
<Superscript
superscript={`(${t`Default`})`}
text={typeof title === 'string' ? title : t(title)}
/>
);
})()}
</Button>
</CustomTooltip>
);
}
return (
<ClickAwayListener onClickAway={() => setIsExpanded(false)}>
<SelectButtonBase
{...props}
onDefault={finalOnDefault}
setValue={finalSetValue}
slotProps={{
stack: {
sx: {
flexGrow: 1,
},
},
defaultButton: {
hideText: true,
startIcon: <RestartAltIcon />,
size: 'large',
sx: {
flexGrow: 1,
height,
minWidth: 'unset',
px: '10px',
},
},
button: {
hideText: true,
tooltip: (isDefault, title) => (isDefault ? t`Active setting (${title})` : title),
size: 'large',
sx: {
flexGrow: 1,
height,
minWidth: 'unset',
px: '10px',
},
},
}}
/>
</ClickAwayListener>
);
};
export const SelectButton = <Value extends string | number, MultiValue extends Value | Value[] = Value>(
props: SelectButtonProps<Value, MultiValue>,
) => {
const { isCollapsible } = props;
if (isCollapsible) {
return <SelectButtonCollapsible {...props} />;
}
return <SelectButtonBase {...props} />;
};

View File

@@ -8,10 +8,11 @@
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import type { ReactNode } from 'react';
interface SuperscriptProps { interface SuperscriptProps {
superscript: string; superscript: string;
text: string; text: ReactNode;
} }
/** /**

View File

@@ -10,7 +10,6 @@ import Stack from '@mui/material/Stack';
import FitScreenIcon from '@mui/icons-material/FitScreen'; import FitScreenIcon from '@mui/icons-material/FitScreen';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { ValueRotationButton } from '@/base/components/buttons/ValueRotationButton.tsx';
import type { import type {
IReaderSettings, IReaderSettings,
IReaderSettingsWithDefaultFlag, IReaderSettingsWithDefaultFlag,
@@ -23,6 +22,7 @@ import {
} from '@/features/reader/settings/ReaderSettings.constants.tsx'; } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import { CustomIconButton } from '@/base/components/buttons/CustomIconButton.tsx'; import { CustomIconButton } from '@/base/components/buttons/CustomIconButton.tsx';
import type { SelectButtonDefaultableProps } from '@/base/components/buttons/SelectButton.tsx'; import type { SelectButtonDefaultableProps } from '@/base/components/buttons/SelectButton.tsx';
import { SelectButton } from '@/base/components/buttons/SelectButton.tsx';
export const ReaderNavBarDesktopPageScale = ({ export const ReaderNavBarDesktopPageScale = ({
pageScaleMode, pageScaleMode,
@@ -40,7 +40,7 @@ export const ReaderNavBarDesktopPageScale = ({
return ( return (
<Stack sx={{ flexDirection: 'row', gap: 1 }}> <Stack sx={{ flexDirection: 'row', gap: 1 }}>
<ValueRotationButton <SelectButton<ReaderPageScaleMode>
{...buttonSelectInputProps} {...buttonSelectInputProps}
tooltip={t`Scale type`} tooltip={t`Scale type`}
value={pageScaleMode.isDefault ? undefined : pageScaleMode.value} value={pageScaleMode.isDefault ? undefined : pageScaleMode.value}
@@ -49,6 +49,7 @@ export const ReaderNavBarDesktopPageScale = ({
setValue={(value) => updateSetting('pageScaleMode', value)} setValue={(value) => updateSetting('pageScaleMode', value)}
valueToDisplayData={PAGE_SCALE_VALUE_TO_DISPLAY_DATA} valueToDisplayData={PAGE_SCALE_VALUE_TO_DISPLAY_DATA}
defaultIcon={PAGE_SCALE_VALUE_TO_DISPLAY_DATA[pageScaleMode.value].icon} defaultIcon={PAGE_SCALE_VALUE_TO_DISPLAY_DATA[pageScaleMode.value].icon}
isCollapsible
/> />
{READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED[pageScaleMode.value] && ( {READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED[pageScaleMode.value] && (
<CustomTooltip title={t`Stretch small pages`}> <CustomTooltip title={t`Stretch small pages`}>

View File

@@ -7,7 +7,6 @@
*/ */
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { ValueRotationButton } from '@/base/components/buttons/ValueRotationButton.tsx';
import type { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/features/reader/Reader.types.ts'; import type { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/features/reader/Reader.types.ts';
import { import {
READING_DIRECTION_VALUES, READING_DIRECTION_VALUES,
@@ -15,6 +14,7 @@ import {
} from '@/features/reader/settings/ReaderSettings.constants.tsx'; } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import type { SelectButtonDefaultableProps } from '@/base/components/buttons/SelectButton.tsx'; import type { SelectButtonDefaultableProps } from '@/base/components/buttons/SelectButton.tsx';
import { SelectButton } from '@/base/components/buttons/SelectButton.tsx';
export const ReaderNavBarDesktopReadingDirection = ({ export const ReaderNavBarDesktopReadingDirection = ({
readingDirection, readingDirection,
@@ -27,7 +27,7 @@ export const ReaderNavBarDesktopReadingDirection = ({
const { t } = useLingui(); const { t } = useLingui();
return ( return (
<ValueRotationButton <SelectButton
{...buttonSelectInputProps} {...buttonSelectInputProps}
tooltip={t`Reading direction`} tooltip={t`Reading direction`}
value={readingDirection.isDefault ? undefined : readingDirection.value} value={readingDirection.isDefault ? undefined : readingDirection.value}
@@ -36,6 +36,7 @@ export const ReaderNavBarDesktopReadingDirection = ({
setValue={setReadingDirection} setValue={setReadingDirection}
valueToDisplayData={READING_DIRECTION_VALUE_TO_DISPLAY_DATA} valueToDisplayData={READING_DIRECTION_VALUE_TO_DISPLAY_DATA}
defaultIcon={READING_DIRECTION_VALUE_TO_DISPLAY_DATA[readingDirection.value].icon} defaultIcon={READING_DIRECTION_VALUE_TO_DISPLAY_DATA[readingDirection.value].icon}
isCollapsible
/> />
); );
}; };

View File

@@ -7,7 +7,6 @@
*/ */
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { ValueRotationButton } from '@/base/components/buttons/ValueRotationButton.tsx';
import type { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/Reader.types.ts'; import type { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/Reader.types.ts';
import { import {
READING_MODE_VALUE_TO_DISPLAY_DATA, READING_MODE_VALUE_TO_DISPLAY_DATA,
@@ -15,6 +14,7 @@ import {
} from '@/features/reader/settings/ReaderSettings.constants.tsx'; } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import type { SelectButtonDefaultableProps } from '@/base/components/buttons/SelectButton.tsx'; import type { SelectButtonDefaultableProps } from '@/base/components/buttons/SelectButton.tsx';
import { SelectButton } from '@/base/components/buttons/SelectButton.tsx';
export const ReaderNavBarDesktopReadingMode = ({ export const ReaderNavBarDesktopReadingMode = ({
readingMode, readingMode,
@@ -27,7 +27,7 @@ export const ReaderNavBarDesktopReadingMode = ({
const { t } = useLingui(); const { t } = useLingui();
return ( return (
<ValueRotationButton <SelectButton
{...buttonSelectInputProps} {...buttonSelectInputProps}
tooltip={t`Reading mode`} tooltip={t`Reading mode`}
value={readingMode.isDefault ? undefined : readingMode.value} value={readingMode.isDefault ? undefined : readingMode.value}
@@ -36,6 +36,7 @@ export const ReaderNavBarDesktopReadingMode = ({
setValue={setReadingMode} setValue={setReadingMode}
valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA} valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA}
defaultIcon={READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].icon} defaultIcon={READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].icon}
isCollapsible
/> />
); );
}; };

View File

@@ -358,6 +358,10 @@ msgstr "Active device"
msgid "Active setting" msgid "Active setting"
msgstr "Active setting" msgstr "Active setting"
#: src/base/components/buttons/SelectButton.tsx
msgid "Active setting ({title})"
msgstr "Active setting ({title})"
#: src/base/components/settings/MutableListSetting.tsx #: src/base/components/settings/MutableListSetting.tsx
#: src/features/manga/hooks/useManageMangaLibraryState.tsx #: src/features/manga/hooks/useManageMangaLibraryState.tsx
#: src/features/reader/hotkeys/settings/components/ReaderSettingHotkey.tsx #: src/features/reader/hotkeys/settings/components/ReaderSettingHotkey.tsx