Add tooltip in notification center

pull/1515/head
LASER-Yi 4 years ago
parent 43ebecbdb2
commit 2c5aecc0db

@ -4,8 +4,8 @@ export function useIsAnyTaskRunning() {
return BGT.isRunning(); return BGT.isRunning();
} }
export function useIsAnyTaskRunningWithId(id: number) { export function useIsAnyTaskRunningWithId(ids: number[]) {
return BGT.hasId(id); return BGT.hasId(ids);
} }
export function useIsGroupTaskRunning(groupName: string) { export function useIsGroupTaskRunning(groupName: string) {

@ -2,6 +2,7 @@ import { keys } from "lodash";
import { import {
siteAddProgress, siteAddProgress,
siteRemoveProgress, siteRemoveProgress,
siteUpdateNotifier,
siteUpdateProgressCount, siteUpdateProgressCount,
} from "../../@redux/actions"; } from "../../@redux/actions";
import store from "../../@redux/store"; import store from "../../@redux/store";
@ -61,13 +62,15 @@ class BackgroundTask {
return groupName in this.groups; return groupName in this.groups;
} }
hasId(id: number) { hasId(ids: number[]) {
for (const id of ids) {
for (const key in this.groups) { for (const key in this.groups) {
const tasks = this.groups[key]; const tasks = this.groups[key];
if (tasks.find((v) => v.id === id) !== undefined) { if (tasks.find((v) => v.id === id) !== undefined) {
return true; return true;
} }
} }
}
return false; return false;
} }
@ -76,4 +79,18 @@ class BackgroundTask {
} }
} }
export default new BackgroundTask(); const BGT = new BackgroundTask();
export default BGT;
export function dispatchTask<T extends Task.Callable>(
groupName: string,
tasks: Task.Task<T>[],
comment?: string
) {
BGT.dispatch(groupName, tasks);
if (comment) {
store.dispatch(siteUpdateNotifier(comment));
}
}

@ -43,6 +43,10 @@ export const siteRemoveProgress = createAsyncThunk(
} }
); );
export const siteUpdateNotifier = createAction<string>(
"site/progress/update_notifier"
);
export const siteChangeSidebar = createAction<string>("site/sidebar/update"); export const siteChangeSidebar = createAction<string>("site/sidebar/update");
export const siteUpdateOffline = createAction<boolean>("site/offline/update"); export const siteUpdateOffline = createAction<boolean>("site/offline/update");

