mirror of https://github.com/morpheus65535/bazarr
Add tooltip in notification center
This commit is contained in:
parent
43ebecbdb2
commit
2c5aecc0db
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>>(
|
||||
|
|
|
@ -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>[]>(
|
||||
() =>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -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>>(
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue