From c79ae27dda49dbb3431d491520a2ea16c28d7e41 Mon Sep 17 00:00:00 2001 From: Mark D'Avella Date: Wed, 13 Mar 2024 15:53:59 -0400 Subject: [PATCH 1/7] initial grid work --- .../channel_config/PlexGridItem.tsx | 64 +++++++++++++++---- .../PlexProgrammingSelector.tsx | 6 +- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/web/src/components/channel_config/PlexGridItem.tsx b/web/src/components/channel_config/PlexGridItem.tsx index 03c57e0c1..735940688 100644 --- a/web/src/components/channel_config/PlexGridItem.tsx +++ b/web/src/components/channel_config/PlexGridItem.tsx @@ -10,8 +10,6 @@ import { ImageListItem, ImageListItemBar, List, - ListItemButton, - ListItemIcon, Skeleton, } from '@mui/material'; import { @@ -100,27 +98,67 @@ export function PlexGridItem(props: PlexGridItemProps) { ); }; + // + // {/* {hasChildren && ( + // + // {open ? : } + // + // )} */} + // + // + + // display: flex; + // flex-wrap: wrap; + return ( {hasChildren ? ( - - {hasChildren && ( - - {open ? : } - - )} - + Five items} // temp value for testing - add var + position="below" + actionIcon={ + + {selectedMediaIds.includes(item.guid) ? ( + + ) : ( + + )} + + } + actionPosition="right" + /> + ) : ( {map(collectionsData.Metadata, (item) => viewType === 'list' ? ( From 67a2a1bcc7449970f460fe9747486a4ff01a7285 Mon Sep 17 00:00:00 2001 From: Mark D'Avella Date: Wed, 13 Mar 2024 16:33:11 -0400 Subject: [PATCH 2/7] progress on grid ux inline working but janky inline modal component fix issue with loading glitch progress inline modal working more work fix collapse TS fixes fix list type display issue fix issue with alignment and switchig tab values fix last row modal issue adjust transitions for modal --- web/src/components/InlineModal.tsx | 89 +++++ web/src/components/TabPanel.tsx | 49 +++ .../channel_config/PlexGridItem.tsx | 330 +++++++++--------- .../channel_config/PlexListItem.tsx | 4 +- .../PlexProgrammingSelector.tsx | 272 +++++++++++---- web/src/helpers/inlineModalUtil.ts | 75 ++++ .../useReleaseDateSort.ts | 1 + 7 files changed, 576 insertions(+), 244 deletions(-) create mode 100644 web/src/components/InlineModal.tsx create mode 100644 web/src/components/TabPanel.tsx create mode 100644 web/src/helpers/inlineModalUtil.ts diff --git a/web/src/components/InlineModal.tsx b/web/src/components/InlineModal.tsx new file mode 100644 index 000000000..7f9e7ad8f --- /dev/null +++ b/web/src/components/InlineModal.tsx @@ -0,0 +1,89 @@ +import { Collapse, List } from '@mui/material'; +import { PlexMedia } from '@tunarr/types/plex'; +import { usePrevious } from '@uidotdev/usehooks'; +import { memo, useEffect, useRef, useState } from 'react'; +import { getEstimatedModalHeight } from '../helpers/inlineModalUtil'; +import useStore from '../store'; +import PlexGridItem from './channel_config/PlexGridItem'; + +type InlineModalProps = { + modalIndex: number; + modalChildren?: PlexMedia[]; + open?: boolean; +}; + +function InlineModal(props: InlineModalProps) { + const { modalChildren, modalIndex, open } = props; + const [containerWidth, setContainerWidth] = useState(0); + const [itemWidth, setItemWidth] = useState(0); + const previousData = usePrevious(props); + const ref = useRef(null); + const gridItemRef = useRef(null); + const darkMode = useStore((state) => state.theme.darkMode); + const modalHeight = getEstimatedModalHeight( + containerWidth, + itemWidth, + modalChildren?.length || 0, + ); + + useEffect(() => { + if ( + ref.current && + previousData && + previousData.modalChildren !== modalChildren + ) { + const containerWidth = ref?.current?.offsetWidth || 0; + const itemWidth = gridItemRef?.current?.offsetWidth || 0; + + setItemWidth(itemWidth); + setContainerWidth(containerWidth); + } + }, [ref, modalChildren, gridItemRef]); + + return ( + + + darkMode ? theme.palette.grey[800] : theme.palette.grey[400], + padding: '0', + paddingTop: 2, + minHeight: modalHeight || 'auto', + }} + ref={ref} + > + {modalChildren?.map( + (child: PlexMedia, idx: number, arr: PlexMedia[]) => ( + + ), + )} + + + ); +} + +export default memo(InlineModal); diff --git a/web/src/components/TabPanel.tsx b/web/src/components/TabPanel.tsx new file mode 100644 index 000000000..6f87fb49f --- /dev/null +++ b/web/src/components/TabPanel.tsx @@ -0,0 +1,49 @@ +import { Grid } from '@mui/material'; +import { forwardRef } from 'react'; +import useStore from '../store'; + +type TabPanelProps = { + children?: React.ReactNode; + index: number; + value: number; + ref?: any; // to do +}; + +const CustomTabPanel = forwardRef((props: TabPanelProps, ref: any) => { + const { children, value, index, ...other } = props; + + const viewType = useStore((state) => state.theme.programmingSelectorView); + + return ( + + ); +}); + +export default CustomTabPanel; diff --git a/web/src/components/channel_config/PlexGridItem.tsx b/web/src/components/channel_config/PlexGridItem.tsx index 735940688..1523311a3 100644 --- a/web/src/components/channel_config/PlexGridItem.tsx +++ b/web/src/components/channel_config/PlexGridItem.tsx @@ -1,16 +1,10 @@ +import { CheckCircle, RadioButtonUnchecked } from '@mui/icons-material'; import { - CheckCircle, - ExpandLess, - ExpandMore, - RadioButtonUnchecked, -} from '@mui/icons-material'; -import { - Collapse, + Fade, + Grid, IconButton, ImageListItem, ImageListItemBar, - List, - Skeleton, } from '@mui/material'; import { PlexChildMediaApiType, @@ -18,9 +12,16 @@ import { isPlexCollection, isTerminalItem, } from '@tunarr/types/plex'; -import { filter } from 'lodash-es'; -import React, { MouseEvent, useCallback, useEffect, useState } from 'react'; -import { prettyItemDuration } from '../../helpers/util.ts'; +import _, { filter, isNaN } from 'lodash-es'; +import React, { + MouseEvent, + forwardRef, + useCallback, + useEffect, + useState, +} from 'react'; +import { useIntersectionObserver } from 'usehooks-ts'; +import { forPlexMedia, prettyItemDuration } from '../../helpers/util.ts'; import { usePlexTyped } from '../../hooks/plexHooks.ts'; import useStore from '../../store/index.ts'; import { @@ -36,171 +37,170 @@ export interface PlexGridItemProps { index?: number; length?: number; parent?: string; + moveModal?: CallableFunction; + modalChildren?: CallableFunction; + modalIsPending?: CallableFunction; + modalIndex?: number; + onClick?: any; + ref?: any; } -export function PlexGridItem(props: PlexGridItemProps) { - const server = useStore((s) => s.currentServer!); // We have to have a server at this point - const darkMode = useStore((state) => state.theme.darkMode); - const [open, setOpen] = useState(false); - const { item } = props; - const hasChildren = !isTerminalItem(item); - const childPath = isPlexCollection(item) ? 'collections' : 'metadata'; - const { isPending, data: children } = usePlexTyped>( - server.name, - `/library/${childPath}/${props.item.ratingKey}/children`, - hasChildren && open, - ); - const selectedServer = useStore((s) => s.currentServer); - const selectedMedia = useStore((s) => - filter(s.selectedMedia, (p): p is PlexSelectedMedia => p.type === 'plex'), - ); - const selectedMediaIds = selectedMedia.map((item) => item.guid); +const PlexGridItem = forwardRef( + (props: PlexGridItemProps, ref: any) => { + const server = useStore((s) => s.currentServer!); // We have to have a server at this point + const darkMode = useStore((state) => state.theme.darkMode); + const [open, setOpen] = useState(false); + const { item } = props; + const hasChildren = !isTerminalItem(item); + const childPath = isPlexCollection(item) ? 'collections' : 'metadata'; + const { isPending, data: children } = usePlexTyped< + PlexChildMediaApiType + >( + server.name, + `/library/${childPath}/${props.item.ratingKey}/children`, + hasChildren && open, + ); + const selectedServer = useStore((s) => s.currentServer); + const selectedMedia = useStore((s) => + filter(s.selectedMedia, (p): p is PlexSelectedMedia => p.type === 'plex'), + ); + const selectedMediaIds = selectedMedia.map((item) => item.guid); + + const handleClick = () => { + setOpen(!open); - const handleClick = () => { - setOpen(!open); - }; + if (props.moveModal) { + props.moveModal(); - useEffect(() => { - if (children) { - addKnownMediaForServer(server.name, children.Metadata, item.guid); - } - }, [item.guid, server.name, children]); + if (children && props.modalChildren) { + props.modalChildren(children.Metadata); + } + } + }; + + useEffect(() => { + if (props.modalIsPending) { + props.modalIsPending(isPending); + } + }, [isPending]); - const handleItem = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); + useEffect(() => { + if (children) { + addKnownMediaForServer(server.name, children.Metadata, item.guid); - if (selectedMediaIds.includes(item.guid)) { - removePlexSelectedMedia(selectedServer!.name, [item.guid]); - } else { - addPlexSelectedMedia(selectedServer!.name, [item]); + if (children.Metadata.length > 0 && !!props.modalChildren) { + props.modalChildren(children.Metadata); + } } - }, - [item, selectedServer, selectedMediaIds], - ); + }, [item.guid, server.name, children]); - const renderChildren = () => { - return isPending ? ( - - ) : ( - - {children?.Metadata.map((child, idx, arr) => ( - - ))} - + const handleItem = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + + if (selectedMediaIds.includes(item.guid)) { + removePlexSelectedMedia(selectedServer!.name, [item.guid]); + } else { + addPlexSelectedMedia(selectedServer!.name, [item]); + } + }, + [item, selectedServer, selectedMediaIds], ); - }; - // - // {/* {hasChildren && ( - // - // {open ? : } - // - // )} */} - // - // + const { isIntersecting: isInViewport, ref: imageRef } = + useIntersectionObserver({ + threshold: 0, + rootMargin: '0px', + freezeOnceVisible: true, + }); - // display: flex; - // flex-wrap: wrap; + const extractChildCount = forPlexMedia({ + season: (s) => s.leafCount, + show: (s) => s.childCount, + collection: (s) => parseInt(s.childCount), + }); - return ( - - {hasChildren ? ( - + - - Five items} // temp value for testing - add var - position="below" - actionIcon={ - - {selectedMediaIds.includes(item.guid) ? ( - - ) : ( - - )} - + + props.modalIndex === props.index + ? darkMode + ? theme.palette.grey[800] + : theme.palette.grey[400] + : 'transparent', + transition: 'background-color 10s ease', + }} + onClick={ + hasChildren + ? handleClick + : (event: MouseEvent) => handleItem(event) } - actionPosition="right" - /> - - ) : ( - handleItem(e)} - > - {item.title} - {prettyItemDuration(item.duration)}} - position="below" - actionIcon={ - handleItem(e)} - > - {selectedMediaIds.includes(item.guid) ? ( - + ref={ref} + > + {isInViewport && ( // To do: Eventually turn this itno isNearViewport so images load before they hit the viewport + + )} + {`${ + !isNaN(extractChildCount(item)) && extractChildCount(item) + } items`} ) : ( - - )} - - } - actionPosition="right" - /> - - )} - - {renderChildren()} - - - ); -} + {prettyItemDuration(item.duration)} + ) + } + position="below" + actionIcon={ + ) => + handleItem(event) + } + > + {selectedMediaIds.includes(item.guid) ? ( + + ) : ( + + )} + + } + actionPosition="right" + /> + + + + ); + }, +); + +export default PlexGridItem; diff --git a/web/src/components/channel_config/PlexListItem.tsx b/web/src/components/channel_config/PlexListItem.tsx index e6749c86c..ebc4f25a6 100644 --- a/web/src/components/channel_config/PlexListItem.tsx +++ b/web/src/components/channel_config/PlexListItem.tsx @@ -130,7 +130,7 @@ export function PlexListItem(props: PlexListItemProps) { return ( - + {hasChildren && ( {open ? : } )} @@ -143,7 +143,7 @@ export function PlexListItem(props: PlexListItemProps) { : `Add ${plexTypeString(item)}`} - + {renderChildren()} diff --git a/web/src/components/channel_config/PlexProgrammingSelector.tsx b/web/src/components/channel_config/PlexProgrammingSelector.tsx index 0de21ee61..7af82f1d4 100644 --- a/web/src/components/channel_config/PlexProgrammingSelector.tsx +++ b/web/src/components/channel_config/PlexProgrammingSelector.tsx @@ -1,25 +1,20 @@ -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, + Tab, + Tabs, TextField, ToggleButton, - ToggleButtonGroup, - Typography, + ToggleButtonGroup } from '@mui/material'; import { DataTag, useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { @@ -27,13 +22,21 @@ import { PlexLibraryMovies, PlexLibraryMusic, PlexLibraryShows, + PlexMedia, PlexMovie, PlexMusicArtist, PlexTvShow, } from '@tunarr/types/plex'; import { chain, first, isEmpty, isNil, isUndefined, map } from 'lodash-es'; -import { Fragment, useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useIntersectionObserver } from 'usehooks-ts'; +<<<<<<< HEAD +======= +import { + firstItemInNextRow, + getImagesPerRow, +} from '../../helpers/inlineModalUtil'; +>>>>>>> a7dab69 (progress on grid ux) import { toggle } from '../../helpers/util'; import { fetchPlexPath, usePlex } from '../../hooks/plexHooks'; import { usePlexServerSettings } from '../../hooks/settingsHooks'; @@ -42,10 +45,19 @@ import useStore from '../../store'; import { addKnownMediaForServer } from '../../store/programmingSelector/actions'; import { setProgrammingSelectorViewState } from '../../store/themeEditor/actions'; import { ProgramSelectorViewType } from '../../types'; +import InlineModal from '../InlineModal'; +import CustomTabPanel from '../TabPanel'; import ConnectPlex from '../settings/ConnectPlex'; -import { PlexGridItem } from './PlexGridItem'; +import PlexGridItem from './PlexGridItem'; import { PlexListItem } from './PlexListItem'; +function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; +} + export default function PlexProgrammingSelector() { const { data: plexServers } = usePlexServerSettings(); const selectedServer = useStore((s) => s.currentServer); @@ -53,11 +65,85 @@ export default function PlexProgrammingSelector() { s.currentLibrary?.type === 'plex' ? s.currentLibrary : null, ); const viewType = useStore((state) => state.theme.programmingSelectorView); - + const [tabValue, setTabValue] = useState(0); + const [modalChildren, setModalChildren] = useState([]); + const [rowSize, setRowSize] = useState(16); + const [modalIndex, setModalIndex] = useState(-1); + const [modalIsPending, setModalIsPending] = useState(true); 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 containerRef = useRef(null); + const imageRef = useRef(null); + const libraryImageRef = useRef(null); + const libraryContainerRef = useRef(null); + + const handleResize = () => { + if (tabValue === 0) { + const libraryContainerWidth = + libraryContainerRef?.current?.offsetWidth || 0; + setRowSize(getImagesPerRow(libraryContainerWidth, 160)); // to do: remove magic number + } else { + // Collections initial load + const containerWidth = containerRef?.current?.offsetWidth || 0; + const itemWidth = imageRef?.current?.offsetWidth || 0; + + setRowSize(getImagesPerRow(containerWidth, itemWidth)); + } + }; + + useEffect(() => { + if (viewType === 'grid') { + const handleResizeEvent = () => handleResize(); + handleResize(); // Call initially to set width + window.addEventListener('resize', handleResizeEvent); + + // Cleanup function to remove event listener + return () => window.removeEventListener('resize', handleResizeEvent); + } + }, []); + + useEffect(() => { + if (viewType === 'grid') { + const containerWidth = containerRef?.current?.offsetWidth || 0; + const itemWidth = imageRef?.current?.offsetWidth || 0; + + setRowSize(getImagesPerRow(containerWidth, itemWidth)); + } + }, [containerRef, imageRef, modalChildren]); + + useEffect(() => { + setModalIndex(-1); + }, [tabValue]); + + const handleChange = (_: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + const handleMoveModal = useCallback( + (index: number) => { + if (index === modalIndex) { + setModalIndex(-1); + } else { + setModalIndex(index); + } + }, + [modalIndex], + ); + + const handleModalChildren = useCallback( + (children: PlexMedia[]) => { + setModalChildren(children); + }, + [modalChildren], + ); + + const handleModalIsPending = useCallback( + (isPending: boolean) => { + setModalIsPending(isPending); + }, + [modalIsPending], + ); const { data: directoryChildren } = usePlex( selectedServer?.name ?? '', @@ -81,7 +167,7 @@ export default function PlexProgrammingSelector() { setViewType(newFormats); }; - const { data: collectionsData } = useQuery({ + const { isLoading: isCollectionLoading, data: collectionsData } = useQuery({ queryKey: [ 'plex', selectedServer?.name, @@ -97,6 +183,13 @@ export default function PlexProgrammingSelector() { enabled: !isNil(selectedServer) && !isNil(selectedLibrary), }); + useEffect(() => { + // When switching between Libraries, if a collection doesn't exist switch back to 'Library' tab + if (!collectionsData && !isCollectionLoading && tabValue === 1) { + setTabValue(0); + } + }, [collectionsData, isCollectionLoading]); + const { isLoading: searchLoading, data: searchData } = useInfiniteQuery({ queryKey: [ 'plex-search', @@ -112,7 +205,7 @@ export default function PlexProgrammingSelector() { queryFn: ({ pageParam }) => { const plexQuery = new URLSearchParams({ 'Plex-Container-Start': pageParam.toString(), - 'Plex-Container-Size': '10', + 'Plex-Container-Size': (rowSize * 4).toString(), }); if (!isNil(debounceSearch) && !isEmpty(debounceSearch)) { @@ -129,20 +222,22 @@ export default function PlexProgrammingSelector() { )(); }, getNextPageParam: (res, _, last) => { - if (res.size < 10) { + if (res.size < rowSize * 4) { return null; } - return last + 10; + return last + rowSize * 4; }, }); useEffect(() => { if (searchData) { + handleResize(); // Call initially to set rowSize + // We're using this as an analogue for detecting the start of a new 'query' if (searchData.pages.length === 1) { setScrollParams({ - limit: 16, + limit: rowSize * 4, max: first(searchData.pages)!.size, }); } @@ -158,14 +253,14 @@ export default function PlexProgrammingSelector() { addKnownMediaForServer(selectedServer.name, allMedia); } } - }, [selectedServer, searchData, setScrollParams]); + }, [selectedServer, searchData, setScrollParams, rowSize]); const { ref } = useIntersectionObserver({ onChange: (_, entry) => { if (entry.isIntersecting && scrollParams.limit < scrollParams.max) { setScrollParams(({ limit: prevLimit, max }) => ({ max, - limit: prevLimit + 10, + limit: prevLimit + rowSize * 4, })); } }, @@ -175,44 +270,49 @@ export default function PlexProgrammingSelector() { const renderListItems = () => { const elements: JSX.Element[] = []; - if (collectionsData && collectionsData.size > 0) { + if (collectionsData && collectionsData.size > 0 && tabValue === 1) { elements.push( - - setCollectionsOpen(toggle)} - dense - sx={ - viewType === 'grid' ? { display: 'block', width: '100%' } : null - } - > - - {collectionsOpen ? : } - - - - - {map(collectionsData.Metadata, (item) => - viewType === 'list' ? ( - - ) : ( - - ), - )} - - , + + {map(collectionsData.Metadata, (item, index: number) => + viewType === 'list' ? ( + + ) : ( + <> + + handleMoveModal(index)} + modalChildren={(children: PlexMedia[]) => + handleModalChildren(children) + } + modalIsPending={(isPending: boolean) => + handleModalIsPending(isPending) + } + ref={imageRef} + /> + + ), + )} + , ); } @@ -223,7 +323,9 @@ export default function PlexProgrammingSelector() { .flatten() .take(scrollParams.limit) .value(); + elements.push( +<<<<<<< HEAD ...map(items, (item: PlexMovie | PlexTvShow | PlexMusicArtist) => { return viewType === 'list' ? ( @@ -231,8 +333,31 @@ export default function PlexProgrammingSelector() { ); }), +======= + + {map(items, (item: PlexMovie | PlexTvShow, idx: number) => { + return viewType === 'list' ? ( + + ) : ( + + ); + })} + , +>>>>>>> a7dab69 (progress on grid ux) ); } + return elements; }; @@ -321,28 +446,21 @@ export default function PlexProgrammingSelector() { marginTop: 1, }} /> - {!searchLoading && ( - t.palette.grey[700]}> - {first(searchData?.pages)?.size} Items - - )} - - {renderListItems()} -
-
+ + + + {collectionsData && collectionsData.size > 0 && ( + + )} + + + {renderListItems()} +
)} diff --git a/web/src/helpers/inlineModalUtil.ts b/web/src/helpers/inlineModalUtil.ts new file mode 100644 index 000000000..2e2ca1f4c --- /dev/null +++ b/web/src/helpers/inlineModalUtil.ts @@ -0,0 +1,75 @@ +import { PlexMedia } from '@tunarr/types/plex'; + +export function getImagesPerRow( + containerWidth: number, + imageWidth: number, +): number { + return Math.floor(containerWidth / imageWidth); +} + +// Estimate the modal height to prevent div collapse while new modal images load +export function getEstimatedModalHeight( + containerWidth: number, + imageWidth: number, + listSize: number, +): number { + const columns = getImagesPerRow(containerWidth, imageWidth); + const widthPerItem = containerWidth / columns; + const heightPerItem = widthPerItem * 1.72 + 8; //8 magic number for margin-top of each item + const rows = listSize < columns ? 1 : Math.ceil(listSize / columns); + //This is min-height so we only need to adjust it for visible rows since we + //use interesectionObserver to load them in + const maxRows = rows >= 3 ? 3 : rows; + + return maxRows * heightPerItem; +} + +export function firstItemInNextRow( + modalIndex: number, + itemsPerRow: number, + numberOfItems: number, +): number { + // Calculate the row number of the current item + const rowNumber = Math.floor(modalIndex / itemsPerRow); + + if (modalIndex === -1) { + return -1; + } + + // If the item clicked is on the last row and the last row isn't full, adjust where modal is inserted + // for now the final rows modal will be inserted above these items + const numberOfItemsLastRow = numberOfItems % itemsPerRow; + + if ( + modalIndex >= numberOfItems - numberOfItemsLastRow && + numberOfItemsLastRow < itemsPerRow + ) { + console.log('TEST'); + + return numberOfItems - numberOfItemsLastRow; + } + + // If the current item is not in the last row, return the index of the first item in the next row + if ( + rowNumber <= + (modalIndex > 0 ? modalIndex : 1) / // Account for modalIndex = 0 + itemsPerRow + ) { + return (rowNumber + 1) * itemsPerRow; + } + + // Otherwise, return -1 to indicate modal is closed + return -1; +} + +export function extractLastIndexes(arr: PlexMedia[], x: number): number[] { + if (x > arr.length) { + return arr.map((_, i) => i); // Return all indexes if x is too large + } + + // Extract the last x elements + const lastElements = arr.slice(-x); + + // Use map to create a new array with corresponding indexes + return lastElements.map((_) => arr.indexOf(_)); +} diff --git a/web/src/hooks/programming_controls/useReleaseDateSort.ts b/web/src/hooks/programming_controls/useReleaseDateSort.ts index 8a4a52d13..d8ea14798 100644 --- a/web/src/hooks/programming_controls/useReleaseDateSort.ts +++ b/web/src/hooks/programming_controls/useReleaseDateSort.ts @@ -30,6 +30,7 @@ export const sortPrograms = ( } else { n = sortOrder === 'asc' ? Number.MAX_VALUE : Number.MAX_VALUE; } + return n; }); From 2d07bcf63a7756467bfc99fef1e40b8afc6350e9 Mon Sep 17 00:00:00 2001 From: Mark D'Avella Date: Wed, 20 Mar 2024 16:17:29 -0400 Subject: [PATCH 3/7] rebase missing part --- .../PlexProgrammingSelector.tsx | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/web/src/components/channel_config/PlexProgrammingSelector.tsx b/web/src/components/channel_config/PlexProgrammingSelector.tsx index 7af82f1d4..d332d1be8 100644 --- a/web/src/components/channel_config/PlexProgrammingSelector.tsx +++ b/web/src/components/channel_config/PlexProgrammingSelector.tsx @@ -14,7 +14,7 @@ import { Tabs, TextField, ToggleButton, - ToggleButtonGroup + ToggleButtonGroup, } from '@mui/material'; import { DataTag, useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { @@ -30,13 +30,10 @@ import { import { chain, first, isEmpty, isNil, isUndefined, map } from 'lodash-es'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useIntersectionObserver } from 'usehooks-ts'; -<<<<<<< HEAD -======= import { firstItemInNextRow, getImagesPerRow, } from '../../helpers/inlineModalUtil'; ->>>>>>> a7dab69 (progress on grid ux) import { toggle } from '../../helpers/util'; import { fetchPlexPath, usePlex } from '../../hooks/plexHooks'; import { usePlexServerSettings } from '../../hooks/settingsHooks'; @@ -325,36 +322,29 @@ export default function PlexProgrammingSelector() { .value(); elements.push( -<<<<<<< HEAD - ...map(items, (item: PlexMovie | PlexTvShow | PlexMusicArtist) => { - return viewType === 'list' ? ( - - ) : ( - - ); - }), -======= - {map(items, (item: PlexMovie | PlexTvShow, idx: number) => { - return viewType === 'list' ? ( - - ) : ( - - ); - })} + {map( + items, + (item: PlexMovie | PlexTvShow | PlexMusicArtist, idx: number) => { + return viewType === 'list' ? ( + + ) : ( + + ); + }, + )} , ->>>>>>> a7dab69 (progress on grid ux) ); } From a6197e0e4057274aeeae4c7d53256e2e5c6b16e7 Mon Sep 17 00:00:00 2001 From: Mark D'Avella Date: Wed, 20 Mar 2024 16:36:05 -0400 Subject: [PATCH 4/7] added some defaults to make modal load smoother --- web/src/components/InlineModal.tsx | 4 ++-- web/src/helpers/inlineModalUtil.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/web/src/components/InlineModal.tsx b/web/src/components/InlineModal.tsx index 7f9e7ad8f..0b20aa0e3 100644 --- a/web/src/components/InlineModal.tsx +++ b/web/src/components/InlineModal.tsx @@ -43,7 +43,7 @@ function InlineModal(props: InlineModalProps) { return ( diff --git a/web/src/helpers/inlineModalUtil.ts b/web/src/helpers/inlineModalUtil.ts index 2e2ca1f4c..223a56928 100644 --- a/web/src/helpers/inlineModalUtil.ts +++ b/web/src/helpers/inlineModalUtil.ts @@ -13,6 +13,11 @@ export function getEstimatedModalHeight( imageWidth: number, listSize: number, ): number { + // Exit with defaults if container & image width are not provided + if (containerWidth === 0 || imageWidth === 0) { + return 294; //default modal height for 1 row + } + const columns = getImagesPerRow(containerWidth, imageWidth); const widthPerItem = containerWidth / columns; const heightPerItem = widthPerItem * 1.72 + 8; //8 magic number for margin-top of each item @@ -21,7 +26,7 @@ export function getEstimatedModalHeight( //use interesectionObserver to load them in const maxRows = rows >= 3 ? 3 : rows; - return maxRows * heightPerItem; + return Math.ceil(maxRows * heightPerItem); } export function firstItemInNextRow( @@ -44,8 +49,6 @@ export function firstItemInNextRow( modalIndex >= numberOfItems - numberOfItemsLastRow && numberOfItemsLastRow < itemsPerRow ) { - console.log('TEST'); - return numberOfItems - numberOfItemsLastRow; } From c44347a18cfabe8889dd08c7d78df411ea47a75a Mon Sep 17 00:00:00 2001 From: Mark D'Avella Date: Thu, 21 Mar 2024 10:27:50 -0400 Subject: [PATCH 5/7] fix modal height calc and missing ref types --- web/src/components/InlineModal.tsx | 3 +- web/src/components/TabPanel.tsx | 74 ++++++++++--------- .../channel_config/PlexGridItem.tsx | 27 ++++--- .../PlexProgrammingSelector.tsx | 30 +++++++- web/src/helpers/inlineModalUtil.ts | 27 +++++-- 5 files changed, 101 insertions(+), 60 deletions(-) diff --git a/web/src/components/InlineModal.tsx b/web/src/components/InlineModal.tsx index 0b20aa0e3..d4a589dc5 100644 --- a/web/src/components/InlineModal.tsx +++ b/web/src/components/InlineModal.tsx @@ -43,7 +43,8 @@ function InlineModal(props: InlineModalProps) { return ( ; }; -const CustomTabPanel = forwardRef((props: TabPanelProps, ref: any) => { - const { children, value, index, ...other } = props; +const CustomTabPanel = forwardRef( + (props: TabPanelProps, ref: ForwardedRef) => { + const { children, value, index, ...other } = props; - const viewType = useStore((state) => state.theme.programmingSelectorView); + const viewType = useStore((state) => state.theme.programmingSelectorView); - return ( - - ); -}); + return ( + + ); + }, +); export default CustomTabPanel; diff --git a/web/src/components/channel_config/PlexGridItem.tsx b/web/src/components/channel_config/PlexGridItem.tsx index 1523311a3..5663928e4 100644 --- a/web/src/components/channel_config/PlexGridItem.tsx +++ b/web/src/components/channel_config/PlexGridItem.tsx @@ -14,6 +14,7 @@ import { } from '@tunarr/types/plex'; import _, { filter, isNaN } from 'lodash-es'; import React, { + ForwardedRef, MouseEvent, forwardRef, useCallback, @@ -42,15 +43,18 @@ export interface PlexGridItemProps { modalIsPending?: CallableFunction; modalIndex?: number; onClick?: any; - ref?: any; + ref?: React.RefObject; } const PlexGridItem = forwardRef( - (props: PlexGridItemProps, ref: any) => { + ( + props: PlexGridItemProps, + ref: ForwardedRef, + ) => { const server = useStore((s) => s.currentServer!); // We have to have a server at this point const darkMode = useStore((state) => state.theme.darkMode); const [open, setOpen] = useState(false); - const { item } = props; + const { item, style, moveModal, modalChildren } = props; const hasChildren = !isTerminalItem(item); const childPath = isPlexCollection(item) ? 'collections' : 'metadata'; const { isPending, data: children } = usePlexTyped< @@ -69,11 +73,11 @@ const PlexGridItem = forwardRef( const handleClick = () => { setOpen(!open); - if (props.moveModal) { - props.moveModal(); + if (moveModal) { + moveModal(); - if (children && props.modalChildren) { - props.modalChildren(children.Metadata); + if (children && modalChildren) { + modalChildren(children.Metadata); } } }; @@ -88,8 +92,8 @@ const PlexGridItem = forwardRef( if (children) { addKnownMediaForServer(server.name, children.Metadata, item.guid); - if (children.Metadata.length > 0 && !!props.modalChildren) { - props.modalChildren(children.Metadata); + if (children.Metadata.length > 0 && !!modalChildren) { + modalChildren(children.Metadata); } } }, [item.guid, server.name, children]); @@ -107,7 +111,7 @@ const PlexGridItem = forwardRef( [item, selectedServer, selectedMediaIds], ); - const { isIntersecting: isInViewport, ref: imageRef } = + const { isIntersecting: isInViewport, ref: imageContainerRef } = useIntersectionObserver({ threshold: 0, rootMargin: '0px', @@ -125,7 +129,7 @@ const PlexGridItem = forwardRef( { setModalIndex(-1); + handleModalChildren([]); }, [tabValue]); const handleChange = (_: React.SyntheticEvent, newValue: number) => { @@ -119,15 +120,31 @@ export default function PlexProgrammingSelector() { const handleMoveModal = useCallback( (index: number) => { + console.log('TEST'); if (index === modalIndex) { + handleModalChildren([]); setModalIndex(-1); } else { + handleModalChildren([]); setModalIndex(index); } }, [modalIndex], ); + const handleHeightCalc = (childCount: string) => { + console.log(childCount); + + // const containerWidth = containerRef?.current?.offsetWidth || 0; + // const itemWidth = imageRef?.current?.offsetWidth || 0; + + // return getEstimatedModalHeight( + // containerWidth, + // itemWidth, + // parseInt(childCount), + // ); + }; + const handleModalChildren = useCallback( (children: PlexMedia[]) => { setModalChildren(children); @@ -279,7 +296,7 @@ export default function PlexProgrammingSelector() { viewType === 'list' ? ( ) : ( - <> + handleModalIsPending(isPending) } + // style={{ + // opacity: + // index === modalIndex && modalIndex === -1 + // ? '0.75 !important' + // : '0.55 !important', + // }} ref={imageRef} /> - + ), )} , diff --git a/web/src/helpers/inlineModalUtil.ts b/web/src/helpers/inlineModalUtil.ts index 223a56928..3354ab8f5 100644 --- a/web/src/helpers/inlineModalUtil.ts +++ b/web/src/helpers/inlineModalUtil.ts @@ -10,23 +10,33 @@ export function getImagesPerRow( // Estimate the modal height to prevent div collapse while new modal images load export function getEstimatedModalHeight( containerWidth: number, - imageWidth: number, + imageContainerWidth: number, listSize: number, ): number { // Exit with defaults if container & image width are not provided - if (containerWidth === 0 || imageWidth === 0) { + if (containerWidth === 0 || imageContainerWidth === 0) { return 294; //default modal height for 1 row } - const columns = getImagesPerRow(containerWidth, imageWidth); - const widthPerItem = containerWidth / columns; - const heightPerItem = widthPerItem * 1.72 + 8; //8 magic number for margin-top of each item + const columns = getImagesPerRow(containerWidth, imageContainerWidth); + + // Magic Numbers + // to do: eventually grab this data via refs just in case it changes in the future + const inlineModalTopPadding = 16; + const imageContainerXPadding = 8; + const listItemBarContainerHeight = 54; + + const imagewidth = imageContainerWidth - imageContainerXPadding * 2; // 16px padding on each item + const heightPerImage = (3 * imagewidth) / 2; // Movie Posters are 2:3 + const heightPerItem = + heightPerImage + listItemBarContainerHeight + imageContainerXPadding; // 54px + const rows = listSize < columns ? 1 : Math.ceil(listSize / columns); //This is min-height so we only need to adjust it for visible rows since we //use interesectionObserver to load them in const maxRows = rows >= 3 ? 3 : rows; - return Math.ceil(maxRows * heightPerItem); + return Math.ceil(maxRows * heightPerItem + inlineModalTopPadding); // 16px padding added to top } export function firstItemInNextRow( @@ -37,7 +47,8 @@ export function firstItemInNextRow( // Calculate the row number of the current item const rowNumber = Math.floor(modalIndex / itemsPerRow); - if (modalIndex === -1) { + // Modal is closed or collection has no data, exit + if (modalIndex === -1 || numberOfItems === 0) { return -1; } @@ -73,6 +84,6 @@ export function extractLastIndexes(arr: PlexMedia[], x: number): number[] { // Extract the last x elements const lastElements = arr.slice(-x); - // Use map to create a new array with corresponding indexes + // Return last X indexes in new array return lastElements.map((_) => arr.indexOf(_)); } From b573c9b8c1ed51e216c5176b19201b2b86b7302c Mon Sep 17 00:00:00 2001 From: Mark D'Avella Date: Thu, 21 Mar 2024 10:34:42 -0400 Subject: [PATCH 6/7] fix build --- web/src/components/InlineModal.tsx | 1 - .../PlexProgrammingSelector.tsx | 19 ------------------- 2 files changed, 20 deletions(-) diff --git a/web/src/components/InlineModal.tsx b/web/src/components/InlineModal.tsx index d4a589dc5..e7fedcd17 100644 --- a/web/src/components/InlineModal.tsx +++ b/web/src/components/InlineModal.tsx @@ -43,7 +43,6 @@ function InlineModal(props: InlineModalProps) { return ( { - console.log(childCount); - - // const containerWidth = containerRef?.current?.offsetWidth || 0; - // const itemWidth = imageRef?.current?.offsetWidth || 0; - - // return getEstimatedModalHeight( - // containerWidth, - // itemWidth, - // parseInt(childCount), - // ); - }; - const handleModalChildren = useCallback( (children: PlexMedia[]) => { setModalChildren(children); @@ -320,12 +307,6 @@ export default function PlexProgrammingSelector() { modalIsPending={(isPending: boolean) => handleModalIsPending(isPending) } - // style={{ - // opacity: - // index === modalIndex && modalIndex === -1 - // ? '0.75 !important' - // : '0.55 !important', - // }} ref={imageRef} />
From 9ed0b60fbd9da4c04acd9af43b6e46d4b82c611a Mon Sep 17 00:00:00 2001 From: Mark D'Avella Date: Thu, 21 Mar 2024 10:51:42 -0400 Subject: [PATCH 7/7] add animation on modal index select --- web/src/components/channel_config/PlexGridItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/channel_config/PlexGridItem.tsx b/web/src/components/channel_config/PlexGridItem.tsx index 5663928e4..e5da5193c 100644 --- a/web/src/components/channel_config/PlexGridItem.tsx +++ b/web/src/components/channel_config/PlexGridItem.tsx @@ -153,7 +153,7 @@ const PlexGridItem = forwardRef( ? theme.palette.grey[800] : theme.palette.grey[400] : 'transparent', - transition: 'background-color 10s ease', + transition: 'background-color 350ms linear !important', ...style, }} onClick={