Add a new notification center to the UI

pull/1509/head
LASER-Yi 3 years ago
parent 56729e0dbb
commit d7533bac57

@ -85,12 +85,6 @@ def sync_episodes(series_id=None, send_event=True):
episodes_to_add.append(episodeParser(episode))
if send_event:
show_progress(id='episodes_progress',
header='Syncing episodes...',
name='Completed successfully',
value=series_count,
count=series_count)
hide_progress(id='episodes_progress')
# Remove old episodes from DB

@ -88,14 +88,8 @@ def update_movies(send_event=True):
tags_dict=tagsDict,
movie_default_profile=movie_default_profile,
audio_profiles=audio_profiles))
if send_event:
show_progress(id='movies_progress',
header='Syncing movies...',
name='Completed successfully',
value=movies_count,
count=movies_count)
hide_progress(id='movies_progress')
# Remove old movies from DB

@ -72,12 +72,6 @@ def update_series(send_event=True):
audio_profiles=audio_profiles))
if send_event:
show_progress(id='series_progress',
header='Syncing series...',
name='Completed successfully',
value=series_count,
count=series_count)
hide_progress(id='series_progress')
# Remove old series from DB

@ -796,13 +796,6 @@ def series_download_subtitles(no):
logging.info("BAZARR All providers are throttled")
break
if count_episodes_details:
show_progress(id='series_search_progress_{}'.format(no),
header='Searching missing subtitles...',
name='Completed successfully',
value=count_episodes_details,
count=count_episodes_details)
hide_progress(id='series_search_progress_{}'.format(no))
@ -975,13 +968,6 @@ def movies_download_subtitles(no):
logging.info("BAZARR All providers are throttled")
break
if count_movie:
show_progress(id='movie_search_progress_{}'.format(no),
header='Searching missing subtitles...',
name='Completed successfully',
value=count_movie,
count=count_movie)
hide_progress(id='movie_search_progress_{}'.format(no))
@ -1189,12 +1175,6 @@ def wanted_search_missing_subtitles_series():
logging.info("BAZARR All providers are throttled")
return
show_progress(id='wanted_episodes_progress',
header='Searching subtitles...',
name='Completed successfully',
value=count_episodes,
count=count_episodes)
hide_progress(id='wanted_episodes_progress')
logging.info('BAZARR Finished searching for missing Series Subtitles. Check History for more information.')
@ -1226,12 +1206,6 @@ def wanted_search_missing_subtitles_movies():
logging.info("BAZARR All providers are throttled")
return
show_progress(id='wanted_movies_progress',
header='Searching subtitles...',
name='Completed successfully',
value=count_movies,
count=count_movies)
hide_progress(id='wanted_movies_progress')
logging.info('BAZARR Finished searching for missing Movies Subtitles. Check History for more information.')
@ -1559,12 +1533,6 @@ def upgrade_subtitles():
language_code, provider, score, subs_id, subs_path)
send_notifications(episode['sonarrSeriesId'], episode['sonarrEpisodeId'], message)
show_progress(id='upgrade_episodes_progress',
header='Upgrading episodes subtitles...',
name='Completed successfully',
value=count_episode_to_upgrade,
count=count_episode_to_upgrade)
hide_progress(id='upgrade_episodes_progress')
if settings.general.getboolean('use_radarr'):
@ -1632,12 +1600,6 @@ def upgrade_subtitles():
history_log_movie(3, movie['radarrId'], message, path, language_code, provider, score, subs_id, subs_path)
send_notifications_movie(movie['radarrId'], message)
show_progress(id='upgrade_movies_progress',
header='Upgrading movies subtitles...',
name='Completed successfully',
value=count_movie_to_upgrade,
count=count_movie_to_upgrade)
hide_progress(id='upgrade_movies_progress')
logging.info('BAZARR Finished searching for Subtitles to upgrade. Check History for more information.')

@ -458,12 +458,6 @@ def series_full_scan_subtitles():
count=count_episodes)
store_subtitles(episode['path'], path_mappings.path_replace(episode['path']))
show_progress(id='episodes_disk_scan',
header='Full disk scan...',
name='Completed successfully',
value=count_episodes,
count=count_episodes)
hide_progress(id='episodes_disk_scan')
gc.collect()
@ -482,12 +476,6 @@ def movies_full_scan_subtitles():
count=count_movies)
store_subtitles_movie(movie['path'], path_mappings.path_replace_movie(movie['path']))
show_progress(id='movies_disk_scan',
header='Full disk scan...',
name='Completed successfully',
value=count_movies,
count=count_movies)
hide_progress(id='movies_disk_scan')
gc.collect()

