diff --git a/web/src/components/Breadcrumbs.tsx b/web/src/components/Breadcrumbs.tsx
index 3e18f03e8..d0388aacf 100644
--- a/web/src/components/Breadcrumbs.tsx
+++ b/web/src/components/Breadcrumbs.tsx
@@ -31,7 +31,7 @@ export default function Breadcrumbs(props: BreadcrumbsProps) {
// Don't display crumbs for pages that aren't excplicely defined in useRouteNames hook
return isLast ? (
- {getRouteName(to) ?? 'null'}
+ {getRouteName(to) ?? ''}
) : getRouteName(to) ? (
diff --git a/web/src/components/channel_config/CustomShowProgrammingSelector.tsx b/web/src/components/channel_config/CustomShowProgrammingSelector.tsx
new file mode 100644
index 000000000..bb3ca6de9
--- /dev/null
+++ b/web/src/components/channel_config/CustomShowProgrammingSelector.tsx
@@ -0,0 +1,3 @@
+export function CustomShowProgrammingSelector() {
+ return
;
+}
diff --git a/web/src/components/channel_config/PlexProgrammingSelector.tsx b/web/src/components/channel_config/PlexProgrammingSelector.tsx
new file mode 100644
index 000000000..705d94b26
--- /dev/null
+++ b/web/src/components/channel_config/PlexProgrammingSelector.tsx
@@ -0,0 +1,350 @@
+import { ExpandLess, ExpandMore } from '@mui/icons-material';
+import Clear from '@mui/icons-material/Clear';
+import GridView from '@mui/icons-material/GridView';
+import Search from '@mui/icons-material/Search';
+import ViewList from '@mui/icons-material/ViewList';
+import {
+ Box,
+ Collapse,
+ Divider,
+ Grow,
+ IconButton,
+ InputAdornment,
+ LinearProgress,
+ List,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ Stack,
+ TextField,
+ ToggleButton,
+ ToggleButtonGroup,
+ Typography,
+} from '@mui/material';
+import { DataTag, useInfiniteQuery, useQuery } from '@tanstack/react-query';
+import {
+ PlexLibraryCollections,
+ PlexLibraryMovies,
+ PlexLibraryShows,
+ PlexMovie,
+ PlexTvShow,
+} from '@tunarr/types/plex';
+import { chain, first, isEmpty, isNil, isUndefined, map } from 'lodash-es';
+import { Fragment, useCallback, useEffect, useState } from 'react';
+import { useIntersectionObserver } from 'usehooks-ts';
+import { toggle } from '../../helpers/util';
+import {
+ EnrichedPlexMedia,
+ fetchPlexPath,
+ usePlex,
+} from '../../hooks/plexHooks';
+import { usePlexServerSettings } from '../../hooks/settingsHooks';
+import useDebouncedState from '../../hooks/useDebouncedState';
+import useStore from '../../store';
+import { addKnownMediaForServer } from '../../store/programmingSelector/actions';
+import { setProgrammingSelectorViewState } from '../../store/themeEditor/actions';
+import ConnectPlex from '../settings/ConnectPlex';
+import { PlexGridItem } from './PlexGridItem';
+import { PlexListItem } from './PlexListItem';
+import SelectedProgrammingList from './SelectedProgrammingList';
+
+type ViewType = 'list' | 'grid';
+
+type Props = {
+ onAddSelectedMedia: (items: EnrichedPlexMedia[]) => void;
+};
+
+export default function PlexProgrammingSelector({ onAddSelectedMedia }: Props) {
+ const { data: plexServers } = usePlexServerSettings();
+ const selectedServer = useStore((s) => s.currentServer);
+ const selectedLibrary = useStore((s) =>
+ s.currentLibrary?.type === 'plex' ? s.currentLibrary : null,
+ );
+ const viewType = useStore((state) => state.theme.programmingSelectorView);
+
+ const [searchBarOpen, setSearchBarOpen] = useState(false);
+ const [search, debounceSearch, setSearch] = useDebouncedState('', 300);
+ const [collectionsOpen, setCollectionsOpen] = useState(false);
+ const [scrollParams, setScrollParams] = useState({ limit: 0, max: -1 });
+
+ const { data: directoryChildren } = usePlex(
+ selectedServer?.name ?? '',
+ '/library/sections',
+ !isUndefined(selectedServer),
+ );
+
+ const clearSearchInput = useCallback(() => {
+ setSearch('');
+ setSearchBarOpen(false);
+ }, [setSearch]);
+
+ const setViewType = (view: ViewType) => {
+ setProgrammingSelectorViewState(view);
+ };
+
+ const handleFormat = (
+ _event: React.MouseEvent,
+ newFormats: ViewType,
+ ) => {
+ setViewType(newFormats);
+ };
+
+ const { data: collectionsData } = useQuery({
+ queryKey: [
+ 'plex',
+ selectedServer?.name,
+ selectedLibrary?.library.key,
+ 'collections',
+ ],
+ queryFn: () => {
+ return fetchPlexPath(
+ selectedServer!.name,
+ `/library/sections/${selectedLibrary?.library.key}/collections?`,
+ )();
+ },
+ enabled: !isNil(selectedServer) && !isNil(selectedLibrary),
+ });
+
+ const { isLoading: searchLoading, data: searchData } = useInfiniteQuery({
+ queryKey: [
+ 'plex-search',
+ selectedServer?.name,
+ selectedLibrary?.library.key,
+ debounceSearch,
+ ] as DataTag<
+ ['plex-search', string, string, string],
+ PlexLibraryMovies | PlexLibraryShows
+ >,
+ enabled: !isNil(selectedServer) && !isNil(selectedLibrary),
+ initialPageParam: 0,
+ queryFn: ({ pageParam }) => {
+ const plexQuery = new URLSearchParams({
+ 'Plex-Container-Start': pageParam.toString(),
+ 'Plex-Container-Size': '10',
+ });
+
+ if (!isNil(debounceSearch) && !isEmpty(debounceSearch)) {
+ plexQuery.set('title<', debounceSearch);
+ }
+
+ return fetchPlexPath(
+ selectedServer!.name,
+ `/library/sections/${
+ selectedLibrary!.library.key
+ }/all?${plexQuery.toString()}`,
+ )();
+ },
+ getNextPageParam: (res, _, last) => {
+ if (res.size < 10) {
+ return null;
+ }
+
+ return last + 10;
+ },
+ });
+
+ useEffect(() => {
+ if (searchData) {
+ // We're using this as an analogue for detecting the start of a new 'query'
+ if (searchData.pages.length === 1) {
+ setScrollParams({
+ limit: 16,
+ max: first(searchData.pages)!.size,
+ });
+ }
+
+ // We probably wouldn't have made it this far if we didnt have a server, but
+ // putting this here to prevent crashes
+ if (selectedServer) {
+ const allMedia = chain(searchData.pages)
+ .reject((page) => page.size === 0)
+ .map((page) => page.Metadata)
+ .flatten()
+ .value();
+ addKnownMediaForServer(selectedServer.name, allMedia);
+ }
+ }
+ }, [selectedServer, searchData, setScrollParams]);
+
+ const { ref } = useIntersectionObserver({
+ onChange: (_, entry) => {
+ if (entry.isIntersecting && scrollParams.limit < scrollParams.max) {
+ setScrollParams(({ limit: prevLimit, max }) => ({
+ max,
+ limit: prevLimit + 10,
+ }));
+ }
+ },
+ threshold: 0.5,
+ });
+
+ const renderListItems = () => {
+ const elements: JSX.Element[] = [];
+
+ if (collectionsData && collectionsData.size > 0) {
+ elements.push(
+
+ setCollectionsOpen(toggle)}
+ dense
+ sx={
+ viewType === 'grid' ? { display: 'block', width: '100%' } : null
+ }
+ >
+
+ {collectionsOpen ? : }
+
+
+
+
+ {map(collectionsData.Metadata, (item) =>
+ viewType === 'list' ? (
+
+ ) : (
+
+ ),
+ )}
+
+ ,
+ );
+ }
+
+ if (searchData) {
+ const items = chain(searchData.pages)
+ .reject((page) => page.size === 0)
+ .map((page) => page.Metadata)
+ .flatten()
+ .take(scrollParams.limit)
+ .value();
+ elements.push(
+ ...map(items, (item: PlexMovie | PlexTvShow) => {
+ return viewType === 'list' ? (
+
+ ) : (
+
+ );
+ }),
+ );
+ }
+ return elements;
+ };
+
+ return (
+ <>
+ {!isNil(directoryChildren) &&
+ directoryChildren.size > 0 &&
+ selectedLibrary && (
+ <>
+
+
+ setSearch(e.target.value)}
+ key={'searchbar'}
+ sx={{ m: 0 }}
+ InputProps={{
+ endAdornment: (
+
+ e.preventDefault()}
+ edge="end"
+ >
+
+
+
+ ),
+ sx: { height: '48px' },
+ }}
+ />
+
+ {!searchBarOpen && (
+ setSearchBarOpen(toggle)}
+ >
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ {plexServers?.length === 0 ? (
+
+
+
+ ) : (
+ <>
+
+
+ {renderListItems()}
+
+
+
+
+ Selected Items
+
+ >
+ )}
+ >
+ );
+}
diff --git a/web/src/components/channel_config/ProgrammingSelector.tsx b/web/src/components/channel_config/ProgrammingSelector.tsx
index 137aa7640..12e96a60b 100644
--- a/web/src/components/channel_config/ProgrammingSelector.tsx
+++ b/web/src/components/channel_config/ProgrammingSelector.tsx
@@ -1,65 +1,24 @@
import {
- Clear,
- ExpandLess,
- ExpandMore,
- GridView,
- Search,
- ViewList,
-} from '@mui/icons-material';
-import {
- Box,
- Collapse,
- Divider,
FormControl,
- Grow,
- IconButton,
- InputAdornment,
InputLabel,
- LinearProgress,
- List,
- ListItemButton,
- ListItemIcon,
- ListItemText,
MenuItem,
Select,
Stack,
- TextField,
- ToggleButton,
- ToggleButtonGroup,
- Typography,
} from '@mui/material';
-import { DataTag, useInfiniteQuery, useQuery } from '@tanstack/react-query';
-import {
- PlexLibraryCollections,
- PlexLibraryMovies,
- PlexLibraryShows,
- PlexMedia,
- PlexMovie,
- PlexTvShow,
- isPlexDirectory,
-} from '@tunarr/types/plex';
-import { chain, first, isEmpty, isNil, isUndefined, map } from 'lodash-es';
-import React, { Fragment, useCallback, useEffect, useState } from 'react';
-import { useIntersectionObserver } from 'usehooks-ts';
-import { toggle } from '../../helpers/util.ts';
-import {
- EnrichedPlexMedia,
- fetchPlexPath,
- usePlex,
-} from '../../hooks/plexHooks.ts';
+import { PlexMedia, isPlexDirectory } from '@tunarr/types/plex';
+import { find, isEmpty, isNil, isUndefined } from 'lodash-es';
+import React, { useCallback, useEffect, useState } from 'react';
+import { EnrichedPlexMedia, usePlex } from '../../hooks/plexHooks.ts';
import { usePlexServerSettings } from '../../hooks/settingsHooks.ts';
-import useDebouncedState from '../../hooks/useDebouncedState.ts';
+import { useCustomShows } from '../../hooks/useCustomShows.ts';
import useStore from '../../store/index.ts';
import {
addKnownMediaForServer,
setProgrammingListLibrary,
setProgrammingListingServer,
} from '../../store/programmingSelector/actions.ts';
-import { setProgrammingSelectorViewState } from '../../store/themeEditor/actions.ts';
-import ConnectPlex from '../settings/ConnectPlex.tsx';
-import { PlexGridItem } from './PlexGridItem.tsx';
-import { PlexListItem } from './PlexListItem.tsx';
-import SelectedProgrammingList from './SelectedProgrammingList.tsx';
+import { CustomShowProgrammingSelector } from './CustomShowProgrammingSelector.tsx';
+import PlexProgrammingSelector from './PlexProgrammingSelector.tsx';
export interface PlexListItemProps {
item: T;
@@ -69,8 +28,6 @@ export interface PlexListItemProps {
parent?: string;
}
-type ViewType = 'list' | 'grid';
-
type Props = {
onAddSelectedMedia: (items: EnrichedPlexMedia[]) => void;
};
@@ -80,24 +37,26 @@ export default function ProgrammingSelector({ onAddSelectedMedia }: Props) {
const selectedServer = useStore((s) => s.currentServer);
const selectedLibrary = useStore((s) => s.currentLibrary);
const knownMedia = useStore((s) => s.knownMediaByServer);
- const [collectionsOpen, setCollectionsOpen] = useState(false);
- const viewType = useStore((state) => state.theme.programmingSelectorView);
- const [searchBarOpen, setSearchBarOpen] = useState(false);
-
- const setViewType = (view: ViewType) => {
- setProgrammingSelectorViewState(view);
- };
-
- const handleFormat = (
- _event: React.MouseEvent,
- newFormats: ViewType,
- ) => {
- setViewType(newFormats);
- };
-
- const handleSearchOpen = () => {
- setSearchBarOpen(!searchBarOpen);
- };
+ const [mediaSource, setMediaSource] = useState(selectedServer?.name);
+
+ // Convenience sub-selectors for specific library types
+ const selectedPlexLibrary =
+ selectedLibrary?.type === 'plex' ? selectedLibrary.library : undefined;
+ const selectedCustomShow =
+ selectedLibrary?.type === 'custom-show'
+ ? selectedLibrary.library
+ : undefined;
+
+ const viewingCustomShows = mediaSource === 'custom-shows';
+
+ /**
+ * Load Plex libraries
+ */
+ const { data: plexLibraryChildren } = usePlex(
+ selectedServer?.name ?? '',
+ '/library/sections',
+ !isUndefined(selectedServer),
+ );
useEffect(() => {
const server =
@@ -108,107 +67,56 @@ export default function ProgrammingSelector({ onAddSelectedMedia }: Props) {
setProgrammingListingServer(server);
}, [plexServers]);
- const [scrollParams, setScrollParams] = useState({ limit: 0, max: -1 });
- const [search, debounceSearch, setSearch] = useDebouncedState('', 300);
-
- const { data: directoryChildren } = usePlex(
- selectedServer?.name ?? '',
- '/library/sections',
- !isUndefined(selectedServer),
- );
-
useEffect(() => {
- if (directoryChildren) {
- if (directoryChildren.size > 0) {
- setProgrammingListLibrary(directoryChildren.Directory[0]);
+ if (selectedServer && plexLibraryChildren) {
+ if (plexLibraryChildren.size > 0) {
+ setProgrammingListLibrary({
+ type: 'plex',
+ library: plexLibraryChildren.Directory[0],
+ });
}
addKnownMediaForServer(selectedServer!.name, [
- ...directoryChildren.Directory,
+ ...plexLibraryChildren.Directory,
]);
}
- setCollectionsOpen(false);
- }, [selectedServer, directoryChildren]);
-
- const { isLoading: searchLoading, data: searchData } = useInfiniteQuery({
- queryKey: [
- 'plex-search',
- selectedServer?.name,
- selectedLibrary?.key,
- debounceSearch,
- ] as DataTag<
- ['plex-search', string, string, string],
- PlexLibraryMovies | PlexLibraryShows
- >,
- enabled: !isNil(selectedServer) && !isNil(selectedLibrary),
- initialPageParam: 0,
- queryFn: ({ pageParam }) => {
- const plexQuery = new URLSearchParams({
- 'Plex-Container-Start': pageParam.toString(),
- 'Plex-Container-Size': '10',
- });
-
- if (!isNil(debounceSearch) && !isEmpty(debounceSearch)) {
- plexQuery.set('title<', debounceSearch);
- }
-
- return fetchPlexPath(
- selectedServer!.name,
- `/library/sections/${selectedLibrary!.key}/all?${plexQuery.toString()}`,
- )();
- },
- getNextPageParam: (res, _, last) => {
- if (res.size < 10) {
- return null;
- }
+ }, [selectedServer, plexLibraryChildren]);
- return last + 10;
- },
- });
-
- const { data: collectionsData } = useQuery({
- queryKey: [
- 'plex',
- selectedServer?.name,
- selectedLibrary?.key,
- 'collections',
- ],
- queryFn: () => {
- return fetchPlexPath(
- selectedServer!.name,
- `/library/sections/${selectedLibrary!.key}/collections?`,
- )();
- },
- enabled: !isNil(selectedServer) && !isNil(selectedLibrary),
+ /**
+ * Load custom shows
+ */
+ const { data: customShows } = useCustomShows([], {
+ enabled: viewingCustomShows,
});
useEffect(() => {
- if (searchData) {
- // We're using this as an analogue for detecting the start of a new 'query'
- if (searchData.pages.length === 1) {
- setScrollParams({
- limit: 16,
- max: first(searchData.pages)!.size,
- });
- }
-
- // We probably wouldn't have made it this far if we didnt have a server, but
- // putting this here to prevent crashes
- if (selectedServer) {
- const allMedia = chain(searchData.pages)
- .reject((page) => page.size === 0)
- .map((page) => page.Metadata)
- .flatten()
- .value();
- addKnownMediaForServer(selectedServer.name, allMedia);
- }
- }
- }, [selectedServer, searchData, setScrollParams]);
-
- useEffect(() => {
- if (selectedServer?.name && collectionsData && collectionsData.Metadata) {
- addKnownMediaForServer(selectedServer.name, collectionsData.Metadata);
+ if (
+ mediaSource === 'custom-shows' &&
+ customShows &&
+ customShows.length > 0
+ ) {
+ setProgrammingListLibrary({
+ type: 'custom-show',
+ library: customShows[0],
+ });
}
- }, [selectedServer?.name, collectionsData]);
+ }, [mediaSource, customShows]);
+
+ const onMediaSourceChange = useCallback(
+ (mediaSource: string) => {
+ if (mediaSource === 'custom-shows') {
+ // Not dealing with a server
+ setProgrammingListingServer(undefined);
+ setMediaSource(mediaSource);
+ } else {
+ const server = find(plexServers, { name: mediaSource });
+ if (server) {
+ setProgrammingListingServer(server);
+ setMediaSource(server.name);
+ }
+ }
+ },
+ [plexServers],
+ );
const onLibraryChange = useCallback(
(libraryUuid: string) => {
@@ -217,88 +125,23 @@ export default function ProgrammingSelector({ onAddSelectedMedia }: Props) {
const known = knownMedia[selectedServer.name] ?? {};
const library = known[libraryUuid];
if (library && isPlexDirectory(library)) {
- setProgrammingListLibrary(library);
+ setProgrammingListLibrary({ type: 'plex', library });
}
}
},
[knownMedia, selectedServer],
);
- const { ref } = useIntersectionObserver({
- onChange: (_, entry) => {
- if (entry.isIntersecting && scrollParams.limit < scrollParams.max) {
- setScrollParams(({ limit: prevLimit, max }) => ({
- max,
- limit: prevLimit + 10,
- }));
- }
- },
- threshold: 0.5,
- });
-
- const clearSearchInput = useCallback(() => {
- setSearch('');
- setSearchBarOpen(false);
- }, [setSearch]);
-
- const renderListItems = () => {
- const elements: JSX.Element[] = [];
-
- if (collectionsData && collectionsData.size > 0) {
- elements.push(
-
- setCollectionsOpen(toggle)}
- dense
- sx={
- viewType === 'grid' ? { display: 'block', width: '100%' } : null
- }
- >
-
- {collectionsOpen ? : }
-
-
-
-
- {map(collectionsData.Metadata, (item) =>
- viewType === 'list' ? (
-
- ) : (
-
- ),
- )}
-
- ,
+ const renderMediaSourcePrograms = () => {
+ if (selectedLibrary?.type === 'custom-show') {
+ return ;
+ } else if (selectedLibrary?.type === 'plex') {
+ return (
+
);
}
- if (searchData) {
- const items = chain(searchData.pages)
- .reject((page) => page.size === 0)
- .map((page) => page.Metadata)
- .flatten()
- .take(scrollParams.limit)
- .value();
- elements.push(
- ...map(items, (item: PlexMovie | PlexTvShow) => {
- return viewType === 'list' ? (
-
- ) : (
-
- );
- }),
- );
- }
- return elements;
+ return null;
};
return (
@@ -312,10 +155,16 @@ export default function ProgrammingSelector({ onAddSelectedMedia }: Props) {
flexGrow: 1,
}}
>
- {selectedServer && (
+ {plexServers && (
Media Source
-
)}
- {!isNil(directoryChildren) &&
- directoryChildren.size > 0 &&
- selectedLibrary && (
+ {!isNil(plexLibraryChildren) &&
+ plexLibraryChildren.size > 0 &&
+ selectedPlexLibrary && (
Library
onLibraryChange(e.target.value)}
>
- {directoryChildren.Directory.map((dir) => (
+ {plexLibraryChildren.Directory.map((dir) => (
))}
-
)}
-
- {!isNil(directoryChildren) &&
- directoryChildren.size > 0 &&
- selectedLibrary && (
- <>
-
+ Custom Show
+ onLibraryChange(e.target.value)}
>
-
- setSearch(e.target.value)}
- key={'searchbar'}
- sx={{ m: 0 }}
- InputProps={{
- endAdornment: (
-
- e.preventDefault()}
- edge="end"
- >
-
-
-
- ),
- sx: { height: '48px' },
- }}
- />
-
- {!searchBarOpen && (
- handleSearchOpen()}
- >
-
-
- )}
-
-
-
-
-
-
-
-
-
- >
+ {customShows.map((cs) => (
+
+ ))}
+
+
)}
- {plexServers?.length === 0 ? (
-
-
-
- ) : (
- <>
-
-
- {renderListItems()}
-
-
-
-
- Selected Items
-
- >
- )}
+
+ {renderMediaSourcePrograms()}
>
);
}
diff --git a/web/src/hooks/useCustomShows.ts b/web/src/hooks/useCustomShows.ts
index 6cbe4407c..59c35f357 100644
--- a/web/src/hooks/useCustomShows.ts
+++ b/web/src/hooks/useCustomShows.ts
@@ -1,18 +1,38 @@
-import { DataTag, useQuery } from '@tanstack/react-query';
+import {
+ DataTag,
+ DefinedInitialDataOptions,
+ useQuery,
+} from '@tanstack/react-query';
import { CustomShow } from '@tunarr/types';
import { apiClient } from '../external/api.ts';
import { ZodiosAliasReturnType } from '../types/index.ts';
import { makeQueryOptionsInitialData } from './useQueryHelpers.ts';
-export const customShowsQuery = (initialData: CustomShow[] = []) =>
+export type CustomShowsQueryOpts = Omit<
+ DefinedInitialDataOptions<
+ CustomShow[],
+ Error,
+ CustomShow[],
+ DataTag<['custom-shows'], CustomShow[]>
+ >,
+ 'queryKey' | 'queryFn' | 'initialData'
+>;
+
+export const customShowsQuery = (
+ initialData: CustomShow[] = [],
+ opts?: CustomShowsQueryOpts,
+) =>
makeQueryOptionsInitialData(
['custom-shows'],
() => apiClient.getCustomShows(),
initialData,
+ opts ?? {},
);
-export const useCustomShows = (initialData: CustomShow[] = []) =>
- useQuery(customShowsQuery(initialData));
+export const useCustomShows = (
+ initialData: CustomShow[] = [],
+ opts?: CustomShowsQueryOpts,
+) => useQuery(customShowsQuery(initialData, opts ?? {}));
export const customShowQuery = (id: string) => ({
queryKey: ['custom-shows', id] as DataTag<
diff --git a/web/src/hooks/useQueryHelpers.ts b/web/src/hooks/useQueryHelpers.ts
index f0156373f..c515dbc0e 100644
--- a/web/src/hooks/useQueryHelpers.ts
+++ b/web/src/hooks/useQueryHelpers.ts
@@ -4,6 +4,7 @@ import {
QueryFunction,
UseQueryOptions,
} from '@tanstack/react-query';
+import { NonUndefinedGuard } from '../types';
export function makeQueryOptions<
K extends readonly unknown[],
@@ -25,8 +26,6 @@ export function makeQueryOptions<
};
}
-type NonUndefinedGuard = T extends undefined ? never : T;
-
export function makeQueryOptionsInitialData<
K extends readonly unknown[],
Fn extends QueryFunction,
diff --git a/web/src/hooks/useRouteName.ts b/web/src/hooks/useRouteName.ts
index 902da2cda..204f03fb1 100644
--- a/web/src/hooks/useRouteName.ts
+++ b/web/src/hooks/useRouteName.ts
@@ -2,6 +2,18 @@ import { find, memoize } from 'lodash-es';
type Route = { matcher: RegExp; name: string };
+const uuidRegexPattern =
+ '[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}';
+
+const entityPageMatcher = (entity: string, path: string) =>
+ new RegExp(`^\/${entity}\/${uuidRegexPattern}\/${path}\/?$`);
+
+const channelsPageMatcher = (path: string) =>
+ entityPageMatcher('channels', path);
+
+const customShowsPageMatcher = (path: string) =>
+ entityPageMatcher('library/custom-shows', path);
+
const namedRoutes: Route[] = [
{
matcher: /^\/channels$/g,
@@ -12,33 +24,31 @@ const namedRoutes: Route[] = [
name: 'New',
},
{
- matcher:
- /^\/channels\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\/watch$/g,
+ matcher: channelsPageMatcher('watch'),
name: 'Watch',
},
{
- matcher:
- /^\/channels\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\/edit$/g,
+ matcher: channelsPageMatcher('edit'),
+ name: 'Edit',
+ },
+ {
+ matcher: customShowsPageMatcher('edit'),
name: 'Edit',
},
{
- matcher:
- /^\/channels\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\/programming$/g,
+ matcher: channelsPageMatcher('programming'),
name: 'Programming',
},
{
- matcher:
- /^\/channels\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\/programming\/add$/g,
+ matcher: channelsPageMatcher('programming/add'),
name: 'Add',
},
{
- matcher:
- /^\/channels\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\/programming\/time-slot-editor$/g,
+ matcher: channelsPageMatcher('time-slot-editor'),
name: 'Time Slot Editor',
},
{
- matcher:
- /^\/channels\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\/programming\/random-slot-editor$/g,
+ matcher: channelsPageMatcher('random-slot-editor'),
name: 'Random Slot Editor',
},
{
@@ -64,7 +74,6 @@ const namedRoutes: Route[] = [
];
const getRouteName = memoize((path: string) => {
- console.log(path);
return find(namedRoutes, ({ matcher }) => {
return matcher.test(path);
})?.name;
diff --git a/web/src/pages/library/EditCustomShowPage.tsx b/web/src/pages/library/EditCustomShowPage.tsx
index 653b3897d..a09a77e56 100644
--- a/web/src/pages/library/EditCustomShowPage.tsx
+++ b/web/src/pages/library/EditCustomShowPage.tsx
@@ -25,7 +25,10 @@ import AddSelectedMediaButton from '../../components/channel_config/AddSelectedM
import ProgrammingSelector from '../../components/channel_config/ProgrammingSelector.tsx';
import { apiClient } from '../../external/api.ts';
import { usePreloadedData } from '../../hooks/preloadedDataHook.ts';
-import { customShowLoader } from '../../preloaders/customShowLoaders.ts';
+import {
+ existingCustomShowLoader,
+ newCustomShowLoader,
+} from '../../preloaders/customShowLoaders.ts';
import {
addPlexMediaToCurrentCustomShow,
removeCustomShowProgram,
@@ -40,7 +43,9 @@ type CustomShowForm = {
};
export default function EditCustomShowPage({ isNew }: Props) {
- const customShow = usePreloadedData(customShowLoader(isNew));
+ const { show: customShow } = usePreloadedData(
+ isNew ? existingCustomShowLoader : newCustomShowLoader,
+ );
const customShowPrograms = useStore((s) => s.customShowEditor.programList);
const queryClient = useQueryClient();
const navigate = useNavigate();
@@ -57,10 +62,11 @@ export default function EditCustomShowPage({ isNew }: Props) {
});
useEffect(() => {
+ console.log(customShow, 'reset');
reset({
name: customShow.name,
});
- }, [customShow.name, reset]);
+ }, [customShow, reset]);
const saveShowMutation = useMutation({
mutationKey: ['custom-shows', isNew ? 'new' : customShow.id],
@@ -147,7 +153,7 @@ export default function EditCustomShowPage({ isNew }: Props) {
- New Custom Show
+ {isNew ? 'New' : 'Edit'} Custom Show
diff --git a/web/src/preloaders/customShowLoaders.ts b/web/src/preloaders/customShowLoaders.ts
index f08e567e2..bfab57e43 100644
--- a/web/src/preloaders/customShowLoaders.ts
+++ b/web/src/preloaders/customShowLoaders.ts
@@ -10,7 +10,12 @@ import {
import { setCurrentCustomShow } from '../store/channelEditor/actions.ts';
import { Preloader } from '../types/index.ts';
-export const customShowLoader = (isNew: boolean) => {
+export type CustomShowPreload = {
+ show: CustomShow;
+ programs: CustomShowProgramming;
+};
+
+export const customShowLoader = (isNew: boolean): Preloader => {
if (!isNew) {
return createPreloader(
({ params }) => customShowQuery(params.id!),
@@ -29,20 +34,17 @@ export const customShowLoader = (isNew: boolean) => {
}
};
-export const newCustomShowLoader: Preloader<{
- show: CustomShow;
- programs: CustomShowProgramming;
-}> = (queryClient: QueryClient) => (args: LoaderFunctionArgs) => {
- return customShowLoader(true)(queryClient)(args).then((show) => ({
- show,
- programs: [],
- }));
-};
+export const newCustomShowLoader: Preloader =
+ (queryClient: QueryClient) => (args: LoaderFunctionArgs) => {
+ return customShowLoader(true)(queryClient)(args).then((show) => ({
+ show,
+ programs: [],
+ }));
+ };
-export const existingCustomShowLoader: Preloader<{
- show: CustomShow;
- programs: CustomShowProgramming;
-}> = (queryClient: QueryClient) => {
+export const existingCustomShowLoader: Preloader = (
+ queryClient: QueryClient,
+) => {
const showLoader = customShowLoader(false)(queryClient);
return async (args: LoaderFunctionArgs) => {
@@ -53,7 +55,6 @@ export const existingCustomShowLoader: Preloader<{
return await Promise.all([showLoaderPromise, programsPromise]).then(
([show, programs]) => {
- console.log(show, programs);
setCurrentCustomShow(show, programs);
return {
show,
diff --git a/web/src/store/customShowEditor/store.ts b/web/src/store/customShowEditor/store.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/src/store/programmingSelector/actions.ts b/web/src/store/programmingSelector/actions.ts
index d776ba6bb..6578341d3 100644
--- a/web/src/store/programmingSelector/actions.ts
+++ b/web/src/store/programmingSelector/actions.ts
@@ -7,7 +7,7 @@ import {
} from '@tunarr/types/plex';
import { map, reject } from 'lodash-es';
import useStore from '..';
-import { SelectedMedia } from './store';
+import { SelectedLibrary, SelectedMedia } from './store';
export const setProgrammingListingServer = (
server: PlexServerSettings | undefined,
@@ -16,7 +16,7 @@ export const setProgrammingListingServer = (
state.currentServer = server;
});
-export const setProgrammingListLibrary = (library: PlexLibrarySection) =>
+export const setProgrammingListLibrary = (library: SelectedLibrary) =>
useStore.setState((state) => {
state.currentLibrary = library;
});
diff --git a/web/src/store/programmingSelector/store.ts b/web/src/store/programmingSelector/store.ts
index 8ca715e63..8723f6d39 100644
--- a/web/src/store/programmingSelector/store.ts
+++ b/web/src/store/programmingSelector/store.ts
@@ -1,5 +1,5 @@
-import { PlexServerSettings } from '@tunarr/types';
-import { PlexMedia, PlexLibrarySection } from '@tunarr/types/plex';
+import { CustomShow, PlexServerSettings } from '@tunarr/types';
+import { PlexLibrarySection, PlexMedia } from '@tunarr/types/plex';
import { StateCreator } from 'zustand';
type ServerName = string;
@@ -10,9 +10,21 @@ export interface SelectedMedia {
guid: PlexItemGuid;
}
+export type PlexLibrary = {
+ type: 'plex';
+ library: PlexLibrarySection;
+};
+
+export type CustomShowLibrary = {
+ type: 'custom-show';
+ library: CustomShow;
+};
+
+export type SelectedLibrary = PlexLibrary | CustomShowLibrary;
+
export interface ProgrammingListingsState {
currentServer?: PlexServerSettings;
- currentLibrary?: PlexLibrarySection;
+ currentLibrary?: SelectedLibrary;
// Tracks the parent-child mappings of library items
contentHierarchyByServer: Record<
ServerName,
diff --git a/web/src/types/index.ts b/web/src/types/index.ts
index 584dbc928..cd718e246 100644
--- a/web/src/types/index.ts
+++ b/web/src/types/index.ts
@@ -105,3 +105,4 @@ export const isUIRedirectProgram = (
export type UIFillerListProgram = (ContentProgram | CustomProgram) & UIIndex;
export type UICustomShowProgram = (ContentProgram | CustomProgram) & UIIndex;
+export type NonUndefinedGuard = T extends undefined ? never : T;