@ -11,6 +11,7 @@ import {
siteRemoveProgress, siteRemoveProgress,
siteUpdateBadges, siteUpdateBadges,
siteUpdateInitialization, siteUpdateInitialization,
siteUpdateNotifier,
siteUpdateOffline, siteUpdateOffline,
siteUpdateProgressCount, siteUpdateProgressCount,
} from "../actions/site"; } from "../actions/site";
@ -18,18 +19,26 @@ import {
interface Site { interface Site {
// Initialization state or error message // Initialization state or error message
initialized: boolean | string; initialized: boolean | string;
offline: boolean;
auth: boolean; auth: boolean;
progress: Site.Progress[]; progress: Site.Progress[];
notifier: {
content: string | null;
update: Date;
};
notifications: Server.Notification[]; notifications: Server.Notification[];
sidebar: string; sidebar: string;
badges: Badge; badges: Badge;
offline: boolean;
} }
const defaultSite: Site = { const defaultSite: Site = {
initialized: false, initialized: false,
auth: true, auth: true,
progress: [], progress: [],
notifier: {
content: null,
update: new Date(),
},
notifications: [], notifications: [],
sidebar: "", sidebar: "",
badges: { badges: {
@ -100,6 +109,11 @@ const reducer = createReducer(defaultSite, (builder) => {
} }
}); });
builder.addCase(siteUpdateNotifier, (state, action) => {
state.notifier.content = action.payload;
state.notifier.update = new Date();
});
builder builder
.addCase(siteChangeSidebar, (state, action) => { .addCase(siteChangeSidebar, (state, action) => {
state.sidebar = action.payload; state.sidebar = action.payload;

@ -25,7 +25,7 @@ import {
ProgressBar, ProgressBar,
Tooltip, Tooltip,
} from "react-bootstrap"; } from "react-bootstrap";
import { useDidUpdate } from "rooks"; import { useDidUpdate, useTimeoutWhen } from "rooks";
import { useReduxStore } from "../@redux/hooks/base"; import { useReduxStore } from "../@redux/hooks/base";
import { BuildKey, useIsArrayExtended } from "../utilites"; import { BuildKey, useIsArrayExtended } from "../utilites";
import "./notification.scss"; import "./notification.scss";
@ -63,7 +63,7 @@ function useHasErrorNotification(notifications: Server.Notification[]) {
} }
const NotificationCenter: FunctionComponent = () => { const NotificationCenter: FunctionComponent = () => {
const { progress, notifications } = useReduxStore((s) => s.site); const { progress, notifications, notifier } = useReduxStore((s) => s.site);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const [hasNew, setHasNew] = useState(false); const [hasNew, setHasNew] = useState(false);
@ -147,6 +147,15 @@ const NotificationCenter: FunctionComponent = () => {
setHasNew(false); setHasNew(false);
}, []); }, []);
// Tooltip Controller
const [showTooltip, setTooltip] = useState(false);
useTimeoutWhen(() => setTooltip(false), 3 * 1000, showTooltip);
useDidUpdate(() => {
if (notifier.content) {
setTooltip(true);
}
}, [notifier.update]);
return ( return (
<React.Fragment> <React.Fragment>
<Dropdown <Dropdown
@ -160,12 +169,11 @@ const NotificationCenter: FunctionComponent = () => {
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className="pb-3">{content}</Dropdown.Menu> <Dropdown.Menu className="pb-3">{content}</Dropdown.Menu>
</Dropdown> </Dropdown>
{/* Handle this later */} <Overlay target={dropdownRef} show={showTooltip} placement="bottom">
<Overlay target={dropdownRef} show={false} placement="bottom">
{(props) => { {(props) => {
return ( return (
<Tooltip id="new-notification-tip" {...props}> <Tooltip id="new-notification-tip" {...props}>
New Notifications {notifier.content}
</Tooltip> </Tooltip>
); );
}} }}

@ -58,7 +58,7 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => {
const [valid, setValid] = useState(true); const [valid, setValid] = useState(true);
const hasTask = useIsAnyTaskRunningWithId(id); const hasTask = useIsAnyTaskRunningWithId([id]);
useOnLoadedOnce(() => { useOnLoadedOnce(() => {
if (movie.content === null) { if (movie.content === null) {

@ -3,7 +3,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { FunctionComponent, useMemo } from "react"; import React, { FunctionComponent, useMemo } from "react";
import { Badge } from "react-bootstrap"; import { Badge } from "react-bootstrap";
import { Column } from "react-table"; import { Column } from "react-table";
import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks";
import { useProfileItemsToLanguages } from "../../@redux/hooks"; import { useProfileItemsToLanguages } from "../../@redux/hooks";
import { useShowOnlyDesired } from "../../@redux/hooks/site"; import { useShowOnlyDesired } from "../../@redux/hooks/site";
import { MoviesApi } from "../../apis"; import { MoviesApi } from "../../apis";
@ -14,16 +13,15 @@ const missingText = "Missing Subtitles";
interface Props { interface Props {
movie: Item.Movie; movie: Item.Movie;
disabled?: boolean;
profile?: Language.Profile; profile?: Language.Profile;
} }
const Table: FunctionComponent<Props> = ({ movie, profile }) => { const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
const onlyDesired = useShowOnlyDesired(); const onlyDesired = useShowOnlyDesired();
const profileItems = useProfileItemsToLanguages(profile); const profileItems = useProfileItemsToLanguages(profile);
const hasTask = useIsAnyTaskRunningWithId(movie.radarrId);
const columns: Column<Subtitle>[] = useMemo<Column<Subtitle>[]>( const columns: Column<Subtitle>[] = useMemo<Column<Subtitle>[]>(
() => [ () => [
{ {
@ -67,7 +65,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
} else if (original.path === missingText) { } else if (original.path === missingText) {
return ( return (
<AsyncButton <AsyncButton
disabled={hasTask} disabled={disabled}
promise={() => promise={() =>
MoviesApi.downloadSubtitles(movie.radarrId, { MoviesApi.downloadSubtitles(movie.radarrId, {
language: original.code2, language: original.code2,
@ -84,7 +82,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
} else { } else {
return ( return (
<AsyncButton <AsyncButton
disabled={hasTask} disabled={disabled}
variant="light" variant="light"
size="sm" size="sm"
promise={() => promise={() =>
@ -103,7 +101,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
}, },
}, },
], ],
[movie, hasTask] [movie, disabled]
); );
const data: Subtitle[] = useMemo(() => { const data: Subtitle[] = useMemo(() => {

@ -67,7 +67,9 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
const profile = useProfileBy(series.content?.profileId); const profile = useProfileBy(series.content?.profileId);
const hasTask = useIsAnyTaskRunningWithId(id); const hasTask = useIsAnyTaskRunningWithId(
episodes.content.map((v) => v.sonarrEpisodeId)
);
if (isNaN(id) || !valid) { if (isNaN(id) || !valid) {
return <Redirect to={RouterEmptyPath}></Redirect>; return <Redirect to={RouterEmptyPath}></Redirect>;
@ -150,7 +152,12 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
<ItemOverview item={serie} details={details}></ItemOverview> <ItemOverview item={serie} details={details}></ItemOverview>
</Row> </Row>
<Row> <Row>
<Table serie={series} episodes={episodes} profile={profile}></Table> <Table
serie={series}
episodes={episodes}
profile={profile}
disabled={hasTask}
></Table>
</Row> </Row>
<ItemEditorModal <ItemEditorModal
modalKey="edit" modalKey="edit"

@ -9,7 +9,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { FunctionComponent, useCallback, useMemo } from "react"; import React, { FunctionComponent, useCallback, useMemo } from "react";
import { Badge, ButtonGroup } from "react-bootstrap"; import { Badge, ButtonGroup } from "react-bootstrap";
import { Column, TableUpdater } from "react-table"; import { Column, TableUpdater } from "react-table";
import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks";
import { useProfileItemsToLanguages } from "../../@redux/hooks"; import { useProfileItemsToLanguages } from "../../@redux/hooks";
import { useShowOnlyDesired } from "../../@redux/hooks/site"; import { useShowOnlyDesired } from "../../@redux/hooks/site";
import { ProvidersApi } from "../../apis"; import { ProvidersApi } from "../../apis";
@ -29,6 +28,7 @@ import { SubtitleAction } from "./components";
interface Props { interface Props {
serie: Async.Item<Item.Series>; serie: Async.Item<Item.Series>;
episodes: Async.Base<Item.Episode[]>; episodes: Async.Base<Item.Episode[]>;
disabled?: boolean;
profile?: Language.Profile; profile?: Language.Profile;
} }
@ -48,17 +48,18 @@ const download = (item: any, result: SearchResultType) => {
); );
}; };
const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => { const Table: FunctionComponent<Props> = ({
serie,
episodes,
profile,
disabled,
}) => {
const showModal = useShowModal(); const showModal = useShowModal();
const onlyDesired = useShowOnlyDesired(); const onlyDesired = useShowOnlyDesired();
const profileItems = useProfileItemsToLanguages(profile); const profileItems = useProfileItemsToLanguages(profile);
const hasTask = useIsAnyTaskRunningWithId(
serie.content?.sonarrSeriesId ?? -1
);
const columns: Column<Item.Episode>[] = useMemo<Column<Item.Episode>[]>( const columns: Column<Item.Episode>[] = useMemo<Column<Item.Episode>[]>(
() => [ () => [
{ {
@ -152,20 +153,21 @@ const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => {
<ButtonGroup> <ButtonGroup>
<ActionButton <ActionButton
icon={faUser} icon={faUser}
disabled={serie.content?.profileId === null || hasTask} disabled={serie.content?.profileId === null || disabled}
onClick={() => { onClick={() => {
externalUpdate && externalUpdate(row, "manual-search"); externalUpdate && externalUpdate(row, "manual-search");
}} }}
></ActionButton> ></ActionButton>
<ActionButton <ActionButton
icon={faHistory} icon={faHistory}
disabled={disabled}
onClick={() => { onClick={() => {
externalUpdate && externalUpdate(row, "history"); externalUpdate && externalUpdate(row, "history");
}} }}
></ActionButton> ></ActionButton>
<ActionButton <ActionButton
icon={faBriefcase} icon={faBriefcase}
disabled={hasTask} disabled={disabled}
onClick={() => { onClick={() => {
externalUpdate && externalUpdate(row, "tools"); externalUpdate && externalUpdate(row, "tools");
}} }}
@ -175,7 +177,7 @@ const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => {
}, },
}, },
], ],
[onlyDesired, profileItems, serie, hasTask] [onlyDesired, profileItems, serie, disabled]
); );
const updateRow = useCallback<TableUpdater<Item.Episode>>( const updateRow = useCallback<TableUpdater<Item.Episode>>(

@ -22,7 +22,9 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
); );
// TODO: Separate movies and series // TODO: Separate movies and series
const hasTask = useIsAnyTaskRunningWithId(payload ? GetItemId(payload) : -1); const hasTask = useIsAnyTaskRunningWithId([
payload ? GetItemId(payload) : -1,
]);
const profileOptions = useMemo<SelectorOption<number>[]>( const profileOptions = useMemo<SelectorOption<number>[]>(
() => () =>

@ -1,7 +1,7 @@
import React, { FunctionComponent, useEffect, useMemo, useState } from "react"; import React, { FunctionComponent, useEffect, useMemo, useState } from "react";
import { Button, Container, Form } from "react-bootstrap"; import { Button, Container, Form } from "react-bootstrap";
import { FileForm, LanguageSelector } from ".."; import { FileForm, LanguageSelector } from "..";
import BackgroundTask from "../../@modules/task"; import { dispatchTask } from "../../@modules/task";
import { createTask } from "../../@modules/task/utilites"; import { createTask } from "../../@modules/task/utilites";
import { import {
useEnabledLanguages, useEnabledLanguages,
@ -56,7 +56,7 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
language: language.code2, language: language.code2,
} }
); );
BackgroundTask.dispatch(TaskGroupName, [task]); dispatchTask(TaskGroupName, [task], "Uploading subtitles...");
closeModal(props.modalKey); closeModal(props.modalKey);
} }
}} }}

@ -21,7 +21,7 @@ import {
MessageIcon, MessageIcon,
SimpleTable, SimpleTable,
} from ".."; } from "..";
import BackgroundTask from "../../@modules/task"; import { dispatchTask } from "../../@modules/task";
import { createTask } from "../../@modules/task/utilites"; import { createTask } from "../../@modules/task/utilites";
import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks"; import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks";
import { EpisodesApi, SubtitlesApi } from "../../apis"; import { EpisodesApi, SubtitlesApi } from "../../apis";
@ -29,15 +29,9 @@ import { Selector } from "../inputs";
import BaseModal, { BaseModalProps } from "./BaseModal"; import BaseModal, { BaseModalProps } from "./BaseModal";
import { useModalInformation } from "./hooks"; import { useModalInformation } from "./hooks";
enum State {
Updating,
Valid,
Warning,
}
interface PendingSubtitle { interface PendingSubtitle {
file: File; file: File;
state: State; fetching: boolean;
instance?: Item.Episode; instance?: Item.Episode;
} }
@ -95,11 +89,14 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
}, {}); }, {});
setPending((pd) => setPending((pd) =>
pd.map((v) => ({ pd.map((v) => {
const instance = episodeMap[v.file.name];
return {
...v, ...v,
state: State.Valid, instance,
instance: episodeMap[v.file.name], fetching: false,
})) };
})
); );
} }
}, },
@ -113,7 +110,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
return { return {
file: f, file: f,
didCheck: false, didCheck: false,
state: State.Updating, fetching: true,
}; };
}); });
setPending(list); setPending(list);
@ -144,7 +141,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
return createTask( return createTask(
v.file.name, v.file.name,
seriesid, episodeid,
EpisodesApi.uploadSubtitles.bind(EpisodesApi), EpisodesApi.uploadSubtitles.bind(EpisodesApi),
seriesid, seriesid,
episodeid, episodeid,
@ -152,7 +149,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
); );
}); });
BackgroundTask.dispatch(TaskGroupName, tasks); dispatchTask(TaskGroupName, tasks, "Uploading subtitles...");
}, [payload, pending, language]); }, [payload, pending, language]);
const canUpload = useMemo( const canUpload = useMemo(
@ -169,28 +166,35 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
() => [ () => [
{ {
id: "Icon", id: "Icon",
accessor: "state", accessor: "fetching",
className: "text-center", className: "text-center",
Cell: ({ value: state }) => { Cell: ({ value: fetching, row: { original } }) => {
let icon = faCircleNotch; let icon = faCircleNotch;
let color: string | undefined = undefined; let color: string | undefined = undefined;
let spin = false; let spin = false;
let msgs: string[] = []; let msgs: string[] = [];
switch (state) { const override = useMemo(
case State.Valid: () =>
original.instance?.subtitles.find(
(v) => v.code2 === language?.code2
) !== undefined,
[original.instance?.subtitles]
);
if (fetching) {
spin = true;
} else if (override) {
icon = faInfoCircle;
color = "var(--warning)";
msgs.push("Overwrite existing subtitle");
} else if (original.instance) {
icon = faCheck; icon = faCheck;
color = "var(--success)"; color = "var(--success)";
break; } else {
case State.Warning:
icon = faInfoCircle; icon = faInfoCircle;
color = "var(--warning)"; color = "var(--warning)";
break; msgs.push("Season or episode info is missing");
case State.Updating:
spin = true;
break;
default:
break;
} }
return ( return (
@ -262,7 +266,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
}, },
}, },
], ],
[] [language?.code2]
); );
const updateItem = useCallback<TableUpdater<PendingSubtitle>>( const updateItem = useCallback<TableUpdater<PendingSubtitle>>(

@ -39,7 +39,7 @@ import {
useModalPayload, useModalPayload,
useShowModal, useShowModal,
} from ".."; } from "..";
import BackgroundTask from "../../@modules/task"; import { dispatchTask } from "../../@modules/task";
import { createTask } from "../../@modules/task/utilites"; import { createTask } from "../../@modules/task/utilites";
import { useEnabledLanguages } from "../../@redux/hooks"; import { useEnabledLanguages } from "../../@redux/hooks";
import { SubtitlesApi } from "../../apis"; import { SubtitlesApi } from "../../apis";
@ -323,7 +323,7 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
); );
}); });
BackgroundTask.dispatch(TaskGroupName, tasks); dispatchTask(TaskGroupName, tasks, "Modifying subtitles...");
}, },
[closeModal, selections, props.modalKey] [closeModal, selections, props.modalKey]
); );

@ -5,6 +5,7 @@ import React, { useCallback, useMemo, useState } from "react";
import { Container, Dropdown, Row } from "react-bootstrap"; import { Container, Dropdown, Row } from "react-bootstrap";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Column } from "react-table"; import { Column } from "react-table";
import { useIsAnyTaskRunning } from "../../@modules/task/hooks";
import { useLanguageProfiles } from "../../@redux/hooks"; import { useLanguageProfiles } from "../../@redux/hooks";
import { useAppDispatch } from "../../@redux/hooks/base"; import { useAppDispatch } from "../../@redux/hooks/base";
import { ContentHeader } from "../../components"; import { ContentHeader } from "../../components";
@ -111,6 +112,8 @@ function BaseItemView<T extends Item.Base>({
return shared.modify(form); return shared.modify(form);
}, [dirtyItems, shared]); }, [dirtyItems, shared]);
const hasTask = useIsAnyTaskRunning();
return ( return (
<Container fluid> <Container fluid>
<Helmet> <Helmet>
@ -136,7 +139,7 @@ function BaseItemView<T extends Item.Base>({
</ContentHeader.Button> </ContentHeader.Button>
<ContentHeader.AsyncButton <ContentHeader.AsyncButton
icon={faCheck} icon={faCheck}
disabled={dirtyItems.length === 0} disabled={dirtyItems.length === 0 || hasTask}
promise={saveItems} promise={saveItems}
onSuccess={endEdit} onSuccess={endEdit}
> >
@ -148,7 +151,8 @@ function BaseItemView<T extends Item.Base>({
<ContentHeader.Button <ContentHeader.Button
updating={pendingEditMode !== editMode} updating={pendingEditMode !== editMode}
disabled={ disabled={
state.content.ids.length === 0 && state.state === "loading" (state.content.ids.length === 0 && state.state === "loading") ||
hasTask
} }
icon={faList} icon={faList}
onClick={startEdit} onClick={startEdit}

Loading…
Cancel
Save