mirror of https://github.com/morpheus65535/bazarr
Merge remote-tracking branch 'origin/development' into development
This commit is contained in:
commit
a455dc2379
|
@ -131,6 +131,5 @@ export * from "./buttons";
|
|||
export * from "./header";
|
||||
export * from "./inputs";
|
||||
export * from "./LanguageSelector";
|
||||
export * from "./modals";
|
||||
export * from "./SearchBar";
|
||||
export * from "./tables";
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import { useIsShowed, useModalControl } from "@/modules/redux/hooks/modal";
|
||||
import clsx from "clsx";
|
||||
import { FunctionComponent, useCallback, useState } from "react";
|
||||
import { Modal } from "react-bootstrap";
|
||||
|
||||
export interface BaseModalProps {
|
||||
modalKey: string;
|
||||
size?: "sm" | "lg" | "xl";
|
||||
closeable?: boolean;
|
||||
title?: string;
|
||||
footer?: JSX.Element;
|
||||
}
|
||||
|
||||
export const BaseModal: FunctionComponent<BaseModalProps> = (props) => {
|
||||
const { size, modalKey, title, children, footer, closeable = true } = props;
|
||||
const [needExit, setExit] = useState(false);
|
||||
|
||||
const { hide: hideModal } = useModalControl();
|
||||
const showIndex = useIsShowed(modalKey);
|
||||
const isShowed = showIndex !== -1;
|
||||
|
||||
const hide = useCallback(() => {
|
||||
setExit(true);
|
||||
}, []);
|
||||
|
||||
const exit = useCallback(() => {
|
||||
if (isShowed) {
|
||||
hideModal(modalKey);
|
||||
}
|
||||
setExit(false);
|
||||
}, [isShowed, hideModal, modalKey]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
size={size}
|
||||
show={isShowed && !needExit}
|
||||
onHide={hide}
|
||||
onExited={exit}
|
||||
backdrop={closeable ? undefined : "static"}
|
||||
className={clsx(`index-${showIndex}`)}
|
||||
backdropClassName={clsx(`index-${showIndex}`)}
|
||||
>
|
||||
<Modal.Header closeButton={closeable}>{title}</Modal.Header>
|
||||
<Modal.Body>{children}</Modal.Body>
|
||||
<Modal.Footer hidden={footer === undefined}>{footer}</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseModal;
|
|
@ -4,18 +4,17 @@ import {
|
|||
useMovieAddBlacklist,
|
||||
useMovieHistory,
|
||||
} from "@/apis/hooks";
|
||||
import { usePayload } from "@/modules/redux/hooks/modal";
|
||||
import { useModal, usePayload, withModal } from "@/modules/modals";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
import { HistoryIcon, PageTable, QueryOverlay, TextPopover } from "..";
|
||||
import Language from "../bazarr/Language";
|
||||
import { BlacklistButton } from "../inputs/blacklist";
|
||||
import BaseModal, { BaseModalProps } from "./BaseModal";
|
||||
|
||||
export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => {
|
||||
const { ...modal } = props;
|
||||
const MovieHistoryView: FunctionComponent = () => {
|
||||
const movie = usePayload<Item.Movie>();
|
||||
|
||||
const movie = usePayload<Item.Movie>(modal.modalKey);
|
||||
const Modal = useModal({ size: "lg" });
|
||||
|
||||
const history = useMovieHistory(movie?.radarrId);
|
||||
|
||||
|
@ -84,7 +83,7 @@ export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<BaseModal title={`History - ${movie?.title ?? ""}`} {...modal}>
|
||||
<Modal title={`History - ${movie?.title ?? ""}`}>
|
||||
<QueryOverlay result={history}>
|
||||
<PageTable
|
||||
emptyText="No History Found"
|
||||
|
@ -92,14 +91,16 @@ export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => {
|
|||
data={data ?? []}
|
||||
></PageTable>
|
||||
</QueryOverlay>
|
||||
</BaseModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = (
|
||||
props
|
||||
) => {
|
||||
const episode = usePayload<Item.Episode>(props.modalKey);
|
||||
export const MovieHistoryModal = withModal(MovieHistoryView, "movie-history");
|
||||
|
||||
const EpisodeHistoryView: FunctionComponent = () => {
|
||||
const episode = usePayload<Item.Episode>();
|
||||
|
||||
const Modal = useModal({ size: "lg" });
|
||||
|
||||
const history = useEpisodeHistory(episode?.sonarrEpisodeId);
|
||||
|
||||
|
@ -175,7 +176,7 @@ export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = (
|
|||
);
|
||||
|
||||
return (
|
||||
<BaseModal title={`History - ${episode?.title ?? ""}`} {...props}>
|
||||
<Modal title={`History - ${episode?.title ?? ""}`}>
|
||||
<QueryOverlay result={history}>
|
||||
<PageTable
|
||||
emptyText="No History Found"
|
||||
|
@ -183,6 +184,11 @@ export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = (
|
|||
data={data ?? []}
|
||||
></PageTable>
|
||||
</QueryOverlay>
|
||||
</BaseModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const EpisodeHistoryModal = withModal(
|
||||
EpisodeHistoryView,
|
||||
"episode-history"
|
||||
);
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
import { useIsAnyActionRunning, useLanguageProfiles } from "@/apis/hooks";
|
||||
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
|
||||
import {
|
||||
useModal,
|
||||
useModalControl,
|
||||
usePayload,
|
||||
withModal,
|
||||
} from "@/modules/modals";
|
||||
import { GetItemId } from "@/utilities";
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from "react";
|
||||
import { FunctionComponent, useMemo, useState } from "react";
|
||||
import { Container, Form } from "react-bootstrap";
|
||||
import { UseMutationResult } from "react-query";
|
||||
import { AsyncButton, Selector, SelectorOption } from "..";
|
||||
import BaseModal, { BaseModalProps } from "./BaseModal";
|
||||
|
||||
interface Props {
|
||||
mutation: UseMutationResult<void, unknown, FormType.ModifyItem, unknown>;
|
||||
}
|
||||
|
||||
const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
|
||||
const { mutation, ...modal } = props;
|
||||
|
||||
const Editor: FunctionComponent<Props> = ({ mutation }) => {
|
||||
const { data: profiles } = useLanguageProfiles();
|
||||
|
||||
const payload = usePayload<Item.Base>(modal.modalKey);
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const payload = usePayload<Item.Base>();
|
||||
const { mutateAsync, isLoading } = mutation;
|
||||
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const hasTask = useIsAnyActionRunning();
|
||||
|
||||
const profileOptions = useMemo<SelectorOption<number>[]>(
|
||||
|
@ -33,9 +35,12 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
|
|||
|
||||
const [id, setId] = useState<Nullable<number>>(payload?.profileId ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
setId(payload?.profileId ?? null);
|
||||
}, [payload]);
|
||||
const Modal = useModal({
|
||||
closeable: !isLoading,
|
||||
onMounted: () => {
|
||||
setId(payload?.profileId ?? null);
|
||||
},
|
||||
});
|
||||
|
||||
const footer = (
|
||||
<AsyncButton
|
||||
|
@ -56,21 +61,14 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
|
|||
return null;
|
||||
}
|
||||
}}
|
||||
onSuccess={() => {
|
||||
hide();
|
||||
}}
|
||||
onSuccess={() => hide()}
|
||||
>
|
||||
Save
|
||||
</AsyncButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
closeable={!isLoading}
|
||||
footer={footer}
|
||||
title={payload?.title}
|
||||
{...modal}
|
||||
>
|
||||
<Modal title={payload?.title ?? "Item Editor"} footer={footer}>
|
||||
<Container fluid>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
|
@ -95,8 +93,8 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
|
|||
</Form.Group>
|
||||
</Form>
|
||||
</Container>
|
||||
</BaseModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
export default withModal(Editor, "edit");
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { usePayload } from "@/modules/redux/hooks/modal";
|
||||
import { useModal, usePayload, withModal } from "@/modules/modals";
|
||||
import { createAndDispatchTask } from "@/modules/task/utilities";
|
||||
import { GetItemId, isMovie } from "@/utilities";
|
||||
import {
|
||||
|
@ -10,13 +10,7 @@ import {
|
|||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
|
@ -29,7 +23,7 @@ import {
|
|||
} from "react-bootstrap";
|
||||
import { UseQueryResult } from "react-query";
|
||||
import { Column } from "react-table";
|
||||
import { BaseModal, BaseModalProps, LoadingIndicator, PageTable } from "..";
|
||||
import { LoadingIndicator, PageTable } from "..";
|
||||
import Language from "../bazarr/Language";
|
||||
|
||||
type SupportType = Item.Movie | Item.Episode;
|
||||
|
@ -41,24 +35,15 @@ interface Props<T extends SupportType> {
|
|||
) => UseQueryResult<SearchResultType[] | undefined, unknown>;
|
||||
}
|
||||
|
||||
export function ManualSearchModal<T extends SupportType>(
|
||||
props: Props<T> & BaseModalProps
|
||||
) {
|
||||
const { download, query: useSearch, ...modal } = props;
|
||||
function ManualSearchView<T extends SupportType>(props: Props<T>) {
|
||||
const { download, query: useSearch } = props;
|
||||
|
||||
const item = usePayload<T>(modal.modalKey);
|
||||
const item = usePayload<T>();
|
||||
|
||||
const itemId = useMemo(() => GetItemId(item ?? {}), [item]);
|
||||
|
||||
const [id, setId] = useState<number | undefined>(undefined);
|
||||
|
||||
// Cleanup the ID when user switches episode / movie
|
||||
useEffect(() => {
|
||||
if (itemId !== undefined && itemId !== id) {
|
||||
setId(undefined);
|
||||
}
|
||||
}, [id, itemId]);
|
||||
|
||||
const results = useSearch(id);
|
||||
|
||||
const isStale = results.data === undefined;
|
||||
|
@ -225,12 +210,6 @@ export function ManualSearchModal<T extends SupportType>(
|
|||
}
|
||||
};
|
||||
|
||||
const footer = (
|
||||
<Button variant="light" hidden={isStale} onClick={search}>
|
||||
Search Again
|
||||
</Button>
|
||||
);
|
||||
|
||||
const title = useMemo(() => {
|
||||
let title = "Unknown";
|
||||
|
||||
|
@ -246,19 +225,39 @@ export function ManualSearchModal<T extends SupportType>(
|
|||
return `Search - ${title}`;
|
||||
}, [item]);
|
||||
|
||||
const Modal = useModal({
|
||||
size: "xl",
|
||||
closeable: results.isFetching === false,
|
||||
onMounted: () => {
|
||||
// Cleanup the ID when user switches episode / movie
|
||||
if (itemId !== id) {
|
||||
setId(undefined);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const footer = (
|
||||
<Button variant="light" hidden={isStale} onClick={search}>
|
||||
Search Again
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
closeable={results.isFetching === false}
|
||||
size="xl"
|
||||
title={title}
|
||||
footer={footer}
|
||||
{...modal}
|
||||
>
|
||||
<Modal title={title} footer={footer}>
|
||||
{content()}
|
||||
</BaseModal>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export const MovieSearchModal = withModal<Props<Item.Movie>>(
|
||||
ManualSearchView,
|
||||
"movie-manual-search"
|
||||
);
|
||||
export const EpisodeSearchModal = withModal<Props<Item.Episode>>(
|
||||
ManualSearchView,
|
||||
"episode-manual-search"
|
||||
);
|
||||
|
||||
const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({
|
||||
matches,
|
||||
dont,
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import { useMovieSubtitleModification } from "@/apis/hooks";
|
||||
import { usePayload } from "@/modules/redux/hooks/modal";
|
||||
import { usePayload, withModal } from "@/modules/modals";
|
||||
import { createTask, dispatchTask } from "@/modules/task/utilities";
|
||||
import {
|
||||
useLanguageProfileBy,
|
||||
useProfileItemsToLanguages,
|
||||
} from "@/utilities/languages";
|
||||
import { FunctionComponent, useCallback } from "react";
|
||||
import { BaseModalProps } from "./BaseModal";
|
||||
import SubtitleUploadModal, {
|
||||
import SubtitleUploader, {
|
||||
PendingSubtitle,
|
||||
Validator,
|
||||
} from "./SubtitleUploadModal";
|
||||
|
||||
const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
|
||||
const modal = props;
|
||||
|
||||
const payload = usePayload<Item.Movie>(modal.modalKey);
|
||||
const MovieUploadModal: FunctionComponent = () => {
|
||||
const payload = usePayload<Item.Movie>();
|
||||
|
||||
const profile = useLanguageProfileBy(payload?.profileId);
|
||||
|
||||
|
@ -87,7 +84,7 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<SubtitleUploadModal
|
||||
<SubtitleUploader
|
||||
hideAllLanguages
|
||||
initial={{ forced: false }}
|
||||
availableLanguages={availableLanguages}
|
||||
|
@ -95,9 +92,8 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
|
|||
upload={upload}
|
||||
update={update}
|
||||
validate={validate}
|
||||
{...modal}
|
||||
></SubtitleUploadModal>
|
||||
></SubtitleUploader>
|
||||
);
|
||||
};
|
||||
|
||||
export default MovieUploadModal;
|
||||
export default withModal(MovieUploadModal, "movie-upload");
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEpisodeSubtitleModification } from "@/apis/hooks";
|
||||
import api from "@/apis/raw";
|
||||
import { usePayload } from "@/modules/redux/hooks/modal";
|
||||
import { usePayload, withModal } from "@/modules/modals";
|
||||
import { createTask, dispatchTask } from "@/modules/task/utilities";
|
||||
import {
|
||||
useLanguageProfileBy,
|
||||
|
@ -9,8 +9,7 @@ import {
|
|||
import { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
import { Selector, SelectorOption } from "../inputs";
|
||||
import { BaseModalProps } from "./BaseModal";
|
||||
import SubtitleUploadModal, {
|
||||
import SubtitleUploader, {
|
||||
PendingSubtitle,
|
||||
useRowMutation,
|
||||
Validator,
|
||||
|
@ -24,11 +23,8 @@ interface SeriesProps {
|
|||
episodes: readonly Item.Episode[];
|
||||
}
|
||||
|
||||
const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({
|
||||
episodes,
|
||||
...modal
|
||||
}) => {
|
||||
const payload = usePayload<Item.Series>(modal.modalKey);
|
||||
const SeriesUploadModal: FunctionComponent<SeriesProps> = ({ episodes }) => {
|
||||
const payload = usePayload<Item.Series>();
|
||||
|
||||
const profile = useLanguageProfileBy(payload?.profileId);
|
||||
|
||||
|
@ -165,16 +161,15 @@ const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<SubtitleUploadModal
|
||||
<SubtitleUploader
|
||||
columns={columns}
|
||||
initial={{ instance: null }}
|
||||
availableLanguages={availableLanguages}
|
||||
upload={upload}
|
||||
update={update}
|
||||
validate={validate}
|
||||
{...modal}
|
||||
></SubtitleUploadModal>
|
||||
></SubtitleUploader>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeriesUploadModal;
|
||||
export default withModal(SeriesUploadModal, "series-upload");
|
||||
|
|
|
@ -1,474 +0,0 @@
|
|||
import { useSubtitleAction } from "@/apis/hooks";
|
||||
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
|
||||
import { createTask, dispatchTask } from "@/modules/task/utilities";
|
||||
import { isMovie, submodProcessColor } from "@/utilities";
|
||||
import { LOG } from "@/utilities/console";
|
||||
import { useEnabledLanguages } from "@/utilities/languages";
|
||||
import {
|
||||
faClock,
|
||||
faCode,
|
||||
faDeaf,
|
||||
faExchangeAlt,
|
||||
faFilm,
|
||||
faImage,
|
||||
faLanguage,
|
||||
faMagic,
|
||||
faMinus,
|
||||
faPaintBrush,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faTextHeight,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Dropdown,
|
||||
Form,
|
||||
InputGroup,
|
||||
} from "react-bootstrap";
|
||||
import { Column, useRowSelect } from "react-table";
|
||||
import {
|
||||
ActionButton,
|
||||
ActionButtonItem,
|
||||
LanguageSelector,
|
||||
Selector,
|
||||
SimpleTable,
|
||||
} from "..";
|
||||
import Language from "../bazarr/Language";
|
||||
import { useCustomSelection } from "../tables/plugins";
|
||||
import BaseModal, { BaseModalProps } from "./BaseModal";
|
||||
import { availableTranslation, colorOptions } from "./toolOptions";
|
||||
|
||||
type SupportType = Item.Episode | Item.Movie;
|
||||
|
||||
type TableColumnType = FormType.ModifySubtitle & {
|
||||
_language: Language.Info;
|
||||
};
|
||||
|
||||
function getIdAndType(item: SupportType): [number, "episode" | "movie"] {
|
||||
if (isMovie(item)) {
|
||||
return [item.radarrId, "movie"];
|
||||
} else {
|
||||
return [item.sonarrEpisodeId, "episode"];
|
||||
}
|
||||
}
|
||||
|
||||
function submodProcessFrameRate(from: number, to: number) {
|
||||
return `change_FPS(from=${from},to=${to})`;
|
||||
}
|
||||
|
||||
function submodProcessOffset(h: number, m: number, s: number, ms: number) {
|
||||
return `shift_offset(h=${h},m=${m},s=${s},ms=${ms})`;
|
||||
}
|
||||
|
||||
interface ToolModalProps {
|
||||
process: (
|
||||
action: string,
|
||||
override?: Partial<FormType.ModifySubtitle>
|
||||
) => void;
|
||||
}
|
||||
|
||||
const AddColorModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
|
||||
props
|
||||
) => {
|
||||
const { process, ...modal } = props;
|
||||
const [selection, setSelection] = useState<Nullable<string>>(null);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (selection) {
|
||||
const action = submodProcessColor(selection);
|
||||
process(action);
|
||||
}
|
||||
}, [selection, process]);
|
||||
|
||||
const footer = useMemo(
|
||||
() => (
|
||||
<Button disabled={selection === null} onClick={submit}>
|
||||
Save
|
||||
</Button>
|
||||
),
|
||||
[selection, submit]
|
||||
);
|
||||
return (
|
||||
<BaseModal title="Choose Color" footer={footer} {...modal}>
|
||||
<Selector options={colorOptions} onChange={setSelection}></Selector>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
const FrameRateModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
|
||||
props
|
||||
) => {
|
||||
const { process, ...modal } = props;
|
||||
|
||||
const [from, setFrom] = useState<Nullable<number>>(null);
|
||||
const [to, setTo] = useState<Nullable<number>>(null);
|
||||
|
||||
const canSave = from !== null && to !== null && from !== to;
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (canSave) {
|
||||
const action = submodProcessFrameRate(from, to);
|
||||
process(action);
|
||||
}
|
||||
}, [canSave, from, to, process]);
|
||||
|
||||
const footer = (
|
||||
<Button disabled={!canSave} onClick={submit}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal title="Change Frame Rate" footer={footer} {...modal}>
|
||||
<InputGroup className="px-2">
|
||||
<Form.Control
|
||||
placeholder="From"
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = parseFloat(e.currentTarget.value);
|
||||
if (isNaN(value)) {
|
||||
setFrom(null);
|
||||
} else {
|
||||
setFrom(value);
|
||||
}
|
||||
}}
|
||||
></Form.Control>
|
||||
<Form.Control
|
||||
placeholder="To"
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = parseFloat(e.currentTarget.value);
|
||||
if (isNaN(value)) {
|
||||
setTo(null);
|
||||
} else {
|
||||
setTo(value);
|
||||
}
|
||||
}}
|
||||
></Form.Control>
|
||||
</InputGroup>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
const AdjustTimesModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
|
||||
props
|
||||
) => {
|
||||
const { process, ...modal } = props;
|
||||
|
||||
const [isPlus, setPlus] = useState(true);
|
||||
const [offset, setOffset] = useState<[number, number, number, number]>([
|
||||
0, 0, 0, 0,
|
||||
]);
|
||||
|
||||
const updateOffset = useCallback(
|
||||
(idx: number): ChangeEventHandler<HTMLInputElement> => {
|
||||
return (e) => {
|
||||
let value = parseFloat(e.currentTarget.value);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
const newOffset = [...offset] as [number, number, number, number];
|
||||
newOffset[idx] = value;
|
||||
setOffset(newOffset);
|
||||
};
|
||||
},
|
||||
[offset]
|
||||
);
|
||||
|
||||
const canSave = offset.some((v) => v !== 0);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (canSave) {
|
||||
const newOffset = offset.map((v) => (isPlus ? v : -v));
|
||||
const action = submodProcessOffset(
|
||||
newOffset[0],
|
||||
newOffset[1],
|
||||
newOffset[2],
|
||||
newOffset[3]
|
||||
);
|
||||
process(action);
|
||||
}
|
||||
}, [process, canSave, offset, isPlus]);
|
||||
|
||||
const footer = useMemo(
|
||||
() => (
|
||||
<Button disabled={!canSave} onClick={submit}>
|
||||
Save
|
||||
</Button>
|
||||
),
|
||||
[submit, canSave]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal title="Adjust Times" footer={footer} {...modal}>
|
||||
<InputGroup>
|
||||
<InputGroup.Prepend>
|
||||
<Button
|
||||
variant="secondary"
|
||||
title={isPlus ? "Later" : "Earlier"}
|
||||
onClick={() => setPlus(!isPlus)}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPlus ? faPlus : faMinus}></FontAwesomeIcon>
|
||||
</Button>
|
||||
</InputGroup.Prepend>
|
||||
<Form.Control
|
||||
type="number"
|
||||
placeholder="hour"
|
||||
onChange={updateOffset(0)}
|
||||
></Form.Control>
|
||||
<Form.Control
|
||||
type="number"
|
||||
placeholder="min"
|
||||
onChange={updateOffset(1)}
|
||||
></Form.Control>
|
||||
<Form.Control
|
||||
type="number"
|
||||
placeholder="sec"
|
||||
onChange={updateOffset(2)}
|
||||
></Form.Control>
|
||||
<Form.Control
|
||||
type="number"
|
||||
placeholder="ms"
|
||||
onChange={updateOffset(3)}
|
||||
></Form.Control>
|
||||
</InputGroup>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({
|
||||
process,
|
||||
...modal
|
||||
}) => {
|
||||
const { data: languages } = useEnabledLanguages();
|
||||
|
||||
const available = useMemo(
|
||||
() => languages.filter((v) => v.code2 in availableTranslation),
|
||||
[languages]
|
||||
);
|
||||
|
||||
const [selectedLanguage, setLanguage] =
|
||||
useState<Nullable<Language.Info>>(null);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (selectedLanguage) {
|
||||
process("translate", { language: selectedLanguage.code2 });
|
||||
}
|
||||
}, [selectedLanguage, process]);
|
||||
|
||||
const footer = useMemo(
|
||||
() => (
|
||||
<Button disabled={!selectedLanguage} onClick={submit}>
|
||||
Translate
|
||||
</Button>
|
||||
),
|
||||
[submit, selectedLanguage]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal title="Translate to" footer={footer} {...modal}>
|
||||
<Form.Label>
|
||||
Enabled languages not listed here are unsupported by Google Translate.
|
||||
</Form.Label>
|
||||
<LanguageSelector
|
||||
options={available}
|
||||
onChange={setLanguage}
|
||||
></LanguageSelector>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
const CanSelectSubtitle = (item: TableColumnType) => {
|
||||
return item.path.endsWith(".srt");
|
||||
};
|
||||
|
||||
const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
|
||||
const payload = usePayload<SupportType[]>(props.modalKey);
|
||||
const [selections, setSelections] = useState<TableColumnType[]>([]);
|
||||
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const { mutateAsync } = useSubtitleAction();
|
||||
|
||||
const process = useCallback(
|
||||
(action: string, override?: Partial<FormType.ModifySubtitle>) => {
|
||||
LOG("info", "executing action", action);
|
||||
hide(props.modalKey);
|
||||
|
||||
const tasks = selections.map((s) => {
|
||||
const form: FormType.ModifySubtitle = {
|
||||
id: s.id,
|
||||
type: s.type,
|
||||
language: s.language,
|
||||
path: s.path,
|
||||
...override,
|
||||
};
|
||||
return createTask(s.path, mutateAsync, { action, form });
|
||||
});
|
||||
|
||||
dispatchTask(tasks, "modify-subtitles");
|
||||
},
|
||||
[hide, props.modalKey, selections, mutateAsync]
|
||||
);
|
||||
|
||||
const { show } = useModalControl();
|
||||
|
||||
const columns: Column<TableColumnType>[] = useMemo<Column<TableColumnType>[]>(
|
||||
() => [
|
||||
{
|
||||
Header: "Language",
|
||||
accessor: "_language",
|
||||
Cell: ({ value }) => (
|
||||
<Badge variant="secondary">
|
||||
<Language.Text value={value} long></Language.Text>
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "file",
|
||||
Header: "File",
|
||||
accessor: "path",
|
||||
Cell: ({ value }) => {
|
||||
const path = value;
|
||||
|
||||
let idx = path.lastIndexOf("/");
|
||||
|
||||
if (idx === -1) {
|
||||
idx = path.lastIndexOf("\\");
|
||||
}
|
||||
|
||||
if (idx !== -1) {
|
||||
return path.slice(idx + 1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const data = useMemo<TableColumnType[]>(
|
||||
() =>
|
||||
payload?.flatMap((item) => {
|
||||
const [id, type] = getIdAndType(item);
|
||||
return item.subtitles.flatMap((v) => {
|
||||
if (v.path !== null) {
|
||||
return [
|
||||
{
|
||||
id,
|
||||
type,
|
||||
language: v.code2,
|
||||
path: v.path,
|
||||
_language: v,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}) ?? [],
|
||||
[payload]
|
||||
);
|
||||
|
||||
const plugins = [useRowSelect, useCustomSelection];
|
||||
|
||||
const footer = useMemo(
|
||||
() => (
|
||||
<Dropdown as={ButtonGroup} onSelect={(k) => k && process(k)}>
|
||||
<ActionButton
|
||||
size="sm"
|
||||
disabled={selections.length === 0}
|
||||
icon={faPlay}
|
||||
onClick={() => process("sync")}
|
||||
>
|
||||
Sync
|
||||
</ActionButton>
|
||||
<Dropdown.Toggle
|
||||
disabled={selections.length === 0}
|
||||
split
|
||||
variant="light"
|
||||
size="sm"
|
||||
className="px-2"
|
||||
></Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item eventKey="remove_HI">
|
||||
<ActionButtonItem icon={faDeaf}>Remove HI Tags</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item eventKey="remove_tags">
|
||||
<ActionButtonItem icon={faCode}>Remove Style Tags</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item eventKey="OCR_fixes">
|
||||
<ActionButtonItem icon={faImage}>OCR Fixes</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item eventKey="common">
|
||||
<ActionButtonItem icon={faMagic}>Common Fixes</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item eventKey="fix_uppercase">
|
||||
<ActionButtonItem icon={faTextHeight}>
|
||||
Fix Uppercase
|
||||
</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item eventKey="reverse_rtl">
|
||||
<ActionButtonItem icon={faExchangeAlt}>
|
||||
Reverse RTL
|
||||
</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onSelect={() => show("add-color")}>
|
||||
<ActionButtonItem icon={faPaintBrush}>Add Color</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onSelect={() => show("change-frame-rate")}>
|
||||
<ActionButtonItem icon={faFilm}>Change Frame Rate</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onSelect={() => show("adjust-times")}>
|
||||
<ActionButtonItem icon={faClock}>Adjust Times</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onSelect={() => show("translate-sub")}>
|
||||
<ActionButtonItem icon={faLanguage}>Translate</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
),
|
||||
[selections.length, process, show]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseModal title={"Subtitle Tools"} footer={footer} {...props}>
|
||||
<SimpleTable
|
||||
emptyText="No External Subtitles Found"
|
||||
plugins={plugins}
|
||||
columns={columns}
|
||||
onSelect={setSelections}
|
||||
canSelect={CanSelectSubtitle}
|
||||
data={data}
|
||||
></SimpleTable>
|
||||
</BaseModal>
|
||||
<AddColorModal process={process} modalKey="add-color"></AddColorModal>
|
||||
<FrameRateModal
|
||||
process={process}
|
||||
modalKey="change-frame-rate"
|
||||
></FrameRateModal>
|
||||
<AdjustTimesModal
|
||||
process={process}
|
||||
modalKey="adjust-times"
|
||||
></AdjustTimesModal>
|
||||
<TranslateModal
|
||||
process={process}
|
||||
modalKey="translate-sub"
|
||||
></TranslateModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default STM;
|
|
@ -1,4 +1,4 @@
|
|||
import { useModalControl } from "@/modules/redux/hooks/modal";
|
||||
import { useModal, useModalControl } from "@/modules/modals";
|
||||
import { BuildKey } from "@/utilities";
|
||||
import { LOG } from "@/utilities/console";
|
||||
import {
|
||||
|
@ -23,7 +23,6 @@ import { Column } from "react-table";
|
|||
import { LanguageSelector, MessageIcon } from "..";
|
||||
import { FileForm } from "../inputs";
|
||||
import { SimpleTable } from "../tables";
|
||||
import BaseModal, { BaseModalProps } from "./BaseModal";
|
||||
|
||||
type ModifyFn<T> = (index: number, info?: PendingSubtitle<T>) => void;
|
||||
|
||||
|
@ -59,10 +58,7 @@ interface Props<T = unknown> {
|
|||
hideAllLanguages?: boolean;
|
||||
}
|
||||
|
||||
type ComponentProps<T> = Props<T> &
|
||||
Omit<BaseModalProps, "footer" | "title" | "size">;
|
||||
|
||||
function SubtitleUploadModal<T>(props: ComponentProps<T>) {
|
||||
function SubtitleUploader<T>(props: Props<T>) {
|
||||
const {
|
||||
initial,
|
||||
columns,
|
||||
|
@ -73,10 +69,16 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) {
|
|||
hideAllLanguages,
|
||||
} = props;
|
||||
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const [pending, setPending] = useState<PendingSubtitle<T>[]>([]);
|
||||
|
||||
const showTable = pending.length > 0;
|
||||
|
||||
const Modal = useModal({
|
||||
size: showTable ? "xl" : "lg",
|
||||
});
|
||||
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const fileList = useMemo(() => pending.map((v) => v.file), [pending]);
|
||||
|
||||
const initialRef = useRef(initial);
|
||||
|
@ -281,8 +283,6 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) {
|
|||
[columns, availableLanguages]
|
||||
);
|
||||
|
||||
const showTable = pending.length > 0;
|
||||
|
||||
const canUpload = useMemo(
|
||||
() =>
|
||||
pending.length > 0 &&
|
||||
|
@ -332,12 +332,7 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) {
|
|||
);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
size={showTable ? "xl" : "lg"}
|
||||
title="Upload Subtitles"
|
||||
footer={footer}
|
||||
{...props}
|
||||
>
|
||||
<Modal title="Update Subtitles" footer={footer}>
|
||||
<Container fluid className="flex-column">
|
||||
<Form>
|
||||
<Form.Group>
|
||||
|
@ -360,8 +355,8 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) {
|
|||
</RowContext.Provider>
|
||||
</div>
|
||||
</Container>
|
||||
</BaseModal>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default SubtitleUploadModal;
|
||||
export default SubtitleUploader;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
export * from "./BaseModal";
|
||||
export * from "./HistoryModal";
|
||||
export { default as ItemEditorModal } from "./ItemEditorModal";
|
||||
export { default as MovieUploadModal } from "./MovieUploadModal";
|
||||
export { default as SeriesUploadModal } from "./SeriesUploadModal";
|
||||
export { default as SubtitleToolModal } from "./SubtitleToolModal";
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { Selector } from "@/components";
|
||||
import { useModal, withModal } from "@/modules/modals";
|
||||
import { submodProcessColor } from "@/utilities";
|
||||
import { FunctionComponent, useCallback, useState } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { useProcess } from "./ToolContext";
|
||||
import { colorOptions } from "./tools";
|
||||
|
||||
const ColorTool: FunctionComponent = () => {
|
||||
const [selection, setSelection] = useState<Nullable<string>>(null);
|
||||
|
||||
const Modal = useModal();
|
||||
|
||||
const process = useProcess();
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (selection) {
|
||||
const action = submodProcessColor(selection);
|
||||
process(action);
|
||||
}
|
||||
}, [process, selection]);
|
||||
|
||||
const footer = (
|
||||
<Button disabled={selection === null} onClick={submit}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal title="Choose Color" footer={footer}>
|
||||
<Selector options={colorOptions} onChange={setSelection}></Selector>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withModal(ColorTool, "color-tool");
|
|
@ -0,0 +1,65 @@
|
|||
import { useModal, withModal } from "@/modules/modals";
|
||||
import { FunctionComponent, useCallback, useState } from "react";
|
||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||
import { useProcess } from "./ToolContext";
|
||||
|
||||
function submodProcessFrameRate(from: number, to: number) {
|
||||
return `change_FPS(from=${from},to=${to})`;
|
||||
}
|
||||
|
||||
const FrameRateTool: FunctionComponent = () => {
|
||||
const [from, setFrom] = useState<Nullable<number>>(null);
|
||||
const [to, setTo] = useState<Nullable<number>>(null);
|
||||
|
||||
const canSave = from !== null && to !== null && from !== to;
|
||||
|
||||
const Modal = useModal();
|
||||
|
||||
const process = useProcess();
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (canSave) {
|
||||
const action = submodProcessFrameRate(from, to);
|
||||
process(action);
|
||||
}
|
||||
}, [canSave, from, process, to]);
|
||||
|
||||
const footer = (
|
||||
<Button disabled={!canSave} onClick={submit}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal title="Change Frame Rate" footer={footer}>
|
||||
<InputGroup className="px-2">
|
||||
<Form.Control
|
||||
placeholder="From"
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = parseFloat(e.currentTarget.value);
|
||||
if (isNaN(value)) {
|
||||
setFrom(null);
|
||||
} else {
|
||||
setFrom(value);
|
||||
}
|
||||
}}
|
||||
></Form.Control>
|
||||
<Form.Control
|
||||
placeholder="To"
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = parseFloat(e.currentTarget.value);
|
||||
if (isNaN(value)) {
|
||||
setTo(null);
|
||||
} else {
|
||||
setTo(value);
|
||||
}
|
||||
}}
|
||||
></Form.Control>
|
||||
</InputGroup>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withModal(FrameRateTool, "frame-rate-tool");
|
|
@ -0,0 +1,100 @@
|
|||
import { useModal, withModal } from "@/modules/modals";
|
||||
import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||
import { useProcess } from "./ToolContext";
|
||||
|
||||
function submodProcessOffset(h: number, m: number, s: number, ms: number) {
|
||||
return `shift_offset(h=${h},m=${m},s=${s},ms=${ms})`;
|
||||
}
|
||||
|
||||
const TimeAdjustmentTool: FunctionComponent = () => {
|
||||
const [isPlus, setPlus] = useState(true);
|
||||
const [offset, setOffset] = useState<[number, number, number, number]>([
|
||||
0, 0, 0, 0,
|
||||
]);
|
||||
|
||||
const Modal = useModal();
|
||||
|
||||
const updateOffset = useCallback(
|
||||
(idx: number): ChangeEventHandler<HTMLInputElement> => {
|
||||
return (e) => {
|
||||
let value = parseFloat(e.currentTarget.value);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
const newOffset = [...offset] as [number, number, number, number];
|
||||
newOffset[idx] = value;
|
||||
setOffset(newOffset);
|
||||
};
|
||||
},
|
||||
[offset]
|
||||
);
|
||||
|
||||
const canSave = offset.some((v) => v !== 0);
|
||||
|
||||
const process = useProcess();
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (canSave) {
|
||||
const newOffset = offset.map((v) => (isPlus ? v : -v));
|
||||
const action = submodProcessOffset(
|
||||
newOffset[0],
|
||||
newOffset[1],
|
||||
newOffset[2],
|
||||
newOffset[3]
|
||||
);
|
||||
process(action);
|
||||
}
|
||||
}, [canSave, offset, process, isPlus]);
|
||||
|
||||
const footer = (
|
||||
<Button disabled={!canSave} onClick={submit}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal title="Adjust Times" footer={footer}>
|
||||
<InputGroup>
|
||||
<InputGroup.Prepend>
|
||||
<Button
|
||||
variant="secondary"
|
||||
title={isPlus ? "Later" : "Earlier"}
|
||||
onClick={() => setPlus(!isPlus)}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPlus ? faPlus : faMinus}></FontAwesomeIcon>
|
||||
</Button>
|
||||
</InputGroup.Prepend>
|
||||
<Form.Control
|
||||
type="number"
|
||||
placeholder="hour"
|
||||
onChange={updateOffset(0)}
|
||||
></Form.Control>
|
||||
<Form.Control
|
||||
type="number"
|
||||
placeholder="min"
|
||||
onChange={updateOffset(1)}
|
||||
></Form.Control>
|
||||
<Form.Control
|
||||
type="number"
|
||||
placeholder="sec"
|
||||
onChange={updateOffset(2)}
|
||||
></Form.Control>
|
||||
<Form.Control
|
||||
type="number"
|
||||
placeholder="ms"
|
||||
onChange={updateOffset(3)}
|
||||
></Form.Control>
|
||||
</InputGroup>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withModal(TimeAdjustmentTool, "time-adjustment");
|
|
@ -0,0 +1,14 @@
|
|||
import { createContext, useContext } from "react";
|
||||
|
||||
export type ProcessSubtitleType = (
|
||||
action: string,
|
||||
override?: Partial<FormType.ModifySubtitle>
|
||||
) => void;
|
||||
|
||||
export const ProcessSubtitleContext = createContext<ProcessSubtitleType>(() => {
|
||||
throw new Error("ProcessSubtitleContext not initialized");
|
||||
});
|
||||
|
||||
export function useProcess() {
|
||||
return useContext(ProcessSubtitleContext);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { LanguageSelector } from "@/components/LanguageSelector";
|
||||
import { useModal, withModal } from "@/modules/modals";
|
||||
import { useEnabledLanguages } from "@/utilities/languages";
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { useProcess } from "./ToolContext";
|
||||
import { availableTranslation } from "./tools";
|
||||
|
||||
const TranslationTool: FunctionComponent = () => {
|
||||
const { data: languages } = useEnabledLanguages();
|
||||
|
||||
const available = useMemo(
|
||||
() => languages.filter((v) => v.code2 in availableTranslation),
|
||||
[languages]
|
||||
);
|
||||
|
||||
const Modal = useModal();
|
||||
|
||||
const [selectedLanguage, setLanguage] =
|
||||
useState<Nullable<Language.Info>>(null);
|
||||
|
||||
const process = useProcess();
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (selectedLanguage) {
|
||||
process("translate", { language: selectedLanguage.code2 });
|
||||
}
|
||||
}, [process, selectedLanguage]);
|
||||
|
||||
const footer = (
|
||||
<Button disabled={!selectedLanguage} onClick={submit}>
|
||||
Translate
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<Modal title="Translation" footer={footer}>
|
||||
<Form.Label>
|
||||
Enabled languages not listed here are unsupported by Google Translate.
|
||||
</Form.Label>
|
||||
<LanguageSelector
|
||||
options={available}
|
||||
onChange={setLanguage}
|
||||
></LanguageSelector>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withModal(TranslationTool, "translation-tool");
|
|
@ -0,0 +1,230 @@
|
|||
import { useSubtitleAction } from "@/apis/hooks";
|
||||
import Language from "@/components/bazarr/Language";
|
||||
import { ActionButton, ActionButtonItem } from "@/components/buttons";
|
||||
import { SimpleTable } from "@/components/tables";
|
||||
import { useCustomSelection } from "@/components/tables/plugins";
|
||||
import {
|
||||
useModal,
|
||||
useModalControl,
|
||||
usePayload,
|
||||
withModal,
|
||||
} from "@/modules/modals";
|
||||
import { createTask, dispatchTask } from "@/modules/task/utilities";
|
||||
import { isMovie } from "@/utilities";
|
||||
import { LOG } from "@/utilities/console";
|
||||
import { isObject } from "lodash";
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from "react";
|
||||
import { Badge, ButtonGroup, Dropdown } from "react-bootstrap";
|
||||
import { Column, useRowSelect } from "react-table";
|
||||
import {
|
||||
ProcessSubtitleContext,
|
||||
ProcessSubtitleType,
|
||||
useProcess,
|
||||
} from "./ToolContext";
|
||||
import { tools } from "./tools";
|
||||
import { ToolOptions } from "./types";
|
||||
|
||||
type SupportType = Item.Episode | Item.Movie;
|
||||
|
||||
type TableColumnType = FormType.ModifySubtitle & {
|
||||
raw_language: Language.Info;
|
||||
};
|
||||
|
||||
function getIdAndType(item: SupportType): [number, "episode" | "movie"] {
|
||||
if (isMovie(item)) {
|
||||
return [item.radarrId, "movie"];
|
||||
} else {
|
||||
return [item.sonarrEpisodeId, "episode"];
|
||||
}
|
||||
}
|
||||
|
||||
const CanSelectSubtitle = (item: TableColumnType) => {
|
||||
return item.path.endsWith(".srt");
|
||||
};
|
||||
|
||||
function isElement(value: unknown): value is JSX.Element {
|
||||
return isObject(value);
|
||||
}
|
||||
|
||||
interface SubtitleToolViewProps {
|
||||
count: number;
|
||||
tools: ToolOptions[];
|
||||
select: (items: TableColumnType[]) => void;
|
||||
}
|
||||
|
||||
const SubtitleToolView: FunctionComponent<SubtitleToolViewProps> = ({
|
||||
tools,
|
||||
count,
|
||||
select,
|
||||
}) => {
|
||||
const payload = usePayload<SupportType[]>();
|
||||
|
||||
const Modal = useModal({
|
||||
size: "lg",
|
||||
});
|
||||
const { show } = useModalControl();
|
||||
|
||||
const columns: Column<TableColumnType>[] = useMemo<Column<TableColumnType>[]>(
|
||||
() => [
|
||||
{
|
||||
Header: "Language",
|
||||
accessor: "raw_language",
|
||||
Cell: ({ value }) => (
|
||||
<Badge variant="secondary">
|
||||
<Language.Text value={value} long></Language.Text>
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "file",
|
||||
Header: "File",
|
||||
accessor: "path",
|
||||
Cell: ({ value }) => {
|
||||
const path = value;
|
||||
|
||||
let idx = path.lastIndexOf("/");
|
||||
|
||||
if (idx === -1) {
|
||||
idx = path.lastIndexOf("\\");
|
||||
}
|
||||
|
||||
if (idx !== -1) {
|
||||
return path.slice(idx + 1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const data = useMemo<TableColumnType[]>(
|
||||
() =>
|
||||
payload?.flatMap((item) => {
|
||||
const [id, type] = getIdAndType(item);
|
||||
return item.subtitles.flatMap((v) => {
|
||||
if (v.path !== null) {
|
||||
return [
|
||||
{
|
||||
id,
|
||||
type,
|
||||
language: v.code2,
|
||||
path: v.path,
|
||||
raw_language: v,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}) ?? [],
|
||||
[payload]
|
||||
);
|
||||
|
||||
const plugins = [useRowSelect, useCustomSelection];
|
||||
|
||||
const process = useProcess();
|
||||
|
||||
const footer = useMemo(() => {
|
||||
const action = tools[0];
|
||||
const others = tools.slice(1);
|
||||
|
||||
return (
|
||||
<Dropdown as={ButtonGroup} onSelect={(k) => k && process(k)}>
|
||||
<ActionButton
|
||||
size="sm"
|
||||
disabled={count === 0}
|
||||
icon={action.icon}
|
||||
onClick={() => process(action.key)}
|
||||
>
|
||||
{action.name}
|
||||
</ActionButton>
|
||||
<Dropdown.Toggle
|
||||
disabled={count === 0}
|
||||
split
|
||||
variant="light"
|
||||
size="sm"
|
||||
className="px-2"
|
||||
></Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{others.map((v) => (
|
||||
<Dropdown.Item
|
||||
key={v.key}
|
||||
eventKey={v.modal ? undefined : v.key}
|
||||
onSelect={() => {
|
||||
if (v.modal) {
|
||||
show(v.modal);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ActionButtonItem icon={v.icon}>{v.name}</ActionButtonItem>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}, [count, process, show, tools]);
|
||||
|
||||
return (
|
||||
<Modal title="Subtitle Tools" footer={footer}>
|
||||
<SimpleTable
|
||||
emptyText="No External Subtitles Found"
|
||||
plugins={plugins}
|
||||
columns={columns}
|
||||
onSelect={select}
|
||||
canSelect={CanSelectSubtitle}
|
||||
data={data}
|
||||
></SimpleTable>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const SubtitleToolModal = withModal(SubtitleToolView, "subtitle-tools");
|
||||
|
||||
const SubtitleTools: FunctionComponent = () => {
|
||||
const modals = useMemo(
|
||||
() =>
|
||||
tools
|
||||
.map((t) => t.modal && <t.modal key={t.key}></t.modal>)
|
||||
.filter(isElement),
|
||||
[]
|
||||
);
|
||||
|
||||
const { hide } = useModalControl();
|
||||
const [selections, setSelections] = useState<TableColumnType[]>([]);
|
||||
const { mutateAsync } = useSubtitleAction();
|
||||
|
||||
const process = useCallback<ProcessSubtitleType>(
|
||||
(action, override) => {
|
||||
LOG("info", "executing action", action);
|
||||
hide(SubtitleToolModal.modalKey);
|
||||
const tasks = selections.map((s) => {
|
||||
const form: FormType.ModifySubtitle = {
|
||||
id: s.id,
|
||||
type: s.type,
|
||||
language: s.language,
|
||||
path: s.path,
|
||||
...override,
|
||||
};
|
||||
return createTask(s.path, mutateAsync, { action, form });
|
||||
});
|
||||
|
||||
dispatchTask(tasks, "modify-subtitles");
|
||||
},
|
||||
[hide, selections, mutateAsync]
|
||||
);
|
||||
|
||||
return (
|
||||
<ProcessSubtitleContext.Provider value={process}>
|
||||
<SubtitleToolModal
|
||||
count={selections.length}
|
||||
tools={tools}
|
||||
select={setSelections}
|
||||
></SubtitleToolModal>
|
||||
{modals}
|
||||
</ProcessSubtitleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubtitleTools;
|
|
@ -1,4 +1,84 @@
|
|||
import { SelectorOption } from "..";
|
||||
import { SelectorOption } from "@/components";
|
||||
import {
|
||||
faClock,
|
||||
faCode,
|
||||
faDeaf,
|
||||
faExchangeAlt,
|
||||
faFilm,
|
||||
faImage,
|
||||
faLanguage,
|
||||
faMagic,
|
||||
faPaintBrush,
|
||||
faPlay,
|
||||
faTextHeight,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import ColorTool from "./ColorTool";
|
||||
import FrameRateTool from "./FrameRateTool";
|
||||
import TimeTool from "./TimeTool";
|
||||
import Translation from "./Translation";
|
||||
import { ToolOptions } from "./types";
|
||||
|
||||
export const tools: ToolOptions[] = [
|
||||
{
|
||||
key: "sync",
|
||||
icon: faPlay,
|
||||
name: "Sync",
|
||||
},
|
||||
{
|
||||
key: "remove_HI",
|
||||
icon: faDeaf,
|
||||
name: "Remove HI Tags",
|
||||
},
|
||||
{
|
||||
key: "remove_tags",
|
||||
icon: faCode,
|
||||
name: "Remove Style Tags",
|
||||
},
|
||||
{
|
||||
key: "OCR_fixes",
|
||||
icon: faImage,
|
||||
name: "OCR Fixes",
|
||||
},
|
||||
{
|
||||
key: "common",
|
||||
icon: faMagic,
|
||||
name: "Common Fixes",
|
||||
},
|
||||
{
|
||||
key: "fix_uppercase",
|
||||
icon: faTextHeight,
|
||||
name: "Fix Uppercase",
|
||||
},
|
||||
{
|
||||
key: "reverse_rtl",
|
||||
icon: faExchangeAlt,
|
||||
name: "Reverse RTL",
|
||||
},
|
||||
{
|
||||
key: "add_color",
|
||||
icon: faPaintBrush,
|
||||
name: "Add Color",
|
||||
modal: ColorTool,
|
||||
},
|
||||
{
|
||||
key: "change_frame_rate",
|
||||
icon: faFilm,
|
||||
name: "Change Frame Rate",
|
||||
modal: FrameRateTool,
|
||||
},
|
||||
{
|
||||
key: "adjust_time",
|
||||
icon: faClock,
|
||||
name: "Adjust Times",
|
||||
modal: TimeTool,
|
||||
},
|
||||
{
|
||||
key: "translation",
|
||||
icon: faLanguage,
|
||||
name: "Translate",
|
||||
modal: Translation,
|
||||
},
|
||||
];
|
||||
|
||||
export const availableTranslation = {
|
||||
af: "afrikaans",
|
|
@ -0,0 +1,9 @@
|
|||
import { ModalComponent } from "@/modules/modals/WithModal";
|
||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export interface ToolOptions {
|
||||
key: string;
|
||||
icon: IconDefinition;
|
||||
name: string;
|
||||
modal?: ModalComponent<unknown>;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { createContext, Dispatch, SetStateAction } from "react";
|
||||
|
||||
export interface ModalData {
|
||||
key: string;
|
||||
closeable: boolean;
|
||||
size: "sm" | "lg" | "xl" | undefined;
|
||||
}
|
||||
|
||||
export type ModalSetter = {
|
||||
[P in keyof Omit<ModalData, "key">]: Dispatch<SetStateAction<ModalData[P]>>;
|
||||
};
|
||||
|
||||
export const ModalDataContext = createContext<ModalData | null>(null);
|
||||
export const ModalSetterContext = createContext<ModalSetter | null>(null);
|
|
@ -0,0 +1,44 @@
|
|||
import clsx from "clsx";
|
||||
import { FunctionComponent, useCallback, useState } from "react";
|
||||
import { Modal } from "react-bootstrap";
|
||||
import { useCurrentLayer, useModalControl, useModalData } from "./hooks";
|
||||
|
||||
interface Props {}
|
||||
|
||||
export const ModalWrapper: FunctionComponent<Props> = ({ children }) => {
|
||||
const { size, closeable, key } = useModalData();
|
||||
const [needExit, setExit] = useState(false);
|
||||
|
||||
const { hide: hideModal } = useModalControl();
|
||||
|
||||
const layer = useCurrentLayer();
|
||||
const isShowed = layer !== -1;
|
||||
|
||||
const hide = useCallback(() => {
|
||||
setExit(true);
|
||||
}, []);
|
||||
|
||||
const exit = useCallback(() => {
|
||||
if (isShowed) {
|
||||
hideModal(key);
|
||||
}
|
||||
setExit(false);
|
||||
}, [isShowed, hideModal, key]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
size={size}
|
||||
show={isShowed && !needExit}
|
||||
onHide={hide}
|
||||
onExited={exit}
|
||||
backdrop={closeable ? undefined : "static"}
|
||||
className={clsx(`index-${layer}`)}
|
||||
backdropClassName={clsx(`index-${layer}`)}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalWrapper;
|
|
@ -0,0 +1,52 @@
|
|||
import { FunctionComponent, useMemo, useState } from "react";
|
||||
import {
|
||||
ModalData,
|
||||
ModalDataContext,
|
||||
ModalSetter,
|
||||
ModalSetterContext,
|
||||
} from "./ModalContext";
|
||||
import ModalWrapper from "./ModalWrapper";
|
||||
|
||||
export interface ModalProps {}
|
||||
|
||||
export type ModalComponent<P> = FunctionComponent<P> & {
|
||||
modalKey: string;
|
||||
};
|
||||
|
||||
export default function withModal<T>(
|
||||
Content: FunctionComponent<T>,
|
||||
key: string
|
||||
) {
|
||||
const Comp: ModalComponent<T> = (props: ModalProps & T) => {
|
||||
const [closeable, setCloseable] = useState(true);
|
||||
const [size, setSize] = useState<ModalData["size"]>(undefined);
|
||||
const data: ModalData = useMemo(
|
||||
() => ({
|
||||
key,
|
||||
size,
|
||||
closeable,
|
||||
}),
|
||||
[closeable, size]
|
||||
);
|
||||
|
||||
const setter: ModalSetter = useMemo(
|
||||
() => ({
|
||||
closeable: setCloseable,
|
||||
size: setSize,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalDataContext.Provider value={data}>
|
||||
<ModalSetterContext.Provider value={setter}>
|
||||
<ModalWrapper>
|
||||
<Content {...props}></Content>
|
||||
</ModalWrapper>
|
||||
</ModalSetterContext.Provider>
|
||||
</ModalDataContext.Provider>
|
||||
);
|
||||
};
|
||||
Comp.modalKey = key;
|
||||
return Comp;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { FunctionComponent, ReactNode } from "react";
|
||||
import { Modal } from "react-bootstrap";
|
||||
import { useModalData } from "./hooks";
|
||||
|
||||
interface StandardModalProps {
|
||||
title: string;
|
||||
footer?: ReactNode;
|
||||
}
|
||||
|
||||
export const StandardModalView: FunctionComponent<StandardModalProps> = ({
|
||||
children,
|
||||
footer,
|
||||
title,
|
||||
}) => {
|
||||
const { closeable } = useModalData();
|
||||
return (
|
||||
<>
|
||||
<Modal.Header closeButton={closeable}>{title}</Modal.Header>
|
||||
<Modal.Body>{children}</Modal.Body>
|
||||
<Modal.Footer hidden={footer === undefined}>{footer}</Modal.Footer>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
import {
|
||||
hideModalAction,
|
||||
showModalAction,
|
||||
} from "@/modules/redux/actions/modal";
|
||||
import { useReduxAction, useReduxStore } from "@/modules/redux/hooks/base";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
||||
import { StandardModalView } from "./components";
|
||||
import {
|
||||
ModalData,
|
||||
ModalDataContext,
|
||||
ModalSetterContext,
|
||||
} from "./ModalContext";
|
||||
import { ModalComponent } from "./WithModal";
|
||||
|
||||
type ModalProps = Partial<Omit<ModalData, "key">> & {
|
||||
onMounted?: () => void;
|
||||
};
|
||||
|
||||
export function useModal(props?: ModalProps): typeof StandardModalView {
|
||||
const setter = useContext(ModalSetterContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (setter && props) {
|
||||
setter.closeable(props.closeable ?? true);
|
||||
setter.size(props.size);
|
||||
}
|
||||
}, [props, setter]);
|
||||
|
||||
const ref = useRef<ModalProps["onMounted"]>(props?.onMounted);
|
||||
ref.current = props?.onMounted;
|
||||
|
||||
const layer = useCurrentLayer();
|
||||
|
||||
useEffect(() => {
|
||||
if (layer !== -1 && ref.current) {
|
||||
ref.current();
|
||||
}
|
||||
}, [layer]);
|
||||
|
||||
return StandardModalView;
|
||||
}
|
||||
|
||||
export function useModalControl() {
|
||||
const showAction = useReduxAction(showModalAction);
|
||||
|
||||
const show = useCallback(
|
||||
<P>(comp: ModalComponent<P>, payload?: unknown) => {
|
||||
showAction({ key: comp.modalKey, payload });
|
||||
},
|
||||
[showAction]
|
||||
);
|
||||
|
||||
const hideAction = useReduxAction(hideModalAction);
|
||||
|
||||
const hide = useCallback(
|
||||
(key?: string) => {
|
||||
hideAction(key);
|
||||
},
|
||||
[hideAction]
|
||||
);
|
||||
|
||||
return { show, hide };
|
||||
}
|
||||
|
||||
export function useModalData(): ModalData {
|
||||
const data = useContext(ModalDataContext);
|
||||
|
||||
if (data === null) {
|
||||
throw new Error("useModalData should be used inside Modal");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function usePayload<T>(): T | null {
|
||||
const { key } = useModalData();
|
||||
const stack = useReduxStore((s) => s.modal.stack);
|
||||
|
||||
return useMemo(
|
||||
() => (stack.find((m) => m.key === key)?.payload as T) ?? null,
|
||||
[stack, key]
|
||||
);
|
||||
}
|
||||
|
||||
export function useCurrentLayer() {
|
||||
const { key } = useModalData();
|
||||
const stack = useReduxStore((s) => s.modal.stack);
|
||||
|
||||
return useMemo(() => stack.findIndex((m) => m.key === key), [stack, key]);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./components";
|
||||
export * from "./hooks";
|
||||
export { default as withModal } from "./WithModal";
|
|
@ -1,36 +0,0 @@
|
|||
import {
|
||||
hideModalAction,
|
||||
showModalAction,
|
||||
} from "@/modules/redux/actions/modal";
|
||||
import { useReduxAction, useReduxStore } from "@/modules/redux/hooks/base";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
export function useModalControl() {
|
||||
const showModal = useReduxAction(showModalAction);
|
||||
|
||||
const show = useCallback(
|
||||
(key: string, payload?: unknown) => {
|
||||
showModal({ key, payload });
|
||||
},
|
||||
[showModal]
|
||||
);
|
||||
|
||||
const hide = useReduxAction(hideModalAction);
|
||||
|
||||
return { show, hide };
|
||||
}
|
||||
|
||||
export function useIsShowed(key: string) {
|
||||
const stack = useReduxStore((s) => s.modal.stack);
|
||||
|
||||
return useMemo(() => stack.findIndex((m) => m.key === key), [stack, key]);
|
||||
}
|
||||
|
||||
export function usePayload<T>(key: string): T | null {
|
||||
const stack = useReduxStore((s) => s.modal.stack);
|
||||
|
||||
return useMemo(
|
||||
() => (stack.find((m) => m.key === key)?.payload as T) ?? null,
|
||||
[stack, key]
|
||||
);
|
||||
}
|
|
@ -5,14 +5,11 @@ import {
|
|||
useSeriesById,
|
||||
useSeriesModification,
|
||||
} from "@/apis/hooks";
|
||||
import {
|
||||
ContentHeader,
|
||||
ItemEditorModal,
|
||||
LoadingIndicator,
|
||||
SeriesUploadModal,
|
||||
} from "@/components";
|
||||
import { ContentHeader, LoadingIndicator } from "@/components";
|
||||
import ItemOverview from "@/components/ItemOverview";
|
||||
import { useModalControl } from "@/modules/redux/hooks/modal";
|
||||
import { ItemEditorModal, SeriesUploadModal } from "@/components/modals";
|
||||
import { SubtitleToolModal } from "@/components/modals/subtitle-tools";
|
||||
import { useModalControl } from "@/modules/modals";
|
||||
import { createAndDispatchTask } from "@/modules/task/utilities";
|
||||
import { useLanguageProfileBy } from "@/utilities/languages";
|
||||
import {
|
||||
|
@ -109,7 +106,7 @@ const SeriesEpisodesView: FunctionComponent = () => {
|
|||
<ContentHeader.Button
|
||||
disabled={series.episodeFileCount === 0 || !available || hasTask}
|
||||
icon={faBriefcase}
|
||||
onClick={() => show("tools", episodes)}
|
||||
onClick={() => show(SubtitleToolModal, episodes)}
|
||||
>
|
||||
Tools
|
||||
</ContentHeader.Button>
|
||||
|
@ -120,14 +117,14 @@ const SeriesEpisodesView: FunctionComponent = () => {
|
|||
!available
|
||||
}
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={() => show("upload", series)}
|
||||
onClick={() => show(SeriesUploadModal, series)}
|
||||
>
|
||||
Upload
|
||||
</ContentHeader.Button>
|
||||
<ContentHeader.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => show("edit", series)}
|
||||
onClick={() => show(ItemEditorModal, series)}
|
||||
>
|
||||
Edit Series
|
||||
</ContentHeader.Button>
|
||||
|
@ -158,11 +155,8 @@ const SeriesEpisodesView: FunctionComponent = () => {
|
|||
></Table>
|
||||
)}
|
||||
</Row>
|
||||
<ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal>
|
||||
<SeriesUploadModal
|
||||
modalKey="upload"
|
||||
episodes={episodes ?? []}
|
||||
></SeriesUploadModal>
|
||||
<ItemEditorModal mutation={mutation}></ItemEditorModal>
|
||||
<SeriesUploadModal episodes={episodes ?? []}></SeriesUploadModal>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks";
|
||||
import {
|
||||
ActionButton,
|
||||
EpisodeHistoryModal,
|
||||
GroupTable,
|
||||
import { ActionButton, GroupTable, TextPopover } from "@/components";
|
||||
import { EpisodeHistoryModal } from "@/components/modals";
|
||||
import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal";
|
||||
import SubtitleTools, {
|
||||
SubtitleToolModal,
|
||||
TextPopover,
|
||||
} from "@/components";
|
||||
import { ManualSearchModal } from "@/components/modals/ManualSearchModal";
|
||||
} from "@/components/modals/subtitle-tools";
|
||||
import { useModalControl } from "@/modules/modals";
|
||||
import { useShowOnlyDesired } from "@/modules/redux/hooks";
|
||||
import { useModalControl } from "@/modules/redux/hooks/modal";
|
||||
import { BuildKey, filterSubtitleBy } from "@/utilities";
|
||||
import { useProfileItemsToLanguages } from "@/utilities/languages";
|
||||
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
||||
|
@ -166,21 +164,21 @@ const Table: FunctionComponent<Props> = ({
|
|||
icon={faUser}
|
||||
disabled={series?.profileId === null || disabled}
|
||||
onClick={() => {
|
||||
show("manual-search", row.original);
|
||||
show(EpisodeSearchModal, row.original);
|
||||
}}
|
||||
></ActionButton>
|
||||
<ActionButton
|
||||
icon={faHistory}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
show("history", row.original);
|
||||
show(EpisodeHistoryModal, row.original);
|
||||
}}
|
||||
></ActionButton>
|
||||
<ActionButton
|
||||
icon={faBriefcase}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
show("tools", [row.original]);
|
||||
show(SubtitleToolModal, [row.original]);
|
||||
}}
|
||||
></ActionButton>
|
||||
</ButtonGroup>
|
||||
|
@ -214,13 +212,12 @@ const Table: FunctionComponent<Props> = ({
|
|||
}}
|
||||
emptyText="No Episode Found For This Series"
|
||||
></GroupTable>
|
||||
<SubtitleToolModal modalKey="tools" size="lg"></SubtitleToolModal>
|
||||
<EpisodeHistoryModal modalKey="history" size="lg"></EpisodeHistoryModal>
|
||||
<ManualSearchModal
|
||||
modalKey="manual-search"
|
||||
<SubtitleTools></SubtitleTools>
|
||||
<EpisodeHistoryModal></EpisodeHistoryModal>
|
||||
<EpisodeSearchModal
|
||||
download={download}
|
||||
query={useEpisodesProvider}
|
||||
></ManualSearchModal>
|
||||
></EpisodeSearchModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,17 +8,18 @@ import {
|
|||
useMovieById,
|
||||
useMovieModification,
|
||||
} from "@/apis/hooks/movies";
|
||||
import { ContentHeader, LoadingIndicator } from "@/components";
|
||||
import ItemOverview from "@/components/ItemOverview";
|
||||
import {
|
||||
ContentHeader,
|
||||
ItemEditorModal,
|
||||
LoadingIndicator,
|
||||
MovieHistoryModal,
|
||||
MovieUploadModal,
|
||||
} from "@/components/modals";
|
||||
import { MovieSearchModal } from "@/components/modals/ManualSearchModal";
|
||||
import SubtitleTools, {
|
||||
SubtitleToolModal,
|
||||
} from "@/components";
|
||||
import ItemOverview from "@/components/ItemOverview";
|
||||
import { ManualSearchModal } from "@/components/modals/ManualSearchModal";
|
||||
import { useModalControl } from "@/modules/redux/hooks/modal";
|
||||
} from "@/components/modals/subtitle-tools";
|
||||
import { useModalControl } from "@/modules/modals";
|
||||
import { createAndDispatchTask } from "@/modules/task/utilities";
|
||||
import { useLanguageProfileBy } from "@/utilities/languages";
|
||||
import {
|
||||
|
@ -122,20 +123,20 @@ const MovieDetailView: FunctionComponent = () => {
|
|||
<ContentHeader.Button
|
||||
icon={faUser}
|
||||
disabled={movie.profileId === null || hasTask}
|
||||
onClick={() => show("manual-search", movie)}
|
||||
onClick={() => show(MovieSearchModal, movie)}
|
||||
>
|
||||
Manual
|
||||
</ContentHeader.Button>
|
||||
<ContentHeader.Button
|
||||
icon={faHistory}
|
||||
onClick={() => show("history", movie)}
|
||||
onClick={() => show(MovieHistoryModal, movie)}
|
||||
>
|
||||
History
|
||||
</ContentHeader.Button>
|
||||
<ContentHeader.Button
|
||||
icon={faToolbox}
|
||||
disabled={hasTask}
|
||||
onClick={() => show("tools", [movie])}
|
||||
onClick={() => show(SubtitleToolModal, [movie])}
|
||||
>
|
||||
Tools
|
||||
</ContentHeader.Button>
|
||||
|
@ -145,14 +146,14 @@ const MovieDetailView: FunctionComponent = () => {
|
|||
<ContentHeader.Button
|
||||
disabled={!allowEdit || movie.profileId === null || hasTask}
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={() => show("upload", movie)}
|
||||
onClick={() => show(MovieUploadModal, movie)}
|
||||
>
|
||||
Upload
|
||||
</ContentHeader.Button>
|
||||
<ContentHeader.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => show("edit", movie)}
|
||||
onClick={() => show(ItemEditorModal, movie)}
|
||||
>
|
||||
Edit Movie
|
||||
</ContentHeader.Button>
|
||||
|
@ -174,15 +175,14 @@ const MovieDetailView: FunctionComponent = () => {
|
|||
<Row>
|
||||
<Table movie={movie} profile={profile} disabled={hasTask}></Table>
|
||||
</Row>
|
||||
<ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal>
|
||||
<SubtitleToolModal modalKey="tools" size="lg"></SubtitleToolModal>
|
||||
<MovieHistoryModal modalKey="history" size="lg"></MovieHistoryModal>
|
||||
<MovieUploadModal modalKey="upload" size="lg"></MovieUploadModal>
|
||||
<ManualSearchModal
|
||||
modalKey="manual-search"
|
||||
<ItemEditorModal mutation={mutation}></ItemEditorModal>
|
||||
<SubtitleTools></SubtitleTools>
|
||||
<MovieHistoryModal></MovieHistoryModal>
|
||||
<MovieUploadModal></MovieUploadModal>
|
||||
<MovieSearchModal
|
||||
download={download}
|
||||
query={useMoviesProvider}
|
||||
></ManualSearchModal>
|
||||
></MovieSearchModal>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { useMovieModification, useMoviesPagination } from "@/apis/hooks";
|
||||
import { ActionBadge, ItemEditorModal, TextPopover } from "@/components";
|
||||
import { ActionBadge, TextPopover } from "@/components";
|
||||
import Language from "@/components/bazarr/Language";
|
||||
import LanguageProfile from "@/components/bazarr/LanguageProfile";
|
||||
import { ItemEditorModal } from "@/components/modals";
|
||||
import ItemView from "@/components/views/ItemView";
|
||||
import { useModalControl } from "@/modules/redux/hooks/modal";
|
||||
import { useModalControl } from "@/modules/modals";
|
||||
import { BuildKey } from "@/utilities";
|
||||
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
|
@ -90,7 +91,7 @@ const MovieView: FunctionComponent = () => {
|
|||
return (
|
||||
<ActionBadge
|
||||
icon={faWrench}
|
||||
onClick={() => show("edit", row.original)}
|
||||
onClick={() => show(ItemEditorModal, row.original)}
|
||||
></ActionBadge>
|
||||
);
|
||||
},
|
||||
|
@ -105,7 +106,7 @@ const MovieView: FunctionComponent = () => {
|
|||
<title>Movies - Bazarr</title>
|
||||
</Helmet>
|
||||
<ItemView query={query} columns={columns}></ItemView>
|
||||
<ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal>
|
||||
<ItemEditorModal mutation={mutation}></ItemEditorModal>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { useSeriesModification, useSeriesPagination } from "@/apis/hooks";
|
||||
import { ActionBadge, ItemEditorModal } from "@/components";
|
||||
import { ActionBadge } from "@/components";
|
||||
import LanguageProfile from "@/components/bazarr/LanguageProfile";
|
||||
import { ItemEditorModal } from "@/components/modals";
|
||||
import ItemView from "@/components/views/ItemView";
|
||||
import { useModalControl } from "@/modules/redux/hooks/modal";
|
||||
import { useModalControl } from "@/modules/modals";
|
||||
import { BuildKey } from "@/utilities";
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
|
@ -92,7 +93,7 @@ const SeriesView: FunctionComponent = () => {
|
|||
return (
|
||||
<ActionBadge
|
||||
icon={faWrench}
|
||||
onClick={() => show("edit", original)}
|
||||
onClick={() => show(ItemEditorModal, original)}
|
||||
></ActionBadge>
|
||||
);
|
||||
},
|
||||
|
@ -107,7 +108,7 @@ const SeriesView: FunctionComponent = () => {
|
|||
<title>Series - Bazarr</title>
|
||||
</Helmet>
|
||||
<ItemView query={query} columns={columns}></ItemView>
|
||||
<ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal>
|
||||
<ItemEditorModal mutation={mutation}></ItemEditorModal>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import {
|
||||
ActionButton,
|
||||
BaseModal,
|
||||
BaseModalProps,
|
||||
Chips,
|
||||
LanguageSelector,
|
||||
Selector,
|
||||
SelectorOption,
|
||||
SimpleTable,
|
||||
} from "@/components";
|
||||
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
|
||||
import {
|
||||
useModal,
|
||||
useModalControl,
|
||||
usePayload,
|
||||
withModal,
|
||||
} from "@/modules/modals";
|
||||
import { BuildKey } from "@/utilities";
|
||||
import { LOG } from "@/utilities/console";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
|
@ -17,7 +20,6 @@ import {
|
|||
FunctionComponent,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
|
@ -53,12 +55,8 @@ function createDefaultProfile(): Language.Profile {
|
|||
};
|
||||
}
|
||||
|
||||
const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = (
|
||||
props
|
||||
) => {
|
||||
const { update, ...modal } = props;
|
||||
|
||||
const profile = usePayload<Language.Profile>(modal.modalKey);
|
||||
const LanguagesProfileModal: FunctionComponent<Props> = ({ update }) => {
|
||||
const profile = usePayload<Language.Profile>();
|
||||
|
||||
const { hide } = useModalControl();
|
||||
|
||||
|
@ -66,13 +64,12 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = (
|
|||
|
||||
const [current, setProfile] = useState(createDefaultProfile);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile) {
|
||||
setProfile(profile);
|
||||
} else {
|
||||
setProfile(createDefaultProfile);
|
||||
}
|
||||
}, [profile]);
|
||||
const Modal = useModal({
|
||||
size: "lg",
|
||||
onMounted: () => {
|
||||
setProfile(profile ?? createDefaultProfile);
|
||||
},
|
||||
});
|
||||
|
||||
const cutoff: SelectorOption<number>[] = useMemo(() => {
|
||||
const options = [...cutoffOptions];
|
||||
|
@ -134,18 +131,6 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = (
|
|||
|
||||
const canSave = current.name.length > 0 && current.items.length > 0;
|
||||
|
||||
const footer = (
|
||||
<Button
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
hide();
|
||||
update(current);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
|
||||
const columns = useMemo<Column<Language.ProfileItem>[]>(
|
||||
() => [
|
||||
{
|
||||
|
@ -253,8 +238,20 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = (
|
|||
[languages]
|
||||
);
|
||||
|
||||
const footer = (
|
||||
<Button
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
hide();
|
||||
update(current);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal size="lg" title="Languages Profile" footer={footer} {...modal}>
|
||||
<Modal title="Languages Profile" footer={footer}>
|
||||
<Input>
|
||||
<Form.Control
|
||||
type="text"
|
||||
|
@ -319,8 +316,8 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = (
|
|||
></Selector>
|
||||
<Message>Download subtitle file without format conversion</Message>
|
||||
</Input>
|
||||
</BaseModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguagesProfileModal;
|
||||
export default withModal(LanguagesProfileModal, "languages-profile-editor");
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ActionButton, SimpleTable } from "@/components";
|
||||
import { useModalControl } from "@/modules/redux/hooks/modal";
|
||||
import { useModalControl } from "@/modules/modals";
|
||||
import { LOG } from "@/utilities/console";
|
||||
import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
@ -69,7 +69,7 @@ const Table: FunctionComponent = () => {
|
|||
const mutateRow = useCallback<ModifyFn>(
|
||||
(index, item) => {
|
||||
if (item) {
|
||||
show("profile", cloneDeep(item));
|
||||
show(Modal, cloneDeep(item));
|
||||
} else {
|
||||
const list = [...profiles];
|
||||
list.splice(index, 1);
|
||||
|
@ -185,12 +185,12 @@ const Table: FunctionComponent = () => {
|
|||
mustNotContain: [],
|
||||
originalFormat: false,
|
||||
};
|
||||
show("profile", profile);
|
||||
show(Modal, profile);
|
||||
}}
|
||||
>
|
||||
{canAdd ? "Add New Profile" : "No Enabled Languages"}
|
||||
</Button>
|
||||
<Modal update={updateProfile} modalKey="profile"></Modal>
|
||||
<Modal update={updateProfile}></Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
import api from "@/apis/raw";
|
||||
import { AsyncButton, Selector, SelectorOption } from "@/components";
|
||||
import {
|
||||
AsyncButton,
|
||||
BaseModal,
|
||||
BaseModalProps,
|
||||
Selector,
|
||||
SelectorOption,
|
||||
} from "@/components";
|
||||
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
|
||||
useModal,
|
||||
useModalControl,
|
||||
usePayload,
|
||||
withModal,
|
||||
} from "@/modules/modals";
|
||||
import { BuildKey } from "@/utilities";
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from "react";
|
||||
import { Button, Col, Container, Form, Row } from "react-bootstrap";
|
||||
import { ColCard, useLatestArray, useUpdateArray } from "../components";
|
||||
import { notificationsKey } from "../keys";
|
||||
|
||||
interface ModalProps {
|
||||
interface Props {
|
||||
selections: readonly Settings.NotificationInfo[];
|
||||
}
|
||||
|
||||
const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
|
||||
selections,
|
||||
...modal
|
||||
}) => {
|
||||
const NotificationTool: FunctionComponent<Props> = ({ selections }) => {
|
||||
const options = useMemo<SelectorOption<Settings.NotificationInfo>[]>(
|
||||
() =>
|
||||
selections
|
||||
|
@ -37,8 +33,7 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
|
|||
"name"
|
||||
);
|
||||
|
||||
const payload = usePayload<Settings.NotificationInfo>(modal.modalKey);
|
||||
const { hide } = useModalControl();
|
||||
const payload = usePayload<Settings.NotificationInfo>();
|
||||
|
||||
const [current, setCurrent] =
|
||||
useState<Nullable<Settings.NotificationInfo>>(payload);
|
||||
|
@ -59,55 +54,60 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
|
|||
const canSave =
|
||||
current !== null && current?.url !== null && current?.url.length !== 0;
|
||||
|
||||
const footer = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<AsyncButton
|
||||
className="mr-auto"
|
||||
disabled={!canSave}
|
||||
variant="outline-secondary"
|
||||
promise={() => {
|
||||
if (current && current.url) {
|
||||
return api.system.testNotification(current.url);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Test
|
||||
</AsyncButton>
|
||||
<Button
|
||||
hidden={payload === null}
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
if (current) {
|
||||
update({ ...current, enabled: false });
|
||||
}
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
if (current) {
|
||||
update({ ...current, enabled: true });
|
||||
}
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
[canSave, payload, current, hide, update]
|
||||
);
|
||||
|
||||
const getLabel = useCallback((v: Settings.NotificationInfo) => v.name, []);
|
||||
|
||||
const Modal = useModal({
|
||||
onMounted: () => {
|
||||
setCurrent(payload);
|
||||
},
|
||||
});
|
||||
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const footer = (
|
||||
<>
|
||||
<AsyncButton
|
||||
className="mr-auto"
|
||||
disabled={!canSave}
|
||||
variant="outline-secondary"
|
||||
promise={() => {
|
||||
if (current && current.url) {
|
||||
return api.system.testNotification(current.url);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Test
|
||||
</AsyncButton>
|
||||
<Button
|
||||
hidden={payload === null}
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
if (current) {
|
||||
update({ ...current, enabled: false });
|
||||
}
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
if (current) {
|
||||
update({ ...current, enabled: true });
|
||||
}
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal title="Notification" footer={footer} {...modal}>
|
||||
<Modal title="Notification" footer={footer}>
|
||||
<Container fluid>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
|
@ -135,10 +135,12 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
|
|||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</BaseModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const NotificationModal = withModal(NotificationTool, "notification-tool");
|
||||
|
||||
export const NotificationView: FunctionComponent = () => {
|
||||
const notifications = useLatestArray<Settings.NotificationInfo>(
|
||||
notificationsKey,
|
||||
|
@ -155,7 +157,7 @@ export const NotificationView: FunctionComponent = () => {
|
|||
<ColCard
|
||||
key={BuildKey(idx, v.name)}
|
||||
header={v.name}
|
||||
onClick={() => show("notifications", v)}
|
||||
onClick={() => show(NotificationModal, v)}
|
||||
></ColCard>
|
||||
));
|
||||
}, [notifications, show]);
|
||||
|
@ -164,12 +166,9 @@ export const NotificationView: FunctionComponent = () => {
|
|||
<Container fluid>
|
||||
<Row>
|
||||
{elements}{" "}
|
||||
<ColCard plus onClick={() => show("notifications")}></ColCard>
|
||||
<ColCard plus onClick={() => show(NotificationModal)}></ColCard>
|
||||
</Row>
|
||||
<NotificationModal
|
||||
selections={notifications ?? []}
|
||||
modalKey="notifications"
|
||||
></NotificationModal>
|
||||
<NotificationModal selections={notifications ?? []}></NotificationModal>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Selector, SelectorComponents, SelectorOption } from "@/components";
|
||||
import {
|
||||
BaseModal,
|
||||
Selector,
|
||||
SelectorComponents,
|
||||
SelectorOption,
|
||||
} from "@/components";
|
||||
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
|
||||
useModal,
|
||||
useModalControl,
|
||||
usePayload,
|
||||
withModal,
|
||||
} from "@/modules/modals";
|
||||
import { BuildKey, isReactText } from "@/utilities";
|
||||
import { capitalize, isArray, isBoolean } from "lodash";
|
||||
import {
|
||||
|
@ -27,7 +27,6 @@ import {
|
|||
} from "../components";
|
||||
import { ProviderInfo, ProviderList } from "./list";
|
||||
|
||||
const ModalKey = "provider-modal";
|
||||
const ProviderKey = "settings-general-enabled_providers";
|
||||
|
||||
export const ProviderView: FunctionComponent = () => {
|
||||
|
@ -37,7 +36,7 @@ export const ProviderView: FunctionComponent = () => {
|
|||
|
||||
const select = useCallback(
|
||||
(v?: ProviderInfo) => {
|
||||
show(ModalKey, v ?? null);
|
||||
show(ProviderModal, v ?? null);
|
||||
},
|
||||
[show]
|
||||
);
|
||||
|
@ -72,12 +71,14 @@ export const ProviderView: FunctionComponent = () => {
|
|||
{cards}
|
||||
<ColCard key="add-card" plus onClick={select}></ColCard>
|
||||
</Row>
|
||||
<ProviderModal></ProviderModal>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProviderModal: FunctionComponent = () => {
|
||||
const payload = usePayload<ProviderInfo>(ModalKey);
|
||||
const ProviderTool: FunctionComponent = () => {
|
||||
const payload = usePayload<ProviderInfo>();
|
||||
const Modal = useModal();
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const [staged, setChange] = useState<LooseObject>({});
|
||||
|
@ -121,20 +122,6 @@ export const ProviderModal: FunctionComponent = () => {
|
|||
|
||||
const canSave = info !== null;
|
||||
|
||||
const footer = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<Button hidden={!payload} variant="danger" onClick={deletePayload}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button disabled={!canSave} onClick={addProvider}>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
[canSave, payload, deletePayload, addProvider]
|
||||
);
|
||||
|
||||
const onSelect = useCallback((item: Nullable<ProviderInfo>) => {
|
||||
if (item) {
|
||||
setInfo(item);
|
||||
|
@ -237,8 +224,19 @@ export const ProviderModal: FunctionComponent = () => {
|
|||
[]
|
||||
);
|
||||
|
||||
const footer = (
|
||||
<>
|
||||
<Button hidden={!payload} variant="danger" onClick={deletePayload}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button disabled={!canSave} onClick={addProvider}>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal title="Provider" footer={footer} modalKey={ModalKey}>
|
||||
<Modal title="Provider" footer={footer}>
|
||||
<StagedChangesContext.Provider value={[staged, setChange]}>
|
||||
<Container>
|
||||
<Row>
|
||||
|
@ -266,6 +264,8 @@ export const ProviderModal: FunctionComponent = () => {
|
|||
</Row>
|
||||
</Container>
|
||||
</StagedChangesContext.Provider>
|
||||
</BaseModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const ProviderModal = withModal(ProviderTool, "provider-tool");
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FunctionComponent } from "react";
|
||||
import { Group, Input, Layout } from "../components";
|
||||
import { ProviderModal, ProviderView } from "./components";
|
||||
import { ProviderView } from "./components";
|
||||
|
||||
const SettingsProvidersView: FunctionComponent = () => {
|
||||
return (
|
||||
|
@ -10,7 +10,6 @@ const SettingsProvidersView: FunctionComponent = () => {
|
|||
<ProviderView></ProviderView>
|
||||
</Input>
|
||||
</Group>
|
||||
<ProviderModal></ProviderModal>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import { AsyncButton, BaseModal, BaseModalProps } from "@/components";
|
||||
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
|
||||
import { AsyncButton } from "@/components";
|
||||
import {
|
||||
useModal,
|
||||
useModalControl,
|
||||
usePayload,
|
||||
withModal,
|
||||
} from "@/modules/modals";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { useDeleteBackups } from "../../../apis/hooks";
|
||||
|
||||
interface Props extends BaseModalProps {}
|
||||
|
||||
const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => {
|
||||
const SystemBackupDeleteModal: FunctionComponent = () => {
|
||||
const { mutateAsync } = useDeleteBackups();
|
||||
|
||||
const result = usePayload<string>(modal.modalKey);
|
||||
const result = usePayload<string>();
|
||||
|
||||
const Modal = useModal();
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const footer = (
|
||||
|
@ -19,9 +23,7 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => {
|
|||
<Button
|
||||
variant="outline-secondary"
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
hide(modal.modalKey);
|
||||
}}
|
||||
onClick={() => hide()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
@ -34,7 +36,7 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => {
|
|||
return null;
|
||||
}
|
||||
}}
|
||||
onSuccess={() => hide(modal.modalKey)}
|
||||
onSuccess={() => hide()}
|
||||
>
|
||||
Delete
|
||||
</AsyncButton>
|
||||
|
@ -43,10 +45,10 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<BaseModal title="Delete Backup" footer={footer} {...modal}>
|
||||
Are you sure you want to delete the backup '{result}'?
|
||||
</BaseModal>
|
||||
<Modal title="Delete Backup" footer={footer}>
|
||||
<span>Are you sure you want to delete the backup '{result}'?</span>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemBackupDeleteModal;
|
||||
export default withModal(SystemBackupDeleteModal, "delete");
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
import { useRestoreBackups } from "@/apis/hooks/system";
|
||||
import { AsyncButton, BaseModal, BaseModalProps } from "@/components";
|
||||
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
|
||||
import { AsyncButton } from "@/components";
|
||||
import {
|
||||
useModal,
|
||||
useModalControl,
|
||||
usePayload,
|
||||
withModal,
|
||||
} from "@/modules/modals";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
|
||||
interface Props extends BaseModalProps {}
|
||||
const SystemBackupRestoreModal: FunctionComponent = () => {
|
||||
const result = usePayload<string>();
|
||||
|
||||
const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => {
|
||||
const result = usePayload<string>(modal.modalKey);
|
||||
const Modal = useModal();
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const { mutateAsync } = useRestoreBackups();
|
||||
|
||||
const { hide } = useModalControl();
|
||||
|
||||
const footer = (
|
||||
<div className="d-flex flex-row-reverse flex-grow-1 justify-content-between">
|
||||
<div>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
hide(modal.modalKey);
|
||||
}}
|
||||
onClick={() => hide()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
@ -34,7 +36,7 @@ const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => {
|
|||
return null;
|
||||
}
|
||||
}}
|
||||
onSuccess={() => hide(modal.modalKey)}
|
||||
onSuccess={() => hide()}
|
||||
>
|
||||
Restore
|
||||
</AsyncButton>
|
||||
|
@ -43,11 +45,13 @@ const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<BaseModal title="Restore Backup" footer={footer} {...modal}>
|
||||
Are you sure you want to restore the backup '{result}'? Bazarr will
|
||||
automatically restart and reload the UI during the restore process.
|
||||
</BaseModal>
|
||||
<Modal title="Restore Backup" footer={footer}>
|
||||
<span>
|
||||
Are you sure you want to restore the backup '{result}'? Bazarr will
|
||||
automatically restart and reload the UI during the restore process.
|
||||
</span>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemBackupRestoreModal;
|
||||
export default withModal(SystemBackupRestoreModal, "restore");
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ActionButton, PageTable } from "@/components";
|
||||
import { useModalControl } from "@/modules/redux/hooks/modal";
|
||||
import { useModalControl } from "@/modules/modals";
|
||||
import { faClock, faHistory, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
|
@ -42,11 +42,15 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
|
|||
<ButtonGroup>
|
||||
<ActionButton
|
||||
icon={faHistory}
|
||||
onClick={() => show("restore", row.row.original.filename)}
|
||||
onClick={() =>
|
||||
show(SystemBackupRestoreModal, row.row.original.filename)
|
||||
}
|
||||
></ActionButton>
|
||||
<ActionButton
|
||||
icon={faTrash}
|
||||
onClick={() => show("delete", row.row.original.filename)}
|
||||
onClick={() =>
|
||||
show(SystemBackupDeleteModal, row.row.original.filename)
|
||||
}
|
||||
></ActionButton>
|
||||
</ButtonGroup>
|
||||
);
|
||||
|
@ -59,14 +63,8 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<PageTable columns={columns} data={backups}></PageTable>
|
||||
<SystemBackupRestoreModal
|
||||
modalKey="restore"
|
||||
size="lg"
|
||||
></SystemBackupRestoreModal>
|
||||
<SystemBackupDeleteModal
|
||||
modalKey="delete"
|
||||
size="lg"
|
||||
></SystemBackupDeleteModal>
|
||||
<SystemBackupRestoreModal></SystemBackupRestoreModal>
|
||||
<SystemBackupDeleteModal></SystemBackupDeleteModal>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { BaseModal, BaseModalProps } from "@/components";
|
||||
import { usePayload } from "@/modules/redux/hooks/modal";
|
||||
import { useModal, usePayload, withModal } from "@/modules/modals";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
|
||||
const SystemLogModal: FunctionComponent<BaseModalProps> = ({ ...modal }) => {
|
||||
const stack = usePayload<string>(modal.modalKey);
|
||||
const SystemLogModal: FunctionComponent = () => {
|
||||
const stack = usePayload<string>();
|
||||
|
||||
const Modal = useModal();
|
||||
|
||||
const result = useMemo(
|
||||
() =>
|
||||
stack?.split("\\n").map((v, idx) => (
|
||||
|
@ -13,13 +15,14 @@ const SystemLogModal: FunctionComponent<BaseModalProps> = ({ ...modal }) => {
|
|||
)),
|
||||
[stack]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal title="Stack traceback" {...modal}>
|
||||
<Modal title="Stack traceback">
|
||||
<pre>
|
||||
<code>{result}</code>
|
||||
</pre>
|
||||
</BaseModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemLogModal;
|
||||
export default withModal(SystemLogModal, "system-log");
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ActionButton, PageTable } from "@/components";
|
||||
import { useModalControl } from "@/modules/redux/hooks/modal";
|
||||
import { useModalControl } from "@/modules/modals";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faBug,
|
||||
|
@ -60,7 +60,7 @@ const Table: FunctionComponent<Props> = ({ logs }) => {
|
|||
return (
|
||||
<ActionButton
|
||||
icon={faLayerGroup}
|
||||
onClick={() => show("system-log", value)}
|
||||
onClick={() => show(SystemLogModal, value)}
|
||||
></ActionButton>
|
||||
);
|
||||
} else {
|
||||
|
@ -75,7 +75,7 @@ const Table: FunctionComponent<Props> = ({ logs }) => {
|
|||
return (
|
||||
<>
|
||||
<PageTable columns={columns} data={logs}></PageTable>
|
||||
<SystemLogModal size="xl" modalKey="system-log"></SystemLogModal>
|
||||
<SystemLogModal></SystemLogModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue