parent
56729e0dbb
commit
d7533bac57
@ -0,0 +1,225 @@
|
||||
import {
|
||||
faBug,
|
||||
faCircleNotch,
|
||||
faExclamationTriangle,
|
||||
faInfoCircle,
|
||||
faStream,
|
||||
IconDefinition,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconProps,
|
||||
} from "@fortawesome/react-fontawesome";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Overlay,
|
||||
ProgressBar,
|
||||
Tooltip,
|
||||
} from "react-bootstrap";
|
||||
import { useDidUpdate } from "rooks";
|
||||
import { useReduxStore } from "../@redux/hooks/base";
|
||||
import { BuildKey, useIsArrayExtended } from "../utilites";
|
||||
import "./notification.scss";
|
||||
|
||||
enum State {
|
||||
Idle,
|
||||
Working,
|
||||
Failed,
|
||||
}
|
||||
|
||||
function useTotalProgress(progress: Server.Progress[]) {
|
||||
return useMemo(() => {
|
||||
const { value, count } = progress.reduce(
|
||||
(prev, { value, count }) => {
|
||||
prev.value += value;
|
||||
prev.count += count;
|
||||
return prev;
|
||||
},
|
||||
{ value: 0, count: 0 }
|
||||
);
|
||||
|
||||
if (count === 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return value / count;
|
||||
}
|
||||
}, [progress]);
|
||||
}
|
||||
|
||||
function useHasErrorNotification(notifications: Server.Notification[]) {
|
||||
return useMemo(
|
||||
() => notifications.find((v) => v.type !== "info") !== undefined,
|
||||
[notifications]
|
||||
);
|
||||
}
|
||||
|
||||
const NotificationCenter: FunctionComponent = () => {
|
||||
const { progress, notifications } = useReduxStore((s) => s.site);
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [hasNew, setHasNew] = useState(false);
|
||||
|
||||
const hasNewProgress = useIsArrayExtended(progress);
|
||||
const hasNewNotifications = useIsArrayExtended(notifications);
|
||||
useDidUpdate(() => {
|
||||
if (hasNewNotifications || hasNewProgress) {
|
||||
setHasNew(true);
|
||||
}
|
||||
}, [hasNewProgress, hasNewNotifications]);
|
||||
|
||||
const [btnState, setBtnState] = useState(State.Idle);
|
||||
|
||||
const totalProgress = useTotalProgress(progress);
|
||||
const hasError = useHasErrorNotification(notifications);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasError) {
|
||||
setBtnState(State.Failed);
|
||||
} else if (totalProgress > 0) {
|
||||
setBtnState(State.Working);
|
||||
} else if (totalProgress <= 0) {
|
||||
setBtnState(State.Idle);
|
||||
}
|
||||
}, [totalProgress, hasError]);
|
||||
|
||||
const iconProps = useMemo<FontAwesomeIconProps>(() => {
|
||||
switch (btnState) {
|
||||
case State.Idle:
|
||||
return {
|
||||
icon: faStream,
|
||||
};
|
||||
case State.Working:
|
||||
return {
|
||||
icon: faCircleNotch,
|
||||
spin: true,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: faExclamationTriangle,
|
||||
};
|
||||
}
|
||||
}, [btnState]);
|
||||
|
||||
const content = useMemo<React.ReactNode>(() => {
|
||||
const nodes: JSX.Element[] = [];
|
||||
|
||||
nodes.push(
|
||||
<Dropdown.Header key="notifications-header">
|
||||
{notifications.length > 0 ? "Notifications" : "No Notifications"}
|
||||
</Dropdown.Header>
|
||||
);
|
||||
nodes.push(
|
||||
...notifications.map((v, idx) => (
|
||||
<Dropdown.Item disabled key={BuildKey(idx, v.id, "notification")}>
|
||||
<Notification {...v}></Notification>
|
||||
</Dropdown.Item>
|
||||
))
|
||||
);
|
||||
|
||||
nodes.push(<Dropdown.Divider key="dropdown-divider"></Dropdown.Divider>);
|
||||
|
||||
nodes.push(
|
||||
<Dropdown.Header key="background-task-header">
|
||||
{progress.length > 0 ? "Background Tasks" : "No Background Tasks"}
|
||||
</Dropdown.Header>
|
||||
);
|
||||
nodes.push(
|
||||
...progress.map((v, idx) => (
|
||||
<Dropdown.Item disabled key={BuildKey(idx, v.id, "progress")}>
|
||||
<Progress {...v}></Progress>
|
||||
</Dropdown.Item>
|
||||
))
|
||||
);
|
||||
|
||||
return nodes;
|
||||
}, [progress, notifications]);
|
||||
|
||||
const onToggleClick = useCallback(() => {
|
||||
setHasNew(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Dropdown
|
||||
onClick={onToggleClick}
|
||||
className={`notification-btn ${hasNew ? "new-item" : ""}`}
|
||||
ref={dropdownRef}
|
||||
alignRight
|
||||
>
|
||||
<Dropdown.Toggle as={Button} className="dropdown-hidden">
|
||||
<FontAwesomeIcon {...iconProps}></FontAwesomeIcon>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="pb-3">{content}</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
{/* Handle this later */}
|
||||
<Overlay target={dropdownRef} show={false} placement="bottom">
|
||||
{(props) => {
|
||||
return (
|
||||
<Tooltip id="new-notification-tip" {...props}>
|
||||
New Notifications
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
</Overlay>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const Notification: FunctionComponent<Server.Notification> = ({
|
||||
type,
|
||||
message,
|
||||
}) => {
|
||||
const icon = useMemo<IconDefinition>(() => {
|
||||
switch (type) {
|
||||
case "info":
|
||||
return faInfoCircle;
|
||||
case "warning":
|
||||
return faExclamationTriangle;
|
||||
default:
|
||||
return faBug;
|
||||
}
|
||||
}, [type]);
|
||||
return (
|
||||
<div className="notification-center-notification d-flex flex-nowrap align-items-center justify-content-start my-1">
|
||||
<FontAwesomeIcon className="mr-2 text-dark" icon={icon}></FontAwesomeIcon>
|
||||
<span className="text-dark small">{message}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Progress: FunctionComponent<Server.Progress> = ({
|
||||
name,
|
||||
value,
|
||||
count,
|
||||
header,
|
||||
}) => {
|
||||
const isCompleted = value / count >= 1;
|
||||
return (
|
||||
<div className="notification-center-progress d-flex flex-column">
|
||||
<p className="progress-header m-0 h-6 text-dark font-weight-bold">
|
||||
{header}
|
||||
</p>
|
||||
<p className="progress-name m-0 small text-secondary">
|
||||
{isCompleted ? "Completed successfully" : name}
|
||||
</p>
|
||||
<ProgressBar
|
||||
className="mt-2"
|
||||
animated={!isCompleted}
|
||||
now={value / count}
|
||||
max={1}
|
||||
label={`${value}/${count}`}
|
||||
></ProgressBar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationCenter;
|
@ -0,0 +1,43 @@
|
||||
@function theme-color($key: "primary") {
|
||||
@return map-get($theme-colors, $key);
|
||||
}
|
||||
|
||||
.notification-btn {
|
||||
&.new-item {
|
||||
&::after {
|
||||
position: absolute;
|
||||
background-color: red;
|
||||
content: "";
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
right: 10%;
|
||||
top: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
max-width: 20rem;
|
||||
max-height: 40rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
$content-width: 16rem;
|
||||
|
||||
.notification-center-progress {
|
||||
width: $content-width;
|
||||
max-width: $content-width;
|
||||
|
||||
.progress-name {
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-center-notification {
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
width: $content-width;
|
||||
max-width: $content-width;
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
import {
|
||||
faExclamationTriangle,
|
||||
faPaperPlane,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { capitalize } from "lodash";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { ProgressBar, Toast } from "react-bootstrap";
|
||||
import {
|
||||
siteRemoveNotifications,
|
||||
siteRemoveProgress,
|
||||
} from "../../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../../@redux/hooks/base";
|
||||
import "./style.scss";
|
||||
|
||||
export interface NotificationContainerProps {}
|
||||
|
||||
const NotificationContainer: FunctionComponent<NotificationContainerProps> =
|
||||
() => {
|
||||
const { progress, notifications } = useReduxStore((s) => s.site);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const progressItems = progress.map((v) => (
|
||||
<ProgressToast key={v.id} {...v}></ProgressToast>
|
||||
));
|
||||
|
||||
const notificationItems = notifications.map((v) => (
|
||||
<NotificationToast key={v.id} {...v}></NotificationToast>
|
||||
));
|
||||
|
||||
return [...progressItems, ...notificationItems];
|
||||
}, [notifications, progress]);
|
||||
return (
|
||||
<div className="alert-container">
|
||||
<div className="toast-container">{items}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type MessageHolderProps = Server.Notification & {};
|
||||
|
||||
const NotificationToast: FunctionComponent<MessageHolderProps> = (props) => {
|
||||
const { message, type, id, timeout } = props;
|
||||
const removeNotification = useReduxAction(siteRemoveNotifications);
|
||||
|
||||
const remove = useCallback(
|
||||
() => removeNotification(id),
|
||||
[removeNotification, id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(remove, timeout);
|
||||
return () => {
|
||||
clearTimeout(handle);
|
||||
};
|
||||
}, [props, remove, timeout]);
|
||||
|
||||
return (
|
||||
<Toast onClose={remove} animation={false}>
|
||||
<Toast.Header>
|
||||
<FontAwesomeIcon
|
||||
className="mr-1"
|
||||
icon={faExclamationTriangle}
|
||||
></FontAwesomeIcon>
|
||||
<strong className="mr-auto">{capitalize(type)}</strong>
|
||||
</Toast.Header>
|
||||
<Toast.Body>{message}</Toast.Body>
|
||||
</Toast>
|
||||
);
|
||||
};
|
||||
|
||||
type ProgressHolderProps = Server.Progress & {};
|
||||
|
||||
const ProgressToast: FunctionComponent<ProgressHolderProps> = ({
|
||||
id,
|
||||
header,
|
||||
name,
|
||||
value,
|
||||
count,
|
||||
}) => {
|
||||
const removeProgress = useReduxAction(siteRemoveProgress);
|
||||
const remove = useCallback(() => removeProgress(id), [removeProgress, id]);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(remove, 10 * 1000);
|
||||
return () => {
|
||||
clearTimeout(handle);
|
||||
};
|
||||
}, [value, remove]);
|
||||
|
||||
const incomplete = value / count < 1;
|
||||
|
||||
return (
|
||||
<Toast onClose={remove}>
|
||||
<Toast.Header closeButton={false}>
|
||||
<FontAwesomeIcon className="mr-2" icon={faPaperPlane}></FontAwesomeIcon>
|
||||
<span className="mr-auto">{header}</span>
|
||||
</Toast.Header>
|
||||
<Toast.Body>
|
||||
<span>{name}</span>
|
||||
<ProgressBar
|
||||
className="my-1"
|
||||
animated={incomplete}
|
||||
now={value / count}
|
||||
max={1}
|
||||
label={`${value}/${count}`}
|
||||
></ProgressBar>
|
||||
</Toast.Body>
|
||||
</Toast>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationContainer;
|
@ -1,46 +0,0 @@
|
||||
@import "../../@scss/variable.scss";
|
||||
@import "../../@scss/bazarr.scss";
|
||||
|
||||
@function theme-color($key: "primary") {
|
||||
@return map-get($theme-colors, $key);
|
||||
}
|
||||
|
||||
.alert-container {
|
||||
position: fixed;
|
||||
display: block;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin-top: $header-height;
|
||||
|
||||
z-index: 9999;
|
||||
|
||||
.toast-container {
|
||||
padding: 1rem;
|
||||
|
||||
.toast {
|
||||
width: 16rem;
|
||||
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
|
||||
.toast-body {
|
||||
span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
.progress {
|
||||
.progress-bar {
|
||||
text-shadow: -2px -2px 5px theme-color("primary"),
|
||||
2px -2px 5px theme-color("primary"),
|
||||
-2px 2px 5px theme-color("primary"),
|
||||
2px 2px 5px theme-color("primary");
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue