diff options
author | LASER-Yi <[email protected]> | 2021-08-22 13:20:08 +0800 |
---|---|---|
committer | LASER-Yi <[email protected]> | 2021-08-22 13:20:08 +0800 |
commit | 2c5aecc0dbbba241512a016fc7bb302767fac600 (patch) | |
tree | 84f626215d3f3f629c183d21740278926f21989b | |
parent | 43ebecbdb26d4bcd3fc6f6cf18a6489bcc34b5fc (diff) | |
download | bazarr-2c5aecc0dbbba241512a016fc7bb302767fac600.tar.gz bazarr-2c5aecc0dbbba241512a016fc7bb302767fac600.zip |
Add tooltip in notification center
-rw-r--r-- | frontend/src/@modules/task/hooks.ts | 4 | ||||
-rw-r--r-- | frontend/src/@modules/task/index.ts | 29 | ||||
-rw-r--r-- | frontend/src/@redux/actions/site.ts | 4 | ||||
-rw-r--r-- | frontend/src/@redux/reducers/site.ts | 16 | ||||
-rw-r--r-- | frontend/src/App/Notification.tsx | 18 | ||||
-rw-r--r-- | frontend/src/Movies/Detail/index.tsx | 2 | ||||
-rw-r--r-- | frontend/src/Movies/Detail/table.tsx | 12 | ||||
-rw-r--r-- | frontend/src/Series/Episodes/index.tsx | 11 | ||||
-rw-r--r-- | frontend/src/Series/Episodes/table.tsx | 20 | ||||
-rw-r--r-- | frontend/src/components/modals/ItemEditorModal.tsx | 4 | ||||
-rw-r--r-- | frontend/src/components/modals/MovieUploadModal.tsx | 4 | ||||
-rw-r--r-- | frontend/src/components/modals/SeriesUploadModal.tsx | 70 | ||||
-rw-r--r-- | frontend/src/components/modals/SubtitleToolModal.tsx | 4 | ||||
-rw-r--r-- | frontend/src/generic/BaseItemView/index.tsx | 8 |
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} |