mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
364 lines
10 KiB
TypeScript
364 lines
10 KiB
TypeScript
/*
|
|
* Copyright (c) 2021 - 2024, Ludvig Lundgren and the autobrr contributors.
|
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
import {
|
|
createRootRouteWithContext,
|
|
createRoute,
|
|
createRouter,
|
|
Navigate,
|
|
notFound,
|
|
Outlet,
|
|
redirect,
|
|
} from "@tanstack/react-router";
|
|
import { z } from "zod";
|
|
import { QueryClient } from "@tanstack/react-query";
|
|
|
|
import { Actions, Advanced, External, General, MoviesTv, Music } from "@screens/filters/sections";
|
|
import { APIClient } from "@api/APIClient";
|
|
import { Login, Onboarding } from "@screens/auth";
|
|
import ReleaseSettings from "@screens/settings/Releases";
|
|
import { NotFound } from "@components/alerts/NotFound";
|
|
import { FilterDetails, FilterNotFound, Filters } from "@screens/filters";
|
|
import { Settings } from "@screens/Settings";
|
|
import {
|
|
ApikeysQueryOptions,
|
|
ConfigQueryOptions,
|
|
DownloadClientsQueryOptions,
|
|
FeedsQueryOptions,
|
|
FilterByIdQueryOptions,
|
|
IndexersQueryOptions,
|
|
IrcQueryOptions,
|
|
NotificationsQueryOptions
|
|
} from "@api/queries";
|
|
import LogSettings from "@screens/settings/Logs";
|
|
import NotificationSettings from "@screens/settings/Notifications";
|
|
import ApplicationSettings from "@screens/settings/Application";
|
|
import { Logs } from "@screens/Logs";
|
|
import IrcSettings from "@screens/settings/Irc";
|
|
import { Header } from "@components/header";
|
|
import { RingResizeSpinner } from "@components/Icons";
|
|
import APISettings from "@screens/settings/Api";
|
|
import { Releases } from "@screens/Releases";
|
|
import IndexerSettings from "@screens/settings/Indexer";
|
|
import DownloadClientSettings from "@screens/settings/DownloadClient";
|
|
import FeedSettings from "@screens/settings/Feed";
|
|
import { Dashboard } from "@screens/Dashboard";
|
|
import AccountSettings from "@screens/settings/Account";
|
|
import { AuthContext, SettingsContext } from "@utils/Context";
|
|
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
|
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
import { queryClient } from "@api/QueryClient";
|
|
|
|
import { ErrorPage } from "@components/alerts";
|
|
|
|
const DashboardRoute = createRoute({
|
|
getParentRoute: () => AuthIndexRoute,
|
|
path: '/',
|
|
loader: () => {
|
|
// https://tanstack.com/router/v1/docs/guide/deferred-data-loading#deferred-data-loading-with-defer-and-await
|
|
// TODO load stats
|
|
|
|
// TODO load recent releases
|
|
|
|
return {}
|
|
},
|
|
component: Dashboard,
|
|
});
|
|
|
|
const FiltersRoute = createRoute({
|
|
getParentRoute: () => AuthIndexRoute,
|
|
path: 'filters'
|
|
});
|
|
|
|
const FilterIndexRoute = createRoute({
|
|
getParentRoute: () => FiltersRoute,
|
|
path: '/',
|
|
component: Filters,
|
|
});
|
|
|
|
export const FilterGetByIdRoute = createRoute({
|
|
getParentRoute: () => FiltersRoute,
|
|
path: '$filterId',
|
|
parseParams: (params) => ({
|
|
filterId: z.number().int().parse(Number(params.filterId)),
|
|
}),
|
|
stringifyParams: ({filterId}) => ({filterId: `${filterId}`}),
|
|
loader: async ({context, params}) => {
|
|
try {
|
|
const filter = await context.queryClient.ensureQueryData(FilterByIdQueryOptions(params.filterId))
|
|
return { filter }
|
|
} catch (e) {
|
|
throw notFound()
|
|
}
|
|
},
|
|
component: FilterDetails,
|
|
notFoundComponent: () => {
|
|
return <FilterNotFound />
|
|
},
|
|
});
|
|
|
|
export const FilterGeneralRoute = createRoute({
|
|
getParentRoute: () => FilterGetByIdRoute,
|
|
path: '/',
|
|
component: General
|
|
});
|
|
|
|
export const FilterMoviesTvRoute = createRoute({
|
|
getParentRoute: () => FilterGetByIdRoute,
|
|
path: 'movies-tv',
|
|
component: MoviesTv
|
|
});
|
|
|
|
export const FilterMusicRoute = createRoute({
|
|
getParentRoute: () => FilterGetByIdRoute,
|
|
path: 'music',
|
|
component: Music
|
|
});
|
|
|
|
export const FilterAdvancedRoute = createRoute({
|
|
getParentRoute: () => FilterGetByIdRoute,
|
|
path: 'advanced',
|
|
component: Advanced
|
|
});
|
|
|
|
export const FilterExternalRoute = createRoute({
|
|
getParentRoute: () => FilterGetByIdRoute,
|
|
path: 'external',
|
|
component: External
|
|
});
|
|
|
|
export const FilterActionsRoute = createRoute({
|
|
getParentRoute: () => FilterGetByIdRoute,
|
|
path: 'actions',
|
|
component: Actions
|
|
});
|
|
|
|
export const ReleasesRoute = createRoute({
|
|
getParentRoute: () => AuthIndexRoute,
|
|
path: 'releases',
|
|
component: Releases,
|
|
validateSearch: (search) => z.object({
|
|
offset: z.number().optional(),
|
|
limit: z.number().optional(),
|
|
filter: z.string().optional(),
|
|
q: z.string().optional(),
|
|
action_status: z.enum(['PUSH_APPROVED', 'PUSH_REJECTED', 'PUSH_ERROR', '']).optional(),
|
|
// filters: z.array().catch(''),
|
|
// sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
|
|
}).parse(search),
|
|
});
|
|
|
|
export const SettingsRoute = createRoute({
|
|
getParentRoute: () => AuthIndexRoute,
|
|
path: 'settings',
|
|
pendingMs: 3000,
|
|
component: Settings
|
|
});
|
|
|
|
export const SettingsIndexRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: '/',
|
|
component: ApplicationSettings
|
|
});
|
|
|
|
export const SettingsLogRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: 'logs',
|
|
loader: (opts) => opts.context.queryClient.ensureQueryData(ConfigQueryOptions()),
|
|
component: LogSettings
|
|
});
|
|
|
|
export const SettingsIndexersRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: 'indexers',
|
|
loader: (opts) => opts.context.queryClient.ensureQueryData(IndexersQueryOptions()),
|
|
component: IndexerSettings
|
|
});
|
|
|
|
export const SettingsIrcRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: 'irc',
|
|
loader: (opts) => opts.context.queryClient.ensureQueryData(IrcQueryOptions()),
|
|
component: IrcSettings
|
|
});
|
|
|
|
export const SettingsFeedsRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: 'feeds',
|
|
loader: (opts) => opts.context.queryClient.ensureQueryData(FeedsQueryOptions()),
|
|
component: FeedSettings
|
|
});
|
|
|
|
export const SettingsClientsRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: 'clients',
|
|
loader: (opts) => opts.context.queryClient.ensureQueryData(DownloadClientsQueryOptions()),
|
|
component: DownloadClientSettings
|
|
});
|
|
|
|
export const SettingsNotificationsRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: 'notifications',
|
|
loader: (opts) => opts.context.queryClient.ensureQueryData(NotificationsQueryOptions()),
|
|
component: NotificationSettings
|
|
});
|
|
|
|
export const SettingsApiRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: 'api',
|
|
loader: (opts) => opts.context.queryClient.ensureQueryData(ApikeysQueryOptions()),
|
|
component: APISettings
|
|
});
|
|
|
|
export const SettingsReleasesRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: 'releases',
|
|
component: ReleaseSettings
|
|
});
|
|
|
|
export const SettingsAccountRoute = createRoute({
|
|
getParentRoute: () => SettingsRoute,
|
|
path: 'account',
|
|
component: AccountSettings
|
|
});
|
|
|
|
export const LogsRoute = createRoute({
|
|
getParentRoute: () => AuthIndexRoute,
|
|
path: 'logs',
|
|
component: Logs
|
|
});
|
|
|
|
export const OnboardRoute = createRoute({
|
|
getParentRoute: () => RootRoute,
|
|
path: 'onboard',
|
|
beforeLoad: async () => {
|
|
// Check if onboarding is available for this instance
|
|
// and redirect if needed
|
|
try {
|
|
await APIClient.auth.canOnboard()
|
|
} catch (e) {
|
|
console.error("onboarding not available, redirect to login")
|
|
|
|
throw redirect({
|
|
to: LoginRoute.to,
|
|
})
|
|
}
|
|
},
|
|
component: Onboarding
|
|
});
|
|
|
|
export const LoginRoute = createRoute({
|
|
getParentRoute: () => RootRoute,
|
|
path: 'login',
|
|
validateSearch: z.object({
|
|
redirect: z.string().optional(),
|
|
}),
|
|
beforeLoad: ({ navigate }) => {
|
|
// handle canOnboard
|
|
APIClient.auth.canOnboard().then(() => {
|
|
console.info("onboarding available, redirecting")
|
|
|
|
navigate({ to: OnboardRoute.to })
|
|
}).catch(() => {
|
|
console.info("onboarding not available, please login")
|
|
})
|
|
},
|
|
}).update({component: Login});
|
|
|
|
export const AuthRoute = createRoute({
|
|
getParentRoute: () => RootRoute,
|
|
id: 'auth',
|
|
// Before loading, authenticate the user via our auth context
|
|
// This will also happen during prefetching (e.g. hovering over links, etc.)
|
|
beforeLoad: ({ context, location }) => {
|
|
// If the user is not logged in, check for item in localStorage
|
|
if (!AuthContext.get().isLoggedIn) {
|
|
throw redirect({
|
|
to: LoginRoute.to,
|
|
search: {
|
|
// Use the current location to power a redirect after login
|
|
// (Do not use `router.state.resolvedLocation` as it can
|
|
// potentially lag behind the actual current location)
|
|
redirect: location.href,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Otherwise, return the user in context
|
|
return context;
|
|
},
|
|
})
|
|
|
|
function AuthenticatedLayout() {
|
|
const isLoggedIn = AuthContext.useSelector((s) => s.isLoggedIn);
|
|
if (!isLoggedIn) {
|
|
const redirect = (
|
|
location.pathname.length > 1
|
|
? { redirect: location.pathname }
|
|
: undefined
|
|
);
|
|
return <Navigate to="/login" search={redirect} />;
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col min-h-screen">
|
|
<Header/>
|
|
<Outlet/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const AuthIndexRoute = createRoute({
|
|
getParentRoute: () => AuthRoute,
|
|
component: AuthenticatedLayout,
|
|
id: 'authenticated-routes',
|
|
});
|
|
|
|
export const RootComponent = () => {
|
|
const settings = SettingsContext.useValue();
|
|
return (
|
|
<div className="flex flex-col min-h-screen">
|
|
<Outlet/>
|
|
{settings.debug ? (
|
|
<>
|
|
<TanStackRouterDevtools/>
|
|
<ReactQueryDevtools initialIsOpen={false}/>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const RootRoute = createRootRouteWithContext<{
|
|
queryClient: QueryClient
|
|
}>()({
|
|
component: RootComponent,
|
|
notFoundComponent: NotFound,
|
|
});
|
|
|
|
const filterRouteTree = FiltersRoute.addChildren([FilterIndexRoute, FilterGetByIdRoute.addChildren([FilterGeneralRoute, FilterMoviesTvRoute, FilterMusicRoute, FilterAdvancedRoute, FilterExternalRoute, FilterActionsRoute])])
|
|
const settingsRouteTree = SettingsRoute.addChildren([SettingsIndexRoute, SettingsLogRoute, SettingsIndexersRoute, SettingsIrcRoute, SettingsFeedsRoute, SettingsClientsRoute, SettingsNotificationsRoute, SettingsApiRoute, SettingsReleasesRoute, SettingsAccountRoute])
|
|
const authenticatedTree = AuthRoute.addChildren([AuthIndexRoute.addChildren([DashboardRoute, filterRouteTree, ReleasesRoute, settingsRouteTree, LogsRoute])])
|
|
const routeTree = RootRoute.addChildren([
|
|
authenticatedTree,
|
|
LoginRoute,
|
|
OnboardRoute
|
|
]);
|
|
|
|
export const Router = createRouter({
|
|
routeTree,
|
|
defaultPendingComponent: () => (
|
|
<div className="flex flex-grow items-center justify-center col-span-9">
|
|
<RingResizeSpinner className="text-blue-500 size-24"/>
|
|
</div>
|
|
),
|
|
defaultErrorComponent: (ctx) => (
|
|
<ErrorPage error={ctx.error} reset={ctx.reset} />
|
|
),
|
|
context: {
|
|
queryClient
|
|
},
|
|
});
|
|
|