summaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/@redux/actions/site.ts4
-rw-r--r--frontend/src/@redux/hooks/site.ts11
-rw-r--r--frontend/src/@redux/reducers/site.ts10
-rw-r--r--frontend/src/App/Header.tsx15
-rw-r--r--frontend/src/App/Router.tsx71
-rw-r--r--frontend/src/App/index.tsx33
-rw-r--r--frontend/src/Blacklist/Router.tsx36
-rw-r--r--frontend/src/DisplayItem/Router.tsx45
-rw-r--r--frontend/src/History/Router.tsx40
-rw-r--r--frontend/src/Navigation/index.ts238
-rw-r--r--frontend/src/Navigation/nav.d.ts26
-rw-r--r--frontend/src/Router/index.tsx83
-rw-r--r--frontend/src/Settings/Router.tsx58
-rw-r--r--frontend/src/Sidebar/index.tsx285
-rw-r--r--frontend/src/Sidebar/items.tsx179
-rw-r--r--frontend/src/Sidebar/list.ts148
-rw-r--r--frontend/src/Sidebar/types.d.ts29
-rw-r--r--frontend/src/System/Releases/index.tsx23
-rw-r--r--frontend/src/System/Router.tsx37
-rw-r--r--frontend/src/Wanted/Router.tsx36
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;