refactor
This commit is contained in:
59
src/components/util/EmptyView.tsx
Normal file
59
src/components/util/EmptyView.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
|
||||
const ERROR_FACES = [
|
||||
'(・o・;)',
|
||||
'Σ(ಠ_ಠ)',
|
||||
'ಥ_ಥ',
|
||||
'(˘・_・˘)',
|
||||
'(; ̄Д ̄)',
|
||||
'(・Д・。',
|
||||
];
|
||||
|
||||
function getRandomErrorFace() {
|
||||
const randIndex = Math.floor(Math.random() * ERROR_FACES.length);
|
||||
return ERROR_FACES[randIndex];
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
message: string
|
||||
messageExtra?: JSX.Element
|
||||
}
|
||||
|
||||
export default function EmptyView({ message, messageExtra }: IProps) {
|
||||
const theme = useTheme();
|
||||
const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: `calc(50% + ${isMobileWidth ? '0px' : theme.spacing(8 / 2)})`,
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h3" gutterBottom>
|
||||
{getRandomErrorFace()}
|
||||
</Typography>
|
||||
<Typography variant="h5">
|
||||
{message}
|
||||
</Typography>
|
||||
{messageExtra}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EmptyView.defaultProps = {
|
||||
messageExtra: undefined,
|
||||
};
|
||||
59
src/components/util/LoadingPlaceholder.tsx
Normal file
59
src/components/util/LoadingPlaceholder.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/require-default-props */
|
||||
/*
|
||||
* 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 makeStyles from '@mui/styles/makeStyles';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
loading: {
|
||||
margin: '10px auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
shouldRender?: boolean | (() => boolean)
|
||||
children?: React.ReactNode
|
||||
component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any>
|
||||
componentProps?: any
|
||||
}
|
||||
|
||||
export default function LoadingPlaceholder(props: IProps) {
|
||||
const {
|
||||
children, shouldRender, component, componentProps,
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
|
||||
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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.loading}>
|
||||
<CircularProgress thickness={5} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
src/components/util/SpinnerImage.tsx
Normal file
76
src/components/util/SpinnerImage.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
|
||||
interface IProps {
|
||||
src: string
|
||||
alt: string
|
||||
|
||||
imgRef?: React.RefObject<HTMLImageElement>
|
||||
|
||||
spinnerClassName?: string
|
||||
imgClassName?: string
|
||||
|
||||
onImageLoad?: () => void
|
||||
}
|
||||
|
||||
export default function SpinnerImage(props: IProps) {
|
||||
const {
|
||||
src, alt, onImageLoad, imgRef, spinnerClassName, imgClassName,
|
||||
} = props;
|
||||
const [imageSrc, setImagsrc] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
|
||||
img.onload = () => {
|
||||
setImagsrc(src);
|
||||
onImageLoad?.();
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
// Setting to an actual image so CSS styling works consistently
|
||||
setImagsrc('/notFound.svg');
|
||||
};
|
||||
|
||||
return () => {
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
if (imageSrc.length === 0) {
|
||||
return (
|
||||
<div className={spinnerClassName}>
|
||||
<CircularProgress thickness={5} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (imageSrc === 'Not Found') {
|
||||
return <div className={spinnerClassName} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={imgClassName}
|
||||
ref={imgRef}
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SpinnerImage.defaultProps = {
|
||||
spinnerClassName: '',
|
||||
imgClassName: '',
|
||||
onImageLoad: () => {},
|
||||
imgRef: undefined,
|
||||
};
|
||||
90
src/components/util/ThreeStateCheckbox.tsx
Normal file
90
src/components/util/ThreeStateCheckbox.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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, createSvgIcon } from '@mui/material';
|
||||
import React, {
|
||||
useEffect, useState,
|
||||
} from 'react';
|
||||
|
||||
export interface IThreeStateCheckboxProps {
|
||||
name: string
|
||||
checked: boolean | undefined | null
|
||||
onChange: (change: boolean | undefined | null) => void
|
||||
}
|
||||
|
||||
enum CheckState {
|
||||
SELECTED, INTERMEDIATE, UNSELECTED,
|
||||
}
|
||||
|
||||
function checkedToState(checked: boolean | undefined | null): CheckState {
|
||||
switch (checked) {
|
||||
case true:
|
||||
return CheckState.SELECTED;
|
||||
case false:
|
||||
return CheckState.INTERMEDIATE;
|
||||
default:
|
||||
return CheckState.UNSELECTED;
|
||||
}
|
||||
}
|
||||
function stateToChecked(state: CheckState): boolean | undefined {
|
||||
switch (state) {
|
||||
case CheckState.SELECTED:
|
||||
return true;
|
||||
case CheckState.INTERMEDIATE:
|
||||
return false;
|
||||
default:
|
||||
case CheckState.UNSELECTED:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function stateTransition(state: CheckState): CheckState {
|
||||
switch (state) {
|
||||
case CheckState.SELECTED:
|
||||
return CheckState.INTERMEDIATE;
|
||||
case CheckState.INTERMEDIATE:
|
||||
return CheckState.UNSELECTED;
|
||||
case CheckState.UNSELECTED:
|
||||
default:
|
||||
return CheckState.SELECTED;
|
||||
}
|
||||
}
|
||||
|
||||
const ThreeStateCheckbox = (props: IThreeStateCheckboxProps) => {
|
||||
const {
|
||||
name, checked, onChange,
|
||||
} = props;
|
||||
const [localChecked, setLocalChecked] = useState(checkedToState(checked));
|
||||
useEffect(() => setLocalChecked(checkedToState(checked)), [checked]);
|
||||
const handleChange = () => {
|
||||
setLocalChecked(stateTransition(localChecked));
|
||||
if (onChange) {
|
||||
onChange(stateToChecked(stateTransition(localChecked)));
|
||||
}
|
||||
};
|
||||
const CancelBox = createSvgIcon(
|
||||
<>
|
||||
<path
|
||||
d="M 19 6.41 L 13.41 12 L 19 17.59 L 17.59 19 L 12 13.41 L 6.41 19 V 19 H 6.41 L 5 17.59 L 11 12 L 5 6.41 L 6.41 5 L 12 10.59 L 17.59 5 L 19 6.41 M 5 5 m 0 -2 H 5 c -1.1 0 -2 0.9 -2 2 v 14 c 0 1.1 0.9 2 2 2 h 14 c 1.1 0 2 -0.9 2 -2 V 5 c 0 -1.1 -0.9 -2 -2 -2 z "
|
||||
/>
|
||||
</>,
|
||||
'CancelBox',
|
||||
);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
name={name}
|
||||
checked={localChecked === CheckState.SELECTED}
|
||||
indeterminate={localChecked === CheckState.INTERMEDIATE}
|
||||
indeterminateIcon={<CancelBox />}
|
||||
onChange={handleChange}
|
||||
className={`${localChecked}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ThreeStateCheckbox;
|
||||
76
src/components/util/Toast.tsx
Normal file
76
src/components/util/Toast.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import MuiAlert, { AlertColor as Severity } from '@mui/material/Alert';
|
||||
|
||||
function removeToast(id: string) {
|
||||
const container = document.querySelector(`#${id}`)!!;
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
|
||||
function Transition(props: SlideProps) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <Slide {...props} direction="up" />;
|
||||
}
|
||||
|
||||
interface IToastProps{
|
||||
message: string
|
||||
severity: Severity
|
||||
}
|
||||
|
||||
export function Toast(props: IToastProps) {
|
||||
const { message, severity } = props;
|
||||
const [open, setOpen] = React.useState(true);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
autoHideDuration={3000}
|
||||
TransitionComponent={Transition}
|
||||
message="I love snacks"
|
||||
>
|
||||
<MuiAlert elevation={6} variant="filled" onClose={handleClose} severity={severity}>
|
||||
{message}
|
||||
</MuiAlert>
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
||||
|
||||
export default function makeToast(message: string, severity: Severity) {
|
||||
const id = Math.floor(Math.random() * 1000);
|
||||
const container = document.createElement('div');
|
||||
container.id = `alert-${id}`;
|
||||
|
||||
document.body.appendChild(container);
|
||||
|
||||
ReactDOM.render(<Toast message={message} severity={severity} />, container);
|
||||
|
||||
setTimeout(() => removeToast(container.id), 3500);
|
||||
}
|
||||
|
||||
export function makeToaster(
|
||||
[toasts, setToasts] : [React.ReactElement[],
|
||||
(arg0: React.ReactElement[]) => void],
|
||||
): [React.ReactElement[], ((message: string, severity: Severity) => void)] {
|
||||
return [toasts, (message: string, severity: Severity) => {
|
||||
setToasts([<Toast
|
||||
key={Math.floor(Math.random() * 1000) + 1}
|
||||
message={message}
|
||||
severity={severity}
|
||||
/>]);
|
||||
}];
|
||||
}
|
||||
Reference in New Issue
Block a user