mirror of https://github.com/morpheus65535/bazarr
Add background task support for UI. Let subtitle modification runs in background
This commit is contained in:
parent
eb47356d10
commit
9c8119df3b
|
@ -57,6 +57,6 @@ declare namespace SocketIO {
|
|||
};
|
||||
|
||||
namespace CustomEvent {
|
||||
type Progress = Server.Progress;
|
||||
type Progress = Site.Progress;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
|
@ -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>[];
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -5,7 +5,9 @@ declare namespace Server {
|
|||
message: string;
|
||||
timeout: number;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace Site {
|
||||
interface Progress {
|
||||
id: string;
|
||||
header: string;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
return createTask(
|
||||
s.path,
|
||||
s.id,
|
||||
SubtitlesApi.modify.bind(SubtitlesApi),
|
||||
action,
|
||||
form
|
||||
);
|
||||
});
|
||||
|
||||
states = {
|
||||
...states,
|
||||
[raw.path]: State.Done,
|
||||
};
|
||||
setProcessState(states);
|
||||
}
|
||||
setUpdate(false);
|
||||
BackgroundTask.dispatch(TaskGroupName, tasks);
|
||||
},
|
||||
[closeModal, selections, props.modalKey]
|
||||
);
|
||||
|
@ -381,21 +335,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",
|
||||
|
@ -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>
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 } }),
|
||||
|
|
Loading…
Reference in New Issue