Skip to content

Commit

Permalink
Implement 'select all' functionality in program selector (#482)
Browse files Browse the repository at this point in the history
Closes #481

This adds new functionality to 'select all' of the visible items in the
program selector. The use-case here is to execute a Plex search/filter
and then add all of the results at once

Also includes the following changes:
* Refactor giant plex hooks file into multiple smaller files
* Breakout some common plex search queries into new hooks
* Use FixedList in the selected programming list drawer - I noticed a
  slowdown after implementing select all; this helps.
* Refactor the way the SelectedProgrammingList component works a bit.
  We're no longer mounting this in App with hard-coded paths -- there's
more fixes here, though. Now we have a temporary drawer with various
toggle states, etc etc. This should free up the viewport to view content
* Fixes a bug in Plex search/filtering for TV shows
* Enables the use of path aliases in imports using "@/", since we're
  starting to deal with a lot of files in many nested directories. This
cleans things up a bit
  • Loading branch information
chrisbenincasa authored Jun 6, 2024
1 parent 7ecba02 commit 03bcc69
Show file tree
Hide file tree
Showing 27 changed files with 947 additions and 590 deletions.
30 changes: 2 additions & 28 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,13 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { isNull, isUndefined } from 'lodash-es';
import React, { ReactNode, useCallback, useMemo, useState } from 'react';
import {
Outlet,
Link as RouterLink,
useLocation,
useNavigate,
} from 'react-router-dom';
import { Outlet, Link as RouterLink } from 'react-router-dom';
import './App.css';
import ServerEvents from './components/ServerEvents.tsx';
import TunarrLogo from './components/TunarrLogo.tsx';
import VersionFooter from './components/VersionFooter.tsx';
import SelectedProgrammingList from './components/channel_config/SelectedProgrammingList.tsx';
import DarkModeButton from './components/settings/DarkModeButton.tsx';
import { useVersion } from './hooks/useVersion.ts';
import { addMediaToCurrentChannel } from './store/channelEditor/actions.ts';
import useStore from './store/index.ts';
import { useSettings } from './store/settings/selectors.ts';
import { setDarkModeState } from './store/themeEditor/actions.ts';
Expand Down Expand Up @@ -120,18 +113,6 @@ export function Root({ children }: { children?: React.ReactNode }) {
);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const mobileLinksOpen = !isNull(anchorEl);
const navigate = useNavigate();
const location = useLocation();

const displayPaths = [
'/programming/add',
'library/custom-shows/new',
'library/fillers/new',
];

const displaySelectedProgramming = displayPaths.some((path) =>
location.pathname.match(new RegExp(path)),
);

const toggleDrawerOpen = () => {
setOpen(true);
Expand Down Expand Up @@ -297,7 +278,7 @@ export function Root({ children }: { children?: React.ReactNode }) {
icon: <TextSnippetIcon />,
},
],
[mobileLinksOpen],
[settings.backendUri],
);

