mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Refactor: viewitemcontainer
This commit is contained in:
parent
550ad476af
commit
c61df2eb92
28 changed files with 520 additions and 1001 deletions
|
@ -5,10 +5,11 @@ import globalize from 'scripts/globalize';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import GenresSectionContainer from './GenresSectionContainer';
|
import GenresSectionContainer from './GenresSectionContainer';
|
||||||
import { CollectionType } from 'types/collectionType';
|
import { CollectionType } from 'types/collectionType';
|
||||||
|
import { ParentId } from 'types/library';
|
||||||
|
|
||||||
interface GenresItemsContainerProps {
|
interface GenresItemsContainerProps {
|
||||||
parentId?: string | null;
|
parentId: ParentId;
|
||||||
collectionType?: CollectionType;
|
collectionType: CollectionType;
|
||||||
itemType: BaseItemKind;
|
itemType: BaseItemKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,11 @@ import Loading from 'components/loading/LoadingComponent';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import SectionContainer from './SectionContainer';
|
import SectionContainer from './SectionContainer';
|
||||||
import { CollectionType } from 'types/collectionType';
|
import { CollectionType } from 'types/collectionType';
|
||||||
|
import { ParentId } from 'types/library';
|
||||||
|
|
||||||
interface GenresSectionContainerProps {
|
interface GenresSectionContainerProps {
|
||||||
parentId?: string | null;
|
parentId: ParentId;
|
||||||
collectionType?: CollectionType;
|
collectionType: CollectionType;
|
||||||
itemType: BaseItemKind;
|
itemType: BaseItemKind;
|
||||||
genre: BaseItemDto;
|
genre: BaseItemDto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import React, { FC, useEffect, useRef } from 'react';
|
import React, { FC, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
import ItemsContainerElement from 'elements/ItemsContainerElement';
|
||||||
import imageLoader from '../images/imageLoader';
|
import imageLoader from 'components/images/imageLoader';
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
import { ViewQuerySettings } from '../../types/interface';
|
import { LibraryViewSettings, ViewMode } from 'types/library';
|
||||||
|
|
||||||
interface ItemsContainerI {
|
interface ItemsContainerI {
|
||||||
viewQuerySettings: ViewQuerySettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
getItemsHtml: () => string
|
getItemsHtml: () => string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemsContainer: FC<ItemsContainerI> = ({ viewQuerySettings, getItemsHtml }) => {
|
const ItemsContainer: FC<ItemsContainerI> = ({ libraryViewSettings, getItemsHtml }) => {
|
||||||
const element = useRef<HTMLDivElement>(null);
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -19,7 +19,7 @@ const ItemsContainer: FC<ItemsContainerI> = ({ viewQuerySettings, getItemsHtml }
|
||||||
imageLoader.lazyChildren(itemsContainer);
|
imageLoader.lazyChildren(itemsContainer);
|
||||||
}, [getItemsHtml]);
|
}, [getItemsHtml]);
|
||||||
|
|
||||||
const cssClass = viewQuerySettings.imageType == 'list' ? 'vertical-list' : 'vertical-wrap';
|
const cssClass = libraryViewSettings.ViewMode === ViewMode.ListView ? 'vertical-list' : 'vertical-wrap';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={element}>
|
<div ref={element}>
|
272
src/apps/experimental/components/library/ItemsView.tsx
Normal file
272
src/apps/experimental/components/library/ItemsView.tsx
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
|
import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems';
|
||||||
|
import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items';
|
||||||
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
import listview from 'components/listview/listview';
|
||||||
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
|
import { playbackManager } from 'components/playback/playbackmanager';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import AlphabetPicker from './AlphabetPicker';
|
||||||
|
import FilterButton from './filter/FilterButton';
|
||||||
|
import ItemsContainer from './ItemsContainer';
|
||||||
|
import NewCollectionButton from './NewCollectionButton';
|
||||||
|
import Pagination from './Pagination';
|
||||||
|
import PlayAllButton from './PlayAllButton';
|
||||||
|
import QueueButton from './QueueButton';
|
||||||
|
import ShuffleButton from './ShuffleButton';
|
||||||
|
import SortButton from './SortButton';
|
||||||
|
import GridListViewButton from './GridListViewButton';
|
||||||
|
import { LibraryViewSettings, ParentId, ViewMode } from 'types/library';
|
||||||
|
import { CollectionType } from 'types/collectionType';
|
||||||
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
|
import { CardOptions } from 'types/cardOptions';
|
||||||
|
|
||||||
|
interface ItemsViewProps {
|
||||||
|
viewType: LibraryTab;
|
||||||
|
parentId: ParentId;
|
||||||
|
itemType: BaseItemKind[];
|
||||||
|
collectionType?: CollectionType;
|
||||||
|
isBtnPlayAllEnabled?: boolean;
|
||||||
|
isBtnQueueEnabled?: boolean;
|
||||||
|
isBtnShuffleEnabled?: boolean;
|
||||||
|
isBtnSortEnabled?: boolean;
|
||||||
|
isBtnFilterEnabled?: boolean;
|
||||||
|
isBtnNewCollectionEnabled?: boolean;
|
||||||
|
isBtnGridListEnabled?: boolean;
|
||||||
|
isAlphabetPickerEnabled?: boolean;
|
||||||
|
noItemsMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemsView: FC<ItemsViewProps> = ({
|
||||||
|
viewType,
|
||||||
|
parentId,
|
||||||
|
collectionType,
|
||||||
|
isBtnPlayAllEnabled = false,
|
||||||
|
isBtnQueueEnabled = false,
|
||||||
|
isBtnShuffleEnabled = false,
|
||||||
|
isBtnSortEnabled = true,
|
||||||
|
isBtnFilterEnabled = true,
|
||||||
|
isBtnNewCollectionEnabled = false,
|
||||||
|
isBtnGridListEnabled = true,
|
||||||
|
isAlphabetPickerEnabled = true,
|
||||||
|
itemType,
|
||||||
|
noItemsMessage
|
||||||
|
}) => {
|
||||||
|
const [libraryViewSettings, setLibraryViewSettings] =
|
||||||
|
useLocalStorage<LibraryViewSettings>(
|
||||||
|
getSettingsKey(viewType, parentId),
|
||||||
|
getDefaultLibraryViewSettings(viewType)
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
data: itemsResult,
|
||||||
|
isPreviousData
|
||||||
|
} = useGetItemsViewByType(
|
||||||
|
viewType,
|
||||||
|
parentId,
|
||||||
|
itemType,
|
||||||
|
libraryViewSettings
|
||||||
|
);
|
||||||
|
const { data: item } = useGetItem(parentId);
|
||||||
|
|
||||||
|
const getCardOptions = useCallback(() => {
|
||||||
|
let shape;
|
||||||
|
let preferThumb;
|
||||||
|
let preferDisc;
|
||||||
|
let preferLogo;
|
||||||
|
let lines = libraryViewSettings.ShowTitle ? 2 : 0;
|
||||||
|
|
||||||
|
if (libraryViewSettings.ImageType === ImageType.Banner) {
|
||||||
|
shape = 'banner';
|
||||||
|
} else if (libraryViewSettings.ImageType === ImageType.Disc) {
|
||||||
|
shape = 'square';
|
||||||
|
preferDisc = true;
|
||||||
|
} else if (libraryViewSettings.ImageType === ImageType.Logo) {
|
||||||
|
shape = 'backdrop';
|
||||||
|
preferLogo = true;
|
||||||
|
} else if (libraryViewSettings.ImageType === ImageType.Thumb) {
|
||||||
|
shape = 'backdrop';
|
||||||
|
preferThumb = true;
|
||||||
|
} else {
|
||||||
|
shape = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardOptions: CardOptions = {
|
||||||
|
shape: shape,
|
||||||
|
showTitle: libraryViewSettings.ShowTitle,
|
||||||
|
showYear: libraryViewSettings.ShowYear,
|
||||||
|
cardLayout: libraryViewSettings.CardLayout,
|
||||||
|
centerText: true,
|
||||||
|
context: collectionType,
|
||||||
|
coverImage: true,
|
||||||
|
preferThumb: preferThumb,
|
||||||
|
preferDisc: preferDisc,
|
||||||
|
preferLogo: preferLogo,
|
||||||
|
overlayPlayButton: false,
|
||||||
|
overlayMoreButton: true,
|
||||||
|
overlayText: !libraryViewSettings.ShowTitle
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
viewType === LibraryTab.Songs
|
||||||
|
|| viewType === LibraryTab.Albums
|
||||||
|
|| viewType === LibraryTab.Episodes
|
||||||
|
) {
|
||||||
|
cardOptions.showParentTitle = libraryViewSettings.ShowTitle;
|
||||||
|
} else if (viewType === LibraryTab.Artists) {
|
||||||
|
cardOptions.showYear = false;
|
||||||
|
lines = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cardOptions.lines = lines;
|
||||||
|
|
||||||
|
return cardOptions;
|
||||||
|
}, [
|
||||||
|
libraryViewSettings.ShowTitle,
|
||||||
|
libraryViewSettings.ImageType,
|
||||||
|
libraryViewSettings.ShowYear,
|
||||||
|
libraryViewSettings.CardLayout,
|
||||||
|
collectionType,
|
||||||
|
viewType
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getItemsHtml = useCallback(() => {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (libraryViewSettings.ViewMode === ViewMode.ListView) {
|
||||||
|
html = listview.getListViewHtml({
|
||||||
|
items: itemsResult?.Items ?? [],
|
||||||
|
context: collectionType
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html = cardBuilder.getCardsHtml(
|
||||||
|
itemsResult?.Items ?? [],
|
||||||
|
getCardOptions()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemsResult?.Items?.length) {
|
||||||
|
html += '<div class="noItemsMessage centerMessage">';
|
||||||
|
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
|
||||||
|
html += '<p>' + globalize.translate(noItemsMessage) + '</p>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}, [
|
||||||
|
libraryViewSettings.ViewMode,
|
||||||
|
itemsResult?.Items,
|
||||||
|
collectionType,
|
||||||
|
getCardOptions,
|
||||||
|
noItemsMessage
|
||||||
|
]);
|
||||||
|
|
||||||
|
const totalRecordCount = itemsResult?.TotalRecordCount ?? 0;
|
||||||
|
const items = itemsResult?.Items ?? [];
|
||||||
|
const hasFilters = Object.values(libraryViewSettings.Filters ?? {}).some(
|
||||||
|
(filter) => !!filter
|
||||||
|
);
|
||||||
|
const hasSortName = libraryViewSettings.SortBy.includes(
|
||||||
|
ItemSortBy.SortName
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||||
|
<Pagination
|
||||||
|
totalRecordCount={totalRecordCount}
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
isPreviousData={isPreviousData}
|
||||||
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isBtnPlayAllEnabled && (
|
||||||
|
<PlayAllButton
|
||||||
|
item={item}
|
||||||
|
items={items}
|
||||||
|
viewType={viewType}
|
||||||
|
hasFilters={hasFilters}
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isBtnQueueEnabled
|
||||||
|
&& item
|
||||||
|
&& playbackManager.canQueue(item) && (
|
||||||
|
<QueueButton
|
||||||
|
item={item}
|
||||||
|
items={items}
|
||||||
|
hasFilters={hasFilters}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isBtnShuffleEnabled && totalRecordCount > 1 && (
|
||||||
|
<ShuffleButton
|
||||||
|
item={item}
|
||||||
|
items={items}
|
||||||
|
viewType={viewType}
|
||||||
|
hasFilters={hasFilters}
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isBtnSortEnabled && (
|
||||||
|
<SortButton
|
||||||
|
viewType={viewType}
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isBtnFilterEnabled && (
|
||||||
|
<FilterButton
|
||||||
|
parentId={parentId}
|
||||||
|
itemType={itemType}
|
||||||
|
viewType={viewType}
|
||||||
|
hasFilters={hasFilters}
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isBtnNewCollectionEnabled && <NewCollectionButton />}
|
||||||
|
{isBtnGridListEnabled && (
|
||||||
|
<GridListViewButton
|
||||||
|
viewType={viewType}
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{isAlphabetPickerEnabled && hasSortName && (
|
||||||
|
<AlphabetPicker
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
|
<ItemsContainer
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
getItemsHtml={getItemsHtml}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||||
|
<Pagination
|
||||||
|
totalRecordCount={totalRecordCount}
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
isPreviousData={isPreviousData}
|
||||||
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItemsView;
|
|
@ -13,15 +13,17 @@ interface PaginationProps {
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
|
isPreviousData: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Pagination: FC<PaginationProps> = ({
|
const Pagination: FC<PaginationProps> = ({
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings,
|
setLibraryViewSettings,
|
||||||
totalRecordCount
|
totalRecordCount,
|
||||||
|
isPreviousData
|
||||||
}) => {
|
}) => {
|
||||||
const limit = userSettings.libraryPageSize(undefined);
|
const limit = userSettings.libraryPageSize(undefined);
|
||||||
const startIndex = libraryViewSettings.StartIndex || 0;
|
const startIndex = libraryViewSettings.StartIndex ?? 0;
|
||||||
const recordsStart = totalRecordCount ? startIndex + 1 : 0;
|
const recordsStart = totalRecordCount ? startIndex + 1 : 0;
|
||||||
const recordsEnd = limit ?
|
const recordsEnd = limit ?
|
||||||
Math.min(startIndex + limit, totalRecordCount) :
|
Math.min(startIndex + limit, totalRecordCount) :
|
||||||
|
@ -29,23 +31,19 @@ const Pagination: FC<PaginationProps> = ({
|
||||||
const showControls = limit > 0 && limit < totalRecordCount;
|
const showControls = limit > 0 && limit < totalRecordCount;
|
||||||
|
|
||||||
const onNextPageClick = useCallback(() => {
|
const onNextPageClick = useCallback(() => {
|
||||||
if (limit > 0) {
|
|
||||||
const newIndex = startIndex + limit;
|
const newIndex = startIndex + limit;
|
||||||
setLibraryViewSettings((prevState) => ({
|
setLibraryViewSettings((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
StartIndex: newIndex
|
StartIndex: newIndex
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
}, [limit, setLibraryViewSettings, startIndex]);
|
}, [limit, setLibraryViewSettings, startIndex]);
|
||||||
|
|
||||||
const onPreviousPageClick = useCallback(() => {
|
const onPreviousPageClick = useCallback(() => {
|
||||||
if (limit > 0) {
|
|
||||||
const newIndex = Math.max(0, startIndex - limit);
|
const newIndex = Math.max(0, startIndex - limit);
|
||||||
setLibraryViewSettings((prevState) => ({
|
setLibraryViewSettings((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
StartIndex: newIndex
|
StartIndex: newIndex
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
}, [limit, setLibraryViewSettings, startIndex]);
|
}, [limit, setLibraryViewSettings, startIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -67,7 +65,7 @@ const Pagination: FC<PaginationProps> = ({
|
||||||
<IconButton
|
<IconButton
|
||||||
title={globalize.translate('Previous')}
|
title={globalize.translate('Previous')}
|
||||||
className='paper-icon-button-light btnPreviousPage autoSize'
|
className='paper-icon-button-light btnPreviousPage autoSize'
|
||||||
disabled={startIndex == 0}
|
disabled={startIndex == 0 || isPreviousData}
|
||||||
onClick={onPreviousPageClick}
|
onClick={onPreviousPageClick}
|
||||||
>
|
>
|
||||||
<ArrowBackIcon />
|
<ArrowBackIcon />
|
||||||
|
@ -76,7 +74,7 @@ const Pagination: FC<PaginationProps> = ({
|
||||||
<IconButton
|
<IconButton
|
||||||
title={globalize.translate('Next')}
|
title={globalize.translate('Next')}
|
||||||
className='paper-icon-button-light btnNextPage autoSize'
|
className='paper-icon-button-light btnNextPage autoSize'
|
||||||
disabled={startIndex + limit >= totalRecordCount }
|
disabled={startIndex + limit >= totalRecordCount || isPreviousData }
|
||||||
onClick={onNextPageClick}
|
onClick={onNextPageClick}
|
||||||
>
|
>
|
||||||
<ArrowForwardIcon />
|
<ArrowForwardIcon />
|
||||||
|
|
|
@ -49,7 +49,7 @@ const RecommendationContainer: FC<RecommendationContainerProps> = ({
|
||||||
return (
|
return (
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
sectionTitle={escapeHTML(title)}
|
sectionTitle={escapeHTML(title)}
|
||||||
items={recommendation.Items || []}
|
items={recommendation.Items ?? []}
|
||||||
cardOptions={{
|
cardOptions={{
|
||||||
shape: 'overflowPortrait',
|
shape: 'overflowPortrait',
|
||||||
showYear: true,
|
showYear: true,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React, { FC } from 'react';
|
||||||
import * as userSettings from 'scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import SuggestionsSectionContainer from './SuggestionsSectionContainer';
|
import SuggestionsSectionContainer from './SuggestionsSectionContainer';
|
||||||
import { Sections, SectionsView, SectionsViewType } from 'types/suggestionsSections';
|
import { Sections, SectionsView, SectionsViewType } from 'types/suggestionsSections';
|
||||||
|
import { ParentId } from 'types/library';
|
||||||
|
|
||||||
const getSuggestionsSections = (): Sections[] => {
|
const getSuggestionsSections = (): Sections[] => {
|
||||||
return [
|
return [
|
||||||
|
@ -178,7 +179,7 @@ const getSuggestionsSections = (): Sections[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SuggestionsItemsContainerProps {
|
interface SuggestionsItemsContainerProps {
|
||||||
parentId?: string | null;
|
parentId: ParentId;
|
||||||
sectionsView: SectionsView[];
|
sectionsView: SectionsView[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,10 @@ import { appRouter } from 'components/router/appRouter';
|
||||||
import SectionContainer from './SectionContainer';
|
import SectionContainer from './SectionContainer';
|
||||||
|
|
||||||
import { Sections } from 'types/suggestionsSections';
|
import { Sections } from 'types/suggestionsSections';
|
||||||
|
import { ParentId } from 'types/library';
|
||||||
|
|
||||||
interface SuggestionsSectionContainerProps {
|
interface SuggestionsSectionContainerProps {
|
||||||
parentId?: string | null;
|
parentId: ParentId;
|
||||||
section: Sections;
|
section: Sections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ const SuggestionsSectionContainer: FC<SuggestionsSectionContainerProps> = ({
|
||||||
return (
|
return (
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
sectionTitle={globalize.translate(section.name)}
|
sectionTitle={globalize.translate(section.name)}
|
||||||
items={items || []}
|
items={items ?? []}
|
||||||
url={getRouteUrl()}
|
url={getRouteUrl()}
|
||||||
cardOptions={{
|
cardOptions={{
|
||||||
...section.cardOptions
|
...section.cardOptions
|
||||||
|
|
|
@ -28,7 +28,7 @@ import FiltersTags from './FiltersTags';
|
||||||
import FiltersVideoTypes from './FiltersVideoTypes';
|
import FiltersVideoTypes from './FiltersVideoTypes';
|
||||||
import FiltersYears from './FiltersYears';
|
import FiltersYears from './FiltersYears';
|
||||||
|
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings, ParentId } from 'types/library';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
const Accordion = styled((props: AccordionProps) => (
|
const Accordion = styled((props: AccordionProps) => (
|
||||||
|
@ -73,9 +73,10 @@ const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface FilterButtonProps {
|
interface FilterButtonProps {
|
||||||
parentId: string | null | undefined;
|
parentId: ParentId;
|
||||||
itemType: BaseItemKind;
|
itemType: BaseItemKind[];
|
||||||
viewType: LibraryTab;
|
viewType: LibraryTab;
|
||||||
|
hasFilters: boolean;
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<
|
setLibraryViewSettings: React.Dispatch<
|
||||||
React.SetStateAction<LibraryViewSettings>
|
React.SetStateAction<LibraryViewSettings>
|
||||||
|
@ -86,6 +87,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
parentId,
|
parentId,
|
||||||
itemType,
|
itemType,
|
||||||
viewType,
|
viewType,
|
||||||
|
hasFilters,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -153,16 +155,13 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
return viewType === LibraryTab.Episodes;
|
return viewType === LibraryTab.Episodes;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasFilters =
|
|
||||||
Object.values(libraryViewSettings.Filters || {}).some((filter) => !!filter);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<IconButton
|
<IconButton
|
||||||
title={globalize.translate('Filter')}
|
title={globalize.translate('Filter')}
|
||||||
sx={{ ml: 2 }}
|
sx={{ ml: 2 }}
|
||||||
aria-describedby={id}
|
aria-describedby={id}
|
||||||
className='paper-icon-button-light btnShuffle autoSize'
|
className='paper-icon-button-light btnFilter autoSize'
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<Badge color='info' variant='dot' invisible={!hasFilters}>
|
<Badge color='info' variant='dot' invisible={!hasFilters}>
|
||||||
|
|
|
@ -27,7 +27,7 @@ type ControllerProps = {
|
||||||
|
|
||||||
const Home: FunctionComponent = () => {
|
const Home: FunctionComponent = () => {
|
||||||
const [ searchParams ] = useSearchParams();
|
const [ searchParams ] = useSearchParams();
|
||||||
const initialTabIndex = parseInt(searchParams.get('tab') || '0', 10);
|
const initialTabIndex = parseInt(searchParams.get('tab') ?? '0', 10);
|
||||||
|
|
||||||
const tabController = useRef<ControllerProps | null>();
|
const tabController = useRef<ControllerProps | null>();
|
||||||
const tabControllers = useMemo<ControllerProps[]>(() => [], []);
|
const tabControllers = useMemo<ControllerProps[]>(() => [], []);
|
||||||
|
|
|
@ -1,30 +1,22 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
import ItemsView from '../../components/library/ItemsView';
|
||||||
import { LibraryViewProps } from 'types/library';
|
import { LibraryViewProps } from 'types/library';
|
||||||
|
import { CollectionType } from 'types/collectionType';
|
||||||
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
const CollectionsView: FC<LibraryViewProps> = ({ parentId }) => {
|
const CollectionsView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const getBasekey = useCallback(() => {
|
|
||||||
return 'collections';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getItemTypes = useCallback(() => {
|
|
||||||
return ['BoxSet'];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getNoItemsMessage = useCallback(() => {
|
|
||||||
return 'MessageNoCollectionsAvailable';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewItemsContainer
|
<ItemsView
|
||||||
topParentId={parentId}
|
viewType={LibraryTab.Collections}
|
||||||
|
parentId={parentId}
|
||||||
|
collectionType={CollectionType.Movies}
|
||||||
isBtnFilterEnabled={false}
|
isBtnFilterEnabled={false}
|
||||||
isBtnNewCollectionEnabled={true}
|
isBtnNewCollectionEnabled={true}
|
||||||
isAlphaPickerEnabled={false}
|
isAlphabetPickerEnabled={false}
|
||||||
getBasekey={getBasekey}
|
itemType={[BaseItemKind.BoxSet]}
|
||||||
getItemTypes={getItemTypes}
|
noItemsMessage='MessageNoCollectionsAvailable'
|
||||||
getNoItemsMessage={getNoItemsMessage}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,27 +1,17 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
import ItemsView from '../../components/library/ItemsView';
|
||||||
import { LibraryViewProps } from 'types/library';
|
import { LibraryViewProps } from 'types/library';
|
||||||
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
const FavoritesView: FC<LibraryViewProps> = ({ parentId }) => {
|
const FavoritesView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const getBasekey = useCallback(() => {
|
|
||||||
return 'favorites';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getItemTypes = useCallback(() => {
|
|
||||||
return ['Movie'];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getNoItemsMessage = useCallback(() => {
|
|
||||||
return 'MessageNoFavoritesAvailable';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewItemsContainer
|
<ItemsView
|
||||||
topParentId={parentId}
|
viewType={LibraryTab.Favorites}
|
||||||
getBasekey={getBasekey}
|
parentId={parentId}
|
||||||
getItemTypes={getItemTypes}
|
itemType={[BaseItemKind.Movie]}
|
||||||
getNoItemsMessage={getNoItemsMessage}
|
noItemsMessage='MessageNoFavoritesAvailable'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,20 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
import ItemsView from '../../components/library/ItemsView';
|
||||||
import { LibraryViewProps } from 'types/library';
|
import { LibraryViewProps } from 'types/library';
|
||||||
|
import { CollectionType } from 'types/collectionType';
|
||||||
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
const MoviesView: FC<LibraryViewProps> = ({ parentId }) => {
|
const MoviesView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const getBasekey = useCallback(() => {
|
|
||||||
return 'movies';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getItemTypes = useCallback(() => {
|
|
||||||
return ['Movie'];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getNoItemsMessage = useCallback(() => {
|
|
||||||
return 'MessageNoItemsAvailable';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewItemsContainer
|
<ItemsView
|
||||||
topParentId={parentId}
|
viewType={LibraryTab.Movies}
|
||||||
|
parentId={parentId}
|
||||||
|
collectionType={CollectionType.Movies}
|
||||||
isBtnShuffleEnabled={true}
|
isBtnShuffleEnabled={true}
|
||||||
getBasekey={getBasekey}
|
itemType={[BaseItemKind.Movie]}
|
||||||
getItemTypes={getItemTypes}
|
noItemsMessage='MessageNoItemsAvailable'
|
||||||
getNoItemsMessage={getNoItemsMessage}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,17 @@
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import React, { FC, useCallback } from 'react';
|
import ItemsView from '../../components/library/ItemsView';
|
||||||
|
|
||||||
import ViewItemsContainer from 'components/common/ViewItemsContainer';
|
|
||||||
import { LibraryViewProps } from 'types/library';
|
import { LibraryViewProps } from 'types/library';
|
||||||
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
const TrailersView: FC<LibraryViewProps> = ({ parentId }) => {
|
const TrailersView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
const getBasekey = useCallback(() => {
|
|
||||||
return 'trailers';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getItemTypes = useCallback(() => {
|
|
||||||
return ['Trailer'];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getNoItemsMessage = useCallback(() => {
|
|
||||||
return 'MessageNoTrailersFound';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewItemsContainer
|
<ItemsView
|
||||||
topParentId={parentId}
|
viewType={LibraryTab.Trailers}
|
||||||
getBasekey={getBasekey}
|
parentId={parentId}
|
||||||
getItemTypes={getItemTypes}
|
itemType={[BaseItemKind.Trailer]}
|
||||||
getNoItemsMessage={getNoItemsMessage}
|
noItemsMessage='MessageNoTrailersFound'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import 'elements/emby-scroller/emby-scroller';
|
|
||||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
import 'elements/emby-tabs/emby-tabs';
|
|
||||||
import 'elements/emby-button/emby-button';
|
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||||
import Page from 'components/Page';
|
|
||||||
|
|
||||||
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
|
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
|
||||||
|
import Page from 'components/Page';
|
||||||
import CollectionsView from './CollectionsView';
|
import CollectionsView from './CollectionsView';
|
||||||
import FavoritesView from './FavoritesView';
|
import FavoritesView from './FavoritesView';
|
||||||
import GenresView from './GenresView';
|
import GenresView from './GenresView';
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import AlphaPicker from '../alphaPicker/alphaPicker';
|
|
||||||
import { ViewQuerySettings } from '../../types/interface';
|
|
||||||
|
|
||||||
interface AlphaPickerContainerProps {
|
|
||||||
viewQuerySettings: ViewQuerySettings;
|
|
||||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AlphaPickerContainer: FC<AlphaPickerContainerProps> = ({ viewQuerySettings, setViewQuerySettings }) => {
|
|
||||||
const [ alphaPicker, setAlphaPicker ] = useState<AlphaPicker>();
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
alphaPicker?.updateControls(viewQuerySettings);
|
|
||||||
|
|
||||||
const onAlphaPickerChange = useCallback((e) => {
|
|
||||||
const newValue = (e as CustomEvent).detail.value;
|
|
||||||
let updatedValue: React.SetStateAction<ViewQuerySettings>;
|
|
||||||
if (newValue === '#') {
|
|
||||||
updatedValue = {
|
|
||||||
NameLessThan: 'A',
|
|
||||||
NameStartsWith: undefined
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
updatedValue = {
|
|
||||||
NameLessThan: undefined,
|
|
||||||
NameStartsWith: newValue
|
|
||||||
};
|
|
||||||
}
|
|
||||||
setViewQuerySettings((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
StartIndex: 0,
|
|
||||||
...updatedValue
|
|
||||||
}));
|
|
||||||
}, [setViewQuerySettings]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const alphaPickerElement = element.current;
|
|
||||||
|
|
||||||
setAlphaPicker(new AlphaPicker({
|
|
||||||
element: alphaPickerElement,
|
|
||||||
valueChangeEvent: 'click'
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (alphaPickerElement) {
|
|
||||||
alphaPickerElement.addEventListener('alphavaluechanged', onAlphaPickerChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
alphaPickerElement?.removeEventListener('alphavaluechanged', onAlphaPickerChange);
|
|
||||||
};
|
|
||||||
}, [onAlphaPickerChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element} className='alphaPicker alphaPicker-fixed alphaPicker-fixed-right alphaPicker-vertical alphabetPicker-right' />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlphaPickerContainer;
|
|
|
@ -1,65 +0,0 @@
|
||||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
|
||||||
import IconButtonElement from '../../elements/IconButtonElement';
|
|
||||||
import { ViewQuerySettings } from '../../types/interface';
|
|
||||||
|
|
||||||
interface FilterProps {
|
|
||||||
topParentId?: string | null;
|
|
||||||
getItemTypes: () => string[];
|
|
||||||
getFilterMenuOptions: () => Record<string, never>;
|
|
||||||
getVisibleFilters: () => string[];
|
|
||||||
viewQuerySettings: ViewQuerySettings;
|
|
||||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Filter: FC<FilterProps> = ({
|
|
||||||
topParentId,
|
|
||||||
getItemTypes,
|
|
||||||
getVisibleFilters,
|
|
||||||
getFilterMenuOptions,
|
|
||||||
viewQuerySettings,
|
|
||||||
setViewQuerySettings
|
|
||||||
}) => {
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const showFilterMenu = useCallback(() => {
|
|
||||||
import('../filtermenu/filtermenu').then(({ default: FilterMenu }) => {
|
|
||||||
const filterMenu = new FilterMenu();
|
|
||||||
filterMenu.show({
|
|
||||||
settings: viewQuerySettings,
|
|
||||||
visibleSettings: getVisibleFilters(),
|
|
||||||
parentId: topParentId,
|
|
||||||
itemTypes: getItemTypes(),
|
|
||||||
serverId: window.ApiClient.serverId(),
|
|
||||||
filterMenuOptions: getFilterMenuOptions(),
|
|
||||||
setfilters: setViewQuerySettings
|
|
||||||
}).catch(() => {
|
|
||||||
// filter menu closed
|
|
||||||
});
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[Filter] failed to load filter menu', err);
|
|
||||||
});
|
|
||||||
}, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const btnFilter = element.current?.querySelector('.btnFilter');
|
|
||||||
|
|
||||||
btnFilter?.addEventListener('click', showFilterMenu);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
btnFilter?.removeEventListener('click', showFilterMenu);
|
|
||||||
};
|
|
||||||
}, [showFilterMenu]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
<IconButtonElement
|
|
||||||
is='paper-icon-button-light'
|
|
||||||
className='btnFilter autoSize'
|
|
||||||
title='Filter'
|
|
||||||
icon='material-icons filter_list'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Filter;
|
|
|
@ -1,42 +0,0 @@
|
||||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import IconButtonElement from '../../elements/IconButtonElement';
|
|
||||||
|
|
||||||
const NewCollection: FC = () => {
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const showCollectionEditor = useCallback(() => {
|
|
||||||
import('../collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
|
|
||||||
const serverId = window.ApiClient.serverId();
|
|
||||||
const collectionEditor = new CollectionEditor();
|
|
||||||
collectionEditor.show({
|
|
||||||
items: [],
|
|
||||||
serverId: serverId
|
|
||||||
}).catch(() => {
|
|
||||||
// closed collection editor
|
|
||||||
});
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[NewCollection] failed to load collection editor', err);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const btnNewCollection = element.current?.querySelector('.btnNewCollection');
|
|
||||||
if (btnNewCollection) {
|
|
||||||
btnNewCollection.addEventListener('click', showCollectionEditor);
|
|
||||||
}
|
|
||||||
}, [showCollectionEditor]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
<IconButtonElement
|
|
||||||
is='paper-icon-button-light'
|
|
||||||
className='btnNewCollection autoSize'
|
|
||||||
title='Add'
|
|
||||||
icon='material-icons add'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NewCollection;
|
|
|
@ -1,97 +0,0 @@
|
||||||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
|
||||||
import IconButtonElement from '../../elements/IconButtonElement';
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
|
||||||
import { ViewQuerySettings } from '../../types/interface';
|
|
||||||
|
|
||||||
interface PaginationProps {
|
|
||||||
viewQuerySettings: ViewQuerySettings;
|
|
||||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
|
||||||
itemsResult?: BaseItemDtoQueryResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Pagination: FC<PaginationProps> = ({ viewQuerySettings, setViewQuerySettings, itemsResult = {} }) => {
|
|
||||||
const limit = userSettings.libraryPageSize(undefined);
|
|
||||||
const totalRecordCount = itemsResult.TotalRecordCount || 0;
|
|
||||||
const startIndex = viewQuerySettings.StartIndex || 0;
|
|
||||||
const recordsStart = totalRecordCount ? startIndex + 1 : 0;
|
|
||||||
const recordsEnd = limit ? Math.min(startIndex + limit, totalRecordCount) : totalRecordCount;
|
|
||||||
const showControls = limit > 0 && limit < totalRecordCount;
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const onNextPageClick = useCallback(() => {
|
|
||||||
if (limit > 0) {
|
|
||||||
const newIndex = startIndex + limit;
|
|
||||||
setViewQuerySettings((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
StartIndex: newIndex
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [limit, setViewQuerySettings, startIndex]);
|
|
||||||
|
|
||||||
const onPreviousPageClick = useCallback(() => {
|
|
||||||
if (limit > 0) {
|
|
||||||
const newIndex = Math.max(0, startIndex - limit);
|
|
||||||
setViewQuerySettings((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
StartIndex: newIndex
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [limit, setViewQuerySettings, startIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const btnNextPage = element.current?.querySelector('.btnNextPage') as HTMLButtonElement;
|
|
||||||
if (btnNextPage) {
|
|
||||||
if (startIndex + limit >= totalRecordCount) {
|
|
||||||
btnNextPage.disabled = true;
|
|
||||||
} else {
|
|
||||||
btnNextPage.disabled = false;
|
|
||||||
}
|
|
||||||
btnNextPage.addEventListener('click', onNextPageClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
const btnPreviousPage = element.current?.querySelector('.btnPreviousPage') as HTMLButtonElement;
|
|
||||||
if (btnPreviousPage) {
|
|
||||||
if (startIndex) {
|
|
||||||
btnPreviousPage.disabled = false;
|
|
||||||
} else {
|
|
||||||
btnPreviousPage.disabled = true;
|
|
||||||
}
|
|
||||||
btnPreviousPage.addEventListener('click', onPreviousPageClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
btnNextPage?.removeEventListener('click', onNextPageClick);
|
|
||||||
btnPreviousPage?.removeEventListener('click', onPreviousPageClick);
|
|
||||||
};
|
|
||||||
}, [totalRecordCount, onNextPageClick, onPreviousPageClick, limit, startIndex]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
<div className='paging'>
|
|
||||||
<div className='listPaging' style={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<span>
|
|
||||||
{globalize.translate('ListPaging', recordsStart, recordsEnd, totalRecordCount)}
|
|
||||||
</span>
|
|
||||||
{showControls && (
|
|
||||||
<>
|
|
||||||
<IconButtonElement
|
|
||||||
is='paper-icon-button-light'
|
|
||||||
className='btnPreviousPage autoSize'
|
|
||||||
icon='material-icons arrow_back'
|
|
||||||
/>
|
|
||||||
<IconButtonElement
|
|
||||||
is='paper-icon-button-light'
|
|
||||||
className='btnNextPage autoSize'
|
|
||||||
icon='material-icons arrow_forward'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Pagination;
|
|
|
@ -1,54 +0,0 @@
|
||||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
|
||||||
import IconButtonElement from '../../elements/IconButtonElement';
|
|
||||||
import { ViewQuerySettings } from '../../types/interface';
|
|
||||||
|
|
||||||
interface SelectViewProps {
|
|
||||||
getVisibleViewSettings: () => string[];
|
|
||||||
viewQuerySettings: ViewQuerySettings;
|
|
||||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SelectView: FC<SelectViewProps> = ({
|
|
||||||
getVisibleViewSettings,
|
|
||||||
viewQuerySettings,
|
|
||||||
setViewQuerySettings
|
|
||||||
}) => {
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const showViewSettingsMenu = useCallback(() => {
|
|
||||||
import('../viewSettings/viewSettings').then(({ default: ViewSettings }) => {
|
|
||||||
const viewsettings = new ViewSettings();
|
|
||||||
viewsettings.show({
|
|
||||||
settings: viewQuerySettings,
|
|
||||||
visibleSettings: getVisibleViewSettings(),
|
|
||||||
setviewsettings: setViewQuerySettings
|
|
||||||
}).catch(() => {
|
|
||||||
// view settings closed
|
|
||||||
});
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[SelectView] failed to load view settings', err);
|
|
||||||
});
|
|
||||||
}, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const btnSelectView = element.current?.querySelector('.btnSelectView') as HTMLButtonElement;
|
|
||||||
btnSelectView?.addEventListener('click', showViewSettingsMenu);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
btnSelectView?.removeEventListener('click', showViewSettingsMenu);
|
|
||||||
};
|
|
||||||
}, [showViewSettingsMenu]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
<IconButtonElement
|
|
||||||
is='paper-icon-button-light'
|
|
||||||
className='btnSelectView autoSize'
|
|
||||||
title='ButtonSelectView'
|
|
||||||
icon='material-icons view_comfy'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectView;
|
|
|
@ -1,45 +0,0 @@
|
||||||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { playbackManager } from '../playback/playbackmanager';
|
|
||||||
import IconButtonElement from '../../elements/IconButtonElement';
|
|
||||||
|
|
||||||
interface ShuffleProps {
|
|
||||||
itemsResult?: BaseItemDtoQueryResult;
|
|
||||||
topParentId: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Shuffle: FC<ShuffleProps> = ({ itemsResult = {}, topParentId }) => {
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const shuffle = useCallback(() => {
|
|
||||||
window.ApiClient.getItem(
|
|
||||||
window.ApiClient.getCurrentUserId(),
|
|
||||||
topParentId as string
|
|
||||||
).then((item) => {
|
|
||||||
playbackManager.shuffle(item);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[Shuffle] failed to fetch items', err);
|
|
||||||
});
|
|
||||||
}, [topParentId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const btnShuffle = element.current?.querySelector('.btnShuffle');
|
|
||||||
if (btnShuffle) {
|
|
||||||
btnShuffle.addEventListener('click', shuffle);
|
|
||||||
}
|
|
||||||
}, [itemsResult.TotalRecordCount, shuffle]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
<IconButtonElement
|
|
||||||
is='paper-icon-button-light'
|
|
||||||
className='btnShuffle autoSize'
|
|
||||||
title='Shuffle'
|
|
||||||
icon='material-icons shuffle'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Shuffle;
|
|
|
@ -1,58 +0,0 @@
|
||||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
|
||||||
import IconButtonElement from '../../elements/IconButtonElement';
|
|
||||||
import { ViewQuerySettings } from '../../types/interface';
|
|
||||||
|
|
||||||
interface SortProps {
|
|
||||||
getSortMenuOptions: () => {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
viewQuerySettings: ViewQuerySettings;
|
|
||||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Sort: FC<SortProps> = ({
|
|
||||||
getSortMenuOptions,
|
|
||||||
viewQuerySettings,
|
|
||||||
setViewQuerySettings
|
|
||||||
}) => {
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const showSortMenu = useCallback(() => {
|
|
||||||
import('../sortmenu/sortmenu').then(({ default: SortMenu }) => {
|
|
||||||
const sortMenu = new SortMenu();
|
|
||||||
sortMenu.show({
|
|
||||||
settings: viewQuerySettings,
|
|
||||||
sortOptions: getSortMenuOptions(),
|
|
||||||
setSortValues: setViewQuerySettings
|
|
||||||
}).catch(() => {
|
|
||||||
// sort menu closed
|
|
||||||
});
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[Sort] failed to load sort menu', err);
|
|
||||||
});
|
|
||||||
}, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const btnSort = element.current?.querySelector('.btnSort');
|
|
||||||
|
|
||||||
btnSort?.addEventListener('click', showSortMenu);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
btnSort?.removeEventListener('click', showSortMenu);
|
|
||||||
};
|
|
||||||
}, [showSortMenu]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
<IconButtonElement
|
|
||||||
is='paper-icon-button-light'
|
|
||||||
className='btnSort autoSize'
|
|
||||||
title='Sort'
|
|
||||||
icon='material-icons sort_by_alpha'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sort;
|
|
|
@ -1,411 +0,0 @@
|
||||||
import { type BaseItemDtoQueryResult, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import loading from '../loading/loading';
|
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
|
||||||
import AlphaPickerContainer from './AlphaPickerContainer';
|
|
||||||
import Filter from './Filter';
|
|
||||||
import ItemsContainer from './ItemsContainer';
|
|
||||||
import Pagination from './Pagination';
|
|
||||||
import SelectView from './SelectView';
|
|
||||||
import Shuffle from './Shuffle';
|
|
||||||
import Sort from './Sort';
|
|
||||||
import NewCollection from './NewCollection';
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import ServerConnections from '../ServerConnections';
|
|
||||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
|
||||||
import listview from '../listview/listview';
|
|
||||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
|
||||||
|
|
||||||
import { ViewQuerySettings } from '../../types/interface';
|
|
||||||
import { CardOptions } from '../../types/cardOptions';
|
|
||||||
|
|
||||||
interface ViewItemsContainerProps {
|
|
||||||
topParentId: string | null;
|
|
||||||
isBtnShuffleEnabled?: boolean;
|
|
||||||
isBtnFilterEnabled?: boolean;
|
|
||||||
isBtnNewCollectionEnabled?: boolean;
|
|
||||||
isAlphaPickerEnabled?: boolean;
|
|
||||||
getBasekey: () => string;
|
|
||||||
getItemTypes: () => string[];
|
|
||||||
getNoItemsMessage: () => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDefaultSortBy = () => {
|
|
||||||
return 'SortName';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFields = (viewQuerySettings: ViewQuerySettings) => {
|
|
||||||
const fields: ItemFields[] = [
|
|
||||||
ItemFields.BasicSyncInfo,
|
|
||||||
ItemFields.MediaSourceCount
|
|
||||||
];
|
|
||||||
|
|
||||||
if (viewQuerySettings.imageType === 'primary') {
|
|
||||||
fields.push(ItemFields.PrimaryImageAspectRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fields.join(',');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilters = (viewQuerySettings: ViewQuerySettings) => {
|
|
||||||
const filters: ItemFilter[] = [];
|
|
||||||
|
|
||||||
if (viewQuerySettings.IsPlayed) {
|
|
||||||
filters.push(ItemFilter.IsPlayed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewQuerySettings.IsUnplayed) {
|
|
||||||
filters.push(ItemFilter.IsUnplayed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewQuerySettings.IsFavorite) {
|
|
||||||
filters.push(ItemFilter.IsFavorite);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewQuerySettings.IsResumable) {
|
|
||||||
filters.push(ItemFilter.IsResumable);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filters;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVisibleViewSettings = () => {
|
|
||||||
return [
|
|
||||||
'showTitle',
|
|
||||||
'showYear',
|
|
||||||
'imageType',
|
|
||||||
'cardLayout'
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilterMenuOptions = () => {
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVisibleFilters = () => {
|
|
||||||
return [
|
|
||||||
'IsUnplayed',
|
|
||||||
'IsPlayed',
|
|
||||||
'IsFavorite',
|
|
||||||
'IsResumable',
|
|
||||||
'VideoType',
|
|
||||||
'HasSubtitles',
|
|
||||||
'HasTrailer',
|
|
||||||
'HasSpecialFeature',
|
|
||||||
'HasThemeSong',
|
|
||||||
'HasThemeVideo'
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSortMenuOptions = () => {
|
|
||||||
return [{
|
|
||||||
name: globalize.translate('Name'),
|
|
||||||
value: 'SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionRandom'),
|
|
||||||
value: 'Random'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionImdbRating'),
|
|
||||||
value: 'CommunityRating,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionCriticRating'),
|
|
||||||
value: 'CriticRating,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionDateAdded'),
|
|
||||||
value: 'DateCreated,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionDatePlayed'),
|
|
||||||
value: 'DatePlayed,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionParentalRating'),
|
|
||||||
value: 'OfficialRating,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionPlayCount'),
|
|
||||||
value: 'PlayCount,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionReleaseDate'),
|
|
||||||
value: 'PremiereDate,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('Runtime'),
|
|
||||||
value: 'Runtime,SortName,ProductionYear'
|
|
||||||
}];
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultViewQuerySettings: ViewQuerySettings = {
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
imageType: 'primary',
|
|
||||||
viewType: '',
|
|
||||||
cardLayout: false,
|
|
||||||
SortBy: getDefaultSortBy(),
|
|
||||||
SortOrder: 'Ascending',
|
|
||||||
IsPlayed: false,
|
|
||||||
IsUnplayed: false,
|
|
||||||
IsFavorite: false,
|
|
||||||
IsResumable: false,
|
|
||||||
Is4K: null,
|
|
||||||
IsHD: null,
|
|
||||||
IsSD: null,
|
|
||||||
Is3D: null,
|
|
||||||
VideoTypes: '',
|
|
||||||
SeriesStatus: '',
|
|
||||||
HasSubtitles: null,
|
|
||||||
HasTrailer: null,
|
|
||||||
HasSpecialFeature: null,
|
|
||||||
HasThemeSong: null,
|
|
||||||
HasThemeVideo: null,
|
|
||||||
GenreIds: '',
|
|
||||||
StartIndex: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
|
||||||
topParentId,
|
|
||||||
isBtnShuffleEnabled = false,
|
|
||||||
isBtnFilterEnabled = true,
|
|
||||||
isBtnNewCollectionEnabled = false,
|
|
||||||
isAlphaPickerEnabled = true,
|
|
||||||
getBasekey,
|
|
||||||
getItemTypes,
|
|
||||||
getNoItemsMessage
|
|
||||||
}) => {
|
|
||||||
const getSettingsKey = useCallback(() => {
|
|
||||||
return `${topParentId} - ${getBasekey()}`;
|
|
||||||
}, [getBasekey, topParentId]);
|
|
||||||
|
|
||||||
const [isLoading, setisLoading] = useState(false);
|
|
||||||
|
|
||||||
const [viewQuerySettings, setViewQuerySettings] = useLocalStorage<ViewQuerySettings>(
|
|
||||||
`viewQuerySettings - ${getSettingsKey()}`,
|
|
||||||
defaultViewQuerySettings
|
|
||||||
);
|
|
||||||
|
|
||||||
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
|
|
||||||
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const getContext = useCallback(() => {
|
|
||||||
const itemType = getItemTypes().join(',');
|
|
||||||
if (itemType === 'Movie' || itemType === 'BoxSet') {
|
|
||||||
return 'movies';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [getItemTypes]);
|
|
||||||
|
|
||||||
const getCardOptions = useCallback(() => {
|
|
||||||
let shape;
|
|
||||||
let preferThumb;
|
|
||||||
let preferDisc;
|
|
||||||
let preferLogo;
|
|
||||||
|
|
||||||
if (viewQuerySettings.imageType === 'banner') {
|
|
||||||
shape = 'banner';
|
|
||||||
} else if (viewQuerySettings.imageType === 'disc') {
|
|
||||||
shape = 'square';
|
|
||||||
preferDisc = true;
|
|
||||||
} else if (viewQuerySettings.imageType === 'logo') {
|
|
||||||
shape = 'backdrop';
|
|
||||||
preferLogo = true;
|
|
||||||
} else if (viewQuerySettings.imageType === 'thumb') {
|
|
||||||
shape = 'backdrop';
|
|
||||||
preferThumb = true;
|
|
||||||
} else {
|
|
||||||
shape = 'autoVertical';
|
|
||||||
}
|
|
||||||
|
|
||||||
const cardOptions: CardOptions = {
|
|
||||||
shape: shape,
|
|
||||||
showTitle: viewQuerySettings.showTitle,
|
|
||||||
showYear: viewQuerySettings.showYear,
|
|
||||||
cardLayout: viewQuerySettings.cardLayout,
|
|
||||||
centerText: true,
|
|
||||||
context: getContext(),
|
|
||||||
coverImage: true,
|
|
||||||
preferThumb: preferThumb,
|
|
||||||
preferDisc: preferDisc,
|
|
||||||
preferLogo: preferLogo,
|
|
||||||
overlayPlayButton: false,
|
|
||||||
overlayMoreButton: true,
|
|
||||||
overlayText: !viewQuerySettings.showTitle
|
|
||||||
};
|
|
||||||
|
|
||||||
cardOptions.items = itemsResult.Items || [];
|
|
||||||
|
|
||||||
return cardOptions;
|
|
||||||
}, [
|
|
||||||
getContext,
|
|
||||||
itemsResult.Items,
|
|
||||||
viewQuerySettings.cardLayout,
|
|
||||||
viewQuerySettings.imageType,
|
|
||||||
viewQuerySettings.showTitle,
|
|
||||||
viewQuerySettings.showYear
|
|
||||||
]);
|
|
||||||
|
|
||||||
const getItemsHtml = useCallback(() => {
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
if (viewQuerySettings.imageType === 'list') {
|
|
||||||
html = listview.getListViewHtml({
|
|
||||||
items: itemsResult.Items || [],
|
|
||||||
context: getContext()
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
html = cardBuilder.getCardsHtml(itemsResult.Items || [], getCardOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!itemsResult.Items?.length) {
|
|
||||||
html += '<div class="noItemsMessage centerMessage">';
|
|
||||||
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
|
|
||||||
html += '<p>' + globalize.translate(getNoItemsMessage()) + '</p>';
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]);
|
|
||||||
|
|
||||||
const getQuery = useCallback(() => {
|
|
||||||
const queryFilters = getFilters(viewQuerySettings);
|
|
||||||
|
|
||||||
let queryIsHD;
|
|
||||||
|
|
||||||
if (viewQuerySettings.IsHD) {
|
|
||||||
queryIsHD = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewQuerySettings.IsSD) {
|
|
||||||
queryIsHD = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
SortBy: viewQuerySettings.SortBy,
|
|
||||||
SortOrder: viewQuerySettings.SortOrder,
|
|
||||||
IncludeItemTypes: getItemTypes().join(','),
|
|
||||||
Recursive: true,
|
|
||||||
Fields: getFields(viewQuerySettings),
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo',
|
|
||||||
Limit: userSettings.libraryPageSize(undefined) || undefined,
|
|
||||||
IsFavorite: getBasekey() === 'favorites' ? true : null,
|
|
||||||
VideoTypes: viewQuerySettings.VideoTypes,
|
|
||||||
GenreIds: viewQuerySettings.GenreIds,
|
|
||||||
Is4K: viewQuerySettings.Is4K ? true : null,
|
|
||||||
IsHD: queryIsHD,
|
|
||||||
Is3D: viewQuerySettings.Is3D ? true : null,
|
|
||||||
HasSubtitles: viewQuerySettings.HasSubtitles ? true : null,
|
|
||||||
HasTrailer: viewQuerySettings.HasTrailer ? true : null,
|
|
||||||
HasSpecialFeature: viewQuerySettings.HasSpecialFeature ? true : null,
|
|
||||||
HasThemeSong: viewQuerySettings.HasThemeSong ? true : null,
|
|
||||||
HasThemeVideo: viewQuerySettings.HasThemeVideo ? true : null,
|
|
||||||
Filters: queryFilters.length ? queryFilters.join(',') : null,
|
|
||||||
StartIndex: viewQuerySettings.StartIndex,
|
|
||||||
NameLessThan: viewQuerySettings.NameLessThan,
|
|
||||||
NameStartsWith: viewQuerySettings.NameStartsWith,
|
|
||||||
ParentId: topParentId
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
viewQuerySettings,
|
|
||||||
getItemTypes,
|
|
||||||
getBasekey,
|
|
||||||
topParentId
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fetchData = useCallback(() => {
|
|
||||||
loading.show();
|
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(window.ApiClient.serverId());
|
|
||||||
return apiClient.getItems(
|
|
||||||
apiClient.getCurrentUserId(),
|
|
||||||
{
|
|
||||||
...getQuery()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [getQuery]);
|
|
||||||
|
|
||||||
const reloadItems = useCallback(() => {
|
|
||||||
const page = element.current;
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
console.error('Unexpected null reference');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setisLoading(false);
|
|
||||||
fetchData().then((result) => {
|
|
||||||
setItemsResult(result);
|
|
||||||
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
|
||||||
autoFocuser.autoFocus(page);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[ViewItemsContainer] failed to load autofocuser', err);
|
|
||||||
});
|
|
||||||
loading.hide();
|
|
||||||
setisLoading(true);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[ViewItemsContainer] failed to fetch data', err);
|
|
||||||
});
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reloadItems();
|
|
||||||
}, [reloadItems]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
|
||||||
<Pagination
|
|
||||||
itemsResult= {itemsResult}
|
|
||||||
viewQuerySettings={viewQuerySettings}
|
|
||||||
setViewQuerySettings={setViewQuerySettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isBtnShuffleEnabled && <Shuffle itemsResult={itemsResult} topParentId={topParentId} />}
|
|
||||||
|
|
||||||
<SelectView
|
|
||||||
getVisibleViewSettings={getVisibleViewSettings}
|
|
||||||
viewQuerySettings={viewQuerySettings}
|
|
||||||
setViewQuerySettings={setViewQuerySettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Sort
|
|
||||||
getSortMenuOptions={getSortMenuOptions}
|
|
||||||
viewQuerySettings={viewQuerySettings}
|
|
||||||
setViewQuerySettings={setViewQuerySettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isBtnFilterEnabled && <Filter
|
|
||||||
topParentId={topParentId}
|
|
||||||
getItemTypes={getItemTypes}
|
|
||||||
getVisibleFilters={getVisibleFilters}
|
|
||||||
getFilterMenuOptions={getFilterMenuOptions}
|
|
||||||
viewQuerySettings={viewQuerySettings}
|
|
||||||
setViewQuerySettings={setViewQuerySettings}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
{isBtnNewCollectionEnabled && <NewCollection />}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isAlphaPickerEnabled && <AlphaPickerContainer
|
|
||||||
viewQuerySettings={viewQuerySettings}
|
|
||||||
setViewQuerySettings={setViewQuerySettings}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
{isLoading && <ItemsContainer
|
|
||||||
viewQuerySettings={viewQuerySettings}
|
|
||||||
getItemsHtml={getItemsHtml}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
|
||||||
<Pagination
|
|
||||||
itemsResult= {itemsResult}
|
|
||||||
viewQuerySettings={viewQuerySettings}
|
|
||||||
setViewQuerySettings={setViewQuerySettings}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewItemsContainer;
|
|
|
@ -1,12 +1,11 @@
|
||||||
import type { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client';
|
import type { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import { AxiosRequestConfig } from 'axios';
|
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
|
||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
|
||||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||||
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||||
import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter';
|
import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filter';
|
||||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
|
import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api';
|
||||||
import { getFilterApi } from '@jellyfin/sdk/lib/utils/api/filter-api';
|
import { getFilterApi } from '@jellyfin/sdk/lib/utils/api/filter-api';
|
||||||
import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api';
|
import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api';
|
||||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
||||||
|
@ -14,11 +13,14 @@ import { getMoviesApi } from '@jellyfin/sdk/lib/utils/api/movies-api';
|
||||||
import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api';
|
import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api';
|
||||||
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
|
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
|
||||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
||||||
|
import { AxiosRequestConfig } from 'axios';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { JellyfinApiContext, useApi } from './useApi';
|
import { JellyfinApiContext, useApi } from './useApi';
|
||||||
|
import { getAlphaPickerQuery, getFieldsQuery, getFiltersQuery, getLimitQuery } from 'utils/items';
|
||||||
import { Sections, SectionsViewType } from 'types/suggestionsSections';
|
import { Sections, SectionsViewType } from 'types/suggestionsSections';
|
||||||
import { ParentId } from 'types/library';
|
import { LibraryViewSettings, ParentId } from 'types/library';
|
||||||
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
const fetchGetItem = async (
|
const fetchGetItem = async (
|
||||||
currentApi: JellyfinApiContext,
|
currentApi: JellyfinApiContext,
|
||||||
|
@ -291,7 +293,7 @@ export const useGetGenres = (itemType: BaseItemKind, parentId: ParentId) => {
|
||||||
const fetchGetStudios = async (
|
const fetchGetStudios = async (
|
||||||
currentApi: JellyfinApiContext,
|
currentApi: JellyfinApiContext,
|
||||||
parentId: ParentId,
|
parentId: ParentId,
|
||||||
itemType: BaseItemKind,
|
itemType: BaseItemKind[],
|
||||||
options?: AxiosRequestConfig
|
options?: AxiosRequestConfig
|
||||||
) => {
|
) => {
|
||||||
const { api, user } = currentApi;
|
const { api, user } = currentApi;
|
||||||
|
@ -299,7 +301,7 @@ const fetchGetStudios = async (
|
||||||
const response = await getStudiosApi(api).getStudios(
|
const response = await getStudiosApi(api).getStudios(
|
||||||
{
|
{
|
||||||
userId: user.Id,
|
userId: user.Id,
|
||||||
includeItemTypes: [itemType],
|
includeItemTypes: itemType,
|
||||||
fields: [
|
fields: [
|
||||||
ItemFields.DateCreated,
|
ItemFields.DateCreated,
|
||||||
ItemFields.PrimaryImageAspectRatio
|
ItemFields.PrimaryImageAspectRatio
|
||||||
|
@ -316,7 +318,7 @@ const fetchGetStudios = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind) => {
|
export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind[]) => {
|
||||||
const currentApi = useApi();
|
const currentApi = useApi();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['Studios', parentId, itemType],
|
queryKey: ['Studios', parentId, itemType],
|
||||||
|
@ -329,7 +331,7 @@ export const useGetStudios = (parentId: ParentId, itemType: BaseItemKind) => {
|
||||||
const fetchGetQueryFiltersLegacy = async (
|
const fetchGetQueryFiltersLegacy = async (
|
||||||
currentApi: JellyfinApiContext,
|
currentApi: JellyfinApiContext,
|
||||||
parentId: ParentId,
|
parentId: ParentId,
|
||||||
itemType: BaseItemKind,
|
itemType: BaseItemKind[],
|
||||||
options?: AxiosRequestConfig
|
options?: AxiosRequestConfig
|
||||||
) => {
|
) => {
|
||||||
const { api, user } = currentApi;
|
const { api, user } = currentApi;
|
||||||
|
@ -338,7 +340,7 @@ const fetchGetQueryFiltersLegacy = async (
|
||||||
{
|
{
|
||||||
userId: user.Id,
|
userId: user.Id,
|
||||||
parentId: parentId ?? undefined,
|
parentId: parentId ?? undefined,
|
||||||
includeItemTypes: [itemType]
|
includeItemTypes: itemType
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
signal: options?.signal
|
signal: options?.signal
|
||||||
|
@ -350,7 +352,7 @@ const fetchGetQueryFiltersLegacy = async (
|
||||||
|
|
||||||
export const useGetQueryFiltersLegacy = (
|
export const useGetQueryFiltersLegacy = (
|
||||||
parentId: ParentId,
|
parentId: ParentId,
|
||||||
itemType: BaseItemKind
|
itemType: BaseItemKind[]
|
||||||
) => {
|
) => {
|
||||||
const currentApi = useApi();
|
const currentApi = useApi();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
@ -362,3 +364,148 @@ export const useGetQueryFiltersLegacy = (
|
||||||
enabled: !!parentId
|
enabled: !!parentId
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchGetItemsViewByType = async (
|
||||||
|
currentApi: JellyfinApiContext,
|
||||||
|
viewType: LibraryTab,
|
||||||
|
parentId: ParentId,
|
||||||
|
itemType: BaseItemKind[],
|
||||||
|
libraryViewSettings: LibraryViewSettings,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const { api, user } = currentApi;
|
||||||
|
if (api && user?.Id) {
|
||||||
|
let response;
|
||||||
|
switch (viewType) {
|
||||||
|
case LibraryTab.AlbumArtists: {
|
||||||
|
response = await getArtistsApi(api).getAlbumArtists(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
enableImageTypes: [libraryViewSettings.ImageType, ImageType.Backdrop],
|
||||||
|
...getFieldsQuery(viewType, libraryViewSettings),
|
||||||
|
...getFiltersQuery(viewType, libraryViewSettings),
|
||||||
|
...getLimitQuery(),
|
||||||
|
...getAlphaPickerQuery(libraryViewSettings),
|
||||||
|
sortBy: [libraryViewSettings.SortBy],
|
||||||
|
sortOrder: [libraryViewSettings.SortOrder],
|
||||||
|
includeItemTypes: itemType,
|
||||||
|
startIndex: libraryViewSettings.StartIndex
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LibraryTab.Artists: {
|
||||||
|
response = await getArtistsApi(api).getArtists(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
enableImageTypes: [libraryViewSettings.ImageType, ImageType.Backdrop],
|
||||||
|
...getFieldsQuery(viewType, libraryViewSettings),
|
||||||
|
...getFiltersQuery(viewType, libraryViewSettings),
|
||||||
|
...getLimitQuery(),
|
||||||
|
...getAlphaPickerQuery(libraryViewSettings),
|
||||||
|
sortBy: [libraryViewSettings.SortBy],
|
||||||
|
sortOrder: [libraryViewSettings.SortOrder],
|
||||||
|
includeItemTypes: itemType,
|
||||||
|
startIndex: libraryViewSettings.StartIndex
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LibraryTab.Networks:
|
||||||
|
response = await getStudiosApi(api).getStudios(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
...getFieldsQuery(viewType, libraryViewSettings),
|
||||||
|
includeItemTypes: itemType,
|
||||||
|
enableImageTypes: [ImageType.Thumb],
|
||||||
|
startIndex: libraryViewSettings.StartIndex
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
response = await getItemsApi(api).getItems(
|
||||||
|
{
|
||||||
|
userId: user.Id,
|
||||||
|
recursive: true,
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
enableImageTypes: [libraryViewSettings.ImageType, ImageType.Backdrop],
|
||||||
|
...getFieldsQuery(viewType, libraryViewSettings),
|
||||||
|
...getFiltersQuery(viewType, libraryViewSettings),
|
||||||
|
...getLimitQuery(),
|
||||||
|
...getAlphaPickerQuery(libraryViewSettings),
|
||||||
|
isFavorite: viewType === LibraryTab.Favorites ? true : undefined,
|
||||||
|
sortBy: [libraryViewSettings.SortBy],
|
||||||
|
sortOrder: [libraryViewSettings.SortOrder],
|
||||||
|
includeItemTypes: itemType,
|
||||||
|
startIndex: libraryViewSettings.StartIndex
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal: options?.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetItemsViewByType = (
|
||||||
|
viewType: LibraryTab,
|
||||||
|
parentId: ParentId,
|
||||||
|
itemType: BaseItemKind[],
|
||||||
|
libraryViewSettings: LibraryViewSettings
|
||||||
|
) => {
|
||||||
|
const currentApi = useApi();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [
|
||||||
|
'ItemsViewByType',
|
||||||
|
viewType,
|
||||||
|
parentId,
|
||||||
|
itemType,
|
||||||
|
libraryViewSettings
|
||||||
|
],
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
fetchGetItemsViewByType(
|
||||||
|
currentApi,
|
||||||
|
viewType,
|
||||||
|
parentId,
|
||||||
|
itemType,
|
||||||
|
libraryViewSettings,
|
||||||
|
{ signal }
|
||||||
|
),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
keepPreviousData : true,
|
||||||
|
enabled:
|
||||||
|
[
|
||||||
|
LibraryTab.Movies,
|
||||||
|
LibraryTab.Favorites,
|
||||||
|
LibraryTab.Collections,
|
||||||
|
LibraryTab.Trailers,
|
||||||
|
LibraryTab.Series,
|
||||||
|
LibraryTab.Episodes,
|
||||||
|
LibraryTab.Networks,
|
||||||
|
LibraryTab.Albums,
|
||||||
|
LibraryTab.AlbumArtists,
|
||||||
|
LibraryTab.Artists,
|
||||||
|
LibraryTab.Playlists,
|
||||||
|
LibraryTab.Songs,
|
||||||
|
LibraryTab.Books,
|
||||||
|
LibraryTab.Photos,
|
||||||
|
LibraryTab.Videos
|
||||||
|
].includes(viewType) && !!parentId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { CollectionType } from './collectionType';
|
||||||
|
|
||||||
export interface CardOptions {
|
export interface CardOptions {
|
||||||
itemsContainer?: HTMLElement | null;
|
itemsContainer?: HTMLElement | null;
|
||||||
|
@ -32,7 +33,7 @@ export interface CardOptions {
|
||||||
showUnplayedIndicator?: boolean;
|
showUnplayedIndicator?: boolean;
|
||||||
showChildCountIndicator?: boolean;
|
showChildCountIndicator?: boolean;
|
||||||
lines?: number;
|
lines?: number;
|
||||||
context?: string | null;
|
context?: CollectionType;
|
||||||
action?: string | null;
|
action?: string | null;
|
||||||
defaultShape?: string;
|
defaultShape?: string;
|
||||||
indexBy?: string;
|
indexBy?: string;
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
export interface ViewQuerySettings {
|
|
||||||
showTitle?: boolean;
|
|
||||||
showYear?: boolean;
|
|
||||||
imageType?: string;
|
|
||||||
viewType?: string;
|
|
||||||
cardLayout?: boolean;
|
|
||||||
SortBy?: string | null;
|
|
||||||
SortOrder?: string | null;
|
|
||||||
IsPlayed?: boolean | null;
|
|
||||||
IsUnplayed?: boolean | null;
|
|
||||||
IsFavorite?: boolean | null;
|
|
||||||
IsResumable?: boolean | null;
|
|
||||||
Is4K?: boolean | null;
|
|
||||||
IsHD?: boolean | null;
|
|
||||||
IsSD?: boolean | null;
|
|
||||||
Is3D?: boolean | null;
|
|
||||||
VideoTypes?: string | null;
|
|
||||||
SeriesStatus?: string | null;
|
|
||||||
HasSubtitles?: boolean | null;
|
|
||||||
HasTrailer?: boolean | null;
|
|
||||||
HasSpecialFeature?: boolean | null;
|
|
||||||
ParentIndexNumber?: boolean | null;
|
|
||||||
HasThemeSong?: boolean | null;
|
|
||||||
HasThemeVideo?: boolean | null;
|
|
||||||
GenreIds?: string | null;
|
|
||||||
NameLessThan?: string | null;
|
|
||||||
NameStartsWith?: string | null;
|
|
||||||
StartIndex?: number;
|
|
||||||
}
|
|
|
@ -8,7 +8,7 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
export type ParentId = string | null | undefined;
|
export type ParentId = string | null | undefined;
|
||||||
|
|
||||||
export interface LibraryViewProps {
|
export interface LibraryViewProps {
|
||||||
parentId: string | null;
|
parentId: ParentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FeatureFilters {
|
export enum FeatureFilters {
|
||||||
|
|
|
@ -107,7 +107,7 @@ export const getFieldsQuery = (
|
||||||
|
|
||||||
export const getLimitQuery = () => {
|
export const getLimitQuery = () => {
|
||||||
return {
|
return {
|
||||||
limit: userSettings.libraryPageSize(undefined) || undefined
|
limit: userSettings.libraryPageSize(undefined) ?? undefined
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -144,12 +144,12 @@ export const getSettingsKey = (viewType: LibraryTab, parentId: ParentId) => {
|
||||||
return `${viewType} - ${parentId}`;
|
return `${viewType} - ${parentId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefaultLibraryViewSettings = (): LibraryViewSettings => {
|
export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryViewSettings => {
|
||||||
return {
|
return {
|
||||||
ShowTitle: true,
|
ShowTitle: true,
|
||||||
ShowYear: false,
|
ShowYear: false,
|
||||||
ViewMode: ViewMode.GridView,
|
ViewMode: viewType === LibraryTab.Songs ? ViewMode.ListView : ViewMode.GridView,
|
||||||
ImageType: ImageType.Primary,
|
ImageType: viewType === LibraryTab.Networks ? ImageType.Thumb : ImageType.Primary,
|
||||||
CardLayout: false,
|
CardLayout: false,
|
||||||
SortBy: ItemSortBy.SortName,
|
SortBy: ItemSortBy.SortName,
|
||||||
SortOrder: SortOrder.Ascending,
|
SortOrder: SortOrder.Ascending,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue