diff options
author | LASER-Yi <[email protected]> | 2021-08-22 00:44:52 +0800 |
---|---|---|
committer | LASER-Yi <[email protected]> | 2021-08-22 00:44:52 +0800 |
commit | 9c8119df3b0781119592f06da76fe4a37f739989 (patch) | |
tree | 1c648d87d6998298108a46de6c52251b99d7cee8 /frontend | |
parent | eb47356d1017b5fc329cc85cbab06de069a43823 (diff) | |
download | bazarr-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.ts | 13 | ||||
-rw-r--r-- | frontend/src/@modules/task/index.ts | 62 | ||||
-rw-r--r-- | frontend/src/@modules/task/task.d.ts | 14 | ||||
-rw-r--r-- | frontend/src/@modules/task/utilites.ts | 13 | ||||
-rw-r--r-- | frontend/src/@redux/actions/site.ts | 2 | ||||
-rw-r--r-- | frontend/src/@redux/reducers/site.ts | 2 | ||||
-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.tsx | 11 | ||||
-rw-r--r-- | frontend/src/components/modals/BaseModal.tsx | 6 | ||||
-rw-r--r-- | frontend/src/components/modals/SubtitleToolModal.tsx | 128 | ||||
-rw-r--r-- | frontend/src/components/modals/hooks.tsx | 36 | ||||
-rw-r--r-- | frontend/src/components/modals/provider.tsx | 44 |
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 } }), |