Move core files into new folder

This commit is contained in:
schroda
2024-10-05 16:09:34 +02:00
parent 996bb62888
commit 23a86a77f0
173 changed files with 514 additions and 481 deletions

View File

@@ -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;

View File

@@ -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';

View File

@@ -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) => (

View File

@@ -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();

View File

@@ -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;

View File

@@ -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';

View File

@@ -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} />
);

View File

@@ -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>
),
);

View File

@@ -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>
);

View File

@@ -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}
/>
);
};

View File

@@ -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} />
);

View File

@@ -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>
),
},
}}
/>
);

View File

@@ -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>
);

View File

@@ -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}
/>
));

View File

@@ -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}
/>
);
};

View File

@@ -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} />
);

View File

@@ -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 }>;

View File

@@ -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 };

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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',

View File

@@ -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';

View File

@@ -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>
);
}

View File

@@ -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>,

View File

@@ -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;

View File

@@ -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',

View File

@@ -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',

View File

@@ -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';

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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) => {

View File

@@ -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>
),
);

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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> = [

View File

@@ -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;

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>
</>
);
}

View File

@@ -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',

View File

@@ -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[] }) => {

View File

@@ -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';

View File

@@ -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 =>

View File

@@ -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;

View File

@@ -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++) {

View File

@@ -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++) {

View File

@@ -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,

View File

@@ -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>
</>
);
};

View File

@@ -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();

View File

@@ -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}
/>
)}
</>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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: {

View File

@@ -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;

View File

@@ -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',
});

View File

@@ -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'>;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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} />
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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);

View File

@@ -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[];

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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>
);
}

View File

@@ -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>
);
},
);

View File

@@ -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>
);
};

View File

@@ -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 }) => {

View File

@@ -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';

View File

@@ -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 }) => (

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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>
);
};

View File

@@ -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');

View File

@@ -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>
);
}

View File

@@ -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,
}}
/>
);
}

View File

@@ -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} />;
}

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -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();

View File

@@ -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}
/>
)}
</>
);
});

View File

@@ -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;

View File

@@ -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