@ -26,11 +26,12 @@ export const siteRemoveNotifications = createAction<string>(
"site/notifications/remove"
);
export const siteAddProgress = createAction<Server.Progress[]>(
"site/progress/add"
);
export const siteAddProgress =
createAction<Server.Progress[]>("site/progress/add");
export const siteRemoveProgress = createAction<string>("site/progress/remove");
export const siteRemoveProgress = createAction<string[]>(
"site/progress/remove"
);
export const siteChangeSidebar = createAction<string>("site/sidebar/update");

@ -1,5 +1,5 @@
import { createReducer } from "@reduxjs/toolkit";
import { remove, uniqBy } from "lodash";
import { pullAllWith, remove, uniqBy } from "lodash";
import apis from "../../apis";
import {
siteAddNotifications,
@ -73,7 +73,7 @@ const reducer = createReducer(defaultSite, (builder) => {
);
})
.addCase(siteRemoveProgress, (state, action) => {
remove(state.progress, (n) => n.id === action.payload);
pullAllWith(state.progress, action.payload, (l, r) => l.id === r);
})
.addCase(siteChangeSidebar, (state, action) => {
state.sidebar = action.payload;

@ -87,9 +87,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
update: bindReduxAction(siteAddProgress),
delete: (ids) => {
setTimeout(() => {
ids.forEach((id) => {
reduxStore.dispatch(siteRemoveProgress(id));
});
reduxStore.dispatch(siteRemoveProgress(ids));
}, 3 * 1000);
},
},

@ -25,6 +25,7 @@ import { SystemApi } from "../apis";
import { ActionButton, SearchBar, SearchResult } from "../components";
import { useGotoHomepage } from "../utilites";
import "./header.scss";
import NotificationCenter from "./Notification";
async function SearchItem(text: string) {
const results = await SystemApi.search(text);
@ -58,7 +59,7 @@ const Header: FunctionComponent<Props> = () => {
const offline = useIsOffline();
const dropdown = useMemo(
const serverActions = useMemo(
() => (
<Dropdown alignRight>
<Dropdown.Toggle className="dropdown-hidden" as={Button}>
@ -117,6 +118,7 @@ const Header: FunctionComponent<Props> = () => {
<SearchBar onSearch={SearchItem}></SearchBar>
</Col>
<Col className="d-flex flex-row align-items-center justify-content-end pr-2">
<NotificationCenter></NotificationCenter>
<Button
href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=XHHRWXT9YB7WE&source=url"
target="_blank"
@ -134,7 +136,7 @@ const Header: FunctionComponent<Props> = () => {
Connecting...
</ActionButton>
) : (
dropdown
serverActions
)}
</Col>
</Row>

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

@ -22,7 +22,6 @@ import LaunchError from "../special-pages/LaunchError";
import UIError from "../special-pages/UIError";
import { useBaseUrl, useHasUpdateInject } from "../utilites";
import Header from "./Header";
import NotificationContainer from "./notifications";
import Router from "./Router";
// Sidebar Toggle
@ -75,7 +74,6 @@ const App: FunctionComponent<Props> = () => {
<Router className="d-flex flex-row flex-grow-1 main-router"></Router>
</ModalProvider>
</Row>
<NotificationContainer></NotificationContainer>
</SidebarToggleContext.Provider>
);
} catch (e) {

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

@ -1,5 +1,6 @@
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import { useHistory } from "react-router";
import { useDidUpdate } from "rooks";
import { getBaseUrl } from ".";
export function useBaseUrl(slash: boolean = false) {
@ -26,3 +27,15 @@ export function useHasUpdateInject() {
return window.Bazarr.hasUpdate;
}
}
export function useIsArrayExtended(arr: any[]) {
const [size, setSize] = useState(arr.length);
const [isExtended, setExtended] = useState(arr.length !== 0);
useDidUpdate(() => {
setExtended(arr.length > size);
setSize(arr.length);
}, [arr.length]);
return isExtended;
}

Loading…
Cancel
Save