From 8ee0a120300460141ee17e4476dedcd65bdcb7b7 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 11 Jun 2026 18:14:26 -0400 Subject: [PATCH] Merge library toolbar into main AppBar --- src/apps/experimental/AppLayout.tsx | 8 +- .../components/library/GuideView.tsx | 5 +- .../components/library/ItemsView.tsx | 310 +++--------------- .../components/library/PageTabContent.tsx | 120 ++----- .../libraries/components/LibraryToolbar.tsx | 232 +++++++++++++ .../libraries/constants/libraryRoutes.ts | 12 + .../libraries/constants/views/defaults.ts | 16 + .../features/libraries/hooks/useLibrary.tsx | 79 +++++ .../features/libraries/types/LibraryRoute.ts | 2 + .../features/libraries/utils/settings.ts | 32 ++ .../features/libraries/utils/viewContent.ts | 17 + src/hooks/useFetchItems.ts | 12 +- src/types/libraryTabContent.ts | 2 - src/utils/items.ts | 31 +- 14 files changed, 476 insertions(+), 402 deletions(-) create mode 100644 src/apps/experimental/features/libraries/components/LibraryToolbar.tsx create mode 100644 src/apps/experimental/features/libraries/constants/views/defaults.ts create mode 100644 src/apps/experimental/features/libraries/hooks/useLibrary.tsx create mode 100644 src/apps/experimental/features/libraries/utils/settings.ts create mode 100644 src/apps/experimental/features/libraries/utils/viewContent.ts diff --git a/src/apps/experimental/AppLayout.tsx b/src/apps/experimental/AppLayout.tsx index 9996d453db..e1081209f1 100644 --- a/src/apps/experimental/AppLayout.tsx +++ b/src/apps/experimental/AppLayout.tsx @@ -12,6 +12,9 @@ import { useApi } from 'hooks/useApi'; import AppToolbar from './components/AppToolbar'; import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer'; +import LibraryToolbar from './features/libraries/components/LibraryToolbar'; +import { LibraryProvider } from './features/libraries/hooks/useLibrary'; +import { isLibraryPath } from './features/libraries/utils/path'; import './AppOverrides.scss'; @@ -29,7 +32,7 @@ export const Component = () => { }, [ isDrawerActive, setIsDrawerActive ]); return ( - <> + { isDrawerOpen={isDrawerOpen} onDrawerButtonClick={onToggleDrawer} /> + {isLibraryPath(location.pathname) && } { @@ -73,6 +77,6 @@ export const Component = () => { - + ); }; diff --git a/src/apps/experimental/components/library/GuideView.tsx b/src/apps/experimental/components/library/GuideView.tsx index 7f1e687d9e..b33ef40ab5 100644 --- a/src/apps/experimental/components/library/GuideView.tsx +++ b/src/apps/experimental/components/library/GuideView.tsx @@ -54,10 +54,7 @@ const GuideView: FC = () => { width: 'auto', paddingTop: '0', paddingBottom: '0 !important', - top: { - xs: '6.9em !important', - lg: '4em !important' - } + top: '0 !important' }} />; }; diff --git a/src/apps/experimental/components/library/ItemsView.tsx b/src/apps/experimental/components/library/ItemsView.tsx index f4badaa393..dceea1b5bc 100644 --- a/src/apps/experimental/components/library/ItemsView.tsx +++ b/src/apps/experimental/components/library/ItemsView.tsx @@ -1,109 +1,46 @@ -import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; -import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'; import Box from '@mui/material/Box'; -import ButtonGroup from '@mui/material/ButtonGroup'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import type { Theme } from '@mui/material/styles'; -import Toolbar from '@mui/material/Toolbar'; -import useMediaQuery from '@mui/material/useMediaQuery'; import classNames from 'classnames'; -import React, { type FC, useCallback } from 'react'; +import React, { type FC, SetStateAction, useCallback } from 'react'; +import { useLibrary } from 'apps/experimental/features/libraries/hooks/useLibrary'; +import { getDefaultLibraryViewSettings } from 'apps/experimental/features/libraries/utils/settings'; +import Cards from 'components/cardbuilder/Card/Cards'; import { CardShape } from 'components/cardbuilder/utils/shape'; -import OffsetAppBar from 'components/OffsetAppBar'; -import { ItemAction } from 'constants/itemAction'; -import { useApi } from 'hooks/useApi'; -import { useLocalStorage } from 'hooks/useLocalStorage'; -import { useGetItemsViewByType } from 'hooks/useFetchItems'; -import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items'; -import Loading from 'components/loading/LoadingComponent'; -import { playbackManager } from 'components/playback/playbackmanager'; -import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer'; import NoItemsMessage from 'components/common/NoItemsMessage'; import Lists from 'components/listview/List/Lists'; -import Cards from 'components/cardbuilder/Card/Cards'; -import { useItem } from 'hooks/useItem'; -import { useUserSettings } from 'hooks/useUserSettings'; -import globalize from 'lib/globalize'; -import { LibraryTab } from 'types/libraryTab'; -import { type LibraryViewSettings, type ParentId, ViewMode } from 'types/library'; +import Loading from 'components/loading/LoadingComponent'; +import { ItemAction } from 'constants/itemAction'; +import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer'; +import { useApi } from 'hooks/useApi'; import type { CardOptions } from 'types/cardOptions'; +import { type LibraryViewSettings, ViewMode } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; import type { ListOptions } from 'types/listOptions'; import AlphabetPicker from './AlphabetPicker'; -import FilterButton from './filter/FilterButton'; -import NewCollectionButton from './NewCollectionButton'; -import NewPlaylistButton from './NewPlaylistButton'; -import Pagination from './Pagination'; -import PlayAllButton from './PlayAllButton'; -import QueueButton from './QueueButton'; -import ShuffleButton from './ShuffleButton'; -import SortButton from './SortButton'; -import LibraryViewMenu from './LibraryViewMenu'; -import ViewSettingsButton from './ViewSettingsButton'; -interface ItemsViewProps { - viewType: LibraryTab; - parentId: ParentId; - itemType: BaseItemKind[]; - collectionType?: CollectionType; - isPaginationEnabled?: boolean; - isBtnPlayAllEnabled?: boolean; - isBtnQueueEnabled?: boolean; - isBtnShuffleEnabled?: boolean; - isBtnSortEnabled?: boolean; - isBtnFilterEnabled?: boolean; - isBtnNewCollectionEnabled?: boolean; - isBtnNewPlaylistEnabled?: boolean; - isBtnGridListEnabled?: boolean; - isAlphabetPickerEnabled?: boolean; - noItemsMessage: string; -} - -const ItemsView: FC = ({ - viewType, - parentId, - collectionType, - isPaginationEnabled = true, - isBtnPlayAllEnabled = false, - isBtnQueueEnabled = false, - isBtnShuffleEnabled = false, - isBtnSortEnabled = true, - isBtnFilterEnabled = true, - isBtnNewCollectionEnabled = false, - isBtnNewPlaylistEnabled = false, - isBtnGridListEnabled = true, - isAlphabetPickerEnabled = true, - itemType, - noItemsMessage -}) => { - const [libraryViewSettings, setLibraryViewSettings] = - useLocalStorage( - getSettingsKey(viewType, parentId), - getDefaultLibraryViewSettings(viewType) - ); - const isSmallScreen = useMediaQuery((t: Theme) => t.breakpoints.up('sm')); +const ItemsView: FC = () => { + const { + id: parentId, + collectionType, + content, + itemsResult, + viewSettings, + setViewSettings + } = useLibrary(); + const viewType = content?.viewType ?? LibraryTab.Movies; + const libraryViewSettings = viewSettings ?? getDefaultLibraryViewSettings(viewType); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const setLibraryViewSettings = setViewSettings ?? ((action: SetStateAction) => { /* no-op */ }); + const { isAlphabetPickerEnabled, noItemsMessage } = content ?? {}; const { __legacyApiClient__ } = useApi(); - const { - isPending, - data: itemsResult, - isPlaceholderData, - refetch - } = useGetItemsViewByType( - viewType, - parentId, - itemType, - libraryViewSettings - ); - const { data: item } = useItem(parentId || undefined); const getListOptions = useCallback(() => { const listOptions: ListOptions = { - items: itemsResult?.Items ?? [], + items: itemsResult?.data?.Items ?? [], context: collectionType }; @@ -121,7 +58,7 @@ const ItemsView: FC = ({ } return listOptions; - }, [itemsResult?.Items, collectionType, viewType, libraryViewSettings.SortBy]); + }, [itemsResult?.data?.Items, collectionType, viewType, libraryViewSettings.SortBy]); const getCardOptions = useCallback(() => { let shape; @@ -145,16 +82,16 @@ const ItemsView: FC = ({ } const cardOptions: CardOptions = { - shape: shape, + shape, showTitle: libraryViewSettings.ShowTitle, showYear: libraryViewSettings.ShowYear, cardLayout: libraryViewSettings.CardLayout, centerText: true, context: collectionType, coverImage: true, - preferThumb: preferThumb, - preferDisc: preferDisc, - preferLogo: preferLogo, + preferThumb, + preferDisc, + preferLogo, overlayText: !libraryViewSettings.ShowTitle, imageType: libraryViewSettings.ImageType, queryKey: ['ItemsViewByType'], @@ -191,6 +128,7 @@ const ItemsView: FC = ({ return cardOptions; }, [ + __legacyApiClient__, libraryViewSettings.ShowTitle, libraryViewSettings.ImageType, libraryViewSettings.ShowYear, @@ -200,56 +138,34 @@ const ItemsView: FC = ({ ]); const getItems = useCallback(() => { - if (!itemsResult?.Items?.length) { - return ; + if (!itemsResult?.data?.Items?.length) { + return ; } if (libraryViewSettings.ViewMode === ViewMode.ListView) { return ( ); } return ( ); }, [ libraryViewSettings.ViewMode, - itemsResult?.Items, + itemsResult?.data?.Items, getListOptions, getCardOptions, noItemsMessage ]); - const totalRecordCount = itemsResult?.TotalRecordCount ?? 0; - const items = itemsResult?.Items ?? []; - const hasFilters = Object.values(libraryViewSettings.Filters ?? {}).some( - (filter) => !!filter - ); const hasSortName = libraryViewSettings.SortBy !== ItemSortBy.Random; - // Pagination - const startIndex = libraryViewSettings.StartIndex ?? 0; - const { libraryPageSize: paginationLimit } = useUserSettings(); - const paginationStart = totalRecordCount ? startIndex + 1 : 0; - const paginationEnd = paginationLimit ? - Math.min(startIndex + paginationLimit, totalRecordCount) : - totalRecordCount; - /** True if the data is larger than the page limit */ - const isPaginationRequired = paginationLimit > 0 && paginationLimit < totalRecordCount; - - let itemCountDisplay = '\u2219'; // Bullet "operator" character as a loading indicator - if (!isPending) { - itemCountDisplay = isPaginationRequired ? - globalize.translate('ListPaging', paginationStart, paginationEnd, totalRecordCount) : - totalRecordCount; - } - const itemsContainerClass = classNames( 'padded-left padded-right', libraryViewSettings.ViewMode === ViewMode.ListView ? @@ -259,156 +175,6 @@ const ItemsView: FC = ({ return ( - ({ - top: 0, - paddingTop: 6, - // This should render under the main AppBar and AlphabetPicker - zIndex: theme.zIndex.appBar - 2 - })} - > - - - - - {!isPending && ( - <> - - {isBtnPlayAllEnabled && totalRecordCount > 0 && ( - - )} - - {isBtnShuffleEnabled && totalRecordCount > 1 && ( - - )} - - {isBtnQueueEnabled && item && playbackManager.canQueue(item) && ( - - )} - - - {isBtnNewCollectionEnabled && } - {isBtnNewPlaylistEnabled && } - - )} - - - - - - - - - {isBtnFilterEnabled && ( - - )} - - {isBtnSortEnabled && ( - - )} - - {isBtnGridListEnabled && ( - - )} - - - {isPaginationEnabled && ( - - )} - - - - {isAlphabetPickerEnabled && hasSortName && ( = ({ /> )} - {isPending ? ( + {(!itemsResult || itemsResult.isPending) ? ( ) : ( {getItems()} diff --git a/src/apps/experimental/components/library/PageTabContent.tsx b/src/apps/experimental/components/library/PageTabContent.tsx index 7c41d8d884..484b48bb42 100644 --- a/src/apps/experimental/components/library/PageTabContent.tsx +++ b/src/apps/experimental/components/library/PageTabContent.tsx @@ -1,15 +1,15 @@ -import Box from '@mui/material/Box/Box'; import React, { type FC } from 'react'; + +import type { ParentId } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; +import type { LibraryTabContent } from 'types/libraryTabContent'; + +import GenresView from './GenresView'; +import GuideView from './GuideView'; +import ItemsView from './ItemsView'; +import ProgramsSectionView from './ProgramsSectionView'; import SuggestionsSectionView from './SuggestionsSectionView'; import UpcomingView from './UpcomingView'; -import GenresView from './GenresView'; -import ItemsView from './ItemsView'; -import GuideView from './GuideView'; -import ProgramsSectionView from './ProgramsSectionView'; -import { LibraryTab } from 'types/libraryTab'; -import type { ParentId } from 'types/library'; -import type { LibraryTabContent } from 'types/libraryTabContent'; -import LibraryViewMenu from './LibraryViewMenu'; interface PageTabContentProps { parentId: ParentId; @@ -19,108 +19,54 @@ interface PageTabContentProps { const PageTabContent: FC = ({ parentId, currentTab }) => { if (currentTab.viewType === LibraryTab.Suggestions) { return ( - <> - - - - - - + ); } if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) { return ( - <> - - - - - - + ); } if (currentTab.viewType === LibraryTab.Upcoming) { return ( - <> - - - - - - + ); } if (currentTab.viewType === LibraryTab.Genres) { return ( - <> - - - - - - + ); } if (currentTab.viewType === LibraryTab.Guide) { return ( - <> - - - - - - + ); } return ( - + ); }; diff --git a/src/apps/experimental/features/libraries/components/LibraryToolbar.tsx b/src/apps/experimental/features/libraries/components/LibraryToolbar.tsx new file mode 100644 index 0000000000..37d50d813f --- /dev/null +++ b/src/apps/experimental/features/libraries/components/LibraryToolbar.tsx @@ -0,0 +1,232 @@ +import Box from '@mui/material/Box'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Toolbar from '@mui/material/Toolbar'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import React, { type SetStateAction, useMemo, type FC } from 'react'; + +import FilterButton from 'apps/experimental/components/library/filter/FilterButton'; +import LibraryViewMenu from 'apps/experimental/components/library/LibraryViewMenu'; +import NewCollectionButton from 'apps/experimental/components/library/NewCollectionButton'; +import NewPlaylistButton from 'apps/experimental/components/library/NewPlaylistButton'; +import Pagination from 'apps/experimental/components/library/Pagination'; +import PlayAllButton from 'apps/experimental/components/library/PlayAllButton'; +import QueueButton from 'apps/experimental/components/library/QueueButton'; +import ShuffleButton from 'apps/experimental/components/library/ShuffleButton'; +import SortButton from 'apps/experimental/components/library/SortButton'; +import ViewSettingsButton from 'apps/experimental/components/library/ViewSettingsButton'; +import { playbackManager } from 'components/playback/playbackmanager'; +import { useItem } from 'hooks/useItem'; +import { useUserSettings } from 'hooks/useUserSettings'; +import globalize from 'lib/globalize'; +import type { LibraryViewSettings } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; + +import { useLibrary } from '../hooks/useLibrary'; +import { getDefaultLibraryViewSettings } from '../utils/settings'; + +/** Views that only show the menu, not the toolbar buttons */ +const MENU_ONLY_VIEWS = [ + LibraryTab.Genres, + LibraryTab.Guide, + LibraryTab.Suggestions, + LibraryTab.Programs, + LibraryTab.Recordings, + LibraryTab.Schedule, + LibraryTab.Upcoming +]; + +const LibraryToolbar: FC = () => { + const { + id: parentId, + collectionType, + content, + isLibraryPath, + itemsResult, + viewSettings, + setViewSettings + } = useLibrary(); + const viewType = content?.viewType ?? LibraryTab.Movies; + const libraryViewSettings = viewSettings ?? getDefaultLibraryViewSettings(viewType); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const setLibraryViewSettings = setViewSettings ?? ((action: SetStateAction) => { /* no-op */ }); + const { itemType, isPaginationEnabled, isBtnPlayAllEnabled, isBtnQueueEnabled, isBtnShuffleEnabled, isBtnSortEnabled, isBtnFilterEnabled, isBtnNewCollectionEnabled, isBtnNewPlaylistEnabled, isBtnGridListEnabled } = content ?? {}; + + const isSmallScreen = useMediaQuery(t => t.breakpoints.up('sm')); + + const { data: item } = useItem(parentId || undefined); + + const isPending = itemsResult?.isPending ?? true; + const totalRecordCount = itemsResult?.data?.TotalRecordCount ?? 0; + const items = itemsResult?.data?.Items ?? []; + const hasFilters = Object.values(viewSettings?.Filters ?? {}).some( + (filter) => !!filter + ); + + // Pagination + const startIndex = viewSettings?.StartIndex ?? 0; + const { libraryPageSize: paginationLimit } = useUserSettings(); + const paginationStart = totalRecordCount ? startIndex + 1 : 0; + const paginationEnd = paginationLimit ? + Math.min(startIndex + paginationLimit, totalRecordCount) : + totalRecordCount; + /** True if the data is larger than the page limit */ + const isPaginationRequired = paginationLimit > 0 && paginationLimit < totalRecordCount; + + const itemCountDisplay = useMemo(() => { + if (isPending) return '\u2219'; // Bullet "operator" character as a loading indicator + + return isPaginationRequired ? + globalize.translate('ListPaging', paginationStart, paginationEnd, totalRecordCount) : + totalRecordCount; + }, [isPending, isPaginationRequired, paginationStart, paginationEnd, totalRecordCount]); + + if (!isLibraryPath) return null; + + return ( + + + + + {!isPending && ( + <> + + {isBtnPlayAllEnabled && totalRecordCount > 0 && ( + + )} + + {isBtnShuffleEnabled && totalRecordCount > 1 && ( + + )} + + {isBtnQueueEnabled && item && playbackManager.canQueue(item) && ( + + )} + + + {isBtnNewCollectionEnabled && } + {isBtnNewPlaylistEnabled && } + + )} + + + {!MENU_ONLY_VIEWS.includes(viewType) && ( + + + + + + + {isBtnFilterEnabled && ( + + )} + + {isBtnSortEnabled && ( + + )} + + {isBtnGridListEnabled && ( + + )} + + + {isPaginationEnabled && ( + + )} + + )} + + ); +}; + +export default LibraryToolbar; diff --git a/src/apps/experimental/features/libraries/constants/libraryRoutes.ts b/src/apps/experimental/features/libraries/constants/libraryRoutes.ts index 224fe4487c..392d60755e 100644 --- a/src/apps/experimental/features/libraries/constants/libraryRoutes.ts +++ b/src/apps/experimental/features/libraries/constants/libraryRoutes.ts @@ -1,3 +1,5 @@ +import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; + import { LibraryTab } from 'types/libraryTab'; import { LibraryRoute } from '../types/LibraryRoute'; @@ -5,6 +7,7 @@ import { LibraryRoute } from '../types/LibraryRoute'; export const LibraryRoutes: LibraryRoute[] = [ { path: '/livetv', + type: CollectionType.Livetv, views: [ { index: 0, @@ -41,6 +44,7 @@ export const LibraryRoutes: LibraryRoute[] = [ }, { path: '/books', + type: CollectionType.Books, views: [ { index: 0, @@ -82,6 +86,7 @@ export const LibraryRoutes: LibraryRoute[] = [ }, { path: '/boxsets', + type: CollectionType.Boxsets, views: [ { index: 0, @@ -103,6 +108,7 @@ export const LibraryRoutes: LibraryRoute[] = [ }, { path: '/movies', + type: CollectionType.Movies, views: [ { index: 0, @@ -144,6 +150,7 @@ export const LibraryRoutes: LibraryRoute[] = [ }, { path: '/music', + type: CollectionType.Music, views: [ { index: 0, @@ -190,6 +197,7 @@ export const LibraryRoutes: LibraryRoute[] = [ }, { path: '/tv', + type: CollectionType.Tvshows, views: [ { index: 0, @@ -236,6 +244,7 @@ export const LibraryRoutes: LibraryRoute[] = [ }, { path: '/homevideos', + type: CollectionType.Homevideos, views: [ { index: 0, @@ -262,6 +271,7 @@ export const LibraryRoutes: LibraryRoute[] = [ }, { path: '/musicvideos', + type: CollectionType.Musicvideos, views: [ { index: 0, @@ -288,6 +298,7 @@ export const LibraryRoutes: LibraryRoute[] = [ }, { path: '/playlists', + type: CollectionType.Playlists, views: [ { index: 0, @@ -304,6 +315,7 @@ export const LibraryRoutes: LibraryRoute[] = [ }, { path: '/mixed', + type: CollectionType.Unknown, views: [ { index: 0, diff --git a/src/apps/experimental/features/libraries/constants/views/defaults.ts b/src/apps/experimental/features/libraries/constants/views/defaults.ts new file mode 100644 index 0000000000..729ba6fe41 --- /dev/null +++ b/src/apps/experimental/features/libraries/constants/views/defaults.ts @@ -0,0 +1,16 @@ +import { LibraryTabContent } from 'types/libraryTabContent'; + +const DEFAULT_VIEW_CONTENT: Partial = { + isPaginationEnabled: true, + isBtnPlayAllEnabled: false, + isBtnQueueEnabled: false, + isBtnShuffleEnabled: false, + isBtnSortEnabled: true, + isBtnFilterEnabled: true, + isBtnNewCollectionEnabled: false, + isBtnNewPlaylistEnabled: false, + isBtnGridListEnabled: true, + isAlphabetPickerEnabled: true +}; + +export default DEFAULT_VIEW_CONTENT; diff --git a/src/apps/experimental/features/libraries/hooks/useLibrary.tsx b/src/apps/experimental/features/libraries/hooks/useLibrary.tsx new file mode 100644 index 0000000000..4485666525 --- /dev/null +++ b/src/apps/experimental/features/libraries/hooks/useLibrary.tsx @@ -0,0 +1,79 @@ +import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import { UseQueryResult } from '@tanstack/react-query'; +import React, { type FC, type PropsWithChildren, createContext, useContext, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; + +import useCurrentTab from 'hooks/useCurrentTab'; +import { useGetItemsViewByType } from 'hooks/useFetchItems'; +import { useLocalStorage } from 'hooks/useLocalStorage'; +import { ItemDtoQueryResult } from 'types/base/models/item-dto-query-result'; +import { LibraryViewSettings } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; +import { LibraryTabContent } from 'types/libraryTabContent'; + +import { LibraryRoutes } from '../constants/libraryRoutes'; +import { isLibraryPath } from '../utils/path'; +import { getDefaultLibraryViewSettings, getSettingsKey } from '../utils/settings'; +import { getViewContent } from '../utils/viewContent'; + +interface LibraryState { + collectionType?: CollectionType; + content?: LibraryTabContent; + isLibraryPath: boolean; + id?: string; + itemsResult?: UseQueryResult; + viewSettings?: LibraryViewSettings; + setViewSettings?: React.Dispatch>; +} + +const DEFAULT_LIBRARY_STATE: LibraryState = { + isLibraryPath: false +}; + +export const LibraryContext = createContext(DEFAULT_LIBRARY_STATE); +export const useLibrary = () => useContext(LibraryContext); + +export const LibraryProvider: FC> = ({ children }) => { + const { pathname } = useLocation(); + const { libraryId, activeTab } = useCurrentTab(); + + const route = useMemo(() => LibraryRoutes.find(({ path }) => path === pathname), [pathname]); + const collectionType = route?.type; + const isLibPath = isLibraryPath(pathname); + const id = libraryId ?? undefined; + + const content = useMemo(() => collectionType && getViewContent(collectionType, activeTab), [collectionType, activeTab]); + const viewType = content?.viewType; + + // Local storage requires the view type to be known upfront so default to movies if unknown + const settingsViewType = viewType ?? LibraryTab.Movies; + const [viewSettings, setViewSettings] = + useLocalStorage( + getSettingsKey(settingsViewType, libraryId), + getDefaultLibraryViewSettings(settingsViewType) + ); + + const itemsResult = useGetItemsViewByType( + viewType, + libraryId, + content?.itemType, + viewSettings + ); + + const state = useMemo(() => ({ + ...DEFAULT_LIBRARY_STATE, + collectionType, + isLibraryPath: isLibPath, + id, + content, + viewSettings, + setViewSettings, + itemsResult + }), [collectionType, isLibPath, id, content, viewSettings, setViewSettings, itemsResult]); + + return ( + + {children} + + ); +}; diff --git a/src/apps/experimental/features/libraries/types/LibraryRoute.ts b/src/apps/experimental/features/libraries/types/LibraryRoute.ts index f05cfdb5f6..515d9bee2e 100644 --- a/src/apps/experimental/features/libraries/types/LibraryRoute.ts +++ b/src/apps/experimental/features/libraries/types/LibraryRoute.ts @@ -1,3 +1,4 @@ +import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { LibraryTab } from 'types/libraryTab'; interface LibraryViewDefinition { @@ -9,5 +10,6 @@ interface LibraryViewDefinition { export interface LibraryRoute { path: string, + type: CollectionType, views: LibraryViewDefinition[] } diff --git a/src/apps/experimental/features/libraries/utils/settings.ts b/src/apps/experimental/features/libraries/utils/settings.ts new file mode 100644 index 0000000000..79eae16e3e --- /dev/null +++ b/src/apps/experimental/features/libraries/utils/settings.ts @@ -0,0 +1,32 @@ + +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'; +import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; + +import { type ParentId, ViewMode, type LibraryViewSettings } from 'types/library'; +import { LibraryTab } from 'types/libraryTab'; + +export const getDefaultSortBy = (viewType: LibraryTab) => { + if (viewType === LibraryTab.Episodes) { + return ItemSortBy.SeriesSortName; + } + + return ItemSortBy.SortName; +}; + +export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryViewSettings => { + return { + ShowTitle: true, + ShowYear: true, + ViewMode: viewType === LibraryTab.Songs ? ViewMode.ListView : ViewMode.GridView, + ImageType: viewType === LibraryTab.Studios ? ImageType.Thumb : ImageType.Primary, + CardLayout: false, + SortBy: getDefaultSortBy(viewType), + SortOrder: SortOrder.Ascending, + StartIndex: 0 + }; +}; + +export const getSettingsKey = (viewType: LibraryTab, parentId: ParentId) => { + return `${viewType} - ${parentId}`; +}; diff --git a/src/apps/experimental/features/libraries/utils/viewContent.ts b/src/apps/experimental/features/libraries/utils/viewContent.ts new file mode 100644 index 0000000000..c0eabfa7d1 --- /dev/null +++ b/src/apps/experimental/features/libraries/utils/viewContent.ts @@ -0,0 +1,17 @@ +import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; + +import { LibraryTabContent } from 'types/libraryTabContent'; + +import defaultViewContent from '../constants/views/defaults'; +import viewsByKind from '../constants/views'; + +export function getViewContent( + collectionType: CollectionType, + viewIndex: number +): LibraryTabContent { + const viewContent = viewsByKind[collectionType][viewIndex]; + return { + ...defaultViewContent, + ...viewContent + }; +} diff --git a/src/hooks/useFetchItems.ts b/src/hooks/useFetchItems.ts index 1f9bdc6bea..919cc7964b 100644 --- a/src/hooks/useFetchItems.ts +++ b/src/hooks/useFetchItems.ts @@ -20,7 +20,7 @@ import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api' import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api'; import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api'; import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api'; -import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import datetime from 'scripts/datetime'; import globalize from 'lib/globalize'; @@ -395,9 +395,9 @@ const fetchGetItemsViewByType = async ( }; export const useGetItemsViewByType = ( - viewType: LibraryTab, + viewType: LibraryTab | undefined, parentId: ParentId, - itemType: BaseItemKind[], + itemType: BaseItemKind[] = [], libraryViewSettings: LibraryViewSettings ) => { const currentApi = useApi(); @@ -414,15 +414,15 @@ export const useGetItemsViewByType = ( queryFn: ({ signal }) => fetchGetItemsViewByType( currentApi, - viewType, + viewType!, parentId, itemType, - libraryViewSettings, + libraryViewSettings!, { signal } ), refetchOnWindowFocus: false, - placeholderData : keepPreviousData, enabled: !!currentApi.api && !!currentApi.user?.Id + && viewType && [ LibraryTab.Movies, LibraryTab.Favorites, diff --git a/src/types/libraryTabContent.ts b/src/types/libraryTabContent.ts index 6e872ae475..3efacd378b 100644 --- a/src/types/libraryTabContent.ts +++ b/src/types/libraryTabContent.ts @@ -28,5 +28,3 @@ export interface LibraryTabContent { isAlphabetPickerEnabled?: boolean; noItemsMessage?: string; } - -export type LibraryTabMapping = Record; diff --git a/src/utils/items.ts b/src/utils/items.ts index eb688bc808..ee406decfd 100644 --- a/src/utils/items.ts +++ b/src/utils/items.ts @@ -1,10 +1,9 @@ import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; -import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'; -import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; + import * as userSettings from 'scripts/settings/userSettings'; import layoutManager from 'components/layoutManager'; -import { EpisodeFilter, FeatureFilters, LibraryViewSettings, ParentId, VideoBasicFilter, ViewMode } from '../types/library'; +import { EpisodeFilter, FeatureFilters, LibraryViewSettings, VideoBasicFilter } from 'types/library'; import { LibraryTab } from 'types/libraryTab'; import type { AttributesOpts, DataAttributes } from 'types/dataAttributes'; @@ -142,31 +141,6 @@ export const getFiltersQuery = ( }; }; -export const getSettingsKey = (viewType: LibraryTab, parentId: ParentId) => { - return `${viewType} - ${parentId}`; -}; - -export const getDefaultSortBy = (viewType: LibraryTab) => { - if (viewType === LibraryTab.Episodes) { - return ItemSortBy.SeriesSortName; - } - - return ItemSortBy.SortName; -}; - -export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryViewSettings => { - return { - ShowTitle: true, - ShowYear: true, - ViewMode: viewType === LibraryTab.Songs ? ViewMode.ListView : ViewMode.GridView, - ImageType: viewType === LibraryTab.Studios ? ImageType.Thumb : ImageType.Primary, - CardLayout: false, - SortBy: getDefaultSortBy(viewType), - SortOrder: SortOrder.Ascending, - StartIndex: 0 - }; -}; - export function getDataAttributes( opts: AttributesOpts ): DataAttributes { @@ -193,4 +167,3 @@ export function getDataAttributes( 'data-enddate': opts.itemEndDate?.toString() }; } -