Add tooltip in notification center

This commit is contained in:
LASER-Yi 2021-08-22 13:20:08 +08:00
parent 43ebecbdb2
commit 2c5aecc0db
14 changed files with 133 additions and 73 deletions

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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(() => {

View File

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

View File

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

View File

@ -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>[]>(
() =>

View File

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

View File

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

View File

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

View File

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