Move core files into new folder
This commit is contained in:
@@ -14,12 +14,12 @@ import Avatar from '@mui/material/Avatar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TranslationKey } from '@/typings';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { TExtension } from '@/screens/util/Extensions.ts';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
interface IProps {
|
||||
extension: TExtension;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useMemo, useState } from 'react';
|
||||
import { useLongPress } from 'use-long-press';
|
||||
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||
import { MangaActionMenuItems, SingleModeProps } from '@/components/manga/MangaActionMenuItems.tsx';
|
||||
import { Menu } from '@/components/menu/Menu.tsx';
|
||||
import { Menu } from '@/modules/core/components/menu/Menu.tsx';
|
||||
import { MigrateDialog } from '@/components/MigrateDialog.tsx';
|
||||
import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx';
|
||||
import { MangaGridCard } from '@/components/manga/MangaGridCard.tsx';
|
||||
|
||||
@@ -21,17 +21,17 @@ import Box, { BoxProps } from '@mui/material/Box';
|
||||
import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { MangaCard } from '@/components/MangaCard';
|
||||
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
||||
import { useLocalStorage, useSessionStorage } from '@/util/useStorage.tsx';
|
||||
import { useLocalStorage, useSessionStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab.tsx';
|
||||
import { AppStorage } from '@/util/AppStorage.ts';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
|
||||
import { AppStorage } from '@/lib/AppStorage.ts';
|
||||
import { MangaCardProps } from '@/components/manga/MangaCard.types.tsx';
|
||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { useResizeObserver } from '@/util/useResizeObserver.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
import { useNavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
|
||||
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => (
|
||||
|
||||
@@ -16,15 +16,15 @@ import Stack from '@mui/material/Stack';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
import { Mangas, MigrateMode } from '@/lib/data/Mangas.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import {
|
||||
createUpdateMetadataServerSettings,
|
||||
useMetadataServerSettings,
|
||||
} from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { MetadataMigrationSettings } from '@/typings.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
|
||||
export const MigrateDialog = ({ mangaIdToMigrateTo, onClose }: { mangaIdToMigrateTo: number; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -15,10 +15,10 @@ import Avatar from '@mui/material/Avatar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { translateExtensionLanguage } from '@/screens/util/Extensions.ts';
|
||||
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
|
||||
export type TMigratableSource = NonNullable<GetMigratableSourcesQuery['mangas']['nodes'][number]['source']> & {
|
||||
mangaCount: number;
|
||||
|
||||
@@ -17,10 +17,10 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { translateExtensionLanguage } from '@/screens/util/Extensions';
|
||||
import { SourceContentType } from '@/screens/SourceMangas';
|
||||
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { GetSourcesListQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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 Checkbox, { CheckboxProps } from '@mui/material/Checkbox';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps extends CheckboxProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const CheckboxInput: React.FC<IProps> = ({ label, sx, ...rest }) => (
|
||||
<FormControlLabel control={<Checkbox {...rest} />} label={label} sx={sx} />
|
||||
);
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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 Button, { ButtonProps } from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { ForwardedRef, forwardRef } from 'react';
|
||||
|
||||
export const CustomIconButton = forwardRef(
|
||||
<C extends React.ElementType>(
|
||||
{ children, ...props }: ButtonProps<C, { component?: C }>,
|
||||
ref: ForwardedRef<HTMLButtonElement | null>,
|
||||
) => (
|
||||
<Button ref={ref} {...props}>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</Button>
|
||||
),
|
||||
);
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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, { StackProps } from '@mui/material/Stack';
|
||||
import Typography, { TypographyProps } from '@mui/material/Typography';
|
||||
|
||||
export const Metadata = ({
|
||||
title,
|
||||
value,
|
||||
stackProps,
|
||||
titleProps,
|
||||
valueProps,
|
||||
}: {
|
||||
title: string;
|
||||
value: string;
|
||||
stackProps?: StackProps;
|
||||
titleProps?: TypographyProps;
|
||||
valueProps?: TypographyProps;
|
||||
}) => (
|
||||
<Stack
|
||||
{...stackProps}
|
||||
sx={{ flexDirection: 'row', columnGap: 1, flexWrap: 'wrap', alignItems: 'baseline', ...stackProps?.sx }}
|
||||
>
|
||||
<Typography
|
||||
{...titleProps}
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
...titleProps?.sx,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography {...valueProps}>{value}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* 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 InputAdornment from '@mui/material/InputAdornment';
|
||||
import TextField, { TextFieldProps } from '@mui/material/TextField';
|
||||
import { useState } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const PasswordTextField = (props: TextFieldProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
id="password"
|
||||
name="password"
|
||||
label={t('global.label.password')}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={handleClickShowPassword}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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 FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Radio, { RadioProps } from '@mui/material/Radio';
|
||||
import React from 'react';
|
||||
|
||||
export interface RadioInputProps extends RadioProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const RadioInput: React.FC<RadioInputProps> = ({ label, sx, ...rest }) => (
|
||||
<FormControlLabel control={<Radio {...rest} />} label={label} sx={sx} />
|
||||
);
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* 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 TextField, { TextFieldProps } from '@mui/material/TextField';
|
||||
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
|
||||
export const SearchTextField = ({
|
||||
onCancel,
|
||||
cancelButtonProps,
|
||||
...textFieldProps
|
||||
}: TextFieldProps & { onCancel: () => void; cancelButtonProps?: IconButtonProps }) => (
|
||||
<TextField
|
||||
{...textFieldProps}
|
||||
slotProps={{
|
||||
input: {
|
||||
...textFieldProps.InputProps,
|
||||
sx: {
|
||||
color: 'inherit',
|
||||
},
|
||||
endAdornment: textFieldProps.InputProps?.endAdornment ?? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton {...cancelButtonProps} onClick={() => onCancel()}>
|
||||
<CancelIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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 MuiSelect from '@mui/material/Select';
|
||||
|
||||
export const Select = <Value,>({
|
||||
children,
|
||||
maxSelectionHeightPx = 250,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MuiSelect<Value>> & { maxSelectionHeightPx?: number }) => (
|
||||
<MuiSelect<Value> MenuProps={{ PaperProps: { style: { maxHeight: maxSelectionHeightPx } } }} {...props}>
|
||||
{children}
|
||||
</MuiSelect>
|
||||
);
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* 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 ArrowDownward from '@mui/icons-material/ArrowDownward';
|
||||
import ArrowUpward from '@mui/icons-material/ArrowUpward';
|
||||
import { memo } from 'react';
|
||||
import { RadioInput, RadioInputProps } from '@/components/atoms/RadioInput';
|
||||
|
||||
interface IProps extends RadioInputProps {
|
||||
sortDescending?: boolean | null | undefined;
|
||||
}
|
||||
|
||||
export const SortRadioInput = memo(({ sortDescending, ...rest }: IProps) => (
|
||||
<RadioInput
|
||||
checkedIcon={sortDescending ? <ArrowDownward color="primary" /> : <ArrowUpward color="primary" />}
|
||||
{...rest}
|
||||
/>
|
||||
));
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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 DisabledByDefaultRounded from '@mui/icons-material/DisabledByDefaultRounded';
|
||||
import Checkbox, { CheckboxProps } from '@mui/material/Checkbox';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
type CheckState = boolean | undefined | null;
|
||||
|
||||
function nextState(state: CheckState): CheckState {
|
||||
if (state === true) return false;
|
||||
if (state === false) return undefined;
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface ThreeStateCheckboxProps extends Omit<CheckboxProps, 'checked' | 'onChange'> {
|
||||
checked?: boolean | undefined | null;
|
||||
onChange?: (checked: boolean | undefined | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* When checked is true, checkbox contains checkmark
|
||||
* When checked is false, checkbox contains cross
|
||||
* When checked is null or undefined, checkbox is empty
|
||||
*/
|
||||
export const ThreeStateCheckbox: React.FC<ThreeStateCheckboxProps> = ({ checked, onChange, ...rest }) => {
|
||||
const handleChange = useCallback(() => {
|
||||
if (onChange) {
|
||||
const newState = nextState(checked);
|
||||
onChange(newState);
|
||||
}
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
indeterminateIcon={<DisabledByDefaultRounded />}
|
||||
checked={checked === true}
|
||||
indeterminate={checked === false}
|
||||
onChange={handleChange}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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 FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import React from 'react';
|
||||
import { ThreeStateCheckbox, ThreeStateCheckboxProps } from '@/components/atoms/ThreeStateCheckbox';
|
||||
|
||||
interface IProps extends ThreeStateCheckboxProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const ThreeStateCheckboxInput: React.FC<IProps> = ({ label, sx, ...rest }) => (
|
||||
<FormControlLabel control={<ThreeStateCheckbox {...rest} />} label={label} sx={sx} />
|
||||
);
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* 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 styled from '@emotion/styled';
|
||||
import Typography, { TypographyProps } from '@mui/material/Typography';
|
||||
import { shouldForwardProp } from '@/lib/ui/ShouldForwardProp.ts';
|
||||
|
||||
type TypographyMaxLinesProps = {
|
||||
lines?: number;
|
||||
};
|
||||
|
||||
export const TypographyMaxLines = styled(Typography, {
|
||||
shouldForwardProp: shouldForwardProp<TypographyMaxLinesProps>(['lines']),
|
||||
})<TypographyMaxLinesProps>(({ lines = 2 }) => ({
|
||||
lineHeight: '1.5rem',
|
||||
maxHeight: '3rem',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: `${lines}`,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
overflowWrap: 'break-word',
|
||||
})) as React.FC<TypographyProps & { lines?: number }>;
|
||||
@@ -29,11 +29,15 @@ import {
|
||||
ChapterRealUrlInfo,
|
||||
Chapters,
|
||||
} from '@/lib/data/Chapters.ts';
|
||||
import { MenuItem } from '@/components/menu/MenuItem.tsx';
|
||||
import { MenuItem } from '@/modules/core/components/menu/MenuItem.tsx';
|
||||
import { IChapterWithMeta } from '@/components/chapter/ChapterList.tsx';
|
||||
import { ChaptersWithMeta } from '@/lib/data/ChaptersWithMeta.ts';
|
||||
import { createGetMenuItemTitle, createIsMenuItemDisabled, createShouldShowMenuItem } from '@/components/menu/util.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import {
|
||||
createGetMenuItemTitle,
|
||||
createIsMenuItemDisabled,
|
||||
createShouldShowMenuItem,
|
||||
} from '@/modules/core/components/menu/Menu.utils.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
|
||||
type BaseProps = { onClose: () => void };
|
||||
|
||||
@@ -22,12 +22,12 @@ import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||
import { useLongPress } from 'use-long-press';
|
||||
import { getDateString } from '@/util/date.ts';
|
||||
import { DownloadStateIndicator } from '@/components/molecules/DownloadStateIndicator.tsx';
|
||||
import { getDateString } from '@/util/DateHelper.ts';
|
||||
import { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx';
|
||||
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { ChapterActionMenuItems } from '@/components/chapter/ChapterActionMenuItems.tsx';
|
||||
import { Menu } from '@/components/menu/Menu.tsx';
|
||||
import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines.tsx';
|
||||
import { Menu } from '@/modules/core/components/menu/Menu.tsx';
|
||||
import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx';
|
||||
import {
|
||||
ChapterBookmarkInfo,
|
||||
ChapterDownloadInfo,
|
||||
|
||||
@@ -19,14 +19,14 @@ import DownloadIcon from '@mui/icons-material/Download';
|
||||
import DoneAllIcon from '@mui/icons-material/DoneAll';
|
||||
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { ChapterCard } from '@/components/chapter/ChapterCard.tsx';
|
||||
import { ResumeFab } from '@/components/manga/ResumeFAB.tsx';
|
||||
import { filterAndSortChapters, useChapterOptions } from '@/components/chapter/util.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { ChaptersToolbarMenu } from '@/components/chapter/ChaptersToolbarMenu.tsx';
|
||||
import { SelectionFAB } from '@/components/collection/SelectionFAB.tsx';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab.tsx';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
|
||||
import {
|
||||
GetChaptersMangaQuery,
|
||||
GetChaptersMangaQueryVariables,
|
||||
@@ -38,14 +38,14 @@ import { Chapters } from '@/lib/data/Chapters.ts';
|
||||
import { ChaptersWithMeta, ChapterWithMetaType } from '@/lib/data/ChaptersWithMeta.ts';
|
||||
import { ChapterActionMenuItems } from '@/components/chapter/ChapterActionMenuItems.tsx';
|
||||
import { ChaptersDownloadActionMenuItems } from '@/components/chapter/ChaptersDownloadActionMenuItems.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { GET_CHAPTERS_MANGA } from '@/lib/graphql/queries/ChapterQuery.ts';
|
||||
import { Mangas } from '@/lib/data/Mangas';
|
||||
import { useNavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
import { useResizeObserver } from '@/util/useResizeObserver.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||
import { shouldForwardProp } from '@/lib/ui/ShouldForwardProp.ts';
|
||||
import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts';
|
||||
|
||||
type ChapterListHeaderProps = {
|
||||
scrollbarWidth: number;
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChapterListOptions, ChapterOptionsReducerAction, TranslationKey } from '@/typings.ts';
|
||||
import { RadioInput } from '@/components/atoms/RadioInput.tsx';
|
||||
import { SortRadioInput } from '@/components/atoms/SortRadioInput.tsx';
|
||||
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx';
|
||||
import { OptionsTabs } from '@/components/molecules/OptionsTabs.tsx';
|
||||
import { ChapterListOptions, ChapterOptionsReducerAction } from '@/typings.ts';
|
||||
import { RadioInput } from '@/modules/core/components/inputs/RadioInput.tsx';
|
||||
import { SortRadioInput } from '@/modules/core/components/inputs/SortRadioInput.tsx';
|
||||
import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { OptionsTabs } from '@/modules/core/components/OptionsTabs.tsx';
|
||||
import { SORT_OPTIONS } from '@/components/chapter/util.tsx';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -10,7 +10,7 @@ import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
const DownloadRange = {
|
||||
|
||||
@@ -7,16 +7,11 @@
|
||||
*/
|
||||
|
||||
import { t } from 'i18next';
|
||||
import {
|
||||
ChapterListOptions,
|
||||
ChapterOptionsReducerAction,
|
||||
ChapterSortMode,
|
||||
NullAndUndefined,
|
||||
TranslationKey,
|
||||
} from '@/typings.ts';
|
||||
import { useReducerLocalStorage } from '@/util/useStorage.tsx';
|
||||
import { ChapterListOptions, ChapterOptionsReducerAction, ChapterSortMode } from '@/typings.ts';
|
||||
import { useReducerLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { ChapterBookmarkInfo, ChapterDownloadInfo, ChapterReadInfo } from '@/lib/data/Chapters.ts';
|
||||
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { NullAndUndefined, TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
const defaultChapterOptions: ChapterListOptions = {
|
||||
active: false,
|
||||
|
||||
@@ -13,9 +13,9 @@ import { styled } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||
import { DEFAULT_FAB_STYLE } from '@/components/util/StyledFab.tsx';
|
||||
import { TranslationKey } from '@/typings.ts';
|
||||
import { Menu } from '@/components/menu/Menu.tsx';
|
||||
import { DEFAULT_FAB_STYLE } from '@/modules/core/components/buttons/StyledFab.tsx';
|
||||
import { Menu } from '@/modules/core/components/menu/Menu.tsx';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
interface SelectionFABProps {
|
||||
children: (handleClose: () => void, setHideMenu: (hide: boolean) => void) => JSX.Element;
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* 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 { Direction, StyledEngineProvider, ThemeProvider } from '@mui/material/styles';
|
||||
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CacheProvider, EmotionCache } from '@emotion/react';
|
||||
import createCache from '@emotion/cache';
|
||||
import { prefixer } from 'stylis';
|
||||
import rtlPlugin from 'stylis-plugin-rtl';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import { createAndSetTheme } from '@/theme.tsx';
|
||||
import { useLocalStorage } from '@/util/useStorage.tsx';
|
||||
import { ThemeMode, ThemeModeContext } from '@/components/context/ThemeModeContext.tsx';
|
||||
import { NavBarContextProvider } from '@/components/navbar/NavBarContextProvider';
|
||||
import { LibraryOptionsContextProvider } from '@/components/library/LibraryOptionsProvider';
|
||||
import { ActiveDevice, DEFAULT_DEVICE, setActiveDevice } from '@/util/device.ts';
|
||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||
import { AppThemes, getTheme } from '@/lib/ui/AppThemes.ts';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const directionToCache: Record<Direction, EmotionCache> = {
|
||||
ltr: createCache({
|
||||
key: 'muiltr',
|
||||
}),
|
||||
rtl: createCache({
|
||||
key: 'muirtl',
|
||||
stylisPlugins: [prefixer, rtlPlugin],
|
||||
}),
|
||||
};
|
||||
|
||||
export const AppContext: React.FC<Props> = ({ children }) => {
|
||||
const directionRef = useRef<Direction>('ltr');
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const currentDirection = i18n.dir();
|
||||
|
||||
if (directionRef.current !== currentDirection) {
|
||||
document.dir = currentDirection;
|
||||
directionRef.current = currentDirection;
|
||||
}
|
||||
|
||||
const {
|
||||
settings: { customThemes },
|
||||
} = useMetadataServerSettings();
|
||||
|
||||
const [systemThemeMode, setSystemThemeMode] = useState<ThemeMode>(MediaQuery.getSystemThemeMode());
|
||||
useLayoutEffect(() => {
|
||||
const unsubscribe = MediaQuery.listenToSystemThemeChange(setSystemThemeMode);
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const [appTheme, setAppTheme] = useLocalStorage<AppThemes>('appTheme', 'default');
|
||||
const [themeMode, setThemeMode] = useLocalStorage<ThemeMode>('themeMode', ThemeMode.SYSTEM);
|
||||
const [pureBlackMode, setPureBlackMode] = useLocalStorage<boolean>('pureBlackMode', false);
|
||||
const [activeDevice, setActiveDeviceContext] = useLocalStorage('activeDevice', DEFAULT_DEVICE);
|
||||
|
||||
const darkThemeContext = useMemo(
|
||||
() => ({
|
||||
appTheme,
|
||||
setAppTheme,
|
||||
themeMode,
|
||||
setThemeMode,
|
||||
pureBlackMode,
|
||||
setPureBlackMode,
|
||||
}),
|
||||
[themeMode, pureBlackMode, appTheme],
|
||||
);
|
||||
|
||||
const activeDeviceContext = useMemo(
|
||||
() => ({ activeDevice, setActiveDevice: setActiveDeviceContext }),
|
||||
[activeDevice],
|
||||
);
|
||||
|
||||
const theme = useMemo(
|
||||
() => createAndSetTheme(themeMode, getTheme(appTheme, customThemes), pureBlackMode, currentDirection),
|
||||
[themeMode, currentDirection, systemThemeMode, pureBlackMode, appTheme, customThemes],
|
||||
);
|
||||
|
||||
setActiveDevice(activeDevice);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<CacheProvider value={directionToCache[currentDirection]}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeModeContext.Provider value={darkThemeContext}>
|
||||
<QueryParamProvider adapter={ReactRouter6Adapter}>
|
||||
<LibraryOptionsContextProvider>
|
||||
<NavBarContextProvider>
|
||||
<ActiveDevice.Provider value={activeDeviceContext}>
|
||||
<SnackbarProvider>{children}</SnackbarProvider>
|
||||
</ActiveDevice.Provider>
|
||||
</NavBarContextProvider>
|
||||
</LibraryOptionsContextProvider>
|
||||
</QueryParamProvider>
|
||||
</ThemeModeContext.Provider>
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
</StyledEngineProvider>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
@@ -10,25 +10,26 @@ import FormLabel from '@mui/material/FormLabel';
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LibrarySortMode, TranslationKey } from '@/typings';
|
||||
import { CheckboxInput } from '@/components/atoms/CheckboxInput';
|
||||
import { RadioInput } from '@/components/atoms/RadioInput';
|
||||
import { SortRadioInput } from '@/components/atoms/SortRadioInput';
|
||||
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput';
|
||||
import { LibrarySortMode } from '@/typings';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
import { RadioInput } from '@/modules/core/components/inputs/RadioInput.tsx';
|
||||
import { SortRadioInput } from '@/modules/core/components/inputs/SortRadioInput.tsx';
|
||||
import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
||||
import { OptionsTabs } from '@/components/molecules/OptionsTabs';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { OptionsTabs } from '@/modules/core/components/OptionsTabs.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { Trackers } from '@/lib/data/Trackers.ts';
|
||||
import { GetTrackersSettingsQuery, MangaStatus } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_TRACKERS_SETTINGS } from '@/lib/graphql/queries/TrackerQuery.ts';
|
||||
import { statusToTranslationKey } from '@/lib/data/Mangas.ts';
|
||||
import { CategoryMetadataInfo } from '@/lib/data/Categories.ts';
|
||||
import { createUpdateCategoryMetadata, getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import {
|
||||
createUpdateMetadataServerSettings,
|
||||
useMetadataServerSettings,
|
||||
} from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
filter: 'global.label.filter',
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { LibraryOptions } from '@/typings';
|
||||
import { useLocalStorage } from '@/util/useStorage.tsx';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { LibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||
import { getDefaultCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
* 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 { useEffect, useMemo, useState } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/components/util/Toast';
|
||||
import { UpdaterSubscription } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { Progress } from '@/components/util/Progress';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { dateTimeFormatter } from '@/util/date.ts';
|
||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||
import { CategoryIdInfo } from '@/lib/data/Categories.ts';
|
||||
|
||||
const calcProgress = (status: UpdaterSubscription['updateStatusChanged'] | undefined) => {
|
||||
if (!status) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const finishedUpdates = status.failedJobs.mangas.totalCount + status.completeJobs.mangas.totalCount;
|
||||
const totalMangas = finishedUpdates + status.pendingJobs.mangas.totalCount + status.runningJobs.mangas.totalCount;
|
||||
|
||||
const progress = 100 * (finishedUpdates / totalMangas);
|
||||
|
||||
return Number.isNaN(progress) ? 0 : progress;
|
||||
};
|
||||
|
||||
let lastRunningState = false;
|
||||
|
||||
export function UpdateChecker({
|
||||
categoryId,
|
||||
handleFinishedUpdate,
|
||||
}: {
|
||||
categoryId?: CategoryIdInfo['id'];
|
||||
handleFinishedUpdate?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const { data: lastUpdateTimestampData, refetch: reFetchLastTimestamp } =
|
||||
requestManager.useGetLastGlobalUpdateTimestamp();
|
||||
const lastUpdateTimestamp = lastUpdateTimestampData?.lastUpdateTimestamp.timestamp;
|
||||
const { data: updaterData } = requestManager.useGetGlobalUpdateSummary();
|
||||
const status = updaterData?.updateStatus;
|
||||
|
||||
const isRunning = !!status?.isRunning;
|
||||
const progress = useMemo(
|
||||
() => calcProgress(status),
|
||||
[
|
||||
status?.failedJobs.mangas.totalCount,
|
||||
status?.completeJobs.mangas.totalCount,
|
||||
status?.pendingJobs.mangas.totalCount,
|
||||
status?.runningJobs.mangas.totalCount,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastRunningState && status?.isRunning) {
|
||||
lastRunningState = true;
|
||||
}
|
||||
|
||||
const isUpdateFinished = lastRunningState && progress === 100;
|
||||
if (!isUpdateFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastRunningState = false;
|
||||
handleFinishedUpdate?.();
|
||||
// this re-fetch is necessary since a running update could have been triggered by the server or another client
|
||||
reFetchLastTimestamp().catch(defaultPromiseErrorHandler('UpdateChecker::reFetchLastTimestamp'));
|
||||
}, [status?.isRunning]);
|
||||
|
||||
const startUpdate = async (category?: CategoryIdInfo['id']) => {
|
||||
try {
|
||||
lastRunningState = true;
|
||||
await requestManager.startGlobalUpdate(category !== undefined ? [category] : undefined).response;
|
||||
reFetchLastTimestamp().catch(defaultPromiseErrorHandler('UpdateChecker::reFetchLastTimestamp'));
|
||||
} catch (e) {
|
||||
lastRunningState = false;
|
||||
makeToast(t('global.error.label.update_failed'), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const stopUpdate = async () => {
|
||||
try {
|
||||
await requestManager.resetGlobalUpdate();
|
||||
} catch (e) {
|
||||
makeToast(t('library.error.label.stop_global_update'), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const onClick = async (category?: CategoryIdInfo['id']) => {
|
||||
if (isRunning) {
|
||||
stopUpdate();
|
||||
} else {
|
||||
startUpdate(category);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupState variant="popover" popupId="library-update-checker-menu">
|
||||
{(popupState) => (
|
||||
<>
|
||||
<Tooltip
|
||||
title={
|
||||
isRunning
|
||||
? t('library.action.label.stop_update')
|
||||
: t('library.settings.global_update.label.last_update_tooltip', {
|
||||
date: lastUpdateTimestamp ? dateTimeFormatter.format(+lastUpdateTimestamp) : '-',
|
||||
})
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
sx={{ position: 'relative' }}
|
||||
{...(categoryId !== undefined && !isRunning
|
||||
? bindTrigger(popupState)
|
||||
: { onClick: () => onClick() })}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
color="inherit"
|
||||
>
|
||||
{!isRunning ? (
|
||||
<RefreshIcon />
|
||||
) : (
|
||||
<>
|
||||
<ClearIcon sx={{ opacity: Number(isTouchDevice || isHovered) }} />
|
||||
<Stack sx={{ position: 'absolute' }}>
|
||||
<Progress
|
||||
progress={progress}
|
||||
showText={!isTouchDevice && !isHovered}
|
||||
progressProps={{ color: 'inherit' }}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
popupState.close();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{t('library.action.label.update_library')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
popupState.close();
|
||||
onClick(categoryId);
|
||||
}}
|
||||
>
|
||||
{t('library.action.label.update_category')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</PopupState>
|
||||
);
|
||||
}
|
||||
@@ -8,13 +8,14 @@
|
||||
|
||||
import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import { useMemo } from 'react';
|
||||
import { LibraryOptions, LibrarySortMode, NullAndUndefined } from '@/typings.ts';
|
||||
import { LibraryOptions, LibrarySortMode } from '@/typings.ts';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { ChapterType, MangaType, SourceType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { MangaChapterCountInfo, MangaIdInfo } from '@/lib/data/Mangas.ts';
|
||||
import { enhancedCleanup } from '@/lib/data/Strings.ts';
|
||||
import { CategoryMetadataInfo } from '@/lib/data/Categories.ts';
|
||||
import { getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
import { NullAndUndefined } from '@/Base.types.ts';
|
||||
|
||||
const triStateFilter = (
|
||||
triState: NullAndUndefined<boolean>,
|
||||
|
||||
@@ -28,13 +28,17 @@ import {
|
||||
MangaUnreadInfo,
|
||||
} from '@/lib/data/Mangas.ts';
|
||||
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
|
||||
import { MenuItem } from '@/components/menu/MenuItem.tsx';
|
||||
import { createGetMenuItemTitle, createIsMenuItemDisabled, createShouldShowMenuItem } from '@/components/menu/util.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { MenuItem } from '@/modules/core/components/menu/MenuItem.tsx';
|
||||
import {
|
||||
createGetMenuItemTitle,
|
||||
createIsMenuItemDisabled,
|
||||
createShouldShowMenuItem,
|
||||
} from '@/modules/core/components/menu/Menu.utils.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { TrackManga } from '@/components/tracker/TrackManga.tsx';
|
||||
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
|
||||
import { ChaptersDownloadActionMenuItems } from '@/components/chapter/ChaptersDownloadActionMenuItems.tsx';
|
||||
import { NestedMenuItem } from '@/components/menu/NestedMenuItem.tsx';
|
||||
import { NestedMenuItem } from '@/modules/core/components/menu/NestedMenuItem.tsx';
|
||||
import { MangaChapterStatFieldsFragment, MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const;
|
||||
|
||||
@@ -26,19 +26,19 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import OpenInFullIcon from '@mui/icons-material/OpenInFull';
|
||||
import Modal from '@mui/material/Modal';
|
||||
import { bindPopover, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import { makeToast } from '@/components/util/Toast';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { Mangas, MangaThumbnailInfo, MangaTrackRecordInfo, statusToTranslationKey } from '@/lib/data/Mangas.ts';
|
||||
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
||||
import { CustomIconButton } from '@/components/atoms/CustomIconButton';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx';
|
||||
import { TrackMangaButton } from '@/components/manga/TrackMangaButton.tsx';
|
||||
import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx';
|
||||
import { Metadata as BaseMetadata } from '@/components/atoms/Metadata.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { Metadata as BaseMetadata } from '@/modules/core/components/Metadata.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { MangaType, SourceType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { useLocalStorage } from '@/util/useStorage.tsx';
|
||||
import { useResizeObserver } from '@/util/useResizeObserver.tsx';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { shouldForwardProp } from '@/lib/ui/ShouldForwardProp.ts';
|
||||
import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts';
|
||||
|
||||
const DetailsWrapper = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
|
||||
@@ -15,12 +15,12 @@ import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { useRef } from 'react';
|
||||
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { MangaOptionButton } from '@/components/manga/MangaOptionButton.tsx';
|
||||
import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx';
|
||||
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||
import { SpecificMangaCardProps } from '@/components/manga/MangaCard.types.tsx';
|
||||
import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines';
|
||||
import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx';
|
||||
|
||||
const BottomGradient = styled('div')({
|
||||
position: 'absolute',
|
||||
|
||||
@@ -15,8 +15,8 @@ import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useRef } from 'react';
|
||||
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
||||
import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines.tsx';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx';
|
||||
import { SpecificMangaCardProps } from '@/components/manga/MangaCard.types.tsx';
|
||||
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||
import { MangaOptionButton } from '@/components/manga/MangaOptionButton.tsx';
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import PlayArrow from '@mui/icons-material/PlayArrow';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyledFab } from '@/components/util/StyledFab';
|
||||
import { StyledFab } from '@/modules/core/components/buttons/StyledFab.tsx';
|
||||
|
||||
interface ResumeFABProps {
|
||||
chapterIndex: number;
|
||||
|
||||
@@ -12,11 +12,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import PopupState, { bindDialog, bindTrigger } from 'material-ui-popup-state';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { TrackManga } from '@/components/tracker/TrackManga.tsx';
|
||||
import { Trackers } from '@/lib/data/Trackers.ts';
|
||||
import { CustomIconButton } from '@/components/atoms/CustomIconButton.tsx';
|
||||
import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx';
|
||||
import { GetTrackersSettingsQuery, MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_TRACKERS_SETTINGS } from '@/lib/graphql/queries/TrackerQuery.ts';
|
||||
import { MangaTrackRecordInfo } from '@/lib/data/Mangas.ts';
|
||||
|
||||
@@ -11,11 +11,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import gql from 'graphql-tag';
|
||||
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { Categories } from '@/lib/data/Categories.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||
import { awaitConfirmation } from '@/lib/ui/AwaitableDialog.tsx';
|
||||
import { GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables, MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { baseCleanup } from '@/lib/data/Strings.ts';
|
||||
|
||||
export const useRefreshManga = (mangaId: string) => {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/*
|
||||
* src: https://github.com/webzep/mui-nested-menu/blob/main/packages/mui-nested-menu/src/components/IconMenuItem.tsx (2024-04-20 01:42)
|
||||
*/
|
||||
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import MenuItem, { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem';
|
||||
import { SxProps, Theme } from '@mui/material/styles';
|
||||
import React, { forwardRef, RefObject } from 'react';
|
||||
|
||||
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||
import { SvgIconTypeMap } from '@mui/material/SvgIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
|
||||
type IconMenuItemProps = {
|
||||
MenuItemProps?: MuiMenuItemProps;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
renderLabel?: () => React.ReactNode;
|
||||
LeftIcon?: OverridableComponent<SvgIconTypeMap> & { muiName: string };
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
ref?: RefObject<HTMLLIElement>;
|
||||
RightIcon?: OverridableComponent<SvgIconTypeMap> & { muiName: string };
|
||||
sx?: SxProps<Theme>;
|
||||
};
|
||||
|
||||
export const IconMenuItem = forwardRef<HTMLLIElement, IconMenuItemProps>(
|
||||
({ MenuItemProps, className, label, LeftIcon, renderLabel, RightIcon, ...props }, ref) => (
|
||||
<MenuItem {...MenuItemProps} ref={ref} className={className} {...props}>
|
||||
{LeftIcon && (
|
||||
<ListItemIcon>
|
||||
<LeftIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText>{label}</ListItemText>
|
||||
{RightIcon && (
|
||||
<ListItemIcon style={{ minWidth: 0 }}>
|
||||
<RightIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
</MenuItem>
|
||||
),
|
||||
);
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* 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 MuiMenu, { MenuProps } from '@mui/material/Menu';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const Menu = ({
|
||||
children,
|
||||
onClose,
|
||||
...props
|
||||
}: Omit<MenuProps, 'children' | 'onClose'> &
|
||||
Required<Pick<MenuProps, 'onClose'>> & {
|
||||
children: (onClose: () => void, setHideMenu: (hide: boolean) => void) => JSX.Element | JSX.Element[];
|
||||
}) => {
|
||||
const [shouldHideMenu, setShouldHideMenu] = useState(false);
|
||||
|
||||
return (
|
||||
<MuiMenu
|
||||
{...props}
|
||||
open={props.open}
|
||||
onClose={onClose}
|
||||
sx={{ visibility: !props.open || shouldHideMenu ? 'hidden' : 'visible' }}
|
||||
>
|
||||
{children(() => {
|
||||
onClose({}, 'backdropClick');
|
||||
setShouldHideMenu(false);
|
||||
}, setShouldHideMenu)}
|
||||
</MuiMenu>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* 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 ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import MuiMenuItem, { MenuItemProps } from '@mui/material/MenuItem';
|
||||
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||
import { SvgIconTypeMap } from '@mui/material/SvgIcon';
|
||||
|
||||
interface IProps extends MenuItemProps {
|
||||
title: string;
|
||||
Icon: OverridableComponent<SvgIconTypeMap> & { muiName: string };
|
||||
}
|
||||
|
||||
export const MenuItem = ({ title, Icon, ...menuItemProps }: IProps) => (
|
||||
<MuiMenuItem {...menuItemProps}>
|
||||
<ListItemIcon>
|
||||
<Icon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{title}</ListItemText>
|
||||
</MuiMenuItem>
|
||||
);
|
||||
@@ -1,236 +0,0 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/*
|
||||
* src: https://github.com/webzep/mui-nested-menu/blob/main/packages/mui-nested-menu/src/components/NestedMenuItem.tsx (2024-04-20 01:42)
|
||||
*
|
||||
* with a few changes to fix a bug on mobile devices where opening the sub menu immediately triggered the on click of the underlying menu item
|
||||
*/
|
||||
|
||||
import Menu, { MenuProps as MuiMenuProps } from '@mui/material/Menu';
|
||||
import { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem';
|
||||
import {
|
||||
ElementType,
|
||||
forwardRef,
|
||||
HTMLAttributes,
|
||||
KeyboardEvent,
|
||||
FocusEvent,
|
||||
MouseEvent,
|
||||
ReactNode,
|
||||
RefAttributes,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
Ref,
|
||||
} from 'react';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||
import { SvgIconTypeMap } from '@mui/material/SvgIcon';
|
||||
import { IconMenuItem } from '@/components/menu/IconMenuItem.tsx';
|
||||
import { getOptionForDirection } from '@/theme.tsx';
|
||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||
|
||||
export type NestedMenuItemProps = Omit<MuiMenuItemProps, 'button'> & {
|
||||
parentMenuOpen: boolean;
|
||||
component?: ElementType;
|
||||
label?: string;
|
||||
renderLabel?: () => ReactNode;
|
||||
RightIcon?: OverridableComponent<SvgIconTypeMap> & { muiName: string };
|
||||
LeftIcon?: OverridableComponent<SvgIconTypeMap> & { muiName: string };
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
tabIndex?: number;
|
||||
disabled?: boolean;
|
||||
ContainerProps?: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement>;
|
||||
MenuProps?: Partial<Omit<MuiMenuProps, 'children'>>;
|
||||
button?: true | undefined;
|
||||
};
|
||||
|
||||
const NestedMenuItem = forwardRef<HTMLLIElement | null, NestedMenuItemProps>((props, ref) => {
|
||||
const {
|
||||
parentMenuOpen,
|
||||
label,
|
||||
renderLabel,
|
||||
RightIcon = getOptionForDirection(ChevronRightIcon, ChevronLeftIcon),
|
||||
LeftIcon,
|
||||
children,
|
||||
className,
|
||||
tabIndex: tabIndexProp,
|
||||
ContainerProps: ContainerPropsProp = {},
|
||||
MenuProps,
|
||||
...MenuItemProps
|
||||
} = props;
|
||||
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
|
||||
const { ref: containerRefProp, ...ContainerProps } = ContainerPropsProp;
|
||||
|
||||
const menuItemRef = useRef<HTMLLIElement | null>(null);
|
||||
useImperativeHandle(ref, () => menuItemRef.current!);
|
||||
|
||||
const containerRef = useRef<HTMLElement>(null);
|
||||
useImperativeHandle(containerRefProp as Ref<HTMLElement | null>, () => containerRef.current as HTMLElement);
|
||||
|
||||
const menuContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
|
||||
|
||||
const changeMenuOpenState = (open: boolean) => {
|
||||
if (isSubMenuOpen === open) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
setIsSubMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubMenuOpen(open);
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: MouseEvent<HTMLElement>) => {
|
||||
if (isTouchDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
changeMenuOpenState(true);
|
||||
|
||||
if (ContainerProps.onMouseEnter) {
|
||||
ContainerProps.onMouseEnter(e);
|
||||
}
|
||||
};
|
||||
const handleMouseLeave = (e: MouseEvent<HTMLElement>) => {
|
||||
changeMenuOpenState(false);
|
||||
|
||||
if (ContainerProps.onMouseLeave) {
|
||||
ContainerProps.onMouseLeave(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if any immediate children are active
|
||||
const isSubmenuFocused = () => {
|
||||
const active = containerRef.current?.ownerDocument.activeElement ?? null;
|
||||
if (menuContainerRef.current == null) {
|
||||
return false;
|
||||
}
|
||||
for (const child of menuContainerRef.current.children) {
|
||||
if (child === active) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleFocus = (e: FocusEvent<HTMLElement>) => {
|
||||
if (isTouchDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target === containerRef.current) {
|
||||
changeMenuOpenState(true);
|
||||
}
|
||||
|
||||
if (ContainerProps.onFocus) {
|
||||
ContainerProps.onFocus(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: MouseEvent<HTMLElement>) => {
|
||||
changeMenuOpenState(!isSubMenuOpen);
|
||||
|
||||
if (ContainerProps.onClick) {
|
||||
ContainerProps.onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubmenuFocused()) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const active = containerRef.current?.ownerDocument.activeElement;
|
||||
|
||||
if (e.key === 'ArrowLeft' && isSubmenuFocused()) {
|
||||
containerRef.current?.focus();
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowRight' && e.target === containerRef.current && e.target === active) {
|
||||
const firstChild = menuContainerRef.current?.children[0] as HTMLDivElement;
|
||||
firstChild?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const open = isSubMenuOpen && parentMenuOpen;
|
||||
|
||||
// Root element must have a `tabIndex` attribute for keyboard navigation
|
||||
let tabIndex;
|
||||
if (!props.disabled) {
|
||||
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...ContainerProps}
|
||||
ref={containerRef}
|
||||
onFocus={handleFocus}
|
||||
onClick={handleClick}
|
||||
tabIndex={tabIndex}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<IconMenuItem
|
||||
MenuItemProps={MenuItemProps}
|
||||
className={className}
|
||||
ref={menuItemRef}
|
||||
LeftIcon={LeftIcon}
|
||||
RightIcon={RightIcon}
|
||||
label={label}
|
||||
renderLabel={renderLabel}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
// Set pointer events to 'none' to prevent the invisible Popover div
|
||||
// from capturing events for clicks and hovers
|
||||
style={{ pointerEvents: 'none' }}
|
||||
anchorEl={menuItemRef.current}
|
||||
anchorOrigin={{
|
||||
horizontal: getOptionForDirection('right', 'left'),
|
||||
vertical: 'top',
|
||||
}}
|
||||
transformOrigin={{
|
||||
horizontal: getOptionForDirection('left', 'right'),
|
||||
vertical: 'top',
|
||||
}}
|
||||
open={open}
|
||||
autoFocus={false}
|
||||
disableAutoFocus
|
||||
disableEnforceFocus
|
||||
onClose={() => {
|
||||
changeMenuOpenState(false);
|
||||
}}
|
||||
{...MenuProps}
|
||||
>
|
||||
<Box ref={menuContainerRef} style={{ pointerEvents: 'auto' }}>
|
||||
{children}
|
||||
</Box>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
NestedMenuItem.displayName = 'NestedMenuItem';
|
||||
export { NestedMenuItem };
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* 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 { t as translate } from 'i18next';
|
||||
import { TranslationKey } from '@/typings.ts';
|
||||
|
||||
export const createGetMenuItemTitle =
|
||||
<Action extends string>(
|
||||
isSingleMode: boolean,
|
||||
actionToTranslationKey: Record<
|
||||
Action,
|
||||
{
|
||||
action: {
|
||||
single: TranslationKey;
|
||||
selected: TranslationKey;
|
||||
};
|
||||
success: TranslationKey;
|
||||
error: TranslationKey;
|
||||
}
|
||||
>,
|
||||
) =>
|
||||
(action: Action, count: number): string => {
|
||||
const countSuffix = count > 0 ? ` (${count})` : '';
|
||||
return `${translate(
|
||||
actionToTranslationKey[action].action[isSingleMode ? 'single' : 'selected'],
|
||||
)}${countSuffix}`;
|
||||
};
|
||||
|
||||
export const createShouldShowMenuItem =
|
||||
(isSingleMode: boolean) =>
|
||||
(shouldBeVisible: boolean = false): boolean =>
|
||||
isSingleMode ? shouldBeVisible : true;
|
||||
|
||||
export const createIsMenuItemDisabled =
|
||||
(isSingleMode: boolean) =>
|
||||
(shouldBeDisabled: boolean): boolean =>
|
||||
isSingleMode ? false : shouldBeDisabled;
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* 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 Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
type Action = {
|
||||
show?: boolean;
|
||||
title?: string;
|
||||
contain?: boolean;
|
||||
};
|
||||
|
||||
type Actions = {
|
||||
extra?: Action;
|
||||
cancel?: Action;
|
||||
confirm?: Action;
|
||||
};
|
||||
|
||||
export const ConfirmDialog = ({
|
||||
title,
|
||||
message,
|
||||
actions: passedActions,
|
||||
onExtra,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: {
|
||||
title: string;
|
||||
message: string;
|
||||
actions?: Actions;
|
||||
onExtra?: () => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const actions = {
|
||||
extra: {
|
||||
show: passedActions?.extra?.show ?? false,
|
||||
title: passedActions?.extra?.title ?? '',
|
||||
contain: passedActions?.extra?.contain ?? false,
|
||||
},
|
||||
cancel: {
|
||||
show: passedActions?.cancel?.show ?? true,
|
||||
title: passedActions?.cancel?.title ?? t('global.button.cancel'),
|
||||
contain: passedActions?.cancel?.contain ?? false,
|
||||
},
|
||||
confirm: {
|
||||
show: passedActions?.confirm?.show ?? true,
|
||||
title: passedActions?.confirm?.title ?? t('global.button.ok'),
|
||||
contain:
|
||||
!passedActions?.extra?.contain &&
|
||||
!passedActions?.cancel?.contain &&
|
||||
!passedActions?.confirm?.contain &&
|
||||
true,
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
||||
return (
|
||||
<Dialog open>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent
|
||||
sx={{
|
||||
whiteSpace: 'pre-line',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: actions.extra.show ? 'space-between' : 'end',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{actions.extra.show && (
|
||||
<Button onClick={onExtra} variant={actions.extra.contain ? 'contained' : undefined}>
|
||||
{actions.extra.title}
|
||||
</Button>
|
||||
)}
|
||||
<Stack direction="row">
|
||||
{actions.cancel.show && (
|
||||
<Button onClick={onCancel} variant={actions.cancel.contain ? 'contained' : undefined}>
|
||||
{actions.cancel.title}
|
||||
</Button>
|
||||
)}
|
||||
{actions.confirm.show && (
|
||||
<Button onClick={onConfirm} variant={actions.confirm.contain ? 'contained' : undefined}>
|
||||
{actions.confirm.title}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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 CircularProgress from '@mui/material/CircularProgress';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TranslationKey } from '@/typings';
|
||||
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { ChapterDownloadStatus } from '@/lib/data/Chapters.ts';
|
||||
|
||||
const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: TranslationKey } = {
|
||||
DOWNLOADING: 'download.state.label.downloading',
|
||||
ERROR: 'download.state.label.error',
|
||||
FINISHED: 'download.state.label.finished',
|
||||
QUEUED: 'download.state.label.queued',
|
||||
} as const;
|
||||
|
||||
export const DownloadStateIndicator = ({ download }: { download: ChapterDownloadStatus }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
width: '50px',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{download.progress !== 0 && <CircularProgress variant="determinate" value={download.progress * 100} />}
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{download.progress !== 0 && `${Math.round(download.progress * 100)}%`}
|
||||
{download.progress === 0 && t(DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP[download.state])}
|
||||
</>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* 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 Drawer from '@mui/material/Drawer';
|
||||
import Box from '@mui/material/Box';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
minHeight?: number;
|
||||
}
|
||||
|
||||
export const OptionsPanel: React.FC<IProps> = ({ open, onClose, children, minHeight }) => (
|
||||
<Drawer
|
||||
anchor="bottom"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
PaperProps={{
|
||||
style: {
|
||||
maxWidth: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
minHeight,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>{children}</Box>
|
||||
</Drawer>
|
||||
);
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* 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 Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import React, { useState } from 'react';
|
||||
import { TabPanel } from '@/components/tabs/TabPanel.tsx';
|
||||
import { OptionsPanel } from '@/components/molecules/OptionsPanel';
|
||||
|
||||
interface IProps<T = string> {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
tabs: T[];
|
||||
tabTitle: (key: T) => React.ReactNode;
|
||||
tabContent: (key: T) => React.ReactNode;
|
||||
minHeight?: number;
|
||||
}
|
||||
|
||||
export const OptionsTabs = <T extends string = string>({
|
||||
open,
|
||||
onClose,
|
||||
tabs,
|
||||
tabTitle,
|
||||
tabContent,
|
||||
minHeight,
|
||||
}: IProps<T>) => {
|
||||
const [tabNum, setTabNum] = useState(0);
|
||||
|
||||
return (
|
||||
<OptionsPanel open={open} onClose={onClose} minHeight={minHeight}>
|
||||
<Tabs
|
||||
value={tabNum}
|
||||
variant="fullWidth"
|
||||
onChange={(e, newTab) => setTabNum(newTab)}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
>
|
||||
{tabs.map((tab, tabIndex) => (
|
||||
<Tab key={tab} value={tabIndex} label={tabTitle(tab)} />
|
||||
))}
|
||||
</Tabs>
|
||||
{tabs.map((tab, tabIndex) => (
|
||||
<TabPanel key={tab} index={tabIndex} currentIndex={tabNum}>
|
||||
<Stack sx={{ px: 3, py: 1, minHeight }}>{tabContent(tab)}</Stack>
|
||||
</TabPanel>
|
||||
))}
|
||||
</OptionsPanel>
|
||||
);
|
||||
};
|
||||
@@ -28,11 +28,11 @@ import Stack from '@mui/material/Stack';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { NavbarItem } from '@/typings';
|
||||
import { NavBarContext } from '@/components/context/NavbarContext';
|
||||
import { useBackButton } from '@/util/useBackButton.ts';
|
||||
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
|
||||
import { useGetOptionForDirection } from '@/theme.tsx';
|
||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||
import { DesktopSideBar } from '@/components/navbar/navigation/DesktopSideBar.tsx';
|
||||
import { useResizeObserver } from '@/util/useResizeObserver.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
import { MobileBottomBar } from '@/components/navbar/navigation/MobileBottomBar.tsx';
|
||||
|
||||
const navbarItems: Array<NavbarItem> = [
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { INavbarOverride } from '@/typings';
|
||||
import { NavBarContext } from '@/components/context/NavbarContext';
|
||||
import { useHistory } from '@/util/useHistory.ts';
|
||||
import { useLocalStorage } from '@/util/useStorage.tsx';
|
||||
import { useHistory } from '@/modules/core/hooks/useHistory.ts';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -27,16 +27,17 @@ import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AllowedMetadataValueTypes, ChapterOffset, IReaderSettings } from '@/typings';
|
||||
import { AllowedMetadataValueTypes, IReaderSettings } from '@/typings';
|
||||
import { ReaderSettingsOptions } from '@/components/reader/ReaderSettingsOptions';
|
||||
import { useBackButton } from '@/util/useBackButton.ts';
|
||||
import { Select } from '@/components/atoms/Select.tsx';
|
||||
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
|
||||
import { Select } from '@/modules/core/components/inputs/Select.tsx';
|
||||
import { useGetOptionForDirection } from '@/theme.tsx';
|
||||
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { MangaChapterCountInfo, MangaIdInfo } from '@/lib/data/Mangas.ts';
|
||||
import { useNavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
import { useResizeObserver } from '@/util/useResizeObserver.tsx';
|
||||
import { CustomIconButton } from '@/components/atoms/CustomIconButton.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx';
|
||||
import { DirectionOffset } from '@/Base.types.ts';
|
||||
|
||||
const Root = styled('div')({
|
||||
zIndex: 10,
|
||||
@@ -99,7 +100,7 @@ interface IProps {
|
||||
chapters: Pick<ChapterType, 'id' | 'sourceOrder' | 'name' | 'chapterNumber' | 'scanlator'>[];
|
||||
curPage: number;
|
||||
scrollToPage: (page: number) => void;
|
||||
openNextChapter: (offset: ChapterOffset) => void;
|
||||
openNextChapter: (offset: DirectionOffset) => void;
|
||||
retrievingNextChapter: boolean;
|
||||
}
|
||||
|
||||
@@ -319,7 +320,7 @@ export function ReaderNavBar(props: IProps) {
|
||||
<IconButton
|
||||
sx={{ gridArea: 'pre' }}
|
||||
disabled={disableChapterNavButtons || chapter.sourceOrder <= 1}
|
||||
onClick={() => openNextChapter(ChapterOffset.PREV)}
|
||||
onClick={() => openNextChapter(DirectionOffset.PREVIOUS)}
|
||||
>
|
||||
{getOptionForDirection(<KeyboardArrowLeftIcon />, <KeyboardArrowRightIcon />)}
|
||||
</IconButton>
|
||||
@@ -358,7 +359,7 @@ export function ReaderNavBar(props: IProps) {
|
||||
chapter.sourceOrder < 1 ||
|
||||
chapter.sourceOrder >= manga.chapters.totalCount
|
||||
}
|
||||
onClick={() => openNextChapter(ChapterOffset.NEXT)}
|
||||
onClick={() => openNextChapter(DirectionOffset.NEXT)}
|
||||
>
|
||||
{getOptionForDirection(<KeyboardArrowRightIcon />, <KeyboardArrowLeftIcon />)}
|
||||
</IconButton>
|
||||
|
||||
@@ -16,14 +16,14 @@ import FormGroup from '@mui/material/FormGroup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { Mangas } from '@/lib/data/Mangas.ts';
|
||||
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
|
||||
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx';
|
||||
import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { Categories } from '@/lib/data/Categories.ts';
|
||||
import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { updateMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import {
|
||||
GetCategoriesBaseQuery,
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* 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 React, { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cloneObject } from '@/util/cloneObject';
|
||||
import { translateExtensionLanguage } from '@/screens/util/Extensions';
|
||||
|
||||
function removeAll(firstList: any[], secondList: any[]) {
|
||||
secondList.forEach((item) => {
|
||||
const index = firstList.indexOf(item);
|
||||
if (index !== -1) {
|
||||
firstList.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
return firstList;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
shownLangs: string[];
|
||||
setShownLangs: (arg0: string[]) => void;
|
||||
allLangs: string[];
|
||||
forcedLangs?: string[];
|
||||
}
|
||||
|
||||
export function LangSelect(props: IProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { shownLangs, setShownLangs, allLangs, forcedLangs = [] } = props;
|
||||
// hold a copy and only sate state on parent when OK pressed, improves performance
|
||||
const [mShownLangs, setMShownLangs] = useState(removeAll(cloneObject(shownLangs), forcedLangs));
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
setOpen(false);
|
||||
setShownLangs(mShownLangs);
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, lang: string) => {
|
||||
const { checked } = event.target as HTMLInputElement;
|
||||
|
||||
if (checked) {
|
||||
setMShownLangs([...mShownLangs, lang]);
|
||||
} else {
|
||||
const clone = cloneObject(mShownLangs);
|
||||
clone.splice(clone.indexOf(lang), 1);
|
||||
setMShownLangs(clone);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={t('settings.title')}>
|
||||
<IconButton
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
size="large"
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Dialog
|
||||
sx={{
|
||||
'.MuiDialog-paper': {
|
||||
maxHeight: 435,
|
||||
width: '80%',
|
||||
},
|
||||
}}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle>{t('global.language.title.enabled_languages')}</DialogTitle>
|
||||
<DialogContent dividers sx={{ padding: 0 }}>
|
||||
<List>
|
||||
{allLangs.map((lang) => (
|
||||
<ListItem key={lang}>
|
||||
<ListItemText primary={translateExtensionLanguage(lang)} />
|
||||
|
||||
<Switch
|
||||
checked={mShownLangs.indexOf(lang) !== -1}
|
||||
onChange={(e) => handleChange(e, lang)}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary">
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -22,10 +22,10 @@ import Box from '@mui/material/Box';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { NavbarItem } from '@/typings.ts';
|
||||
import { ListItemLink } from '@/components/util/ListItemLink.tsx';
|
||||
import { ListItemLink } from '@/modules/core/components/ListItemLink.tsx';
|
||||
import { useGetOptionForDirection } from '@/theme.tsx';
|
||||
import { useNavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
import { useResizeObserver } from '@/util/useResizeObserver.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
|
||||
const DrawerHeader = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
|
||||
@@ -13,7 +13,7 @@ import Paper from '@mui/material/Paper';
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { NavbarItem } from '@/typings.ts';
|
||||
import { useResizeObserver } from '@/util/useResizeObserver.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
import { useNavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
|
||||
export const MobileBottomBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) => {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { CSSProperties, forwardRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { IReaderSettings } from '@/typings';
|
||||
import { SpinnerImage } from '@/components/util/SpinnerImage';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { imageStyle } from '@/components/reader/Page';
|
||||
import { getOptionForDirection } from '@/theme.tsx';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import Box from '@mui/material/Box';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { IReaderSettings, ReaderType } from '@/typings';
|
||||
import { SpinnerImage } from '@/components/util/SpinnerImage';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||
|
||||
export const isHorizontalReaderType = (readerType: ReaderType): boolean =>
|
||||
|
||||
@@ -13,9 +13,9 @@ import Switch from '@mui/material/Switch';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AllowedMetadataValueTypes, IReaderSettings } from '@/typings';
|
||||
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||
import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx';
|
||||
import { isHorizontalReaderType } from '@/components/reader/Page.tsx';
|
||||
import { Select } from '@/components/atoms/Select.tsx';
|
||||
import { Select } from '@/modules/core/components/inputs/Select.tsx';
|
||||
|
||||
interface IProps extends IReaderSettings {
|
||||
setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { MouseEvent as ReactMouseEvent, WheelEvent as ReactWheelEvent, useCallba
|
||||
import Box from '@mui/material/Box';
|
||||
import { IReaderProps } from '@/typings';
|
||||
import { Page } from '@/components/reader/Page';
|
||||
import { useResizeObserver } from '@/util/useResizeObserver.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
|
||||
const findCurrentPageIndex = (wrapper: HTMLDivElement): number => {
|
||||
for (let i = 0; i < wrapper.children.length; i++) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useCallback, useEffect, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { IReaderProps } from '@/typings';
|
||||
import { Page } from '@/components/reader/Page';
|
||||
import { useResizeObserver } from '@/util/useResizeObserver.tsx';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
|
||||
const findCurrentPageIndex = (wrapper: HTMLDivElement): number => {
|
||||
for (let i = 0; i < wrapper.children.length; i++) {
|
||||
|
||||
@@ -17,11 +17,11 @@ import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { t as translate } from 'i18next';
|
||||
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { IncludeOrExclude } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { CheckboxContainer } from '@/components/settings/globalUpdate/CheckboxContainer.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { CheckboxContainer } from '@/modules/core/components/inputs/CheckboxContainer.ts';
|
||||
import {
|
||||
CategoryDownloadInclusionInfo,
|
||||
CategoryIdInfo,
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* 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 Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
|
||||
export const DateSetting = ({
|
||||
settingName,
|
||||
value,
|
||||
defaultValue,
|
||||
handleChange,
|
||||
remove,
|
||||
}: {
|
||||
settingName: string;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
handleChange: (path?: string | null) => void;
|
||||
remove?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [dialogValue, setDialogValue] = useState(value ?? defaultValue);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDialogValue(value);
|
||||
}, [value]);
|
||||
|
||||
const closeDialog = useCallback(
|
||||
(resetValue: boolean) => {
|
||||
setIsDialogOpen(false);
|
||||
|
||||
if (resetValue) {
|
||||
setDialogValue(value ?? defaultValue);
|
||||
}
|
||||
},
|
||||
[value],
|
||||
);
|
||||
|
||||
const closeDialogWithReset = useCallback(() => closeDialog(true), [closeDialog]);
|
||||
|
||||
const updateSetting = useCallback(
|
||||
(newValue?: string, shouldCloseDialog: boolean = true) => {
|
||||
if (shouldCloseDialog) {
|
||||
closeDialog(false);
|
||||
}
|
||||
|
||||
const didValueChange = value !== newValue;
|
||||
if (!didValueChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleChange(newValue);
|
||||
},
|
||||
[value, handleChange, closeDialog],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton onClick={() => setIsDialogOpen(true)}>
|
||||
<ListItemText
|
||||
primary={settingName}
|
||||
secondary={value ? dayjs(Number(value)).format('L') : '-'}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
<Dialog open={isDialogOpen} onClose={closeDialog}>
|
||||
<DialogContent>
|
||||
<DialogTitle sx={{ paddingLeft: 0 }}>{settingName}</DialogTitle>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={dayjs.locale()}>
|
||||
<DatePicker
|
||||
value={dialogValue ? dayjs(Number(dialogValue)) : null}
|
||||
onChange={(date) => {
|
||||
if (!date) return;
|
||||
setDialogValue(date.valueOf().toString());
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'end',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
{defaultValue !== undefined && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDialogValue(defaultValue);
|
||||
updateSetting(defaultValue, false);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{t('global.button.reset_to_default')}
|
||||
</Button>
|
||||
)}
|
||||
{remove && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDialogValue(undefined);
|
||||
updateSetting(undefined, false);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{t('global.button.remove')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Button onClick={closeDialogWithReset} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateSetting(dialogValue);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -14,14 +14,14 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useContext, useLayoutEffect } from 'react';
|
||||
import { updateMetadataServerSettings, useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { MetadataServerSettingKeys, MetadataServerSettings } from '@/typings.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { MutableListSetting } from '@/components/settings/MutableListSetting.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { MutableListSetting } from '@/modules/core/components/settings/MutableListSetting.tsx';
|
||||
import { NavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
import { ActiveDevice, DEFAULT_DEVICE } from '@/util/device.ts';
|
||||
import { Select } from '@/components/atoms/Select.tsx';
|
||||
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { Select } from '@/modules/core/components/inputs/Select.tsx';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
|
||||
export const DeviceSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
/*
|
||||
* 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 Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useEffect, useState } from 'react';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import List from '@mui/material/List';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { TextSetting, TextSettingProps } from '@/components/settings/text/TextSetting.tsx';
|
||||
import { TextSettingDialog } from '@/components/settings/text/TextSettingDialog.tsx';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
|
||||
const MutableListItem = ({
|
||||
handleDelete,
|
||||
mutable = true,
|
||||
deletable = true,
|
||||
...textSettingProps
|
||||
}: Omit<TextSettingProps, 'isPassword' | 'disabled'> & {
|
||||
handleDelete: () => void;
|
||||
mutable?: boolean;
|
||||
deletable?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack direction="row">
|
||||
{mutable ? (
|
||||
<TextSetting {...textSettingProps} dialogTitle="" />
|
||||
) : (
|
||||
<ListItem>
|
||||
<ListItemText secondary={textSettingProps.value} />
|
||||
</ListItem>
|
||||
)}
|
||||
<Tooltip title={t('chapter.action.download.delete.label.action')}>
|
||||
<IconButton disabled={!deletable} size="large" onClick={handleDelete}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
type MutableListSettingProps = Pick<TextSettingProps, 'settingName' | 'placeholder'> & {
|
||||
valueInfos?: (
|
||||
| [value: string]
|
||||
| [value: string, Pick<React.ComponentProps<typeof MutableListItem>, 'mutable' | 'deletable'>]
|
||||
)[];
|
||||
description?: string;
|
||||
dialogDisclaimer?: JSX.Element | string;
|
||||
addItemButtonTitle?: string;
|
||||
handleChange: (values: string[]) => void;
|
||||
allowDuplicates?: boolean;
|
||||
validateItem?: (value: string) => boolean;
|
||||
invalidItemError?: string;
|
||||
};
|
||||
|
||||
const getValues = (valueInfos: MutableListSettingProps['valueInfos']): string[] =>
|
||||
valueInfos?.map((valueInfo) => valueInfo[0]) ?? [];
|
||||
|
||||
export const MutableListSetting = ({
|
||||
settingName,
|
||||
description,
|
||||
dialogDisclaimer,
|
||||
valueInfos,
|
||||
handleChange,
|
||||
addItemButtonTitle,
|
||||
placeholder,
|
||||
allowDuplicates = false,
|
||||
validateItem = () => true,
|
||||
invalidItemError,
|
||||
}: MutableListSettingProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const values = getValues(valueInfos);
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [dialogValues, setDialogValues] = useState(values);
|
||||
|
||||
const [isAddItemDialogOpen, setIsAddItemDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!valueInfos) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDialogValues(values);
|
||||
}, [valueInfos]);
|
||||
|
||||
const closeDialog = (resetValue: boolean = true) => {
|
||||
if (resetValue) {
|
||||
setDialogValues(values);
|
||||
}
|
||||
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const updateSetting = (index: number, newValue: string | undefined) => {
|
||||
const deleteValue = newValue === undefined;
|
||||
if (deleteValue) {
|
||||
setDialogValues(dialogValues.toSpliced(index, 1));
|
||||
return;
|
||||
}
|
||||
|
||||
const isDuplicate = !allowDuplicates && dialogValues.includes(newValue);
|
||||
if (isDuplicate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateItem(newValue)) {
|
||||
makeToast(invalidItemError ?? t('global.error.label.invalid_input'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setDialogValues(dialogValues.toSpliced(index, 1, newValue.trim()));
|
||||
};
|
||||
|
||||
const saveChanges = () => {
|
||||
closeDialog(true);
|
||||
handleChange(dialogValues.filter((dialogValue) => dialogValue !== ''));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton onClick={() => setIsDialogOpen(true)}>
|
||||
<ListItemText
|
||||
primary={settingName}
|
||||
secondary={values?.length ? values?.join(', ') : description}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
|
||||
<DialogTitle>{settingName}</DialogTitle>
|
||||
{(!!description || !!dialogDisclaimer) && (
|
||||
<DialogContent>
|
||||
<DialogContentText sx={{ paddingBottom: '10px' }} component="div">
|
||||
{description && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
whiteSpace: 'pre-line',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
{dialogDisclaimer && (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<InfoIcon color="warning" />
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
marginLeft: '10px',
|
||||
marginTop: '5px',
|
||||
whiteSpace: 'pre-line',
|
||||
}}
|
||||
>
|
||||
{dialogDisclaimer}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
)}
|
||||
<DialogContent dividers sx={{ maxHeight: '300px' }}>
|
||||
<List>
|
||||
{dialogValues.map((dialogValue, index) => (
|
||||
<MutableListItem
|
||||
settingName=""
|
||||
placeholder={placeholder}
|
||||
handleChange={(newValue: string) => updateSetting(index, newValue)}
|
||||
handleDelete={() => updateSetting(index, undefined)}
|
||||
value={dialogValue}
|
||||
mutable={valueInfos?.find(([value]) => value === dialogValue)?.[1]?.mutable}
|
||||
deletable={valueInfos?.find(([value]) => value === dialogValue)?.[1]?.deletable}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => setIsAddItemDialogOpen(true)}>
|
||||
{addItemButtonTitle ?? t('global.button.add')}
|
||||
</Button>
|
||||
<Stack direction="row">
|
||||
<Button onClick={() => closeDialog()}>{t('global.button.cancel')}</Button>
|
||||
<Button onClick={() => saveChanges()}>{t('global.button.ok')}</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{isAddItemDialogOpen && (
|
||||
<TextSettingDialog
|
||||
settingName=""
|
||||
placeholder={placeholder}
|
||||
handleChange={(newValue: string) => updateSetting(dialogValues.length, newValue)}
|
||||
isDialogOpen={isAddItemDialogOpen}
|
||||
setIsDialogOpen={setIsAddItemDialogOpen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,212 +0,0 @@
|
||||
/*
|
||||
* 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 Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import * as React from 'react';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import Slider from '@mui/material/Slider';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { SxProps, Theme } from '@mui/material/styles';
|
||||
|
||||
type BaseProps = {
|
||||
settingTitle: string;
|
||||
settingValue: string;
|
||||
settingIcon?: React.ReactNode;
|
||||
value: number;
|
||||
defaultValue?: number;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
stepSize?: number;
|
||||
dialogTitle?: string;
|
||||
dialogDescription?: string;
|
||||
dialogDisclaimer?: string;
|
||||
valueUnit: string;
|
||||
handleUpdate: (value: number) => void;
|
||||
showSlider?: never;
|
||||
disabled?: boolean;
|
||||
listItemTextSx?: SxProps<Theme>;
|
||||
handleLiveUpdate?: (value: number) => void;
|
||||
};
|
||||
|
||||
type PropsWithSlider = Omit<BaseProps, 'defaultValue' | 'minValue' | 'maxValue' | 'showSlider'> &
|
||||
Required<Pick<BaseProps, 'defaultValue' | 'minValue' | 'maxValue'>> & { showSlider: true };
|
||||
|
||||
type Props = BaseProps | PropsWithSlider;
|
||||
|
||||
export const NumberSetting = ({
|
||||
settingTitle,
|
||||
settingValue,
|
||||
settingIcon,
|
||||
dialogDescription,
|
||||
dialogDisclaimer,
|
||||
value,
|
||||
defaultValue,
|
||||
minValue,
|
||||
maxValue,
|
||||
stepSize,
|
||||
dialogTitle = settingTitle,
|
||||
valueUnit,
|
||||
handleUpdate,
|
||||
showSlider,
|
||||
disabled = false,
|
||||
handleLiveUpdate,
|
||||
listItemTextSx: sx,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [dialogValue, setDialogValue] = useState(value);
|
||||
const [originalValue, setOriginalValue] = useState(value);
|
||||
|
||||
const updateValue = useCallback(
|
||||
(newValue: number, persist: boolean) => {
|
||||
setDialogValue(newValue);
|
||||
const didValueChange = newValue !== originalValue;
|
||||
// Call handleUpdate if the value changed and 'persist' is true,
|
||||
// otherwise call handleLiveUpdate if it's defined.
|
||||
if (persist && didValueChange) {
|
||||
handleUpdate(newValue);
|
||||
} else if (handleLiveUpdate) {
|
||||
handleLiveUpdate(newValue);
|
||||
}
|
||||
},
|
||||
[originalValue, setDialogValue, handleLiveUpdate, handleUpdate],
|
||||
);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
updateValue(originalValue, true);
|
||||
setOriginalValue(originalValue);
|
||||
setIsDialogOpen(false);
|
||||
}, [originalValue, handleUpdate]);
|
||||
|
||||
const resetToDefault = useCallback(() => {
|
||||
if (defaultValue !== undefined) {
|
||||
updateValue(defaultValue, true);
|
||||
setOriginalValue(defaultValue);
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
}, [defaultValue, handleUpdate]);
|
||||
|
||||
const submit = () => {
|
||||
updateValue(dialogValue, true);
|
||||
setOriginalValue(dialogValue);
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton disabled={disabled} onClick={() => setIsDialogOpen(true)}>
|
||||
{settingIcon ? <ListItemIcon>{settingIcon}</ListItemIcon> : null}
|
||||
<ListItemText
|
||||
primary={settingTitle}
|
||||
secondary={settingValue}
|
||||
sx={sx}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
<Dialog open={isDialogOpen} onClose={cancel}>
|
||||
<DialogContent>
|
||||
<DialogTitle sx={{ paddingLeft: 0 }}>{dialogTitle}</DialogTitle>
|
||||
{(!!dialogDescription || !!dialogDisclaimer) && (
|
||||
<DialogContentText sx={{ paddingBottom: '10px' }} component="div">
|
||||
{dialogDescription && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
whiteSpace: 'pre-line',
|
||||
}}
|
||||
>
|
||||
{dialogDescription}
|
||||
</Typography>
|
||||
)}
|
||||
{dialogDisclaimer && (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<InfoIcon color="warning" />
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
marginLeft: '10px',
|
||||
marginTop: '5px',
|
||||
whiteSpace: 'pre-line',
|
||||
}}
|
||||
>
|
||||
{dialogDisclaimer}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</DialogContentText>
|
||||
)}
|
||||
<TextField
|
||||
sx={{
|
||||
width: '100%',
|
||||
margin: 'auto',
|
||||
}}
|
||||
autoFocus
|
||||
value={dialogValue}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const newValue = Number(e.target.value);
|
||||
updateValue(newValue, false);
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
inputProps: { min: minValue, max: maxValue, step: stepSize },
|
||||
endAdornment: <InputAdornment position="end">{valueUnit}</InputAdornment>,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{showSlider ? (
|
||||
<Slider
|
||||
aria-label="number-setting-slider"
|
||||
defaultValue={defaultValue}
|
||||
value={dialogValue}
|
||||
step={stepSize}
|
||||
min={minValue}
|
||||
max={maxValue}
|
||||
onChange={(_, newValue) => {
|
||||
updateValue(newValue as number, false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{defaultValue !== undefined ? (
|
||||
<Button onClick={resetToDefault} color="primary">
|
||||
{t('global.button.reset_to_default')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button onClick={cancel} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={submit} color="primary">
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
* 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 Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { TranslationKey } from '@/typings.ts';
|
||||
import { Select } from '@/components/atoms/Select.tsx';
|
||||
|
||||
export type SelectSettingValueDisplayInfo = {
|
||||
text: TranslationKey | string;
|
||||
description?: TranslationKey | string;
|
||||
disclaimer?: TranslationKey | string;
|
||||
};
|
||||
|
||||
export type SelectSettingValue<Value> = [Value: Value, DisplayInfo: SelectSettingValueDisplayInfo];
|
||||
|
||||
export const SelectSetting = <SettingValue extends string | number>({
|
||||
settingName,
|
||||
dialogDescription,
|
||||
value,
|
||||
values,
|
||||
handleChange,
|
||||
disabled = false,
|
||||
}: {
|
||||
settingName: string;
|
||||
dialogDescription?: string;
|
||||
value: SettingValue;
|
||||
values: SelectSettingValue<SettingValue>[];
|
||||
handleChange: (value: SettingValue) => void;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [dialogValue, setDialogValue] = useState(value);
|
||||
|
||||
const valueDisplayText = useMemo(() => values.find(([key]) => key === value)?.[1]?.text, [value]);
|
||||
const dialogValueDisplayInfo = useMemo(() => values.find(([key]) => key === dialogValue)![1], [dialogValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDialogValue(value);
|
||||
}, [value]);
|
||||
|
||||
const closeDialog = (resetValue: boolean = true) => {
|
||||
if (resetValue) {
|
||||
setDialogValue(value);
|
||||
}
|
||||
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const updateSetting = () => {
|
||||
closeDialog(false);
|
||||
handleChange(dialogValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton disabled={disabled} onClick={() => setIsDialogOpen(true)}>
|
||||
<ListItemText
|
||||
primary={settingName}
|
||||
secondary={valueDisplayText ? t(valueDisplayText as TranslationKey) : t('global.label.loading')}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
|
||||
<DialogContent>
|
||||
<DialogTitle sx={{ paddingLeft: 0 }}>{settingName}</DialogTitle>
|
||||
{!!dialogDescription && (
|
||||
<DialogContentText sx={{ paddingBottom: '10px' }}>{dialogDescription}</DialogContentText>
|
||||
)}
|
||||
{(!!dialogValueDisplayInfo.description || !!dialogValueDisplayInfo.disclaimer) && (
|
||||
<DialogContentText sx={{ paddingBottom: '10px' }} component="div">
|
||||
{dialogValueDisplayInfo.description && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
whiteSpace: 'pre-line',
|
||||
}}
|
||||
>
|
||||
{t(dialogValueDisplayInfo.description as TranslationKey)}
|
||||
</Typography>
|
||||
)}
|
||||
{dialogValueDisplayInfo.disclaimer && (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<InfoIcon color="warning" />
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
marginLeft: '10px',
|
||||
marginTop: '5px',
|
||||
whiteSpace: 'pre-line',
|
||||
}}
|
||||
>
|
||||
{t(dialogValueDisplayInfo.disclaimer as TranslationKey)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</DialogContentText>
|
||||
)}
|
||||
<FormControl fullWidth>
|
||||
<Select
|
||||
id="dialog-select"
|
||||
value={dialogValue}
|
||||
onChange={(e) => setDialogValue(e.target.value as SettingValue)}
|
||||
>
|
||||
{values.map(([selectValue, { text: selectText }]) => (
|
||||
<MenuItem key={selectValue} value={selectValue}>
|
||||
{t(selectText as TranslationKey)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => closeDialog()} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={() => updateSetting()} color="primary">
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* 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 Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const TimeSetting = ({
|
||||
settingName,
|
||||
value,
|
||||
defaultValue,
|
||||
handleChange,
|
||||
}: {
|
||||
settingName: string;
|
||||
value: string;
|
||||
defaultValue: string;
|
||||
handleChange: (path: string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [dialogValue, setDialogValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDialogValue(value);
|
||||
}, [value]);
|
||||
|
||||
const closeDialog = useCallback(
|
||||
(resetValue: boolean) => {
|
||||
setIsDialogOpen(false);
|
||||
|
||||
if (resetValue) {
|
||||
setDialogValue(value);
|
||||
}
|
||||
},
|
||||
[value],
|
||||
);
|
||||
|
||||
const closeDialogWithReset = useCallback(() => closeDialog(true), [closeDialog]);
|
||||
|
||||
const updateSetting = useCallback(
|
||||
(newValue: string, shouldCloseDialog: boolean = true) => {
|
||||
if (shouldCloseDialog) {
|
||||
closeDialog(false);
|
||||
}
|
||||
|
||||
const didValueChange = value !== newValue;
|
||||
if (!didValueChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleChange(newValue);
|
||||
},
|
||||
[value, handleChange, closeDialog],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton onClick={() => setIsDialogOpen(true)}>
|
||||
<ListItemText
|
||||
primary={settingName}
|
||||
secondary={dayjs(value, 'HH:mm').format('LT')}
|
||||
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
<Dialog open={isDialogOpen} onClose={closeDialog}>
|
||||
<DialogContent>
|
||||
<DialogTitle sx={{ paddingLeft: 0 }}>{settingName}</DialogTitle>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={dayjs.locale()}>
|
||||
<TimePicker
|
||||
value={dayjs(dialogValue, 'HH:mm')}
|
||||
defaultValue={dayjs(defaultValue, 'HH:mm')}
|
||||
format="LT"
|
||||
onChange={(time) => setDialogValue(time?.format('HH:mm') ?? '00:00')}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{defaultValue !== undefined ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDialogValue(defaultValue);
|
||||
updateSetting(defaultValue, false);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{t('global.button.reset_to_default')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button onClick={closeDialogWithReset} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateSetting(dialogValue);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SelectSetting,
|
||||
SelectSettingValue,
|
||||
SelectSettingValueDisplayInfo,
|
||||
} from '@/components/settings/SelectSetting.tsx';
|
||||
} from '@/modules/core/components/settings/SelectSetting.tsx';
|
||||
|
||||
const CHAPTERS_TO_DELETE = [0, 1, 2, 3, 4, 5] as const;
|
||||
const CHAPTERS_TO_DELETE_TO_TRANSLATION_KEY: {
|
||||
|
||||
@@ -11,11 +11,11 @@ import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||
import { getPersistedServerSetting, usePersistedValue } from '@/util/usePersistedValue.tsx';
|
||||
import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx';
|
||||
import { getPersistedServerSetting, usePersistedValue } from '@/modules/core/hooks/usePersistedValue.tsx';
|
||||
import { updateMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { MetadataDownloadSettings, MetadataServerSettings } from '@/typings.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
|
||||
const MIN_LIMIT = 2;
|
||||
const MAX_LIMIT = 10;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* 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 { styled } from '@mui/material/styles';
|
||||
|
||||
export const CheckboxContainer = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxHeight: '170px',
|
||||
overflow: 'auto',
|
||||
});
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
} from '@/components/settings/CategoriesInclusionSetting.tsx';
|
||||
import { GlobalUpdateSettingsEntries } from '@/components/settings/globalUpdate/GlobalUpdateSettingsEntries.tsx';
|
||||
import { GlobalUpdateSettingsInterval } from '@/components/settings/globalUpdate/GlobalUpdateSettingsInterval.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { ServerSettings } from '@/typings.ts';
|
||||
|
||||
type LibrarySettingsType = Pick<ServerSettings, 'updateMangas'>;
|
||||
|
||||
@@ -16,11 +16,12 @@ import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import { ServerSettings, TranslationKey } from '@/typings.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { CheckboxContainer } from '@/components/settings/globalUpdate/CheckboxContainer.ts';
|
||||
import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx';
|
||||
import { ServerSettings } from '@/typings.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { CheckboxContainer } from '@/modules/core/components/inputs/CheckboxContainer.ts';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
type GlobalUpdateSkipEntriesSettings = Pick<
|
||||
ServerSettings,
|
||||
|
||||
@@ -12,9 +12,9 @@ import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { useCallback } from 'react';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||
import { getPersistedServerSetting, usePersistedValue } from '@/util/usePersistedValue.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx';
|
||||
import { getPersistedServerSetting, usePersistedValue } from '@/modules/core/hooks/usePersistedValue.tsx';
|
||||
import { ServerSettings } from '@/typings.ts';
|
||||
|
||||
const DEFAULT_INTERVAL_HOURS = 12;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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 ListItemText from '@mui/material/ListItemText';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import { useState } from 'react';
|
||||
import { TextSettingDialog, TextSettingDialogProps } from '@/components/settings/text/TextSettingDialog.tsx';
|
||||
|
||||
export type TextSettingProps = Omit<TextSettingDialogProps, 'isDialogOpen' | 'setIsDialogOpen' | 'value'> &
|
||||
Required<Pick<TextSettingDialogProps, 'value'>> & {
|
||||
disabled?: boolean;
|
||||
settingDescription?: string;
|
||||
};
|
||||
|
||||
export const TextSetting = (props: TextSettingProps) => {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
const { settingName, settingDescription, value, isPassword = false, disabled = false } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton disabled={disabled} onClick={() => setIsDialogOpen(true)}>
|
||||
<ListItemText
|
||||
primary={settingName}
|
||||
secondary={settingDescription ?? (isPassword ? value.replace(/./g, '*') : value)}
|
||||
secondaryTypographyProps={{
|
||||
sx: { display: 'flex', flexDirection: 'column', wordWrap: 'break-word' },
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
<TextSettingDialog {...props} isDialogOpen={isDialogOpen} setIsDialogOpen={setIsDialogOpen} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* 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 Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PasswordTextField } from '@/components/atoms/PasswordTextField.tsx';
|
||||
|
||||
export type TextSettingDialogProps = {
|
||||
settingName: string;
|
||||
dialogTitle?: string;
|
||||
dialogDescription?: string;
|
||||
value?: string;
|
||||
handleChange: (value: string) => void;
|
||||
isPassword?: boolean;
|
||||
placeholder?: string;
|
||||
isDialogOpen: boolean;
|
||||
setIsDialogOpen: (open: boolean) => void;
|
||||
validate?: (value: string) => boolean;
|
||||
};
|
||||
|
||||
export const TextSettingDialog = ({
|
||||
settingName,
|
||||
dialogTitle = settingName,
|
||||
dialogDescription,
|
||||
value,
|
||||
handleChange,
|
||||
isPassword = false,
|
||||
placeholder = '',
|
||||
isDialogOpen,
|
||||
setIsDialogOpen,
|
||||
validate = () => true,
|
||||
}: TextSettingDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [dialogValue, setDialogValue] = useState(value ?? '');
|
||||
const [isValidValue, setIsValidValue] = useState(true);
|
||||
|
||||
const TextFieldComponent = useMemo(() => (isPassword ? PasswordTextField : TextField), [isPassword]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDialogValue(value);
|
||||
}, [value]);
|
||||
|
||||
const closeDialog = (resetValue: boolean = true) => {
|
||||
if (resetValue) {
|
||||
setDialogValue(value ?? '');
|
||||
}
|
||||
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const updateSetting = () => {
|
||||
closeDialog(false);
|
||||
handleChange(dialogValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isDialogOpen} onClose={() => closeDialog()} fullWidth>
|
||||
<DialogContent>
|
||||
<DialogTitle sx={{ paddingLeft: 0 }}>{dialogTitle}</DialogTitle>
|
||||
{!!dialogDescription && (
|
||||
<DialogContentText sx={{ paddingBottom: '10px' }}>{dialogDescription}</DialogContentText>
|
||||
)}
|
||||
<TextFieldComponent
|
||||
sx={{
|
||||
width: '100%',
|
||||
margin: 'auto',
|
||||
}}
|
||||
autoFocus
|
||||
placeholder={placeholder}
|
||||
value={dialogValue}
|
||||
error={!isValidValue}
|
||||
helperText={!isValidValue ? t('global.error.label.invalid_input') : ''}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
|
||||
setIsValidValue(validate(newValue));
|
||||
setDialogValue(newValue);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => closeDialog()} color="primary">
|
||||
{t('global.button.cancel')}
|
||||
</Button>
|
||||
<Button onClick={() => updateSetting()} disabled={!isValidValue} color="primary">
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -12,11 +12,11 @@ import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { useCallback } from 'react';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||
import { getPersistedServerSetting, usePersistedValue } from '@/util/usePersistedValue.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx';
|
||||
import { getPersistedServerSetting, usePersistedValue } from '@/modules/core/hooks/usePersistedValue.tsx';
|
||||
import { ServerSettings } from '@/typings.ts';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
|
||||
const DEFAULT_VALUE = 23;
|
||||
const MIN_VALUE = 1;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
||||
import { GridLayouts } from '@/components/source/GridLayouts.tsx';
|
||||
import { useLocalStorage } from '@/util/useStorage.tsx';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
|
||||
export function SourceGridLayout() {
|
||||
const [sourceGridLayout, setSourceGridLayout] = useLocalStorage('source-grid-layout', GridLayout.Compact);
|
||||
|
||||
@@ -25,7 +25,7 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { ISourceMetadata, SourceFilters } from '@/typings';
|
||||
import { OptionsPanel } from '@/components/molecules/OptionsPanel';
|
||||
import { OptionsPanel } from '@/modules/core/components/OptionsPanel.tsx';
|
||||
import { CheckBoxFilter } from '@/components/source/filters/CheckBoxFilter';
|
||||
import { HeaderFilter } from '@/components/source/filters/HeaderFilter';
|
||||
import { SelectFilter } from '@/components/source/filters/SelectFilter';
|
||||
@@ -36,9 +36,9 @@ import { TriStateFilter } from '@/components/source/filters/TriStateFilter';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { GroupFilter } from '@/components/source/filters/GroupFilter';
|
||||
import { SeparatorFilter } from '@/components/source/filters/SeparatorFilter';
|
||||
import { StyledFab } from '@/components/util/StyledFab';
|
||||
import { StyledFab } from '@/modules/core/components/buttons/StyledFab.tsx';
|
||||
import { awaitConfirmation } from '@/lib/ui/AwaitableDialog.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
|
||||
interface IFilters {
|
||||
sourceFilter: SourceFilters[];
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CheckboxInput } from '@/components/atoms/CheckboxInput';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
|
||||
interface Props {
|
||||
state: boolean;
|
||||
|
||||
@@ -14,7 +14,7 @@ import ListItemText from '@mui/material/ListItemText';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Box from '@mui/material/Box';
|
||||
import React from 'react';
|
||||
import { SortRadioInput } from '@/components/atoms/SortRadioInput';
|
||||
import { SortRadioInput } from '@/modules/core/components/inputs/SortRadioInput.tsx';
|
||||
import { SortSelectionInput } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -12,7 +12,7 @@ import Input from '@mui/material/Input';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDebounce } from '@/util/useDebounce.ts';
|
||||
import { useDebounce } from '@/modules/core/hooks/useDebounce.ts';
|
||||
|
||||
interface Props {
|
||||
state: string;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput';
|
||||
import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { TriState } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
index: any;
|
||||
currentIndex: any;
|
||||
}
|
||||
|
||||
export function TabPanel(props: IProps) {
|
||||
const { children, index, currentIndex } = props;
|
||||
|
||||
return (
|
||||
<div role="tabpanel" hidden={index !== currentIndex} id={`simple-tabpanel-${index}`}>
|
||||
{currentIndex === index && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* 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 Tabs, { TabsProps } from '@mui/material/Tabs';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { ForwardedRef, forwardRef } from 'react';
|
||||
import { useNavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
|
||||
const StyledTabsMenu = styled(Tabs)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: 0,
|
||||
borderBottomWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}));
|
||||
|
||||
export const TabsMenu = forwardRef(
|
||||
({ children, sx, ...props }: TabsProps, ref: ForwardedRef<HTMLDivElement | null>) => {
|
||||
const { appBarHeight } = useNavBarContext();
|
||||
|
||||
return (
|
||||
<StyledTabsMenu
|
||||
sx={{ ...sx, top: appBarHeight }}
|
||||
ref={ref}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons
|
||||
allowScrollButtonsMobile
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</StyledTabsMenu>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* 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 Box, { BoxProps } from '@mui/material/Box';
|
||||
import { useNavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
|
||||
export const TabsWrapper = ({ children, ...props }: BoxProps) => {
|
||||
const { appBarHeight } = useNavBarContext();
|
||||
|
||||
return (
|
||||
<Box {...props} sx={{ ...props.sx, position: 'relative', height: `calc(100% - ${appBarHeight}px)` }}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -21,9 +21,9 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { PasswordTextField } from '@/components/atoms/PasswordTextField.tsx';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { PasswordTextField } from '@/modules/core/components/inputs/PasswordTextField.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { Trackers, TTrackerSearch } from '@/lib/data/Trackers.ts';
|
||||
|
||||
export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) => {
|
||||
|
||||
@@ -11,13 +11,13 @@ import Box from '@mui/material/Box';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { Trackers } from '@/lib/data/Trackers.ts';
|
||||
import { TrackerCard, TrackerMode } from '@/components/tracker/TrackerCard.tsx';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { GetMangaTrackRecordsQuery, GetTrackersBindQuery, MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_TRACKERS_BIND } from '@/lib/graphql/queries/TrackerQuery.ts';
|
||||
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
|
||||
|
||||
@@ -28,17 +28,17 @@ import Typography from '@mui/material/Typography';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import PopupState, { bindDialog, bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { Trackers, TTrackerBind, TTrackRecordBind, UNSET_DATE } from '@/lib/data/Trackers.ts';
|
||||
import { ListPreference } from '@/components/sourceConfiguration/ListPreference.tsx';
|
||||
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||
import { DateSetting } from '@/components/settings/DateSetting.tsx';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { Menu } from '@/components/menu/Menu';
|
||||
import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx';
|
||||
import { DateSetting } from '@/modules/core/components/settings/DateSetting.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { Menu } from '@/modules/core/components/menu/Menu';
|
||||
import { CARD_STYLING } from '@/components/tracker/constants.ts';
|
||||
import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines.tsx';
|
||||
import { SelectSetting, SelectSettingValue } from '@/components/settings/SelectSetting.tsx';
|
||||
import { CheckboxInput } from '@/components/atoms/CheckboxInput';
|
||||
import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx';
|
||||
import { SelectSetting, SelectSettingValue } from '@/modules/core/components/settings/SelectSetting.tsx';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
import { TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
const TrackerActiveLink = ({ children, url }: { children: React.ReactNode; url: string }) => (
|
||||
|
||||
@@ -22,9 +22,9 @@ import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import parseHtml from 'html-react-parser';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
|
||||
import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines.tsx';
|
||||
import { Metadata } from '@/components/atoms/Metadata.tsx';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx';
|
||||
import { Metadata } from '@/modules/core/components/Metadata.tsx';
|
||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||
import { TTrackerManga } from '@/lib/data/Trackers.ts';
|
||||
|
||||
|
||||
@@ -21,16 +21,16 @@ import InfoIcon from '@mui/icons-material/Info';
|
||||
import PopupState, { bindPopover, bindTrigger } from 'material-ui-popup-state';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { Tracker, TTrackerBase } from '@/lib/data/Trackers.ts';
|
||||
import { SearchTextField } from '@/components/atoms/SearchTextField.tsx';
|
||||
import { makeToast } from '@/components/util/Toast.tsx';
|
||||
import { SearchTextField } from '@/modules/core/components/inputs/SearchTextField.tsx';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { TrackerMangaCard } from '@/components/tracker/TrackerMangaCard.tsx';
|
||||
import { DIALOG_PADDING } from '@/components/tracker/constants.ts';
|
||||
import { useGetOptionForDirection } from '@/theme.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import Avatar from '@mui/material/Avatar';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { CARD_STYLING } from '@/components/tracker/constants.ts';
|
||||
import { TTrackerBase } from '@/lib/data/Trackers.ts';
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
* 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 React, { useState, useEffect } from 'react';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { useQueryParam, StringParam } from 'use-query-params';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { SearchTextField } from '@/components/atoms/SearchTextField.tsx';
|
||||
|
||||
interface IProps {
|
||||
isClosable?: boolean;
|
||||
}
|
||||
|
||||
export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
|
||||
const { isClosable = true } = props;
|
||||
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [prevLocationKey, setPrevLocationKey] = useState<string>();
|
||||
const location = useLocation();
|
||||
|
||||
const [query, setQuery] = useQueryParam('query', StringParam);
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(!isClosable || !!query);
|
||||
const inputRef = React.useRef<HTMLInputElement>();
|
||||
|
||||
const [searchString, setSearchString] = useState(query ?? '');
|
||||
|
||||
if (prevLocationKey !== location.key) {
|
||||
setPrevLocationKey(location.key);
|
||||
setSearchString(query ?? '');
|
||||
setIsSearchOpen(!isClosable || !!query);
|
||||
}
|
||||
|
||||
const isOpen = isSearchOpen || !!query;
|
||||
|
||||
const updateSearchOpenState = (open: boolean) => {
|
||||
if (!isClosable) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearchOpen(open);
|
||||
|
||||
// try to focus input component since in case of navigating to the previous/next page in the browser history
|
||||
// the "openSearch" state might not change and thus, won't trigger a focus
|
||||
if (open) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
function handleChange(newQuery: string) {
|
||||
if (newQuery === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
setQuery(newQuery);
|
||||
updateSearchOpenState(false);
|
||||
}
|
||||
|
||||
const cancelSearch = () => {
|
||||
setSearchString('');
|
||||
setQuery(undefined);
|
||||
updateSearchOpenState(false);
|
||||
};
|
||||
const handleBlur = () => {
|
||||
if (!searchString) updateSearchOpenState(false);
|
||||
};
|
||||
|
||||
const handleKeyboardEvent = (e: KeyboardEvent) => {
|
||||
if (e.key === 'F3' || (e.ctrlKey && e.key === 'f')) {
|
||||
e.preventDefault();
|
||||
updateSearchOpenState(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyboardEvent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyboardEvent);
|
||||
};
|
||||
}, [handleKeyboardEvent]);
|
||||
|
||||
if (isOpen) {
|
||||
return (
|
||||
<SearchTextField
|
||||
autoFocus
|
||||
variant="standard"
|
||||
value={searchString}
|
||||
onCancel={cancelSearch}
|
||||
onChange={(e) => setSearchString(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleChange(searchString);
|
||||
}
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
inputRef={inputRef}
|
||||
sx={{
|
||||
...theme.applyStyles('light', {
|
||||
'& .MuiInput-underline:before': {
|
||||
borderBottomColor: 'primary.contrastText', // Default color
|
||||
},
|
||||
'& .MuiInput-underline:hover:before': {
|
||||
borderBottomColor: 'primary.contrastText', // Hover color
|
||||
},
|
||||
'& .MuiInput-underline:after': {
|
||||
borderBottomColor: 'primary.dark', // Focused color
|
||||
},
|
||||
}),
|
||||
}}
|
||||
cancelButtonProps={{ sx: { ...theme.applyStyles('light', { color: 'primary.contrastText' }) } }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={t('search.title.search')}>
|
||||
<IconButton onClick={() => updateSearchOpenState(true)} color="inherit">
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
* 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 { createSvgIcon } from '@mui/material/utils';
|
||||
|
||||
const d =
|
||||
'M 11 1 C 9.3550302 1 8 2.3550302 8 4 L 4 4 C 2.9069372 4 2 4.9069372 2 6 L 2 12 L 4 12 C 4.5650302 12 5 12.43497 5 13 C 5 13.56503 4.5650302 14 4 14 L 2 14 L 2 20 C 2 21.093063 2.9069372 22 4 22 L 10 22 L 10 20 C 10 19.43497 10.43497 19 11 19 C 11.56503 19 12 19.43497 12 20 L 12 22 L 18 22 C 19.093063 22 20 21.093063 20 20 L 20 16 C 21.64497 16 23 14.64497 23 13 C 23 11.35503 21.64497 10 20 10 L 20 6 C 20 4.9069372 19.093063 4 18 4 L 14 4 C 14 2.3550302 12.64497 1 11 1 z M 11 3 C 11.56503 3 12 3.4349698 12 4 L 12 6 L 18 6 L 18 12 L 20 12 C 20.56503 12 21 12.43497 21 13 C 21 13.56503 20.56503 14 20 14 L 18 14 L 18 20 L 14 20 C 14 18.35503 12.64497 17 11 17 C 9.3550302 17 8 18.35503 8 20 L 4 20 L 4 16 C 5.6449698 16 7 14.64497 7 13 C 7 11.35503 5.6449698 10 4 10 L 4 6 L 10 6 L 10 4 C 10 3.4349698 10.43497 3 11 3 z';
|
||||
|
||||
export const ExtensionOutlinedIcon = createSvgIcon(<path d={d} />, 'CustomExtensionOutlined');
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { SxProps, Theme } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
const ERROR_FACES = ['(・o・;)', 'Σ(ಠ_ಠ)', 'ಥ_ಥ', '(˘・_・˘)', '(; ̄Д ̄)', '(・Д・。'];
|
||||
|
||||
function getRandomErrorFace() {
|
||||
const randIndex = Math.floor(Math.random() * ERROR_FACES.length);
|
||||
return ERROR_FACES[randIndex];
|
||||
}
|
||||
|
||||
export interface EmptyViewProps {
|
||||
message: string;
|
||||
messageExtra?: JSX.Element | string;
|
||||
retry?: () => void;
|
||||
noFaces?: boolean;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export function EmptyView({ message, messageExtra, retry, noFaces, sx }: EmptyViewProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const errorFace = useMemo(() => getRandomErrorFace(), []);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{!noFaces && (
|
||||
<Typography variant="h3" gutterBottom>
|
||||
{errorFace}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="h5">{message}</Typography>
|
||||
{messageExtra}
|
||||
{retry && <Button onClick={retry}>{t('global.button.retry')}</Button>}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* 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 { EmptyView, EmptyViewProps } from '@/components/util/EmptyView.tsx';
|
||||
|
||||
export function EmptyViewAbsoluteCentered({ sx, ...emptyViewProps }: EmptyViewProps) {
|
||||
return (
|
||||
<EmptyView
|
||||
{...emptyViewProps}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
height: undefined,
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
* 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 { Link } from 'react-router-dom';
|
||||
import ListItemButton, { ListItemButtonProps } from '@mui/material/ListItemButton';
|
||||
|
||||
export function ListItemLink(props: ListItemButtonProps<typeof Link>) {
|
||||
return <ListItemButton component={Link} {...props} />;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
interface IProps {
|
||||
shouldRender?: boolean | (() => boolean);
|
||||
children?: React.ReactNode;
|
||||
component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any>;
|
||||
componentProps?: any;
|
||||
usePadding?: boolean;
|
||||
}
|
||||
|
||||
export function LoadingPlaceholder(props: IProps) {
|
||||
const { children, shouldRender, component, componentProps, usePadding } = props;
|
||||
|
||||
let condition = true;
|
||||
if (shouldRender !== undefined) {
|
||||
condition = shouldRender instanceof Function ? shouldRender() : shouldRender;
|
||||
}
|
||||
|
||||
if (condition) {
|
||||
if (component) {
|
||||
return React.createElement(component, componentProps);
|
||||
}
|
||||
|
||||
if (children) {
|
||||
return children as JSX.Element;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
margin: '0px auto',
|
||||
marginTop: usePadding ? 'unset' : '10px',
|
||||
marginBottom: usePadding ? 'unset' : '10px',
|
||||
padding: usePadding ? '10px 0' : 'unset',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<CircularProgress thickness={5} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import CircularProgress, { CircularProgressProps } from '@mui/material/CircularProgress';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export const Progress = ({
|
||||
progress,
|
||||
showText = true,
|
||||
progressProps = {},
|
||||
}: {
|
||||
progress: number;
|
||||
showText?: boolean;
|
||||
progressProps?: CircularProgressProps;
|
||||
}) => (
|
||||
<Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}>
|
||||
<CircularProgress {...progressProps} variant="determinate" value={progress} />
|
||||
{showText && (
|
||||
<Box sx={{ position: 'absolute' }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>{`${Math.round(progress)}%`}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -14,12 +14,12 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { getVersion } from '@/screens/settings/About.tsx';
|
||||
import { useUpdateChecker } from '@/util/useUpdateChecker.tsx';
|
||||
import { VersionUpdateInfoDialog } from '@/components/util/VersionUpdateInfoDialog.tsx';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { useLocalStorage } from '@/util/useStorage.tsx';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
|
||||
const disabledUpdateCheck = () => Promise.resolve();
|
||||
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* 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 { useState, CSSProperties, useEffect, forwardRef, ForwardedRef } from 'react';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Button from '@mui/material/Button';
|
||||
import BrokenImageIcon from '@mui/icons-material/BrokenImage';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ImageIcon from '@mui/icons-material/Image';
|
||||
import { SxProps, Theme } from '@mui/material/styles';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { Priority } from '@/lib/Queue.ts';
|
||||
|
||||
interface IProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
|
||||
spinnerStyle?: SxProps<Theme> & { small?: boolean };
|
||||
imgStyle?: CSSProperties;
|
||||
|
||||
onImageLoad?: () => void;
|
||||
|
||||
useFetchApi?: boolean;
|
||||
}
|
||||
|
||||
export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTMLImageElement | null>) => {
|
||||
const { useFetchApi, src, alt, onImageLoad, spinnerStyle: { small, ...spinnerStyle } = {}, imgStyle } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showMissingImageIcon = !src.length;
|
||||
|
||||
const [imageSourceUrl, setImageSourceUrl] = useState('');
|
||||
const [imgLoadRetryKey, setImgLoadRetryKey] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean | undefined>(undefined);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const updateImageState = (loading: boolean, error: boolean = false, aborted: boolean = false) => {
|
||||
setIsLoading(loading);
|
||||
setHasError(error);
|
||||
|
||||
if (!loading && !error && !aborted) {
|
||||
onImageLoad?.();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showMissingImageIcon) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const imageRequest = requestManager.requestImage(src, Priority.HIGH, useFetchApi);
|
||||
let cacheTimeout: NodeJS.Timeout;
|
||||
|
||||
const fetchImage = async () => {
|
||||
try {
|
||||
const updateImage = async () => {
|
||||
const image = await imageRequest.response;
|
||||
|
||||
updateImageState(false);
|
||||
setImageSourceUrl(image);
|
||||
};
|
||||
|
||||
const checkCache = await Promise.race([
|
||||
imageRequest.response,
|
||||
new Promise((resolve) => {
|
||||
cacheTimeout = setTimeout(resolve, 50);
|
||||
}),
|
||||
]);
|
||||
const isImageCached = !!checkCache;
|
||||
|
||||
if (isImageCached) {
|
||||
await updateImage();
|
||||
return;
|
||||
}
|
||||
|
||||
updateImageState(true);
|
||||
await updateImage();
|
||||
} catch (e) {
|
||||
const wasAborted =
|
||||
e instanceof Error && (e.name === 'AbortError' || e.message === 'Component was unmounted');
|
||||
updateImageState(false, !wasAborted, wasAborted);
|
||||
}
|
||||
};
|
||||
|
||||
fetchImage().catch(() => {});
|
||||
|
||||
return () => {
|
||||
imageRequest.cleanup();
|
||||
clearTimeout(cacheTimeout);
|
||||
imageRequest.abortRequest(new Error('Component was unmounted'));
|
||||
};
|
||||
}, [src, imgLoadRetryKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isLoading || hasError) && (
|
||||
<Box sx={{ height: '100%', ...spinnerStyle }}>
|
||||
<Stack
|
||||
sx={{
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{isLoading && <CircularProgress thickness={5} />}
|
||||
{hasError && isLoading === false && (
|
||||
<>
|
||||
<BrokenImageIcon />
|
||||
<Button
|
||||
startIcon={!small && <RefreshIcon />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setImgLoadRetryKey((prevState) => (prevState + 1) % 100);
|
||||
}}
|
||||
size={small ? 'small' : 'large'}
|
||||
>
|
||||
{small ? <RefreshIcon /> : t('global.button.retry')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showMissingImageIcon ? (
|
||||
<Stack
|
||||
sx={{
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: (theme) => theme.palette.background.default,
|
||||
...spinnerStyle,
|
||||
}}
|
||||
>
|
||||
<ImageIcon fontSize="large" />
|
||||
</Stack>
|
||||
) : (
|
||||
<img
|
||||
key={`${src}_${imgLoadRetryKey}`}
|
||||
style={{
|
||||
...imgStyle,
|
||||
display: !imageSourceUrl || isLoading || hasError ? 'none' : imgStyle?.display,
|
||||
}}
|
||||
ref={imgRef}
|
||||
src={imageSourceUrl}
|
||||
alt={alt}
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* 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 Fab from '@mui/material/Fab';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
export const DEFAULT_FAB_STYLE = {
|
||||
position: 'fixed',
|
||||
height: '48px',
|
||||
right: '48px',
|
||||
bottom: '28px',
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_FULL_FAB_HEIGHT = `calc(${DEFAULT_FAB_STYLE.bottom} + ${DEFAULT_FAB_STYLE.height})`;
|
||||
|
||||
export const StyledFab = styled(Fab)({
|
||||
...DEFAULT_FAB_STYLE,
|
||||
}) as typeof Fab;
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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 { enqueueSnackbar, OptionsObject, SnackbarKey } from 'notistack';
|
||||
|
||||
export function makeToast(message: string, severity?: OptionsObject['variant']): SnackbarKey;
|
||||
export function makeToast(message: string, options?: OptionsObject): SnackbarKey;
|
||||
export function makeToast(message: string, options: OptionsObject['variant'] | OptionsObject = 'default'): SnackbarKey {
|
||||
if (typeof options === 'string') {
|
||||
return enqueueSnackbar(message, { variant: options });
|
||||
}
|
||||
|
||||
return enqueueSnackbar(message, options);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user