Merge pull request #3792 from grafixeyehero/Convert-Movies-Page-to-react
This commit is contained in:
commit
00d9c6d71d
43 changed files with 1887 additions and 1692 deletions
|
@ -206,8 +206,8 @@ import toast from '../toast/toast';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export class showEditor {
|
class CollectionEditor {
|
||||||
constructor(options) {
|
show(options) {
|
||||||
const items = options.items || {};
|
const items = options.items || {};
|
||||||
currentServerId = options.serverId;
|
currentServerId = options.serverId;
|
||||||
|
|
||||||
|
@ -266,4 +266,4 @@ import toast from '../toast/toast';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-enable indent */
|
/* eslint-enable indent */
|
||||||
export default showEditor;
|
export default CollectionEditor;
|
||||||
|
|
53
src/components/common/AlphaPickerContainer.tsx
Normal file
53
src/components/common/AlphaPickerContainer.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
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'};
|
||||||
|
} else {
|
||||||
|
updatedValue = {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;
|
61
src/components/common/Filter.tsx
Normal file
61
src/components/common/Filter.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [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;
|
126
src/components/common/GenresItemsContainer.tsx
Normal file
126
src/components/common/GenresItemsContainer.tsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import '../../elements/emby-button/emby-button';
|
||||||
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
|
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import escapeHTML from 'escape-html';
|
||||||
|
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { appRouter } from '../appRouter';
|
||||||
|
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||||
|
import layoutManager from '../layoutManager';
|
||||||
|
import lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement';
|
||||||
|
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
||||||
|
|
||||||
|
const createLinkElement = ({ className, title, href }: { className?: string, title?: string | null, href?: string }) => ({
|
||||||
|
__html: `<a
|
||||||
|
is="emby-linkbutton"
|
||||||
|
class="${className}"
|
||||||
|
href="${href}"
|
||||||
|
>
|
||||||
|
<h2 class='sectionTitle sectionTitle-cards'>
|
||||||
|
${title}
|
||||||
|
</h2>
|
||||||
|
<span class='material-icons chevron_right' aria-hidden='true'></span>
|
||||||
|
</a>`
|
||||||
|
});
|
||||||
|
|
||||||
|
interface GenresItemsContainerProps {
|
||||||
|
topParentId?: string | null;
|
||||||
|
itemsResult: BaseItemDtoQueryResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
||||||
|
topParentId,
|
||||||
|
itemsResult = {}
|
||||||
|
}) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const enableScrollX = useCallback(() => {
|
||||||
|
return !layoutManager.desktop;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getPortraitShape = useCallback(() => {
|
||||||
|
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
||||||
|
}, [enableScrollX]);
|
||||||
|
|
||||||
|
const fillItemsContainer = useCallback((entry) => {
|
||||||
|
const elem = entry.target;
|
||||||
|
const id = elem.getAttribute('data-id');
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
SortBy: 'Random',
|
||||||
|
SortOrder: 'Ascending',
|
||||||
|
IncludeItemTypes: 'Movie',
|
||||||
|
Recursive: true,
|
||||||
|
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
EnableImageTypes: 'Primary',
|
||||||
|
Limit: 12,
|
||||||
|
GenreIds: id,
|
||||||
|
EnableTotalRecordCount: false,
|
||||||
|
ParentId: topParentId
|
||||||
|
};
|
||||||
|
window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => {
|
||||||
|
cardBuilder.buildCards(result.Items || [], {
|
||||||
|
itemsContainer: elem,
|
||||||
|
shape: getPortraitShape(),
|
||||||
|
scalable: true,
|
||||||
|
overlayMoreButton: true,
|
||||||
|
allowBottomPadding: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
showYear: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [getPortraitShape, topParentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const elem = element.current;
|
||||||
|
lazyLoader.lazyChildren(elem, fillItemsContainer);
|
||||||
|
}, [itemsResult.Items, fillItemsContainer]);
|
||||||
|
|
||||||
|
const items = itemsResult.Items || [];
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
{
|
||||||
|
!items.length ? (
|
||||||
|
<div className='noItemsMessage centerMessage'>
|
||||||
|
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||||
|
<p>{globalize.translate('MessageNoGenresAvailable')}</p>
|
||||||
|
</div>
|
||||||
|
) : items.map((item, index) => (
|
||||||
|
<div key={index} className='verticalSection'>
|
||||||
|
<div
|
||||||
|
className='sectionTitleContainer sectionTitleContainer-cards padded-left'
|
||||||
|
dangerouslySetInnerHTML={createLinkElement({
|
||||||
|
className: 'more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre',
|
||||||
|
title: escapeHTML(item.Name),
|
||||||
|
href: appRouter.getRouteUrl(item, {
|
||||||
|
context: 'movies',
|
||||||
|
parentId: topParentId
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{enableScrollX() ?
|
||||||
|
<ItemsScrollerContainerElement
|
||||||
|
scrollerclassName='padded-top-focusscale padded-bottom-focusscale'
|
||||||
|
dataMousewheel='false'
|
||||||
|
dataCenterfocus='true'
|
||||||
|
className='itemsContainer scrollSlider focuscontainer-x lazy'
|
||||||
|
dataId={item.Id}
|
||||||
|
/> : <ItemsContainerElement
|
||||||
|
className='itemsContainer vertical-wrap lazy padded-left padded-right'
|
||||||
|
dataId={item.Id}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenresItemsContainer;
|
33
src/components/common/ItemsContainer.tsx
Normal file
33
src/components/common/ItemsContainer.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { FC, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
||||||
|
import imageLoader from '../images/imageLoader';
|
||||||
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
import { ViewQuerySettings } from '../../types/interface';
|
||||||
|
|
||||||
|
interface ItemsContainerI {
|
||||||
|
viewQuerySettings: ViewQuerySettings;
|
||||||
|
getItemsHtml: () => string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemsContainer: FC<ItemsContainerI> = ({ viewQuerySettings, getItemsHtml }) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement;
|
||||||
|
itemsContainer.innerHTML = getItemsHtml();
|
||||||
|
imageLoader.lazyChildren(itemsContainer);
|
||||||
|
}, [getItemsHtml]);
|
||||||
|
|
||||||
|
const cssClass = viewQuerySettings.imageType == 'list' ? 'vertical-list' : 'vertical-wrap';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<ItemsContainerElement
|
||||||
|
className={`itemsContainer ${cssClass} centered padded-left padded-right padded-right-withalphapicker`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItemsContainer;
|
38
src/components/common/NewCollection.tsx
Normal file
38
src/components/common/NewCollection.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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;
|
96
src/components/common/Pagination.tsx
Normal file
96
src/components/common/Pagination.tsx
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
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 recordsEnd = Math.min(startIndex + limit, totalRecordCount);
|
||||||
|
const showControls = 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'>
|
||||||
|
{showControls && (
|
||||||
|
<div className='listPaging' style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{globalize.translate('ListPaging', (totalRecordCount ? startIndex + 1 : 0), recordsEnd, totalRecordCount)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<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;
|
48
src/components/common/RecommendationContainer.tsx
Normal file
48
src/components/common/RecommendationContainer.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import type { RecommendationDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import escapeHTML from 'escape-html';
|
||||||
|
import SectionContainer from './SectionContainer';
|
||||||
|
|
||||||
|
interface RecommendationContainerProps {
|
||||||
|
getPortraitShape: () => string;
|
||||||
|
enableScrollX: () => boolean;
|
||||||
|
recommendation?: RecommendationDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecommendationContainer: FC<RecommendationContainerProps> = ({ getPortraitShape, enableScrollX, recommendation = {} }) => {
|
||||||
|
let title = '';
|
||||||
|
|
||||||
|
switch (recommendation.RecommendationType) {
|
||||||
|
case 'SimilarToRecentlyPlayed':
|
||||||
|
title = globalize.translate('RecommendationBecauseYouWatched', recommendation.BaselineItemName);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SimilarToLikedItem':
|
||||||
|
title = globalize.translate('RecommendationBecauseYouLike', recommendation.BaselineItemName);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'HasDirectorFromRecentlyPlayed':
|
||||||
|
case 'HasLikedDirector':
|
||||||
|
title = globalize.translate('RecommendationDirectedBy', recommendation.BaselineItemName);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'HasActorFromRecentlyPlayed':
|
||||||
|
case 'HasLikedActor':
|
||||||
|
title = globalize.translate('RecommendationStarring', recommendation.BaselineItemName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SectionContainer
|
||||||
|
sectionTitle={escapeHTML(title)}
|
||||||
|
enableScrollX={enableScrollX}
|
||||||
|
items={recommendation.Items || []}
|
||||||
|
cardOptions={{
|
||||||
|
shape: getPortraitShape(),
|
||||||
|
showYear: true
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecommendationContainer;
|
62
src/components/common/SectionContainer.tsx
Normal file
62
src/components/common/SectionContainer.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { FC, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||||
|
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
||||||
|
import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement';
|
||||||
|
import { CardOptions } from '../../types/interface';
|
||||||
|
|
||||||
|
interface SectionContainerProps {
|
||||||
|
sectionTitle: string;
|
||||||
|
enableScrollX: () => boolean;
|
||||||
|
items?: BaseItemDto[];
|
||||||
|
cardOptions?: CardOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SectionContainer: FC<SectionContainerProps> = ({
|
||||||
|
sectionTitle,
|
||||||
|
enableScrollX,
|
||||||
|
items = [],
|
||||||
|
cardOptions = {}
|
||||||
|
}) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cardBuilder.buildCards(items, {
|
||||||
|
itemsContainer: element.current?.querySelector('.itemsContainer'),
|
||||||
|
parentContainer: element.current?.querySelector('.verticalSection'),
|
||||||
|
scalable: true,
|
||||||
|
overlayPlayButton: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
cardLayout: false,
|
||||||
|
...cardOptions
|
||||||
|
});
|
||||||
|
}, [cardOptions, enableScrollX, items]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<div className='verticalSection hide'>
|
||||||
|
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
||||||
|
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
||||||
|
{sectionTitle}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enableScrollX() ? <ItemsScrollerContainerElement
|
||||||
|
scrollerclassName='padded-top-focusscale padded-bottom-focusscale'
|
||||||
|
dataMousewheel='false'
|
||||||
|
dataCenterfocus='true'
|
||||||
|
className='itemsContainer scrollSlider focuscontainer-x'
|
||||||
|
/> : <ItemsContainerElement
|
||||||
|
className='itemsContainer focuscontainer-x padded-left padded-right vertical-wrap'
|
||||||
|
/>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionContainer;
|
50
src/components/common/SelectView.tsx
Normal file
50
src/components/common/SelectView.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [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;
|
43
src/components/common/Shuffle.tsx
Normal file
43
src/components/common/Shuffle.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}, [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;
|
54
src/components/common/Sort.tsx
Normal file
54
src/components/common/Sort.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [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;
|
417
src/components/common/ViewItemsContainer.tsx
Normal file
417
src/components/common/ViewItemsContainer.tsx
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
import type { BaseItemDtoQueryResult } 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 { CardOptions, ViewQuerySettings } from '../../types/interface';
|
||||||
|
import ServerConnections from '../ServerConnections';
|
||||||
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
|
import listview from '../listview/listview';
|
||||||
|
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||||
|
|
||||||
|
interface ViewItemsContainerProps {
|
||||||
|
topParentId: string | null;
|
||||||
|
isBtnShuffleEnabled?: boolean;
|
||||||
|
isBtnFilterEnabled?: boolean;
|
||||||
|
isBtnNewCollectionEnabled?: boolean;
|
||||||
|
isAlphaPickerEnabled?: boolean;
|
||||||
|
getBasekey: () => string;
|
||||||
|
getItemTypes: () => string[];
|
||||||
|
getNoItemsMessage: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDefaultSortBy = () => {
|
||||||
|
return 'SortName';
|
||||||
|
};
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
let fields = 'BasicSyncInfo,MediaSourceCount';
|
||||||
|
|
||||||
|
if (viewQuerySettings.imageType === 'primary') {
|
||||||
|
fields += ',PrimaryImageAspectRatio';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewQuerySettings.showYear) {
|
||||||
|
fields += ',ProductionYear';
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryFilters: string[] = [];
|
||||||
|
|
||||||
|
if (viewQuerySettings.IsPlayed) {
|
||||||
|
queryFilters.push('IsPlayed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewQuerySettings.IsUnplayed) {
|
||||||
|
queryFilters.push('IsUnplayed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewQuerySettings.IsFavorite) {
|
||||||
|
queryFilters.push('IsFavorite');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewQuerySettings.IsResumable) {
|
||||||
|
queryFilters.push('IsResumable');
|
||||||
|
}
|
||||||
|
|
||||||
|
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: fields,
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo',
|
||||||
|
Limit: userSettings.libraryPageSize(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.imageType,
|
||||||
|
viewQuerySettings.showYear,
|
||||||
|
viewQuerySettings.IsPlayed,
|
||||||
|
viewQuerySettings.IsUnplayed,
|
||||||
|
viewQuerySettings.IsFavorite,
|
||||||
|
viewQuerySettings.IsResumable,
|
||||||
|
viewQuerySettings.IsHD,
|
||||||
|
viewQuerySettings.IsSD,
|
||||||
|
viewQuerySettings.SortBy,
|
||||||
|
viewQuerySettings.SortOrder,
|
||||||
|
viewQuerySettings.VideoTypes,
|
||||||
|
viewQuerySettings.GenreIds,
|
||||||
|
viewQuerySettings.Is4K,
|
||||||
|
viewQuerySettings.Is3D,
|
||||||
|
viewQuerySettings.HasSubtitles,
|
||||||
|
viewQuerySettings.HasTrailer,
|
||||||
|
viewQuerySettings.HasSpecialFeature,
|
||||||
|
viewQuerySettings.HasThemeSong,
|
||||||
|
viewQuerySettings.HasThemeVideo,
|
||||||
|
viewQuerySettings.StartIndex,
|
||||||
|
viewQuerySettings.NameLessThan,
|
||||||
|
viewQuerySettings.NameStartsWith,
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
loading.hide();
|
||||||
|
setisLoading(true);
|
||||||
|
});
|
||||||
|
}, [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;
|
|
@ -102,15 +102,8 @@ function onInputCommand(e) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function saveValues(context, settings, settingsKey) {
|
function saveValues(context, settings, settingsKey, setfilters) {
|
||||||
let elems = context.querySelectorAll('.simpleFilter');
|
let elems = context.querySelectorAll('.simpleFilter');
|
||||||
for (let i = 0, length = elems.length; i < length; i++) {
|
|
||||||
if (elems[i].tagName === 'INPUT') {
|
|
||||||
setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i]);
|
|
||||||
} else {
|
|
||||||
setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i].querySelector('input'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video type
|
// Video type
|
||||||
const videoTypes = [];
|
const videoTypes = [];
|
||||||
|
@ -121,7 +114,6 @@ function saveValues(context, settings, settingsKey) {
|
||||||
videoTypes.push(elems[i].getAttribute('data-filter'));
|
videoTypes.push(elems[i].getAttribute('data-filter'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userSettings.setFilter(settingsKey + '-filter-VideoTypes', videoTypes.join(','));
|
|
||||||
|
|
||||||
// Series status
|
// Series status
|
||||||
const seriesStatuses = [];
|
const seriesStatuses = [];
|
||||||
|
@ -132,7 +124,6 @@ function saveValues(context, settings, settingsKey) {
|
||||||
seriesStatuses.push(elems[i].getAttribute('data-filter'));
|
seriesStatuses.push(elems[i].getAttribute('data-filter'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userSettings.setFilter(`${settingsKey}-filter-SeriesStatus`, seriesStatuses.join(','));
|
|
||||||
|
|
||||||
// Genres
|
// Genres
|
||||||
const genres = [];
|
const genres = [];
|
||||||
|
@ -143,7 +134,39 @@ function saveValues(context, settings, settingsKey) {
|
||||||
genres.push(elems[i].getAttribute('data-filter'));
|
genres.push(elems[i].getAttribute('data-filter'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setfilters) {
|
||||||
|
setfilters((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
StartIndex: 0,
|
||||||
|
IsPlayed: context.querySelector('.chkPlayed').checked,
|
||||||
|
IsUnplayed: context.querySelector('.chkUnplayed').checked,
|
||||||
|
IsFavorite: context.querySelector('.chkFavorite').checked,
|
||||||
|
IsResumable: context.querySelector('.chkResumable').checked,
|
||||||
|
Is4K: context.querySelector('.chk4KFilter').checked,
|
||||||
|
IsHD: context.querySelector('.chkHDFilter').checked,
|
||||||
|
IsSD: context.querySelector('.chkSDFilter').checked,
|
||||||
|
Is3D: context.querySelector('.chk3DFilter').checked,
|
||||||
|
VideoTypes: videoTypes.join(','),
|
||||||
|
SeriesStatus: seriesStatuses.join(','),
|
||||||
|
HasSubtitles: context.querySelector('.chkSubtitle').checked,
|
||||||
|
HasTrailer: context.querySelector('.chkTrailer').checked,
|
||||||
|
HasSpecialFeature: context.querySelector('.chkSpecialFeature').checked,
|
||||||
|
HasThemeSong: context.querySelector('.chkThemeSong').checked,
|
||||||
|
HasThemeVideo: context.querySelector('.chkThemeVideo').checked,
|
||||||
|
GenreIds: genres.join(',')
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
for (let i = 0, length = elems.length; i < length; i++) {
|
||||||
|
if (elems[i].tagName === 'INPUT') {
|
||||||
|
setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i]);
|
||||||
|
} else {
|
||||||
|
setBasicFilter(context, settingsKey + '-filter-' + elems[i].getAttribute('data-settingname'), elems[i].querySelector('input'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
userSettings.setFilter(settingsKey + '-filter-GenreIds', genres.join(','));
|
userSettings.setFilter(settingsKey + '-filter-GenreIds', genres.join(','));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function bindCheckboxInput(context, on) {
|
function bindCheckboxInput(context, on) {
|
||||||
const elems = context.querySelectorAll('.checkboxList-verticalwrap');
|
const elems = context.querySelectorAll('.checkboxList-verticalwrap');
|
||||||
|
@ -275,7 +298,7 @@ class FilterMenu {
|
||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
//if (!options.onChange) {
|
//if (!options.onChange) {
|
||||||
saveValues(dlg, options.settings, options.settingsKey);
|
saveValues(dlg, options.settings, options.settingsKey, options.setfilters);
|
||||||
return resolve();
|
return resolve();
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,19 @@
|
||||||
<div class="verticalSection verticalSection-extrabottompadding basicFilterSection focuscontainer-x" style="margin-top:2em;">
|
<div class="verticalSection verticalSection-extrabottompadding basicFilterSection focuscontainer-x" style="margin-top:2em;">
|
||||||
<div class="checkboxList checkboxList-verticalwrap">
|
<div class="checkboxList checkboxList-verticalwrap">
|
||||||
<label class="viewSetting simpleFilter" data-settingname="IsUnplayed">
|
<label class="viewSetting simpleFilter" data-settingname="IsUnplayed">
|
||||||
<input type="checkbox" is="emby-checkbox" />
|
<input type="checkbox" is="emby-checkbox" class="chkUnplayed" />
|
||||||
<span>${Unplayed}</span>
|
<span>${Unplayed}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="viewSetting simpleFilter" data-settingname="IsPlayed">
|
<label class="viewSetting simpleFilter" data-settingname="IsPlayed">
|
||||||
<input type="checkbox" is="emby-checkbox" />
|
<input type="checkbox" is="emby-checkbox" class="chkPlayed" />
|
||||||
<span>${Played}</span>
|
<span>${Played}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="viewSetting simpleFilter" data-settingname="IsFavorite">
|
<label class="viewSetting simpleFilter" data-settingname="IsFavorite">
|
||||||
<input type="checkbox" is="emby-checkbox" />
|
<input type="checkbox" is="emby-checkbox" class="chkFavorite" />
|
||||||
<span>${Favorite}</span>
|
<span>${Favorite}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="viewSetting simpleFilter" data-settingname="IsResumable">
|
<label class="viewSetting simpleFilter" data-settingname="IsResumable">
|
||||||
<input type="checkbox" is="emby-checkbox" />
|
<input type="checkbox" is="emby-checkbox" class="chkResumable" />
|
||||||
<span>${ContinueWatching}</span>
|
<span>${ContinueWatching}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,22 +49,22 @@
|
||||||
<div class="checkboxList checkboxList-verticalwrap">
|
<div class="checkboxList checkboxList-verticalwrap">
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter" data-settingname="IsHD" />
|
<input type="checkbox" is="emby-checkbox" class="simpleFilter chkHDFilter" data-settingname="IsHD" />
|
||||||
<span>HD</span>
|
<span>HD</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter" data-settingname="Is4K" />
|
<input type="checkbox" is="emby-checkbox" class="simpleFilter chk4KFilter" data-settingname="Is4K" />
|
||||||
<span>4K</span>
|
<span>4K</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter" data-settingname="IsSD" />
|
<input type="checkbox" is="emby-checkbox" class="simpleFilter chkSDFilter" data-settingname="IsSD" />
|
||||||
<span>SD</span>
|
<span>SD</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter" data-settingname="Is3D" />
|
<input type="checkbox" is="emby-checkbox" class="simpleFilter chk3DFilter" data-settingname="Is3D" />
|
||||||
<span>3D</span>
|
<span>3D</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
@ -82,23 +82,23 @@
|
||||||
<h2 class="checkboxListLabel">${Features}</h2>
|
<h2 class="checkboxListLabel">${Features}</h2>
|
||||||
<div class="checkboxList checkboxList-verticalwrap">
|
<div class="checkboxList checkboxList-verticalwrap">
|
||||||
<label class="viewSetting simpleFilter" data-settingname="HasSubtitles">
|
<label class="viewSetting simpleFilter" data-settingname="HasSubtitles">
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkSubtitle" />
|
||||||
<span>${Subtitles}</span>
|
<span>${Subtitles}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="viewSetting simpleFilter" data-settingname="HasTrailer">
|
<label class="viewSetting simpleFilter" data-settingname="HasTrailer">
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkTrailer" />
|
||||||
<span>${Trailers}</span>
|
<span>${Trailers}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="viewSetting simpleFilter" data-settingname="HasSpecialFeature">
|
<label class="viewSetting simpleFilter" data-settingname="HasSpecialFeature">
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkSpecialFeature" />
|
||||||
<span>${Extras}</span>
|
<span>${Extras}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="viewSetting simpleFilter" data-settingname="HasThemeSong">
|
<label class="viewSetting simpleFilter" data-settingname="HasThemeSong">
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkThemeSong" />
|
||||||
<span>${ThemeSongs}</span>
|
<span>${ThemeSongs}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="viewSetting simpleFilter" data-settingname="HasThemeVideo">
|
<label class="viewSetting simpleFilter" data-settingname="HasThemeVideo">
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkThemeVideo" />
|
||||||
<span>${ThemeVideos}</span>
|
<span>${ThemeVideos}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -318,8 +318,9 @@ import toast from './toast/toast';
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'addtocollection':
|
case 'addtocollection':
|
||||||
import('./collectionEditor/collectionEditor').then(({default: collectionEditor}) => {
|
import('./collectionEditor/collectionEditor').then(({default: CollectionEditor}) => {
|
||||||
new collectionEditor({
|
const collectionEditor = new CollectionEditor();
|
||||||
|
collectionEditor.show({
|
||||||
items: [itemId],
|
items: [itemId],
|
||||||
serverId: serverId
|
serverId: serverId
|
||||||
}).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
}).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
||||||
|
|
|
@ -138,6 +138,7 @@ import '../elements/emby-button/emby-button';
|
||||||
|
|
||||||
configureSwipeTabs(view, tabsElem);
|
configureSwipeTabs(view, tabsElem);
|
||||||
|
|
||||||
|
if (getTabContainersFn) {
|
||||||
tabsElem.addEventListener('beforetabchange', function (e) {
|
tabsElem.addEventListener('beforetabchange', function (e) {
|
||||||
const tabContainers = getTabContainersFn();
|
const tabContainers = getTabContainersFn();
|
||||||
if (e.detail.previousIndex != null) {
|
if (e.detail.previousIndex != null) {
|
||||||
|
@ -153,6 +154,7 @@ import '../elements/emby-button/emby-button';
|
||||||
newPanel.classList.add('is-active');
|
newPanel.classList.add('is-active');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (onBeforeTabChange) {
|
if (onBeforeTabChange) {
|
||||||
tabsElem.addEventListener('beforetabchange', onBeforeTabChange);
|
tabsElem.addEventListener('beforetabchange', onBeforeTabChange);
|
||||||
|
|
|
@ -267,8 +267,9 @@ import datetime from '../../scripts/datetime';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'addtocollection':
|
case 'addtocollection':
|
||||||
import('../collectionEditor/collectionEditor').then(({default: collectionEditor}) => {
|
import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => {
|
||||||
new collectionEditor({
|
const collectionEditor = new CollectionEditor();
|
||||||
|
collectionEditor.show({
|
||||||
items: items,
|
items: items,
|
||||||
serverId: serverId
|
serverId: serverId
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,8 +18,8 @@ function onSubmit(e) {
|
||||||
function initEditor(context, settings) {
|
function initEditor(context, settings) {
|
||||||
context.querySelector('form').addEventListener('submit', onSubmit);
|
context.querySelector('form').addEventListener('submit', onSubmit);
|
||||||
|
|
||||||
context.querySelector('.selectSortOrder').value = settings.sortOrder;
|
context.querySelector('.selectSortOrder').value = settings.SortOrder;
|
||||||
context.querySelector('.selectSortBy').value = settings.sortBy;
|
context.querySelector('.selectSortBy').value = settings.SortBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
function centerFocus(elem, horiz, on) {
|
function centerFocus(elem, horiz, on) {
|
||||||
|
@ -37,9 +37,18 @@ function fillSortBy(context, options) {
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveValues(context, settingsKey) {
|
function saveValues(context, settingsKey, setSortValues) {
|
||||||
|
if (setSortValues) {
|
||||||
|
setSortValues((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
StartIndex: 0,
|
||||||
|
SortBy: context.querySelector('.selectSortBy').value,
|
||||||
|
SortOrder: context.querySelector('.selectSortOrder').value
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
userSettings.setFilter(settingsKey + '-sortorder', context.querySelector('.selectSortOrder').value);
|
userSettings.setFilter(settingsKey + '-sortorder', context.querySelector('.selectSortOrder').value);
|
||||||
userSettings.setFilter(settingsKey + '-sortby', context.querySelector('.selectSortBy').value);
|
userSettings.setFilter(settingsKey + '-sortby', context.querySelector('.selectSortBy').value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SortMenu {
|
class SortMenu {
|
||||||
|
@ -95,7 +104,7 @@ class SortMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
saveValues(dlg, options.settingsKey);
|
saveValues(dlg, options.settingsKey, options.setSortValues);
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,13 +29,24 @@ function initEditor(context, settings) {
|
||||||
context.querySelector('.selectImageType').value = settings.imageType || 'primary';
|
context.querySelector('.selectImageType').value = settings.imageType || 'primary';
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveValues(context, settings, settingsKey) {
|
function saveValues(context, settings, settingsKey, setviewsettings) {
|
||||||
|
if (setviewsettings) {
|
||||||
|
setviewsettings((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
StartIndex: 0,
|
||||||
|
imageType: context.querySelector('.selectImageType').value,
|
||||||
|
showTitle: context.querySelector('.chkShowTitle').checked || false,
|
||||||
|
showYear: context.querySelector('.chkShowYear').checked || false,
|
||||||
|
cardLayout: context.querySelector('.chkEnableCardLayout').checked || false
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
const elems = context.querySelectorAll('.viewSetting-checkboxContainer');
|
const elems = context.querySelectorAll('.viewSetting-checkboxContainer');
|
||||||
for (const elem of elems) {
|
for (const elem of elems) {
|
||||||
userSettings.set(settingsKey + '-' + elem.getAttribute('data-settingname'), elem.querySelector('input').checked);
|
userSettings.set(settingsKey + '-' + elem.getAttribute('data-settingname'), elem.querySelector('input').checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
userSettings.set(settingsKey + '-imageType', context.querySelector('.selectImageType').value);
|
userSettings.set(settingsKey + '-imageType', context.querySelector('.selectImageType').value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function centerFocus(elem, horiz, on) {
|
function centerFocus(elem, horiz, on) {
|
||||||
|
@ -57,7 +68,7 @@ function showIfAllowed(context, selector, visible) {
|
||||||
|
|
||||||
class ViewSettings {
|
class ViewSettings {
|
||||||
show(options) {
|
show(options) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve) {
|
||||||
const dialogOptions = {
|
const dialogOptions = {
|
||||||
removeOnClose: true,
|
removeOnClose: true,
|
||||||
scrollY: false
|
scrollY: false
|
||||||
|
@ -99,8 +110,9 @@ class ViewSettings {
|
||||||
initEditor(dlg, options.settings);
|
initEditor(dlg, options.settings);
|
||||||
|
|
||||||
dlg.querySelector('.selectImageType').addEventListener('change', function () {
|
dlg.querySelector('.selectImageType').addEventListener('change', function () {
|
||||||
showIfAllowed(dlg, '.chkTitleContainer', this.value !== 'list');
|
showIfAllowed(dlg, '.chkTitleContainer', this.value !== 'list' && this.value !== 'banner');
|
||||||
showIfAllowed(dlg, '.chkYearContainer', this.value !== 'list');
|
showIfAllowed(dlg, '.chkYearContainer', this.value !== 'list' && this.value !== 'banner');
|
||||||
|
showIfAllowed(dlg, '.chkCardLayoutContainer', this.value !== 'list' && this.value !== 'banner');
|
||||||
});
|
});
|
||||||
|
|
||||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||||
|
@ -125,12 +137,11 @@ class ViewSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
saveValues(dlg, options.settings, options.settingsKey);
|
saveValues(dlg, options.settings, options.settingsKey, options.setviewsettings);
|
||||||
resolve();
|
return resolve();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reject();
|
return resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,24 +17,31 @@
|
||||||
|
|
||||||
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide chkTitleContainer" data-settingname="showTitle">
|
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide chkTitleContainer" data-settingname="showTitle">
|
||||||
<label>
|
<label>
|
||||||
<input is="emby-checkbox" type="checkbox" />
|
<input is="emby-checkbox" type="checkbox" class="chkShowTitle" />
|
||||||
<span>${ShowTitle}</span>
|
<span>${ShowTitle}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide chkYearContainer" data-settingname="showYear">
|
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide chkYearContainer" data-settingname="showYear">
|
||||||
<label>
|
<label>
|
||||||
<input is="emby-checkbox" type="checkbox" />
|
<input is="emby-checkbox" type="checkbox" class="chkShowYear" />
|
||||||
<span>${ShowYear}</span>
|
<span>${ShowYear}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide" data-settingname="groupBySeries">
|
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide" data-settingname="groupBySeries">
|
||||||
<label>
|
<label>
|
||||||
<input is="emby-checkbox" type="checkbox" />
|
<input is="emby-checkbox" type="checkbox" class="chkGroupBySeries" />
|
||||||
<span>${GroupBySeries}</span>
|
<span>${GroupBySeries}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide chkCardLayoutContainer" data-settingname="cardLayout">
|
||||||
|
<label>
|
||||||
|
<input is="emby-checkbox" type="checkbox" class="chkEnableCardLayout" />
|
||||||
|
<span>${EnableCardLayout}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,267 +0,0 @@
|
||||||
import loading from '../../components/loading/loading';
|
|
||||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
|
||||||
import listView from '../../components/listview/listview';
|
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
|
|
||||||
/* eslint-disable indent */
|
|
||||||
|
|
||||||
export default function (view, params, tabContent) {
|
|
||||||
function getPageData(context) {
|
|
||||||
const key = getSavedQueryKey(context);
|
|
||||||
let pageData = data[key];
|
|
||||||
|
|
||||||
if (!pageData) {
|
|
||||||
pageData = data[key] = {
|
|
||||||
query: {
|
|
||||||
SortBy: 'SortName',
|
|
||||||
SortOrder: 'Ascending',
|
|
||||||
IncludeItemTypes: 'BoxSet',
|
|
||||||
Recursive: true,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,SortName',
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
|
||||||
StartIndex: 0
|
|
||||||
},
|
|
||||||
view: libraryBrowser.getSavedView(key) || 'Poster'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (userSettings.libraryPageSize() > 0) {
|
|
||||||
pageData.query['Limit'] = userSettings.libraryPageSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
pageData.query.ParentId = params.topParentId;
|
|
||||||
libraryBrowser.loadSavedQueryValues(key, pageData.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQuery(context) {
|
|
||||||
return getPageData(context).query;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSavedQueryKey(context) {
|
|
||||||
if (!context.savedQueryKey) {
|
|
||||||
context.savedQueryKey = libraryBrowser.getSavedQueryKey('moviecollections');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.savedQueryKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onViewStyleChange = () => {
|
|
||||||
const viewStyle = this.getCurrentViewStyle();
|
|
||||||
const itemsContainer = tabContent.querySelector('.itemsContainer');
|
|
||||||
|
|
||||||
if (viewStyle == 'List') {
|
|
||||||
itemsContainer.classList.add('vertical-list');
|
|
||||||
itemsContainer.classList.remove('vertical-wrap');
|
|
||||||
} else {
|
|
||||||
itemsContainer.classList.remove('vertical-list');
|
|
||||||
itemsContainer.classList.add('vertical-wrap');
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsContainer.innerHTML = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadItems = (page) => {
|
|
||||||
loading.show();
|
|
||||||
isLoading = true;
|
|
||||||
const query = getQuery(page);
|
|
||||||
ApiClient.getItems(ApiClient.getCurrentUserId(), query).then((result) => {
|
|
||||||
function onNextPageClick() {
|
|
||||||
if (isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userSettings.libraryPageSize() > 0) {
|
|
||||||
query.StartIndex += query.Limit;
|
|
||||||
}
|
|
||||||
reloadItems(tabContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPreviousPageClick() {
|
|
||||||
if (isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userSettings.libraryPageSize() > 0) {
|
|
||||||
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
|
|
||||||
}
|
|
||||||
reloadItems(tabContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
let html;
|
|
||||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
|
||||||
startIndex: query.StartIndex,
|
|
||||||
limit: query.Limit,
|
|
||||||
totalRecordCount: result.TotalRecordCount,
|
|
||||||
showLimit: false,
|
|
||||||
updatePageSizeSetting: false,
|
|
||||||
addLayoutButton: false,
|
|
||||||
sortButton: false,
|
|
||||||
filterButton: false
|
|
||||||
});
|
|
||||||
const viewStyle = this.getCurrentViewStyle();
|
|
||||||
if (viewStyle == 'Thumb') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'backdrop',
|
|
||||||
preferThumb: true,
|
|
||||||
context: 'movies',
|
|
||||||
overlayPlayButton: true,
|
|
||||||
centerText: true,
|
|
||||||
showTitle: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'ThumbCard') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'backdrop',
|
|
||||||
preferThumb: true,
|
|
||||||
context: 'movies',
|
|
||||||
lazy: true,
|
|
||||||
cardLayout: true,
|
|
||||||
showTitle: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'Banner') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'banner',
|
|
||||||
preferBanner: true,
|
|
||||||
context: 'movies',
|
|
||||||
lazy: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'List') {
|
|
||||||
html = listView.getListViewHtml({
|
|
||||||
items: result.Items,
|
|
||||||
context: 'movies',
|
|
||||||
sortBy: query.SortBy
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'PosterCard') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'auto',
|
|
||||||
context: 'movies',
|
|
||||||
showTitle: true,
|
|
||||||
centerText: false,
|
|
||||||
cardLayout: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'auto',
|
|
||||||
context: 'movies',
|
|
||||||
centerText: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let elems = tabContent.querySelectorAll('.paging');
|
|
||||||
|
|
||||||
for (const elem of elems) {
|
|
||||||
elem.innerHTML = pagingHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
elems = tabContent.querySelectorAll('.btnNextPage');
|
|
||||||
for (const elem of elems) {
|
|
||||||
elem.addEventListener('click', onNextPageClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
elems = tabContent.querySelectorAll('.btnPreviousPage');
|
|
||||||
for (const elem of elems) {
|
|
||||||
elem.addEventListener('click', onPreviousPageClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.Items.length) {
|
|
||||||
html = '';
|
|
||||||
|
|
||||||
html += '<div class="noItemsMessage centerMessage">';
|
|
||||||
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
|
|
||||||
html += '<p>' + globalize.translate('MessageNoCollectionsAvailable') + '</p>';
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsContainer = tabContent.querySelector('.itemsContainer');
|
|
||||||
itemsContainer.innerHTML = html;
|
|
||||||
imageLoader.lazyChildren(itemsContainer);
|
|
||||||
libraryBrowser.saveQueryValues(getSavedQueryKey(page), query);
|
|
||||||
loading.hide();
|
|
||||||
isLoading = false;
|
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
|
|
||||||
autoFocuser.autoFocus(page);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = {};
|
|
||||||
let isLoading = false;
|
|
||||||
|
|
||||||
this.getCurrentViewStyle = function () {
|
|
||||||
return getPageData(tabContent).view;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initPage = (tabElement) => {
|
|
||||||
tabElement.querySelector('.btnSort').addEventListener('click', function (e) {
|
|
||||||
libraryBrowser.showSortMenu({
|
|
||||||
items: [{
|
|
||||||
name: globalize.translate('Name'),
|
|
||||||
id: 'SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionImdbRating'),
|
|
||||||
id: 'CommunityRating,SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionDateAdded'),
|
|
||||||
id: 'DateCreated,SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionParentalRating'),
|
|
||||||
id: 'OfficialRating,SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionReleaseDate'),
|
|
||||||
id: 'PremiereDate,SortName'
|
|
||||||
}],
|
|
||||||
callback: function () {
|
|
||||||
getQuery(tabElement).StartIndex = 0;
|
|
||||||
reloadItems(tabElement);
|
|
||||||
},
|
|
||||||
query: getQuery(tabElement),
|
|
||||||
button: e.target
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const btnSelectView = tabElement.querySelector('.btnSelectView');
|
|
||||||
btnSelectView.addEventListener('click', (e) => {
|
|
||||||
libraryBrowser.showLayoutMenu(e.target, this.getCurrentViewStyle(), 'List,Poster,PosterCard,Thumb,ThumbCard'.split(','));
|
|
||||||
});
|
|
||||||
btnSelectView.addEventListener('layoutchange', function (e) {
|
|
||||||
const viewStyle = e.detail.viewStyle;
|
|
||||||
getPageData(tabElement).view = viewStyle;
|
|
||||||
libraryBrowser.saveViewSetting(getSavedQueryKey(tabElement), viewStyle);
|
|
||||||
getQuery(tabElement).StartIndex = 0;
|
|
||||||
onViewStyleChange();
|
|
||||||
reloadItems(tabElement);
|
|
||||||
});
|
|
||||||
tabElement.querySelector('.btnNewCollection').addEventListener('click', () => {
|
|
||||||
import('../../components/collectionEditor/collectionEditor').then(({default: collectionEditor}) => {
|
|
||||||
const serverId = ApiClient.serverInfo().Id;
|
|
||||||
new collectionEditor({
|
|
||||||
items: [],
|
|
||||||
serverId: serverId
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
initPage(tabContent);
|
|
||||||
onViewStyleChange();
|
|
||||||
|
|
||||||
this.renderTab = function () {
|
|
||||||
reloadItems(tabContent);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
|
@ -1,224 +0,0 @@
|
||||||
import escapeHtml from 'escape-html';
|
|
||||||
import layoutManager from '../../components/layoutManager';
|
|
||||||
import loading from '../../components/loading/loading';
|
|
||||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
|
||||||
import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver';
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import { appRouter } from '../../components/appRouter';
|
|
||||||
import '../../elements/emby-button/emby-button';
|
|
||||||
|
|
||||||
/* eslint-disable indent */
|
|
||||||
|
|
||||||
export default function (view, params, tabContent) {
|
|
||||||
function getPageData() {
|
|
||||||
const key = getSavedQueryKey();
|
|
||||||
let pageData = data[key];
|
|
||||||
|
|
||||||
if (!pageData) {
|
|
||||||
pageData = data[key] = {
|
|
||||||
query: {
|
|
||||||
SortBy: 'SortName',
|
|
||||||
SortOrder: 'Ascending',
|
|
||||||
IncludeItemTypes: 'Movie',
|
|
||||||
Recursive: true,
|
|
||||||
EnableTotalRecordCount: false
|
|
||||||
},
|
|
||||||
view: 'Poster'
|
|
||||||
};
|
|
||||||
pageData.query.ParentId = params.topParentId;
|
|
||||||
libraryBrowser.loadSavedQueryValues(key, pageData.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQuery() {
|
|
||||||
return getPageData().query;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSavedQueryKey() {
|
|
||||||
return libraryBrowser.getSavedQueryKey('moviegenres');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPromise() {
|
|
||||||
loading.show();
|
|
||||||
const query = getQuery();
|
|
||||||
return ApiClient.getGenres(ApiClient.getCurrentUserId(), query);
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableScrollX() {
|
|
||||||
return !layoutManager.desktop;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThumbShape() {
|
|
||||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPortraitShape() {
|
|
||||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
|
||||||
}
|
|
||||||
|
|
||||||
const fillItemsContainer = (entry) => {
|
|
||||||
const elem = entry.target;
|
|
||||||
const id = elem.getAttribute('data-id');
|
|
||||||
const viewStyle = this.getCurrentViewStyle();
|
|
||||||
let limit = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 5 : 9;
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
limit = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enableImageTypes = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 'Primary,Backdrop,Thumb' : 'Primary';
|
|
||||||
const query = {
|
|
||||||
SortBy: 'Random',
|
|
||||||
SortOrder: 'Ascending',
|
|
||||||
IncludeItemTypes: 'Movie',
|
|
||||||
Recursive: true,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: enableImageTypes,
|
|
||||||
Limit: limit,
|
|
||||||
GenreIds: id,
|
|
||||||
EnableTotalRecordCount: false,
|
|
||||||
ParentId: params.topParentId
|
|
||||||
};
|
|
||||||
ApiClient.getItems(ApiClient.getCurrentUserId(), query).then(function (result) {
|
|
||||||
if (viewStyle == 'Thumb') {
|
|
||||||
cardBuilder.buildCards(result.Items, {
|
|
||||||
itemsContainer: elem,
|
|
||||||
shape: getThumbShape(),
|
|
||||||
preferThumb: true,
|
|
||||||
showTitle: true,
|
|
||||||
scalable: true,
|
|
||||||
centerText: true,
|
|
||||||
overlayMoreButton: true,
|
|
||||||
allowBottomPadding: false
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'ThumbCard') {
|
|
||||||
cardBuilder.buildCards(result.Items, {
|
|
||||||
itemsContainer: elem,
|
|
||||||
shape: getThumbShape(),
|
|
||||||
preferThumb: true,
|
|
||||||
showTitle: true,
|
|
||||||
scalable: true,
|
|
||||||
centerText: false,
|
|
||||||
cardLayout: true,
|
|
||||||
showYear: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'PosterCard') {
|
|
||||||
cardBuilder.buildCards(result.Items, {
|
|
||||||
itemsContainer: elem,
|
|
||||||
shape: getPortraitShape(),
|
|
||||||
showTitle: true,
|
|
||||||
scalable: true,
|
|
||||||
centerText: false,
|
|
||||||
cardLayout: true,
|
|
||||||
showYear: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'Poster') {
|
|
||||||
cardBuilder.buildCards(result.Items, {
|
|
||||||
itemsContainer: elem,
|
|
||||||
shape: getPortraitShape(),
|
|
||||||
scalable: true,
|
|
||||||
overlayMoreButton: true,
|
|
||||||
allowBottomPadding: true,
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
showYear: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (result.Items.length >= query.Limit) {
|
|
||||||
tabContent.querySelector('.btnMoreFromGenre' + id + ' .material-icons').classList.remove('hide');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function reloadItems(context, promise) {
|
|
||||||
const query = getQuery();
|
|
||||||
promise.then(function (result) {
|
|
||||||
const elem = context.querySelector('#items');
|
|
||||||
let html = '';
|
|
||||||
const items = result.Items;
|
|
||||||
|
|
||||||
for (let i = 0, length = items.length; i < length; i++) {
|
|
||||||
const item = items[i];
|
|
||||||
|
|
||||||
html += '<div class="verticalSection">';
|
|
||||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
|
||||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item, {
|
|
||||||
context: 'movies',
|
|
||||||
parentId: params.topParentId
|
|
||||||
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre' + item.Id + '">';
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
|
||||||
html += escapeHtml(item.Name);
|
|
||||||
html += '</h2>';
|
|
||||||
html += '<span class="material-icons hide chevron_right" aria-hidden="true"></span>';
|
|
||||||
html += '</a>';
|
|
||||||
html += '</div>';
|
|
||||||
if (enableScrollX()) {
|
|
||||||
let scrollXClass = 'scrollX hiddenScrollX';
|
|
||||||
|
|
||||||
if (layoutManager.tv) {
|
|
||||||
scrollXClass += 'smoothScrollX padded-top-focusscale padded-bottom-focusscale';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' lazy padded-left padded-right" data-id="' + item.Id + '">';
|
|
||||||
} else {
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap lazy padded-left padded-right" data-id="' + item.Id + '">';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.Items.length) {
|
|
||||||
html = '';
|
|
||||||
|
|
||||||
html += '<div class="noItemsMessage centerMessage">';
|
|
||||||
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
|
|
||||||
html += '<p>' + globalize.translate('MessageNoGenresAvailable') + '</p>';
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
elem.innerHTML = html;
|
|
||||||
lazyLoader.lazyChildren(elem, fillItemsContainer);
|
|
||||||
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
|
|
||||||
loading.hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullyReload = () => {
|
|
||||||
this.preRender();
|
|
||||||
this.renderTab();
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = {};
|
|
||||||
|
|
||||||
this.getViewStyles = function () {
|
|
||||||
return 'Poster,PosterCard,Thumb,ThumbCard'.split(',');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getCurrentViewStyle = function () {
|
|
||||||
return getPageData().view;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setCurrentViewStyle = function (viewStyle) {
|
|
||||||
getPageData().view = viewStyle;
|
|
||||||
libraryBrowser.saveViewSetting(getSavedQueryKey(), viewStyle);
|
|
||||||
fullyReload();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.enableViewSelection = true;
|
|
||||||
let promise;
|
|
||||||
|
|
||||||
this.preRender = function () {
|
|
||||||
promise = getPromise();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.renderTab = function () {
|
|
||||||
reloadItems(tabContent, promise);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
|
@ -1,92 +0,0 @@
|
||||||
<div id="moviesPage" data-role="page" data-dom-cache="true" class="page libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs" data-backdroptype="movie">
|
|
||||||
|
|
||||||
<div class="pageTabContent" id="moviesTab" data-index="0">
|
|
||||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
|
||||||
<div class="paging"></div>
|
|
||||||
<button is="paper-icon-button-light" class="btnShuffle autoSize hide" title="${Shuffle}"><span class="material-icons shuffle" aria-hidden="true"></span></button>
|
|
||||||
<button is="paper-icon-button-light" class="btnSelectView autoSize" title="${ButtonSelectView}"><span class="material-icons view_comfy" aria-hidden="true"></span></button>
|
|
||||||
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
|
|
||||||
<button is="paper-icon-button-light" class="btnFilter autoSize" title="${Filter}"><span class="material-icons filter_list" aria-hidden="true"></span></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alphaPicker alphaPicker-fixed alphaPicker-vertical">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right">
|
|
||||||
</div>
|
|
||||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
|
||||||
<div class="paging"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pageTabContent" id="suggestionsTab" data-index="1">
|
|
||||||
<div id="resumableSection" class="verticalSection hide">
|
|
||||||
<div class="sectionTitleContainer sectionTitleContainer-cards">
|
|
||||||
<h2 class="sectionTitle sectionTitle-cards padded-left">${HeaderContinueWatching}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div is="emby-itemscontainer" id="resumableItems" class="itemsContainer padded-left padded-right">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="verticalSection">
|
|
||||||
<div class="sectionTitleContainer sectionTitleContainer-cards">
|
|
||||||
<h2 class="sectionTitle sectionTitle-cards padded-left">${HeaderLatestMovies}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div is="emby-itemscontainer" id="recentlyAddedItems" class="itemsContainer padded-left padded-right">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="recommendations">
|
|
||||||
</div>
|
|
||||||
<div class="noItemsMessage hide padded-left padded-right">
|
|
||||||
<br />
|
|
||||||
<p>${MessageNoMovieSuggestionsAvailable}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pageTabContent" id="trailersTab" data-index="2">
|
|
||||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
|
||||||
<div class="paging"></div>
|
|
||||||
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
|
|
||||||
<button is="paper-icon-button-light" class="btnFilter autoSize" title="${Filter}"><span class="material-icons filter_list" aria-hidden="true"></span></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alphaPicker alphaPicker-fixed alphaPicker-fixed-right alphaPicker-vertical">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div is="emby-itemscontainer" class="itemsContainer vertical-wrap padded-left padded-right">
|
|
||||||
</div>
|
|
||||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
|
||||||
<div class="paging"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pageTabContent" id="favoritesTab" data-index="3">
|
|
||||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
|
||||||
<div class="paging"></div>
|
|
||||||
<button is="paper-icon-button-light" class="btnSelectView autoSize" title="${ButtonSelectView}"><span class="material-icons view_comfy" aria-hidden="true"></span></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right">
|
|
||||||
</div>
|
|
||||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
|
||||||
<div class="paging"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pageTabContent" id="collectionsTab" data-index="4">
|
|
||||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
|
||||||
<div class="paging"></div>
|
|
||||||
<button is="paper-icon-button-light" class="btnSelectView autoSize" title="${ButtonSelectView}"><span class="material-icons view_comfy" aria-hidden="true"></span></button>
|
|
||||||
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
|
|
||||||
<button type="button" is="paper-icon-button-light" class="btnNewCollection autoSize" title="${NewCollection}"><span class="material-icons add" aria-hidden="true"></span></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div is="emby-itemscontainer" class="itemsContainer vertical-wrap centered padded-left padded-right" style="text-align:center;">
|
|
||||||
</div>
|
|
||||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
|
||||||
<div class="paging"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pageTabContent" id="genresTab" data-index="5">
|
|
||||||
<div id="items"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,327 +0,0 @@
|
||||||
import loading from '../../components/loading/loading';
|
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
|
||||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
|
||||||
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
|
|
||||||
import listView from '../../components/listview/listview';
|
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import Events from '../../utils/events.ts';
|
|
||||||
import { playbackManager } from '../../components/playback/playbackmanager';
|
|
||||||
|
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
|
|
||||||
/* eslint-disable indent */
|
|
||||||
|
|
||||||
export default function (view, params, tabContent, options) {
|
|
||||||
const onViewStyleChange = () => {
|
|
||||||
if (this.getCurrentViewStyle() == 'List') {
|
|
||||||
itemsContainer.classList.add('vertical-list');
|
|
||||||
itemsContainer.classList.remove('vertical-wrap');
|
|
||||||
} else {
|
|
||||||
itemsContainer.classList.remove('vertical-list');
|
|
||||||
itemsContainer.classList.add('vertical-wrap');
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsContainer.innerHTML = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
function fetchData() {
|
|
||||||
isLoading = true;
|
|
||||||
loading.show();
|
|
||||||
return ApiClient.getItems(ApiClient.getCurrentUserId(), query);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shuffle() {
|
|
||||||
ApiClient.getItem(
|
|
||||||
ApiClient.getCurrentUserId(),
|
|
||||||
params.topParentId
|
|
||||||
).then((item) => {
|
|
||||||
playbackManager.shuffle(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const afterRefresh = (result) => {
|
|
||||||
function onNextPageClick() {
|
|
||||||
if (isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userSettings.libraryPageSize() > 0) {
|
|
||||||
query.StartIndex += query.Limit;
|
|
||||||
}
|
|
||||||
itemsContainer.refreshItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPreviousPageClick() {
|
|
||||||
if (isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userSettings.libraryPageSize() > 0) {
|
|
||||||
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
|
|
||||||
}
|
|
||||||
itemsContainer.refreshItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
this.alphaPicker?.updateControls(query);
|
|
||||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
|
||||||
startIndex: query.StartIndex,
|
|
||||||
limit: query.Limit,
|
|
||||||
totalRecordCount: result.TotalRecordCount,
|
|
||||||
showLimit: false,
|
|
||||||
updatePageSizeSetting: false,
|
|
||||||
addLayoutButton: false,
|
|
||||||
sortButton: false,
|
|
||||||
filterButton: false
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const elem of tabContent.querySelectorAll('.paging')) {
|
|
||||||
elem.innerHTML = pagingHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const elem of tabContent.querySelectorAll('.btnNextPage')) {
|
|
||||||
elem.addEventListener('click', onNextPageClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const elem of tabContent.querySelectorAll('.btnPreviousPage')) {
|
|
||||||
elem.addEventListener('click', onPreviousPageClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
tabContent.querySelector('.btnShuffle').classList.toggle('hide', result.TotalRecordCount < 1);
|
|
||||||
|
|
||||||
isLoading = false;
|
|
||||||
loading.hide();
|
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
|
|
||||||
autoFocuser.autoFocus(tabContent);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getItemsHtml = (items) => {
|
|
||||||
let html;
|
|
||||||
const viewStyle = this.getCurrentViewStyle();
|
|
||||||
|
|
||||||
if (viewStyle == 'Thumb') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: items,
|
|
||||||
shape: 'backdrop',
|
|
||||||
preferThumb: true,
|
|
||||||
context: 'movies',
|
|
||||||
lazy: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
centerText: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'ThumbCard') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: items,
|
|
||||||
shape: 'backdrop',
|
|
||||||
preferThumb: true,
|
|
||||||
context: 'movies',
|
|
||||||
lazy: true,
|
|
||||||
cardLayout: true,
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
centerText: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'Banner') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: items,
|
|
||||||
shape: 'banner',
|
|
||||||
preferBanner: true,
|
|
||||||
context: 'movies',
|
|
||||||
lazy: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'List') {
|
|
||||||
html = listView.getListViewHtml({
|
|
||||||
items: items,
|
|
||||||
context: 'movies',
|
|
||||||
sortBy: query.SortBy
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'PosterCard') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: items,
|
|
||||||
shape: 'portrait',
|
|
||||||
context: 'movies',
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
centerText: true,
|
|
||||||
lazy: true,
|
|
||||||
cardLayout: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: items,
|
|
||||||
shape: 'portrait',
|
|
||||||
context: 'movies',
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
centerText: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initPage = (tabElement) => {
|
|
||||||
itemsContainer.fetchData = fetchData;
|
|
||||||
itemsContainer.getItemsHtml = getItemsHtml;
|
|
||||||
itemsContainer.afterRefresh = afterRefresh;
|
|
||||||
const alphaPickerElement = tabElement.querySelector('.alphaPicker');
|
|
||||||
|
|
||||||
if (alphaPickerElement) {
|
|
||||||
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
|
|
||||||
const newValue = e.detail.value;
|
|
||||||
if (newValue === '#') {
|
|
||||||
query.NameLessThan = 'A';
|
|
||||||
delete query.NameStartsWith;
|
|
||||||
} else {
|
|
||||||
query.NameStartsWith = newValue;
|
|
||||||
delete query.NameLessThan;
|
|
||||||
}
|
|
||||||
query.StartIndex = 0;
|
|
||||||
itemsContainer.refreshItems();
|
|
||||||
});
|
|
||||||
this.alphaPicker = new AlphaPicker({
|
|
||||||
element: alphaPickerElement,
|
|
||||||
valueChangeEvent: 'click'
|
|
||||||
});
|
|
||||||
|
|
||||||
tabElement.querySelector('.alphaPicker').classList.add('alphabetPicker-right');
|
|
||||||
alphaPickerElement.classList.add('alphaPicker-fixed-right');
|
|
||||||
itemsContainer.classList.add('padded-right-withalphapicker');
|
|
||||||
}
|
|
||||||
|
|
||||||
const btnFilter = tabElement.querySelector('.btnFilter');
|
|
||||||
|
|
||||||
if (btnFilter) {
|
|
||||||
btnFilter.addEventListener('click', () => {
|
|
||||||
this.showFilterMenu();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const btnSort = tabElement.querySelector('.btnSort');
|
|
||||||
|
|
||||||
if (btnSort) {
|
|
||||||
btnSort.addEventListener('click', function (e) {
|
|
||||||
libraryBrowser.showSortMenu({
|
|
||||||
items: [{
|
|
||||||
name: globalize.translate('Name'),
|
|
||||||
id: 'SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionRandom'),
|
|
||||||
id: 'Random'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionImdbRating'),
|
|
||||||
id: 'CommunityRating,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionCriticRating'),
|
|
||||||
id: 'CriticRating,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionDateAdded'),
|
|
||||||
id: 'DateCreated,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionDatePlayed'),
|
|
||||||
id: 'DatePlayed,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionParentalRating'),
|
|
||||||
id: 'OfficialRating,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionPlayCount'),
|
|
||||||
id: 'PlayCount,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionReleaseDate'),
|
|
||||||
id: 'PremiereDate,SortName,ProductionYear'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('Runtime'),
|
|
||||||
id: 'Runtime,SortName,ProductionYear'
|
|
||||||
}],
|
|
||||||
callback: function () {
|
|
||||||
query.StartIndex = 0;
|
|
||||||
userSettings.saveQuerySettings(savedQueryKey, query);
|
|
||||||
itemsContainer.refreshItems();
|
|
||||||
},
|
|
||||||
query: query,
|
|
||||||
button: e.target
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const btnSelectView = tabElement.querySelector('.btnSelectView');
|
|
||||||
btnSelectView.addEventListener('click', (e) => {
|
|
||||||
libraryBrowser.showLayoutMenu(e.target, this.getCurrentViewStyle(), 'Banner,List,Poster,PosterCard,Thumb,ThumbCard'.split(','));
|
|
||||||
});
|
|
||||||
btnSelectView.addEventListener('layoutchange', function (e) {
|
|
||||||
const viewStyle = e.detail.viewStyle;
|
|
||||||
userSettings.set(savedViewKey, viewStyle);
|
|
||||||
query.StartIndex = 0;
|
|
||||||
onViewStyleChange();
|
|
||||||
itemsContainer.refreshItems();
|
|
||||||
});
|
|
||||||
|
|
||||||
tabElement.querySelector('.btnShuffle').addEventListener('click', shuffle);
|
|
||||||
};
|
|
||||||
|
|
||||||
let itemsContainer = tabContent.querySelector('.itemsContainer');
|
|
||||||
const savedQueryKey = params.topParentId + '-' + options.mode;
|
|
||||||
const savedViewKey = savedQueryKey + '-view';
|
|
||||||
let query = {
|
|
||||||
SortBy: 'SortName,ProductionYear',
|
|
||||||
SortOrder: 'Ascending',
|
|
||||||
IncludeItemTypes: 'Movie',
|
|
||||||
Recursive: true,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
|
||||||
StartIndex: 0,
|
|
||||||
ParentId: params.topParentId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (userSettings.libraryPageSize() > 0) {
|
|
||||||
query['Limit'] = userSettings.libraryPageSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
let isLoading = false;
|
|
||||||
|
|
||||||
if (options.mode === 'favorites') {
|
|
||||||
query.IsFavorite = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
query = userSettings.loadQuerySettings(savedQueryKey, query);
|
|
||||||
|
|
||||||
this.showFilterMenu = function () {
|
|
||||||
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
|
|
||||||
const filterDialog = new filterDialogFactory({
|
|
||||||
query: query,
|
|
||||||
mode: 'movies',
|
|
||||||
serverId: ApiClient.serverId()
|
|
||||||
});
|
|
||||||
Events.on(filterDialog, 'filterchange', () => {
|
|
||||||
query.StartIndex = 0;
|
|
||||||
itemsContainer.refreshItems();
|
|
||||||
});
|
|
||||||
filterDialog.show();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getCurrentViewStyle = function () {
|
|
||||||
return userSettings.get(savedViewKey) || 'Poster';
|
|
||||||
};
|
|
||||||
|
|
||||||
this.initTab = function () {
|
|
||||||
initPage(tabContent);
|
|
||||||
onViewStyleChange();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.renderTab = () => {
|
|
||||||
itemsContainer.refreshItems();
|
|
||||||
this.alphaPicker?.updateControls(query);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.destroy = function () {
|
|
||||||
itemsContainer = null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
|
@ -1,428 +0,0 @@
|
||||||
import escapeHtml from 'escape-html';
|
|
||||||
import layoutManager from '../../components/layoutManager';
|
|
||||||
import inputManager from '../../scripts/inputManager';
|
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
|
||||||
import libraryMenu from '../../scripts/libraryMenu';
|
|
||||||
import * as mainTabsManager from '../../components/maintabsmanager';
|
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
|
||||||
import dom from '../../scripts/dom';
|
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
|
||||||
import { playbackManager } from '../../components/playback/playbackmanager';
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import Dashboard from '../../utils/dashboard';
|
|
||||||
import Events from '../../utils/events.ts';
|
|
||||||
|
|
||||||
import '../../elements/emby-scroller/emby-scroller';
|
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
import '../../elements/emby-tabs/emby-tabs';
|
|
||||||
import '../../elements/emby-button/emby-button';
|
|
||||||
|
|
||||||
/* eslint-disable indent */
|
|
||||||
|
|
||||||
function enableScrollX() {
|
|
||||||
return !layoutManager.desktop;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPortraitShape() {
|
|
||||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThumbShape() {
|
|
||||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadLatest(page, userId, parentId) {
|
|
||||||
const options = {
|
|
||||||
IncludeItemTypes: 'Movie',
|
|
||||||
Limit: 18,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
|
||||||
ParentId: parentId,
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
|
||||||
EnableTotalRecordCount: false
|
|
||||||
};
|
|
||||||
ApiClient.getJSON(ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(function (items) {
|
|
||||||
const allowBottomPadding = !enableScrollX();
|
|
||||||
const container = page.querySelector('#recentlyAddedItems');
|
|
||||||
cardBuilder.buildCards(items, {
|
|
||||||
itemsContainer: container,
|
|
||||||
shape: getPortraitShape(),
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
allowBottomPadding: allowBottomPadding,
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
centerText: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: Wait for all sections to load
|
|
||||||
autoFocus(page);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadResume(page, userId, parentId) {
|
|
||||||
const screenWidth = dom.getWindowSize().innerWidth;
|
|
||||||
const options = {
|
|
||||||
SortBy: 'DatePlayed',
|
|
||||||
SortOrder: 'Descending',
|
|
||||||
IncludeItemTypes: 'Movie',
|
|
||||||
Filters: 'IsResumable',
|
|
||||||
Limit: screenWidth >= 1600 ? 5 : 3,
|
|
||||||
Recursive: true,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
|
||||||
CollapseBoxSetItems: false,
|
|
||||||
ParentId: parentId,
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
|
||||||
EnableTotalRecordCount: false
|
|
||||||
};
|
|
||||||
ApiClient.getItems(userId, options).then(function (result) {
|
|
||||||
if (result.Items.length) {
|
|
||||||
page.querySelector('#resumableSection').classList.remove('hide');
|
|
||||||
} else {
|
|
||||||
page.querySelector('#resumableSection').classList.add('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowBottomPadding = !enableScrollX();
|
|
||||||
const container = page.querySelector('#resumableItems');
|
|
||||||
cardBuilder.buildCards(result.Items, {
|
|
||||||
itemsContainer: container,
|
|
||||||
preferThumb: true,
|
|
||||||
shape: getThumbShape(),
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
allowBottomPadding: allowBottomPadding,
|
|
||||||
cardLayout: false,
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
centerText: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: Wait for all sections to load
|
|
||||||
autoFocus(page);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecommendationHtml(recommendation) {
|
|
||||||
let html = '';
|
|
||||||
let title = '';
|
|
||||||
|
|
||||||
switch (recommendation.RecommendationType) {
|
|
||||||
case 'SimilarToRecentlyPlayed':
|
|
||||||
title = globalize.translate('RecommendationBecauseYouWatched', recommendation.BaselineItemName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'SimilarToLikedItem':
|
|
||||||
title = globalize.translate('RecommendationBecauseYouLike', recommendation.BaselineItemName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'HasDirectorFromRecentlyPlayed':
|
|
||||||
case 'HasLikedDirector':
|
|
||||||
title = globalize.translate('RecommendationDirectedBy', recommendation.BaselineItemName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'HasActorFromRecentlyPlayed':
|
|
||||||
case 'HasLikedActor':
|
|
||||||
title = globalize.translate('RecommendationStarring', recommendation.BaselineItemName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '<div class="verticalSection">';
|
|
||||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + escapeHtml(title) + '</h2>';
|
|
||||||
const allowBottomPadding = true;
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-mousewheel="false" data-centerfocus="true">';
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
|
||||||
} else {
|
|
||||||
html += '<div is="emby-itemscontainer" class="itemsContainer focuscontainer-x padded-left padded-right vertical-wrap">';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += cardBuilder.getCardsHtml(recommendation.Items, {
|
|
||||||
shape: getPortraitShape(),
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
allowBottomPadding: allowBottomPadding,
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
centerText: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (enableScrollX()) {
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
html += '</div>';
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSuggestions(page, userId) {
|
|
||||||
const screenWidth = dom.getWindowSize().innerWidth;
|
|
||||||
let itemLimit = 5;
|
|
||||||
if (screenWidth >= 1600) {
|
|
||||||
itemLimit = 8;
|
|
||||||
} else if (screenWidth >= 1200) {
|
|
||||||
itemLimit = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = ApiClient.getUrl('Movies/Recommendations', {
|
|
||||||
userId: userId,
|
|
||||||
categoryLimit: 6,
|
|
||||||
ItemLimit: itemLimit,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb'
|
|
||||||
});
|
|
||||||
ApiClient.getJSON(url).then(function (recommendations) {
|
|
||||||
if (!recommendations.length) {
|
|
||||||
page.querySelector('.noItemsMessage').classList.remove('hide');
|
|
||||||
page.querySelector('.recommendations').innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = recommendations.map(getRecommendationHtml).join('');
|
|
||||||
page.querySelector('.noItemsMessage').classList.add('hide');
|
|
||||||
const recs = page.querySelector('.recommendations');
|
|
||||||
recs.innerHTML = html;
|
|
||||||
imageLoader.lazyChildren(recs);
|
|
||||||
|
|
||||||
// FIXME: Wait for all sections to load
|
|
||||||
autoFocus(page);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoFocus(page) {
|
|
||||||
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
|
|
||||||
autoFocuser.autoFocus(page);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setScrollClasses(elem, scrollX) {
|
|
||||||
if (scrollX) {
|
|
||||||
elem.classList.add('hiddenScrollX');
|
|
||||||
|
|
||||||
if (layoutManager.tv) {
|
|
||||||
elem.classList.add('smoothScrollX');
|
|
||||||
elem.classList.add('padded-top-focusscale');
|
|
||||||
elem.classList.add('padded-bottom-focusscale');
|
|
||||||
}
|
|
||||||
|
|
||||||
elem.classList.add('scrollX');
|
|
||||||
elem.classList.remove('vertical-wrap');
|
|
||||||
} else {
|
|
||||||
elem.classList.remove('hiddenScrollX');
|
|
||||||
elem.classList.remove('smoothScrollX');
|
|
||||||
elem.classList.remove('scrollX');
|
|
||||||
elem.classList.add('vertical-wrap');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initSuggestedTab(page, tabContent) {
|
|
||||||
const containers = tabContent.querySelectorAll('.itemsContainer');
|
|
||||||
|
|
||||||
for (const container of containers) {
|
|
||||||
setScrollClasses(container, enableScrollX());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSuggestionsTab(view, params, tabContent) {
|
|
||||||
const parentId = params.topParentId;
|
|
||||||
const userId = ApiClient.getCurrentUserId();
|
|
||||||
loadResume(tabContent, userId, parentId);
|
|
||||||
loadLatest(tabContent, userId, parentId);
|
|
||||||
loadSuggestions(tabContent, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTabs() {
|
|
||||||
return [{
|
|
||||||
name: globalize.translate('Movies')
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('Suggestions')
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('Trailers')
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('Favorites')
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('Collections')
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('Genres')
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultTabIndex(folderId) {
|
|
||||||
switch (userSettings.get('landing-' + folderId)) {
|
|
||||||
case 'suggestions':
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
case 'favorites':
|
|
||||||
return 3;
|
|
||||||
|
|
||||||
case 'collections':
|
|
||||||
return 4;
|
|
||||||
|
|
||||||
case 'genres':
|
|
||||||
return 5;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function (view, params) {
|
|
||||||
function onBeforeTabChange(e) {
|
|
||||||
preLoadTab(view, parseInt(e.detail.selectedTabIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTabChange(e) {
|
|
||||||
const newIndex = parseInt(e.detail.selectedTabIndex);
|
|
||||||
loadTab(view, newIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTabContainers() {
|
|
||||||
return view.querySelectorAll('.pageTabContent');
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTabs() {
|
|
||||||
mainTabsManager.setTabs(view, currentTabIndex, getTabs, getTabContainers, onBeforeTabChange, onTabChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTabController = (page, index, callback) => {
|
|
||||||
let depends = '';
|
|
||||||
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
depends = 'movies';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
depends = 'moviesrecommended.js';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
depends = 'movietrailers';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
depends = 'movies';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 4:
|
|
||||||
depends = 'moviecollections';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 5:
|
|
||||||
depends = 'moviegenres';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
import(`../movies/${depends}`).then(({default: controllerFactory}) => {
|
|
||||||
let tabContent;
|
|
||||||
|
|
||||||
if (index === suggestionsTabIndex) {
|
|
||||||
tabContent = view.querySelector(".pageTabContent[data-index='" + index + "']");
|
|
||||||
this.tabContent = tabContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
let controller = tabControllers[index];
|
|
||||||
|
|
||||||
if (!controller) {
|
|
||||||
tabContent = view.querySelector(".pageTabContent[data-index='" + index + "']");
|
|
||||||
|
|
||||||
if (index === suggestionsTabIndex) {
|
|
||||||
controller = this;
|
|
||||||
} else if (index == 0 || index == 3) {
|
|
||||||
controller = new controllerFactory(view, params, tabContent, {
|
|
||||||
mode: index ? 'favorites' : 'movies'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
controller = new controllerFactory(view, params, tabContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
tabControllers[index] = controller;
|
|
||||||
|
|
||||||
if (controller.initTab) {
|
|
||||||
controller.initTab();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(controller);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function preLoadTab(page, index) {
|
|
||||||
getTabController(page, index, function (controller) {
|
|
||||||
if (renderedTabs.indexOf(index) == -1 && controller.preRender) {
|
|
||||||
controller.preRender();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTab(page, index) {
|
|
||||||
currentTabIndex = index;
|
|
||||||
getTabController(page, index, ((controller) => {
|
|
||||||
if (renderedTabs.indexOf(index) == -1) {
|
|
||||||
renderedTabs.push(index);
|
|
||||||
controller.renderTab();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPlaybackStop(e, state) {
|
|
||||||
if (state.NowPlayingItem && state.NowPlayingItem.MediaType == 'Video') {
|
|
||||||
renderedTabs = [];
|
|
||||||
mainTabsManager.getTabsElement().triggerTabChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onInputCommand(e) {
|
|
||||||
if (e.detail.command === 'search') {
|
|
||||||
e.preventDefault();
|
|
||||||
Dashboard.navigate('search.html?collectionType=movies&parentId=' + params.topParentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentTabIndex = parseInt(params.tab || getDefaultTabIndex(params.topParentId));
|
|
||||||
const suggestionsTabIndex = 1;
|
|
||||||
|
|
||||||
this.initTab = function () {
|
|
||||||
const tabContent = view.querySelector(".pageTabContent[data-index='" + suggestionsTabIndex + "']");
|
|
||||||
initSuggestedTab(view, tabContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.renderTab = function () {
|
|
||||||
const tabContent = view.querySelector(".pageTabContent[data-index='" + suggestionsTabIndex + "']");
|
|
||||||
loadSuggestionsTab(view, params, tabContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabControllers = [];
|
|
||||||
let renderedTabs = [];
|
|
||||||
view.addEventListener('viewshow', function () {
|
|
||||||
initTabs();
|
|
||||||
if (!view.getAttribute('data-title')) {
|
|
||||||
const parentId = params.topParentId;
|
|
||||||
|
|
||||||
if (parentId) {
|
|
||||||
ApiClient.getItem(ApiClient.getCurrentUserId(), parentId).then(function (item) {
|
|
||||||
view.setAttribute('data-title', item.Name);
|
|
||||||
libraryMenu.setTitle(item.Name);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
view.setAttribute('data-title', globalize.translate('Movies'));
|
|
||||||
libraryMenu.setTitle(globalize.translate('Movies'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Events.on(playbackManager, 'playbackstop', onPlaybackStop);
|
|
||||||
inputManager.on(window, onInputCommand);
|
|
||||||
});
|
|
||||||
view.addEventListener('viewbeforehide', function () {
|
|
||||||
inputManager.off(window, onInputCommand);
|
|
||||||
});
|
|
||||||
for (const tabController of tabControllers) {
|
|
||||||
if (tabController.destroy) {
|
|
||||||
tabController.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
|
@ -1,279 +0,0 @@
|
||||||
import loading from '../../components/loading/loading';
|
|
||||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
|
||||||
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
|
|
||||||
import listView from '../../components/listview/listview';
|
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import Events from '../../utils/events.ts';
|
|
||||||
|
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
|
|
||||||
/* eslint-disable indent */
|
|
||||||
|
|
||||||
export default function (view, params, tabContent) {
|
|
||||||
function getPageData(context) {
|
|
||||||
const key = getSavedQueryKey(context);
|
|
||||||
let pageData = data[key];
|
|
||||||
|
|
||||||
if (!pageData) {
|
|
||||||
pageData = data[key] = {
|
|
||||||
query: {
|
|
||||||
SortBy: 'SortName',
|
|
||||||
SortOrder: 'Ascending',
|
|
||||||
IncludeItemTypes: 'Trailer',
|
|
||||||
Recursive: true,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo',
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
|
||||||
StartIndex: 0
|
|
||||||
},
|
|
||||||
view: libraryBrowser.getSavedView(key) || 'Poster'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (userSettings.libraryPageSize() > 0) {
|
|
||||||
pageData.query['Limit'] = userSettings.libraryPageSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryBrowser.loadSavedQueryValues(key, pageData.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQuery(context) {
|
|
||||||
return getPageData(context).query;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSavedQueryKey(context) {
|
|
||||||
if (!context.savedQueryKey) {
|
|
||||||
context.savedQueryKey = libraryBrowser.getSavedQueryKey('trailers');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.savedQueryKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reloadItems = () => {
|
|
||||||
loading.show();
|
|
||||||
isLoading = true;
|
|
||||||
const query = getQuery(tabContent);
|
|
||||||
ApiClient.getItems(ApiClient.getCurrentUserId(), query).then((result) => {
|
|
||||||
function onNextPageClick() {
|
|
||||||
if (isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userSettings.libraryPageSize() > 0) {
|
|
||||||
query.StartIndex += query.Limit;
|
|
||||||
}
|
|
||||||
reloadItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPreviousPageClick() {
|
|
||||||
if (isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userSettings.libraryPageSize() > 0) {
|
|
||||||
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
|
|
||||||
}
|
|
||||||
reloadItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
this.alphaPicker?.updateControls(query);
|
|
||||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
|
||||||
startIndex: query.StartIndex,
|
|
||||||
limit: query.Limit,
|
|
||||||
totalRecordCount: result.TotalRecordCount,
|
|
||||||
showLimit: false,
|
|
||||||
updatePageSizeSetting: false,
|
|
||||||
addLayoutButton: false,
|
|
||||||
sortButton: false,
|
|
||||||
filterButton: false
|
|
||||||
});
|
|
||||||
let html;
|
|
||||||
const viewStyle = this.getCurrentViewStyle();
|
|
||||||
|
|
||||||
if (viewStyle == 'Thumb') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'backdrop',
|
|
||||||
preferThumb: true,
|
|
||||||
context: 'movies',
|
|
||||||
overlayPlayButton: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'ThumbCard') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'backdrop',
|
|
||||||
preferThumb: true,
|
|
||||||
context: 'movies',
|
|
||||||
cardLayout: true,
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
centerText: true
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'Banner') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'banner',
|
|
||||||
preferBanner: true,
|
|
||||||
context: 'movies'
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'List') {
|
|
||||||
html = listView.getListViewHtml({
|
|
||||||
items: result.Items,
|
|
||||||
context: 'movies',
|
|
||||||
sortBy: query.SortBy
|
|
||||||
});
|
|
||||||
} else if (viewStyle == 'PosterCard') {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'portrait',
|
|
||||||
context: 'movies',
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true,
|
|
||||||
cardLayout: true,
|
|
||||||
centerText: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
html = cardBuilder.getCardsHtml({
|
|
||||||
items: result.Items,
|
|
||||||
shape: 'portrait',
|
|
||||||
context: 'movies',
|
|
||||||
centerText: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
showYear: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let elems = tabContent.querySelectorAll('.paging');
|
|
||||||
|
|
||||||
for (const elem of elems) {
|
|
||||||
elem.innerHTML = pagingHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
elems = tabContent.querySelectorAll('.btnNextPage');
|
|
||||||
for (const elem of elems) {
|
|
||||||
elem.addEventListener('click', onNextPageClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
elems = tabContent.querySelectorAll('.btnPreviousPage');
|
|
||||||
for (const elem of elems) {
|
|
||||||
elem.addEventListener('click', onPreviousPageClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.Items.length) {
|
|
||||||
html = '';
|
|
||||||
|
|
||||||
html += '<div class="noItemsMessage centerMessage">';
|
|
||||||
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
|
|
||||||
html += '<p>' + globalize.translate('MessageNoTrailersFound') + '</p>';
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsContainer = tabContent.querySelector('.itemsContainer');
|
|
||||||
itemsContainer.innerHTML = html;
|
|
||||||
imageLoader.lazyChildren(itemsContainer);
|
|
||||||
libraryBrowser.saveQueryValues(getSavedQueryKey(tabContent), query);
|
|
||||||
loading.hide();
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = {};
|
|
||||||
let isLoading = false;
|
|
||||||
|
|
||||||
this.showFilterMenu = function () {
|
|
||||||
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
|
|
||||||
const filterDialog = new filterDialogFactory({
|
|
||||||
query: getQuery(tabContent),
|
|
||||||
mode: 'movies',
|
|
||||||
serverId: ApiClient.serverId()
|
|
||||||
});
|
|
||||||
Events.on(filterDialog, 'filterchange', function () {
|
|
||||||
getQuery(tabContent).StartIndex = 0;
|
|
||||||
reloadItems();
|
|
||||||
});
|
|
||||||
filterDialog.show();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getCurrentViewStyle = function () {
|
|
||||||
return getPageData(tabContent).view;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initPage = (tabElement) => {
|
|
||||||
const alphaPickerElement = tabElement.querySelector('.alphaPicker');
|
|
||||||
const itemsContainer = tabElement.querySelector('.itemsContainer');
|
|
||||||
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
|
|
||||||
const newValue = e.detail.value;
|
|
||||||
const query = getQuery(tabElement);
|
|
||||||
if (newValue === '#') {
|
|
||||||
query.NameLessThan = 'A';
|
|
||||||
delete query.NameStartsWith;
|
|
||||||
} else {
|
|
||||||
query.NameStartsWith = newValue;
|
|
||||||
delete query.NameLessThan;
|
|
||||||
}
|
|
||||||
query.StartIndex = 0;
|
|
||||||
reloadItems();
|
|
||||||
});
|
|
||||||
this.alphaPicker = new AlphaPicker({
|
|
||||||
element: alphaPickerElement,
|
|
||||||
valueChangeEvent: 'click'
|
|
||||||
});
|
|
||||||
|
|
||||||
tabElement.querySelector('.alphaPicker').classList.add('alphabetPicker-right');
|
|
||||||
alphaPickerElement.classList.add('alphaPicker-fixed-right');
|
|
||||||
itemsContainer.classList.add('padded-right-withalphapicker');
|
|
||||||
|
|
||||||
tabElement.querySelector('.btnFilter').addEventListener('click', () => {
|
|
||||||
this.showFilterMenu();
|
|
||||||
});
|
|
||||||
tabElement.querySelector('.btnSort').addEventListener('click', function (e) {
|
|
||||||
libraryBrowser.showSortMenu({
|
|
||||||
items: [{
|
|
||||||
name: globalize.translate('Name'),
|
|
||||||
id: 'SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionImdbRating'),
|
|
||||||
id: 'CommunityRating,SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionDateAdded'),
|
|
||||||
id: 'DateCreated,SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionDatePlayed'),
|
|
||||||
id: 'DatePlayed,SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionParentalRating'),
|
|
||||||
id: 'OfficialRating,SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionPlayCount'),
|
|
||||||
id: 'PlayCount,SortName'
|
|
||||||
}, {
|
|
||||||
name: globalize.translate('OptionReleaseDate'),
|
|
||||||
id: 'PremiereDate,SortName'
|
|
||||||
}],
|
|
||||||
callback: function () {
|
|
||||||
getQuery(tabElement).StartIndex = 0;
|
|
||||||
reloadItems();
|
|
||||||
},
|
|
||||||
query: getQuery(tabElement),
|
|
||||||
button: e.target
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
initPage(tabContent);
|
|
||||||
|
|
||||||
this.renderTab = () => {
|
|
||||||
reloadItems();
|
|
||||||
this.alphaPicker?.updateControls(getQuery(tabContent));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
28
src/elements/ItemsContainerElement.tsx
Normal file
28
src/elements/ItemsContainerElement.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
const createElement = ({ className, dataId }: IProps) => ({
|
||||||
|
__html: `<div
|
||||||
|
is="emby-itemscontainer"
|
||||||
|
class="${className}"
|
||||||
|
${dataId}
|
||||||
|
>
|
||||||
|
</div>`
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
dataId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemsContainerElement: FC<IProps> = ({ className, dataId }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createElement({
|
||||||
|
className: className,
|
||||||
|
dataId: dataId ? `data-id="${dataId}"` : ''
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItemsContainerElement;
|
43
src/elements/ItemsScrollerContainerElement.tsx
Normal file
43
src/elements/ItemsScrollerContainerElement.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
const createScroller = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, dataId, className }: IProps) => ({
|
||||||
|
__html: `<div is="emby-scroller"
|
||||||
|
class="${scrollerclassName}"
|
||||||
|
${dataHorizontal}
|
||||||
|
${dataMousewheel}
|
||||||
|
${dataCenterfocus}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
is="emby-itemscontainer"
|
||||||
|
class="${className}"
|
||||||
|
${dataId}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
scrollerclassName?: string;
|
||||||
|
dataHorizontal?: string;
|
||||||
|
dataMousewheel?: string;
|
||||||
|
dataCenterfocus?: string;
|
||||||
|
dataId?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemsScrollerContainerElement: FC<IProps> = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, dataId, className }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createScroller({
|
||||||
|
scrollerclassName: scrollerclassName,
|
||||||
|
dataHorizontal: dataHorizontal ? `data-horizontal="${dataHorizontal}"` : '',
|
||||||
|
dataMousewheel: dataMousewheel ? `data-mousewheel="${dataMousewheel}"` : '',
|
||||||
|
dataCenterfocus: dataCenterfocus ? `data-centerfocus="${dataCenterfocus}"` : '',
|
||||||
|
dataId: dataId ? `data-id="${dataId}"` : '',
|
||||||
|
className: className
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItemsScrollerContainerElement;
|
20
src/hooks/useLocalStorage.tsx
Normal file
20
src/hooks/useLocalStorage.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useLocalStorage<T>(key: string, initialValue: T | (() => T)) {
|
||||||
|
const [value, setValue] = useState<T>(() => {
|
||||||
|
const storedValues = localStorage.getItem(key);
|
||||||
|
if (storedValues != null) return JSON.parse(storedValues);
|
||||||
|
|
||||||
|
if (typeof initialValue === 'function') {
|
||||||
|
return (initialValue as () => T)();
|
||||||
|
} else {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
}, [key, value]);
|
||||||
|
|
||||||
|
return [value, setValue] as [typeof value, typeof setValue];
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import UserPassword from './user/userpassword';
|
||||||
import UserProfile from './user/userprofile';
|
import UserProfile from './user/userprofile';
|
||||||
import UserProfiles from './user/userprofiles';
|
import UserProfiles from './user/userprofiles';
|
||||||
import Home from './home';
|
import Home from './home';
|
||||||
|
import Movies from './movies';
|
||||||
|
|
||||||
const AppRoutes = () => (
|
const AppRoutes = () => (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
@ -20,6 +21,7 @@ const AppRoutes = () => (
|
||||||
<Route path='search.html' element={<Search />} />
|
<Route path='search.html' element={<Search />} />
|
||||||
<Route path='userprofile.html' element={<UserProfile />} />
|
<Route path='userprofile.html' element={<UserProfile />} />
|
||||||
<Route path='home.html' element={<Home />} />
|
<Route path='home.html' element={<Home />} />
|
||||||
|
<Route path='movies.html' element={<Movies />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Admin routes */}
|
{/* Admin routes */}
|
||||||
|
|
32
src/routes/movies/CollectionsView.tsx
Normal file
32
src/routes/movies/CollectionsView.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
|
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||||
|
import { LibraryViewProps } from '../../types/interface';
|
||||||
|
|
||||||
|
const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||||
|
const getBasekey = useCallback(() => {
|
||||||
|
return 'collections';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getItemTypes = useCallback(() => {
|
||||||
|
return ['BoxSet'];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getNoItemsMessage = useCallback(() => {
|
||||||
|
return 'MessageNoCollectionsAvailable';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewItemsContainer
|
||||||
|
topParentId={topParentId}
|
||||||
|
isBtnFilterEnabled={false}
|
||||||
|
isBtnNewCollectionEnabled={true}
|
||||||
|
isAlphaPickerEnabled={false}
|
||||||
|
getBasekey={getBasekey}
|
||||||
|
getItemTypes={getItemTypes}
|
||||||
|
getNoItemsMessage={getNoItemsMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionsView;
|
29
src/routes/movies/FavoritesView.tsx
Normal file
29
src/routes/movies/FavoritesView.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
|
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||||
|
import { LibraryViewProps } from '../../types/interface';
|
||||||
|
|
||||||
|
const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||||
|
const getBasekey = useCallback(() => {
|
||||||
|
return 'favorites';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getItemTypes = useCallback(() => {
|
||||||
|
return ['Movie'];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getNoItemsMessage = useCallback(() => {
|
||||||
|
return 'MessageNoFavoritesAvailable';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewItemsContainer
|
||||||
|
topParentId={topParentId}
|
||||||
|
getBasekey={getBasekey}
|
||||||
|
getItemTypes={getItemTypes}
|
||||||
|
getNoItemsMessage={getNoItemsMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FavoritesView;
|
41
src/routes/movies/GenresView.tsx
Normal file
41
src/routes/movies/GenresView.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import loading from '../../components/loading/loading';
|
||||||
|
import GenresItemsContainer from '../../components/common/GenresItemsContainer';
|
||||||
|
import { LibraryViewProps } from '../../types/interface';
|
||||||
|
|
||||||
|
const GenresView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||||
|
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
|
||||||
|
|
||||||
|
const reloadItems = useCallback(() => {
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.getGenres(
|
||||||
|
window.ApiClient.getCurrentUserId(),
|
||||||
|
{
|
||||||
|
SortBy: 'SortName',
|
||||||
|
SortOrder: 'Ascending',
|
||||||
|
IncludeItemTypes: 'Movie',
|
||||||
|
Recursive: true,
|
||||||
|
EnableTotalRecordCount: false,
|
||||||
|
ParentId: topParentId
|
||||||
|
}
|
||||||
|
).then((result) => {
|
||||||
|
setItemsResult(result);
|
||||||
|
loading.hide();
|
||||||
|
});
|
||||||
|
}, [topParentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadItems();
|
||||||
|
}, [reloadItems]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenresItemsContainer
|
||||||
|
topParentId={topParentId}
|
||||||
|
itemsResult={itemsResult}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenresView;
|
30
src/routes/movies/MoviesView.tsx
Normal file
30
src/routes/movies/MoviesView.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
|
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||||
|
import { LibraryViewProps } from '../../types/interface';
|
||||||
|
|
||||||
|
const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||||
|
const getBasekey = useCallback(() => {
|
||||||
|
return 'movies';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getItemTypes = useCallback(() => {
|
||||||
|
return ['Movie'];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getNoItemsMessage = useCallback(() => {
|
||||||
|
return 'MessageNoItemsAvailable';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewItemsContainer
|
||||||
|
topParentId={topParentId}
|
||||||
|
isBtnShuffleEnabled={true}
|
||||||
|
getBasekey={getBasekey}
|
||||||
|
getItemTypes={getItemTypes}
|
||||||
|
getNoItemsMessage={getNoItemsMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MoviesView;
|
153
src/routes/movies/SuggestionsView.tsx
Normal file
153
src/routes/movies/SuggestionsView.tsx
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import type { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import layoutManager from '../../components/layoutManager';
|
||||||
|
import loading from '../../components/loading/loading';
|
||||||
|
import dom from '../../scripts/dom';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import RecommendationContainer from '../../components/common/RecommendationContainer';
|
||||||
|
import SectionContainer from '../../components/common/SectionContainer';
|
||||||
|
import { LibraryViewProps } from '../../types/interface';
|
||||||
|
|
||||||
|
const SuggestionsView: FC<LibraryViewProps> = ({topParentId}) => {
|
||||||
|
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
|
||||||
|
const [ resumeResult, setResumeResult ] = useState<BaseItemDtoQueryResult>({});
|
||||||
|
const [ recommendations, setRecommendations ] = useState<RecommendationDto[]>([]);
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const enableScrollX = useCallback(() => {
|
||||||
|
return !layoutManager.desktop;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getPortraitShape = useCallback(() => {
|
||||||
|
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
||||||
|
}, [enableScrollX]);
|
||||||
|
|
||||||
|
const getThumbShape = useCallback(() => {
|
||||||
|
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
||||||
|
}, [enableScrollX]);
|
||||||
|
|
||||||
|
const autoFocus = useCallback((page) => {
|
||||||
|
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
|
||||||
|
autoFocuser.autoFocus(page);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadResume = useCallback((page, userId, parentId) => {
|
||||||
|
loading.show();
|
||||||
|
const screenWidth = dom.getWindowSize().innerWidth;
|
||||||
|
const options = {
|
||||||
|
SortBy: 'DatePlayed',
|
||||||
|
SortOrder: 'Descending',
|
||||||
|
IncludeItemTypes: 'Movie',
|
||||||
|
Filters: 'IsResumable',
|
||||||
|
Limit: screenWidth >= 1600 ? 5 : 3,
|
||||||
|
Recursive: true,
|
||||||
|
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
||||||
|
CollapseBoxSetItems: false,
|
||||||
|
ParentId: parentId,
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||||
|
EnableTotalRecordCount: false
|
||||||
|
};
|
||||||
|
window.ApiClient.getItems(userId, options).then(result => {
|
||||||
|
setResumeResult(result);
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
autoFocus(page);
|
||||||
|
});
|
||||||
|
}, [autoFocus]);
|
||||||
|
|
||||||
|
const loadLatest = useCallback((page: HTMLDivElement, userId: string, parentId: string | null) => {
|
||||||
|
const options = {
|
||||||
|
IncludeItemTypes: 'Movie',
|
||||||
|
Limit: 18,
|
||||||
|
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
||||||
|
ParentId: parentId,
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||||
|
EnableTotalRecordCount: false
|
||||||
|
};
|
||||||
|
window.ApiClient.getJSON(window.ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(items => {
|
||||||
|
setLatestItems(items);
|
||||||
|
|
||||||
|
autoFocus(page);
|
||||||
|
});
|
||||||
|
}, [autoFocus]);
|
||||||
|
|
||||||
|
const loadSuggestions = useCallback((page, userId) => {
|
||||||
|
const screenWidth = dom.getWindowSize().innerWidth;
|
||||||
|
let itemLimit = 5;
|
||||||
|
if (screenWidth >= 1600) {
|
||||||
|
itemLimit = 8;
|
||||||
|
} else if (screenWidth >= 1200) {
|
||||||
|
itemLimit = 6;
|
||||||
|
}
|
||||||
|
const url = window.ApiClient.getUrl('Movies/Recommendations', {
|
||||||
|
userId: userId,
|
||||||
|
categoryLimit: 6,
|
||||||
|
ItemLimit: itemLimit,
|
||||||
|
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb'
|
||||||
|
});
|
||||||
|
window.ApiClient.getJSON(url).then(result => {
|
||||||
|
setRecommendations(result);
|
||||||
|
|
||||||
|
autoFocus(page);
|
||||||
|
});
|
||||||
|
}, [autoFocus]);
|
||||||
|
|
||||||
|
const loadSuggestionsTab = useCallback((view) => {
|
||||||
|
const parentId = topParentId;
|
||||||
|
const userId = window.ApiClient.getCurrentUserId();
|
||||||
|
loadResume(view, userId, parentId);
|
||||||
|
loadLatest(view, userId, parentId);
|
||||||
|
loadSuggestions(view, userId);
|
||||||
|
}, [loadLatest, loadResume, loadSuggestions, topParentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSuggestionsTab(page);
|
||||||
|
}, [loadSuggestionsTab]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<SectionContainer
|
||||||
|
sectionTitle={globalize.translate('HeaderContinueWatching')}
|
||||||
|
enableScrollX={enableScrollX}
|
||||||
|
items={resumeResult.Items || []}
|
||||||
|
cardOptions={{
|
||||||
|
preferThumb: true,
|
||||||
|
shape: getThumbShape(),
|
||||||
|
showYear: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SectionContainer
|
||||||
|
sectionTitle={globalize.translate('HeaderLatestMovies')}
|
||||||
|
enableScrollX={enableScrollX}
|
||||||
|
items={latestItems}
|
||||||
|
cardOptions={{
|
||||||
|
shape: getPortraitShape(),
|
||||||
|
showYear: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!recommendations.length ? <div className='noItemsMessage centerMessage'>
|
||||||
|
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||||
|
<p>{globalize.translate('MessageNoMovieSuggestionsAvailable')}</p>
|
||||||
|
</div> : recommendations.map((recommendation, index) => {
|
||||||
|
return <RecommendationContainer key={index} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SuggestionsView;
|
30
src/routes/movies/TrailersView.tsx
Normal file
30
src/routes/movies/TrailersView.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
|
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||||
|
import { LibraryViewProps } from '../../types/interface';
|
||||||
|
|
||||||
|
const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||||
|
const getBasekey = useCallback(() => {
|
||||||
|
return 'trailers';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getItemTypes = useCallback(() => {
|
||||||
|
return ['Trailer'];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getNoItemsMessage = useCallback(() => {
|
||||||
|
return 'MessageNoTrailersFound';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewItemsContainer
|
||||||
|
topParentId={topParentId}
|
||||||
|
getBasekey={getBasekey}
|
||||||
|
getItemTypes={getItemTypes}
|
||||||
|
getNoItemsMessage={getNoItemsMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrailersView;
|
139
src/routes/movies/index.tsx
Normal file
139
src/routes/movies/index.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
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, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import * as mainTabsManager from '../../components/maintabsmanager';
|
||||||
|
import Page from '../../components/Page';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import libraryMenu from '../../scripts/libraryMenu';
|
||||||
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
|
import CollectionsView from './CollectionsView';
|
||||||
|
import FavoritesView from './FavoritesView';
|
||||||
|
import GenresView from './GenresView';
|
||||||
|
import MoviesView from './MoviesView';
|
||||||
|
import SuggestionsView from './SuggestionsView';
|
||||||
|
import TrailersView from './TrailersView';
|
||||||
|
|
||||||
|
const getDefaultTabIndex = (folderId: string | null) => {
|
||||||
|
switch (userSettings.get('landing-' + folderId, false)) {
|
||||||
|
case 'suggestions':
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
case 'favorites':
|
||||||
|
return 3;
|
||||||
|
|
||||||
|
case 'collections':
|
||||||
|
return 4;
|
||||||
|
|
||||||
|
case 'genres':
|
||||||
|
return 5;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabs = () => {
|
||||||
|
return [{
|
||||||
|
name: globalize.translate('Movies')
|
||||||
|
}, {
|
||||||
|
name: globalize.translate('Suggestions')
|
||||||
|
}, {
|
||||||
|
name: globalize.translate('Trailers')
|
||||||
|
}, {
|
||||||
|
name: globalize.translate('Favorites')
|
||||||
|
}, {
|
||||||
|
name: globalize.translate('Collections')
|
||||||
|
}, {
|
||||||
|
name: globalize.translate('Genres')
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
const Movies: FC = () => {
|
||||||
|
const [ searchParams ] = useSearchParams();
|
||||||
|
const currentTabIndex = parseInt(searchParams.get('tab') || getDefaultTabIndex(searchParams.get('topParentId')).toString());
|
||||||
|
const [ selectedIndex, setSelectedIndex ] = useState(currentTabIndex);
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const getTabComponent = (index: number) => {
|
||||||
|
if (index == null) {
|
||||||
|
throw new Error('index cannot be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
let component;
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
component = <MoviesView topParentId={searchParams.get('topParentId')} />;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
component = <SuggestionsView topParentId={searchParams.get('topParentId')} />;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
component = <TrailersView topParentId={searchParams.get('topParentId')} />;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
component = <FavoritesView topParentId={searchParams.get('topParentId')} />;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
component = <CollectionsView topParentId={searchParams.get('topParentId')} />;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
component = <GenresView topParentId={searchParams.get('topParentId')} />;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return component;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTabChange = useCallback((e: { detail: { selectedTabIndex: string; }; }) => {
|
||||||
|
const newIndex = parseInt(e.detail.selectedTabIndex);
|
||||||
|
setSelectedIndex(newIndex);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mainTabsManager.setTabs(page, selectedIndex, getTabs, undefined, undefined, onTabChange);
|
||||||
|
if (!page.getAttribute('data-title')) {
|
||||||
|
const parentId = searchParams.get('topParentId');
|
||||||
|
|
||||||
|
if (parentId) {
|
||||||
|
window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), parentId).then((item) => {
|
||||||
|
page.setAttribute('data-title', item.Name as string);
|
||||||
|
libraryMenu.setTitle(item.Name);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
page.setAttribute('data-title', globalize.translate('Movies'));
|
||||||
|
libraryMenu.setTitle(globalize.translate('Movies'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [onTabChange, searchParams, selectedIndex]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<Page
|
||||||
|
id='moviesPage'
|
||||||
|
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||||
|
backDropType='movie'
|
||||||
|
>
|
||||||
|
{getTabComponent(selectedIndex)}
|
||||||
|
|
||||||
|
</Page>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Movies;
|
|
@ -145,9 +145,15 @@
|
||||||
windowSize = null;
|
windowSize = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} windowSize
|
||||||
|
* @property {number} innerHeight - window innerHeight.
|
||||||
|
* @property {number} innerWidth - window innerWidth.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns window size.
|
* Returns window size.
|
||||||
* @returns {Object} Window size.
|
* @returns {windowSize} Window size.
|
||||||
*/
|
*/
|
||||||
export function getWindowSize() {
|
export function getWindowSize() {
|
||||||
if (!windowSize) {
|
if (!windowSize) {
|
||||||
|
|
|
@ -345,13 +345,6 @@ import { appRouter } from '../components/appRouter';
|
||||||
controller: 'livetvtuner'
|
controller: 'livetvtuner'
|
||||||
});
|
});
|
||||||
|
|
||||||
defineRoute({
|
|
||||||
alias: '/movies.html',
|
|
||||||
path: 'movies/movies.html',
|
|
||||||
autoFocus: false,
|
|
||||||
controller: 'movies/moviesrecommended'
|
|
||||||
});
|
|
||||||
|
|
||||||
defineRoute({
|
defineRoute({
|
||||||
alias: '/music.html',
|
alias: '/music.html',
|
||||||
path: 'music/music.html',
|
path: 'music/music.html',
|
||||||
|
|
|
@ -467,11 +467,17 @@ export class UserSettings {
|
||||||
return this.get('soundeffects', false);
|
return this.get('soundeffects', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Query
|
||||||
|
* @property {number} StartIndex - query StartIndex.
|
||||||
|
* @property {number} Limit - query Limit.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load query settings.
|
* Load query settings.
|
||||||
* @param {string} key - Query key.
|
* @param {string} key - Query key.
|
||||||
* @param {Object} query - Query base.
|
* @param {Object} query - Query base.
|
||||||
* @return {Object} Query.
|
* @return {Query} Query.
|
||||||
*/
|
*/
|
||||||
loadQuerySettings(key, query) {
|
loadQuerySettings(key, query) {
|
||||||
let values = this.get(key);
|
let values = this.get(key);
|
||||||
|
|
|
@ -236,6 +236,7 @@
|
||||||
"EnableNextVideoInfoOverlayHelp": "At the end of a video, display info about the next video coming up in the current playlist.",
|
"EnableNextVideoInfoOverlayHelp": "At the end of a video, display info about the next video coming up in the current playlist.",
|
||||||
"EnablePhotos": "Display the photos",
|
"EnablePhotos": "Display the photos",
|
||||||
"EnablePhotosHelp": "Images will be detected and displayed alongside other media files.",
|
"EnablePhotosHelp": "Images will be detected and displayed alongside other media files.",
|
||||||
|
"EnableCardLayout": "Display visual CardBox",
|
||||||
"EnableRewatchingNextUp": "Enable Rewatching in Next Up",
|
"EnableRewatchingNextUp": "Enable Rewatching in Next Up",
|
||||||
"EnableRewatchingNextUpHelp": "Enable showing already watched episodes in 'Next Up' sections.",
|
"EnableRewatchingNextUpHelp": "Enable showing already watched episodes in 'Next Up' sections.",
|
||||||
"EnableQuickConnect": "Enable Quick Connect on this server",
|
"EnableQuickConnect": "Enable Quick Connect on this server",
|
||||||
|
@ -1065,6 +1066,8 @@
|
||||||
"MessageItemsAdded": "Items added.",
|
"MessageItemsAdded": "Items added.",
|
||||||
"MessageItemSaved": "Item saved.",
|
"MessageItemSaved": "Item saved.",
|
||||||
"MessageLeaveEmptyToInherit": "Leave empty to inherit settings from a parent item or the global default value.",
|
"MessageLeaveEmptyToInherit": "Leave empty to inherit settings from a parent item or the global default value.",
|
||||||
|
"MessageNoItemsAvailable": "No Items are currently available.",
|
||||||
|
"MessageNoFavoritesAvailable": "No favorites are currently available.",
|
||||||
"MessageNoAvailablePlugins": "No available plugins.",
|
"MessageNoAvailablePlugins": "No available plugins.",
|
||||||
"MessageNoCollectionsAvailable": "Collections allow you to enjoy personalized groupings of Movies, Series, and Albums. Click the '+' button to start creating collections.",
|
"MessageNoCollectionsAvailable": "Collections allow you to enjoy personalized groupings of Movies, Series, and Albums. Click the '+' button to start creating collections.",
|
||||||
"MessageNoGenresAvailable": "Enable some metadata providers to pull genres from the internet.",
|
"MessageNoGenresAvailable": "Enable some metadata providers to pull genres from the internet.",
|
||||||
|
|
122
src/types/interface.ts
Normal file
122
src/types/interface.ts
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
|
||||||
|
export interface Query extends ViewQuerySettings {
|
||||||
|
IncludeItemTypes?: string;
|
||||||
|
Recursive?: boolean;
|
||||||
|
Fields?: string | null;
|
||||||
|
ImageTypeLimit?: number;
|
||||||
|
EnableTotalRecordCount?: boolean;
|
||||||
|
EnableImageTypes?: string;
|
||||||
|
StartIndex?: number;
|
||||||
|
ParentId?: string | null;
|
||||||
|
IsMissing?: boolean | null;
|
||||||
|
Limit?:number;
|
||||||
|
Filters?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardOptions {
|
||||||
|
itemsContainer?: HTMLElement | null;
|
||||||
|
parentContainer?: HTMLElement | null;
|
||||||
|
items?: BaseItemDto[] | null;
|
||||||
|
allowBottomPadding?: boolean;
|
||||||
|
centerText?: boolean;
|
||||||
|
coverImage?: boolean;
|
||||||
|
inheritThumb?: boolean;
|
||||||
|
overlayMoreButton?: boolean;
|
||||||
|
overlayPlayButton?: boolean;
|
||||||
|
overlayText?: boolean;
|
||||||
|
preferThumb?: boolean;
|
||||||
|
preferDisc?: boolean;
|
||||||
|
preferLogo?: boolean;
|
||||||
|
scalable?: boolean;
|
||||||
|
shape?: string | null;
|
||||||
|
lazy?: boolean;
|
||||||
|
cardLayout?: boolean | string;
|
||||||
|
showParentTitle?: boolean;
|
||||||
|
showParentTitleOrTitle?: boolean;
|
||||||
|
showAirTime?: boolean;
|
||||||
|
showAirDateTime?: boolean;
|
||||||
|
showChannelName?: boolean;
|
||||||
|
showTitle?: boolean | string;
|
||||||
|
showYear?: boolean | string;
|
||||||
|
showDetailsMenu?: boolean;
|
||||||
|
missingIndicator?: boolean;
|
||||||
|
showLocationTypeIndicator?: boolean;
|
||||||
|
showSeriesYear?: boolean;
|
||||||
|
showUnplayedIndicator?: boolean;
|
||||||
|
showChildCountIndicator?: boolean;
|
||||||
|
lines?: number;
|
||||||
|
context?: string | null;
|
||||||
|
action?: string | null;
|
||||||
|
defaultShape?: string;
|
||||||
|
indexBy?: string;
|
||||||
|
parentId?: string | null;
|
||||||
|
showMenu?: boolean;
|
||||||
|
cardCssClass?: string | null;
|
||||||
|
cardClass?: string | null;
|
||||||
|
centerPlayButton?: boolean;
|
||||||
|
overlayInfoButton?: boolean;
|
||||||
|
autoUpdate?: boolean;
|
||||||
|
cardFooterAside?: string;
|
||||||
|
includeParentInfoInTitle?: boolean;
|
||||||
|
maxLines?: number;
|
||||||
|
overlayMarkPlayedButton?: boolean;
|
||||||
|
overlayRateButton?: boolean;
|
||||||
|
showAirEndTime?: boolean;
|
||||||
|
showCurrentProgram?: boolean;
|
||||||
|
showCurrentProgramTime?: boolean;
|
||||||
|
showItemCounts?: boolean;
|
||||||
|
showPersonRoleOrType?: boolean;
|
||||||
|
showProgressBar?: boolean;
|
||||||
|
showPremiereDate?: boolean;
|
||||||
|
showRuntime?: boolean;
|
||||||
|
showSeriesTimerTime?: boolean;
|
||||||
|
showSeriesTimerChannel?: boolean;
|
||||||
|
showSongCount?: boolean;
|
||||||
|
width?: number;
|
||||||
|
showChannelLogo?: boolean;
|
||||||
|
showLogo?: boolean;
|
||||||
|
serverId?: string;
|
||||||
|
collectionId?: string | null;
|
||||||
|
playlistId?: string | null;
|
||||||
|
defaultCardImageIcon?: string;
|
||||||
|
disableHoverMenu?: boolean;
|
||||||
|
disableIndicators?: boolean;
|
||||||
|
showGroupCount?: boolean;
|
||||||
|
containerClass?: string;
|
||||||
|
noItemsMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryViewProps {
|
||||||
|
topParentId: string | null;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue