summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLASER-Yi <[email protected]>2021-08-22 13:20:08 +0800
committerLASER-Yi <[email protected]>2021-08-22 13:20:08 +0800
commit2c5aecc0dbbba241512a016fc7bb302767fac600 (patch)
tree84f626215d3f3f629c183d21740278926f21989b
parent43ebecbdb26d4bcd3fc6f6cf18a6489bcc34b5fc (diff)
downloadbazarr-2c5aecc0dbbba241512a016fc7bb302767fac600.tar.gz
bazarr-2c5aecc0dbbba241512a016fc7bb302767fac600.zip
Add tooltip in notification center
-rw-r--r--frontend/src/@modules/task/hooks.ts4
-rw-r--r--frontend/src/@modules/task/index.ts29
-rw-r--r--frontend/src/@redux/actions/site.ts4
-rw-r--r--frontend/src/@redux/reducers/site.ts16
-rw-r--r--frontend/src/App/Notification.tsx18
-rw-r--r--frontend/src/Movies/Detail/index.tsx2
-rw-r--r--frontend/src/Movies/Detail/table.tsx12
-rw-r--r--frontend/src/Series/Episodes/index.tsx11
-rw-r--r--frontend/src/Series/Episodes/table.tsx20
-rw-r--r--frontend/src/components/modals/ItemEditorModal.tsx4
-rw-r--r--frontend/src/components/modals/MovieUploadModal.tsx4
-rw-r--r--frontend/src/components/modals/SeriesUploadModal.tsx70
-rw-r--r--frontend/src/components/modals/SubtitleToolModal.tsx4
-rw-r--r--frontend/src/generic/BaseItemView/index.tsx8
14 files changed, 133 insertions, 73 deletions
diff --git a/frontend/src/@modules/task/hooks.ts b/frontend/src/@modules/task/hooks.ts
index e2caf269c..557146dd2 100644
--- a/frontend/src/@modules/task/hooks.ts
+++ b/frontend/src/@modules/task/hooks.ts
@@ -4,8 +4,8 @@ export function useIsAnyTaskRunning() {
return BGT.isRunning();
}
-export function useIsAnyTaskRunningWithId(id: number) {
- return BGT.hasId(id);
+export function useIsAnyTaskRunningWithId(ids: number[]) {
+ return BGT.hasId(ids);
}
export function useIsGroupTaskRunning(groupName: string) {
diff --git a/frontend/src/@modules/task/index.ts b/frontend/src/@modules/task/index.ts
index da27b412f..72c0ec8e0 100644
--- a/frontend/src/@modules/task/index.ts
+++ b/frontend/src/@modules/task/index.ts
@@ -2,6 +2,7 @@ import { keys } from "lodash";
import {
siteAddProgress,
siteRemoveProgress,
+ siteUpdateNotifier,
siteUpdateProgressCount,
} from "../../@redux/actions";
import store from "../../@redux/store";
@@ -61,11 +62,13 @@ class BackgroundTask {
return groupName in this.groups;
}
- hasId(id: number) {
- for (const key in this.groups) {
- const tasks = this.groups[key];
- if (tasks.find((v) => v.id === id) !== undefined) {
- return true;
+ hasId(ids: number[]) {
+ for (const id of ids) {
+ for (const key in this.groups) {
+ const tasks = this.groups[key];
+ if (tasks.find((v) => v.id === id) !== undefined) {
+ return true;
+ }
}
}
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));
+ }
+}
diff --git a/frontend/src/@redux/actions/site.ts b/frontend/src/@redux/actions/site.ts
index 52af91c3e..873e67eb1 100644
--- a/frontend/src/@redux/actions/site.ts
+++ b/frontend/src/@redux/actions/site.ts
@@ -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 siteUpdateOffline = createAction<boolean>("site/offline/update");
diff --git a/frontend/src/@redux/reducers/site.ts b/frontend/src/@redux/reducers/site.ts
index 4c01a15f7..07796c186 100644
--- a/frontend/src/@redux/reducers/site.ts
+++ b/frontend/src/@redux/reducers/site.ts
@@ -11,6 +11,7 @@ import {
siteRemoveProgress,
siteUpdateBadges,
siteUpdateInitialization,
+ siteUpdateNotifier,
siteUpdateOffline,
siteUpdateProgressCount,
} from "../actions/site";
@@ -18,18 +19,26 @@ import {
interface Site {
// Initialization state or error message
initialized: boolean | string;
+ offline: boolean;
auth: boolean;
progress: Site.Progress[];
+ notifier: {
+ content: string | null;
+ update: Date;
+ };
notifications: Server.Notification[];
sidebar: string;
badges: Badge;
- offline: boolean;
}
const defaultSite: Site = {
initialized: false,
auth: true,
progress: [],
+ notifier: {
+ content: null,
+ update: new Date(),
+ },
notifications: [],
sidebar: "",
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
.addCase(siteChangeSidebar, (state, action) => {
state.sidebar = action.payload;
diff --git a/frontend/src/App/Notification.tsx b/frontend/src/App/Notification.tsx
index 5a6745ce4..ccb6fc4cc 100644
--- a/frontend/src/App/Notification.tsx
+++ b/frontend/src/App/Notification.tsx
@@ -25,7 +25,7 @@ import {
ProgressBar,
Tooltip,
} from "react-bootstrap";
-import { useDidUpdate } from "rooks";
+import { useDidUpdate, useTimeoutWhen } from "rooks";
import { useReduxStore } from "../@redux/hooks/base";
import { BuildKey, useIsArrayExtended } from "../utilites";
import "./notification.scss";
@@ -63,7 +63,7 @@ function useHasErrorNotification(notifications: Server.Notification[]) {
}
const NotificationCenter: FunctionComponent = () => {
- const { progress, notifications } = useReduxStore((s) => s.site);
+ const { progress, notifications, notifier } = useReduxStore((s) => s.site);
const dropdownRef = useRef<HTMLDivElement>(null);
const [hasNew, setHasNew] = useState(false);
@@ -147,6 +147,15 @@ const NotificationCenter: FunctionComponent = () => {
setHasNew(false);
}, []);
+ // Tooltip Controller
+ const [showTooltip, setTooltip] = useState(false);
+ useTimeoutWhen(() => setTooltip(false), 3 * 1000, showTooltip);
+ useDidUpdate(() => {
+ if (notifier.content) {
+ setTooltip(true);
+ }
+ }, [notifier.update]);
+
return (
<React.Fragment>
<Dropdown
@@ -160,12 +169,11 @@ const NotificationCenter: FunctionComponent = () => {
</Dropdown.Toggle>
<Dropdown.Menu className="pb-3">{content}</Dropdown.Menu>
</Dropdown>
- {/* Handle this later */}
- <Overlay target={dropdownRef} show={false} placement="bottom">
+ <Overlay target={dropdownRef} show={showTooltip} placement="bottom">
{(props) => {
return (
<Tooltip id="new-notification-tip" {...props}>
- New Notifications
+ {notifier.content}
</Tooltip>
);
}}
diff --git a/frontend/src/Movies/Detail/index.tsx b/frontend/src/Movies/Detail/index.tsx
index 94b8a61f0..dd6234c31 100644
--- a/frontend/src/Movies/Detail/index.tsx
+++ b/frontend/src/Movies/Detail/index.tsx
@@ -58,7 +58,7 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => {
const [valid, setValid] = useState(true);
- const hasTask = useIsAnyTaskRunningWithId(id);
+ const hasTask = useIsAnyTaskRunningWithId([id]);
useOnLoadedOnce(() => {
if (movie.content === null) {
diff --git a/frontend/src/Movies/Detail/table.tsx b/frontend/src/Movies/Detail/table.tsx
index 5f86587f8..e8785e522 100644
--- a/frontend/src/Movies/Detail/table.tsx
+++ b/frontend/src/Movies/Detail/table.tsx
@@ -3,7 +3,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { FunctionComponent, useMemo } from "react";
import { Badge } from "react-bootstrap";
import { Column } from "react-table";
-import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks";
import { useProfileItemsToLanguages } from "../../@redux/hooks";
import { useShowOnlyDesired } from "../../@redux/hooks/site";
import { MoviesApi } from "../../apis";
@@ -14,16 +13,15 @@ const missingText = "Missing Subtitles";
interface Props {
movie: Item.Movie;
+ disabled?: boolean;
profile?: Language.Profile;
}
-const Table: FunctionComponent<Props> = ({ movie, profile }) => {
+const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
const onlyDesired = useShowOnlyDesired();
const profileItems = useProfileItemsToLanguages(profile);
- const hasTask = useIsAnyTaskRunningWithId(movie.radarrId);
-
const columns: Column<Subtitle>[] = useMemo<Column<Subtitle>[]>(
() => [
{
@@ -67,7 +65,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
} else if (original.path === missingText) {
return (
<AsyncButton
- disabled={hasTask}
+ disabled={disabled}
promise={() =>
MoviesApi.downloadSubtitles(movie.radarrId, {
language: original.code2,
@@ -84,7 +82,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
} else {
return (
<AsyncButton
- disabled={hasTask}
+ disabled={disabled}
variant="light"
size="sm"
promise={() =>
@@ -103,7 +101,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
},
},
],
- [movie, hasTask]
+ [movie, disabled]
);
const data: Subtitle[] = useMemo(() => {
diff --git a/frontend/src/Series/Episodes/index.tsx b/frontend/src/Series/Episodes/index.tsx
index f279a35c2..825d5e47a 100644
--- a/frontend/src/Series/Episodes/index.tsx
+++ b/frontend/src/Series/Episodes/index.tsx
@@ -67,7 +67,9 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
const profile = useProfileBy(series.content?.profileId);
- const hasTask = useIsAnyTaskRunningWithId(id);
+ const hasTask = useIsAnyTaskRunningWithId(
+ episodes.content.map((v) => v.sonarrEpisodeId)
+ );
if (isNaN(id) || !valid) {
return <Redirect to={RouterEmptyPath}></Redirect>;
@@ -150,7 +152,12 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
<ItemOverview item={serie} details={details}></ItemOverview>
</Row>
<Row>
- <Table serie={series} episodes={episodes} profile={profile}></Table>
+ <Table
+ serie={series}
+ episodes={episodes}
+ profile={profile}
+ disabled={hasTask}
+ ></Table>
</Row>
<ItemEditorModal
modalKey="edit"
diff --git a/frontend/src/Series/Episodes/table.tsx b/frontend/src/Series/Episodes/table.tsx
index 987cab835..ad6b5376e 100644
--- a/frontend/src/Series/Episodes/table.tsx
+++ b/frontend/src/Series/Episodes/table.tsx
@@ -9,7 +9,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { FunctionComponent, useCallback, useMemo } from "react";
import { Badge, ButtonGroup } from "react-bootstrap";
import { Column, TableUpdater } from "react-table";
-import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks";
import { useProfileItemsToLanguages } from "../../@redux/hooks";
import { useShowOnlyDesired } from "../../@redux/hooks/site";
import { ProvidersApi } from "../../apis";
@@ -29,6 +28,7 @@ import { SubtitleAction } from "./components";
interface Props {
serie: Async.Item<Item.Series>;
episodes: Async.Base<Item.Episode[]>;
+ disabled?: boolean;
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 onlyDesired = useShowOnlyDesired();
const profileItems = useProfileItemsToLanguages(profile);
- const hasTask = useIsAnyTaskRunningWithId(
- serie.content?.sonarrSeriesId ?? -1
- );
-
const columns: Column<Item.Episode>[] = useMemo<Column<Item.Episode>[]>(
() => [
{
@@ -152,20 +153,21 @@ const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => {
<ButtonGroup>
<ActionButton
icon={faUser}
- disabled={serie.content?.profileId === null || hasTask}
+ disabled={serie.content?.profileId === null || disabled}
onClick={() => {
externalUpdate && externalUpdate(row, "manual-search");
}}
></ActionButton>
<ActionButton
icon={faHistory}
+ disabled={disabled}
onClick={() => {
externalUpdate && externalUpdate(row, "history");
}}
></ActionButton>
<ActionButton
icon={faBriefcase}
- disabled={hasTask}
+ disabled={disabled}
onClick={() => {
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>>(
diff --git a/frontend/src/components/modals/ItemEditorModal.tsx b/frontend/src/components/modals/ItemEditorModal.tsx
index eea6d7e1e..fddfac439 100644
--- a/frontend/src/components/modals/ItemEditorModal.tsx
+++ b/frontend/src/components/modals/ItemEditorModal.tsx
@@ -22,7 +22,9 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
);
// TODO: Separate movies and series
- const hasTask = useIsAnyTaskRunningWithId(payload ? GetItemId(payload) : -1);
+ const hasTask = useIsAnyTaskRunningWithId([
+ payload ? GetItemId(payload) : -1,
+ ]);
const profileOptions = useMemo<SelectorOption<number>[]>(
() =>
diff --git a/frontend/src/components/modals/MovieUploadModal.tsx b/frontend/src/components/modals/MovieUploadModal.tsx
index 0d608f0c8..f93465836 100644
--- a/frontend/src/components/modals/MovieUploadModal.tsx
+++ b/frontend/src/components/modals/MovieUploadModal.tsx
@@ -1,7 +1,7 @@
import React, { FunctionComponent, useEffect, useMemo, useState } from "react";
import { Button, Container, Form } from "react-bootstrap";
import { FileForm, LanguageSelector } from "..";
-import BackgroundTask from "../../@modules/task";
+import { dispatchTask } from "../../@modules/task";
import { createTask } from "../../@modules/task/utilites";
import {
useEnabledLanguages,
@@ -56,7 +56,7 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
language: language.code2,
}
);
- BackgroundTask.dispatch(TaskGroupName, [task]);
+ dispatchTask(TaskGroupName, [task], "Uploading subtitles...");
closeModal(props.modalKey);
}
}}
diff --git a/frontend/src/components/modals/SeriesUploadModal.tsx b/frontend/src/components/modals/SeriesUploadModal.tsx
index 8e62247cd..34e562e2f 100644
--- a/frontend/src/components/modals/SeriesUploadModal.tsx
+++ b/frontend/src/components/modals/SeriesUploadModal.tsx
@@ -21,7 +21,7 @@ import {
MessageIcon,
SimpleTable,
} from "..";
-import BackgroundTask from "../../@modules/task";
+import { dispatchTask } from "../../@modules/task";
import { createTask } from "../../@modules/task/utilites";
import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks";
import { EpisodesApi, SubtitlesApi } from "../../apis";
@@ -29,15 +29,9 @@ import { Selector } from "../inputs";
import BaseModal, { BaseModalProps } from "./BaseModal";
import { useModalInformation } from "./hooks";
-enum State {
- Updating,
- Valid,
- Warning,
-}
-
interface PendingSubtitle {
file: File;
- state: State;
+ fetching: boolean;
instance?: Item.Episode;
}
@@ -95,11 +89,14 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
}, {});
setPending((pd) =>
- pd.map((v) => ({
- ...v,
- state: State.Valid,
- instance: episodeMap[v.file.name],
- }))
+ pd.map((v) => {
+ const instance = episodeMap[v.file.name];
+ return {
+ ...v,
+ instance,
+ fetching: false,
+ };
+ })
);
}
},
@@ -113,7 +110,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
return {
file: f,
didCheck: false,
- state: State.Updating,
+ fetching: true,
};
});
setPending(list);
@@ -144,7 +141,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
return createTask(
v.file.name,
- seriesid,
+ episodeid,
EpisodesApi.uploadSubtitles.bind(EpisodesApi),
seriesid,
episodeid,
@@ -152,7 +149,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
);
});
- BackgroundTask.dispatch(TaskGroupName, tasks);
+ dispatchTask(TaskGroupName, tasks, "Uploading subtitles...");
}, [payload, pending, language]);
const canUpload = useMemo(
@@ -169,28 +166,35 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
() => [
{
id: "Icon",
- accessor: "state",
+ accessor: "fetching",
className: "text-center",
- Cell: ({ value: state }) => {
+ Cell: ({ value: fetching, row: { original } }) => {
let icon = faCircleNotch;
let color: string | undefined = undefined;
let spin = false;
let msgs: string[] = [];
- switch (state) {
- case State.Valid:
- icon = faCheck;
- color = "var(--success)";
- break;
- case State.Warning:
- icon = faInfoCircle;
- color = "var(--warning)";
- break;
- case State.Updating:
- spin = true;
- break;
- default:
- break;
+ const override = useMemo(
+ () =>
+ 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;
+ color = "var(--success)";
+ } else {
+ icon = faInfoCircle;
+ color = "var(--warning)";
+ msgs.push("Season or episode info is missing");
}
return (
@@ -262,7 +266,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
},
},
],
- []
+ [language?.code2]
);
const updateItem = useCallback<TableUpdater<PendingSubtitle>>(
diff --git a/frontend/src/components/modals/SubtitleToolModal.tsx b/frontend/src/components/modals/SubtitleToolModal.tsx
index 7616db12c..eb4114038 100644
--- a/frontend/src/components/modals/SubtitleToolModal.tsx
+++ b/frontend/src/components/modals/SubtitleToolModal.tsx
@@ -39,7 +39,7 @@ import {
useModalPayload,
useShowModal,
} from "..";
-import BackgroundTask from "../../@modules/task";
+import { dispatchTask } from "../../@modules/task";
import { createTask } from "../../@modules/task/utilites";
import { useEnabledLanguages } from "../../@redux/hooks";
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]
);
diff --git a/frontend/src/generic/BaseItemView/index.tsx b/frontend/src/generic/BaseItemView/index.tsx
index 07d970c31..cc060d4a8 100644
--- a/frontend/src/generic/BaseItemView/index.tsx
+++ b/frontend/src/generic/BaseItemView/index.tsx
@@ -5,6 +5,7 @@ import React, { useCallback, useMemo, useState } from "react";
import { Container, Dropdown, Row } from "react-bootstrap";
import { Helmet } from "react-helmet";
import { Column } from "react-table";
+import { useIsAnyTaskRunning } from "../../@modules/task/hooks";
import { useLanguageProfiles } from "../../@redux/hooks";
import { useAppDispatch } from "../../@redux/hooks/base";
import { ContentHeader } from "../../components";
@@ -111,6 +112,8 @@ function BaseItemView<T extends Item.Base>({
return shared.modify(form);
}, [dirtyItems, shared]);
+ const hasTask = useIsAnyTaskRunning();
+
return (
<Container fluid>
<Helmet>
@@ -136,7 +139,7 @@ function BaseItemView<T extends Item.Base>({
</ContentHeader.Button>
<ContentHeader.AsyncButton
icon={faCheck}
- disabled={dirtyItems.length === 0}
+ disabled={dirtyItems.length === 0 || hasTask}
promise={saveItems}
onSuccess={endEdit}
>
@@ -148,7 +151,8 @@ function BaseItemView<T extends Item.Base>({
<ContentHeader.Button
updating={pendingEditMode !== editMode}
disabled={
- state.content.ids.length === 0 && state.state === "loading"
+ (state.content.ids.length === 0 && state.state === "loading") ||
+ hasTask
}
icon={faList}
onClick={startEdit}