const handleOpenClick = useCallback((itemName: string) => {
Expand Down Expand Up @@ -553,13 +534,6 @@ export function Root({ children }: { children?: React.ReactNode }) {
{children ?? <Outlet />}
</Container>
</Box>

{displaySelectedProgramming && (
<SelectedProgrammingList
onAddSelectedMedia={addMediaToCurrentChannel}
onAddMediaSuccess={() => navigate(-1)}
/>
)}
</Box>
<ReactQueryDevtools initialIsOpen={false} />
</ThemeProvider>
Expand Down
12 changes: 8 additions & 4 deletions web/src/components/channel_config/AddSelectedMediaButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { AddCircle } from '@mui/icons-material';
import { CircularProgress, Tooltip } from '@mui/material';
import Button, { ButtonProps } from '@mui/material/Button';
import { flattenDeep, map } from 'lodash-es';
import { MouseEventHandler, useState } from 'react';
import { MouseEventHandler, ReactNode, useState } from 'react';
import {
forSelectedMediaType,
sequentialPromises,
} from '../../helpers/util.ts';
import { enumeratePlexItem } from '../../hooks/plexHooks.ts';
import { enumeratePlexItem } from '../../hooks/plex/plexHookUtil.ts';
import { useTunarrApi } from '../../hooks/useTunarrApi.ts';
import useStore from '../../store/index.ts';
import { clearSelectedMedia } from '../../store/programmingSelector/actions.ts';
Expand All @@ -17,11 +17,15 @@ import { AddedCustomShowProgram, AddedMedia } from '../../types/index.ts';
type Props = {
onAdd: (items: AddedMedia[]) => void;
onSuccess: () => void;
buttonText?: string;
tooltipTitle?: ReactNode;
} & ButtonProps;

export default function AddSelectedMediaButton({
onAdd,
onSuccess,
buttonText,
tooltipTitle,
...rest
}: Props) {
const apiClient = useTunarrApi();
Expand Down Expand Up @@ -71,7 +75,7 @@ export default function AddSelectedMediaButton({
};

return (
<Tooltip title="Add all programs to channel">
<Tooltip title={tooltipTitle ?? 'Add all programs to channel'}>
<span>
<Button
onClick={(e) => addSelectedItems(e)}
Expand All @@ -85,7 +89,7 @@ export default function AddSelectedMediaButton({
)
}
>
Add All
{buttonText ?? 'Add All'}
</Button>
</span>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from '@tunarr/types/plex';
import { take } from 'lodash-es';
import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
import { usePlexTyped2 } from '../../hooks/plexHooks.ts';
import { usePlexTyped2 } from '../../hooks/plex/usePlex.ts';
import useStore from '../../store/index.ts';
import {
addKnownMediaForServer,
Expand Down
13 changes: 6 additions & 7 deletions web/src/components/channel_config/PlexFilterBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@ import {
useForm,
useFormContext,
} from 'react-hook-form';
import {
usePlexTags,
useSelectedLibraryPlexFilters,
} from '../../hooks/plexHooks.ts';
import { usePlexTags } from '../../hooks/plex/usePlexTags.ts';
import { useSelectedLibraryPlexFilters } from '../../hooks/plex/usePlexFilters.ts';
import useStore from '../../store/index.ts';
import { setPlexFilter } from '../../store/programmingSelector/actions.ts';

Expand Down Expand Up @@ -110,7 +108,7 @@ export function PlexValueNode({
}, [selfValue.field, findPlexField]);

const { data: plexTags, isLoading: plexTagsLoading } = usePlexTags(
plexFilter?.type === 'tag' ? plexFilter.key : '',
plexFilter?.type === 'tag' ? plexFilter.key.replace('show.', '') : '',
);

const lookupFieldOperators = useCallback(
Expand Down Expand Up @@ -459,11 +457,12 @@ export function PlexFilterBuilder(
formMethods.reset({
type: 'value',
op: '=',
field: 'title',
field:
selectedLibrary?.library.type === 'show' ? 'show.title' : 'title',
value: '',
});
}
}, [advanced, formMethods, formMethods.reset]);
}, [advanced, formMethods, formMethods.reset, selectedLibrary?.library.type]);

return (
<FilterMetadataContext.Provider
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/channel_config/PlexGridItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
prettyItemDuration,
toggle,
} from '../../helpers/util.ts';
import { usePlexTyped } from '../../hooks/plexHooks.ts';
import { usePlexTyped } from '../../hooks/plex/usePlex.ts';
import useStore from '../../store/index.ts';
import {
addKnownMediaForServer,
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/channel_config/PlexListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
prettyItemDuration,
typedProperty,
} from '../../helpers/util.ts';
import { usePlexTyped } from '../../hooks/plexHooks.ts';
import { usePlexTyped } from '../../hooks/plex/usePlex.ts';
import useStore from '../../store/index.ts';
import {
addKnownMediaForServer,
Expand Down
96 changes: 10 additions & 86 deletions web/src/components/channel_config/PlexProgrammingSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { usePlexSearchInfinite } from '@/hooks/plex/usePlexSearch.ts';
import FilterAlt from '@mui/icons-material/FilterAlt';
import GridView from '@mui/icons-material/GridView';
import ViewList from '@mui/icons-material/ViewList';
Expand All @@ -13,12 +14,7 @@ import {
ToggleButton,
ToggleButtonGroup,
} from '@mui/material';
import { DataTag, useInfiniteQuery } from '@tanstack/react-query';
import {
PlexLibraryCollections,
PlexLibraryMovies,
PlexLibraryMusic,
PlexLibraryShows,
PlexMedia,
PlexMovie,
PlexMusicArtist,
Expand All @@ -31,7 +27,6 @@ import _, {
compact,
first,
flatMap,
forEach,
isNil,
isUndefined,
map,
Expand All @@ -56,9 +51,8 @@ import {
isNewModalAbove,
} from '../../helpers/inlineModalUtil';
import { isNonEmptyString, toggle } from '../../helpers/util';
import { fetchPlexPath, usePlex } from '../../hooks/plexHooks';
import { usePlex } from '../../hooks/plex/usePlex.ts';
import { usePlexServerSettings } from '../../hooks/settingsHooks';
import { useTunarrApi } from '../../hooks/useTunarrApi.ts';
import useStore from '../../store';
import { addKnownMediaForServer } from '../../store/programmingSelector/actions';
import { setProgrammingSelectorViewState } from '../../store/themeEditor/actions';
Expand All @@ -71,6 +65,7 @@ import { PlexFilterBuilder } from './PlexFilterBuilder.tsx';
import { PlexGridItem } from './PlexGridItem';
import { PlexListItem } from './PlexListItem';
import { PlexSortField } from './PlexSortField.tsx';
import { usePlexCollectionsInfinite } from '@/hooks/plex/usePlexCollections.ts';

function a11yProps(index: number) {
return {
Expand All @@ -89,7 +84,6 @@ type Size = {
};

export default function PlexProgrammingSelector() {
const apiClient = useTunarrApi();
const { data: plexServers } = usePlexServerSettings();
const selectedServer = useStore((s) => s.currentServer);
const selectedLibrary = useStore((s) =>
Expand Down Expand Up @@ -228,38 +222,7 @@ export default function PlexProgrammingSelector() {
fetchNextPage: fetchNextCollectionsPage,
isFetchingNextPage: isFetchingNextCollectionsPage,
hasNextPage: hasNextCollectionsPage,
} = useInfiniteQuery({
queryKey: [
'plex',
selectedServer?.name,
selectedLibrary?.library.key,
'collections',
],
queryFn: ({ pageParam }) => {
const plexQuery = new URLSearchParams({
'X-Plex-Container-Start': pageParam.toString(),
'X-Plex-Container-Size': (rowSize * 4).toString(),
});

return fetchPlexPath<PlexLibraryCollections>(
apiClient,
selectedServer!.name,
`/library/sections/${selectedLibrary?.library
.key}/collections?${plexQuery.toString()}`,
)();
},
enabled: !isNil(selectedServer) && !isNil(selectedLibrary),
initialPageParam: 0,
getNextPageParam: (res, all, last) => {
const total = sumBy(all, (page) => page.size);
if (total >= (res.totalSize ?? res.size)) {
return null;
}

// Next offset is the last + how many items we got back.
return last + res.size;
},
});
} = usePlexCollectionsInfinite(selectedServer, selectedLibrary, rowSize * 4);

useEffect(() => {
// When switching between Libraries, if a collection doesn't exist switch back to 'Library' tab
Expand All @@ -278,51 +241,12 @@ export default function PlexProgrammingSelector() {
fetchNextPage: fetchNextItemsPage,
hasNextPage: hasNextItemsPage,
isFetchingNextPage: isFetchingNextItemsPage,
} = useInfiniteQuery({
queryKey: [
'plex-search',
selectedServer?.name,
selectedLibrary?.library.key,
searchKey,
] as DataTag<
['plex-search', string, string, string],
PlexLibraryMovies | PlexLibraryShows | PlexLibraryMusic
>,
enabled: !isNil(selectedServer) && !isNil(selectedLibrary),
initialPageParam: 0,
queryFn: ({ pageParam }) => {
const plexQuery = new URLSearchParams({
'X-Plex-Container-Start': pageParam.toString(),
'X-Plex-Container-Size': (rowSize * 4).toString(),
});
// HACK for now
forEach(searchKey?.split('&'), (keyval) => {
const idx = keyval.lastIndexOf('=');
if (idx !== -1) {
plexQuery.append(keyval.substring(0, idx), keyval.substring(idx + 1));
}
});

return fetchPlexPath<
PlexLibraryMovies | PlexLibraryShows | PlexLibraryMusic
>(
apiClient,
selectedServer!.name,
`/library/sections/${
selectedLibrary!.library.key
}/all?${plexQuery.toString()}`,
)();
},
getNextPageParam: (res, all, last) => {
const total = sumBy(all, (page) => page.size);
if (total >= (res.totalSize ?? res.size)) {
return null;
}

// Next offset is the last + how many items we got back.
return last + res.size;
},
});
} = usePlexSearchInfinite(
selectedServer,
selectedLibrary,
searchKey,
rowSize * 4,
);

useEffect(() => {
if (searchData?.pages.length === 1) {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/channel_config/PlexSortField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import find from 'lodash-es/find';
import isUndefined from 'lodash-es/isUndefined';
import map from 'lodash-es/map';
import { useCallback, useEffect, useState } from 'react';
import { usePlexFilters } from '../../hooks/plexHooks';
import { usePlexFilters } from '../../hooks/plex/usePlexFilters';
import useStore from '../../store';
import { setPlexSort } from '../../store/programmingSelector/actions';

Expand Down
Loading

0 comments on commit 03bcc69

Please sign in to comment.