Merge library toolbar into main AppBar

This commit is contained in:
Bill Thornton
2026-06-11 18:14:26 -04:00
parent cdbf4a7bd0
commit 8ee0a12030
14 changed files with 476 additions and 402 deletions
+6 -2
View File
@@ -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 (
<>
<LibraryProvider>
<Box
sx={{
position: 'relative',
@@ -45,6 +48,7 @@ export const Component = () => {
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
{isLibraryPath(location.pathname) && <LibraryToolbar />}
</OffsetAppBar>
{
@@ -73,6 +77,6 @@ export const Component = () => {
</Box>
<ThemeCss />
<CustomCss />
</>
</LibraryProvider>
);
};
@@ -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'
}}
/>;
};
@@ -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<ItemsViewProps> = ({
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<LibraryViewSettings>(
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<LibraryViewSettings>) => { /* 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<ItemsViewProps> = ({
}
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<ItemsViewProps> = ({
}
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<ItemsViewProps> = ({
return cardOptions;
}, [
__legacyApiClient__,
libraryViewSettings.ShowTitle,
libraryViewSettings.ImageType,
libraryViewSettings.ShowYear,
@@ -200,56 +138,34 @@ const ItemsView: FC<ItemsViewProps> = ({
]);
const getItems = useCallback(() => {
if (!itemsResult?.Items?.length) {
return <NoItemsMessage message={noItemsMessage} />;
if (!itemsResult?.data?.Items?.length) {
return <NoItemsMessage message={noItemsMessage ?? 'MessageNoItemsAvailable'} />;
}
if (libraryViewSettings.ViewMode === ViewMode.ListView) {
return (
<Lists
items={itemsResult?.Items ?? []}
items={itemsResult?.data?.Items ?? []}
listOptions={getListOptions()}
/>
);
}
return (
<Cards
items={itemsResult?.Items ?? []}
items={itemsResult?.data?.Items ?? []}
cardOptions={getCardOptions()}
/>
);
}, [
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<ItemsViewProps> = ({
return (
<Box className='padded-bottom-page'>
<OffsetAppBar
// eslint-disable-next-line react/jsx-no-bind
sx={theme => ({
top: 0,
paddingTop: 6,
// This should render under the main AppBar and AlphabetPicker
zIndex: theme.zIndex.appBar - 2
})}
>
<Toolbar
className='padded-left padded-right'
sx={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center'
}}
>
<LibraryViewMenu />
<Box
sx={{
display: 'flex',
flexGrow: {
xs: 1,
sm: 0
},
justifyContent: 'flex-end',
marginLeft: 1
}}
>
{!isPending && (
<>
<ButtonGroup
variant='contained'
>
{isBtnPlayAllEnabled && totalRecordCount > 0 && (
<PlayAllButton
item={item}
items={items}
viewType={viewType}
collectionType={collectionType}
hasFilters={hasFilters}
isTextVisible={isSmallScreen}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnShuffleEnabled && totalRecordCount > 1 && (
<ShuffleButton
item={item}
items={items}
viewType={viewType}
collectionType={collectionType}
hasFilters={hasFilters}
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnQueueEnabled && item && playbackManager.canQueue(item) && (
<QueueButton
item={item}
items={items}
hasFilters={hasFilters}
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled && !isBtnShuffleEnabled}
/>
)}
</ButtonGroup>
{isBtnNewCollectionEnabled && <NewCollectionButton isTextVisible={isSmallScreen} />}
{isBtnNewPlaylistEnabled && <NewPlaylistButton isTextVisible={isSmallScreen} />}
</>
)}
</Box>
<Stack
direction='row'
spacing={1}
sx={{
justifyContent: {
xs: 'auto',
sm: 'end'
},
flexBasis: {
xs: '100%',
sm: 'auto'
},
flexGrow: 1,
marginTop: 0.5,
marginBottom: 0.5
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexGrow: {
xs: 1,
sm: 0
}
}}
>
<Chip label={itemCountDisplay} />
</Box>
<ButtonGroup
color='inherit'
variant='text'
>
{isBtnFilterEnabled && (
<FilterButton
parentId={parentId}
itemType={itemType}
viewType={viewType}
hasFilters={hasFilters}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
{isBtnSortEnabled && (
<SortButton
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
{isBtnGridListEnabled && (
<ViewSettingsButton
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
</ButtonGroup>
{isPaginationEnabled && (
<Pagination
setLibraryViewSettings={setLibraryViewSettings}
index={startIndex}
pageSize={paginationLimit}
total={totalRecordCount}
disabled={isPending || !isPaginationRequired || isPlaceholderData}
/>
)}
</Stack>
</Toolbar>
</OffsetAppBar>
{isAlphabetPickerEnabled && hasSortName && (
<AlphabetPicker
libraryViewSettings={libraryViewSettings}
@@ -416,13 +182,13 @@ const ItemsView: FC<ItemsViewProps> = ({
/>
)}
{isPending ? (
{(!itemsResult || itemsResult.isPending) ? (
<Loading />
) : (
<ItemsContainer
className={itemsContainerClass}
parentId={parentId}
reloadItems={refetch}
reloadItems={itemsResult?.refetch}
queryKey={['ItemsViewByType']}
>
{getItems()}
@@ -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<PageTabContentProps> = ({ parentId, currentTab }) => {
if (currentTab.viewType === LibraryTab.Suggestions) {
return (
<>
<Box className='padded-top padded-left padded-right padded-bottom'>
<LibraryViewMenu />
</Box>
<SuggestionsSectionView
parentId={parentId}
sectionType={
currentTab.sectionsView?.suggestionSections ?? []
}
isMovieRecommendationEnabled={
currentTab.sectionsView?.isMovieRecommendations
}
/>
</>
<SuggestionsSectionView
parentId={parentId}
sectionType={
currentTab.sectionsView?.suggestionSections ?? []
}
isMovieRecommendationEnabled={
currentTab.sectionsView?.isMovieRecommendations
}
/>
);
}
if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) {
return (
<>
<Box className='padded-top padded-left padded-right padded-bottom'>
<LibraryViewMenu />
</Box>
<ProgramsSectionView
parentId={parentId}
sectionType={
currentTab.sectionsView?.programSections ?? []
}
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
/>
</>
<ProgramsSectionView
parentId={parentId}
sectionType={
currentTab.sectionsView?.programSections ?? []
}
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
/>
);
}
if (currentTab.viewType === LibraryTab.Upcoming) {
return (
<>
<Box className='padded-top padded-left padded-right padded-bottom'>
<LibraryViewMenu />
</Box>
<UpcomingView parentId={parentId} />
</>
<UpcomingView parentId={parentId} />
);
}
if (currentTab.viewType === LibraryTab.Genres) {
return (
<>
<Box className='padded-top padded-left padded-right padded-bottom'>
<LibraryViewMenu />
</Box>
<GenresView
parentId={parentId}
collectionType={currentTab.collectionType ?? undefined}
itemType={currentTab.itemType || []}
/>
</>
<GenresView
parentId={parentId}
collectionType={currentTab.collectionType ?? undefined}
itemType={currentTab.itemType || []}
/>
);
}
if (currentTab.viewType === LibraryTab.Guide) {
return (
<>
<Box
className='padded-top padded-left padded-right padded-bottom'
sx={{
position: 'relative',
zIndex: 2
}}
>
<LibraryViewMenu />
</Box>
<GuideView />
</>
<GuideView />
);
}
return (
<ItemsView
viewType={currentTab.viewType}
parentId={parentId}
collectionType={currentTab.collectionType ?? undefined}
isPaginationEnabled={currentTab.isPaginationEnabled}
isBtnPlayAllEnabled={currentTab.isBtnPlayAllEnabled}
isBtnQueueEnabled={currentTab.isBtnQueueEnabled}
isBtnShuffleEnabled={currentTab.isBtnShuffleEnabled}
isBtnNewCollectionEnabled={currentTab.isBtnNewCollectionEnabled}
isBtnNewPlaylistEnabled={currentTab.isBtnNewPlaylistEnabled}
isBtnFilterEnabled={currentTab.isBtnFilterEnabled}
isBtnGridListEnabled={currentTab.isBtnGridListEnabled}
isBtnSortEnabled={currentTab.isBtnSortEnabled}
isAlphabetPickerEnabled={currentTab.isAlphabetPickerEnabled}
itemType={currentTab.itemType || []}
noItemsMessage={
currentTab.noItemsMessage || 'MessageNoItemsAvailable'
}
/>
<ItemsView />
);
};
@@ -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<LibraryViewSettings>) => { /* 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 (
<Toolbar
className='padded-left padded-right'
sx={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center'
}}
>
<LibraryViewMenu />
<Box
sx={{
display: 'flex',
flexGrow: {
xs: 1,
sm: 0
},
justifyContent: 'flex-end',
marginLeft: 1
}}
>
{!isPending && (
<>
<ButtonGroup
variant='contained'
>
{isBtnPlayAllEnabled && totalRecordCount > 0 && (
<PlayAllButton
item={item}
items={items}
viewType={viewType}
collectionType={collectionType}
hasFilters={hasFilters}
isTextVisible={isSmallScreen}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnShuffleEnabled && totalRecordCount > 1 && (
<ShuffleButton
item={item}
items={items}
viewType={viewType}
collectionType={collectionType}
hasFilters={hasFilters}
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnQueueEnabled && item && playbackManager.canQueue(item) && (
<QueueButton
item={item}
items={items}
hasFilters={hasFilters}
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled && !isBtnShuffleEnabled}
/>
)}
</ButtonGroup>
{isBtnNewCollectionEnabled && <NewCollectionButton isTextVisible={isSmallScreen} />}
{isBtnNewPlaylistEnabled && <NewPlaylistButton isTextVisible={isSmallScreen} />}
</>
)}
</Box>
{!MENU_ONLY_VIEWS.includes(viewType) && (
<Stack
direction='row'
spacing={1}
sx={{
justifyContent: {
xs: 'auto',
sm: 'end'
},
flexBasis: {
xs: '100%',
sm: 'auto'
},
flexGrow: 1,
marginTop: 0.5,
marginBottom: 0.5
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexGrow: {
xs: 1,
sm: 0
}
}}
>
<Chip label={itemCountDisplay} />
</Box>
<ButtonGroup
color='inherit'
variant='text'
>
{isBtnFilterEnabled && (
<FilterButton
parentId={parentId}
itemType={itemType ?? []}
viewType={viewType}
hasFilters={hasFilters}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
{isBtnSortEnabled && (
<SortButton
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
{isBtnGridListEnabled && (
<ViewSettingsButton
viewType={viewType}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
</ButtonGroup>
{isPaginationEnabled && (
<Pagination
setLibraryViewSettings={setLibraryViewSettings}
index={startIndex}
pageSize={paginationLimit}
total={totalRecordCount}
disabled={isPending || !isPaginationRequired || itemsResult?.isPlaceholderData}
/>
)}
</Stack>
)}
</Toolbar>
);
};
export default LibraryToolbar;
@@ -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,
@@ -0,0 +1,16 @@
import { LibraryTabContent } from 'types/libraryTabContent';
const DEFAULT_VIEW_CONTENT: Partial<LibraryTabContent> = {
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;
@@ -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<ItemDtoQueryResult | undefined, Error>;
viewSettings?: LibraryViewSettings;
setViewSettings?: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const DEFAULT_LIBRARY_STATE: LibraryState = {
isLibraryPath: false
};
export const LibraryContext = createContext<LibraryState>(DEFAULT_LIBRARY_STATE);
export const useLibrary = () => useContext(LibraryContext);
export const LibraryProvider: FC<PropsWithChildren<unknown>> = ({ 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<LibraryViewSettings>(
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 (
<LibraryContext.Provider value={state}>
{children}
</LibraryContext.Provider>
);
};
@@ -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[]
}
@@ -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}`;
};
@@ -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
};
}
+6 -6
View File
@@ -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,
-2
View File
@@ -28,5 +28,3 @@ export interface LibraryTabContent {
isAlphabetPickerEnabled?: boolean;
noItemsMessage?: string;
}
export type LibraryTabMapping = Record<number, LibraryTabContent>;
+2 -29
View File
@@ -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()
};
}