Rewrite router and sidebar

This commit is contained in:
LASER-Yi 2021-09-01 00:08:47 +08:00
parent c11483ecc1
commit 3c790549e0
20 changed files with 594 additions and 815 deletions

View File

@ -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");

View File

@ -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]);
}

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

26
frontend/src/Navigation/nav.d.ts vendored Normal file
View File

@ -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;
}

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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 logo from "../@static/logo64.png";
import { SidebarToggleContext } from "../App";
import { useGotoHomepage } from "../utilities/hooks";
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, {
createContext,
FunctionComponent,
useContext,
useMemo,
useState,
} from "react";
import {
BadgesContext,
CollapseItem,
HiddenKeysContext,
LinkItem,
} from "./items";
import { RadarrDisabledKey, SidebarList, SonarrDisabledKey } from "./list";
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 { useNavigationItems } from "../Navigation";
import { Navigation } from "../Navigation/nav";
import { BuildKey } from "../utilities";
import { useGotoHomepage } from "../utilities/hooks";
import "./style.scss";
import { BadgeProvider } from "./types";
interface Props {
open?: boolean;
}
const SelectionContext = createContext<{
selection: string | null;
select: (selection: string | null) => void;
}>({ selection: null, select: () => {} });
const Sidebar: FunctionComponent<Props> = ({ open }) => {
const toggle = useContext(SidebarToggleContext);
const Sidebar: FunctionComponent = () => {
const open = useReduxStore((s) => s.site.showSidebar);
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 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>
);
};

View File

@ -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>
);

View File

@ -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",
},
],
},
];

View File

@ -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;
};

View File

@ -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;

View File

@ -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;

View File

@ -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;