summaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
authorLASER-Yi <[email protected]>2021-08-22 00:44:52 +0800
committerLASER-Yi <[email protected]>2021-08-22 00:44:52 +0800
commit9c8119df3b0781119592f06da76fe4a37f739989 (patch)
tree1c648d87d6998298108a46de6c52251b99d7cee8 /frontend
parenteb47356d1017b5fc329cc85cbab06de069a43823 (diff)
downloadbazarr-9c8119df3b0781119592f06da76fe4a37f739989.tar.gz
bazarr-9c8119df3b0781119592f06da76fe4a37f739989.zip
Add background task support for UI. Let subtitle modification runs in background
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/@modules/socketio/socket.d.ts (renamed from frontend/src/@types/socket.d.ts)2
-rw-r--r--frontend/src/@modules/task/hooks.ts13
-rw-r--r--frontend/src/@modules/task/index.ts62
-rw-r--r--frontend/src/@modules/task/task.d.ts14
-rw-r--r--frontend/src/@modules/task/utilites.ts13
-rw-r--r--frontend/src/@redux/actions/site.ts2
-rw-r--r--frontend/src/@redux/reducers/site.ts2
-rw-r--r--frontend/src/@types/site.d.ts (renamed from frontend/src/@types/server.d.ts)2
-rw-r--r--frontend/src/App/Notification.tsx11
-rw-r--r--frontend/src/components/modals/BaseModal.tsx6
-rw-r--r--frontend/src/components/modals/SubtitleToolModal.tsx128
-rw-r--r--frontend/src/components/modals/hooks.tsx36
-rw-r--r--frontend/src/components/modals/provider.tsx44
13 files changed, 190 insertions, 145 deletions
diff --git a/frontend/src/@types/socket.d.ts b/frontend/src/@modules/socketio/socket.d.ts
index 10224bde3..77a1179cd 100644
--- a/frontend/src/@types/socket.d.ts
+++ b/frontend/src/@modules/socketio/socket.d.ts
@@ -57,6 +57,6 @@ declare namespace SocketIO {
};
namespace CustomEvent {
- type Progress = Server.Progress;
+ type Progress = Site.Progress;
}
}
diff --git a/frontend/src/@modules/task/hooks.ts b/frontend/src/@modules/task/hooks.ts
new file mode 100644
index 000000000..14fea2288
--- /dev/null
+++ b/frontend/src/@modules/task/hooks.ts
@@ -0,0 +1,13 @@
+import BGT from "./";
+
+export function useIsAnyTaskRunning() {
+ return BGT.isRunning();
+}
+
+export function useIsGroupTaskRunning(groupName: string) {
+ return BGT.has(groupName);
+}
+
+export function useIsIdRunning(groupName: string, id: number) {
+ return BGT.find(groupName, id);
+}
diff --git a/frontend/src/@modules/task/index.ts b/frontend/src/@modules/task/index.ts
new file mode 100644
index 000000000..3df9c627d
--- /dev/null
+++ b/frontend/src/@modules/task/index.ts
@@ -0,0 +1,62 @@
+import { keys } from "lodash";
+import { siteAddProgress, siteRemoveProgress } from "../../@redux/actions";
+import store from "../../@redux/store";
+
+// A background task manager, use for dispatching task one by one
+class BackgroundTask {
+ private groups: Task.Group;
+ constructor() {
+ this.groups = {};
+ }
+
+ dispatch<T extends Task.Callable>(groupName: string, tasks: Task.Task<T>[]) {
+ if (groupName in this.groups) {
+ return false;
+ }
+
+ this.groups[groupName] = tasks;
+ setTimeout(async () => {
+ const dispatch = store.dispatch;
+
+ for (let index = 0; index < tasks.length; index++) {
+ const task = tasks[index];
+
+ dispatch(
+ siteAddProgress([
+ {
+ id: groupName,
+ header: groupName,
+ name: task.name,
+ value: index,
+ count: tasks.length,
+ },
+ ])
+ );
+ try {
+ await task.callable(...task.parameters);
+ } catch (error) {}
+ }
+ delete this.groups[groupName];
+ dispatch(siteRemoveProgress([groupName]));
+ });
+
+ return true;
+ }
+
+ find(groupName: string, id: number) {
+ if (groupName in this.groups) {
+ return this.groups[groupName].find((v) => v.id === id) !== undefined;
+ }
+ return false;
+ }
+
+ has(groupName: string) {
+ return groupName in this.groups;
+ }
+
+ isRunning() {
+ return keys(this.groups).length > 0;
+ }
+}
+
+export default new BackgroundTask();
diff --git a/frontend/src/@modules/task/task.d.ts b/frontend/src/@modules/task/task.d.ts
new file mode 100644
index 000000000..a04061915
--- /dev/null
+++ b/frontend/src/@modules/task/task.d.ts
@@ -0,0 +1,14 @@
+declare namespace Task {
+ type Callable = (...args: any[]) => Promise<void>;
+
+ interface Task<FN extends Callable> {
+ name: string;
+ id?: number;
+ callable: FN;
+ parameters: Parameters<FN>;
+ }
+
+ type Group = {
+ [category: string]: Task.Task<Callable>[];
+ };
+}
diff --git a/frontend/src/@modules/task/utilites.ts b/frontend/src/@modules/task/utilites.ts
new file mode 100644
index 000000000..2898467c2
--- /dev/null
+++ b/frontend/src/@modules/task/utilites.ts
@@ -0,0 +1,13 @@
+export function createTask<T extends Task.Callable>(
+ name: string,
+ id: number | undefined,
+ callable: T,
+ ...parameters: Parameters<T>
+): Task.Task<T> {
+ return {
+ name,
+ id,
+ callable,
+ parameters,
+ };
+}
diff --git a/frontend/src/@redux/actions/site.ts b/frontend/src/@redux/actions/site.ts
index 89f89c0c7..ff2303ed0 100644
--- a/frontend/src/@redux/actions/site.ts
+++ b/frontend/src/@redux/actions/site.ts
@@ -28,7 +28,7 @@ export const siteRemoveNotifications = createAction<string>(
);
export const siteAddProgress =
- createAction<Server.Progress[]>("site/progress/add");
+ createAction<Site.Progress[]>("site/progress/add");
export const siteRemoveProgress = createAsyncThunk(
"site/progress/remove",
diff --git a/frontend/src/@redux/reducers/site.ts b/frontend/src/@redux/reducers/site.ts
index 2cf860674..2388910e2 100644
--- a/frontend/src/@redux/reducers/site.ts
+++ b/frontend/src/@redux/reducers/site.ts
@@ -18,7 +18,7 @@ interface Site {
// Initialization state or error message
initialized: boolean | string;
auth: boolean;
- progress: Server.Progress[];
+ progress: Site.Progress[];
notifications: Server.Notification[];
sidebar: string;
badges: Badge;
diff --git a/frontend/src/@types/server.d.ts b/frontend/src/@types/site.d.ts
index e2db5b185..a2182c8cd 100644
--- a/frontend/src/@types/server.d.ts
+++ b/frontend/src/@types/site.d.ts
@@ -5,7 +5,9 @@ declare namespace Server {
message: string;
timeout: number;
}
+}
+declare namespace Site {
interface Progress {
id: string;
header: string;
diff --git a/frontend/src/App/Notification.tsx b/frontend/src/App/Notification.tsx
index 8c62caa37..5a6745ce4 100644
--- a/frontend/src/App/Notification.tsx
+++ b/frontend/src/App/Notification.tsx
@@ -36,7 +36,7 @@ enum State {
Failed,
}
-function useTotalProgress(progress: Server.Progress[]) {
+function useTotalProgress(progress: Site.Progress[]) {
return useMemo(() => {
const { value, count } = progress.reduce(
(prev, { value, count }) => {
@@ -50,7 +50,7 @@ function useTotalProgress(progress: Server.Progress[]) {
if (count === 0) {
return 0;
} else {
- return value / count;
+ return (value + 0.001) / count;
}
}, [progress]);
}
@@ -196,13 +196,14 @@ const Notification: FunctionComponent<Server.Notification> = ({
);
};
-const Progress: FunctionComponent<Server.Progress> = ({
+const Progress: FunctionComponent<Site.Progress> = ({
name,
value,
count,
header,
}) => {
const isCompleted = value / count > 1;
+ const displayValue = Math.min(count, value + 1);
return (
<div className="notification-center-progress d-flex flex-column">
<p className="progress-header m-0 h-6 text-dark font-weight-bold">
@@ -214,9 +215,9 @@ const Progress: FunctionComponent<Server.Progress> = ({
<ProgressBar
className="mt-2"
animated={!isCompleted}
- now={value / count}
+ now={displayValue / count}
max={1}
- label={`${value}/${count}`}
+ label={`${displayValue}/${count}`}
></ProgressBar>
</div>
);
diff --git a/frontend/src/components/modals/BaseModal.tsx b/frontend/src/components/modals/BaseModal.tsx
index c80681ba2..89021effb 100644
--- a/frontend/src/components/modals/BaseModal.tsx
+++ b/frontend/src/components/modals/BaseModal.tsx
@@ -23,9 +23,11 @@ export const BaseModal: FunctionComponent<BaseModalProps> = (props) => {
}, []);
const exit = useCallback(() => {
- closeModal(modalKey);
+ if (isShow) {
+ closeModal(modalKey);
+ }
setExit(false);
- }, [closeModal, modalKey]);
+ }, [closeModal, modalKey, isShow]);
return (
<Modal
diff --git a/frontend/src/components/modals/SubtitleToolModal.tsx b/frontend/src/components/modals/SubtitleToolModal.tsx
index aebd67e19..83ac4aaea 100644
--- a/frontend/src/components/modals/SubtitleToolModal.tsx
+++ b/frontend/src/components/modals/SubtitleToolModal.tsx
@@ -1,7 +1,4 @@
-import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import {
- faCheck,
- faCircleNotch,
faClock,
faCode,
faDeaf,
@@ -39,16 +36,19 @@ import {
LanguageText,
Selector,
SimpleTable,
- useCloseModalIfCovered,
useModalPayload,
useShowModal,
} from "..";
+import BackgroundTask from "../../@modules/task";
+import { useIsGroupTaskRunning } from "../../@modules/task/hooks";
+import { createTask } from "../../@modules/task/utilites";
import { useEnabledLanguages } from "../../@redux/hooks";
import { SubtitlesApi } from "../../apis";
import { isMovie, submodProcessColor } from "../../utilites";
import { log } from "../../utilites/logger";
import { useCustomSelection } from "../tables/plugins";
import BaseModal, { BaseModalProps } from "./BaseModal";
+import { useCloseModal } from "./hooks";
import { availableTranslation, colorOptions } from "./toolOptions";
type SupportType = Item.Episode | Item.Movie;
@@ -57,40 +57,6 @@ type TableColumnType = FormType.ModifySubtitle & {
_language: Language.Info;
};
-enum State {
- Pending,
- Processing,
- Done,
-}
-
-type ProcessState = StrictObject<State>;
-
-// TODO: Extract this
-interface StateIconProps {
- state: State;
-}
-
-const StateIcon: FunctionComponent<StateIconProps> = ({ state }) => {
- let icon = faQuestionCircle;
- switch (state) {
- case State.Pending:
- icon = faClock;
- break;
- case State.Processing:
- icon = faCircleNotch;
- break;
- case State.Done:
- icon = faCheck;
- break;
- }
- return (
- <FontAwesomeIcon
- icon={icon}
- spin={state === State.Processing}
- ></FontAwesomeIcon>
- );
-};
-
function getIdAndType(item: SupportType): [number, "episode" | "movie"] {
if (isMovie(item)) {
return [item.radarrId, "movie"];
@@ -328,51 +294,39 @@ const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({
);
};
-interface STMProps {}
+const TaskGroupName = "Modifying Subtitles";
-const STM: FunctionComponent<BaseModalProps & STMProps> = ({ ...props }) => {
+const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
const payload = useModalPayload<SupportType[]>(props.modalKey);
-
- const [updating, setUpdate] = useState<boolean>(false);
- const [processState, setProcessState] = useState<ProcessState>({});
const [selections, setSelections] = useState<TableColumnType[]>([]);
- const closeModal = useCloseModalIfCovered();
+ const hasTask = useIsGroupTaskRunning(TaskGroupName);
+
+ const closeModal = useCloseModal();
const process = useCallback(
- async (action: string, override?: Partial<FormType.ModifySubtitle>) => {
+ (action: string, override?: Partial<FormType.ModifySubtitle>) => {
log("info", "executing action", action);
closeModal(props.modalKey);
- setUpdate(true);
- let states = selections.reduce<ProcessState>(
- (v, curr) => ({ [curr.path]: State.Pending, ...v }),
- {}
- );
- setProcessState(states);
-
- for (const raw of selections) {
- states = {
- ...states,
- [raw.path]: State.Processing,
- };
- setProcessState(states);
+ const tasks = selections.map((s) => {
const form: FormType.ModifySubtitle = {
- id: raw.id,
- type: raw.type,
- language: raw.language,
- path: raw.path,
+ id: s.id,
+ type: s.type,
+ language: s.language,
+ path: s.path,
...override,
};
- await SubtitlesApi.modify(action, form);
-
- states = {
- ...states,
- [raw.path]: State.Done,
- };
- setProcessState(states);
- }
- setUpdate(false);
+ return createTask(
+ s.path,
+ s.id,
+ SubtitlesApi.modify.bind(SubtitlesApi),
+ action,
+ form
+ );
+ });
+
+ BackgroundTask.dispatch(TaskGroupName, tasks);
},
[closeModal, selections, props.modalKey]
);
@@ -382,21 +336,6 @@ const STM: FunctionComponent<BaseModalProps & STMProps> = ({ ...props }) => {
const columns: Column<TableColumnType>[] = useMemo<Column<TableColumnType>[]>(
() => [
{
- id: "state",
- accessor: "path",
- selectHide: true,
- Cell: ({ value, loose }) => {
- if (loose) {
- const stateList = loose[0] as ProcessState;
- if (value in stateList) {
- const state = stateList[value];
- return <StateIcon state={state}></StateIcon>;
- }
- }
- return null;
- },
- },
- {
Header: "Language",
accessor: "_language",
Cell: ({ value }) => (
@@ -459,15 +398,14 @@ const STM: FunctionComponent<BaseModalProps & STMProps> = ({ ...props }) => {
<Dropdown as={ButtonGroup} onSelect={(k) => k && process(k)}>
<ActionButton
size="sm"
- loading={updating}
- disabled={selections.length === 0}
+ disabled={selections.length === 0 || hasTask}
icon={faPlay}
onClick={() => process("sync")}
>
Sync
</ActionButton>
<Dropdown.Toggle
- disabled={updating || selections.length === 0}
+ disabled={selections.length === 0 || hasTask}
split
variant="light"
size="sm"
@@ -511,25 +449,19 @@ const STM: FunctionComponent<BaseModalProps & STMProps> = ({ ...props }) => {
</Dropdown.Menu>
</Dropdown>
),
- [showModal, updating, selections.length, process]
+ [showModal, selections.length, process, hasTask]
);
return (
<React.Fragment>
- <BaseModal
- title={"Subtitle Tools"}
- footer={footer}
- closeable={!updating}
- {...props}
- >
+ <BaseModal title={"Subtitle Tools"} footer={footer} {...props}>
<SimpleTable
- isSelecting={!updating && data.length !== 0}
+ isSelecting={data.length !== 0}
emptyText="No External Subtitles Found"
plugins={plugins}
columns={columns}
onSelect={setSelections}
data={data}
- loose={[processState]}
></SimpleTable>
</BaseModal>
<AddColorModal process={process} modalKey="add-color"></AddColorModal>
diff --git a/frontend/src/components/modals/hooks.tsx b/frontend/src/components/modals/hooks.tsx
index ce2c3010b..8cb762c9a 100644
--- a/frontend/src/components/modals/hooks.tsx
+++ b/frontend/src/components/modals/hooks.tsx
@@ -41,46 +41,16 @@ export function useShowModal() {
export function useCloseModal() {
const {
- control: { pop, peek },
+ control: { pop },
} = useContext(ModalContext);
return useCallback(
(key?: string) => {
- const modal = peek();
- if (key) {
- if (modal?.key === key) {
- pop();
- }
- } else {
- pop();
- }
+ pop(key);
},
- [pop, peek]
+ [pop]
);
}
-export function useCloseModalIfCovered() {
- const {
- control: { pop, peek },
- } = useContext(ModalContext);
- return useCallback(
- (key: string) => {
- let modal = peek();
- if (modal && modal.key !== key) {
- pop();
- }
- },
- [pop, peek]
- );
-}
-
-export function useModalIsCovered(key: string) {
- const { modals } = useContext(ModalContext);
- return useMemo(() => {
- const idx = modals.findIndex((v) => v.key === key);
- return idx !== -1 && idx !== 0;
- }, [modals, key]);
-}
-
export function useIsModalShow(key: string) {
const {
control: { peek },
diff --git a/frontend/src/components/modals/provider.tsx b/frontend/src/components/modals/provider.tsx
index 2481b62bb..0681537da 100644
--- a/frontend/src/components/modals/provider.tsx
+++ b/frontend/src/components/modals/provider.tsx
@@ -1,5 +1,9 @@
-import React, { FunctionComponent, useMemo } from "react";
-import { useStackState } from "rooks";
+import React, {
+ FunctionComponent,
+ useCallback,
+ useMemo,
+ useState,
+} from "react";
interface Modal {
key: string;
@@ -9,7 +13,7 @@ interface Modal {
interface ModalControl {
push: (modal: Modal) => void;
peek: () => Modal | undefined;
- pop: () => Modal | undefined;
+ pop: (key: string | undefined) => void;
}
interface ModalContextType {
@@ -33,7 +37,39 @@ export const ModalContext = React.createContext<ModalContextType>({
});
const ModalProvider: FunctionComponent = ({ children }) => {
- const [stack, { push, pop, peek }] = useStackState([]);
+ const [stack, setStack] = useState<Modal[]>([]);
+
+ const push = useCallback<ModalControl["push"]>((model) => {
+ setStack((old) => {
+ return [...old, model];
+ });
+ }, []);
+
+ const pop = useCallback<ModalControl["pop"]>((key) => {
+ setStack((old) => {
+ if (old.length === 0) {
+ return [];
+ }
+
+ if (key === undefined) {
+ const newOld = old;
+ newOld.pop();
+ return newOld;
+ }
+
+ // find key
+ const index = old.findIndex((v) => v.key === key);
+ if (index !== -1) {
+ return old.slice(0, index);
+ } else {
+ return old;
+ }
+ });
+ }, []);
+
+ const peek = useCallback<ModalControl["peek"]>(() => {
+ return stack.length > 0 ? stack[stack.length - 1] : undefined;
+ }, [stack]);
const context = useMemo<ModalContextType>(
() => ({ modals: stack, control: { push, pop, peek } }),