Reader overlay mobile bottom bar
This commit is contained in:
@@ -754,6 +754,7 @@
|
||||
},
|
||||
"reader": {
|
||||
"button": {
|
||||
"chapter_list": "Chapter list",
|
||||
"close_menu": "Close menu",
|
||||
"exit": "Exit reader",
|
||||
"next_chapter": "Next Chapter",
|
||||
@@ -786,6 +787,8 @@
|
||||
"offset_first_page": "Offset first page",
|
||||
"reader_type": "Reader type",
|
||||
"reader_width": "Reader width",
|
||||
"reading_direction": "Reading direction",
|
||||
"reading_mode": "Reading mode",
|
||||
"scale_page": "Scale small pages",
|
||||
"show_page_number": "Show page number",
|
||||
"skip_dup_chapters": "Skip duplicate chapters",
|
||||
@@ -793,16 +796,17 @@
|
||||
},
|
||||
"reader_type": {
|
||||
"label": {
|
||||
"continuous_horizontal_ltr": "Horizontal (LTR) ➡️",
|
||||
"continuous_horizontal_rtl": "Horizontal (RTL) ⬅️",
|
||||
"continuous_vertical": "Continues vertical",
|
||||
"double_page_ltr": "Double page (LTR) ➡️",
|
||||
"double_page_rtl": "Double page (RTL) ⬅️",
|
||||
"single_page_ltr": "Single page (LTR) ➡️",
|
||||
"single_page_rtl": "Single page (RTL) ⬅️",
|
||||
"continuous_horizontal": "Continuous horizontal",
|
||||
"continuous_vertical": "Continuous vertical",
|
||||
"double_page": "Double page",
|
||||
"single_page": "Single page",
|
||||
"webtoon": "Webtoon"
|
||||
}
|
||||
},
|
||||
"reading_direction": {
|
||||
"ltr": "Left to right",
|
||||
"rtl": "Right to left"
|
||||
},
|
||||
"title": {
|
||||
"default_reader_settings": "Default Reader Settings",
|
||||
"reader_settings": "Reader Settings"
|
||||
|
||||
24
src/assets/icons/svg/ContinuousHorizontalPageIcon.tsx
Normal file
24
src/assets/icons/svg/ContinuousHorizontalPageIcon.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 SvgIcon from '@mui/material/SvgIcon';
|
||||
|
||||
export const ContinuousHorizontalPageIcon = () => (
|
||||
<SvgIcon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M22 20h-6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h6M2 4h6a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2"
|
||||
/>
|
||||
</svg>
|
||||
</SvgIcon>
|
||||
);
|
||||
24
src/assets/icons/svg/ContinuousVerticalPageIcon.tsx
Normal file
24
src/assets/icons/svg/ContinuousVerticalPageIcon.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 SvgIcon from '@mui/material/SvgIcon';
|
||||
|
||||
export const ContinuousVerticalPageIcon = () => (
|
||||
<SvgIcon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M20 2v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2m0 20v-6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6"
|
||||
/>
|
||||
</svg>
|
||||
</SvgIcon>
|
||||
);
|
||||
24
src/assets/icons/svg/DoublePageIcon.tsx
Normal file
24
src/assets/icons/svg/DoublePageIcon.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 SvgIcon from '@mui/material/SvgIcon';
|
||||
|
||||
export const DoublePageIcon = () => (
|
||||
<SvgIcon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zm20 0h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"
|
||||
/>
|
||||
</svg>
|
||||
</SvgIcon>
|
||||
);
|
||||
17
src/assets/icons/svg/OffsetDoubleSpreadIcon.tsx
Normal file
17
src/assets/icons/svg/OffsetDoubleSpreadIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 SvgIcon from '@mui/material/SvgIcon';
|
||||
|
||||
export const OffsetDoubleSpreadIcon = () => (
|
||||
<SvgIcon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path d="M2 4h6a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2m20 0h-6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h6M2 12h20m-3-3 3 3-3 3M5 9l-3 3 3 3" />
|
||||
</svg>
|
||||
</SvgIcon>
|
||||
);
|
||||
32
src/assets/icons/svg/SinglePageIcon.tsx
Normal file
32
src/assets/icons/svg/SinglePageIcon.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 SvgIcon from '@mui/material/SvgIcon';
|
||||
|
||||
export const SinglePageIcon = () => (
|
||||
<SvgIcon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 2v7h7"
|
||||
/>
|
||||
</svg>
|
||||
</SvgIcon>
|
||||
);
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
||||
|
||||
type BaseProps = { onClose: () => void };
|
||||
type BaseProps = { onClose: () => void; selectable?: boolean };
|
||||
|
||||
type TChapter = ChapterIdInfo &
|
||||
ChapterMangaInfo &
|
||||
@@ -71,6 +71,7 @@ export const ChapterActionMenuItems = ({
|
||||
canBeDownloaded = false,
|
||||
selectedChapters = [],
|
||||
onClose,
|
||||
selectable = true,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -158,7 +159,7 @@ export const ChapterActionMenuItems = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSingleMode && (
|
||||
{isSingleMode && selectable && (
|
||||
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t('chapter.action.label.select')} />
|
||||
)}
|
||||
{isSingleMode && (
|
||||
|
||||
@@ -50,12 +50,15 @@ type TChapter = ChapterIdInfo &
|
||||
Pick<ChapterType, 'name' | 'sourceOrder' | 'uploadDate'>;
|
||||
|
||||
interface IProps {
|
||||
mode?: 'manga.page' | 'reader';
|
||||
chapter: TChapter;
|
||||
allChapters: TChapter[];
|
||||
downloadChapter: ChapterDownloadStatus | undefined;
|
||||
showChapterNumber: boolean;
|
||||
onSelect: (selected: boolean, isShiftKey?: boolean) => void;
|
||||
selected: boolean | null;
|
||||
selectable?: boolean;
|
||||
isActiveChapter?: boolean; // reader
|
||||
}
|
||||
|
||||
export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
@@ -64,7 +67,17 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const { chapter, allChapters, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
|
||||
const {
|
||||
mode = 'manga.page',
|
||||
chapter,
|
||||
allChapters,
|
||||
downloadChapter: dc,
|
||||
showChapterNumber,
|
||||
onSelect,
|
||||
selected,
|
||||
selectable = true,
|
||||
isActiveChapter = false,
|
||||
} = props;
|
||||
const isSelecting = selected !== null;
|
||||
|
||||
const { isDownloaded } = chapter;
|
||||
@@ -98,102 +111,115 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<li>
|
||||
<PopupState variant="popover" popupId="chapter-card-action-menu">
|
||||
{(popupState) => (
|
||||
<Stack sx={{ pt: 1, px: 1 }}>
|
||||
<Card sx={{ touchCallout: 'none' }}>
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.sourceOrder}`}
|
||||
style={{
|
||||
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
|
||||
<PopupState variant="popover" popupId="chapter-card-action-menu">
|
||||
{(popupState) => (
|
||||
<Stack sx={{ pt: 1, px: 1 }}>
|
||||
<Card
|
||||
sx={{
|
||||
touchCallout: 'none',
|
||||
backgroundColor:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
mode === 'reader'
|
||||
? isActiveChapter
|
||||
? 'primary.main'
|
||||
: 'background.default'
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.sourceOrder}`}
|
||||
style={{
|
||||
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
|
||||
}}
|
||||
replace={mode === 'reader'}
|
||||
onClick={(e) => handleClick(e)}
|
||||
{...longPressBind(popupState.open)}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 1.5,
|
||||
'&:last-child': { pb: 1.5 },
|
||||
}}
|
||||
onClick={(e) => handleClick(e)}
|
||||
{...longPressBind(popupState.open)}
|
||||
>
|
||||
<CardContent
|
||||
<Stack
|
||||
direction="column"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 1.5,
|
||||
'&:last-child': { pb: 1.5 },
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="column"
|
||||
sx={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
gap: 0.5,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
flexDirection: 'row',
|
||||
gap: 0.5,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{chapter.isBookmarked && <BookmarkIcon color="primary" />}
|
||||
<TypographyMaxLines variant="h6" component="h4">
|
||||
{showChapterNumber
|
||||
? `${t('chapter.title_one')} ${chapter.chapterNumber}`
|
||||
: chapter.name}
|
||||
</TypographyMaxLines>
|
||||
</Stack>
|
||||
<Typography variant="caption">{chapter.scanlator}</Typography>
|
||||
<Typography variant="caption">
|
||||
{getDateString(Number(chapter.uploadDate ?? 0), true)}
|
||||
{isDownloaded && ` • ${t('chapter.status.label.downloaded')}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{dc && <DownloadStateIndicator download={dc} />}
|
||||
|
||||
<Stack sx={{ minHeight: '48px' }}>
|
||||
{selected === null ? (
|
||||
<Tooltip title={t('global.button.options')}>
|
||||
<IconButton
|
||||
ref={menuButtonRef}
|
||||
{...bindTrigger(popupState)}
|
||||
onClick={(e) => handleClickOpenMenu(e, popupState.open)}
|
||||
onTouchStart={(e) => handleClickOpenMenu(e, popupState.open)}
|
||||
aria-label="more"
|
||||
size="large"
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={t(selected ? 'global.button.deselect' : 'global.button.select')}
|
||||
>
|
||||
<Checkbox checked={selected} />
|
||||
</Tooltip>
|
||||
{chapter.isBookmarked && (
|
||||
<BookmarkIcon color={isActiveChapter ? 'secondary' : 'primary'} />
|
||||
)}
|
||||
<TypographyMaxLines variant="h6" component="h4">
|
||||
{showChapterNumber
|
||||
? `${t('chapter.title_one')} ${chapter.chapterNumber}`
|
||||
: chapter.name}
|
||||
</TypographyMaxLines>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
{!isSelecting && popupState.isOpen && (
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
{(onClose) => (
|
||||
<ChapterActionMenuItems
|
||||
onClose={onClose}
|
||||
chapter={chapter}
|
||||
allChapters={allChapters}
|
||||
handleSelection={() => onSelect(true)}
|
||||
canBeDownloaded={ChaptersWithMeta.isDownloadable({
|
||||
chapter,
|
||||
downloadChapter: dc,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</PopupState>
|
||||
</li>
|
||||
<Typography variant="caption">{chapter.scanlator}</Typography>
|
||||
<Typography variant="caption">
|
||||
{getDateString(Number(chapter.uploadDate ?? 0), true)}
|
||||
{isDownloaded && ` • ${t('chapter.status.label.downloaded')}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{dc && <DownloadStateIndicator download={dc} />}
|
||||
|
||||
<Stack sx={{ minHeight: '48px' }}>
|
||||
{selected === null ? (
|
||||
<Tooltip title={t('global.button.options')}>
|
||||
<IconButton
|
||||
ref={menuButtonRef}
|
||||
{...bindTrigger(popupState)}
|
||||
onClick={(e) => handleClickOpenMenu(e, popupState.open)}
|
||||
onTouchStart={(e) => handleClickOpenMenu(e, popupState.open)}
|
||||
aria-label="more"
|
||||
size="large"
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={t(selected ? 'global.button.deselect' : 'global.button.select')}
|
||||
>
|
||||
<Checkbox checked={selected} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
{!isSelecting && popupState.isOpen && (
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
{(onClose) => (
|
||||
<ChapterActionMenuItems
|
||||
onClose={onClose}
|
||||
chapter={chapter}
|
||||
allChapters={allChapters}
|
||||
handleSelection={() => onSelect(true)}
|
||||
canBeDownloaded={ChaptersWithMeta.isDownloadable({
|
||||
chapter,
|
||||
downloadChapter: dc,
|
||||
})}
|
||||
selectable={selectable}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</PopupState>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ export class Chapters {
|
||||
}
|
||||
|
||||
static getReaderUrl<Chapter extends ChapterMangaInfo & ChapterSourceOrderInfo>(chapter: Chapter): string {
|
||||
return `manga/${chapter.mangaId}/chapter/${chapter.sourceOrder}`;
|
||||
return `/manga/${chapter.mangaId}/chapter/${chapter.sourceOrder}`;
|
||||
}
|
||||
|
||||
static isDownloading(id: number): boolean {
|
||||
|
||||
@@ -6,8 +6,19 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
export enum GridLayout {
|
||||
Compact = 0,
|
||||
Comfortable = 1,
|
||||
List = 2,
|
||||
}
|
||||
|
||||
export type ValueToDisplayData<Value extends string | number> = Record<
|
||||
Value,
|
||||
{
|
||||
title: TranslationKey;
|
||||
icon: ReactNode;
|
||||
}
|
||||
>;
|
||||
|
||||
41
src/modules/core/components/buttons/ButtonSelect.tsx
Normal file
41
src/modules/core/components/buttons/ButtonSelect.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export const ButtonSelect = <Value extends string | number>({
|
||||
value,
|
||||
values,
|
||||
setValue,
|
||||
valueToDisplayData,
|
||||
}: {
|
||||
value: Value;
|
||||
values: Value[];
|
||||
setValue: (value: Value) => void;
|
||||
valueToDisplayData: ValueToDisplayData<Value>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
|
||||
{values.map((displayValue) => (
|
||||
<Button
|
||||
key={displayValue}
|
||||
onClick={() => setValue(displayValue)}
|
||||
variant={displayValue === value ? 'contained' : 'outlined'}
|
||||
startIcon={valueToDisplayData[displayValue].icon}
|
||||
>
|
||||
{t(valueToDisplayData[displayValue].title)}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
23
src/modules/core/components/inputs/ButtonSelectInput.tsx
Normal file
23
src/modules/core/components/inputs/ButtonSelectInput.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { ButtonSelect } from '@/modules/core/components/buttons/ButtonSelect.tsx';
|
||||
|
||||
export const ButtonSelectInput = <Value extends string | number>({
|
||||
label,
|
||||
...buttonSelectProps
|
||||
}: ComponentProps<typeof ButtonSelect<Value>> & { label: string }) => (
|
||||
<Stack>
|
||||
<Typography>{label}</Typography>
|
||||
<ButtonSelect {...buttonSelectProps} />
|
||||
</Stack>
|
||||
);
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { AppMetadataKeys, IMetadataMigration } from '@/modules/metadata/Metadata.types.ts';
|
||||
import { ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
|
||||
export const APP_METADATA_KEY_PREFIX = 'webUI_';
|
||||
|
||||
@@ -60,6 +61,7 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
||||
progressBarType: undefined,
|
||||
progressBarSize: undefined,
|
||||
progressBarPosition: undefined,
|
||||
readingMode: undefined,
|
||||
};
|
||||
|
||||
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
|
||||
@@ -121,6 +123,7 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
|
||||
'customThemes',
|
||||
'mangaThumbnailBackdrop',
|
||||
];
|
||||
|
||||
/**
|
||||
* Once all changes have been done in the current branch, a new migration for all changes should be
|
||||
* created.
|
||||
@@ -184,4 +187,56 @@ export const METADATA_MIGRATIONS: IMetadataMigration[] = [
|
||||
{
|
||||
keys: [{ oldKey: 'deleteChaptersAutoMarkedRead', newKey: 'deleteChaptersWhileReading' }],
|
||||
},
|
||||
{
|
||||
keys: [
|
||||
{
|
||||
oldKey: 'readerType',
|
||||
newKey: 'readingMode',
|
||||
},
|
||||
],
|
||||
values: [
|
||||
// START: readerType
|
||||
{
|
||||
key: 'readerType',
|
||||
oldValue: 'ContinuesVertical',
|
||||
newValue: `${ReadingMode.CONTINUOUS_VERTICAL}`,
|
||||
},
|
||||
{
|
||||
key: 'readerType',
|
||||
oldValue: 'Webtoon',
|
||||
newValue: `${ReadingMode.CONTINUOUS_VERTICAL}`,
|
||||
},
|
||||
{
|
||||
key: 'readerType',
|
||||
oldValue: 'SingleRTL',
|
||||
newValue: `${ReadingMode.SINGLE_PAGE}`,
|
||||
},
|
||||
{
|
||||
key: 'readerType',
|
||||
oldValue: 'SingleLTR',
|
||||
newValue: `${ReadingMode.SINGLE_PAGE}`,
|
||||
},
|
||||
{
|
||||
key: 'readerType',
|
||||
oldValue: 'DoubleRTL',
|
||||
newValue: `${ReadingMode.DOUBLE_PAGE}`,
|
||||
},
|
||||
{
|
||||
key: 'readerType',
|
||||
oldValue: 'DoubleLTR',
|
||||
newValue: `${ReadingMode.DOUBLE_PAGE}`,
|
||||
},
|
||||
{
|
||||
key: 'readerType',
|
||||
oldValue: 'ContinuesHorizontalLTR',
|
||||
newValue: `${ReadingMode.CONTINUOUS_HORIZONTAL}`,
|
||||
},
|
||||
{
|
||||
key: 'readerType',
|
||||
oldValue: 'ContinuesHorizontalRTL',
|
||||
newValue: `${ReadingMode.CONTINUOUS_HORIZONTAL}`,
|
||||
},
|
||||
// END: readerType
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 { Virtuoso } from 'react-virtuoso';
|
||||
import { useMemo } from 'react';
|
||||
import { IChapterWithMeta } from '@/modules/chapter/components/ChapterList.tsx';
|
||||
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||
|
||||
export const ReaderChapterList = ({
|
||||
currentChapter,
|
||||
chapters,
|
||||
}: Required<Pick<ReaderStateChapters, 'chapters' | 'currentChapter'>>) => {
|
||||
const { data: downloaderData } = requestManager.useGetDownloadStatus();
|
||||
const queue = downloaderData?.downloadStatus.queue ?? [];
|
||||
|
||||
const currentChapterIndex = useMemo(
|
||||
() => currentChapter && chapters.findIndex((chapter) => chapter.id === currentChapter.id),
|
||||
[currentChapter, chapters],
|
||||
);
|
||||
|
||||
const chaptersWithMeta: IChapterWithMeta[] = useMemo(
|
||||
() =>
|
||||
chapters.map((chapter) => {
|
||||
const downloadChapter = queue?.find((cd) => cd.chapter.id === chapter.id);
|
||||
|
||||
return {
|
||||
chapter,
|
||||
downloadChapter,
|
||||
selected: null,
|
||||
};
|
||||
}),
|
||||
[queue, chapters],
|
||||
);
|
||||
|
||||
return (
|
||||
<Virtuoso
|
||||
style={{
|
||||
height: `calc(${chaptersWithMeta.length} * 100px)`,
|
||||
minHeight: '15vh',
|
||||
maxHeight: '75vh',
|
||||
}}
|
||||
initialTopMostItemIndex={currentChapterIndex ?? 0}
|
||||
totalCount={chaptersWithMeta.length}
|
||||
computeItemKey={(index) => chaptersWithMeta[index].chapter.id}
|
||||
itemContent={(index) => (
|
||||
<ChapterCard
|
||||
key={chaptersWithMeta[index].chapter.id}
|
||||
mode="reader"
|
||||
chapter={chaptersWithMeta[index].chapter}
|
||||
downloadChapter={chaptersWithMeta[index].downloadChapter}
|
||||
allChapters={chapters}
|
||||
showChapterNumber={false}
|
||||
selected={null}
|
||||
onSelect={() => undefined}
|
||||
selectable={false}
|
||||
isActiveChapter={index === currentChapterIndex}
|
||||
/>
|
||||
)}
|
||||
increaseViewportBy={400}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 IconButton from '@mui/material/IconButton';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import AppSettingsAltIcon from '@mui/icons-material/AppSettingsAlt';
|
||||
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import { bindDialog, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { ReaderBottomBarMobileProps } from '@/modules/reader/types/ReaderOverlay.types.ts';
|
||||
import { MobileReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx';
|
||||
import { ReaderChapterList } from '@/modules/reader/components/overlay/navigation/ReaderChapterList.tsx';
|
||||
import { ReaderBottomBarMobileQuickSettings } from '@/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileQuickSettings.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
|
||||
export const ReaderBottomBarMobile = ({ openSettings, isVisible }: ReaderBottomBarMobileProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentChapter, chapters } = useReaderStateChaptersContext();
|
||||
const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext();
|
||||
|
||||
const chapterListPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-chapter-list-dialog' });
|
||||
const quickSettingsPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-quick-settings-dialog' });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
chapterListPopupState.close();
|
||||
}, [currentChapter?.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Slide direction="up" in={isVisible} mountOnEnter unmountOnExit>
|
||||
<Stack
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
right: `${scrollbarYSize}px`,
|
||||
bottom: `${scrollbarXSize}px`,
|
||||
left: 0,
|
||||
gap: 2,
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
>
|
||||
<MobileReaderProgressBar />
|
||||
<Stack
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.95),
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
width: '50%',
|
||||
flexDirection: 'row',
|
||||
p: 2,
|
||||
gap: 1,
|
||||
justifyContent: 'space-evenly',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Tooltip title={t('reader.button.chapter_list')}>
|
||||
<IconButton {...bindTrigger(chapterListPopupState)} size="large" color="inherit">
|
||||
<FormatListBulletedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('reader.settings.label.reader_type')}>
|
||||
<IconButton {...bindTrigger(quickSettingsPopupState)} size="large" color="inherit">
|
||||
<AppSettingsAltIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('settings.title')}>
|
||||
<IconButton onClick={openSettings} size="large" color="inherit">
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Slide>
|
||||
{chapterListPopupState.isOpen && (
|
||||
<Dialog {...bindDialog(chapterListPopupState)} fullWidth maxWidth="md" scroll="paper">
|
||||
<DialogContent sx={{ p: 0, pb: 1 }}>
|
||||
<ReaderChapterList currentChapter={currentChapter} chapters={chapters} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{quickSettingsPopupState.isOpen && (
|
||||
<Dialog {...bindDialog(quickSettingsPopupState)} fullWidth maxWidth="md" scroll="paper">
|
||||
<DialogContent>
|
||||
<ReaderBottomBarMobileQuickSettings />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReaderBottomBarMobileReadingMode } from '@/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingMode.tsx';
|
||||
import { ReaderBottomBarMobileReadingDirection } from '@/modules/reader/components/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileReadingDirection.tsx';
|
||||
import { createUpdateReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
|
||||
export const ReaderBottomBarMobileQuickSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { manga } = useReaderStateMangaContext();
|
||||
const { readingMode, readingDirection } = ReaderService.useSettings();
|
||||
|
||||
const updateReaderSettings = createUpdateReaderSettings<
|
||||
keyof Pick<IReaderSettings, 'readingMode' | 'readingDirection'>
|
||||
>(manga ?? { id: -1 }, () => makeToast(t('reader.settings.error.label.failed_to_save_settings')));
|
||||
|
||||
if (!manga) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<ReaderBottomBarMobileReadingMode
|
||||
readingMode={readingMode}
|
||||
setReadingMode={(value) => updateReaderSettings('readingMode', value)}
|
||||
/>
|
||||
<ReaderBottomBarMobileReadingDirection
|
||||
readingDirection={readingDirection}
|
||||
setReadingDirection={(value) => updateReaderSettings('readingDirection', value)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft';
|
||||
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = {
|
||||
[ReadingDirection.LTR]: {
|
||||
title: 'reader.settings.reading_direction.ltr',
|
||||
icon: <ArrowCircleRightIcon />,
|
||||
},
|
||||
[ReadingDirection.RTL]: {
|
||||
title: 'reader.settings.reading_direction.rtl',
|
||||
icon: <ArrowCircleLeftIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderBottomBarMobileReadingDirection = ({
|
||||
readingDirection,
|
||||
setReadingDirection,
|
||||
}: Pick<IReaderSettings, 'readingDirection'> & {
|
||||
setReadingDirection: (readingDirection: ReadingDirection) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.label.reading_direction')}
|
||||
value={readingDirection}
|
||||
values={READING_DIRECTION_VALUES}
|
||||
setValue={setReadingDirection}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { SinglePageIcon } from '@/assets/icons/svg/SinglePageIcon.tsx';
|
||||
import { DoublePageIcon } from '@/assets/icons/svg/DoublePageIcon.tsx';
|
||||
import { ContinuousVerticalPageIcon } from '@/assets/icons/svg/ContinuousVerticalPageIcon.tsx';
|
||||
import { ContinuousHorizontalPageIcon } from '@/assets/icons/svg/ContinuousHorizontalPageIcon.tsx';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/modules/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingMode> = {
|
||||
[ReadingMode.SINGLE_PAGE]: {
|
||||
title: 'reader.settings.reader_type.label.single_page',
|
||||
icon: <SinglePageIcon />,
|
||||
},
|
||||
[ReadingMode.DOUBLE_PAGE]: {
|
||||
title: 'reader.settings.reader_type.label.double_page',
|
||||
icon: <DoublePageIcon />,
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_VERTICAL]: {
|
||||
title: 'reader.settings.reader_type.label.continuous_vertical',
|
||||
icon: <ContinuousVerticalPageIcon />,
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
|
||||
title: 'reader.settings.reader_type.label.continuous_horizontal',
|
||||
icon: <ContinuousHorizontalPageIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderBottomBarMobileReadingMode = ({
|
||||
readingMode,
|
||||
setReadingMode,
|
||||
}: Pick<IReaderSettings, 'readingMode'> & {
|
||||
setReadingMode: (mode: ReadingMode) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.label.reading_mode')}
|
||||
value={readingMode}
|
||||
values={READING_MODE_VALUES}
|
||||
setValue={setReadingMode}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ import { ReaderProgressBarSlot } from '@/modules/reader/components/overlay/progr
|
||||
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils';
|
||||
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { getOptionForDirection } from '@/theme.tsx';
|
||||
import { ProgressBarPosition } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ReaderProgressBarDirectionWrapper } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx';
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ProgressBarPosition,
|
||||
ProgressBarType,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||
|
||||
@@ -29,4 +30,5 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
||||
progressBarSize: 4,
|
||||
progressBarPosition: ProgressBarPosition.BOTTOM,
|
||||
readingDirection: ReadingDirection.LTR,
|
||||
readingMode: ReadingMode.SINGLE_PAGE,
|
||||
};
|
||||
|
||||
@@ -25,6 +25,13 @@ export enum ReadingDirection {
|
||||
RTL,
|
||||
}
|
||||
|
||||
export enum ReadingMode {
|
||||
SINGLE_PAGE,
|
||||
DOUBLE_PAGE,
|
||||
CONTINUOUS_VERTICAL,
|
||||
CONTINUOUS_HORIZONTAL,
|
||||
}
|
||||
|
||||
export interface ReaderStateChapters {
|
||||
chapters: TChapterReader[];
|
||||
currentChapter?: TChapterReader | null;
|
||||
@@ -48,4 +55,5 @@ export interface IReaderSettings {
|
||||
progressBarSize: number;
|
||||
progressBarPosition: ProgressBarPosition;
|
||||
readingDirection: ReadingDirection;
|
||||
readingMode: ReadingMode;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
export interface MobileHeaderProps {
|
||||
export interface BaseReaderOverlayProps {
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
export interface MobileHeaderProps extends BaseReaderOverlayProps {}
|
||||
|
||||
export interface ReaderBottomBarMobileProps extends BaseReaderOverlayProps {
|
||||
openSettings: () => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user