summaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/ErrorBoundary.tsx8
-rw-r--r--frontend/src/components/ItemOverview.tsx16
-rw-r--r--frontend/src/components/LanguageSelector.tsx4
-rw-r--r--frontend/src/components/Lazy.tsx8
-rw-r--r--frontend/src/components/MassEditor.tsx121
-rw-r--r--frontend/src/components/SearchBar.tsx10
-rw-r--r--frontend/src/components/async.tsx9
-rw-r--r--frontend/src/components/bazarr/Language.tsx88
-rw-r--r--frontend/src/components/bazarr/LanguageProfile.tsx25
-rw-r--r--frontend/src/components/buttons.tsx6
-rw-r--r--frontend/src/components/header/Button.tsx10
-rw-r--r--frontend/src/components/header/Group.tsx2
-rw-r--r--frontend/src/components/header/index.tsx5
-rw-r--r--frontend/src/components/header/style.scss9
-rw-r--r--frontend/src/components/index.tsx40
-rw-r--r--frontend/src/components/inputs/Chips.tsx3
-rw-r--r--frontend/src/components/inputs/FileBrowser.tsx7
-rw-r--r--frontend/src/components/inputs/FileForm.tsx2
-rw-r--r--frontend/src/components/inputs/Selector.tsx83
-rw-r--r--frontend/src/components/inputs/Slider.tsx3
-rw-r--r--frontend/src/components/inputs/blacklist.tsx2
-rw-r--r--frontend/src/components/inputs/chip.scss43
-rw-r--r--frontend/src/components/inputs/selector.scss30
-rw-r--r--frontend/src/components/inputs/slider.scss49
-rw-r--r--frontend/src/components/modals/BaseModal.tsx23
-rw-r--r--frontend/src/components/modals/HistoryModal.tsx31
-rw-r--r--frontend/src/components/modals/ItemEditorModal.tsx19
-rw-r--r--frontend/src/components/modals/ManualSearchModal.tsx46
-rw-r--r--frontend/src/components/modals/MovieUploadModal.tsx33
-rw-r--r--frontend/src/components/modals/SeriesUploadModal.tsx63
-rw-r--r--frontend/src/components/modals/SubtitleToolModal.tsx76
-rw-r--r--frontend/src/components/modals/SubtitleUploadModal.tsx109
-rw-r--r--frontend/src/components/modals/hooks.tsx90
-rw-r--r--frontend/src/components/modals/index.ts2
-rw-r--r--frontend/src/components/modals/msmStyle.scss20
-rw-r--r--frontend/src/components/modals/provider.tsx84
-rw-r--r--frontend/src/components/modals/toolOptions.ts2
-rw-r--r--frontend/src/components/tables/BaseTable.tsx2
-rw-r--r--frontend/src/components/tables/GroupTable.tsx5
-rw-r--r--frontend/src/components/tables/PageControl.tsx2
-rw-r--r--frontend/src/components/tables/PageTable.tsx25
-rw-r--r--frontend/src/components/tables/QueryPageTable.tsx10
-rw-r--r--frontend/src/components/tables/SimpleTable.tsx9
-rw-r--r--frontend/src/components/tables/plugins/useCustomSelection.tsx67
-rw-r--r--frontend/src/components/tables/plugins/useDefaultSettings.tsx2
-rw-r--r--frontend/src/components/views/HistoryView.tsx3
-rw-r--r--frontend/src/components/views/ItemView.tsx191
-rw-r--r--frontend/src/components/views/WantedView.tsx13
48 files changed, 613 insertions, 897 deletions
diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx
index e419d6da5..b7b7494fb 100644
--- a/frontend/src/components/ErrorBoundary.tsx
+++ b/frontend/src/components/ErrorBoundary.tsx
@@ -1,12 +1,12 @@
-import UIError from "pages/UIError";
-import React from "react";
+import UIError from "@/pages/UIError";
+import { Component } from "react";
interface State {
error: Error | null;
}
-class ErrorBoundary extends React.Component<{}, State> {
- constructor(props: {}) {
+class ErrorBoundary extends Component<object, State> {
+ constructor(props: object) {
super(props);
this.state = { error: null };
}
diff --git a/frontend/src/components/ItemOverview.tsx b/frontend/src/components/ItemOverview.tsx
index 7915a2ed4..565430f2e 100644
--- a/frontend/src/components/ItemOverview.tsx
+++ b/frontend/src/components/ItemOverview.tsx
@@ -1,3 +1,8 @@
+import { BuildKey, isMovie } from "@/utilities";
+import {
+ useLanguageProfileBy,
+ useProfileItemsToLanguages,
+} from "@/utilities/languages";
import {
faBookmark as farBookmark,
faClone as fasClone,
@@ -12,7 +17,7 @@ import {
IconDefinition,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import React, { FunctionComponent, useMemo } from "react";
+import { FunctionComponent, useMemo } from "react";
import {
Badge,
Col,
@@ -22,12 +27,7 @@ import {
Popover,
Row,
} from "react-bootstrap";
-import { BuildKey, isMovie } from "utilities";
-import {
- useLanguageProfileBy,
- useProfileItemsToLanguages,
-} from "utilities/languages";
-import { LanguageText } from ".";
+import Language from "./bazarr/Language";
interface Props {
item: Item.Base;
@@ -102,7 +102,7 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
icon={faLanguage}
desc="Language"
>
- <LanguageText long text={v}></LanguageText>
+ <Language.Text long value={v}></Language.Text>
</DetailBadge>
))
);
diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx
index 1d6271a64..648ccf167 100644
--- a/frontend/src/components/LanguageSelector.tsx
+++ b/frontend/src/components/LanguageSelector.tsx
@@ -1,5 +1,5 @@
-import { Selector, SelectorProps } from "components";
-import React, { useMemo } from "react";
+import { Selector, SelectorOption, SelectorProps } from "@/components";
+import { useMemo } from "react";
interface Props {
options: readonly Language.Info[];
diff --git a/frontend/src/components/Lazy.tsx b/frontend/src/components/Lazy.tsx
new file mode 100644
index 000000000..a22dfd15a
--- /dev/null
+++ b/frontend/src/components/Lazy.tsx
@@ -0,0 +1,8 @@
+import { FunctionComponent, Suspense } from "react";
+import { LoadingIndicator } from ".";
+
+const Lazy: FunctionComponent = ({ children }) => {
+ return <Suspense fallback={<LoadingIndicator />}>{children}</Suspense>;
+};
+
+export default Lazy;
diff --git a/frontend/src/components/MassEditor.tsx b/frontend/src/components/MassEditor.tsx
new file mode 100644
index 000000000..9a6fdb0e3
--- /dev/null
+++ b/frontend/src/components/MassEditor.tsx
@@ -0,0 +1,121 @@
+import { useIsAnyMutationRunning, useLanguageProfiles } from "@/apis/hooks";
+import { GetItemId } from "@/utilities";
+import { faCheck, faUndo } from "@fortawesome/free-solid-svg-icons";
+import { uniqBy } from "lodash";
+import { useCallback, useMemo, useState } from "react";
+import { Container, Dropdown, Row } from "react-bootstrap";
+import { UseMutationResult } from "react-query";
+import { useNavigate } from "react-router-dom";
+import { Column, useRowSelect } from "react-table";
+import { ContentHeader, SimpleTable } from ".";
+import { useCustomSelection } from "./tables/plugins";
+
+interface MassEditorProps<T extends Item.Base = Item.Base> {
+ columns: Column<T>[];
+ data: T[];
+ mutation: UseMutationResult<void, unknown, FormType.ModifyItem>;
+}
+
+function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
+ const { columns, data: raw, mutation } = props;
+
+ const [selections, setSelections] = useState<T[]>([]);
+ const [dirties, setDirties] = useState<T[]>([]);
+ const hasTask = useIsAnyMutationRunning();
+ const { data: profiles } = useLanguageProfiles();
+
+ const navigate = useNavigate();
+
+ const onEnded = useCallback(() => navigate(".."), [navigate]);
+
+ const data = useMemo(
+ () => uniqBy([...dirties, ...(raw ?? [])], GetItemId),
+ [dirties, raw]
+ );
+
+ const profileOptions = useMemo(() => {
+ const items: JSX.Element[] = [];
+ if (profiles) {
+ items.push(
+ <Dropdown.Item key="clear-profile">Clear Profile</Dropdown.Item>
+ );
+ items.push(<Dropdown.Divider key="dropdown-divider"></Dropdown.Divider>);
+ items.push(
+ ...profiles.map((v) => (
+ <Dropdown.Item key={v.profileId} eventKey={v.profileId.toString()}>
+ {v.name}
+ </Dropdown.Item>
+ ))
+ );
+ }
+
+ return items;
+ }, [profiles]);
+
+ const { mutateAsync } = mutation;
+
+ const save = useCallback(() => {
+ const form: FormType.ModifyItem = {
+ id: [],
+ profileid: [],
+ };
+ dirties.forEach((v) => {
+ const id = GetItemId(v);
+ if (id) {
+ form.id.push(id);
+ form.profileid.push(v.profileId);
+ }
+ });
+ return mutateAsync(form);
+ }, [dirties, mutateAsync]);
+
+ const setProfiles = useCallback(
+ (key: Nullable<string>) => {
+ const id = key ? parseInt(key) : null;
+
+ const newItems = selections.map((v) => ({ ...v, profileId: id }));
+
+ setDirties((dirty) => {
+ return uniqBy([...newItems, ...dirty], GetItemId);
+ });
+ },
+ [selections]
+ );
+ return (
+ <Container fluid>
+ <ContentHeader scroll={false}>
+ <ContentHeader.Group pos="start">
+ <Dropdown onSelect={setProfiles}>
+ <Dropdown.Toggle disabled={selections.length === 0} variant="light">
+ Change Profile
+ </Dropdown.Toggle>
+ <Dropdown.Menu>{profileOptions}</Dropdown.Menu>
+ </Dropdown>
+ </ContentHeader.Group>
+ <ContentHeader.Group pos="end">
+ <ContentHeader.Button icon={faUndo} onClick={onEnded}>
+ Cancel
+ </ContentHeader.Button>
+ <ContentHeader.AsyncButton
+ icon={faCheck}
+ disabled={dirties.length === 0 || hasTask}
+ promise={save}
+ onSuccess={onEnded}
+ >
+ Save
+ </ContentHeader.AsyncButton>
+ </ContentHeader.Group>
+ </ContentHeader>
+ <Row>
+ <SimpleTable
+ columns={columns}
+ data={data}
+ onSelect={setSelections}
+ plugins={[useRowSelect, useCustomSelection]}
+ ></SimpleTable>
+ </Row>
+ </Container>
+ );
+}
+
+export default MassEditor;
diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx
index f8de27ec4..acdfd56a9 100644
--- a/frontend/src/components/SearchBar.tsx
+++ b/frontend/src/components/SearchBar.tsx
@@ -1,6 +1,6 @@
-import { useServerSearch } from "apis/hooks";
+import { useServerSearch } from "@/apis/hooks";
import { uniqueId } from "lodash";
-import React, {
+import {
FunctionComponent,
useCallback,
useEffect,
@@ -8,7 +8,7 @@ import React, {
useState,
} from "react";
import { Dropdown, Form } from "react-bootstrap";
-import { useHistory } from "react-router";
+import { useNavigate } from "react-router-dom";
import { useThrottle } from "rooks";
function useSearch(query: string) {
@@ -66,7 +66,7 @@ export const SearchBar: FunctionComponent<Props> = ({
const results = useSearch(query);
- const history = useHistory();
+ const navigate = useNavigate();
const clear = useCallback(() => {
setDisplay("");
@@ -100,7 +100,7 @@ export const SearchBar: FunctionComponent<Props> = ({
onSelect={(link) => {
if (link) {
clear();
- history.push(link);
+ navigate(link);
}
}}
>
diff --git a/frontend/src/components/async.tsx b/frontend/src/components/async.tsx
index 105cd567e..7c781707e 100644
--- a/frontend/src/components/async.tsx
+++ b/frontend/src/components/async.tsx
@@ -4,9 +4,10 @@ import {
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import React, {
+import {
FunctionComponent,
PropsWithChildren,
+ ReactElement,
useCallback,
useEffect,
useState,
@@ -18,7 +19,7 @@ import { LoadingIndicator } from ".";
interface QueryOverlayProps {
result: UseQueryResult<unknown, unknown>;
- children: React.ReactElement;
+ children: ReactElement;
}
export const QueryOverlay: FunctionComponent<QueryOverlayProps> = ({
@@ -43,9 +44,7 @@ export function PromiseOverlay<T>({ promise, children }: PromiseProps<T>) {
const [item, setItem] = useState<T | null>(null);
useEffect(() => {
- promise()
- .then(setItem)
- .catch(() => {});
+ promise().then(setItem);
}, [promise]);
if (item === null) {
diff --git a/frontend/src/components/bazarr/Language.tsx b/frontend/src/components/bazarr/Language.tsx
new file mode 100644
index 000000000..ad60a7b65
--- /dev/null
+++ b/frontend/src/components/bazarr/Language.tsx
@@ -0,0 +1,88 @@
+import { useLanguages } from "@/apis/hooks";
+import { Selector, SelectorOption, SelectorProps } from "@/components";
+import { FunctionComponent, useMemo } from "react";
+
+interface TextProps {
+ value: Language.Info;
+ className?: string;
+ long?: boolean;
+}
+
+declare type LanguageComponent = {
+ Text: typeof LanguageText;
+ Selector: typeof LanguageSelector;
+};
+
+const LanguageText: FunctionComponent<TextProps> = ({
+ value,
+ className,
+ long,
+}) => {
+ const result = useMemo(() => {
+ let lang = value.code2;
+ let hi = ":HI";
+ let forced = ":Forced";
+ if (long) {
+ lang = value.name;
+ hi = " HI";
+ forced = " Forced";
+ }
+
+ let res = lang;
+ if (value.hi) {
+ res += hi;
+ } else if (value.forced) {
+ res += forced;
+ }
+ return res;
+ }, [value, long]);
+
+ return (
+ <span title={value.name} className={className}>
+ {result}
+ </span>
+ );
+};
+
+type LanguageSelectorProps<M extends boolean> = Omit<
+ SelectorProps<Language.Info, M>,
+ "label" | "options"
+> & {
+ history?: boolean;
+};
+
+function getLabel(lang: Language.Info) {
+ return lang.name;
+}
+
+export function LanguageSelector<M extends boolean = false>(
+ props: LanguageSelectorProps<M>
+) {
+ const { history, ...rest } = props;
+ const { data: options } = useLanguages(history);
+
+ const items = useMemo<SelectorOption<Language.Info>[]>(
+ () =>
+ options?.map((v) => ({
+ label: v.name,
+ value: v,
+ })) ?? [],
+ [options]
+ );
+
+ return (
+ <Selector
+ placeholder="Language..."
+ options={items}
+ label={getLabel}
+ {...rest}
+ ></Selector>
+ );
+}
+
+const Components: LanguageComponent = {
+ Text: LanguageText,
+ Selector: LanguageSelector,
+};
+
+export default Components;
diff --git a/frontend/src/components/bazarr/LanguageProfile.tsx b/frontend/src/components/bazarr/LanguageProfile.tsx
new file mode 100644
index 000000000..c3724dc89
--- /dev/null
+++ b/frontend/src/components/bazarr/LanguageProfile.tsx
@@ -0,0 +1,25 @@
+import { useLanguageProfiles } from "@/apis/hooks";
+import { FunctionComponent, useMemo } from "react";
+
+interface Props {
+ index: number | null;
+ className?: string;
+ empty?: string;
+}
+
+const LanguageProfile: FunctionComponent<Props> = ({
+ index,
+ className,
+ empty = "Unknown Profile",
+}) => {
+ const { data } = useLanguageProfiles();
+
+ const name = useMemo(
+ () => data?.find((v) => v.profileId === index)?.name ?? empty,
+ [data, empty, index]
+ );
+
+ return <span className={className}>{name}</span>;
+};
+
+export default LanguageProfile;
diff --git a/frontend/src/components/buttons.tsx b/frontend/src/components/buttons.tsx
index c472c1256..db8b9836a 100644
--- a/frontend/src/components/buttons.tsx
+++ b/frontend/src/components/buttons.tsx
@@ -1,7 +1,7 @@
import { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import React, { FunctionComponent, MouseEvent } from "react";
+import { FunctionComponent, MouseEvent } from "react";
import { Badge, Button, ButtonProps } from "react-bootstrap";
export const ActionBadge: FunctionComponent<{
@@ -66,7 +66,7 @@ export const ActionButtonItem: FunctionComponent<ActionButtonItemProps> = ({
}) => {
const showText = alwaysShowText === true || loading !== true;
return (
- <React.Fragment>
+ <>
<FontAwesomeIcon
style={{ width: "1rem" }}
icon={loading ? faCircleNotch : icon}
@@ -75,6 +75,6 @@ export const ActionButtonItem: FunctionComponent<ActionButtonItemProps> = ({
{children && showText ? (
<span className="ml-2 font-weight-bold">{children}</span>
) : null}
- </React.Fragment>
+ </>
);
};
diff --git a/frontend/src/components/header/Button.tsx b/frontend/src/components/header/Button.tsx
index fa0480689..7cd952876 100644
--- a/frontend/src/components/header/Button.tsx
+++ b/frontend/src/components/header/Button.tsx
@@ -1,7 +1,7 @@
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import React, {
+import {
FunctionComponent,
MouseEvent,
PropsWithChildren,
@@ -46,13 +46,13 @@ const ContentHeaderButton: FunctionComponent<CHButtonProps> = (props) => {
);
};
-type CHAsyncButtonProps<T extends () => Promise<any>> = {
+type CHAsyncButtonProps<R, T extends () => Promise<R>> = {
promise: T;
- onSuccess?: (item: PromiseType<ReturnType<T>>) => void;
+ onSuccess?: (item: R) => void;
} & Omit<CHButtonProps, "updating" | "updatingIcon" | "onClick">;
-export function ContentHeaderAsyncButton<T extends () => Promise<any>>(
- props: PropsWithChildren<CHAsyncButtonProps<T>>
+export function ContentHeaderAsyncButton<R, T extends () => Promise<R>>(
+ props: PropsWithChildren<CHAsyncButtonProps<R, T>>
): JSX.Element {
const { promise, onSuccess, ...button } = props;
diff --git a/frontend/src/components/header/Group.tsx b/frontend/src/components/header/Group.tsx
index b7bd2d4ee..085065631 100644
--- a/frontend/src/components/header/Group.tsx
+++ b/frontend/src/components/header/Group.tsx
@@ -1,4 +1,4 @@
-import React, { FunctionComponent } from "react";
+import { FunctionComponent } from "react";
type GroupPosition = "start" | "end";
interface GroupProps {
diff --git a/frontend/src/components/header/index.tsx b/frontend/src/components/header/index.tsx
index a94fdb7f5..200f9e343 100644
--- a/frontend/src/components/header/index.tsx
+++ b/frontend/src/components/header/index.tsx
@@ -1,8 +1,7 @@
-import React, { FunctionComponent, useMemo } from "react";
+import { FunctionComponent, ReactNode, useMemo } from "react";
import { Row } from "react-bootstrap";
import ContentHeaderButton, { ContentHeaderAsyncButton } from "./Button";
import ContentHeaderGroup from "./Group";
-import "./style.scss";
interface Props {
scroll?: boolean;
@@ -29,7 +28,7 @@ export const ContentHeader: Header = ({ children, scroll, className }) => {
return rowCls.join(" ");
}, [scroll, className]);
- let childItem: React.ReactNode;
+ let childItem: ReactNode;
if (scroll !== false) {
childItem = (
diff --git a/frontend/src/components/header/style.scss b/frontend/src/components/header/style.scss
deleted file mode 100644
index 7ce71bb36..000000000
--- a/frontend/src/components/header/style.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.content-header {
- position: sticky;
- top: 0;
- z-index: 99;
-
- &.scroll {
- overflow-x: auto;
- }
-}
diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx
index dc0e0999c..acb6b1fa1 100644
--- a/frontend/src/components/index.tsx
+++ b/frontend/src/components/index.tsx
@@ -11,7 +11,7 @@ import {
FontAwesomeIconProps,
} from "@fortawesome/react-fontawesome";
import { isNull, isUndefined } from "lodash";
-import React, { FunctionComponent, useMemo } from "react";
+import { FunctionComponent, ReactElement } from "react";
import {
OverlayTrigger,
OverlayTriggerProps,
@@ -97,44 +97,8 @@ export const LoadingIndicator: FunctionComponent<{
);
};
-interface LanguageTextProps {
- text: Language.Info;
- className?: string;
- long?: boolean;
-}
-
-export const LanguageText: FunctionComponent<LanguageTextProps> = ({
- text,
- className,
- long,
-}) => {
- const result = useMemo(() => {
- let lang = text.code2;
- let hi = ":HI";
- let forced = ":Forced";
- if (long) {
- lang = text.name;
- hi = " HI";
- forced = " Forced";
- }
-
- let res = lang;
- if (text.hi) {
- res += hi;
- } else if (text.forced) {
- res += forced;
- }
- return res;
- }, [text, long]);
- return (
- <span title={text.name} className={className}>
- {result}
- </span>
- );
-};
-
interface TextPopoverProps {
- children: React.ReactElement<any, any>;
+ children: ReactElement;
text: string | undefined | null;
placement?: OverlayTriggerProps["placement"];
delay?: number;
diff --git a/frontend/src/components/inputs/Chips.tsx b/frontend/src/components/inputs/Chips.tsx
index 1be0050a0..e52f751d7 100644
--- a/frontend/src/components/inputs/Chips.tsx
+++ b/frontend/src/components/inputs/Chips.tsx
@@ -1,4 +1,4 @@
-import React, {
+import {
FocusEvent,
FunctionComponent,
KeyboardEvent,
@@ -8,7 +8,6 @@ import React, {
useRef,
useState,
} from "react";
-import "./chip.scss";
const SplitKeys = ["Tab", "Enter", " ", ",", ";"];
diff --git a/frontend/src/components/inputs/FileBrowser.tsx b/frontend/src/components/inputs/FileBrowser.tsx
index f3bc1d37d..5bf289de7 100644
--- a/frontend/src/components/inputs/FileBrowser.tsx
+++ b/frontend/src/components/inputs/FileBrowser.tsx
@@ -1,8 +1,9 @@
+import { useFileSystem } from "@/apis/hooks";
import { faFile, faFolder } from "@fortawesome/free-regular-svg-icons";
import { faReply } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useFileSystem } from "apis/hooks";
-import React, {
+import {
+ ChangeEvent,
FunctionComponent,
useEffect,
useMemo,
@@ -147,7 +148,7 @@ export const FileBrowser: FunctionComponent<FileBrowserProps> = ({
placeholder="Click to start"
type="text"
value={text}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
setText(e.currentTarget.value);
}}
ref={input}
diff --git a/frontend/src/components/inputs/FileForm.tsx b/frontend/src/components/inputs/FileForm.tsx
index bfff960d6..af7a6a238 100644
--- a/frontend/src/components/inputs/FileForm.tsx
+++ b/frontend/src/components/inputs/FileForm.tsx
@@ -1,4 +1,4 @@
-import React, {
+import {
ChangeEvent,
FunctionComponent,
useEffect,
diff --git a/frontend/src/components/inputs/Selector.tsx b/frontend/src/components/inputs/Selector.tsx
index f3df67459..98ea9eeb3 100644
--- a/frontend/src/components/inputs/Selector.tsx
+++ b/frontend/src/components/inputs/Selector.tsx
@@ -1,8 +1,22 @@
-import { isArray } from "lodash";
-import React, { useCallback, useMemo } from "react";
-import Select from "react-select";
+import clsx from "clsx";
+import { FocusEvent, useCallback, useMemo, useRef } from "react";
+import Select, { GroupBase, OnChangeValue } from "react-select";
import { SelectComponents } from "react-select/dist/declarations/src/components";
-import "./selector.scss";
+
+export type SelectorOption<T> = {
+ label: string;
+ value: T;
+};
+
+export type SelectorComponents<T, M extends boolean> = SelectComponents<
+ SelectorOption<T>,
+ M,
+ GroupBase<SelectorOption<T>>
+>;
+
+export type SelectorValueType<T, M extends boolean> = M extends true
+ ? ReadonlyArray<T>
+ : Nullable<T>;
export interface SelectorProps<T, M extends boolean> {
className?: string;
@@ -13,11 +27,13 @@ export interface SelectorProps<T, M extends boolean> {
loading?: boolean;
multiple?: M;
onChange?: (k: SelectorValueType<T, M>) => void;
- onFocus?: (e: React.FocusEvent<HTMLElement>) => void;
+ onFocus?: (e: FocusEvent<HTMLElement>) => void;
label?: (item: T) => string;
defaultValue?: SelectorValueType<T, M>;
value?: SelectorValueType<T, M>;
- components?: Partial<SelectComponents<T, M, any>>;
+ components?: Partial<
+ SelectComponents<SelectorOption<T>, M, GroupBase<SelectorOption<T>>>
+ >;
}
export function Selector<T = string, M extends boolean = false>(
@@ -39,34 +55,45 @@ export function Selector<T = string, M extends boolean = false>(
value,
} = props;
- const nameFromItems = useCallback(
+ const labelRef = useRef(label);
+
+ const getName = useCallback(
(item: T) => {
- return options.find((v) => v.value === item)?.label;
+ if (labelRef.current) {
+ return labelRef.current(item);
+ }
+
+ return options.find((v) => v.value === item)?.label ?? "Unknown";
},
[options]
);
- // TODO: Force as any
const wrapper = useCallback(
- (value: SelectorValueType<T, M> | undefined | null): any => {
- if (value !== null && value !== undefined) {
- if (multiple) {
+ (
+ value: SelectorValueType<T, M> | undefined | null
+ ):
+ | SelectorOption<T>
+ | ReadonlyArray<SelectorOption<T>>
+ | null
+ | undefined => {
+ if (value === null || value === undefined) {
+ return value as null | undefined;
+ } else {
+ if (multiple === true) {
return (value as SelectorValueType<T, true>).map((v) => ({
- label: label ? label(v) : nameFromItems(v) ?? "Unknown",
+ label: getName(v),
value: v,
}));
} else {
const v = value as T;
return {
- label: label ? label(v) : nameFromItems(v) ?? "Unknown",
+ label: getName(v),
value: v,
};
}
}
-
- return value;
},
- [label, multiple, nameFromItems]
+ [multiple, getName]
);
const defaultWrapper = useMemo(
@@ -89,21 +116,23 @@ export function Selector<T = string, M extends boolean = false>(
isDisabled={disabled}
options={options}
components={components}
- className={`custom-selector w-100 ${className ?? ""}`}
+ className={clsx("custom-selector w-100", className)}
classNamePrefix="selector"
onFocus={onFocus}
- onChange={(v: SelectorOption<T>[]) => {
+ onChange={(newValue) => {
if (onChange) {
- let res: T | T[] | null = null;
- if (isArray(v)) {
- res = (v as ReadonlyArray<SelectorOption<T>>).map(
- (val) => val.value
- );
+ if (multiple === true) {
+ const values = (
+ newValue as OnChangeValue<SelectorOption<T>, true>
+ ).map((v) => v.value) as ReadonlyArray<T>;
+
+ onChange(values as SelectorValueType<T, M>);
} else {
- res = (v as SelectorOption<T>)?.value ?? null;
+ const value = (newValue as OnChangeValue<SelectorOption<T>, false>)
+ ?.value;
+
+ onChange(value as SelectorValueType<T, M>);
}
- // TODO: Force as any
- onChange(res as any);
}
}}
></Select>
diff --git a/frontend/src/components/inputs/Slider.tsx b/frontend/src/components/inputs/Slider.tsx
index e8a8c3c35..ab3fb0996 100644
--- a/frontend/src/components/inputs/Slider.tsx
+++ b/frontend/src/components/inputs/Slider.tsx
@@ -1,7 +1,6 @@
import RcSlider from "rc-slider";
import "rc-slider/assets/index.css";
-import React, { FunctionComponent, useMemo, useState } from "react";
-import "./slider.scss";
+import { FunctionComponent, useMemo, useState } from "react";
type TooltipsOptions = boolean | "Always";
diff --git a/frontend/src/components/inputs/blacklist.tsx b/frontend/src/components/inputs/blacklist.tsx
index fe079a925..d55e843ec 100644
--- a/frontend/src/components/inputs/blacklist.tsx
+++ b/frontend/src/components/inputs/blacklist.tsx
@@ -1,6 +1,6 @@
import { faFileExcel } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import React, { FunctionComponent } from "react";
+import { FunctionComponent } from "react";
import { AsyncButton } from "..";
interface Props {
diff --git a/frontend/src/components/inputs/chip.scss b/frontend/src/components/inputs/chip.scss
deleted file mode 100644
index 92e172541..000000000
--- a/frontend/src/components/inputs/chip.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-.custom-chip-input {
- overflow: hidden;
- &:focus-within {
- border-color: var(--primary);
- }
- .main-input {
- border: hidden;
- outline: none;
- width: 100%;
- flex-grow: 2;
- }
-
- .chip-container {
- display: flex;
- flex-wrap: nowrap;
- flex-grow: 1;
- }
-
- .custom-chip {
- padding: 0 0.5rem;
- margin-right: 0.25rem;
- background-color: var(--light);
- border-radius: 0.25rem;
-
- max-width: 10rem;
-
- overflow-x: hidden;
- text-overflow: ellipsis;
-
- transition: {
- duration: 0.1s;
- timing-function: ease-in-out;
- }
-
- &.active {
- &:hover {
- cursor: pointer;
- background-color: var(--danger);
- color: white;
- }
- }
- }
-}
diff --git a/frontend/src/components/inputs/selector.scss b/frontend/src/components/inputs/selector.scss
deleted file mode 100644
index 5f4b139c5..000000000
--- a/frontend/src/components/inputs/selector.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-@import "../../@scss/variable.scss";
-
-.custom-selector {
- .selector__control {
- outline: none !important;
- box-shadow: none !important;
- border-radius: 0.25rem;
- }
- .selector__control--is-focused {
- border-color: var(--primary) !important;
- }
- .selector__menu {
- z-index: 1000;
- }
-
- .selector__option--is-focused {
- background-color: $theme-color-transparent;
- &:focus,
- &:active {
- background-color: $theme-color-less-transparent;
- }
- }
-
- .selector__option--is-selected {
- background-color: var(--primary);
- &:active {
- background-color: $theme-color-darked;
- }
- }
-}
diff --git a/frontend/src/components/inputs/slider.scss b/frontend/src/components/inputs/slider.scss
deleted file mode 100644
index f0fe518dd..000000000
--- a/frontend/src/components/inputs/slider.scss
+++ /dev/null
@@ -1,49 +0,0 @@
-.custom-rc-slider {
- .rc-slider-track {
- background-color: var(--primary);
- }
- .rc-slider-step {
- cursor: pointer;
- }
- .rc-slider-handle {
- border: 3px solid var(--primary);
- margin-top: 0;
- top: 50%;
- transform: translate(-50%, -50%) !important;
- width: 18px;
- height: 18px;
- cursor: pointer;
-
- .rc-slider-handle-tips-always {
- display: block !important;
- }
-
- .rc-slider-handle-tips-hidden {
- display: none !important;
- }
-
- .rc-slider-handle-tips {
- font-size: medium;
- display: none;
- position: absolute;
- top: -1.1rem;
- left: 50%;
- transform: translate(-50%, -50%);
- }
-
- &:hover {
- border-color: var(--primary);
- .rc-slider-handle-tips {
- display: block;
- }
- }
-
- &:active {
- border-color: var(--primary);
- box-shadow: none;
- .rc-slider-handle-tips {
- display: block;
- }
- }
- }
-}
diff --git a/frontend/src/components/modals/BaseModal.tsx b/frontend/src/components/modals/BaseModal.tsx
index 89021effb..f9488ef6b 100644
--- a/frontend/src/components/modals/BaseModal.tsx
+++ b/frontend/src/components/modals/BaseModal.tsx
@@ -1,6 +1,7 @@
-import React, { FunctionComponent, useCallback, useState } from "react";
+import { useIsShowed, useModalControl } from "@/modules/redux/hooks/modal";
+import clsx from "clsx";
+import { FunctionComponent, useCallback, useState } from "react";
import { Modal } from "react-bootstrap";
-import { useModalInformation } from "./hooks";
export interface BaseModalProps {
modalKey: string;
@@ -11,32 +12,34 @@ export interface BaseModalProps {
}
export const BaseModal: FunctionComponent<BaseModalProps> = (props) => {
- const { size, modalKey, title, children, footer } = props;
+ const { size, modalKey, title, children, footer, closeable = true } = props;
const [needExit, setExit] = useState(false);
- const { isShow, closeModal } = useModalInformation(modalKey);
-
- const closeable = props.closeable !== false;
+ const { hide: hideModal } = useModalControl();
+ const showIndex = useIsShowed(modalKey);
+ const isShowed = showIndex !== -1;
const hide = useCallback(() => {
setExit(true);
}, []);
const exit = useCallback(() => {
- if (isShow) {
- closeModal(modalKey);
+ if (isShowed) {
+ hideModal(modalKey);
}
setExit(false);
- }, [closeModal, modalKey, isShow]);
+ }, [isShowed, hideModal, modalKey]);
return (
<Modal
centered
size={size}
- show={isShow && !needExit}
+ 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>
diff --git a/frontend/src/components/modals/HistoryModal.tsx b/frontend/src/components/modals/HistoryModal.tsx
index 7fe8a40f6..dd4adf2bd 100644
--- a/frontend/src/components/modals/HistoryModal.tsx
+++ b/frontend/src/components/modals/HistoryModal.tsx
@@ -3,24 +3,19 @@ import {
useEpisodeHistory,
useMovieAddBlacklist,
useMovieHistory,
-} from "apis/hooks";
-import React, { FunctionComponent, useMemo } from "react";
+} from "@/apis/hooks";
+import { usePayload } from "@/modules/redux/hooks/modal";
+import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table";
-import {
- HistoryIcon,
- LanguageText,
- PageTable,
- QueryOverlay,
- TextPopover,
-} from "..";
+import { HistoryIcon, PageTable, QueryOverlay, TextPopover } from "..";
+import Language from "../bazarr/Language";
import { BlacklistButton } from "../inputs/blacklist";
import BaseModal, { BaseModalProps } from "./BaseModal";
-import { useModalPayload } from "./hooks";
export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => {
const { ...modal } = props;
- const movie = useModalPayload<Item.Movie>(modal.modalKey);
+ const movie = usePayload<Item.Movie>(modal.modalKey);
const history = useMovieHistory(movie?.radarrId);
@@ -40,7 +35,7 @@ export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => {
accessor: "language",
Cell: ({ value }) => {
if (value) {
- return <LanguageText text={value} long></LanguageText>;
+ return <Language.Text value={value} long></Language.Text>;
} else {
return null;
}
@@ -101,12 +96,10 @@ export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => {
);
};
-interface EpisodeHistoryProps {}
-
-export const EpisodeHistoryModal: FunctionComponent<
- BaseModalProps & EpisodeHistoryProps
-> = (props) => {
- const episode = useModalPayload<Item.Episode>(props.modalKey);
+export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = (
+ props
+) => {
+ const episode = usePayload<Item.Episode>(props.modalKey);
const history = useEpisodeHistory(episode?.sonarrEpisodeId);
@@ -126,7 +119,7 @@ export const EpisodeHistoryModal: FunctionComponent<
accessor: "language",
Cell: ({ value }) => {
if (value) {
- return <LanguageText text={value} long></LanguageText>;
+ return <Language.Text value={value} long></Language.Text>;
} else {
return null;
}
diff --git a/frontend/src/components/modals/ItemEditorModal.tsx b/frontend/src/components/modals/ItemEditorModal.tsx
index 77bbf66f1..ad714598d 100644
--- a/frontend/src/components/modals/ItemEditorModal.tsx
+++ b/frontend/src/components/modals/ItemEditorModal.tsx
@@ -1,11 +1,11 @@
-import { useIsAnyActionRunning, useLanguageProfiles } from "apis/hooks";
-import React, { FunctionComponent, useEffect, useMemo, useState } from "react";
+import { useIsAnyActionRunning, useLanguageProfiles } from "@/apis/hooks";
+import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
+import { GetItemId } from "@/utilities";
+import { FunctionComponent, useEffect, useMemo, useState } from "react";
import { Container, Form } from "react-bootstrap";
import { UseMutationResult } from "react-query";
-import { GetItemId } from "utilities";
-import { AsyncButton, Selector } from "../";
+import { AsyncButton, Selector, SelectorOption } from "..";
import BaseModal, { BaseModalProps } from "./BaseModal";
-import { useModalInformation } from "./hooks";
interface Props {
mutation: UseMutationResult<void, unknown, FormType.ModifyItem, unknown>;
@@ -16,9 +16,8 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
const { data: profiles } = useLanguageProfiles();
- const { payload, closeModal } = useModalInformation<Item.Base>(
- modal.modalKey
- );
+ const payload = usePayload<Item.Base>(modal.modalKey);
+ const { hide } = useModalControl();
const { mutateAsync, isLoading } = mutation;
@@ -57,7 +56,9 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
return null;
}
}}
- onSuccess={() => closeModal()}
+ onSuccess={() => {
+ hide();
+ }}
>
Save
</AsyncButton>
diff --git a/frontend/src/components/modals/ManualSearchModal.tsx b/frontend/src/components/modals/ManualSearchModal.tsx
index 853d93a49..49c95ea5c 100644
--- a/frontend/src/components/modals/ManualSearchModal.tsx
+++ b/frontend/src/components/modals/ManualSearchModal.tsx
@@ -1,3 +1,7 @@
+import { useEpisodesProvider, useMoviesProvider } from "@/apis/hooks";
+import { usePayload } from "@/modules/redux/hooks/modal";
+import { createAndDispatchTask } from "@/modules/task/utilities";
+import { isMovie } from "@/utilities";
import {
faCaretDown,
faCheck,
@@ -6,15 +10,7 @@ import {
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { dispatchTask } from "@modules/task";
-import { createTask } from "@modules/task/utilities";
-import { useEpisodesProvider, useMoviesProvider } from "apis/hooks";
-import React, {
- FunctionComponent,
- useCallback,
- useMemo,
- useState,
-} from "react";
+import { FunctionComponent, useCallback, useMemo, useState } from "react";
import {
Badge,
Button,
@@ -26,16 +22,8 @@ import {
Row,
} from "react-bootstrap";
import { Column } from "react-table";
-import { GetItemId, isMovie } from "utilities";
-import {
- BaseModal,
- BaseModalProps,
- LanguageText,
- LoadingIndicator,
- PageTable,
- useModalPayload,
-} from "..";
-import "./msmStyle.scss";
+import { BaseModal, BaseModalProps, LoadingIndicator, PageTable } from "..";
+import Language from "../bazarr/Language";
type SupportType = Item.Movie | Item.Episode;
@@ -48,7 +36,7 @@ export function ManualSearchModal<T extends SupportType>(
) {
const { download, ...modal } = props;
- const item = useModalPayload<T>(modal.modalKey);
+ const item = usePayload<T>(modal.modalKey);
const [episodeId, setEpisodeId] = useState<number | undefined>(undefined);
const [radarrId, setRadarrId] = useState<number | undefined>(undefined);
@@ -95,7 +83,7 @@ export function ManualSearchModal<T extends SupportType>(
};
return (
<Badge variant="secondary">
- <LanguageText text={lang}></LanguageText>
+ <Language.Text value={lang}></Language.Text>
</Badge>
);
},
@@ -194,12 +182,12 @@ export function ManualSearchModal<T extends SupportType>(
onClick={() => {
if (!item) return;
- const id = GetItemId(item);
- const task = createTask(item.title, id, download, item, result);
- dispatchTask(
- "Downloading subtitles...",
- [task],
- "Downloading..."
+ createAndDispatchTask(
+ item.title,
+ "download-subtitles",
+ download,
+ item,
+ result
);
}}
>
@@ -226,14 +214,14 @@ export function ManualSearchModal<T extends SupportType>(
return <LoadingIndicator animation="grow"></LoadingIndicator>;
} else {
return (
- <React.Fragment>
+ <>
<p className="mb-3 small">{item?.path ?? ""}</p>
<PageTable
emptyText="No Result"
columns={columns}
data={results}
></PageTable>
- </React.Fragment>
+ </>
);
}
};
diff --git a/frontend/src/components/modals/MovieUploadModal.tsx b/frontend/src/components/modals/MovieUploadModal.tsx
index a5e2705b1..3b3730668 100644
--- a/frontend/src/components/modals/MovieUploadModal.tsx
+++ b/frontend/src/components/modals/MovieUploadModal.tsx
@@ -1,32 +1,27 @@
-import { dispatchTask } from "@modules/task";
-import { createTask } from "@modules/task/utilities";
-import { useMovieSubtitleModification } from "apis/hooks";
-import React, { FunctionComponent, useCallback } from "react";
+import { useMovieSubtitleModification } from "@/apis/hooks";
+import { usePayload } from "@/modules/redux/hooks/modal";
+import { createTask, dispatchTask } from "@/modules/task/utilities";
import {
useLanguageProfileBy,
useProfileItemsToLanguages,
-} from "utilities/languages";
+} from "@/utilities/languages";
+import { FunctionComponent, useCallback } from "react";
import { BaseModalProps } from "./BaseModal";
-import { useModalInformation } from "./hooks";
import SubtitleUploadModal, {
PendingSubtitle,
Validator,
} from "./SubtitleUploadModal";
-interface Payload {}
-
-export const TaskGroupName = "Uploading Subtitles...";
-
const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
const modal = props;
- const { payload } = useModalInformation<Item.Movie>(modal.modalKey);
+ const payload = usePayload<Item.Movie>(modal.modalKey);
const profile = useLanguageProfileBy(payload?.profileId);
const availableLanguages = useProfileItemsToLanguages(profile);
- const update = useCallback(async (list: PendingSubtitle<Payload>[]) => {
+ const update = useCallback(async (list: PendingSubtitle<unknown>[]) => {
return list;
}, []);
@@ -34,7 +29,7 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
upload: { mutateAsync },
} = useMovieSubtitleModification();
- const validate = useCallback<Validator<Payload>>(
+ const validate = useCallback<Validator<unknown>>(
(item) => {
if (item.language === null) {
return {
@@ -59,7 +54,7 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
);
const upload = useCallback(
- (items: PendingSubtitle<Payload>[]) => {
+ (items: PendingSubtitle<unknown>[]) => {
if (payload === null) {
return;
}
@@ -71,18 +66,22 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
.map((v) => {
const { file, language, forced, hi } = v;
- return createTask(file.name, radarrId, mutateAsync, {
+ if (language === null) {
+ throw new Error("Language is not selected");
+ }
+
+ return createTask(file.name, mutateAsync, {
radarrId,
form: {
file,
forced,
hi,
- language: language!.code2,
+ language: language.code2,
},
});
});
- dispatchTask(TaskGroupName, tasks, "Uploading...");
+ dispatchTask(tasks, "upload-subtitles");
},
[mutateAsync, payload]
);
diff --git a/frontend/src/components/modals/SeriesUploadModal.tsx b/frontend/src/components/modals/SeriesUploadModal.tsx
index d7c6d359c..23c3101f3 100644
--- a/frontend/src/components/modals/SeriesUploadModal.tsx
+++ b/frontend/src/components/modals/SeriesUploadModal.tsx
@@ -1,18 +1,18 @@
-import { dispatchTask } from "@modules/task";
-import { createTask } from "@modules/task/utilities";
-import { useEpisodeSubtitleModification } from "apis/hooks";
-import api from "apis/raw";
-import React, { FunctionComponent, useCallback, useMemo } from "react";
-import { Column } from "react-table";
+import { useEpisodeSubtitleModification } from "@/apis/hooks";
+import api from "@/apis/raw";
+import { usePayload } from "@/modules/redux/hooks/modal";
+import { createTask, dispatchTask } from "@/modules/task/utilities";
import {
useLanguageProfileBy,
useProfileItemsToLanguages,
-} from "utilities/languages";
-import { Selector } from "../inputs";
+} from "@/utilities/languages";
+import { FunctionComponent, useCallback, useMemo } from "react";
+import { Column } from "react-table";
+import { Selector, SelectorOption } from "../inputs";
import { BaseModalProps } from "./BaseModal";
-import { useModalInformation } from "./hooks";
import SubtitleUploadModal, {
PendingSubtitle,
+ useRowMutation,
Validator,
} from "./SubtitleUploadModal";
@@ -24,13 +24,11 @@ interface SeriesProps {
episodes: readonly Item.Episode[];
}
-export const TaskGroupName = "Uploading Subtitles...";
-
const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({
episodes,
...modal
}) => {
- const { payload } = useModalInformation<Item.Series>(modal.modalKey);
+ const payload = usePayload<Item.Series>(modal.modalKey);
const profile = useLanguageProfileBy(payload?.profileId);
@@ -98,9 +96,19 @@ const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({
const tasks = items
.filter((v) => v.payload.instance !== undefined)
.map((v) => {
- const { hi, forced, payload, language } = v;
- const { code2 } = language!;
- const { sonarrEpisodeId: episodeId } = payload.instance!;
+ const {
+ hi,
+ forced,
+ payload: { instance },
+ language,
+ } = v;
+
+ if (language === null || instance === null) {
+ throw new Error("Invalid state");
+ }
+
+ const { code2 } = language;
+ const { sonarrEpisodeId: episodeId } = instance;
const form: FormType.UploadSubtitle = {
file: v.file,
@@ -109,14 +117,14 @@ const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({
forced: forced,
};
- return createTask(v.file.name, episodeId, mutateAsync, {
+ return createTask(v.file.name, mutateAsync, {
seriesId,
episodeId,
form,
});
});
- dispatchTask(TaskGroupName, tasks, "Uploading subtitles...");
+ dispatchTask(tasks, "upload-subtitles");
},
[mutateAsync, payload]
);
@@ -128,29 +136,26 @@ const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({
Header: "Episode",
accessor: "payload",
className: "vw-1",
- Cell: ({ value, row, update }) => {
+ Cell: ({ value, row }) => {
const options = episodes.map<SelectorOption<Item.Episode>>((ep) => ({
label: `(${ep.season}x${ep.episode}) ${ep.title}`,
value: ep,
}));
- const change = useCallback(
- (ep: Nullable<Item.Episode>) => {
- if (ep) {
- const newInfo = { ...row.original };
- newInfo.payload.instance = ep;
- update && update(row, newInfo);
- }
- },
- [row, update]
- );
+ const mutate = useRowMutation();
return (
<Selector
disabled={row.original.state === "fetching"}
options={options}
value={value.instance}
- onChange={change}
+ onChange={(ep: Nullable<Item.Episode>) => {
+ if (ep) {
+ const newInfo = { ...row.original };
+ newInfo.payload.instance = ep;
+ mutate(row.index, newInfo);
+ }
+ }}
></Selector>
);
},
diff --git a/frontend/src/components/modals/SubtitleToolModal.tsx b/frontend/src/components/modals/SubtitleToolModal.tsx
index f8891ecff..b15444879 100644
--- a/frontend/src/components/modals/SubtitleToolModal.tsx
+++ b/frontend/src/components/modals/SubtitleToolModal.tsx
@@ -1,3 +1,9 @@
+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,
@@ -14,10 +20,8 @@ import {
faTextHeight,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { dispatchTask } from "@modules/task";
-import { createTask } from "@modules/task/utilities";
-import { useSubtitleAction } from "apis/hooks";
-import React, {
+import {
+ ChangeEventHandler,
FunctionComponent,
useCallback,
useMemo,
@@ -32,22 +36,16 @@ import {
InputGroup,
} from "react-bootstrap";
import { Column, useRowSelect } from "react-table";
-import { isMovie, submodProcessColor } from "utilities";
-import { useEnabledLanguages } from "utilities/languages";
-import { log } from "utilities/logger";
import {
ActionButton,
ActionButtonItem,
LanguageSelector,
- LanguageText,
Selector,
SimpleTable,
- useModalPayload,
- useShowModal,
} from "..";
+import Language from "../bazarr/Language";
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;
@@ -119,18 +117,15 @@ const FrameRateModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
const submit = useCallback(() => {
if (canSave) {
- const action = submodProcessFrameRate(from!, to!);
+ const action = submodProcessFrameRate(from, to);
process(action);
}
}, [canSave, from, to, process]);
- const footer = useMemo(
- () => (
- <Button disabled={!canSave} onClick={submit}>
- Save
- </Button>
- ),
- [submit, canSave]
+ const footer = (
+ <Button disabled={!canSave} onClick={submit}>
+ Save
+ </Button>
);
return (
@@ -176,8 +171,8 @@ const AdjustTimesModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
]);
const updateOffset = useCallback(
- (idx: number) => {
- return (e: any) => {
+ (idx: number): ChangeEventHandler<HTMLInputElement> => {
+ return (e) => {
let value = parseFloat(e.currentTarget.value);
if (isNaN(value)) {
value = 0;
@@ -293,24 +288,22 @@ const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({
);
};
-const TaskGroupName = "Modifying Subtitles";
-
const CanSelectSubtitle = (item: TableColumnType) => {
return item.path.endsWith(".srt");
};
const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
- const payload = useModalPayload<SupportType[]>(props.modalKey);
+ const payload = usePayload<SupportType[]>(props.modalKey);
const [selections, setSelections] = useState<TableColumnType[]>([]);
- const closeModal = useCloseModal();
+ const { hide } = useModalControl();
const { mutateAsync } = useSubtitleAction();
const process = useCallback(
(action: string, override?: Partial<FormType.ModifySubtitle>) => {
- log("info", "executing action", action);
- closeModal(props.modalKey);
+ LOG("info", "executing action", action);
+ hide(props.modalKey);
const tasks = selections.map((s) => {
const form: FormType.ModifySubtitle = {
@@ -320,15 +313,15 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
path: s.path,
...override,
};
- return createTask(s.path, s.id, mutateAsync, { action, form });
+ return createTask(s.path, mutateAsync, { action, form });
});
- dispatchTask(TaskGroupName, tasks, "Modifying subtitles...");
+ dispatchTask(tasks, "modify-subtitles");
},
- [closeModal, props.modalKey, selections, mutateAsync]
+ [hide, props.modalKey, selections, mutateAsync]
);
- const showModal = useShowModal();
+ const { show } = useModalControl();
const columns: Column<TableColumnType>[] = useMemo<Column<TableColumnType>[]>(
() => [
@@ -337,7 +330,7 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
accessor: "_language",
Cell: ({ value }) => (
<Badge variant="secondary">
- <LanguageText text={value} long></LanguageText>
+ <Language.Text value={value} long></Language.Text>
</Badge>
),
},
@@ -345,8 +338,8 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
id: "file",
Header: "File",
accessor: "path",
- Cell: (row) => {
- const path = row.value!;
+ Cell: ({ value }) => {
+ const path = value;
let idx = path.lastIndexOf("/");
@@ -431,29 +424,28 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
Reverse RTL
</ActionButtonItem>
</Dropdown.Item>
- <Dropdown.Item onSelect={() => showModal("add-color")}>
+ <Dropdown.Item onSelect={() => show("add-color")}>
<ActionButtonItem icon={faPaintBrush}>Add Color</ActionButtonItem>
</Dropdown.Item>
- <Dropdown.Item onSelect={() => showModal("change-frame-rate")}>
+ <Dropdown.Item onSelect={() => show("change-frame-rate")}>
<ActionButtonItem icon={faFilm}>Change Frame Rate</ActionButtonItem>
</Dropdown.Item>
- <Dropdown.Item onSelect={() => showModal("adjust-times")}>
+ <Dropdown.Item onSelect={() => show("adjust-times")}>
<ActionButtonItem icon={faClock}>Adjust Times</ActionButtonItem>
</Dropdown.Item>
- <Dropdown.Item onSelect={() => showModal("translate-sub")}>
+ <Dropdown.Item onSelect={() => show("translate-sub")}>
<ActionButtonItem icon={faLanguage}>Translate</ActionButtonItem>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
),
- [showModal, selections.length, process]
+ [selections.length, process, show]
);
return (
- <React.Fragment>
+ <>
<BaseModal title={"Subtitle Tools"} footer={footer} {...props}>
<SimpleTable
- isSelecting={data.length !== 0}
emptyText="No External Subtitles Found"
plugins={plugins}
columns={columns}
@@ -475,7 +467,7 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
process={process}
modalKey="translate-sub"
></TranslateModal>
- </React.Fragment>
+ </>
);
};
diff --git a/frontend/src/components/modals/SubtitleUploadModal.tsx b/frontend/src/components/modals/SubtitleUploadModal.tsx
index b5cb11b9d..c77bd980b 100644
--- a/frontend/src/components/modals/SubtitleUploadModal.tsx
+++ b/frontend/src/components/modals/SubtitleUploadModal.tsx
@@ -1,3 +1,6 @@
+import { useModalControl } from "@/modules/redux/hooks/modal";
+import { BuildKey } from "@/utilities";
+import { LOG } from "@/utilities/console";
import {
faCheck,
faCircleNotch,
@@ -6,15 +9,31 @@ import {
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
import { Button, Container, Form } from "react-bootstrap";
-import { Column, TableUpdater } from "react-table";
-import { BuildKey } from "utilities";
+import { Column } from "react-table";
import { LanguageSelector, MessageIcon } from "..";
import { FileForm } from "../inputs";
import { SimpleTable } from "../tables";
import BaseModal, { BaseModalProps } from "./BaseModal";
-import { useCloseModal } from "./hooks";
+
+type ModifyFn<T> = (index: number, info?: PendingSubtitle<T>) => void;
+
+const RowContext = createContext<ModifyFn<unknown>>(() => {
+ LOG("error", "RowContext not initialized");
+});
+
+export function useRowMutation() {
+ return useContext(RowContext);
+}
export interface PendingSubtitle<P> {
file: File;
@@ -30,7 +49,7 @@ export type Validator<T> = (
item: PendingSubtitle<T>
) => Pick<PendingSubtitle<T>, "state" | "messages">;
-interface Props<T> {
+interface Props<T = unknown> {
initial: T;
availableLanguages: Language.Info[];
upload: (items: PendingSubtitle<T>[]) => void;
@@ -40,9 +59,10 @@ interface Props<T> {
hideAllLanguages?: boolean;
}
-export default function SubtitleUploadModal<T>(
- props: Props<T> & Omit<BaseModalProps, "footer" | "title" | "size">
-) {
+type ComponentProps<T> = Props<T> &
+ Omit<BaseModalProps, "footer" | "title" | "size">;
+
+function SubtitleUploadModal<T>(props: ComponentProps<T>) {
const {
initial,
columns,
@@ -53,7 +73,7 @@ export default function SubtitleUploadModal<T>(
hideAllLanguages,
} = props;
- const closeModal = useCloseModal();
+ const { hide } = useModalControl();
const [pending, setPending] = useState<PendingSubtitle<T>[]>([]);
@@ -72,7 +92,7 @@ export default function SubtitleUploadModal<T>(
language: initialLanguage,
forced: false,
hi: false,
- payload: { ...initialRef.current },
+ payload: initialRef.current,
}));
if (update) {
@@ -95,15 +115,15 @@ export default function SubtitleUploadModal<T>(
[update, validate, availableLanguages]
);
- const modify = useCallback<TableUpdater<PendingSubtitle<T>>>(
- (row, info?: PendingSubtitle<T>) => {
+ const modify = useCallback(
+ (index: number, info?: PendingSubtitle<T>) => {
setPending((pd) => {
const newPending = [...pd];
if (info) {
info = { ...info, ...validate(info) };
- newPending[row.index] = info;
+ newPending[index] = info;
} else {
- newPending.splice(row.index, 1);
+ newPending.splice(index, 1);
}
return newPending;
});
@@ -174,8 +194,9 @@ export default function SubtitleUploadModal<T>(
id: "hi",
Header: "HI",
accessor: "hi",
- Cell: ({ row, value, update }) => {
+ Cell: ({ row, value }) => {
const { original, index } = row;
+ const mutate = useRowMutation();
return (
<Form.Check
custom
@@ -185,7 +206,7 @@ export default function SubtitleUploadModal<T>(
onChange={(v) => {
const newInfo = { ...row.original };
newInfo.hi = v.target.checked;
- update && update(row, newInfo);
+ mutate(row.index, newInfo);
}}
></Form.Check>
);
@@ -195,8 +216,9 @@ export default function SubtitleUploadModal<T>(
id: "forced",
Header: "Forced",
accessor: "forced",
- Cell: ({ row, value, update }) => {
+ Cell: ({ row, value }) => {
const { original, index } = row;
+ const mutate = useRowMutation();
return (
<Form.Check
custom
@@ -206,7 +228,7 @@ export default function SubtitleUploadModal<T>(
onChange={(v) => {
const newInfo = { ...row.original };
newInfo.forced = v.target.checked;
- update && update(row, newInfo);
+ mutate(row.index, newInfo);
}}
></Form.Check>
);
@@ -217,17 +239,18 @@ export default function SubtitleUploadModal<T>(
Header: "Language",
accessor: "language",
className: "w-25",
- Cell: ({ row, update, value }) => {
+ Cell: ({ row, value }) => {
+ const mutate = useRowMutation();
return (
<LanguageSelector
disabled={row.original.state === "fetching"}
options={availableLanguages}
value={value}
onChange={(lang) => {
- if (lang && update) {
+ if (lang) {
const newInfo = { ...row.original };
newInfo.language = lang;
- update(row, newInfo);
+ mutate(row.index, newInfo);
}
}}
></LanguageSelector>
@@ -238,18 +261,21 @@ export default function SubtitleUploadModal<T>(
{
id: "action",
accessor: "file",
- Cell: ({ row, update }) => (
- <Button
- size="sm"
- variant="light"
- disabled={row.original.state === "fetching"}
- onClick={() => {
- update && update(row);
- }}
- >
- <FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
- </Button>
- ),
+ Cell: ({ row }) => {
+ const mutate = useRowMutation();
+ return (
+ <Button
+ size="sm"
+ variant="light"
+ disabled={row.original.state === "fetching"}
+ onClick={() => {
+ mutate(row.index);
+ }}
+ >
+ <FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
+ </Button>
+ );
+ },
},
],
[columns, availableLanguages]
@@ -280,7 +306,7 @@ export default function SubtitleUploadModal<T>(
onClick={() => {
upload(pending);
setFiles([]);
- closeModal();
+ hide();
}}
>
Upload
@@ -325,14 +351,17 @@ export default function SubtitleUploadModal<T>(
</Form.Group>
</Form>
<div hidden={!showTable}>
- <SimpleTable
- columns={columnsWithAction}
- data={pending}
- responsive={false}
- update={modify}
- ></SimpleTable>
+ <RowContext.Provider value={modify as ModifyFn<unknown>}>
+ <SimpleTable
+ columns={columnsWithAction}
+ data={pending}
+ responsive={false}
+ ></SimpleTable>
+ </RowContext.Provider>
</div>
</Container>
</BaseModal>
);
}
+
+export default SubtitleUploadModal;
diff --git a/frontend/src/components/modals/hooks.tsx b/frontend/src/components/modals/hooks.tsx
deleted file mode 100644
index 2b9b4c136..000000000
--- a/frontend/src/components/modals/hooks.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { useCallback, useContext, useMemo } from "react";
-import { useDidUpdate } from "rooks";
-import { log } from "utilities/logger";
-import { ModalContext } from "./provider";
-
-interface ModalInformation<T> {
- isShow: boolean;
- payload: T | null;
- closeModal: ReturnType<typeof useCloseModal>;
-}
-
-export function useModalInformation<T>(key: string): ModalInformation<T> {
- const isShow = useIsModalShow(key);
- const payload = useModalPayload<T>(key);
- const closeModal = useCloseModal();
-
- return useMemo(
- () => ({
- isShow,
- payload,
- closeModal,
- }),
- [isShow, payload, closeModal]
- );
-}
-
-export function useShowModal() {
- const {
- control: { push },
- } = useContext(ModalContext);
-
- return useCallback(
- <T,>(key: string, payload?: T) => {
- log("info", `modal ${key} sending payload`, payload);
-
- push({ key, payload });
- },
- [push]
- );
-}
-
-export function useCloseModal() {
- const {
- control: { pop },
- } = useContext(ModalContext);
- return useCallback(
- (key?: string) => {
- pop(key);
- },
- [pop]
- );
-}
-
-export function useIsModalShow(key: string) {
- const {
- control: { peek },
- } = useContext(ModalContext);
- const modal = peek();
- return key === modal?.key;
-}
-
-export function useOnModalShow<T>(
- callback: (payload: T | null) => void,
- key: string
-) {
- const {
- modals,
- control: { peek },
- } = useContext(ModalContext);
- useDidUpdate(() => {
- const modal = peek();
- if (modal && modal.key === key) {
- callback(modal.payload ?? null);
- }
- }, [modals.length, key]);
-}
-
-export function useModalPayload<T>(key: string): T | null {
- const {
- control: { peek },
- } = useContext(ModalContext);
- return useMemo(() => {
- const modal = peek();
- if (modal && modal.key === key) {
- return (modal.payload as T) ?? null;
- } else {
- return null;
- }
- }, [key, peek]);
-}
diff --git a/frontend/src/components/modals/index.ts b/frontend/src/components/modals/index.ts
index 3662bfe5a..5f02dc94e 100644
--- a/frontend/src/components/modals/index.ts
+++ b/frontend/src/components/modals/index.ts
@@ -1,8 +1,6 @@
export * from "./BaseModal";
export * from "./HistoryModal";
-export * from "./hooks";
export { default as ItemEditorModal } from "./ItemEditorModal";
export { default as MovieUploadModal } from "./MovieUploadModal";
-export { default as ModalProvider } from "./provider";
export { default as SeriesUploadModal } from "./SeriesUploadModal";
export { default as SubtitleToolModal } from "./SubtitleToolModal";
diff --git a/frontend/src/components/modals/msmStyle.scss b/frontend/src/components/modals/msmStyle.scss
deleted file mode 100644
index 91c35b3d7..000000000
--- a/frontend/src/components/modals/msmStyle.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-.release-container {
- flex-wrap: nowrap;
- overflow: hidden;
- .text-container {
- max-width: 500px;
- .release-text {
- text-overflow: ellipsis;
- overflow-wrap: break-word;
- word-wrap: break-word;
- white-space: pre-wrap;
-
- &.hidden-item {
- color: gray;
- }
- }
- }
- &.release-multi {
- cursor: zoom-in;
- }
-}
diff --git a/frontend/src/components/modals/provider.tsx b/frontend/src/components/modals/provider.tsx
deleted file mode 100644
index 0681537da..000000000
--- a/frontend/src/components/modals/provider.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React, {
- FunctionComponent,
- useCallback,
- useMemo,
- useState,
-} from "react";
-
-interface Modal {
- key: string;
- payload: any;
-}
-
-interface ModalControl {
- push: (modal: Modal) => void;
- peek: () => Modal | undefined;
- pop: (key: string | undefined) => void;
-}
-
-interface ModalContextType {
- modals: Modal[];
- control: ModalControl;
-}
-
-export const ModalContext = React.createContext<ModalContextType>({
- modals: [],
- control: {
- push: () => {
- throw new Error("Unimplemented");
- },
- pop: () => {
- throw new Error("Unimplemented");
- },
- peek: () => {
- throw new Error("Unimplemented");
- },
- },
-});
-
-const ModalProvider: FunctionComponent = ({ children }) => {
- 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 } }),
- [stack, push, pop, peek]
- );
-
- return (
- <ModalContext.Provider value={context}>{children}</ModalContext.Provider>
- );
-};
-
-export default ModalProvider;
diff --git a/frontend/src/components/modals/toolOptions.ts b/frontend/src/components/modals/toolOptions.ts
index 5639cd4d0..6acee1a6a 100644
--- a/frontend/src/components/modals/toolOptions.ts
+++ b/frontend/src/components/modals/toolOptions.ts
@@ -1,3 +1,5 @@
+import { SelectorOption } from "..";
+
export const availableTranslation = {
af: "afrikaans",
sq: "albanian",
diff --git a/frontend/src/components/tables/BaseTable.tsx b/frontend/src/components/tables/BaseTable.tsx
index b01109114..e2203128a 100644
--- a/frontend/src/components/tables/BaseTable.tsx
+++ b/frontend/src/components/tables/BaseTable.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo } from "react";
+import { useMemo } from "react";
import { Table } from "react-bootstrap";
import {
HeaderGroup,
diff --git a/frontend/src/components/tables/GroupTable.tsx b/frontend/src/components/tables/GroupTable.tsx
index 62d640252..6940b95a1 100644
--- a/frontend/src/components/tables/GroupTable.tsx
+++ b/frontend/src/components/tables/GroupTable.tsx
@@ -1,6 +1,5 @@
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import React from "react";
import {
Cell,
HeaderGroup,
@@ -13,7 +12,7 @@ import {
import { TableStyleProps } from "./BaseTable";
import SimpleTable from "./SimpleTable";
-function renderCell<T extends object = {}>(cell: Cell<T, any>, row: Row<T>) {
+function renderCell<T extends object = object>(cell: Cell<T>, row: Row<T>) {
if (cell.isGrouped) {
return (
<span {...row.getToggleRowExpandedProps()}>{cell.render("Cell")}</span>
@@ -79,7 +78,7 @@ function renderHeaders<T extends object>(
type Props<T extends object> = TableOptions<T> & TableStyleProps<T>;
-function GroupTable<T extends object = {}>(props: Props<T>) {
+function GroupTable<T extends object = object>(props: Props<T>) {
const plugins = [useGroupBy, useSortBy, useExpanded];
return (
<SimpleTable
diff --git a/frontend/src/components/tables/PageControl.tsx b/frontend/src/components/tables/PageControl.tsx
index 1680c5d39..3408341a2 100644
--- a/frontend/src/components/tables/PageControl.tsx
+++ b/frontend/src/components/tables/PageControl.tsx
@@ -1,4 +1,4 @@
-import React, { FunctionComponent, useMemo } from "react";
+import { FunctionComponent, useMemo } from "react";
import { Col, Container, Pagination, Row } from "react-bootstrap";
import { PageControlAction } from "./types";
interface Props {
diff --git a/frontend/src/components/tables/PageTable.tsx b/frontend/src/components/tables/PageTable.tsx
index f7dfd018c..30a18fff8 100644
--- a/frontend/src/components/tables/PageTable.tsx
+++ b/frontend/src/components/tables/PageTable.tsx
@@ -1,33 +1,22 @@
-import React, { useEffect } from "react";
-import {
- PluginHook,
- TableOptions,
- usePagination,
- useRowSelect,
- useTable,
-} from "react-table";
-import { ScrollToTop } from "utilities";
+import { ScrollToTop } from "@/utilities";
+import { useEffect } from "react";
+import { PluginHook, TableOptions, usePagination, useTable } from "react-table";
import BaseTable, { TableStyleProps, useStyleAndOptions } from "./BaseTable";
import PageControl from "./PageControl";
-import { useCustomSelection, useDefaultSettings } from "./plugins";
+import { useDefaultSettings } from "./plugins";
type Props<T extends object> = TableOptions<T> &
TableStyleProps<T> & {
- canSelect?: boolean;
autoScroll?: boolean;
plugins?: PluginHook<T>[];
};
export default function PageTable<T extends object>(props: Props<T>) {
- const { autoScroll, canSelect, plugins, ...remain } = props;
+ const { autoScroll, plugins, ...remain } = props;
const { style, options } = useStyleAndOptions(remain);
const allPlugins: PluginHook<T>[] = [useDefaultSettings, usePagination];
- if (canSelect) {
- allPlugins.push(useRowSelect, useCustomSelection);
- }
-
if (plugins) {
allPlugins.push(...plugins);
}
@@ -60,7 +49,7 @@ export default function PageTable<T extends object>(props: Props<T>) {
}, [pageIndex, autoScroll]);
return (
- <React.Fragment>
+ <>
<BaseTable
{...style}
headers={headerGroups}
@@ -80,6 +69,6 @@ export default function PageTable<T extends object>(props: Props<T>) {
next={nextPage}
goto={gotoPage}
></PageControl>
- </React.Fragment>
+ </>
);
}
diff --git a/frontend/src/components/tables/QueryPageTable.tsx b/frontend/src/components/tables/QueryPageTable.tsx
index 444e4d40f..0ca26dd3b 100644
--- a/frontend/src/components/tables/QueryPageTable.tsx
+++ b/frontend/src/components/tables/QueryPageTable.tsx
@@ -1,7 +1,7 @@
-import { UsePaginationQueryResult } from "apis/queries/hooks";
-import React, { useEffect } from "react";
+import { UsePaginationQueryResult } from "@/apis/queries/hooks";
+import { ScrollToTop } from "@/utilities";
+import { useEffect } from "react";
import { PluginHook, TableOptions, useTable } from "react-table";
-import { ScrollToTop } from "utilities";
import { LoadingIndicator } from "..";
import BaseTable, { TableStyleProps, useStyleAndOptions } from "./BaseTable";
import PageControl from "./PageControl";
@@ -52,7 +52,7 @@ export default function QueryPageTable<T extends object>(props: Props<T>) {
}
return (
- <React.Fragment>
+ <>
<BaseTable
{...style}
headers={headerGroups}
@@ -72,6 +72,6 @@ export default function QueryPageTable<T extends object>(props: Props<T>) {
next={nextPage}
goto={gotoPage}
></PageControl>
- </React.Fragment>
+ </>
);
}
diff --git a/frontend/src/components/tables/SimpleTable.tsx b/frontend/src/components/tables/SimpleTable.tsx
index 4723e9c21..70b57381c 100644
--- a/frontend/src/components/tables/SimpleTable.tsx
+++ b/frontend/src/components/tables/SimpleTable.tsx
@@ -13,13 +13,8 @@ export default function SimpleTable<T extends object>(props: Props<T>) {
const instance = useTable(options, useDefaultSettings, ...(plugins ?? []));
- const {
- getTableProps,
- getTableBodyProps,
- headerGroups,
- rows,
- prepareRow,
- } = instance;
+ const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
+ instance;
return (
<BaseTable
diff --git a/frontend/src/components/tables/plugins/useCustomSelection.tsx b/frontend/src/components/tables/plugins/useCustomSelection.tsx
index 1f6ea2829..650c161fd 100644
--- a/frontend/src/components/tables/plugins/useCustomSelection.tsx
+++ b/frontend/src/components/tables/plugins/useCustomSelection.tsx
@@ -1,4 +1,4 @@
-import React, { forwardRef, useEffect, useRef } from "react";
+import { forwardRef, useEffect, useRef } from "react";
import { Form } from "react-bootstrap";
import {
CellProps,
@@ -52,10 +52,6 @@ const Checkbox = forwardRef<
});
function useCustomSelection<T extends object>(hooks: Hooks<T>) {
- hooks.visibleColumnsDeps.push((deps, { instance }) => [
- ...deps,
- instance.isSelecting,
- ]);
hooks.visibleColumns.push(visibleColumns);
hooks.useInstance.push(useInstance);
}
@@ -68,7 +64,6 @@ function useInstance<T extends object>(instance: TableInstance<T>) {
rows,
onSelect,
canSelect,
- isSelecting,
state: { selectedRowIds },
} = instance;
@@ -76,18 +71,16 @@ function useInstance<T extends object>(instance: TableInstance<T>) {
useEffect(() => {
// Performance
- if (isSelecting) {
- let items = Object.keys(selectedRowIds).flatMap(
- (v) => rows.find((n) => n.id === v)?.original ?? []
- );
-
- if (canSelect) {
- items = items.filter((v) => canSelect(v));
- }
+ let items = Object.keys(selectedRowIds).flatMap(
+ (v) => rows.find((n) => n.id === v)?.original ?? []
+ );
- onSelect && onSelect(items);
+ if (canSelect) {
+ items = items.filter((v) => canSelect(v));
}
- }, [selectedRowIds, onSelect, rows, isSelecting, canSelect]);
+
+ onSelect && onSelect(items);
+ }, [selectedRowIds, onSelect, rows, canSelect]);
}
function visibleColumns<T extends object>(
@@ -95,31 +88,27 @@ function visibleColumns<T extends object>(
meta: MetaBase<T>
): Column<T>[] {
const { instance } = meta;
- if (instance.isSelecting) {
- const checkbox: Column<T> = {
- id: checkboxId,
- Header: ({ getToggleAllRowsSelectedProps }: HeaderProps<any>) => (
+ const checkbox: Column<T> = {
+ id: checkboxId,
+ Header: ({ getToggleAllRowsSelectedProps }: HeaderProps<T>) => (
+ <Checkbox
+ idIn="table-header-selection"
+ {...getToggleAllRowsSelectedProps()}
+ ></Checkbox>
+ ),
+ Cell: ({ row }: CellProps<T>) => {
+ const canSelect = instance.canSelect;
+ const disabled = (canSelect && !canSelect(row.original)) ?? false;
+ return (
<Checkbox
- idIn="table-header-selection"
- {...getToggleAllRowsSelectedProps()}
+ idIn={`table-cell-${row.index}`}
+ disabled={disabled}
+ {...row.getToggleRowSelectedProps()}
></Checkbox>
- ),
- Cell: ({ row }: CellProps<any>) => {
- const canSelect = instance.canSelect;
- const disabled = (canSelect && !canSelect(row.original)) ?? false;
- return (
- <Checkbox
- idIn={`table-cell-${row.index}`}
- disabled={disabled}
- {...row.getToggleRowSelectedProps()}
- ></Checkbox>
- );
- },
- };
- return [checkbox, ...columns.filter((v) => v.selectHide !== true)];
- } else {
- return columns;
- }
+ );
+ },
+ };
+ return [checkbox, ...columns];
}
export default useCustomSelection;
diff --git a/frontend/src/components/tables/plugins/useDefaultSettings.tsx b/frontend/src/components/tables/plugins/useDefaultSettings.tsx
index 444ee2616..772dfb93a 100644
--- a/frontend/src/components/tables/plugins/useDefaultSettings.tsx
+++ b/frontend/src/components/tables/plugins/useDefaultSettings.tsx
@@ -1,5 +1,5 @@
+import { usePageSize } from "@/utilities/storage";
import { Hooks, TableOptions } from "react-table";
-import { usePageSize } from "utilities/storage";
const pluginName = "useLocalSettings";
diff --git a/frontend/src/components/views/HistoryView.tsx b/frontend/src/components/views/HistoryView.tsx
index fb900218e..fa0bf625f 100644
--- a/frontend/src/components/views/HistoryView.tsx
+++ b/frontend/src/components/views/HistoryView.tsx
@@ -1,5 +1,4 @@
-import { UsePaginationQueryResult } from "apis/queries/hooks";
-import React from "react";
+import { UsePaginationQueryResult } from "@/apis/queries/hooks";
import { Container, Row } from "react-bootstrap";
import { Helmet } from "react-helmet";
import { Column } from "react-table";
diff --git a/frontend/src/components/views/ItemView.tsx b/frontend/src/components/views/ItemView.tsx
index c1c0a2052..8c48e243d 100644
--- a/frontend/src/components/views/ItemView.tsx
+++ b/frontend/src/components/views/ItemView.tsx
@@ -1,69 +1,30 @@
-import { faCheck, faList, faUndo } from "@fortawesome/free-solid-svg-icons";
-import { useIsAnyMutationRunning, useLanguageProfiles } from "apis/hooks";
-import { UsePaginationQueryResult } from "apis/queries/hooks";
-import { TableStyleProps } from "components/tables/BaseTable";
-import { useCustomSelection } from "components/tables/plugins";
-import { uniqBy } from "lodash";
-import React, { useCallback, useEffect, useMemo, useState } from "react";
-import { Container, Dropdown, Row } from "react-bootstrap";
-import { Helmet } from "react-helmet";
-import { UseMutationResult, UseQueryResult } from "react-query";
-import { Column, TableOptions, TableUpdater, useRowSelect } from "react-table";
-import { GetItemId } from "utilities";
-import {
- ContentHeader,
- ItemEditorModal,
- LoadingIndicator,
- QueryPageTable,
- SimpleTable,
- useShowModal,
-} from "..";
+import { UsePaginationQueryResult } from "@/apis/queries/hooks";
+import { TableStyleProps } from "@/components/tables/BaseTable";
+import { faList } from "@fortawesome/free-solid-svg-icons";
+import { Row } from "react-bootstrap";
+import { useNavigate } from "react-router-dom";
+import { Column, TableOptions } from "react-table";
+import { ContentHeader, QueryPageTable } from "..";
interface Props<T extends Item.Base = Item.Base> {
- name: string;
- fullQuery: UseQueryResult<T[]>;
query: UsePaginationQueryResult<T>;
columns: Column<T>[];
- mutation: UseMutationResult<void, unknown, FormType.ModifyItem>;
}
-function ItemView<T extends Item.Base>({
- name,
- fullQuery,
- query,
- columns,
- mutation,
-}: Props<T>) {
- const [editMode, setEditMode] = useState(false);
-
- const showModal = useShowModal();
-
- const updateRow = useCallback<TableUpdater<T>>(
- ({ original }, modalKey: string) => {
- showModal(modalKey, original);
- },
- [showModal]
- );
+function ItemView<T extends Item.Base>({ query, columns }: Props<T>) {
+ const navigate = useNavigate();
const options: Partial<TableOptions<T> & TableStyleProps<T>> = {
- emptyText: `No ${name} Found`,
- update: updateRow,
+ emptyText: `No Items Found`,
};
- const content = editMode ? (
- <ItemMassEditor
- query={fullQuery}
- columns={columns}
- mutation={mutation}
- onEnded={() => setEditMode(false)}
- ></ItemMassEditor>
- ) : (
+ return (
<>
<ContentHeader scroll={false}>
<ContentHeader.Button
disabled={query.paginationStatus.totalCount === 0}
icon={faList}
- onClick={() => setEditMode(true)}
+ onClick={() => navigate("edit")}
>
Mass Edit
</ContentHeader.Button>
@@ -75,134 +36,6 @@ function ItemView<T extends Item.Base>({
query={query}
data={[]}
></QueryPageTable>
- <ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal>
- </Row>
- </>
- );
-
- return (
- <Container fluid>
- <Helmet>
- <title>{name} - Bazarr</title>
- </Helmet>
- {content}
- </Container>
- );
-}
-
-interface ItemMassEditorProps<T extends Item.Base> {
- columns: Column<T>[];
- query: UseQueryResult<T[]>;
- mutation: UseMutationResult<void, unknown, FormType.ModifyItem>;
- onEnded: () => void;
-}
-
-function ItemMassEditor<T extends Item.Base = Item.Base>(
- props: ItemMassEditorProps<T>
-) {
- const { columns, mutation, query, onEnded } = props;
- const [selections, setSelections] = useState<T[]>([]);
- const [dirties, setDirties] = useState<T[]>([]);
- const hasTask = useIsAnyMutationRunning();
- const { data: profiles } = useLanguageProfiles();
-
- const { refetch } = query;
-
- useEffect(() => {
- refetch();
- }, [refetch]);
-
- const data = useMemo(
- () => uniqBy([...dirties, ...(query?.data ?? [])], GetItemId),
- [dirties, query?.data]
- );
-
- const profileOptions = useMemo<JSX.Element[]>(() => {
- const items: JSX.Element[] = [];
- if (profiles) {
- items.push(
- <Dropdown.Item key="clear-profile">Clear Profile</Dropdown.Item>
- );
- items.push(<Dropdown.Divider key="dropdown-divider"></Dropdown.Divider>);
- items.push(
- ...profiles.map((v) => (
- <Dropdown.Item key={v.profileId} eventKey={v.profileId.toString()}>
- {v.name}
- </Dropdown.Item>
- ))
- );
- }
-
- return items;
- }, [profiles]);
-
- const { mutateAsync } = mutation;
-
- const save = useCallback(() => {
- const form: FormType.ModifyItem = {
- id: [],
- profileid: [],
- };
- dirties.forEach((v) => {
- const id = GetItemId(v);
- if (id) {
- form.id.push(id);
- form.profileid.push(v.profileId);
- }
- });
- return mutateAsync(form);
- }, [dirties, mutateAsync]);
-
- const setProfiles = useCallback(
- (key: Nullable<string>) => {
- const id = key ? parseInt(key) : null;
-
- const newItems = selections.map((v) => ({ ...v, profileId: id }));
-
- setDirties((dirty) => {
- return uniqBy([...newItems, ...dirty], GetItemId);
- });
- },
- [selections]
- );
-
- return (
- <>
- <ContentHeader scroll={false}>
- <ContentHeader.Group pos="start">
- <Dropdown onSelect={setProfiles}>
- <Dropdown.Toggle disabled={selections.length === 0} variant="light">
- Change Profile
- </Dropdown.Toggle>
- <Dropdown.Menu>{profileOptions}</Dropdown.Menu>
- </Dropdown>
- </ContentHeader.Group>
- <ContentHeader.Group pos="end">
- <ContentHeader.Button icon={faUndo} onClick={onEnded}>
- Cancel
- </ContentHeader.Button>
- <ContentHeader.AsyncButton
- icon={faCheck}
- disabled={dirties.length === 0 || hasTask}
- promise={save}
- onSuccess={onEnded}
- >
- Save
- </ContentHeader.AsyncButton>
- </ContentHeader.Group>
- </ContentHeader>
- <Row>
- {query.data === undefined ? (
- <LoadingIndicator></LoadingIndicator>
- ) : (
- <SimpleTable
- columns={columns}
- data={data}
- onSelect={setSelections}
- isSelecting
- plugins={[useRowSelect, useCustomSelection]}
- ></SimpleTable>
- )}
</Row>
</>
);
diff --git a/frontend/src/components/views/WantedView.tsx b/frontend/src/components/views/WantedView.tsx
index ef0895066..93da3302a 100644
--- a/frontend/src/components/views/WantedView.tsx
+++ b/frontend/src/components/views/WantedView.tsx
@@ -1,9 +1,7 @@
+import { useIsAnyActionRunning } from "@/apis/hooks";
+import { UsePaginationQueryResult } from "@/apis/queries/hooks";
+import { createAndDispatchTask } from "@/modules/task/utilities";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
-import { dispatchTask } from "@modules/task";
-import { createTask } from "@modules/task/utilities";
-import { useIsAnyActionRunning } from "apis/hooks";
-import { UsePaginationQueryResult } from "apis/queries/hooks";
-import React from "react";
import { Container, Row } from "react-bootstrap";
import { Helmet } from "react-helmet";
import { Column } from "react-table";
@@ -16,8 +14,6 @@ interface Props<T extends Wanted.Base> {
searchAll: () => Promise<void>;
}
-const TaskGroupName = "Searching wanted subtitles...";
-
function WantedView<T extends Wanted.Base>({
name,
columns,
@@ -37,8 +33,7 @@ function WantedView<T extends Wanted.Base>({
<ContentHeader.Button
disabled={hasTask || dataCount === 0}
onClick={() => {
- const task = createTask(name, undefined, searchAll);
- dispatchTask(TaskGroupName, [task], "Searching...");
+ createAndDispatchTask(name, "search-subtitles", searchAll);
}}
icon={faSearch}
>