diff options
Diffstat (limited to 'frontend/src/pages')
67 files changed, 793 insertions, 707 deletions
diff --git a/frontend/src/pages/404.tsx b/frontend/src/pages/404.tsx index 2d0033523..12fe005c7 100644 --- a/frontend/src/pages/404.tsx +++ b/frontend/src/pages/404.tsx @@ -1,11 +1,9 @@ import { faEyeSlash as fasEyeSlash } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Container } from "react-bootstrap"; -export const RouterEmptyPath = "/empty-page"; - -const EmptyPage: FunctionComponent = () => { +const NotFound: FunctionComponent = () => { return ( <Container className="d-flex flex-column align-items-center my-5"> <h1> @@ -17,4 +15,4 @@ const EmptyPage: FunctionComponent = () => { ); }; -export default EmptyPage; +export default NotFound; diff --git a/frontend/src/pages/Authentication.scss b/frontend/src/pages/Authentication.scss deleted file mode 100644 index 26b2bb602..000000000 --- a/frontend/src/pages/Authentication.scss +++ /dev/null @@ -1,3 +0,0 @@ -.auth-card { - width: 24rem; -} diff --git a/frontend/src/pages/Authentication.tsx b/frontend/src/pages/Authentication.tsx index 4b2ff721b..085ff6cc1 100644 --- a/frontend/src/pages/Authentication.tsx +++ b/frontend/src/pages/Authentication.tsx @@ -1,23 +1,21 @@ -import { useReduxStore } from "@redux/hooks/base"; -import logo from "@static/logo128.png"; -import { useSystem } from "apis/hooks"; -import React, { FunctionComponent, useState } from "react"; +import { useSystem } from "@/apis/hooks"; +import { useReduxStore } from "@/modules/redux/hooks/base"; +import { FunctionComponent, useState } from "react"; import { Button, Card, Form, Image, Spinner } from "react-bootstrap"; -import { Redirect } from "react-router-dom"; -import "./Authentication.scss"; +import { Navigate } from "react-router-dom"; -interface Props {} - -const Authentication: FunctionComponent<Props> = () => { +const Authentication: FunctionComponent = () => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const { login, isWorking } = useSystem(); - const authenticated = useReduxStore((s) => s.status !== "unauthenticated"); + const authenticated = useReduxStore( + (s) => s.site.status !== "unauthenticated" + ); if (authenticated) { - return <Redirect to="/"></Redirect>; + return <Navigate to="/"></Navigate>; } return ( @@ -31,7 +29,7 @@ const Authentication: FunctionComponent<Props> = () => { > <Card.Body> <Form.Group className="mb-5 d-flex justify-content-center"> - <Image width="64" height="64" src={logo}></Image> + <Image width="64" height="64" src="/static/logo128.png"></Image> </Form.Group> <Form.Group> <Form.Control diff --git a/frontend/src/pages/Blacklist/Movies/index.tsx b/frontend/src/pages/Blacklist/Movies/index.tsx index d2eb9b8cd..063dab308 100644 --- a/frontend/src/pages/Blacklist/Movies/index.tsx +++ b/frontend/src/pages/Blacklist/Movies/index.tsx @@ -1,14 +1,15 @@ +import { + useMovieBlacklist, + useMovieDeleteBlacklist, +} from "@/apis/hooks/movies"; +import { ContentHeader, QueryOverlay } from "@/components"; import { faTrash } from "@fortawesome/free-solid-svg-icons"; -import { useMovieBlacklist, useMovieDeleteBlacklist } from "apis/hooks/movies"; -import { ContentHeader, QueryOverlay } from "components"; -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; import Table from "./table"; -interface Props {} - -const BlacklistMoviesView: FunctionComponent<Props> = () => { +const BlacklistMoviesView: FunctionComponent = () => { const blacklist = useMovieBlacklist(); const { data } = blacklist; diff --git a/frontend/src/pages/Blacklist/Movies/table.tsx b/frontend/src/pages/Blacklist/Movies/table.tsx index d7d6df75f..d87740d7b 100644 --- a/frontend/src/pages/Blacklist/Movies/table.tsx +++ b/frontend/src/pages/Blacklist/Movies/table.tsx @@ -1,8 +1,9 @@ +import { useMovieDeleteBlacklist } from "@/apis/hooks"; +import { AsyncButton, PageTable, TextPopover } from "@/components"; +import Language from "@/components/bazarr/Language"; import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useMovieDeleteBlacklist } from "apis/hooks"; -import { AsyncButton, LanguageText, PageTable, TextPopover } from "components"; -import React, { FunctionComponent, useMemo } from "react"; +import { FunctionComponent, useMemo } from "react"; import { Link } from "react-router-dom"; import { Column } from "react-table"; @@ -31,7 +32,7 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => { 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/pages/Blacklist/Series/index.tsx b/frontend/src/pages/Blacklist/Series/index.tsx index 07870c747..b9441f808 100644 --- a/frontend/src/pages/Blacklist/Series/index.tsx +++ b/frontend/src/pages/Blacklist/Series/index.tsx @@ -1,14 +1,12 @@ +import { useEpisodeBlacklist, useEpisodeDeleteBlacklist } from "@/apis/hooks"; +import { ContentHeader, QueryOverlay } from "@/components"; import { faTrash } from "@fortawesome/free-solid-svg-icons"; -import { useEpisodeBlacklist, useEpisodeDeleteBlacklist } from "apis/hooks"; -import { ContentHeader, QueryOverlay } from "components"; -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; import Table from "./table"; -interface Props {} - -const BlacklistSeriesView: FunctionComponent<Props> = () => { +const BlacklistSeriesView: FunctionComponent = () => { const blacklist = useEpisodeBlacklist(); const { mutateAsync } = useEpisodeDeleteBlacklist(); diff --git a/frontend/src/pages/Blacklist/Series/table.tsx b/frontend/src/pages/Blacklist/Series/table.tsx index 0c522cb4f..43a86959b 100644 --- a/frontend/src/pages/Blacklist/Series/table.tsx +++ b/frontend/src/pages/Blacklist/Series/table.tsx @@ -1,8 +1,9 @@ +import { useEpisodeDeleteBlacklist } from "@/apis/hooks"; +import { AsyncButton, PageTable, TextPopover } from "@/components"; +import Language from "@/components/bazarr/Language"; import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useEpisodeDeleteBlacklist } from "apis/hooks"; -import { AsyncButton, LanguageText, PageTable, TextPopover } from "components"; -import React, { FunctionComponent, useMemo } from "react"; +import { FunctionComponent, useMemo } from "react"; import { Link } from "react-router-dom"; import { Column } from "react-table"; @@ -38,7 +39,7 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => { 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/pages/Episodes/components.tsx b/frontend/src/pages/Episodes/components.tsx index eb440fb82..7e6962699 100644 --- a/frontend/src/pages/Episodes/components.tsx +++ b/frontend/src/pages/Episodes/components.tsx @@ -1,8 +1,9 @@ +import { useEpisodeSubtitleModification } from "@/apis/hooks"; +import { AsyncButton } from "@/components"; +import Language from "@/components/bazarr/Language"; import { faSearch, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useEpisodeSubtitleModification } from "apis/hooks"; -import { AsyncButton, LanguageText } from "components"; -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Badge } from "react-bootstrap"; interface Props { @@ -52,7 +53,7 @@ export const SubtitleAction: FunctionComponent<Props> = ({ className="mr-1" variant={missing ? "primary" : "secondary"} > - <LanguageText className="pr-1" text={subtitle}></LanguageText> + <Language.Text className="pr-1" value={subtitle}></Language.Text> <FontAwesomeIcon size="sm" icon={missing ? faSearch : faTrash} @@ -62,7 +63,7 @@ export const SubtitleAction: FunctionComponent<Props> = ({ } else { return ( <Badge className="mr-1" variant="secondary"> - <LanguageText text={subtitle} long={false}></LanguageText> + <Language.Text value={subtitle} long={false}></Language.Text> </Badge> ); } diff --git a/frontend/src/pages/Episodes/index.tsx b/frontend/src/pages/Episodes/index.tsx index dacc4a47c..a4efa7c4f 100644 --- a/frontend/src/pages/Episodes/index.tsx +++ b/frontend/src/pages/Episodes/index.tsx @@ -1,46 +1,38 @@ import { - faAdjust, - faBriefcase, - faCloudUploadAlt, - faHdd, - faSearch, - faSync, - faWrench, -} from "@fortawesome/free-solid-svg-icons"; -import { dispatchTask } from "@modules/task"; -import { createTask } from "@modules/task/utilities"; -import { useEpisodesBySeriesId, useIsAnyActionRunning, useSeriesAction, useSeriesById, useSeriesModification, -} from "apis/hooks"; +} from "@/apis/hooks"; import { ContentHeader, ItemEditorModal, LoadingIndicator, SeriesUploadModal, - useShowModal, -} from "components"; -import ItemOverview from "components/ItemOverview"; -import { RouterEmptyPath } from "pages/404"; -import React, { FunctionComponent, useMemo } from "react"; +} from "@/components"; +import ItemOverview from "@/components/ItemOverview"; +import { useModalControl } from "@/modules/redux/hooks/modal"; +import { createAndDispatchTask } from "@/modules/task/utilities"; +import { useLanguageProfileBy } from "@/utilities/languages"; +import { + faAdjust, + faBriefcase, + faCloudUploadAlt, + faHdd, + faSearch, + faSync, + faWrench, +} from "@fortawesome/free-solid-svg-icons"; +import { FunctionComponent, useMemo } from "react"; import { Alert, Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; -import { Redirect, RouteComponentProps, withRouter } from "react-router-dom"; -import { useLanguageProfileBy } from "utilities/languages"; +import { Navigate, useParams } from "react-router-dom"; import Table from "./table"; -interface Params { - id: string; -} - -interface Props extends RouteComponentProps<Params> {} - -const SeriesEpisodesView: FunctionComponent<Props> = (props) => { - const { match } = props; - const id = Number.parseInt(match.params.id); +const SeriesEpisodesView: FunctionComponent = () => { + const params = useParams(); + const id = Number.parseInt(params.id as string); const { data: series, isFetched } = useSeriesById(id); const { data: episodes } = useEpisodesBySeriesId(id); @@ -63,14 +55,14 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => { [series] ); - const showModal = useShowModal(); + const { show } = useModalControl(); const profile = useLanguageProfileBy(series?.profileId); const hasTask = useIsAnyActionRunning(); if (isNaN(id) || (isFetched && !series)) { - return <Redirect to={RouterEmptyPath}></Redirect>; + return <Navigate to="/not-found"></Navigate>; } if (!series) { @@ -88,11 +80,10 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => { icon={faSync} disabled={!available || hasTask} onClick={() => { - const task = createTask(series.title, id, action, { + createAndDispatchTask(series.title, "scan-disk", action, { action: "scan-disk", seriesid: id, }); - dispatchTask("Scanning disk...", [task], "Scanning..."); }} > Scan Disk @@ -100,11 +91,10 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => { <ContentHeader.Button icon={faSearch} onClick={() => { - const task = createTask(series.title, id, action, { + createAndDispatchTask(series.title, "search-subtitles", action, { action: "search-missing", seriesid: id, }); - dispatchTask("Searching subtitles...", [task], "Searching..."); }} disabled={ series.episodeFileCount === 0 || @@ -119,7 +109,7 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => { <ContentHeader.Button disabled={series.episodeFileCount === 0 || !available || hasTask} icon={faBriefcase} - onClick={() => showModal("tools", episodes)} + onClick={() => show("tools", episodes)} > Tools </ContentHeader.Button> @@ -130,14 +120,14 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => { !available } icon={faCloudUploadAlt} - onClick={() => showModal("upload", series)} + onClick={() => show("upload", series)} > Upload </ContentHeader.Button> <ContentHeader.Button icon={faWrench} disabled={hasTask} - onClick={() => showModal("edit", series)} + onClick={() => show("edit", series)} > Edit Series </ContentHeader.Button> @@ -177,4 +167,4 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => { ); }; -export default withRouter(SeriesEpisodesView); +export default SeriesEpisodesView; diff --git a/frontend/src/pages/Episodes/table.tsx b/frontend/src/pages/Episodes/table.tsx index 7b5582e6f..6da19a7f2 100644 --- a/frontend/src/pages/Episodes/table.tsx +++ b/frontend/src/pages/Episodes/table.tsx @@ -1,3 +1,16 @@ +import { useDownloadEpisodeSubtitles } from "@/apis/hooks"; +import { + ActionButton, + EpisodeHistoryModal, + GroupTable, + SubtitleToolModal, + TextPopover, +} from "@/components"; +import { ManualSearchModal } from "@/components/modals/ManualSearchModal"; +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"; import { faBookmark, @@ -6,22 +19,9 @@ import { faUser, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useShowOnlyDesired } from "@redux/hooks"; -import { useDownloadEpisodeSubtitles } from "apis/hooks"; -import { - ActionButton, - EpisodeHistoryModal, - GroupTable, - SubtitleToolModal, - TextPopover, - useShowModal, -} from "components"; -import { ManualSearchModal } from "components/modals/ManualSearchModal"; -import React, { FunctionComponent, useCallback, useMemo } from "react"; +import { FunctionComponent, useCallback, useMemo } from "react"; import { Badge, ButtonGroup } from "react-bootstrap"; -import { Column, TableUpdater } from "react-table"; -import { BuildKey, filterSubtitleBy } from "utilities"; -import { useProfileItemsToLanguages } from "utilities/languages"; +import { Column } from "react-table"; import { SubtitleAction } from "./components"; interface Props { @@ -37,8 +37,6 @@ const Table: FunctionComponent<Props> = ({ profile, disabled, }) => { - const showModal = useShowModal(); - const onlyDesired = useShowOnlyDesired(); const profileItems = useProfileItemsToLanguages(profile); @@ -160,28 +158,29 @@ const Table: FunctionComponent<Props> = ({ { Header: "Actions", accessor: "sonarrEpisodeId", - Cell: ({ row, update }) => { + Cell: ({ row }) => { + const { show } = useModalControl(); return ( <ButtonGroup> <ActionButton icon={faUser} disabled={series?.profileId === null || disabled} onClick={() => { - update && update(row, "manual-search"); + show("manual-search", row.original); }} ></ActionButton> <ActionButton icon={faHistory} disabled={disabled} onClick={() => { - update && update(row, "history"); + show("manual-search", row.original); }} ></ActionButton> <ActionButton icon={faBriefcase} disabled={disabled} onClick={() => { - update && update(row, "tools"); + show("tools", [row.original]); }} ></ActionButton> </ButtonGroup> @@ -192,17 +191,6 @@ const Table: FunctionComponent<Props> = ({ [onlyDesired, profileItems, series, disabled] ); - const updateRow = useCallback<TableUpdater<Item.Episode>>( - (row, modalKey: string) => { - if (modalKey === "tools") { - showModal(modalKey, [row.original]); - } else { - showModal(modalKey, row.original); - } - }, - [showModal] - ); - const maxSeason = useMemo( () => episodes.reduce<number>((prev, curr) => Math.max(prev, curr.season), 0), @@ -210,11 +198,10 @@ const Table: FunctionComponent<Props> = ({ ); return ( - <React.Fragment> + <> <GroupTable columns={columns} data={episodes} - update={updateRow} initialState={{ sortBy: [ { id: "season", desc: true }, @@ -233,7 +220,7 @@ const Table: FunctionComponent<Props> = ({ modalKey="manual-search" download={download} ></ManualSearchModal> - </React.Fragment> + </> ); }; diff --git a/frontend/src/pages/History/Movies/index.tsx b/frontend/src/pages/History/Movies/index.tsx index de2ce0098..cbfaba78f 100644 --- a/frontend/src/pages/History/Movies/index.tsx +++ b/frontend/src/pages/History/Movies/index.tsx @@ -1,17 +1,16 @@ +import { useMovieAddBlacklist, useMovieHistoryPagination } from "@/apis/hooks"; +import { HistoryIcon, TextPopover } from "@/components"; +import Language from "@/components/bazarr/Language"; +import { BlacklistButton } from "@/components/inputs/blacklist"; +import HistoryView from "@/components/views/HistoryView"; import { faInfoCircle, faRecycle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useMovieAddBlacklist, useMovieHistoryPagination } from "apis/hooks"; -import { HistoryIcon, LanguageText, TextPopover } from "components"; -import { BlacklistButton } from "components/inputs/blacklist"; -import HistoryView from "components/views/HistoryView"; -import React, { FunctionComponent, useMemo } from "react"; +import { FunctionComponent, useMemo } from "react"; import { Badge, OverlayTrigger, Popover } from "react-bootstrap"; import { Link } from "react-router-dom"; import { Column } from "react-table"; -interface Props {} - -const MoviesHistoryView: FunctionComponent<Props> = () => { +const MoviesHistoryView: FunctionComponent = () => { const columns: Column<History.Movie>[] = useMemo<Column<History.Movie>[]>( () => [ { @@ -40,7 +39,7 @@ const MoviesHistoryView: FunctionComponent<Props> = () => { if (value) { return ( <Badge variant="secondary"> - <LanguageText text={value} long></LanguageText> + <Language.Text value={value} long></Language.Text> </Badge> ); } else { diff --git a/frontend/src/pages/History/Series/index.tsx b/frontend/src/pages/History/Series/index.tsx index 143f360b8..e64e798d7 100644 --- a/frontend/src/pages/History/Series/index.tsx +++ b/frontend/src/pages/History/Series/index.tsx @@ -1,20 +1,19 @@ -import { faInfoCircle, faRecycle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useEpisodeAddBlacklist, useEpisodeHistoryPagination, -} from "apis/hooks"; -import { HistoryIcon, LanguageText, TextPopover } from "components"; -import { BlacklistButton } from "components/inputs/blacklist"; -import HistoryView from "components/views/HistoryView"; -import React, { FunctionComponent, useMemo } from "react"; +} from "@/apis/hooks"; +import { HistoryIcon, TextPopover } from "@/components"; +import Language from "@/components/bazarr/Language"; +import { BlacklistButton } from "@/components/inputs/blacklist"; +import HistoryView from "@/components/views/HistoryView"; +import { faInfoCircle, faRecycle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { FunctionComponent, useMemo } from "react"; import { Badge, OverlayTrigger, Popover } from "react-bootstrap"; import { Link } from "react-router-dom"; import { Column } from "react-table"; -interface Props {} - -const SeriesHistoryView: FunctionComponent<Props> = () => { +const SeriesHistoryView: FunctionComponent = () => { const columns: Column<History.Episode>[] = useMemo<Column<History.Episode>[]>( () => [ { @@ -50,7 +49,7 @@ const SeriesHistoryView: FunctionComponent<Props> = () => { if (value) { return ( <Badge variant="secondary"> - <LanguageText text={value} long></LanguageText> + <Language.Text value={value} long></Language.Text> </Badge> ); } else { diff --git a/frontend/src/pages/History/Statistics/index.tsx b/frontend/src/pages/History/Statistics/index.tsx index e1875c2f1..aee2c0b54 100644 --- a/frontend/src/pages/History/Statistics/index.tsx +++ b/frontend/src/pages/History/Statistics/index.tsx @@ -1,12 +1,13 @@ -import { useHistoryStats, useLanguages, useSystemProviders } from "apis/hooks"; +import { useHistoryStats, useSystemProviders } from "@/apis/hooks"; import { ContentHeader, - LanguageSelector, QueryOverlay, Selector, -} from "components"; + SelectorOption, +} from "@/components"; +import Language from "@/components/bazarr/Language"; import { merge } from "lodash"; -import React, { FunctionComponent, useMemo, useState } from "react"; +import { FunctionComponent, useMemo, useState } from "react"; import { Col, Container } from "react-bootstrap"; import { Helmet } from "react-helmet"; import { @@ -28,8 +29,6 @@ const SelectorContainer: FunctionComponent = ({ children }) => ( ); const HistoryStats: FunctionComponent = () => { - const { data: languages } = useLanguages(true); - const { data: providers } = useSystemProviders(true); const providerOptions = useMemo<SelectorOption<System.Provider>[]>( @@ -63,13 +62,12 @@ const HistoryStats: FunctionComponent = () => { }, [data]); return ( - // TODO: Responsive <Container fluid className="vh-75"> <Helmet> <title>History Statistics - Bazarr</title> </Helmet> <QueryOverlay result={stats}> - <React.Fragment> + <div className="chart-container"> <ContentHeader scroll={false}> <SelectorContainer> <Selector @@ -98,12 +96,12 @@ const HistoryStats: FunctionComponent = () => { ></Selector> </SelectorContainer> <SelectorContainer> - <LanguageSelector + <Language.Selector clearable - options={languages ?? []} value={lang} onChange={setLanguage} - ></LanguageSelector> + history + ></Language.Selector> </SelectorContainer> </ContentHeader> <ResponsiveContainer height="100%"> @@ -117,7 +115,7 @@ const HistoryStats: FunctionComponent = () => { <Bar name="Movies" dataKey="movies" fill="#FFC22F"></Bar> </BarChart> </ResponsiveContainer> - </React.Fragment> + </div> </QueryOverlay> </Container> ); diff --git a/frontend/src/pages/History/Statistics/options.ts b/frontend/src/pages/History/Statistics/options.ts index 49d2bbe59..88e36e85e 100644 --- a/frontend/src/pages/History/Statistics/options.ts +++ b/frontend/src/pages/History/Statistics/options.ts @@ -1,3 +1,5 @@ +import { SelectorOption } from "@/components"; + export const actionOptions: SelectorOption<History.ActionOptions>[] = [ { label: "Automatically Downloaded", diff --git a/frontend/src/pages/LaunchError.tsx b/frontend/src/pages/LaunchError.tsx index 80633e926..910b01ccd 100644 --- a/frontend/src/pages/LaunchError.tsx +++ b/frontend/src/pages/LaunchError.tsx @@ -1,8 +1,8 @@ +import { Reload } from "@/utilities"; import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Alert, Button, Container } from "react-bootstrap"; -import { Reload } from "utilities"; interface Props { children: string; diff --git a/frontend/src/pages/Movies/Details/index.tsx b/frontend/src/pages/Movies/Details/index.tsx index 82a053111..842ceb025 100644 --- a/frontend/src/pages/Movies/Details/index.tsx +++ b/frontend/src/pages/Movies/Details/index.tsx @@ -1,20 +1,12 @@ import { - faCloudUploadAlt, - faHistory, - faSearch, - faSync, - faToolbox, - faUser, - faWrench, -} from "@fortawesome/free-solid-svg-icons"; -import { dispatchTask } from "@modules/task"; -import { createTask } from "@modules/task/utilities"; -import { useDownloadMovieSubtitles, useIsMovieActionRunning } from "apis/hooks"; + useDownloadMovieSubtitles, + useIsMovieActionRunning, +} from "@/apis/hooks"; import { useMovieAction, useMovieById, useMovieModification, -} from "apis/hooks/movies"; +} from "@/apis/hooks/movies"; import { ContentHeader, ItemEditorModal, @@ -22,31 +14,35 @@ import { MovieHistoryModal, MovieUploadModal, SubtitleToolModal, - useShowModal, -} from "components"; -import ItemOverview from "components/ItemOverview"; -import { ManualSearchModal } from "components/modals/ManualSearchModal"; -import { RouterEmptyPath } from "pages/404"; -import React, { FunctionComponent, useCallback } from "react"; +} from "@/components"; +import ItemOverview from "@/components/ItemOverview"; +import { ManualSearchModal } from "@/components/modals/ManualSearchModal"; +import { useModalControl } from "@/modules/redux/hooks/modal"; +import { createAndDispatchTask } from "@/modules/task/utilities"; +import { useLanguageProfileBy } from "@/utilities/languages"; +import { + faCloudUploadAlt, + faHistory, + faSearch, + faSync, + faToolbox, + faUser, + faWrench, +} from "@fortawesome/free-solid-svg-icons"; +import { FunctionComponent, useCallback } from "react"; import { Alert, Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; -import { Redirect, RouteComponentProps, withRouter } from "react-router-dom"; -import { useLanguageProfileBy } from "utilities/languages"; +import { Navigate, useParams } from "react-router-dom"; import Table from "./table"; -interface Params { - id: string; -} - -interface Props extends RouteComponentProps<Params> {} - -const MovieDetailView: FunctionComponent<Props> = ({ match }) => { - const id = Number.parseInt(match.params.id); +const MovieDetailView: FunctionComponent = () => { + const param = useParams(); + const id = Number.parseInt(param.id ?? ""); const { data: movie, isFetched } = useMovieById(id); const profile = useLanguageProfileBy(movie?.profileId); - const showModal = useShowModal(); + const { show } = useModalControl(); const mutation = useMovieModification(); const { mutateAsync: action } = useMovieAction(); @@ -82,7 +78,7 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => { const hasTask = useIsMovieActionRunning(); if (isNaN(id) || (isFetched && !movie)) { - return <Redirect to={RouterEmptyPath}></Redirect>; + return <Navigate to="/not-found"></Navigate>; } if (!movie) { @@ -102,11 +98,10 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => { icon={faSync} disabled={hasTask} onClick={() => { - const task = createTask(movie.title, id, action, { + createAndDispatchTask(movie.title, "scan-disk", action, { action: "scan-disk", radarrid: id, }); - dispatchTask("Scanning Disk...", [task], "Scanning..."); }} > Scan Disk @@ -115,11 +110,10 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => { icon={faSearch} disabled={movie.profileId === null} onClick={() => { - const task = createTask(movie.title, id, action, { + createAndDispatchTask(movie.title, "search-subtitles", action, { action: "search-missing", radarrid: id, }); - dispatchTask("Searching subtitles...", [task], "Searching..."); }} > Search @@ -127,20 +121,20 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => { <ContentHeader.Button icon={faUser} disabled={movie.profileId === null || hasTask} - onClick={() => showModal<Item.Movie>("manual-search", movie)} + onClick={() => show("manual-search", movie)} > Manual </ContentHeader.Button> <ContentHeader.Button icon={faHistory} - onClick={() => showModal("history", movie)} + onClick={() => show("history", movie)} > History </ContentHeader.Button> <ContentHeader.Button icon={faToolbox} disabled={hasTask} - onClick={() => showModal("tools", [movie])} + onClick={() => show("tools", [movie])} > Tools </ContentHeader.Button> @@ -150,14 +144,14 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => { <ContentHeader.Button disabled={!allowEdit || movie.profileId === null || hasTask} icon={faCloudUploadAlt} - onClick={() => showModal("upload", movie)} + onClick={() => show("upload", movie)} > Upload </ContentHeader.Button> <ContentHeader.Button icon={faWrench} disabled={hasTask} - onClick={() => showModal("edit", movie)} + onClick={() => show("edit", movie)} > Edit Movie </ContentHeader.Button> @@ -191,4 +185,4 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => { ); }; -export default withRouter(MovieDetailView); +export default MovieDetailView; diff --git a/frontend/src/pages/Movies/Details/table.tsx b/frontend/src/pages/Movies/Details/table.tsx index 6187731c1..c87651e77 100644 --- a/frontend/src/pages/Movies/Details/table.tsx +++ b/frontend/src/pages/Movies/Details/table.tsx @@ -1,13 +1,14 @@ +import { useMovieSubtitleModification } from "@/apis/hooks"; +import { AsyncButton, SimpleTable } from "@/components"; +import Language from "@/components/bazarr/Language"; +import { useShowOnlyDesired } from "@/modules/redux/hooks"; +import { filterSubtitleBy } from "@/utilities"; +import { useProfileItemsToLanguages } from "@/utilities/languages"; import { faSearch, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useShowOnlyDesired } from "@redux/hooks"; -import { useMovieSubtitleModification } from "apis/hooks"; -import { AsyncButton, LanguageText, SimpleTable } from "components"; -import React, { FunctionComponent, useMemo } from "react"; +import { FunctionComponent, useMemo } from "react"; import { Badge } from "react-bootstrap"; import { Column } from "react-table"; -import { filterSubtitleBy } from "utilities"; -import { useProfileItemsToLanguages } from "utilities/languages"; const missingText = "Missing Subtitles"; @@ -44,13 +45,13 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => { if (row.original.path === missingText) { return ( <Badge variant="primary"> - <LanguageText text={row.original} long></LanguageText> + <Language.Text value={row.original} long></Language.Text> </Badge> ); } else { return ( <Badge variant="secondary"> - <LanguageText text={row.original} long></LanguageText> + <Language.Text value={row.original} long></Language.Text> </Badge> ); } diff --git a/frontend/src/pages/Movies/Editor.tsx b/frontend/src/pages/Movies/Editor.tsx new file mode 100644 index 000000000..97b863a1f --- /dev/null +++ b/frontend/src/pages/Movies/Editor.tsx @@ -0,0 +1,64 @@ +import { useMovieModification, useMovies } from "@/apis/hooks"; +import { QueryOverlay } from "@/components"; +import LanguageProfile from "@/components/bazarr/LanguageProfile"; +import MassEditor from "@/components/MassEditor"; +import { BuildKey } from "@/utilities"; +import { FunctionComponent, useMemo } from "react"; +import { Badge } from "react-bootstrap"; +import { Helmet } from "react-helmet"; +import { Column } from "react-table"; + +const MovieMassEditor: FunctionComponent = () => { + const query = useMovies(); + const mutation = useMovieModification(); + + const columns = useMemo<Column<Item.Movie>[]>( + () => [ + { + Header: "Name", + accessor: "title", + className: "text-nowrap", + }, + { + Header: "Audio", + accessor: "audio_language", + Cell: (row) => { + return row.value.map((v) => ( + <Badge + variant="secondary" + className="mr-2" + key={BuildKey(v.code2, v.code2, v.hi)} + > + {v.name} + </Badge> + )); + }, + }, + { + Header: "Languages Profile", + accessor: "profileId", + Cell: ({ value }) => { + return <LanguageProfile index={value}></LanguageProfile>; + }, + }, + ], + [] + ); + + return ( + <QueryOverlay result={query}> + <> + <Helmet> + <title>Movies - Bazarr (Mass Editor)</title> + </Helmet> + <MassEditor + columns={columns} + data={query.data ?? []} + mutation={mutation} + ></MassEditor> + </> + </QueryOverlay> + ); +}; + +export default MovieMassEditor; diff --git a/frontend/src/pages/Movies/index.tsx b/frontend/src/pages/Movies/index.tsx index b142c21b5..2939ee2a0 100644 --- a/frontend/src/pages/Movies/index.tsx +++ b/frontend/src/pages/Movies/index.tsx @@ -1,34 +1,28 @@ +import { useMovieModification, useMoviesPagination } from "@/apis/hooks"; +import { ActionBadge, ItemEditorModal, TextPopover } from "@/components"; +import Language from "@/components/bazarr/Language"; +import LanguageProfile from "@/components/bazarr/LanguageProfile"; +import ItemView from "@/components/views/ItemView"; +import { useModalControl } from "@/modules/redux/hooks/modal"; +import { BuildKey } from "@/utilities"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - useLanguageProfiles, - useMovieModification, - useMovies, - useMoviesPagination, -} from "apis/hooks"; -import { ActionBadge, LanguageText, TextPopover } from "components"; -import ItemView from "components/views/ItemView"; -import React, { FunctionComponent, useMemo } from "react"; -import { Badge } from "react-bootstrap"; +import { FunctionComponent, useMemo } from "react"; +import { Badge, Container } from "react-bootstrap"; +import { Helmet } from "react-helmet"; import { Link } from "react-router-dom"; import { Column } from "react-table"; -import { BuildKey } from "utilities"; -interface Props {} - -const MovieView: FunctionComponent<Props> = () => { - const { data: profiles } = useLanguageProfiles(); +const MovieView: FunctionComponent = () => { const mutation = useMovieModification(); const query = useMoviesPagination(); - const full = useMovies(); const columns: Column<Item.Movie>[] = useMemo<Column<Item.Movie>[]>( () => [ { accessor: "monitored", - selectHide: true, Cell: ({ value }) => ( <FontAwesomeIcon title={value ? "monitored" : "unmonitored"} @@ -40,19 +34,15 @@ const MovieView: FunctionComponent<Props> = () => { Header: "Name", accessor: "title", className: "text-nowrap", - Cell: ({ row, value, isSelecting: select }) => { - if (select) { - return value; - } else { - const target = `/movies/${row.original.radarrId}`; - return ( - <TextPopover text={row.original.sceneName} delay={1}> - <Link to={target}> - <span>{value}</span> - </Link> - </TextPopover> - ); - } + Cell: ({ row, value }) => { + const target = `/movies/${row.original.radarrId}`; + return ( + <TextPopover text={row.original.sceneName} delay={1}> + <Link to={target}> + <span>{value}</span> + </Link> + </TextPopover> + ); }, }, { @@ -74,13 +64,12 @@ const MovieView: FunctionComponent<Props> = () => { Header: "Languages Profile", accessor: "profileId", Cell: ({ value }) => { - return profiles?.find((v) => v.profileId === value)?.name ?? null; + return <LanguageProfile index={value} empty=""></LanguageProfile>; }, }, { Header: "Missing Subtitles", accessor: "missing_subtitles", - selectHide: true, Cell: (row) => { const missing = row.value; return missing.map((v) => ( @@ -89,35 +78,35 @@ const MovieView: FunctionComponent<Props> = () => { variant="warning" key={BuildKey(v.code2, v.hi, v.forced)} > - <LanguageText text={v}></LanguageText> + <Language.Text value={v}></Language.Text> </Badge> )); }, }, { accessor: "radarrId", - selectHide: true, - Cell: ({ row, update }) => { + Cell: ({ row }) => { + const { show } = useModalControl(); return ( <ActionBadge icon={faWrench} - onClick={() => update && update(row, "edit")} + onClick={() => show("edit", row.original)} ></ActionBadge> ); }, }, ], - [profiles] + [] ); return ( - <ItemView - name="Movies" - fullQuery={full} - query={query} - columns={columns} - mutation={mutation} - ></ItemView> + <Container fluid> + <Helmet> + <title>Movies - Bazarr</title> + </Helmet> + <ItemView query={query} columns={columns}></ItemView> + <ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> + </Container> ); }; diff --git a/frontend/src/pages/Series/Editor.tsx b/frontend/src/pages/Series/Editor.tsx new file mode 100644 index 000000000..9cfc4e855 --- /dev/null +++ b/frontend/src/pages/Series/Editor.tsx @@ -0,0 +1,64 @@ +import { useSeries, useSeriesModification } from "@/apis/hooks"; +import { QueryOverlay } from "@/components"; +import LanguageProfile from "@/components/bazarr/LanguageProfile"; +import MassEditor from "@/components/MassEditor"; +import { BuildKey } from "@/utilities"; +import { FunctionComponent, useMemo } from "react"; +import { Badge } from "react-bootstrap"; +import { Helmet } from "react-helmet"; +import { Column } from "react-table"; + +const SeriesMassEditor: FunctionComponent = () => { + const query = useSeries(); + const mutation = useSeriesModification(); + + const columns = useMemo<Column<Item.Series>[]>( + () => [ + { + Header: "Name", + accessor: "title", + className: "text-nowrap", + }, + { + Header: "Audio", + accessor: "audio_language", + Cell: (row) => { + return row.value.map((v) => ( + <Badge + variant="secondary" + className="mr-2" + key={BuildKey(v.code2, v.forced, v.hi)} + > + {v.name} + </Badge> + )); + }, + }, + { + Header: "Languages Profile", + accessor: "profileId", + Cell: ({ value }) => { + return <LanguageProfile index={value}></LanguageProfile>; + }, + }, + ], + [] + ); + + return ( + <QueryOverlay result={query}> + <> + <Helmet> + <title>Series - Bazarr (Mass Editor)</title> + </Helmet> + <MassEditor + columns={columns} + data={query.data ?? []} + mutation={mutation} + ></MassEditor> + </> + </QueryOverlay> + ); +}; + +export default SeriesMassEditor; diff --git a/frontend/src/pages/Series/index.tsx b/frontend/src/pages/Series/index.tsx index bab2f1ba3..5f96f1d75 100644 --- a/frontend/src/pages/Series/index.tsx +++ b/frontend/src/pages/Series/index.tsx @@ -1,26 +1,20 @@ +import { useSeriesModification, useSeriesPagination } from "@/apis/hooks"; +import { ActionBadge, ItemEditorModal } from "@/components"; +import LanguageProfile from "@/components/bazarr/LanguageProfile"; +import ItemView from "@/components/views/ItemView"; +import { useModalControl } from "@/modules/redux/hooks/modal"; +import { BuildKey } from "@/utilities"; import { faWrench } from "@fortawesome/free-solid-svg-icons"; -import { - useLanguageProfiles, - useSeries, - useSeriesModification, - useSeriesPagination, -} from "apis/hooks"; -import { ActionBadge } from "components"; -import ItemView from "components/views/ItemView"; -import React, { FunctionComponent, useMemo } from "react"; -import { Badge, ProgressBar } from "react-bootstrap"; +import { FunctionComponent, useMemo } from "react"; +import { Badge, Container, ProgressBar } from "react-bootstrap"; +import { Helmet } from "react-helmet"; import { Link } from "react-router-dom"; import { Column } from "react-table"; -import { BuildKey } from "utilities"; -interface Props {} - -const SeriesView: FunctionComponent<Props> = () => { - const { data: profiles } = useLanguageProfiles(); +const SeriesView: FunctionComponent = () => { const mutation = useSeriesModification(); const query = useSeriesPagination(); - const full = useSeries(); const columns: Column<Item.Series>[] = useMemo<Column<Item.Series>[]>( () => [ @@ -28,17 +22,13 @@ const SeriesView: FunctionComponent<Props> = () => { Header: "Name", accessor: "title", className: "text-nowrap", - Cell: ({ row, value, isSelecting: select }) => { - if (select) { - return value; - } else { - const target = `/series/${row.original.sonarrSeriesId}`; - return ( - <Link to={target}> - <span>{value}</span> - </Link> - ); - } + Cell: ({ row, value }) => { + const target = `/series/${row.original.sonarrSeriesId}`; + return ( + <Link to={target}> + <span>{value}</span> + </Link> + ); }, }, { @@ -60,17 +50,15 @@ const SeriesView: FunctionComponent<Props> = () => { Header: "Languages Profile", accessor: "profileId", Cell: ({ value }) => { - return profiles?.find((v) => v.profileId === value)?.name ?? null; + return <LanguageProfile index={value} empty=""></LanguageProfile>; }, }, { Header: "Episodes", accessor: "episodeFileCount", - selectHide: true, - Cell: ({ row }) => { + Cell: (row) => { const { episodeFileCount, episodeMissingCount, profileId, title } = - row.original; - + row.row.original; let progress = 0; let label = ""; if (episodeFileCount === 0 || !profileId) { @@ -99,28 +87,28 @@ const SeriesView: FunctionComponent<Props> = () => { }, { accessor: "sonarrSeriesId", - selectHide: true, - Cell: ({ row, update }) => ( - <ActionBadge - icon={faWrench} - onClick={() => { - update && update(row, "edit"); - }} - ></ActionBadge> - ), + Cell: ({ row: { original } }) => { + const { show } = useModalControl(); + return ( + <ActionBadge + icon={faWrench} + onClick={() => show("edit", original)} + ></ActionBadge> + ); + }, }, ], - [profiles] + [] ); return ( - <ItemView - name="Series" - fullQuery={full} - query={query} - columns={columns} - mutation={mutation} - ></ItemView> + <Container fluid> + <Helmet> + <title>Series - Bazarr</title> + </Helmet> + <ItemView query={query} columns={columns}></ItemView> + <ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> + </Container> ); }; diff --git a/frontend/src/pages/Settings/General/index.tsx b/frontend/src/pages/Settings/General/index.tsx index 5e9b1b5dc..b5ec38dec 100644 --- a/frontend/src/pages/Settings/General/index.tsx +++ b/frontend/src/pages/Settings/General/index.tsx @@ -1,12 +1,12 @@ +import { copyToClipboard, Environment, toggleState } from "@/utilities"; import { faCheck, faClipboard, faSync, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { FunctionComponent, useState } from "react"; +import { FunctionComponent, useState } from "react"; import { InputGroup } from "react-bootstrap"; -import { copyToClipboard, Environment, toggleState } from "utilities"; import { Button, Check, @@ -15,9 +15,9 @@ import { File, Group, Input, + Layout, Message, Selector, - SettingsProvider, Text, } from "../components"; import { branchOptions, proxyOptions, securityOptions } from "./options"; @@ -39,7 +39,7 @@ const SettingsGeneralView: FunctionComponent = () => { const [copied, setCopy] = useState(false); return ( - <SettingsProvider title="General - Bazarr (Settings)"> + <Layout name="General"> <Group header="Host"> <Input name="Address"> <Text placeholder="0.0.0.0" settingKey="settings-general-ip"></Text> @@ -209,7 +209,7 @@ const SettingsGeneralView: FunctionComponent = () => { </Message> </Input> </Group> - </SettingsProvider> + </Layout> ); }; diff --git a/frontend/src/pages/Settings/General/options.ts b/frontend/src/pages/Settings/General/options.ts index ee36eba53..77b1a63bc 100644 --- a/frontend/src/pages/Settings/General/options.ts +++ b/frontend/src/pages/Settings/General/options.ts @@ -1,3 +1,5 @@ +import { SelectorOption } from "@/components"; + export const securityOptions: SelectorOption<string>[] = [ { label: "Basic", diff --git a/frontend/src/pages/Settings/Languages/components.tsx b/frontend/src/pages/Settings/Languages/components.tsx index abfdba601..3fc262706 100644 --- a/frontend/src/pages/Settings/Languages/components.tsx +++ b/frontend/src/pages/Settings/Languages/components.tsx @@ -1,5 +1,8 @@ -import { LanguageSelector as CLanguageSelector } from "components"; -import React, { FunctionComponent, useMemo } from "react"; +import { + LanguageSelector as CLanguageSelector, + SelectorOption, +} from "@/components"; +import { FunctionComponent, useMemo } from "react"; import { useLatestEnabledLanguages, useLatestProfiles } from "."; import { BaseInput, Selector, useSingleUpdate } from "../components"; @@ -25,27 +28,24 @@ export const LanguageSelector: FunctionComponent< ); }; -interface ProfileSelectorProps {} +export const ProfileSelector: FunctionComponent<BaseInput<Language.Profile>> = + ({ settingKey }) => { + const profiles = useLatestProfiles(); -export const ProfileSelector: FunctionComponent< - ProfileSelectorProps & BaseInput<Language.Profile> -> = ({ settingKey }) => { - const profiles = useLatestProfiles(); + const profileOptions = useMemo<SelectorOption<number>[]>( + () => + profiles.map((v) => { + return { label: v.name, value: v.profileId }; + }), + [profiles] + ); - const profileOptions = useMemo<SelectorOption<number>[]>( - () => - profiles.map((v) => { - return { label: v.name, value: v.profileId }; - }), - [profiles] - ); - - return ( - <Selector - clearable - options={profileOptions} - settingKey={settingKey} - beforeStaged={(v) => (v === null ? "" : v)} - ></Selector> - ); -}; + return ( + <Selector + clearable + options={profileOptions} + settingKey={settingKey} + beforeStaged={(v) => (v === null ? "" : v)} + ></Selector> + ); + }; diff --git a/frontend/src/pages/Settings/Languages/index.tsx b/frontend/src/pages/Settings/Languages/index.tsx index fd0473226..25b2b773d 100644 --- a/frontend/src/pages/Settings/Languages/index.tsx +++ b/frontend/src/pages/Settings/Languages/index.tsx @@ -1,14 +1,14 @@ -import { useLanguageProfiles, useLanguages } from "apis/hooks"; +import { useLanguageProfiles, useLanguages } from "@/apis/hooks"; +import { useEnabledLanguages } from "@/utilities/languages"; import { isArray } from "lodash"; -import React, { FunctionComponent } from "react"; -import { useEnabledLanguages } from "utilities/languages"; +import { FunctionComponent } from "react"; import { Check, CollapseBox, Group, Input, + Layout, Message, - SettingsProvider, useLatest, } from "../components"; import { enabledLanguageKey, languageProfileKey } from "../keys"; @@ -37,13 +37,10 @@ export function useLatestProfiles() { } } -interface Props {} - -const SettingsLanguagesView: FunctionComponent<Props> = () => { +const SettingsLanguagesView: FunctionComponent = () => { const { data: languages } = useLanguages(); - return ( - <SettingsProvider title="Languages - Bazarr (Settings)"> + <Layout name="Languages"> <Group header="Subtitles Language"> <Input> <Check @@ -108,7 +105,7 @@ const SettingsLanguagesView: FunctionComponent<Props> = () => { </CollapseBox.Content> </CollapseBox> </Group> - </SettingsProvider> + </Layout> ); }; diff --git a/frontend/src/pages/Settings/Languages/modal.tsx b/frontend/src/pages/Settings/Languages/modal.tsx index 61935b251..86c319de9 100644 --- a/frontend/src/pages/Settings/Languages/modal.tsx +++ b/frontend/src/pages/Settings/Languages/modal.tsx @@ -1,4 +1,3 @@ -import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { ActionButton, BaseModal, @@ -6,22 +5,38 @@ import { Chips, LanguageSelector, Selector, + SelectorOption, SimpleTable, - useModalInformation, -} from "components"; -import React, { +} from "@/components"; +import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; +import { BuildKey } from "@/utilities"; +import { LOG } from "@/utilities/console"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; +import { + createContext, FunctionComponent, useCallback, + useContext, useEffect, useMemo, useState, } from "react"; import { Button, Form } from "react-bootstrap"; -import { Column, TableUpdater } from "react-table"; -import { BuildKey } from "utilities"; +import { Column } from "react-table"; import { useLatestEnabledLanguages } from "."; import { Input, Message } from "../components"; import { cutoffOptions } from "./options"; + +type ModifyFn = (index: number, item?: Language.ProfileItem) => void; + +const RowContext = createContext<ModifyFn>(() => { + LOG("error", "RowContext not initialized"); +}); + +function useRowMutation() { + return useContext(RowContext); +} + interface Props { update: (profile: Language.Profile) => void; } @@ -43,8 +58,9 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( ) => { const { update, ...modal } = props; - const { payload: profile, closeModal } = - useModalInformation<Language.Profile>(modal.modalKey); + const profile = usePayload<Language.Profile>(modal.modalKey); + + const { hide } = useModalControl(); const languages = useLatestEnabledLanguages(); @@ -80,13 +96,13 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( [current] ); - const updateRow = useCallback<TableUpdater<Language.ProfileItem>>( - (row, item: Language.ProfileItem) => { + const mutateRow = useCallback( + (index: number, item?: Language.ProfileItem) => { const list = [...current.items]; if (item) { - list[row.index] = item; + list[index] = item; } else { - list.splice(row.index, 1); + list.splice(index, 1); } updateProfile("items", list); }, @@ -122,7 +138,7 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( <Button disabled={!canSave} onClick={() => { - closeModal(); + hide(); update(current); }} > @@ -139,13 +155,14 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( { Header: "Language", accessor: "language", - Cell: ({ value, row, update }) => { + Cell: ({ value, row }) => { const code = value; const item = row.original; const lang = useMemo( () => languages.find((l) => l.code2 === code) ?? null, [code] ); + const mutate = useRowMutation(); return ( <div style={{ width: "8rem" }}> <LanguageSelector @@ -154,7 +171,7 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( onChange={(l) => { if (l) { item.language = l.code2; - update && update(row, item); + mutate(row.index, item); } }} ></LanguageSelector> @@ -165,8 +182,9 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( { Header: "Forced", accessor: "forced", - Cell: ({ row, value, update }) => { + Cell: ({ row, value }) => { const item = row.original; + const mutate = useRowMutation(); return ( <Form.Check custom @@ -174,7 +192,7 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( checked={value === "True"} onChange={(v) => { item.forced = v.target.checked ? "True" : "False"; - update && update(row, item); + mutate(row.index, item); }} ></Form.Check> ); @@ -183,8 +201,9 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( { Header: "HI", accessor: "hi", - Cell: ({ row, value, update }) => { + Cell: ({ row, value }) => { const item = row.original; + const mutate = useRowMutation(); return ( <Form.Check custom @@ -192,7 +211,7 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( checked={value === "True"} onChange={(v) => { item.hi = v.target.checked ? "True" : "False"; - update && update(row, item); + mutate(row.index, item); }} ></Form.Check> ); @@ -201,8 +220,9 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( { Header: "Exclude Audio", accessor: "audio_exclude", - Cell: ({ row, value, update }) => { + Cell: ({ row, value }) => { const item = row.original; + const mutate = useRowMutation(); return ( <Form.Check custom @@ -210,7 +230,7 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( checked={value === "True"} onChange={(v) => { item.audio_exclude = v.target.checked ? "True" : "False"; - update && update(row, item); + mutate(row.index, item); }} ></Form.Check> ); @@ -219,11 +239,12 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( { id: "action", accessor: "id", - Cell: ({ row, update }) => { + Cell: ({ row }) => { + const mutate = useRowMutation(); return ( <ActionButton icon={faTrash} - onClick={() => update && update(row)} + onClick={() => mutate(row.index)} ></ActionButton> ); }, @@ -245,12 +266,13 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( ></Form.Control> </Input> <Input> - <SimpleTable - responsive={false} - columns={columns} - data={current.items} - update={updateRow} - ></SimpleTable> + <RowContext.Provider value={mutateRow}> + <SimpleTable + responsive={false} + columns={columns} + data={current.items} + ></SimpleTable> + </RowContext.Provider> <Button block variant="light" onClick={addItem}> Add </Button> diff --git a/frontend/src/pages/Settings/Languages/options.ts b/frontend/src/pages/Settings/Languages/options.ts index 01e5da382..fc9021b79 100644 --- a/frontend/src/pages/Settings/Languages/options.ts +++ b/frontend/src/pages/Settings/Languages/options.ts @@ -1,3 +1,5 @@ +import { SelectorOption } from "@/components"; + export const anyCutoff = 65535; export const cutoffOptions: SelectorOption<number>[] = [ diff --git a/frontend/src/pages/Settings/Languages/table.tsx b/frontend/src/pages/Settings/Languages/table.tsx index 13a8ea0b0..ed87274da 100644 --- a/frontend/src/pages/Settings/Languages/table.tsx +++ b/frontend/src/pages/Settings/Languages/table.tsx @@ -1,15 +1,33 @@ +import { ActionButton, SimpleTable } from "@/components"; +import { useModalControl } from "@/modules/redux/hooks/modal"; +import { LOG } from "@/utilities/console"; import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons"; -import { ActionButton, SimpleTable, useShowModal } from "components"; import { cloneDeep } from "lodash"; -import React, { FunctionComponent, useCallback, useMemo } from "react"; +import { + createContext, + FunctionComponent, + useCallback, + useContext, + useMemo, +} from "react"; import { Badge, Button, ButtonGroup } from "react-bootstrap"; -import { Column, TableUpdater } from "react-table"; +import { Column } from "react-table"; import { useLatestEnabledLanguages, useLatestProfiles } from "."; import { useSingleUpdate } from "../components"; import { languageProfileKey } from "../keys"; import Modal from "./modal"; import { anyCutoff } from "./options"; +type ModifyFn = (index: number, item?: Language.Profile) => void; + +const RowContext = createContext<ModifyFn>(() => { + LOG("error", "RowContext not initialized"); +}); + +function useRowMutation() { + return useContext(RowContext); +} + const Table: FunctionComponent = () => { const profiles = useLatestProfiles(); @@ -24,7 +42,7 @@ const Table: FunctionComponent = () => { const update = useSingleUpdate(); - const showModal = useShowModal(); + const { show } = useModalControl(); const submitProfiles = useCallback( (list: Language.Profile[]) => { @@ -48,17 +66,17 @@ const Table: FunctionComponent = () => { [profiles, submitProfiles] ); - const updateRow = useCallback<TableUpdater<Language.Profile>>( - (row, item?: Language.Profile) => { + const mutateRow = useCallback<ModifyFn>( + (index, item) => { if (item) { - showModal("profile", cloneDeep(item)); + show("profile", cloneDeep(item)); } else { const list = [...profiles]; - list.splice(row.index, 1); + list.splice(index, 1); submitProfiles(list); } }, - [submitProfiles, showModal, profiles] + [show, profiles, submitProfiles] ); const columns = useMemo<Column<Language.Profile>[]>( @@ -122,20 +140,21 @@ const Table: FunctionComponent = () => { }, { accessor: "profileId", - Cell: ({ row, update }) => { + Cell: ({ row }) => { const profile = row.original; + const mutate = useRowMutation(); return ( <ButtonGroup> <ActionButton icon={faWrench} onClick={() => { - update && update(row, profile); + mutate(row.index, profile); }} ></ActionButton> <ActionButton icon={faTrash} - onClick={() => update && update(row)} + onClick={() => mutate(row.index)} ></ActionButton> </ButtonGroup> ); @@ -148,12 +167,10 @@ const Table: FunctionComponent = () => { const canAdd = languages.length !== 0; return ( - <React.Fragment> - <SimpleTable - columns={columns} - data={profiles} - update={updateRow} - ></SimpleTable> + <> + <RowContext.Provider value={mutateRow}> + <SimpleTable columns={columns} data={profiles}></SimpleTable> + </RowContext.Provider> <Button block disabled={!canAdd} @@ -168,13 +185,13 @@ const Table: FunctionComponent = () => { mustNotContain: [], originalFormat: false, }; - showModal("profile", profile); + show("profile", profile); }} > {canAdd ? "Add New Profile" : "No Enabled Languages"} </Button> <Modal update={updateProfile} modalKey="profile"></Modal> - </React.Fragment> + </> ); }; diff --git a/frontend/src/pages/Settings/Notifications/components.tsx b/frontend/src/pages/Settings/Notifications/components.tsx index fee5611ae..5b8ed2007 100644 --- a/frontend/src/pages/Settings/Notifications/components.tsx +++ b/frontend/src/pages/Settings/Notifications/components.tsx @@ -1,21 +1,15 @@ -import api from "apis/raw"; +import api from "@/apis/raw"; import { AsyncButton, BaseModal, BaseModalProps, Selector, - useModalInformation, - useOnModalShow, - useShowModal, -} from "components"; -import React, { - FunctionComponent, - useCallback, - useMemo, - useState, -} from "react"; + SelectorOption, +} from "@/components"; +import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; +import { BuildKey } from "@/utilities"; +import { FunctionComponent, useCallback, useMemo, useState } from "react"; import { Button, Col, Container, Form, Row } from "react-bootstrap"; -import { BuildKey } from "utilities"; import { ColCard, useLatestArray, useUpdateArray } from "../components"; import { notificationsKey } from "../keys"; @@ -43,17 +37,12 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({ "name" ); - const { payload, closeModal } = - useModalInformation<Settings.NotificationInfo>(modal.modalKey); + const payload = usePayload<Settings.NotificationInfo>(modal.modalKey); + const { hide } = useModalControl(); const [current, setCurrent] = useState<Nullable<Settings.NotificationInfo>>(payload); - useOnModalShow<Settings.NotificationInfo>( - (p) => setCurrent(p), - modal.modalKey - ); - const updateUrl = useCallback((url: string) => { setCurrent((current) => { if (current) { @@ -72,7 +61,7 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({ const footer = useMemo( () => ( - <React.Fragment> + <> <AsyncButton className="mr-auto" disabled={!canSave} @@ -94,7 +83,7 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({ if (current) { update({ ...current, enabled: false }); } - closeModal(); + hide(); }} > Remove @@ -105,14 +94,14 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({ if (current) { update({ ...current, enabled: true }); } - closeModal(); + hide(); }} > Save </Button> - </React.Fragment> + </> ), - [canSave, closeModal, current, update, payload] + [canSave, payload, current, hide, update] ); const getLabel = useCallback((v: Settings.NotificationInfo) => v.name, []); @@ -157,7 +146,7 @@ export const NotificationView: FunctionComponent = () => { (s) => s.notifications.providers ); - const showModal = useShowModal(); + const { show } = useModalControl(); const elements = useMemo(() => { return notifications @@ -166,16 +155,16 @@ export const NotificationView: FunctionComponent = () => { <ColCard key={BuildKey(idx, v.name)} header={v.name} - onClick={() => showModal("notifications", v)} + onClick={() => show("notifications", v)} ></ColCard> )); - }, [notifications, showModal]); + }, [notifications, show]); return ( <Container fluid> <Row> {elements}{" "} - <ColCard plus onClick={() => showModal("notifications")}></ColCard> + <ColCard plus onClick={() => show("notifications")}></ColCard> </Row> <NotificationModal selections={notifications ?? []} diff --git a/frontend/src/pages/Settings/Notifications/index.tsx b/frontend/src/pages/Settings/Notifications/index.tsx index 06585183f..325950805 100644 --- a/frontend/src/pages/Settings/Notifications/index.tsx +++ b/frontend/src/pages/Settings/Notifications/index.tsx @@ -1,11 +1,11 @@ -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Alert } from "react-bootstrap"; -import { Check, Group, Input, Message, SettingsProvider } from "../components"; +import { Check, Group, Input, Layout, Message } from "../components"; import { NotificationView } from "./components"; const SettingsNotificationsView: FunctionComponent = () => { return ( - <SettingsProvider title="Notifications - Bazarr (Settings)"> + <Layout name="Notifications"> <Alert variant="secondary"> Thanks to caronc for his work on{" "} <a @@ -42,7 +42,7 @@ const SettingsNotificationsView: FunctionComponent = () => { </Message> </Input> </Group> - </SettingsProvider> + </Layout> ); }; diff --git a/frontend/src/pages/Settings/Providers/components.tsx b/frontend/src/pages/Settings/Providers/components.tsx index 81e270610..4af98d95c 100644 --- a/frontend/src/pages/Settings/Providers/components.tsx +++ b/frontend/src/pages/Settings/Providers/components.tsx @@ -1,12 +1,13 @@ import { BaseModal, Selector, - useModalInformation, - useOnModalShow, - useShowModal, -} from "components"; + SelectorComponents, + SelectorOption, +} from "@/components"; +import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; +import { BuildKey, isReactText } from "@/utilities"; import { capitalize, isArray, isBoolean } from "lodash"; -import React, { +import { FunctionComponent, useCallback, useEffect, @@ -15,8 +16,6 @@ import React, { } from "react"; import { Button, Col, Container, Row } from "react-bootstrap"; import { components } from "react-select"; -import { SelectComponents } from "react-select/dist/declarations/src/components"; -import { BuildKey, isReactText } from "utilities"; import { Check, ColCard, @@ -34,13 +33,13 @@ const ProviderKey = "settings-general-enabled_providers"; export const ProviderView: FunctionComponent = () => { const providers = useLatest<string[]>(ProviderKey, isArray); - const showModal = useShowModal(); + const { show } = useModalControl(); const select = useCallback( (v?: ProviderInfo) => { - showModal(ModalKey, v ?? null); + show(ModalKey, v ?? null); }, - [showModal] + [show] ); const cards = useMemo(() => { @@ -78,7 +77,8 @@ export const ProviderView: FunctionComponent = () => { }; export const ProviderModal: FunctionComponent = () => { - const { payload, closeModal } = useModalInformation<ProviderInfo>(ModalKey); + const payload = usePayload<ProviderInfo>(ModalKey); + const { hide } = useModalControl(); const [staged, setChange] = useState<LooseObject>({}); @@ -88,8 +88,6 @@ export const ProviderModal: FunctionComponent = () => { const [info, setInfo] = useState<Nullable<ProviderInfo>>(payload); - useOnModalShow<ProviderInfo>((p) => setInfo(p), ModalKey); - const providers = useLatest<string[]>(ProviderKey, isArray); const updateGlobal = useMultiUpdate(); @@ -101,10 +99,10 @@ export const ProviderModal: FunctionComponent = () => { const newProviders = [...providers]; newProviders.splice(idx, 1); updateGlobal({ [ProviderKey]: newProviders }); - closeModal(); + hide(); } } - }, [payload, providers, updateGlobal, closeModal]); + }, [payload, providers, updateGlobal, hide]); const addProvider = useCallback(() => { if (info && providers) { @@ -117,22 +115,22 @@ export const ProviderModal: FunctionComponent = () => { } updateGlobal(changes); - closeModal(); + hide(); } - }, [info, providers, staged, closeModal, updateGlobal]); + }, [info, providers, staged, updateGlobal, hide]); const canSave = info !== null; const footer = useMemo( () => ( - <React.Fragment> + <> <Button hidden={!payload} variant="danger" onClick={deletePayload}> Delete </Button> <Button disabled={!canSave} onClick={addProvider}> Save </Button> - </React.Fragment> + </> ), [canSave, payload, deletePayload, addProvider] ); @@ -218,12 +216,11 @@ export const ProviderModal: FunctionComponent = () => { }, [info]); const selectorComponents = useMemo< - Partial<SelectComponents<ProviderInfo, false, any>> + Partial<SelectorComponents<ProviderInfo, false>> >( () => ({ Option: ({ data, ...other }) => { - const { label, value } = - data as unknown as SelectorOption<ProviderInfo>; + const { label, value } = data; return ( <components.Option data={data} {...other}> {label} diff --git a/frontend/src/pages/Settings/Providers/index.tsx b/frontend/src/pages/Settings/Providers/index.tsx index a7b9ef171..7ea651f6f 100644 --- a/frontend/src/pages/Settings/Providers/index.tsx +++ b/frontend/src/pages/Settings/Providers/index.tsx @@ -1,17 +1,17 @@ -import React, { FunctionComponent } from "react"; -import { Group, Input, SettingsProvider } from "../components"; +import { FunctionComponent } from "react"; +import { Group, Input, Layout } from "../components"; import { ProviderModal, ProviderView } from "./components"; const SettingsProvidersView: FunctionComponent = () => { return ( - <SettingsProvider title="Providers - Bazarr (Settings)"> + <Layout name="Providers"> <Group header="Providers"> <Input> <ProviderView></ProviderView> </Input> </Group> <ProviderModal></ProviderModal> - </SettingsProvider> + </Layout> ); }; diff --git a/frontend/src/pages/Settings/Radarr/index.tsx b/frontend/src/pages/Settings/Radarr/index.tsx index 4511134f7..7d0822a23 100644 --- a/frontend/src/pages/Settings/Radarr/index.tsx +++ b/frontend/src/pages/Settings/Radarr/index.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useCallback } from "react"; +import { FunctionComponent, useCallback } from "react"; import { InputGroup } from "react-bootstrap"; import { Check, @@ -6,24 +6,22 @@ import { CollapseBox, Group, Input, + Layout, Message, PathMappingTable, - SettingsProvider, Slider, Text, URLTestButton, } from "../components"; import { moviesEnabledKey } from "../keys"; -interface Props {} - -const SettingsRadarrView: FunctionComponent<Props> = () => { +const SettingsRadarrView: FunctionComponent = () => { const baseUrlOverride = useCallback((settings: Settings) => { return settings.radarr.base_url?.slice(1) ?? ""; }, []); return ( - <SettingsProvider title="Radarr - Bazarr (Settings)"> + <Layout name="Radarr"> <CollapseBox> <CollapseBox.Control> <Group header="Use Radarr"> @@ -93,7 +91,7 @@ const SettingsRadarrView: FunctionComponent<Props> = () => { </Group> </CollapseBox.Content> </CollapseBox> - </SettingsProvider> + </Layout> ); }; diff --git a/frontend/src/pages/Settings/Scheduler/index.tsx b/frontend/src/pages/Settings/Scheduler/index.tsx index 4c1936396..2455dacaf 100644 --- a/frontend/src/pages/Settings/Scheduler/index.tsx +++ b/frontend/src/pages/Settings/Scheduler/index.tsx @@ -1,12 +1,13 @@ -import React, { FunctionComponent, useMemo } from "react"; +import { SelectorOption } from "@/components"; +import { FunctionComponent, useMemo } from "react"; import { Check, CollapseBox, Group, Input, + Layout, Message, Selector, - SettingsProvider, } from "../components"; import { backupOptions, @@ -29,7 +30,7 @@ const SettingsSchedulerView: FunctionComponent = () => { }, []); return ( - <SettingsProvider title="Scheduler - Bazarr (Settings)"> + <Layout name="Scheduler"> <Group header="Sonarr/Radarr Sync"> <Input name="Update Series List from Sonarr"> <Selector @@ -172,7 +173,7 @@ const SettingsSchedulerView: FunctionComponent = () => { </CollapseBox.Content> </CollapseBox> </Group> - </SettingsProvider> + </Layout> ); }; diff --git a/frontend/src/pages/Settings/Scheduler/options.ts b/frontend/src/pages/Settings/Scheduler/options.ts index aaf69cebb..78ba35378 100644 --- a/frontend/src/pages/Settings/Scheduler/options.ts +++ b/frontend/src/pages/Settings/Scheduler/options.ts @@ -1,3 +1,5 @@ +import { SelectorOption } from "@/components"; + export const seriesSyncOptions: SelectorOption<number>[] = [ { label: "15 Minutes", value: 15 }, { label: "1 Hour", value: 60 }, diff --git a/frontend/src/pages/Settings/Sonarr/index.tsx b/frontend/src/pages/Settings/Sonarr/index.tsx index 6dd97b0b6..5baf10b9b 100644 --- a/frontend/src/pages/Settings/Sonarr/index.tsx +++ b/frontend/src/pages/Settings/Sonarr/index.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useCallback } from "react"; +import { FunctionComponent, useCallback } from "react"; import { InputGroup } from "react-bootstrap"; import { Check, @@ -6,10 +6,10 @@ import { CollapseBox, Group, Input, + Layout, Message, PathMappingTable, Selector, - SettingsProvider, Slider, Text, URLTestButton, @@ -17,15 +17,13 @@ import { import { seriesEnabledKey } from "../keys"; import { seriesTypeOptions } from "../options"; -interface Props {} - -const SettingsSonarrView: FunctionComponent<Props> = () => { +const SettingsSonarrView: FunctionComponent = () => { const baseUrlOverride = useCallback((settings: Settings) => { return settings.sonarr.base_url?.slice(1) ?? ""; }, []); return ( - <SettingsProvider title="Sonarr - Bazarr (Settings)"> + <Layout name="Sonarr"> <CollapseBox> <CollapseBox.Control> <Group header="Use Sonarr"> @@ -116,7 +114,7 @@ const SettingsSonarrView: FunctionComponent<Props> = () => { </Group> </CollapseBox.Content> </CollapseBox> - </SettingsProvider> + </Layout> ); }; diff --git a/frontend/src/pages/Settings/Subtitles/index.tsx b/frontend/src/pages/Settings/Subtitles/index.tsx index aab391200..d0380130e 100644 --- a/frontend/src/pages/Settings/Subtitles/index.tsx +++ b/frontend/src/pages/Settings/Subtitles/index.tsx @@ -1,12 +1,12 @@ -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Check, CollapseBox, Group, Input, + Layout, Message, Selector, - SettingsProvider, Slider, Text, } from "../components"; @@ -33,7 +33,7 @@ const subzeroColorOverride = (settings: Settings) => { const SettingsSubtitlesView: FunctionComponent = () => { return ( - <SettingsProvider title="Subtitles - Bazarr (Settings)"> + <Layout name="Subtitles"> <Group header="Subtitles Options"> <CollapseBox> <CollapseBox.Control> @@ -514,7 +514,7 @@ const SettingsSubtitlesView: FunctionComponent = () => { </CollapseBox.Content> </CollapseBox> </Group> - </SettingsProvider> + </Layout> ); }; diff --git a/frontend/src/pages/Settings/Subtitles/options.ts b/frontend/src/pages/Settings/Subtitles/options.ts index fe6adaa2a..5549a4128 100644 --- a/frontend/src/pages/Settings/Subtitles/options.ts +++ b/frontend/src/pages/Settings/Subtitles/options.ts @@ -1,3 +1,5 @@ +import { SelectorOption } from "@/components"; + export const hiExtensionOptions: SelectorOption<string>[] = [ { label: ".hi (Hearing-Impaired)", diff --git a/frontend/src/pages/Settings/UI/index.tsx b/frontend/src/pages/Settings/UI/index.tsx index 74e30fada..fb5a58d49 100644 --- a/frontend/src/pages/Settings/UI/index.tsx +++ b/frontend/src/pages/Settings/UI/index.tsx @@ -1,12 +1,12 @@ -import React, { FunctionComponent } from "react"; -import { uiPageSizeKey, usePageSize } from "utilities/storage"; -import { Group, Input, Selector, SettingsProvider } from "../components"; +import { uiPageSizeKey, usePageSize } from "@/utilities/storage"; +import { FunctionComponent } from "react"; +import { Group, Input, Layout, Selector } from "../components"; import { pageSizeOptions } from "./options"; const SettingsUIView: FunctionComponent = () => { const [pageSize] = usePageSize(); return ( - <SettingsProvider title="Interface - Bazarr (Settings)"> + <Layout name="Interface"> <Group header="UI"> <Input name="Page Size"> <Selector @@ -16,7 +16,7 @@ const SettingsUIView: FunctionComponent = () => { ></Selector> </Input> </Group> - </SettingsProvider> + </Layout> ); }; diff --git a/frontend/src/pages/Settings/UI/options.ts b/frontend/src/pages/Settings/UI/options.ts index 9dd6f0d46..57c79c588 100644 --- a/frontend/src/pages/Settings/UI/options.ts +++ b/frontend/src/pages/Settings/UI/options.ts @@ -1,3 +1,5 @@ +import { SelectorOption } from "@/components"; + export const pageSizeOptions: SelectorOption<number>[] = [ { label: "25", diff --git a/frontend/src/pages/Settings/components/provider.tsx b/frontend/src/pages/Settings/components/Layout.tsx index 3f364f1e2..aeeb758cf 100644 --- a/frontend/src/pages/Settings/components/provider.tsx +++ b/frontend/src/pages/Settings/components/Layout.tsx @@ -1,8 +1,11 @@ +import { useSettingsMutation, useSystemSettings } from "@/apis/hooks"; +import { ContentHeader, LoadingIndicator } from "@/components"; +import { LOG } from "@/utilities/console"; +import { useUpdateLocalStorage } from "@/utilities/storage"; import { faSave } from "@fortawesome/free-solid-svg-icons"; -import { useSettingsMutation, useSystemSettings } from "apis/hooks"; -import { ContentHeader, LoadingIndicator } from "components"; import { merge } from "lodash"; -import React, { +import { + createContext, FunctionComponent, useCallback, useEffect, @@ -11,9 +14,6 @@ import React, { } from "react"; import { Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; -import { Prompt } from "react-router"; -import { log } from "utilities/logger"; -import { useUpdateLocalStorage } from "utilities/storage"; import { enabledLanguageKey, languageProfileKey, @@ -22,9 +22,14 @@ import { type SettingDispatcher = Record<string, (settings: LooseObject) => void>; -export const StagedChangesContext = React.createContext< - SimpleStateType<LooseObject> ->([{}, () => {}]); +export const StagedChangesContext = createContext<SimpleStateType<LooseObject>>( + [ + {}, + () => { + throw new Error("StagedChangesContext not initialized"); + }, + ] +); function submitHooks(settings: LooseObject) { if (languageProfileKey in settings) { @@ -44,12 +49,12 @@ function submitHooks(settings: LooseObject) { } interface Props { - title: string; + name: string; children: JSX.Element | JSX.Element[]; } -const SettingsProvider: FunctionComponent<Props> = (props) => { - const { children, title } = props; +const Layout: FunctionComponent<Props> = (props) => { + const { children, name } = props; const updateStorage = useUpdateLocalStorage(); @@ -68,7 +73,7 @@ const SettingsProvider: FunctionComponent<Props> = (props) => { const saveSettings = useCallback( (settings: LooseObject) => { submitHooks(settings); - log("info", "submitting settings", settings); + LOG("info", "submitting settings", settings); mutate(settings); }, [mutate] @@ -134,12 +139,13 @@ const SettingsProvider: FunctionComponent<Props> = (props) => { return ( <Container fluid> <Helmet> - <title>{title}</title> + <title>{name} - Bazarr (Settings)</title> </Helmet> - <Prompt + {/* TODO */} + {/* <Prompt when={Object.keys(stagedChange).length > 0} message="You have unsaved changes, are you sure you want to leave?" - ></Prompt> + ></Prompt> */} <ContentHeader> <ContentHeader.Button icon={faSave} @@ -159,4 +165,4 @@ const SettingsProvider: FunctionComponent<Props> = (props) => { ); }; -export default SettingsProvider; +export default Layout; diff --git a/frontend/src/pages/Settings/components/collapse.tsx b/frontend/src/pages/Settings/components/collapse.tsx index 0f9398481..888c8432e 100644 --- a/frontend/src/pages/Settings/components/collapse.tsx +++ b/frontend/src/pages/Settings/components/collapse.tsx @@ -1,4 +1,5 @@ -import React, { +import { + createContext, Dispatch, FunctionComponent, useContext, @@ -9,11 +10,12 @@ import { Collapse } from "react-bootstrap"; type SupportType = string | boolean; -const CollapseContext = React.createContext< - [SupportType, Dispatch<SupportType>] ->(["", (s) => {}]); -const CollapseUpdateContext = React.createContext<Dispatch<SupportType>>( - (s) => {} +const CollapseContext = createContext< + [SupportType, Dispatch<SupportType> | undefined] +>([false, undefined]); + +const CollapseUpdateContext = createContext<Dispatch<SupportType> | undefined>( + undefined ); export function useCollapse() { diff --git a/frontend/src/pages/Settings/components/container.tsx b/frontend/src/pages/Settings/components/container.tsx index 4808f12c0..bee95240e 100644 --- a/frontend/src/pages/Settings/components/container.tsx +++ b/frontend/src/pages/Settings/components/container.tsx @@ -1,8 +1,7 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Card as BSCard, Col, Form, Row } from "react-bootstrap"; -import "./style.scss"; interface GroupProps { header: string; diff --git a/frontend/src/pages/Settings/components/forms.tsx b/frontend/src/pages/Settings/components/forms.tsx index 32ac352e3..d2d089804 100644 --- a/frontend/src/pages/Settings/components/forms.tsx +++ b/frontend/src/pages/Settings/components/forms.tsx @@ -5,17 +5,18 @@ import { FileBrowserProps, Selector as CSelector, SelectorProps as CSelectorProps, + SelectorValueType, Slider as CSlider, SliderProps as CSliderProps, -} from "components"; +} from "@/components"; +import { isReactText } from "@/utilities"; import { isArray, isBoolean, isNull, isNumber, isString } from "lodash"; -import React, { FunctionComponent, useEffect } from "react"; +import { FunctionComponent, ReactText, useEffect } from "react"; import { Button as BSButton, ButtonProps as BSButtonProps, Form, } from "react-bootstrap"; -import { isReactText } from "utilities"; import { useCollapse, useLatest } from "."; import { OverrideFuncType, useSingleUpdate } from "./hooks"; @@ -32,11 +33,11 @@ export interface BaseInput<T> { disabled?: boolean; settingKey: string; override?: OverrideFuncType<T>; - beforeStaged?: (v: T) => any; + beforeStaged?: (v: T) => unknown; } -export interface TextProps extends BaseInput<React.ReactText> { - placeholder?: React.ReactText; +export interface TextProps extends BaseInput<ReactText> { + placeholder?: ReactText; password?: boolean; controlled?: boolean; numberWithArrows?: boolean; @@ -52,7 +53,7 @@ export const Text: FunctionComponent<TextProps> = ({ settingKey, numberWithArrows, }) => { - const value = useLatest<React.ReactText>(settingKey, isReactText, override); + const value = useLatest<ReactText>(settingKey, isReactText, override); const update = useSingleUpdate(); const collapse = useCollapse(); @@ -76,7 +77,7 @@ export const Text: FunctionComponent<TextProps> = ({ value={controlled ? value ?? undefined : undefined} onChange={(e) => { const val = e.currentTarget.value; - collapse(val.toString()); + collapse && collapse(val.toString()); const value = beforeStaged ? beforeStaged(val) : val; update(value, settingKey); }} @@ -101,7 +102,7 @@ export const Check: FunctionComponent<CheckProps> = ({ const value = useLatest<boolean>(settingKey, isBoolean, override); - useEffect(() => collapse(value ?? false), [collapse, value]); + useEffect(() => collapse && collapse(value ?? false), [collapse, value]); return ( <Form.Check @@ -120,7 +121,7 @@ export const Check: FunctionComponent<CheckProps> = ({ ); }; -function selectorValidator<T>(v: any): v is T { +function selectorValidator<T>(v: unknown): v is T { return isString(v) || isNumber(v) || isArray(v); } @@ -144,7 +145,7 @@ export function Selector< useEffect(() => { if (isString(value) || isNull(value)) { - collapse(value ?? ""); + collapse && collapse(value ?? ""); } }); @@ -153,14 +154,14 @@ export function Selector< {...selector} value={value as SelectorValueType<T, M>} onChange={(v) => { - v = beforeStaged ? beforeStaged(v) : v; - update(v, settingKey); + const result = beforeStaged ? beforeStaged(v) : v; + update(result, settingKey); }} ></CSelector> ); } -type SliderProps = {} & BaseInput<number> & +type SliderProps = BaseInput<number> & Omit<CSliderProps, "onChange" | "onAfterChange">; export const Slider: FunctionComponent<SliderProps> = (props) => { @@ -181,7 +182,7 @@ export const Slider: FunctionComponent<SliderProps> = (props) => { ); }; -type ChipsProp = {} & BaseInput<string[]> & +type ChipsProp = BaseInput<string[]> & Omit<CChipsProps, "onChange" | "defaultValue">; export const Chips: FunctionComponent<ChipsProp> = (props) => { @@ -204,7 +205,7 @@ export const Chips: FunctionComponent<ChipsProp> = (props) => { type ButtonProps = { onClick?: ( - update: (v: any, key: string) => void, + update: (v: unknown, key: string) => void, key: string, value?: string ) => void; @@ -228,7 +229,7 @@ export const Button: FunctionComponent<Override<ButtonProps, BSButtonProps>> = ( ); }; -type FileProps = {} & BaseInput<string>; +interface FileProps extends BaseInput<string> {} export const File: FunctionComponent<Override<FileProps, FileBrowserProps>> = ( props diff --git a/frontend/src/pages/Settings/components/hooks.ts b/frontend/src/pages/Settings/components/hooks.ts index 5f1441641..ef4919d9a 100644 --- a/frontend/src/pages/Settings/components/hooks.ts +++ b/frontend/src/pages/Settings/components/hooks.ts @@ -1,8 +1,8 @@ -import { useSystemSettings } from "apis/hooks"; +import { useSystemSettings } from "@/apis/hooks"; +import { LOG } from "@/utilities/console"; import { isArray, uniqBy } from "lodash"; import { useCallback, useContext, useMemo } from "react"; -import { log } from "utilities/logger"; -import { StagedChangesContext } from "./provider"; +import { StagedChangesContext } from "./Layout"; export function useStagedValues(): LooseObject { const [values] = useContext(StagedChangesContext); @@ -12,12 +12,12 @@ export function useStagedValues(): LooseObject { export function useSingleUpdate() { const [, update] = useContext(StagedChangesContext); return useCallback( - (v: any, key: string) => { + (v: unknown, key: string) => { update((staged) => { const changes = { ...staged }; changes[key] = v; - log("info", "stage settings", changes); + LOG("info", "stage settings", changes); return changes; }); @@ -33,7 +33,7 @@ export function useMultiUpdate() { update((staged) => { const changes = { ...staged, ...obj }; - log("info", "stage settings", changes); + LOG("info", "stage settings", changes); return changes; }); @@ -42,7 +42,7 @@ export function useMultiUpdate() { ); } -type ValidateFuncType<T> = (v: any) => v is T; +type ValidateFuncType<T> = (v: unknown) => v is T; export type OverrideFuncType<T> = (settings: Settings) => T; diff --git a/frontend/src/pages/Settings/components/index.tsx b/frontend/src/pages/Settings/components/index.tsx index fbec4221f..f420031ed 100644 --- a/frontend/src/pages/Settings/components/index.tsx +++ b/frontend/src/pages/Settings/components/index.tsx @@ -1,6 +1,6 @@ -import api from "apis/raw"; +import api from "@/apis/raw"; import { isBoolean, isNumber, isString } from "lodash"; -import React, { FunctionComponent, useCallback, useState } from "react"; +import { FunctionComponent, useCallback, useState } from "react"; import { Button } from "react-bootstrap"; import { useLatest } from "./hooks"; @@ -67,6 +67,6 @@ export { default as CollapseBox } from "./collapse"; export * from "./container"; export * from "./forms"; export * from "./hooks"; +export * from "./Layout"; +export { default as Layout } from "./Layout"; export * from "./pathMapper"; -export * from "./provider"; -export { default as SettingsProvider } from "./provider"; diff --git a/frontend/src/pages/Settings/components/pathMapper.tsx b/frontend/src/pages/Settings/components/pathMapper.tsx index 19564e7ce..56d07e74e 100644 --- a/frontend/src/pages/Settings/components/pathMapper.tsx +++ b/frontend/src/pages/Settings/components/pathMapper.tsx @@ -1,10 +1,17 @@ +import { ActionButton, FileBrowser, SimpleTable } from "@/components"; +import { LOG } from "@/utilities/console"; import { faArrowCircleRight, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { ActionButton, FileBrowser, SimpleTable } from "components"; import { capitalize, isArray, isBoolean } from "lodash"; -import React, { FunctionComponent, useCallback, useMemo } from "react"; +import { + createContext, + FunctionComponent, + useCallback, + useContext, + useMemo, +} from "react"; import { Button } from "react-bootstrap"; -import { Column, TableUpdater } from "react-table"; +import { Column } from "react-table"; import { moviesEnabledKey, pathMappingsKey, @@ -37,6 +44,16 @@ interface PathMappingItem { to: string; } +type ModifyFn = (index: number, item?: PathMappingItem) => void; + +const RowContext = createContext<ModifyFn>(() => { + LOG("error", "RowContext not initialized"); +}); + +function useRowMutation() { + return useContext(RowContext); +} + interface TableProps { type: SupportType; } @@ -72,13 +89,13 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => { [items] ); - const updateCell = useCallback<TableUpdater<PathMappingItem>>( - (row, item?: PathMappingItem) => { + const updateCell = useCallback<ModifyFn>( + (index, item) => { const newItems = [...data]; if (item) { - newItems[row.index] = item; + newItems[index] = item; } else { - newItems.splice(row.index, 1); + newItems.splice(index, 1); } updateRow(newItems); }, @@ -90,18 +107,22 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => { { Header: capitalize(type), accessor: "from", - Cell: ({ value, row, update }) => ( - <FileBrowser - drop="up" - type={type} - defaultValue={value} - onChange={(path) => { - const newItem = { ...row.original }; - newItem.from = path; - update && update(row, newItem); - }} - ></FileBrowser> - ), + Cell: ({ value, row }) => { + const mutate = useRowMutation(); + + return ( + <FileBrowser + drop="up" + type={type} + defaultValue={value} + onChange={(path) => { + const newItem = { ...row.original }; + newItem.from = path; + mutate(row.index, newItem); + }} + ></FileBrowser> + ); + }, }, { id: "arrow", @@ -113,30 +134,35 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => { { Header: "Bazarr", accessor: "to", - Cell: ({ value, row, update }) => ( - <FileBrowser - drop="up" - defaultValue={value} - type="bazarr" - onChange={(path) => { - const newItem = { ...row.original }; - newItem.to = path; - update && update(row, newItem); - }} - ></FileBrowser> - ), + Cell: ({ value, row }) => { + const mutate = useRowMutation(); + return ( + <FileBrowser + drop="up" + defaultValue={value} + type="bazarr" + onChange={(path) => { + const newItem = { ...row.original }; + newItem.to = path; + mutate(row.index, newItem); + }} + ></FileBrowser> + ); + }, }, { id: "action", accessor: "to", - Cell: ({ row, update }) => ( - <ActionButton - icon={faTrash} - onClick={() => { - update && update(row); - }} - ></ActionButton> - ), + Cell: ({ row }) => { + const mutate = useRowMutation(); + + return ( + <ActionButton + icon={faTrash} + onClick={() => mutate(row.index)} + ></ActionButton> + ); + }, }, ], [type] @@ -144,18 +170,17 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => { if (enabled) { return ( - <React.Fragment> + <RowContext.Provider value={updateCell}> <SimpleTable emptyText="No Mapping" responsive={false} columns={columns} data={data} - update={updateCell} ></SimpleTable> <Button block variant="light" onClick={addRow}> Add </Button> - </React.Fragment> + </RowContext.Provider> ); } else { return ( diff --git a/frontend/src/pages/Settings/components/style.scss b/frontend/src/pages/Settings/components/style.scss deleted file mode 100644 index 83b21eb4e..000000000 --- a/frontend/src/pages/Settings/components/style.scss +++ /dev/null @@ -1,14 +0,0 @@ -.settings-card { - cursor: pointer; - - min-height: 85px; - - transition: { - duration: 0.2s; - timing-function: ease-in-out; - } - - &:hover { - border-color: var(--primary); - } -} diff --git a/frontend/src/pages/Settings/options.ts b/frontend/src/pages/Settings/options.ts index 58ff5f490..8f29fb136 100644 --- a/frontend/src/pages/Settings/options.ts +++ b/frontend/src/pages/Settings/options.ts @@ -1,3 +1,5 @@ +import { SelectorOption } from "@/components"; + export const seriesTypeOptions: SelectorOption<string>[] = [ { label: "Standard", value: "standard" }, { label: "Anime", value: "anime" }, diff --git a/frontend/src/pages/System/Backups/BackupDeleteModal.tsx b/frontend/src/pages/System/Backups/BackupDeleteModal.tsx index 906b2f5e1..1f6ea6ece 100644 --- a/frontend/src/pages/System/Backups/BackupDeleteModal.tsx +++ b/frontend/src/pages/System/Backups/BackupDeleteModal.tsx @@ -1,10 +1,5 @@ -import { - AsyncButton, - BaseModal, - BaseModalProps, - useCloseModal, - useModalPayload, -} from "components"; +import { AsyncButton, BaseModal, BaseModalProps } from "@/components"; +import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; import React, { FunctionComponent } from "react"; import { Button } from "react-bootstrap"; import { useDeleteBackups } from "../../../apis/hooks"; @@ -12,11 +7,11 @@ import { useDeleteBackups } from "../../../apis/hooks"; interface Props extends BaseModalProps {} const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => { - const result = useModalPayload<string>(modal.modalKey); - const { mutateAsync } = useDeleteBackups(); - const closeModal = useCloseModal(); + const result = usePayload<string>(modal.modalKey); + + const { hide } = useModalControl(); const footer = ( <div className="d-flex flex-row-reverse flex-grow-1 justify-content-between"> @@ -25,7 +20,7 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => { variant="outline-secondary" className="mr-2" onClick={() => { - closeModal(modal.modalKey); + hide(modal.modalKey); }} > Cancel @@ -39,7 +34,7 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => { return null; } }} - onSuccess={() => closeModal(modal.modalKey)} + onSuccess={() => hide(modal.modalKey)} > Delete </AsyncButton> diff --git a/frontend/src/pages/System/Backups/BackupRestoreModal.tsx b/frontend/src/pages/System/Backups/BackupRestoreModal.tsx index 966d6409c..69d6ae12d 100644 --- a/frontend/src/pages/System/Backups/BackupRestoreModal.tsx +++ b/frontend/src/pages/System/Backups/BackupRestoreModal.tsx @@ -1,22 +1,17 @@ -import { - AsyncButton, - BaseModal, - BaseModalProps, - useCloseModal, - useModalPayload, -} from "components"; +import { useRestoreBackups } from "@/apis/hooks/system"; +import { AsyncButton, BaseModal, BaseModalProps } from "@/components"; +import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; import React, { FunctionComponent } from "react"; import { Button } from "react-bootstrap"; -import { useRestoreBackups } from "../../../apis/hooks"; interface Props extends BaseModalProps {} const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => { - const result = useModalPayload<string>(modal.modalKey); + const result = usePayload<string>(modal.modalKey); const { mutateAsync } = useRestoreBackups(); - const closeModal = useCloseModal(); + const { hide } = useModalControl(); const footer = ( <div className="d-flex flex-row-reverse flex-grow-1 justify-content-between"> @@ -25,7 +20,7 @@ const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => { variant="outline-secondary" className="mr-2" onClick={() => { - closeModal(modal.modalKey); + hide(modal.modalKey); }} > Cancel @@ -39,7 +34,7 @@ const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => { return null; } }} - onSuccess={() => closeModal(modal.modalKey)} + onSuccess={() => hide(modal.modalKey)} > Restore </AsyncButton> diff --git a/frontend/src/pages/System/Backups/index.tsx b/frontend/src/pages/System/Backups/index.tsx index 64225f1c3..fbeb91293 100644 --- a/frontend/src/pages/System/Backups/index.tsx +++ b/frontend/src/pages/System/Backups/index.tsx @@ -1,14 +1,12 @@ +import { useCreateBackups, useSystemBackups } from "@/apis/hooks"; +import { ContentHeader, QueryOverlay } from "@/components"; import { faFileArchive } from "@fortawesome/free-solid-svg-icons"; -import { useCreateBackups, useSystemBackups } from "apis/hooks"; -import { ContentHeader, QueryOverlay } from "components"; import React, { FunctionComponent } from "react"; import { Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; import Table from "./table"; -interface Props {} - -const SystemBackupsView: FunctionComponent<Props> = () => { +const SystemBackupsView: FunctionComponent = () => { const backups = useSystemBackups(); const { mutate: backup, isLoading: isResetting } = useCreateBackups(); diff --git a/frontend/src/pages/System/Backups/table.tsx b/frontend/src/pages/System/Backups/table.tsx index 7aec9ba2d..4beeb5e7f 100644 --- a/frontend/src/pages/System/Backups/table.tsx +++ b/frontend/src/pages/System/Backups/table.tsx @@ -1,6 +1,7 @@ +import { ActionButton, PageTable } from "@/components"; +import { useModalControl } from "@/modules/redux/hooks/modal"; import { faClock, faHistory, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { ActionButton, PageTable, useShowModal } from "components"; import React, { FunctionComponent, useMemo } from "react"; import { ButtonGroup } from "react-bootstrap"; import { Column } from "react-table"; @@ -12,7 +13,6 @@ interface Props { } const Table: FunctionComponent<Props> = ({ backups }) => { - const backupModal = useShowModal(); const columns: Column<System.Backups>[] = useMemo<Column<System.Backups>[]>( () => [ { @@ -37,24 +37,23 @@ const Table: FunctionComponent<Props> = ({ backups }) => { { accessor: "id", Cell: (row) => { + const { show } = useModalControl(); return ( <ButtonGroup> <ActionButton icon={faHistory} - onClick={() => - backupModal("restore", row.row.original.filename) - } + onClick={() => show("restore", row.row.original.filename)} ></ActionButton> <ActionButton icon={faTrash} - onClick={() => backupModal("delete", row.row.original.filename)} + onClick={() => show("delete", row.row.original.filename)} ></ActionButton> </ButtonGroup> ); }, }, ], - [backupModal] + [] ); return ( diff --git a/frontend/src/pages/System/Logs/index.tsx b/frontend/src/pages/System/Logs/index.tsx index 2f835b44b..86cc96df1 100644 --- a/frontend/src/pages/System/Logs/index.tsx +++ b/frontend/src/pages/System/Logs/index.tsx @@ -1,15 +1,13 @@ +import { useDeleteLogs, useSystemLogs } from "@/apis/hooks"; +import { ContentHeader, QueryOverlay } from "@/components"; +import { Environment } from "@/utilities"; import { faDownload, faSync, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { useDeleteLogs, useSystemLogs } from "apis/hooks"; -import { ContentHeader, QueryOverlay } from "components"; -import React, { FunctionComponent, useCallback } from "react"; +import { FunctionComponent, useCallback } from "react"; import { Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; -import { Environment } from "utilities"; import Table from "./table"; -interface Props {} - -const SystemLogsView: FunctionComponent<Props> = () => { +const SystemLogsView: FunctionComponent = () => { const logs = useSystemLogs(); const { isFetching, data, refetch } = logs; diff --git a/frontend/src/pages/System/Logs/modal.tsx b/frontend/src/pages/System/Logs/modal.tsx index c06241cad..946ddf51d 100644 --- a/frontend/src/pages/System/Logs/modal.tsx +++ b/frontend/src/pages/System/Logs/modal.tsx @@ -1,10 +1,9 @@ -import { BaseModal, BaseModalProps, useModalPayload } from "components"; -import React, { FunctionComponent, useMemo } from "react"; +import { BaseModal, BaseModalProps } from "@/components"; +import { usePayload } from "@/modules/redux/hooks/modal"; +import { FunctionComponent, useMemo } from "react"; -interface Props extends BaseModalProps {} - -const SystemLogModal: FunctionComponent<Props> = ({ ...modal }) => { - const stack = useModalPayload<string>(modal.modalKey); +const SystemLogModal: FunctionComponent<BaseModalProps> = ({ ...modal }) => { + const stack = usePayload<string>(modal.modalKey); const result = useMemo( () => stack?.split("\\n").map((v, idx) => ( @@ -17,7 +16,7 @@ const SystemLogModal: FunctionComponent<Props> = ({ ...modal }) => { return ( <BaseModal title="Stack traceback" {...modal}> <pre> - <code className="zmdi-language-python-alt">{result}</code> + <code>{result}</code> </pre> </BaseModal> ); diff --git a/frontend/src/pages/System/Logs/table.tsx b/frontend/src/pages/System/Logs/table.tsx index b8eace161..04a965ed1 100644 --- a/frontend/src/pages/System/Logs/table.tsx +++ b/frontend/src/pages/System/Logs/table.tsx @@ -1,3 +1,5 @@ +import { ActionButton, PageTable } from "@/components"; +import { useModalControl } from "@/modules/redux/hooks/modal"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { faBug, @@ -8,10 +10,9 @@ import { faQuestion, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { ActionButton, PageTable, useShowModal } from "components"; import { isUndefined } from "lodash"; -import React, { FunctionComponent, useCallback, useMemo } from "react"; -import { Column, Row } from "react-table"; +import { FunctionComponent, useMemo } from "react"; +import { Column } from "react-table"; import SystemLogModal from "./modal"; interface Props { @@ -34,12 +35,6 @@ function mapTypeToIcon(type: System.LogType): IconDefinition { } const Table: FunctionComponent<Props> = ({ logs }) => { - const showModal = useShowModal(); - const show = useCallback( - (row: Row<System.Log>, text: string) => - showModal<string>("system-log", text), - [showModal] - ); const columns: Column<System.Log>[] = useMemo<Column<System.Log>[]>( () => [ { @@ -59,12 +54,13 @@ const Table: FunctionComponent<Props> = ({ logs }) => { }, { accessor: "exception", - Cell: ({ row, value, update }) => { + Cell: ({ value }) => { + const { show } = useModalControl(); if (!isUndefined(value)) { return ( <ActionButton icon={faLayerGroup} - onClick={() => update && update(row, value)} + onClick={() => show("system-log", value)} ></ActionButton> ); } else { @@ -77,10 +73,10 @@ const Table: FunctionComponent<Props> = ({ logs }) => { ); return ( - <React.Fragment> - <PageTable columns={columns} data={logs} update={show}></PageTable> + <> + <PageTable columns={columns} data={logs}></PageTable> <SystemLogModal size="xl" modalKey="system-log"></SystemLogModal> - </React.Fragment> + </> ); }; diff --git a/frontend/src/pages/System/Providers/index.tsx b/frontend/src/pages/System/Providers/index.tsx index f6b3b14fc..a468c4638 100644 --- a/frontend/src/pages/System/Providers/index.tsx +++ b/frontend/src/pages/System/Providers/index.tsx @@ -1,14 +1,12 @@ +import { useResetProvider, useSystemProviders } from "@/apis/hooks"; +import { ContentHeader, QueryOverlay } from "@/components"; import { faSync, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { useResetProvider, useSystemProviders } from "apis/hooks"; -import { ContentHeader, QueryOverlay } from "components"; -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; import Table from "./table"; -interface Props {} - -const SystemProvidersView: FunctionComponent<Props> = () => { +const SystemProvidersView: FunctionComponent = () => { const providers = useSystemProviders(); const { isFetching, data, refetch } = providers; diff --git a/frontend/src/pages/System/Providers/table.tsx b/frontend/src/pages/System/Providers/table.tsx index bf2168a5b..47991b3c5 100644 --- a/frontend/src/pages/System/Providers/table.tsx +++ b/frontend/src/pages/System/Providers/table.tsx @@ -1,5 +1,5 @@ -import { SimpleTable } from "components"; -import React, { FunctionComponent, useMemo } from "react"; +import { SimpleTable } from "@/components"; +import { FunctionComponent, useMemo } from "react"; import { Column } from "react-table"; interface Props { diff --git a/frontend/src/pages/System/Releases/index.tsx b/frontend/src/pages/System/Releases/index.tsx index 8863aed6a..32c8bd3e0 100644 --- a/frontend/src/pages/System/Releases/index.tsx +++ b/frontend/src/pages/System/Releases/index.tsx @@ -1,13 +1,11 @@ -import { useSystemReleases } from "apis/hooks"; -import { QueryOverlay } from "components"; -import React, { FunctionComponent, useMemo } from "react"; +import { useSystemReleases } from "@/apis/hooks"; +import { QueryOverlay } from "@/components"; +import { BuildKey } from "@/utilities"; +import { FunctionComponent, useMemo } from "react"; import { Badge, Card, Col, Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; -import { BuildKey } from "utilities"; -interface Props {} - -const SystemReleasesView: FunctionComponent<Props> = () => { +const SystemReleasesView: FunctionComponent = () => { const releases = useSystemReleases(); const { data } = releases; @@ -18,13 +16,13 @@ const SystemReleasesView: FunctionComponent<Props> = () => { </Helmet> <Row> <QueryOverlay result={releases}> - <React.Fragment> + <> {data?.map((v, idx) => ( <Col xs={12} key={BuildKey(idx, v.date)}> <InfoElement {...v}></InfoElement> </Col> ))} - </React.Fragment> + </> </QueryOverlay> </Row> </Container> diff --git a/frontend/src/pages/System/Status/index.tsx b/frontend/src/pages/System/Status/index.tsx index 5fdd9c446..fc7aaa98c 100644 --- a/frontend/src/pages/System/Status/index.tsx +++ b/frontend/src/pages/System/Status/index.tsx @@ -1,3 +1,6 @@ +import { useSystemHealth, useSystemStatus } from "@/apis/hooks"; +import { QueryOverlay } from "@/components"; +import { GithubRepoRoot } from "@/utilities/constants"; import { IconDefinition } from "@fortawesome/fontawesome-common-types"; import { faDiscord, @@ -6,20 +9,16 @@ import { } from "@fortawesome/free-brands-svg-icons"; import { faPaperPlane } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useSystemHealth, useSystemStatus } from "apis/hooks"; -import { QueryOverlay } from "components"; import moment from "moment"; -import React, { FunctionComponent, useState } from "react"; +import { FunctionComponent, ReactNode, useState } from "react"; import { Col, Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; import { useIntervalWhen } from "rooks"; -import { GithubRepoRoot } from "utilities/constants"; -import "./style.scss"; import Table from "./table"; interface InfoProps { title: string; - children: React.ReactNode; + children: ReactNode; } function CRow(props: InfoProps): JSX.Element { @@ -43,12 +42,12 @@ interface IconProps { function Label(props: IconProps): JSX.Element { const { icon, link, children } = props; return ( - <React.Fragment> + <> <FontAwesomeIcon icon={icon} style={{ width: "2rem" }}></FontAwesomeIcon> <a href={link} target="_blank" rel="noopener noreferrer"> {children} </a> - </React.Fragment> + </> ); } @@ -65,9 +64,7 @@ const InfoContainer: FunctionComponent<{ title: string }> = ({ ); }; -interface Props {} - -const SystemStatusView: FunctionComponent<Props> = () => { +const SystemStatusView: FunctionComponent = () => { const health = useSystemHealth(); const { data: status } = useSystemStatus(); @@ -77,7 +74,7 @@ const SystemStatusView: FunctionComponent<Props> = () => { useIntervalWhen( () => { if (status) { - let duration = moment.duration( + const duration = moment.duration( moment().utc().unix() - status.start_time, "seconds" ), diff --git a/frontend/src/pages/System/Status/style.scss b/frontend/src/pages/System/Status/style.scss deleted file mode 100644 index b3f9bbe50..000000000 --- a/frontend/src/pages/System/Status/style.scss +++ /dev/null @@ -1,3 +0,0 @@ -.status-issue { - min-width: 16rem; -} diff --git a/frontend/src/pages/System/Status/table.tsx b/frontend/src/pages/System/Status/table.tsx index 3e499f5f4..18b3588f3 100644 --- a/frontend/src/pages/System/Status/table.tsx +++ b/frontend/src/pages/System/Status/table.tsx @@ -1,5 +1,5 @@ -import { SimpleTable } from "components"; -import React, { FunctionComponent, useMemo } from "react"; +import { SimpleTable } from "@/components"; +import { FunctionComponent, useMemo } from "react"; import { Column } from "react-table"; interface Props { diff --git a/frontend/src/pages/System/Tasks/index.tsx b/frontend/src/pages/System/Tasks/index.tsx index 1bb3430f4..f639ae5c6 100644 --- a/frontend/src/pages/System/Tasks/index.tsx +++ b/frontend/src/pages/System/Tasks/index.tsx @@ -1,14 +1,12 @@ +import { useSystemTasks } from "@/apis/hooks"; +import { ContentHeader, QueryOverlay } from "@/components"; import { faSync } from "@fortawesome/free-solid-svg-icons"; -import { useSystemTasks } from "apis/hooks"; -import { ContentHeader, QueryOverlay } from "components"; -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; import Table from "./table"; -interface Props {} - -const SystemTasksView: FunctionComponent<Props> = () => { +const SystemTasksView: FunctionComponent = () => { const tasks = useSystemTasks(); const { isFetching, data, refetch } = tasks; diff --git a/frontend/src/pages/System/Tasks/table.tsx b/frontend/src/pages/System/Tasks/table.tsx index e7a9bc42e..d41217efe 100644 --- a/frontend/src/pages/System/Tasks/table.tsx +++ b/frontend/src/pages/System/Tasks/table.tsx @@ -1,8 +1,8 @@ +import { useRunTask } from "@/apis/hooks"; +import { AsyncButton, SimpleTable } from "@/components"; import { faSync } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useRunTask } from "apis/hooks"; -import { AsyncButton, SimpleTable } from "components"; -import React, { FunctionComponent, useMemo } from "react"; +import { FunctionComponent, useMemo } from "react"; import { Column, useSortBy } from "react-table"; interface Props { diff --git a/frontend/src/pages/UIError.tsx b/frontend/src/pages/UIError.tsx index ba87bb588..4d86dc71b 100644 --- a/frontend/src/pages/UIError.tsx +++ b/frontend/src/pages/UIError.tsx @@ -1,9 +1,9 @@ +import { Reload } from "@/utilities"; +import { GithubRepoRoot } from "@/utilities/constants"; import { faDizzy } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { FunctionComponent } from "react"; +import { FunctionComponent } from "react"; import { Button, Container } from "react-bootstrap"; -import { Reload } from "utilities"; -import { GithubRepoRoot } from "utilities/constants"; interface Props { error: Error; diff --git a/frontend/src/pages/Wanted/Movies/index.tsx b/frontend/src/pages/Wanted/Movies/index.tsx index 1acf379c6..1be04cee0 100644 --- a/frontend/src/pages/Wanted/Movies/index.tsx +++ b/frontend/src/pages/Wanted/Movies/index.tsx @@ -1,21 +1,20 @@ -import { faSearch } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useMovieAction, useMovieSubtitleModification, useMovieWantedPagination, -} from "apis/hooks"; -import { AsyncButton, LanguageText } from "components"; -import WantedView from "components/views/WantedView"; -import React, { FunctionComponent, useMemo } from "react"; +} from "@/apis/hooks"; +import { AsyncButton } from "@/components"; +import Language from "@/components/bazarr/Language"; +import WantedView from "@/components/views/WantedView"; +import { BuildKey } from "@/utilities"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { FunctionComponent, useMemo } from "react"; import { Badge } from "react-bootstrap"; import { Link } from "react-router-dom"; import { Column } from "react-table"; -import { BuildKey } from "utilities"; - -interface Props {} -const WantedMoviesView: FunctionComponent<Props> = () => { +const WantedMoviesView: FunctionComponent = () => { const columns: Column<Wanted.Movie>[] = useMemo<Column<Wanted.Movie>[]>( () => [ { @@ -56,7 +55,7 @@ const WantedMoviesView: FunctionComponent<Props> = () => { }) } > - <LanguageText className="pr-1" text={item}></LanguageText> + <Language.Text className="pr-1" value={item}></Language.Text> <FontAwesomeIcon size="sm" icon={faSearch}></FontAwesomeIcon> </AsyncButton> )); diff --git a/frontend/src/pages/Wanted/Series/index.tsx b/frontend/src/pages/Wanted/Series/index.tsx index f4548dc48..e9d6ccd70 100644 --- a/frontend/src/pages/Wanted/Series/index.tsx +++ b/frontend/src/pages/Wanted/Series/index.tsx @@ -1,21 +1,20 @@ -import { faSearch } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useEpisodeSubtitleModification, useEpisodeWantedPagination, useSeriesAction, -} from "apis/hooks"; -import { AsyncButton, LanguageText } from "components"; -import WantedView from "components/views/WantedView"; -import React, { FunctionComponent, useMemo } from "react"; +} from "@/apis/hooks"; +import { AsyncButton } from "@/components"; +import Language from "@/components/bazarr/Language"; +import WantedView from "@/components/views/WantedView"; +import { BuildKey } from "@/utilities"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { FunctionComponent, useMemo } from "react"; import { Badge } from "react-bootstrap"; import { Link } from "react-router-dom"; import { Column } from "react-table"; -import { BuildKey } from "utilities"; - -interface Props {} -const WantedSeriesView: FunctionComponent<Props> = () => { +const WantedSeriesView: FunctionComponent = () => { const columns: Column<Wanted.Episode>[] = useMemo<Column<Wanted.Episode>[]>( () => [ { @@ -66,7 +65,7 @@ const WantedSeriesView: FunctionComponent<Props> = () => { }) } > - <LanguageText className="pr-1" text={item}></LanguageText> + <Language.Text className="pr-1" value={item}></Language.Text> <FontAwesomeIcon size="sm" icon={faSearch}></FontAwesomeIcon> </AsyncButton> )); |