Simplify changing reader desktop quick settings
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
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>
|
</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
|
||||||
<Button
|
key={displayValue}
|
||||||
onClick={() => setValue(newValue)}
|
title={slotProps?.button?.tooltip?.(isDefault, text) ?? (isDefault ? t`Active setting` : '')}
|
||||||
variant={isSelected ? 'contained' : 'outlined'}
|
|
||||||
startIcon={valueToDisplayData[displayValue].icon}
|
|
||||||
>
|
>
|
||||||
{isDefault ? <Superscript superscript="*" text={text} /> : text}
|
<Button
|
||||||
|
variant={isSelected ? 'contained' : 'outlined'}
|
||||||
|
startIcon={!slotProps?.button?.hideText && valueToDisplayData[displayValue].icon}
|
||||||
|
{...slotProps?.button}
|
||||||
|
onClick={() => setValue(newValue)}
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
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} />;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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`}>
|
||||||
|
|||||||
@@ -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
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user