diff options
Diffstat (limited to 'frontend')
-rw-r--r-- | frontend/src/@redux/actions/site.ts | 4 | ||||
-rw-r--r-- | frontend/src/@redux/hooks/site.ts | 11 | ||||
-rw-r--r-- | frontend/src/@redux/reducers/site.ts | 10 | ||||
-rw-r--r-- | frontend/src/App/Header.tsx | 15 | ||||
-rw-r--r-- | frontend/src/App/Router.tsx | 71 | ||||
-rw-r--r-- | frontend/src/App/index.tsx | 33 | ||||
-rw-r--r-- | frontend/src/Blacklist/Router.tsx | 36 | ||||
-rw-r--r-- | frontend/src/DisplayItem/Router.tsx | 45 | ||||
-rw-r--r-- | frontend/src/History/Router.tsx | 40 | ||||
-rw-r--r-- | frontend/src/Navigation/index.ts | 238 | ||||
-rw-r--r-- | frontend/src/Navigation/nav.d.ts | 26 | ||||
-rw-r--r-- | frontend/src/Router/index.tsx | 83 | ||||
-rw-r--r-- | frontend/src/Settings/Router.tsx | 58 | ||||
-rw-r--r-- | frontend/src/Sidebar/index.tsx | 285 | ||||
-rw-r--r-- | frontend/src/Sidebar/items.tsx | 179 | ||||
-rw-r--r-- | frontend/src/Sidebar/list.ts | 148 | ||||
-rw-r--r-- | frontend/src/Sidebar/types.d.ts | 29 | ||||
-rw-r--r-- | frontend/src/System/Releases/index.tsx | 23 | ||||
-rw-r--r-- | frontend/src/System/Router.tsx | 37 | ||||
-rw-r--r-- | frontend/src/Wanted/Router.tsx | 36 |
20 files changed, 593 insertions, 814 deletions
diff --git a/frontend/src/@redux/actions/site.ts b/frontend/src/@redux/actions/site.ts index b151bfb5f..fc348b942 100644 --- a/frontend/src/@redux/actions/site.ts +++ b/frontend/src/@redux/actions/site.ts @@ -47,7 +47,9 @@ export const siteUpdateNotifier = createAction<string>( "site/progress/update_notifier" ); -export const siteChangeSidebar = createAction<string>("site/sidebar/update"); +export const siteChangeSidebarVisibility = createAction<boolean>( + "site/sidebar/visibility" +); export const siteUpdateOffline = createAction<boolean>("site/offline/update"); diff --git a/frontend/src/@redux/hooks/site.ts b/frontend/src/@redux/hooks/site.ts index 05354572b..8d93fc13f 100644 --- a/frontend/src/@redux/hooks/site.ts +++ b/frontend/src/@redux/hooks/site.ts @@ -1,6 +1,6 @@ -import { useCallback, useEffect } from "react"; +import { useCallback } from "react"; import { useSystemSettings } from "."; -import { siteAddNotifications, siteChangeSidebar } from "../actions"; +import { siteAddNotifications } from "../actions"; import { useReduxAction, useReduxStore } from "./base"; export function useNotification(id: string, timeout: number = 5000) { @@ -37,10 +37,3 @@ export function useShowOnlyDesired() { const settings = useSystemSettings(); return settings.content?.general.embedded_subs_show_desired ?? false; } - -export function useSetSidebar(key: string) { - const update = useReduxAction(siteChangeSidebar); - useEffect(() => { - update(key); - }, [update, key]); -} diff --git a/frontend/src/@redux/reducers/site.ts b/frontend/src/@redux/reducers/site.ts index 8381a95b4..21cc0b370 100644 --- a/frontend/src/@redux/reducers/site.ts +++ b/frontend/src/@redux/reducers/site.ts @@ -6,7 +6,7 @@ import { siteAddNotifications, siteAddProgress, siteBootstrap, - siteChangeSidebar, + siteChangeSidebarVisibility, siteRedirectToAuth, siteRemoveNotifications, siteRemoveProgress, @@ -28,7 +28,7 @@ interface Site { timestamp: string; }; notifications: Server.Notification[]; - sidebar: string; + showSidebar: boolean; badges: Badge; } @@ -41,7 +41,7 @@ const defaultSite: Site = { timestamp: String(Date.now()), }, notifications: [], - sidebar: "", + showSidebar: false, badges: { movies: 0, episodes: 0, @@ -116,8 +116,8 @@ const reducer = createReducer(defaultSite, (builder) => { }); builder - .addCase(siteChangeSidebar, (state, action) => { - state.sidebar = action.payload; + .addCase(siteChangeSidebarVisibility, (state, action) => { + state.showSidebar = action.payload; }) .addCase(siteUpdateOffline, (state, action) => { state.offline = action.payload; diff --git a/frontend/src/App/Header.tsx b/frontend/src/App/Header.tsx index ae413030c..9ca33a574 100644 --- a/frontend/src/App/Header.tsx +++ b/frontend/src/App/Header.tsx @@ -5,7 +5,7 @@ import { faUser, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { FunctionComponent, useContext, useMemo } from "react"; +import React, { FunctionComponent, useMemo } from "react"; import { Button, Col, @@ -16,8 +16,10 @@ import { Row, } from "react-bootstrap"; import { Helmet } from "react-helmet"; -import { SidebarToggleContext } from "."; -import { siteRedirectToAuth } from "../@redux/actions"; +import { + siteChangeSidebarVisibility, + siteRedirectToAuth, +} from "../@redux/actions"; import { useSystemSettings } from "../@redux/hooks"; import { useReduxAction } from "../@redux/hooks/base"; import { useIsOffline } from "../@redux/hooks/site"; @@ -56,7 +58,7 @@ const Header: FunctionComponent<Props> = () => { const canLogout = (settings.content?.auth.type ?? "none") === "form"; - const toggleSidebar = useContext(SidebarToggleContext); + const changeSidebar = useReduxAction(siteChangeSidebarVisibility); const offline = useIsOffline(); @@ -115,7 +117,10 @@ const Header: FunctionComponent<Props> = () => { className="cursor-pointer" ></Image> </div> - <Button className="mx-2 m-0 d-md-none" onClick={toggleSidebar}> + <Button + className="mx-2 m-0 d-md-none" + onClick={() => changeSidebar(true)} + > <FontAwesomeIcon icon={faBars}></FontAwesomeIcon> </Button> <Container fluid> diff --git a/frontend/src/App/Router.tsx b/frontend/src/App/Router.tsx deleted file mode 100644 index 626ec6a4d..000000000 --- a/frontend/src/App/Router.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { FunctionComponent, useMemo } from "react"; -import { Redirect, Route, Switch, useHistory } from "react-router-dom"; -import { useDidMount } from "rooks"; -import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks/site"; -import BlacklistRouter from "../Blacklist/Router"; -import DisplayItemRouter from "../DisplayItem/Router"; -import HistoryRouter from "../History/Router"; -import SettingRouter from "../Settings/Router"; -import EmptyPage, { RouterEmptyPath } from "../special-pages/404"; -import SystemRouter from "../System/Router"; -import { ScrollToTop } from "../utilities"; -import WantedRouter from "../Wanted/Router"; - -const Router: FunctionComponent<{ className?: string }> = ({ className }) => { - const sonarr = useIsSonarrEnabled(); - const radarr = useIsRadarrEnabled(); - const redirectPath = useMemo(() => { - if (sonarr) { - return "/series"; - } else if (radarr) { - return "/movies"; - } else { - return "/settings"; - } - }, [sonarr, radarr]); - - const history = useHistory(); - - useDidMount(() => { - history.listen(() => { - // This is a hack to make sure ScrollToTop will be triggered in the next frame (When everything are loaded) - setTimeout(ScrollToTop); - }); - }); - - return ( - <div className={className}> - <Switch> - <Route exact path="/"> - <Redirect exact to={redirectPath}></Redirect> - </Route> - <Route path={["/series", "/movies"]}> - <DisplayItemRouter></DisplayItemRouter> - </Route> - <Route path="/wanted"> - <WantedRouter></WantedRouter> - </Route> - <Route path="/history"> - <HistoryRouter></HistoryRouter> - </Route> - <Route path="/blacklist"> - <BlacklistRouter></BlacklistRouter> - </Route> - <Route path="/settings"> - <SettingRouter></SettingRouter> - </Route> - <Route path="/system"> - <SystemRouter></SystemRouter> - </Route> - <Route exact path={RouterEmptyPath}> - <EmptyPage></EmptyPage> - </Route> - <Route path="*"> - <Redirect to={RouterEmptyPath}></Redirect> - </Route> - </Switch> - </div> - ); -}; - -export default Router; diff --git a/frontend/src/App/index.tsx b/frontend/src/App/index.tsx index 9c16ee800..d88651e7b 100644 --- a/frontend/src/App/index.tsx +++ b/frontend/src/App/index.tsx @@ -1,9 +1,4 @@ -import React, { - FunctionComponent, - useCallback, - useEffect, - useState, -} from "react"; +import React, { FunctionComponent, useEffect } from "react"; import { Row } from "react-bootstrap"; import { Provider } from "react-redux"; import { Route, Switch } from "react-router"; @@ -14,16 +9,15 @@ import { useReduxStore } from "../@redux/hooks/base"; import { useNotification } from "../@redux/hooks/site"; import store from "../@redux/store"; import { LoadingIndicator, ModalProvider } from "../components"; +import Router from "../Router"; import Sidebar from "../Sidebar"; import Auth from "../special-pages/AuthPage"; import ErrorBoundary from "../special-pages/ErrorBoundary"; import LaunchError from "../special-pages/LaunchError"; import { Environment } from "../utilities"; import Header from "./Header"; -import Router from "./Router"; // Sidebar Toggle -export const SidebarToggleContext = React.createContext<() => void>(() => {}); interface Props {} @@ -43,9 +37,6 @@ const App: FunctionComponent<Props> = () => { } }, initialized === true); - const [sidebar, setSidebar] = useState(false); - const toggleSidebar = useCallback(() => setSidebar((s) => !s), []); - if (!auth) { return <Redirect to="/login"></Redirect>; } @@ -61,17 +52,15 @@ const App: FunctionComponent<Props> = () => { } return ( <ErrorBoundary> - <SidebarToggleContext.Provider value={toggleSidebar}> - <Row noGutters className="header-container"> - <Header></Header> - </Row> - <Row noGutters className="flex-nowrap"> - <Sidebar open={sidebar}></Sidebar> - <ModalProvider> - <Router className="d-flex flex-row flex-grow-1 main-router"></Router> - </ModalProvider> - </Row> - </SidebarToggleContext.Provider> + <Row noGutters className="header-container"> + <Header></Header> + </Row> + <Row noGutters className="flex-nowrap"> + <Sidebar></Sidebar> + <ModalProvider> + <Router></Router> + </ModalProvider> + </Row> </ErrorBoundary> ); }; diff --git a/frontend/src/Blacklist/Router.tsx b/frontend/src/Blacklist/Router.tsx deleted file mode 100644 index 886a11514..000000000 --- a/frontend/src/Blacklist/Router.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { FunctionComponent } from "react"; -import { Redirect, Route, Switch } from "react-router-dom"; -import { - useIsRadarrEnabled, - useIsSonarrEnabled, - useSetSidebar, -} from "../@redux/hooks/site"; -import { RouterEmptyPath } from "../special-pages/404"; -import BlacklistMovies from "./Movies"; -import BlacklistSeries from "./Series"; - -const Router: FunctionComponent = () => { - const sonarr = useIsSonarrEnabled(); - const radarr = useIsRadarrEnabled(); - - useSetSidebar("Blacklist"); - return ( - <Switch> - {sonarr && ( - <Route exact path="/blacklist/series"> - <BlacklistSeries></BlacklistSeries> - </Route> - )} - {radarr && ( - <Route path="/blacklist/movies"> - <BlacklistMovies></BlacklistMovies> - </Route> - )} - <Route path="/blacklist/*"> - <Redirect to={RouterEmptyPath}></Redirect> - </Route> - </Switch> - ); -}; - -export default Router; diff --git a/frontend/src/DisplayItem/Router.tsx b/frontend/src/DisplayItem/Router.tsx deleted file mode 100644 index 1ab2c4f8a..000000000 --- a/frontend/src/DisplayItem/Router.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { FunctionComponent } from "react"; -import { Redirect, Route, Switch } from "react-router-dom"; -import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks"; -import { RouterEmptyPath } from "../special-pages/404"; -import Episodes from "./Episodes"; -import MovieDetail from "./MovieDetail"; -import Movies from "./Movies"; -import Series from "./Series"; - -interface Props {} - -const Router: FunctionComponent<Props> = () => { - const radarr = useIsRadarrEnabled(); - const sonarr = useIsSonarrEnabled(); - - return ( - <Switch> - {radarr && ( - <Route exact path="/movies"> - <Movies></Movies> - </Route> - )} - {radarr && ( - <Route path="/movies/:id"> - <MovieDetail></MovieDetail> - </Route> - )} - {sonarr && ( - <Route exact path="/series"> - <Series></Series> - </Route> - )} - {sonarr && ( - <Route path="/series/:id"> - <Episodes></Episodes> - </Route> - )} - <Route path="*"> - <Redirect to={RouterEmptyPath}></Redirect> - </Route> - </Switch> - ); -}; - -export default Router; diff --git a/frontend/src/History/Router.tsx b/frontend/src/History/Router.tsx deleted file mode 100644 index b7693355f..000000000 --- a/frontend/src/History/Router.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { FunctionComponent } from "react"; -import { Redirect, Route, Switch } from "react-router-dom"; -import { - useIsRadarrEnabled, - useIsSonarrEnabled, - useSetSidebar, -} from "../@redux/hooks/site"; -import { RouterEmptyPath } from "../special-pages/404"; -import MoviesHistory from "./Movies"; -import SeriesHistory from "./Series"; -import HistoryStats from "./Statistics"; - -const Router: FunctionComponent = () => { - const sonarr = useIsSonarrEnabled(); - const radarr = useIsRadarrEnabled(); - - useSetSidebar("History"); - return ( - <Switch> - {sonarr && ( - <Route exact path="/history/series"> - <SeriesHistory></SeriesHistory> - </Route> - )} - {radarr && ( - <Route exact path="/history/movies"> - <MoviesHistory></MoviesHistory> - </Route> - )} - <Route exact path="/history/stats"> - <HistoryStats></HistoryStats> - </Route> - <Route path="/history/*"> - <Redirect to={RouterEmptyPath}></Redirect> - </Route> - </Switch> - ); -}; - -export default Router; diff --git a/frontend/src/Navigation/index.ts b/frontend/src/Navigation/index.ts new file mode 100644 index 000000000..0e63b9850 --- /dev/null +++ b/frontend/src/Navigation/index.ts @@ -0,0 +1,238 @@ +import { + faClock, + faCogs, + faExclamationTriangle, + faFileExcel, + faFilm, + faLaptop, + faPlay, +} from "@fortawesome/free-solid-svg-icons"; +import { useMemo } from "react"; +import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks"; +import { useReduxStore } from "../@redux/hooks/base"; +import BlacklistMoviesView from "../Blacklist/Movies"; +import BlacklistSeriesView from "../Blacklist/Series"; +import Episodes from "../DisplayItem/Episodes"; +import MovieDetail from "../DisplayItem/MovieDetail"; +import MovieView from "../DisplayItem/Movies"; +import SeriesView from "../DisplayItem/Series"; +import MoviesHistoryView from "../History/Movies"; +import SeriesHistoryView from "../History/Series"; +import HistoryStats from "../History/Statistics"; +import SettingsGeneralView from "../Settings/General"; +import SettingsLanguagesView from "../Settings/Languages"; +import SettingsNotificationsView from "../Settings/Notifications"; +import SettingsProvidersView from "../Settings/Providers"; +import SettingsRadarrView from "../Settings/Radarr"; +import SettingsSchedulerView from "../Settings/Scheduler"; +import SettingsSonarrView from "../Settings/Sonarr"; +import SettingsSubtitlesView from "../Settings/Subtitles"; +import SettingsUIView from "../Settings/UI"; +import EmptyPage, { RouterEmptyPath } from "../special-pages/404"; +import SystemLogsView from "../System/Logs"; +import SystemProvidersView from "../System/Providers"; +import SystemReleasesView from "../System/Releases"; +import SystemStatusView from "../System/Status"; +import SystemTasksView from "../System/Tasks"; +import WantedMoviesView from "../Wanted/Movies"; +import WantedSeriesView from "../Wanted/Series"; +import { Navigation } from "./nav"; + +export function useNavigationItems() { + const sonarr = useIsSonarrEnabled(); + const radarr = useIsRadarrEnabled(); + const { movies, episodes, providers } = useReduxStore((s) => s.site.badges); + + const items = useMemo<Navigation.RouteItem[]>( + () => [ + { + name: "404", + path: RouterEmptyPath, + component: EmptyPage, + routeOnly: true, + }, + { + icon: faPlay, + name: "Series", + path: "/series", + component: SeriesView, + enabled: sonarr, + routes: [ + { + name: "Episode", + path: "/:id", + component: Episodes, + routeOnly: true, + }, + ], + }, + { + icon: faFilm, + name: "Movies", + path: "/movies", + component: MovieView, + enabled: radarr, + routes: [ + { + name: "Movie Details", + path: "/:id", + component: MovieDetail, + routeOnly: true, + }, + ], + }, + { + icon: faClock, + name: "History", + path: "/history", + routes: [ + { + name: "Series", + path: "/series", + enabled: sonarr, + component: SeriesHistoryView, + }, + { + name: "Movies", + path: "/movies", + enabled: radarr, + component: MoviesHistoryView, + }, + { + name: "Statistics", + path: "/stats", + component: HistoryStats, + }, + ], + }, + { + icon: faFileExcel, + name: "Blacklist", + path: "/blacklist", + routes: [ + { + name: "Series", + path: "/series", + enabled: sonarr, + component: BlacklistSeriesView, + }, + { + name: "Movies", + path: "/movies", + enabled: radarr, + component: BlacklistMoviesView, + }, + ], + }, + { + icon: faExclamationTriangle, + name: "Wanted", + path: "/wanted", + routes: [ + { + name: "Series", + path: "/series", + badge: episodes, + enabled: sonarr, + component: WantedSeriesView, + }, + { + name: "Movies", + path: "/movies", + badge: movies, + enabled: radarr, + component: WantedMoviesView, + }, + ], + }, + { + icon: faCogs, + name: "Settings", + path: "/settings", + routes: [ + { + name: "General", + path: "/general", + component: SettingsGeneralView, + }, + { + name: "Languages", + path: "/languages", + component: SettingsLanguagesView, + }, + { + name: "Providers", + path: "/providers", + badge: providers, + component: SettingsProvidersView, + }, + { + name: "Subtitles", + path: "/subtitles", + component: SettingsSubtitlesView, + }, + { + name: "Sonarr", + path: "/sonarr", + component: SettingsSonarrView, + }, + { + name: "Radarr", + path: "/radarr", + component: SettingsRadarrView, + }, + { + name: "Notifications", + path: "/notifications", + component: SettingsNotificationsView, + }, + { + name: "Scheduler", + path: "/scheduler", + component: SettingsSchedulerView, + }, + { + name: "UI", + path: "/ui", + component: SettingsUIView, + }, + ], + }, + { + icon: faLaptop, + name: "System", + path: "/system", + routes: [ + { + name: "Tasks", + path: "/tasks", + component: SystemTasksView, + }, + { + name: "Logs", + path: "/logs", + component: SystemLogsView, + }, + { + name: "Providers", + path: "/providers", + component: SystemProvidersView, + }, + { + name: "Status", + path: "/status", + component: SystemStatusView, + }, + { + name: "Releases", + path: "/releases", + component: SystemReleasesView, + }, + ], + }, + ], + [episodes, movies, providers, radarr, sonarr] + ); + + return items; +} diff --git a/frontend/src/Navigation/nav.d.ts b/frontend/src/Navigation/nav.d.ts new file mode 100644 index 000000000..7ce67f082 --- /dev/null +++ b/frontend/src/Navigation/nav.d.ts @@ -0,0 +1,26 @@ +import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; +import { FunctionComponent } from "react"; + +export declare namespace Navigation { + type RouteWithoutChild = { + icon?: IconDefinition; + name: string; + path: string; + component: FunctionComponent; + badge?: number; + enabled?: boolean; + routeOnly?: boolean; + }; + + type RouteWithChild = { + icon: IconDefinition; + name: string; + path: string; + component?: FunctionComponent; + badge?: number; + enabled?: boolean; + routes: RouteWithoutChild[]; + }; + + type RouteItem = RouteWithChild | RouteWithoutChild; +} diff --git a/frontend/src/Router/index.tsx b/frontend/src/Router/index.tsx new file mode 100644 index 000000000..e9295db7f --- /dev/null +++ b/frontend/src/Router/index.tsx @@ -0,0 +1,83 @@ +import { FunctionComponent } from "react"; +import { Redirect, Route, Switch, useHistory } from "react-router"; +import { useDidMount } from "rooks"; +import { useNavigationItems } from "../Navigation"; +import { Navigation } from "../Navigation/nav"; +import { RouterEmptyPath } from "../special-pages/404"; +import { BuildKey, ScrollToTop } from "../utilities"; + +const Router: FunctionComponent = () => { + const navItems = useNavigationItems(); + + const history = useHistory(); + useDidMount(() => { + history.listen(() => { + // This is a hack to make sure ScrollToTop will be triggered in the next frame (When everything are loaded) + setTimeout(ScrollToTop); + }); + }); + + return ( + <div className="d-flex flex-row flex-grow-1 main-router"> + <Switch> + {navItems.map((v, idx) => { + if ("routes" in v) { + return ( + <Route path={v.path} key={BuildKey(idx, v.name, "router")}> + <ParentRouter {...v}></ParentRouter> + </Route> + ); + } else if (v.enabled !== false) { + return ( + <Route + key={BuildKey(idx, v.name, "root")} + exact + path={v.path} + component={v.component} + ></Route> + ); + } else { + return null; + } + })} + <Route path="*"> + <Redirect to={RouterEmptyPath}></Redirect> + </Route> + </Switch> + </div> + ); +}; + +export default Router; + +const ParentRouter: FunctionComponent<Navigation.RouteWithChild> = ({ + path, + enabled, + component, + routes, +}) => { + if (enabled === false || (component === undefined && routes.length === 0)) { + return null; + } + const ParentComponent = + component ?? (() => <Redirect to={path + routes[0].path}></Redirect>); + + return ( + <Switch> + <Route exact path={path} component={ParentComponent}></Route> + {routes + .filter((v) => v.enabled !== false) + .map((v, idx) => ( + <Route + key={BuildKey(idx, v.name, "route")} + exact + path={path + v.path} + component={v.component} + ></Route> + ))} + <Route path="*"> + <Redirect to={RouterEmptyPath}></Redirect> + </Route> + </Switch> + ); +}; diff --git a/frontend/src/Settings/Router.tsx b/frontend/src/Settings/Router.tsx deleted file mode 100644 index 5bf6828c5..000000000 --- a/frontend/src/Settings/Router.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { FunctionComponent } from "react"; -import { Redirect, Route, Switch } from "react-router-dom"; -import { useSetSidebar } from "../@redux/hooks/site"; -import { RouterEmptyPath } from "../special-pages/404"; -import General from "./General"; -import Languages from "./Languages"; -import Notifications from "./Notifications"; -import Providers from "./Providers"; -import Radarr from "./Radarr"; -import Scheduler from "./Scheduler"; -import Sonarr from "./Sonarr"; -import Subtitles from "./Subtitles"; -import UI from "./UI"; - -interface Props {} - -const Router: FunctionComponent<Props> = () => { - useSetSidebar("Settings"); - return ( - <Switch> - <Route exact path="/settings"> - <Redirect exact to="/settings/general"></Redirect> - </Route> - <Route exact path="/settings/general"> - <General></General> - </Route> - <Route exact path="/settings/ui"> - <UI></UI> - </Route> - <Route exact path="/settings/sonarr"> - <Sonarr></Sonarr> - </Route> - <Route exact path="/settings/radarr"> - <Radarr></Radarr> - </Route> - <Route exact path="/settings/languages"> - <Languages></Languages> - </Route> - <Route exact path="/settings/subtitles"> - <Subtitles></Subtitles> - </Route> - <Route exact path="/settings/scheduler"> - <Scheduler></Scheduler> - </Route> - <Route exact path="/settings/providers"> - <Providers></Providers> - </Route> - <Route exact path="/settings/notifications"> - <Notifications></Notifications> - </Route> - <Route path="/settings/*"> - <Redirect to={RouterEmptyPath}></Redirect> - </Route> - </Switch> - ); -}; - -export default Router; diff --git a/frontend/src/Sidebar/index.tsx b/frontend/src/Sidebar/index.tsx index 00bc43f55..12af3d303 100644 --- a/frontend/src/Sidebar/index.tsx +++ b/frontend/src/Sidebar/index.tsx @@ -1,86 +1,56 @@ -import React, { FunctionComponent, useContext, useMemo } from "react"; -import { Container, Image, ListGroup } from "react-bootstrap"; -import { useReduxStore } from "../@redux/hooks/base"; -import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks/site"; +import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { + createContext, + FunctionComponent, + useContext, + useMemo, + useState, +} from "react"; +import { + Badge, + Collapse, + Container, + Image, + ListGroup, + ListGroupItem, +} from "react-bootstrap"; +import { NavLink, useHistory, useRouteMatch } from "react-router-dom"; +import { siteChangeSidebarVisibility } from "../@redux/actions"; +import { useReduxAction, useReduxStore } from "../@redux/hooks/base"; import logo from "../@static/logo64.png"; -import { SidebarToggleContext } from "../App"; +import { useNavigationItems } from "../Navigation"; +import { Navigation } from "../Navigation/nav"; +import { BuildKey } from "../utilities"; import { useGotoHomepage } from "../utilities/hooks"; -import { - BadgesContext, - CollapseItem, - HiddenKeysContext, - LinkItem, -} from "./items"; -import { RadarrDisabledKey, SidebarList, SonarrDisabledKey } from "./list"; import "./style.scss"; -import { BadgeProvider } from "./types"; - -interface Props { - open?: boolean; -} -const Sidebar: FunctionComponent<Props> = ({ open }) => { - const toggle = useContext(SidebarToggleContext); +const SelectionContext = createContext<{ + selection: string | null; + select: (selection: string | null) => void; +}>({ selection: null, select: () => {} }); - const { movies, episodes, providers, status } = useReduxStore( - (s) => s.site.badges - ); - - const sonarrEnabled = useIsSonarrEnabled(); - const radarrEnabled = useIsRadarrEnabled(); - - const badges = useMemo<BadgeProvider>( - () => ({ - Wanted: { - Series: sonarrEnabled ? episodes : 0, - Movies: radarrEnabled ? movies : 0, - }, - System: { - Providers: providers, - Status: status, - }, - }), - [movies, episodes, providers, sonarrEnabled, radarrEnabled, status] - ); +const Sidebar: FunctionComponent = () => { + const open = useReduxStore((s) => s.site.showSidebar); - const hiddenKeys = useMemo<string[]>(() => { - const list = []; - if (!sonarrEnabled) { - list.push(SonarrDisabledKey); - } - if (!radarrEnabled) { - list.push(RadarrDisabledKey); - } - return list; - }, [sonarrEnabled, radarrEnabled]); + const changeSidebar = useReduxAction(siteChangeSidebarVisibility); const cls = ["sidebar-container"]; const overlay = ["sidebar-overlay"]; - if (open === true) { + if (open) { cls.push("open"); overlay.push("open"); } - const sidebarItems = useMemo( - () => - SidebarList.map((v) => { - if (hiddenKeys.includes(v.hiddenKey ?? "")) { - return null; - } - if ("children" in v) { - return <CollapseItem key={v.name} {...v}></CollapseItem>; - } else { - return <LinkItem key={v.link} {...v}></LinkItem>; - } - }), - [hiddenKeys] - ); - const goHome = useGotoHomepage(); + const [selection, setSelection] = useState<string | null>(null); + return ( - <React.Fragment> + <SelectionContext.Provider + value={{ selection: selection, select: setSelection }} + > <aside className={cls.join(" ")}> <Container className="sidebar-title d-flex align-items-center d-md-none"> <Image @@ -92,13 +62,184 @@ const Sidebar: FunctionComponent<Props> = ({ open }) => { className="cursor-pointer" ></Image> </Container> - <HiddenKeysContext.Provider value={hiddenKeys}> - <BadgesContext.Provider value={badges}> - <ListGroup variant="flush">{sidebarItems}</ListGroup> - </BadgesContext.Provider> - </HiddenKeysContext.Provider> + <SidebarNavigation></SidebarNavigation> </aside> - <div className={overlay.join(" ")} onClick={toggle}></div> + <div + className={overlay.join(" ")} + onClick={() => changeSidebar(false)} + ></div> + </SelectionContext.Provider> + ); +}; + +const SidebarNavigation: FunctionComponent = () => { + const navItems = useNavigationItems(); + + return ( + <ListGroup variant="flush"> + {navItems.map((v, idx) => { + if ("routes" in v) { + return ( + <SidebarParent key={BuildKey(idx, v.name)} {...v}></SidebarParent> + ); + } else { + return ( + <SidebarChild + parent="" + key={BuildKey(idx, v.name)} + {...v} + ></SidebarChild> + ); + } + })} + </ListGroup> + ); +}; + +const SidebarParent: FunctionComponent<Navigation.RouteWithChild> = ({ + icon, + badge, + name, + path, + routes, + enabled, + component, +}) => { + const computedBadge = useMemo(() => { + let computed = badge ?? 0; + + computed += routes.reduce((prev, curr) => { + return prev + (curr.badge ?? 0); + }, 0); + + return computed !== 0 ? computed : undefined; + }, [badge, routes]); + + const enabledRoutes = useMemo( + () => routes.filter((v) => v.enabled !== false && v.routeOnly !== true), + [routes] + ); + + const changeSidebar = useReduxAction(siteChangeSidebarVisibility); + + const { selection, select } = useContext(SelectionContext); + + const match = useRouteMatch({ path }); + const open = match !== null || selection === path; + + const collapseBoxClass = useMemo( + () => `sidebar-collapse-box ${open ? "active" : ""}`, + [open] + ); + + const history = useHistory(); + + if (enabled === false) { + return null; + } else if (enabledRoutes.length === 0) { + if (component) { + return ( + <NavLink + activeClassName="sb-active" + className="list-group-item list-group-item-action sidebar-button" + to={path} + onClick={() => changeSidebar(false)} + > + <SidebarContent + icon={icon} + name={name} + badge={computedBadge} + ></SidebarContent> + </NavLink> + ); + } else { + return null; + } + } + + return ( + <div className={collapseBoxClass}> + <ListGroupItem + action + className="sidebar-button" + onClick={() => { + if (open) { + select(null); + } else { + select(path); + } + if (component !== undefined) { + history.push(path); + } + }} + > + <SidebarContent + icon={icon} + name={name} + badge={computedBadge} + ></SidebarContent> + </ListGroupItem> + <Collapse in={open}> + <div className="sidebar-collapse"> + {enabledRoutes.map((v, idx) => ( + <SidebarChild + key={BuildKey(idx, v.name, "child")} + parent={path} + {...v} + ></SidebarChild> + ))} + </div> + </Collapse> + </div> + ); +}; + +interface SidebarChildProps { + parent: string; +} + +const SidebarChild: FunctionComponent< + SidebarChildProps & Navigation.RouteWithoutChild +> = ({ icon, name, path, badge, enabled, routeOnly, parent }) => { + const changeSidebar = useReduxAction(siteChangeSidebarVisibility); + const { select } = useContext(SelectionContext); + + if (enabled === false || routeOnly === true) { + return null; + } + + return ( + <NavLink + activeClassName="sb-active" + className="list-group-item list-group-item-action sidebar-button sb-collapse" + to={parent + path} + onClick={() => { + select(null); + changeSidebar(false); + }} + > + <SidebarContent icon={icon} name={name} badge={badge}></SidebarContent> + </NavLink> + ); +}; + +const SidebarContent: FunctionComponent<{ + icon?: IconDefinition; + name: string; + badge?: number; +}> = ({ icon, name, badge }) => { + return ( + <React.Fragment> + {icon && ( + <FontAwesomeIcon + size="1x" + className="icon" + icon={icon} + ></FontAwesomeIcon> + )} + <span className="d-flex flex-grow-1 justify-content-between"> + {name} <Badge variant="secondary">{badge !== 0 ? badge : null}</Badge> + </span> </React.Fragment> ); }; diff --git a/frontend/src/Sidebar/items.tsx b/frontend/src/Sidebar/items.tsx deleted file mode 100644 index beb376eb2..000000000 --- a/frontend/src/Sidebar/items.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { FunctionComponent, useContext, useMemo } from "react"; -import { Badge, Collapse, ListGroupItem } from "react-bootstrap"; -import { NavLink } from "react-router-dom"; -import { siteChangeSidebar } from "../@redux/actions"; -import { useReduxAction, useReduxStore } from "../@redux/hooks/base"; -import { SidebarToggleContext } from "../App"; -import { - BadgeProvider, - ChildBadgeProvider, - CollapseItemType, - LinkItemType, -} from "./types"; - -export const HiddenKeysContext = React.createContext<string[]>([]); - -export const BadgesContext = React.createContext<BadgeProvider>({}); - -function useToggleSidebar() { - return useReduxAction(siteChangeSidebar); -} - -function useSidebarKey() { - return useReduxStore((s) => s.site.sidebar); -} - -export const LinkItem: FunctionComponent<LinkItemType> = ({ - link, - name, - icon, -}) => { - const badges = useContext(BadgesContext); - const toggle = useContext(SidebarToggleContext); - - const badgeValue = useMemo(() => { - let badge: Nullable<number> = null; - if (name in badges) { - let item = badges[name]; - if (typeof item === "number") { - badge = item; - } - } - return badge; - }, [badges, name]); - - return ( - <NavLink - activeClassName="sb-active" - className="list-group-item list-group-item-action sidebar-button" - to={link} - onClick={toggle} - > - <DisplayItem - badge={badgeValue ?? undefined} - name={name} - icon={icon} - ></DisplayItem> - </NavLink> - ); -}; - -export const CollapseItem: FunctionComponent<CollapseItemType> = ({ - icon, - name, - children, -}) => { - const badges = useContext(BadgesContext); - const hiddenKeys = useContext(HiddenKeysContext); - const toggleSidebar = useContext(SidebarToggleContext); - - const sidebarKey = useSidebarKey(); - const updateSidebar = useToggleSidebar(); - - const [badgeValue, childValue] = useMemo< - [Nullable<number>, Nullable<ChildBadgeProvider>] - >(() => { - let badge: Nullable<number> = null; - let child: Nullable<ChildBadgeProvider> = null; - - if (name in badges) { - const item = badges[name]; - if (typeof item === "number") { - badge = item; - } else if (typeof item === "object") { - badge = 0; - child = item; - for (const it in item) { - badge += item[it]; - } - } - } - return [badge, child]; - }, [badges, name]); - - const active = useMemo(() => sidebarKey === name, [sidebarKey, name]); - - const collapseBoxClass = useMemo( - () => `sidebar-collapse-box ${active ? "active" : ""}`, - [active] - ); - - const childrenElems = useMemo( - () => - children - .filter((v) => !hiddenKeys.includes(v.hiddenKey ?? "")) - .map((ch) => { - let badge: Nullable<number> = null; - if (childValue && ch.name in childValue) { - badge = childValue[ch.name]; - } - return ( - <NavLink - key={ch.name} - activeClassName="sb-active" - className="list-group-item list-group-item-action sidebar-button sb-collapse" - to={ch.link} - onClick={toggleSidebar} - > - <DisplayItem - badge={badge === 0 ? undefined : badge ?? undefined} - name={ch.name} - ></DisplayItem> - </NavLink> - ); - }), - [children, hiddenKeys, childValue, toggleSidebar] - ); - - if (childrenElems.length === 0) { - return null; - } - - return ( - <div className={collapseBoxClass}> - <ListGroupItem - action - className="sidebar-button" - onClick={() => { - if (active) { - updateSidebar(""); - } else { - updateSidebar(name); - } - }} - > - <DisplayItem - badge={badgeValue === 0 ? undefined : badgeValue ?? undefined} - icon={icon} - name={name} - ></DisplayItem> - </ListGroupItem> - <Collapse in={active}> - <div className="sidebar-collapse">{childrenElems}</div> - </Collapse> - </div> - ); -}; - -interface DisplayProps { - name: string; - icon?: IconDefinition; - badge?: number; -} - -const DisplayItem: FunctionComponent<DisplayProps> = ({ - name, - icon, - badge, -}) => ( - <React.Fragment> - {icon && ( - <FontAwesomeIcon size="1x" className="icon" icon={icon}></FontAwesomeIcon> - )} - <span className="d-flex flex-grow-1 justify-content-between"> - {name} <Badge variant="secondary">{badge}</Badge> - </span> - </React.Fragment> -); diff --git a/frontend/src/Sidebar/list.ts b/frontend/src/Sidebar/list.ts deleted file mode 100644 index 9285f282e..000000000 --- a/frontend/src/Sidebar/list.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { - faClock, - faCogs, - faExclamationTriangle, - faFileExcel, - faFilm, - faLaptop, - faPlay, -} from "@fortawesome/free-solid-svg-icons"; -import { SidebarDefinition } from "./types"; - -export const SonarrDisabledKey = "sonarr-disabled"; -export const RadarrDisabledKey = "radarr-disabled"; - -export const SidebarList: SidebarDefinition[] = [ - { - icon: faPlay, - name: "Series", - link: "/series", - hiddenKey: SonarrDisabledKey, - }, - { - icon: faFilm, - name: "Movies", - link: "/movies", - hiddenKey: RadarrDisabledKey, - }, - { - icon: faClock, - name: "History", - children: [ - { - name: "Series", - link: "/history/series", - hiddenKey: SonarrDisabledKey, - }, - { - name: "Movies", - link: "/history/movies", - hiddenKey: RadarrDisabledKey, - }, - { - name: "Statistics", - link: "/history/stats", - }, - ], - }, - { - icon: faFileExcel, - name: "Blacklist", - children: [ - { - name: "Series", - link: "/blacklist/series", - hiddenKey: SonarrDisabledKey, - }, - { - name: "Movies", - link: "/blacklist/movies", - hiddenKey: RadarrDisabledKey, - }, - ], - }, - { - icon: faExclamationTriangle, - name: "Wanted", - children: [ - { - name: "Series", - link: "/wanted/series", - hiddenKey: SonarrDisabledKey, - }, - { - name: "Movies", - link: "/wanted/movies", - hiddenKey: RadarrDisabledKey, - }, - ], - }, - { - icon: faCogs, - name: "Settings", - children: [ - { - name: "General", - link: "/settings/general", - }, - { - name: "Languages", - link: "/settings/languages", - }, - { - name: "Providers", - link: "/settings/providers", - }, - { - name: "Subtitles", - link: "/settings/subtitles", - }, - { - name: "Sonarr", - link: "/settings/sonarr", - }, - { - name: "Radarr", - link: "/settings/radarr", - }, - { - name: "Notifications", - link: "/settings/notifications", - }, - { - name: "Scheduler", - link: "/settings/scheduler", - }, - { - name: "UI", - link: "/settings/ui", - }, - ], - }, - { - icon: faLaptop, - name: "System", - children: [ - { - name: "Tasks", - link: "/system/tasks", - }, - { - name: "Logs", - link: "/system/logs", - }, - { - name: "Providers", - link: "/system/providers", - }, - { - name: "Status", - link: "/system/status", - }, - { - name: "Releases", - link: "/system/releases", - }, - ], - }, -]; diff --git a/frontend/src/Sidebar/types.d.ts b/frontend/src/Sidebar/types.d.ts deleted file mode 100644 index 7b7d27a22..000000000 --- a/frontend/src/Sidebar/types.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IconDefinition } from "@fortawesome/fontawesome-common-types"; - -type SidebarDefinition = LinkItemType | CollapseItemType; - -type BaseSidebar = { - icon: IconDefinition; - name: string; - hiddenKey?: string; -}; - -type LinkItemType = BaseSidebar & { - link: string; -}; - -type CollapseItemType = BaseSidebar & { - children: { - name: string; - link: string; - hiddenKey?: string; - }[]; -}; - -type BadgeProvider = { - [parent: string]: ChildBadgeProvider | number; -}; - -type ChildBadgeProvider = { - [child: string]: number; -}; diff --git a/frontend/src/System/Releases/index.tsx b/frontend/src/System/Releases/index.tsx index 8df51c975..9c6f1cb93 100644 --- a/frontend/src/System/Releases/index.tsx +++ b/frontend/src/System/Releases/index.tsx @@ -7,7 +7,7 @@ import { BuildKey } from "../../utilities"; interface Props {} -const ReleasesView: FunctionComponent<Props> = () => { +const SystemReleasesView: FunctionComponent<Props> = () => { const releases = useSystemReleases(); return ( @@ -32,25 +32,6 @@ const ReleasesView: FunctionComponent<Props> = () => { </Row> </Container> ); - - // return ( - // <AsyncStateOverlay state={releases}> - // {({ data }) => ( - // <Container fluid className="px-5 py-4 bg-light"> - // <Helmet> - // <title>Releases - Bazarr (System)</title> - // </Helmet> - // <Row> - // {data.map((v, idx) => ( - // <Col xs={12} key={BuildKey(idx, v.date)}> - // <InfoElement {...v}></InfoElement> - // </Col> - // ))} - // </Row> - // </Container> - // )} - // </AsyncStateOverlay> - // ); }; const headerBadgeCls = "mr-2"; @@ -95,4 +76,4 @@ const InfoElement: FunctionComponent<ReleaseInfo> = ({ ); }; -export default ReleasesView; +export default SystemReleasesView; diff --git a/frontend/src/System/Router.tsx b/frontend/src/System/Router.tsx deleted file mode 100644 index 575cf228b..000000000 --- a/frontend/src/System/Router.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { FunctionComponent } from "react"; -import { Redirect, Route, Switch } from "react-router-dom"; -import { useSetSidebar } from "../@redux/hooks/site"; -import { RouterEmptyPath } from "../special-pages/404"; -import Logs from "./Logs"; -import Providers from "./Providers"; -import Releases from "./Releases"; -import Status from "./Status"; -import Tasks from "./Tasks"; - -const Router: FunctionComponent = () => { - useSetSidebar("System"); - return ( - <Switch> - <Route exact path="/system/tasks"> - <Tasks></Tasks> - </Route> - <Route exact path="/system/status"> - <Status></Status> - </Route> - <Route exact path="/system/providers"> - <Providers></Providers> - </Route> - <Route exact path="/system/logs"> - <Logs></Logs> - </Route> - <Route exact path="/system/releases"> - <Releases></Releases> - </Route> - <Route path="/system/*"> - <Redirect to={RouterEmptyPath}></Redirect> - </Route> - </Switch> - ); -}; - -export default Router; diff --git a/frontend/src/Wanted/Router.tsx b/frontend/src/Wanted/Router.tsx deleted file mode 100644 index 750c18ee3..000000000 --- a/frontend/src/Wanted/Router.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { FunctionComponent } from "react"; -import { Redirect, Route, Switch } from "react-router-dom"; -import { - useIsRadarrEnabled, - useIsSonarrEnabled, - useSetSidebar, -} from "../@redux/hooks/site"; -import { RouterEmptyPath } from "../special-pages/404"; -import Movies from "./Movies"; -import Series from "./Series"; - -const Router: FunctionComponent = () => { - const sonarr = useIsSonarrEnabled(); - const radarr = useIsRadarrEnabled(); - - useSetSidebar("Wanted"); - return ( - <Switch> - {sonarr && ( - <Route exact path="/wanted/series"> - <Series></Series> - </Route> - )} - {radarr && ( - <Route exact path="/wanted/movies"> - <Movies></Movies> - </Route> - )} - <Route path="/wanted/*"> - <Redirect to={RouterEmptyPath}></Redirect> - </Route> - </Switch> - ); -}; - -export default Router; |