mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Migrate Movies
This commit is contained in:
parent
122c4ae600
commit
479c53eb8b
21 changed files with 1713 additions and 7 deletions
28
src/elements/ItemsContainerElement.tsx
Normal file
28
src/elements/ItemsContainerElement.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
|
const createButtonElement = ({ id, className }: IProps) => ({
|
||||||
|
__html: `<div
|
||||||
|
is="emby-itemscontainer"
|
||||||
|
id="${id}"
|
||||||
|
class="${className}"
|
||||||
|
>
|
||||||
|
</div>`
|
||||||
|
});
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemsContainerElement: FunctionComponent<IProps> = ({ id, className }: IProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={createButtonElement({
|
||||||
|
id: id,
|
||||||
|
className: className
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItemsContainerElement;
|
|
@ -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 */}
|
||||||
|
|
139
src/routes/movies.tsx
Normal file
139
src/routes/movies.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, { FunctionComponent, 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 '../view/movies/CollectionsView';
|
||||||
|
import FavoritesView from '../view/movies/FavoritesView';
|
||||||
|
import GenresView from '../view/movies/GenresView';
|
||||||
|
import MoviesView from '../view/movies/MoviesView';
|
||||||
|
import SuggestionsView from '../view/movies/SuggestionsView';
|
||||||
|
import TrailersView from '../view/movies/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 Movies: FunctionComponent = () => {
|
||||||
|
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 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 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(element.current, 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;
|
|
@ -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',
|
||||||
|
|
48
src/view/components/AlphaPickerContainer.tsx
Normal file
48
src/view/components/AlphaPickerContainer.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React, { FunctionComponent, useEffect, useRef, useState } from 'react';
|
||||||
|
import AlphaPicker from '../../components/alphaPicker/alphaPicker';
|
||||||
|
import { IQuery } from './type';
|
||||||
|
|
||||||
|
type AlphaPickerProps = {
|
||||||
|
query: IQuery;
|
||||||
|
reloadItems: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlphaPickerContainer: FunctionComponent<AlphaPickerProps> = ({ query, reloadItems }: AlphaPickerProps) => {
|
||||||
|
const [ alphaPicker, setAlphaPicker ] = useState<AlphaPicker>();
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
alphaPicker?.updateControls(query);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const alphaPickerElement = element.current?.querySelector('.alphaPicker');
|
||||||
|
|
||||||
|
if (alphaPickerElement) {
|
||||||
|
alphaPickerElement.addEventListener('alphavaluechanged', (e) => {
|
||||||
|
const newValue = (e as CustomEvent).detail.value;
|
||||||
|
if (newValue === '#') {
|
||||||
|
query.NameLessThan = 'A';
|
||||||
|
delete query.NameStartsWith;
|
||||||
|
} else {
|
||||||
|
query.NameStartsWith = newValue;
|
||||||
|
delete query.NameLessThan;
|
||||||
|
}
|
||||||
|
query.StartIndex = 0;
|
||||||
|
reloadItems();
|
||||||
|
});
|
||||||
|
setAlphaPicker(new AlphaPicker({
|
||||||
|
element: alphaPickerElement,
|
||||||
|
valueChangeEvent: 'click'
|
||||||
|
}));
|
||||||
|
|
||||||
|
alphaPickerElement.classList.add('alphaPicker-fixed-right');
|
||||||
|
}
|
||||||
|
}, [query, reloadItems, setAlphaPicker]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<div className='alphaPicker alphaPicker-fixed alphaPicker-vertical alphabetPicker-right' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlphaPickerContainer;
|
51
src/view/components/Filter.tsx
Normal file
51
src/view/components/Filter.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { Events } from 'jellyfin-apiclient';
|
||||||
|
import IconButtonElement from '../../elements/IconButtonElement';
|
||||||
|
import { IQuery } from './type';
|
||||||
|
|
||||||
|
type FilterProps = {
|
||||||
|
query: IQuery;
|
||||||
|
reloadItems: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Filter: FunctionComponent<FilterProps> = ({ query, reloadItems }: FilterProps) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const showFilterMenu = useCallback(() => {
|
||||||
|
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
|
||||||
|
const filterDialog = new filterDialogFactory({
|
||||||
|
query: query,
|
||||||
|
mode: 'movies',
|
||||||
|
serverId: window.ApiClient.serverId()
|
||||||
|
});
|
||||||
|
Events.on(filterDialog, 'filterchange', () => {
|
||||||
|
query.StartIndex = 0;
|
||||||
|
reloadItems();
|
||||||
|
});
|
||||||
|
filterDialog.show();
|
||||||
|
});
|
||||||
|
}, [query, reloadItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const btnFilter = element.current?.querySelector('.btnFilter');
|
||||||
|
|
||||||
|
if (btnFilter) {
|
||||||
|
btnFilter.addEventListener('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;
|
165
src/view/components/GenresItemsContainer.tsx
Normal file
165
src/view/components/GenresItemsContainer.tsx
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver';
|
||||||
|
import layoutManager from '../../components/layoutManager';
|
||||||
|
import { appRouter } from '../../components/appRouter';
|
||||||
|
import escapeHTML from 'escape-html';
|
||||||
|
import '../../elements/emby-button/emby-button';
|
||||||
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
import { IQuery } from './type';
|
||||||
|
|
||||||
|
type GenresItemsContainerProps = {
|
||||||
|
topParentId?: string | null;
|
||||||
|
getCurrentViewStyle: () => string;
|
||||||
|
query: IQuery;
|
||||||
|
itemsResult?: BaseItemDtoQueryResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenresItemsContainer: FunctionComponent<GenresItemsContainerProps> = ({ topParentId, getCurrentViewStyle, query, itemsResult = {} }: GenresItemsContainerProps) => {
|
||||||
|
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 fillItemsContainer = useCallback((entry) => {
|
||||||
|
const elem = entry.target;
|
||||||
|
const id = elem.getAttribute('data-id');
|
||||||
|
const viewStyle = 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: topParentId
|
||||||
|
};
|
||||||
|
window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => {
|
||||||
|
const items = result.Items || [];
|
||||||
|
if (viewStyle == 'Thumb') {
|
||||||
|
cardBuilder.buildCards(items, {
|
||||||
|
itemsContainer: elem,
|
||||||
|
shape: getThumbShape(),
|
||||||
|
preferThumb: true,
|
||||||
|
showTitle: true,
|
||||||
|
scalable: true,
|
||||||
|
centerText: true,
|
||||||
|
overlayMoreButton: true,
|
||||||
|
allowBottomPadding: false
|
||||||
|
});
|
||||||
|
} else if (viewStyle == 'ThumbCard') {
|
||||||
|
cardBuilder.buildCards(items, {
|
||||||
|
itemsContainer: elem,
|
||||||
|
shape: getThumbShape(),
|
||||||
|
preferThumb: true,
|
||||||
|
showTitle: true,
|
||||||
|
scalable: true,
|
||||||
|
centerText: false,
|
||||||
|
cardLayout: true,
|
||||||
|
showYear: true
|
||||||
|
});
|
||||||
|
} else if (viewStyle == 'PosterCard') {
|
||||||
|
cardBuilder.buildCards(items, {
|
||||||
|
itemsContainer: elem,
|
||||||
|
shape: getPortraitShape(),
|
||||||
|
showTitle: true,
|
||||||
|
scalable: true,
|
||||||
|
centerText: false,
|
||||||
|
cardLayout: true,
|
||||||
|
showYear: true
|
||||||
|
});
|
||||||
|
} else if (viewStyle == 'Poster') {
|
||||||
|
cardBuilder.buildCards(items, {
|
||||||
|
itemsContainer: elem,
|
||||||
|
shape: getPortraitShape(),
|
||||||
|
scalable: true,
|
||||||
|
overlayMoreButton: true,
|
||||||
|
allowBottomPadding: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
showYear: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [enableScrollX, getCurrentViewStyle, getPortraitShape, getThumbShape, topParentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const elem = element.current?.querySelector('#items') as HTMLDivElement;
|
||||||
|
let html = '';
|
||||||
|
const items = itemsResult.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: 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 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 (!itemsResult.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);
|
||||||
|
}, [getCurrentViewStyle, query.SortBy, itemsResult.Items, fillItemsContainer, topParentId, enableScrollX]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<div id='items'></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenresItemsContainer;
|
110
src/view/components/ItemsContainer.tsx
Normal file
110
src/view/components/ItemsContainer.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
|
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
||||||
|
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||||
|
import listview from '../../components/listview/listview';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import imageLoader from '../../components/images/imageLoader';
|
||||||
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
import { IQuery } from './type';
|
||||||
|
|
||||||
|
type ItemsContainerProps = {
|
||||||
|
getCurrentViewStyle: () => string;
|
||||||
|
query: IQuery;
|
||||||
|
items?: BaseItemDto[] | null;
|
||||||
|
noItemsMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemsContainer: FunctionComponent<ItemsContainerProps> = ({ getCurrentViewStyle, query, items = [], noItemsMessage }: ItemsContainerProps) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let html;
|
||||||
|
const viewStyle = getCurrentViewStyle();
|
||||||
|
if (viewStyle == 'Thumb') {
|
||||||
|
html = cardBuilder.getCardsHtml(items, {
|
||||||
|
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: 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: 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: items,
|
||||||
|
shape: 'portrait',
|
||||||
|
context: 'movies',
|
||||||
|
showTitle: true,
|
||||||
|
showYear: true,
|
||||||
|
centerText: true,
|
||||||
|
lazy: true,
|
||||||
|
cardLayout: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html = cardBuilder.getCardsHtml(items, {
|
||||||
|
items: items,
|
||||||
|
shape: 'portrait',
|
||||||
|
context: 'movies',
|
||||||
|
overlayPlayButton: true,
|
||||||
|
showTitle: true,
|
||||||
|
showYear: true,
|
||||||
|
centerText: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items?.length) {
|
||||||
|
html = '';
|
||||||
|
|
||||||
|
html += '<div class="noItemsMessage centerMessage">';
|
||||||
|
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
|
||||||
|
html += '<p>' + globalize.translate(noItemsMessage) + '</p>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement;
|
||||||
|
itemsContainer.innerHTML = html;
|
||||||
|
imageLoader.lazyChildren(itemsContainer);
|
||||||
|
}, [getCurrentViewStyle, query.SortBy, items, noItemsMessage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<ItemsContainerElement
|
||||||
|
id=''
|
||||||
|
className='itemsContainer vertical-wrap centered padded-left padded-right'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItemsContainer;
|
35
src/view/components/NewCollection.tsx
Normal file
35
src/view/components/NewCollection.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import IconButtonElement from '../../elements/IconButtonElement';
|
||||||
|
|
||||||
|
const NewCollection: FunctionComponent = () => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const btnNewCollection = element.current?.querySelector('.btnNewCollection') as HTMLButtonElement;
|
||||||
|
if (btnNewCollection) {
|
||||||
|
btnNewCollection.addEventListener('click', () => {
|
||||||
|
import('../../components/collectionEditor/collectionEditor').then(({ default: collectionEditor }) => {
|
||||||
|
const serverId = window.ApiClient.serverId();
|
||||||
|
new collectionEditor({
|
||||||
|
items: [],
|
||||||
|
serverId: serverId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<IconButtonElement
|
||||||
|
is='paper-icon-button-light'
|
||||||
|
className='btnNewCollection autoSize'
|
||||||
|
title='Add'
|
||||||
|
icon='material-icons add'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewCollection;
|
65
src/view/components/Pagination.tsx
Normal file
65
src/view/components/Pagination.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
|
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||||
|
import libraryBrowser from '../../scripts/libraryBrowser';
|
||||||
|
|
||||||
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
|
import { IQuery } from './type';
|
||||||
|
|
||||||
|
type PaginationProps = {
|
||||||
|
query: IQuery;
|
||||||
|
itemsResult?: BaseItemDtoQueryResult;
|
||||||
|
reloadItems: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination: FunctionComponent<PaginationProps> = ({ query, itemsResult = {}, reloadItems }: PaginationProps) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
function onNextPageClick() {
|
||||||
|
if (userSettings.libraryPageSize(undefined) > 0) {
|
||||||
|
query.StartIndex += query.Limit;
|
||||||
|
}
|
||||||
|
reloadItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPreviousPageClick() {
|
||||||
|
if (userSettings.libraryPageSize(undefined) > 0) {
|
||||||
|
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
|
||||||
|
}
|
||||||
|
reloadItems();
|
||||||
|
}
|
||||||
|
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
||||||
|
startIndex: query.StartIndex,
|
||||||
|
limit: query.Limit,
|
||||||
|
totalRecordCount: itemsResult.TotalRecordCount,
|
||||||
|
showLimit: false,
|
||||||
|
updatePageSizeSetting: false,
|
||||||
|
addLayoutButton: false,
|
||||||
|
sortButton: false,
|
||||||
|
filterButton: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const paging = element.current?.querySelector('.paging') as HTMLDivElement;
|
||||||
|
paging.innerHTML = pagingHtml;
|
||||||
|
|
||||||
|
const btnNextPage = element.current?.querySelector('.btnNextPage') as HTMLButtonElement;
|
||||||
|
if (btnNextPage) {
|
||||||
|
btnNextPage.addEventListener('click', onNextPageClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
const btnPreviousPage = element.current?.querySelector('.btnPreviousPage') as HTMLButtonElement;
|
||||||
|
if (btnPreviousPage) {
|
||||||
|
btnPreviousPage.addEventListener('click', onPreviousPageClick);
|
||||||
|
}
|
||||||
|
}, [itemsResult, query, reloadItems]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<div
|
||||||
|
className='paging'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
45
src/view/components/SelectView.tsx
Normal file
45
src/view/components/SelectView.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||||
|
import IconButtonElement from '../../elements/IconButtonElement';
|
||||||
|
|
||||||
|
import libraryBrowser from '../../scripts/libraryBrowser';
|
||||||
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
|
import { IQuery } from './type';
|
||||||
|
|
||||||
|
type SelectViewProps = {
|
||||||
|
getCurrentViewStyle: () => string;
|
||||||
|
query: IQuery;
|
||||||
|
savedViewKey: string;
|
||||||
|
onViewStyleChange: () => void;
|
||||||
|
reloadItems: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectView: FunctionComponent<SelectViewProps> = ({ getCurrentViewStyle, savedViewKey, query, onViewStyleChange, reloadItems }: SelectViewProps) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const btnSelectView = element.current?.querySelector('.btnSelectView') as HTMLButtonElement;
|
||||||
|
btnSelectView.addEventListener('click', (e) => {
|
||||||
|
libraryBrowser.showLayoutMenu(e.target, getCurrentViewStyle(), 'Banner,List,Poster,PosterCard,Thumb,ThumbCard'.split(','));
|
||||||
|
});
|
||||||
|
btnSelectView.addEventListener('layoutchange', (e) => {
|
||||||
|
const viewStyle = (e as CustomEvent).detail.viewStyle;
|
||||||
|
userSettings.set(savedViewKey, viewStyle, false);
|
||||||
|
query.StartIndex = 0;
|
||||||
|
onViewStyleChange();
|
||||||
|
reloadItems();
|
||||||
|
});
|
||||||
|
}, [getCurrentViewStyle, onViewStyleChange, query, reloadItems, savedViewKey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<IconButtonElement
|
||||||
|
is='paper-icon-button-light'
|
||||||
|
className='btnSelectView autoSize'
|
||||||
|
title='ButtonSelectView'
|
||||||
|
icon='material-icons view_comfy'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectView;
|
44
src/view/components/Shuffle.tsx
Normal file
44
src/view/components/Shuffle.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { playbackManager } from '../../components/playback/playbackmanager';
|
||||||
|
import IconButtonElement from '../../elements/IconButtonElement';
|
||||||
|
|
||||||
|
type ShuffleProps = {
|
||||||
|
itemsResult?: BaseItemDtoQueryResult;
|
||||||
|
topParentId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Shuffle: FunctionComponent<ShuffleProps> = ({ itemsResult = {}, topParentId }: ShuffleProps) => {
|
||||||
|
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') as HTMLButtonElement;
|
||||||
|
btnShuffle.classList.toggle('hide', typeof itemsResult.TotalRecordCount === 'number' && itemsResult.TotalRecordCount < 1);
|
||||||
|
if (btnShuffle) {
|
||||||
|
btnShuffle.addEventListener('click', shuffle);
|
||||||
|
}
|
||||||
|
}, [itemsResult.TotalRecordCount, shuffle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<IconButtonElement
|
||||||
|
is='paper-icon-button-light'
|
||||||
|
className='btnShuffle autoSize hide'
|
||||||
|
title='Shuffle'
|
||||||
|
icon='material-icons shuffle'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Shuffle;
|
48
src/view/components/Sort.tsx
Normal file
48
src/view/components/Sort.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||||
|
import IconButtonElement from '../../elements/IconButtonElement';
|
||||||
|
import libraryBrowser from '../../scripts/libraryBrowser';
|
||||||
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
|
import { IQuery } from './type';
|
||||||
|
|
||||||
|
type SortProps = {
|
||||||
|
SortMenuOptions: () => { name: string; id: string}[];
|
||||||
|
query: IQuery;
|
||||||
|
savedQueryKey: string;
|
||||||
|
reloadItems: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Sort: FunctionComponent<SortProps> = ({ SortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const btnSort = element.current?.querySelector('.btnSort');
|
||||||
|
|
||||||
|
if (btnSort) {
|
||||||
|
btnSort.addEventListener('click', (e) => {
|
||||||
|
libraryBrowser.showSortMenu({
|
||||||
|
items: SortMenuOptions(),
|
||||||
|
callback: () => {
|
||||||
|
query.StartIndex = 0;
|
||||||
|
userSettings.saveQuerySettings(savedQueryKey, query);
|
||||||
|
reloadItems();
|
||||||
|
},
|
||||||
|
query: query,
|
||||||
|
button: e.target
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [SortMenuOptions, query, reloadItems, savedQueryKey]);
|
||||||
|
|
||||||
|
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;
|
15
src/view/components/type.ts
Normal file
15
src/view/components/type.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export type IQuery = {
|
||||||
|
SortBy?: string;
|
||||||
|
SortOrder?: string;
|
||||||
|
IncludeItemTypes?: string;
|
||||||
|
Recursive?: boolean;
|
||||||
|
Fields?: string;
|
||||||
|
ImageTypeLimit?: number;
|
||||||
|
EnableImageTypes?: string;
|
||||||
|
StartIndex: number;
|
||||||
|
ParentId?: string | null;
|
||||||
|
IsFavorite?: boolean;
|
||||||
|
Limit:number;
|
||||||
|
NameLessThan?: string;
|
||||||
|
NameStartsWith?: string;
|
||||||
|
}
|
122
src/view/movies/CollectionsView.tsx
Normal file
122
src/view/movies/CollectionsView.tsx
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
|
import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import loading from '../../components/loading/loading';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
|
import ItemsContainer from '../components/ItemsContainer';
|
||||||
|
import NewCollection from '../components/NewCollection';
|
||||||
|
import Pagination from '../components/Pagination';
|
||||||
|
import SelectView from '../components/SelectView';
|
||||||
|
import Sort from '../components/Sort';
|
||||||
|
import { IQuery } from '../components/type';
|
||||||
|
|
||||||
|
const SortMenuOptions = () => {
|
||||||
|
return [{
|
||||||
|
name: globalize.translate('Name'),
|
||||||
|
id: 'SortName'
|
||||||
|
}, {
|
||||||
|
name: globalize.translate('OptionDateAdded'),
|
||||||
|
id: 'DateCreated,SortName'
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
topParentId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CollectionsView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
|
||||||
|
const savedQueryKey = topParentId + '-moviecollections';
|
||||||
|
const savedViewKey = savedQueryKey + '-view';
|
||||||
|
|
||||||
|
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const query = useMemo<IQuery>(() => ({
|
||||||
|
SortBy: 'SortName',
|
||||||
|
SortOrder: 'Ascending',
|
||||||
|
IncludeItemTypes: 'BoxSet',
|
||||||
|
Recursive: true,
|
||||||
|
Fields: 'PrimaryImageAspectRatio,SortName',
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||||
|
Limit: userSettings.libraryPageSize(undefined),
|
||||||
|
StartIndex: 0,
|
||||||
|
ParentId: topParentId }), [topParentId]);
|
||||||
|
|
||||||
|
userSettings.loadQuerySettings(savedQueryKey, query);
|
||||||
|
|
||||||
|
const getCurrentViewStyle = useCallback(() => {
|
||||||
|
return userSettings.get(savedViewKey, false) || 'Poster';
|
||||||
|
}, [savedViewKey]);
|
||||||
|
|
||||||
|
const reloadItems = useCallback(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => {
|
||||||
|
setItemsResult(result);
|
||||||
|
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
|
||||||
|
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
|
autoFocuser.autoFocus(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
const onViewStyleChange = useCallback(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewStyle = getCurrentViewStyle();
|
||||||
|
const itemsContainer = page.querySelector('.itemsContainer') as HTMLDivElement;
|
||||||
|
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 = '';
|
||||||
|
}, [getCurrentViewStyle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onViewStyleChange();
|
||||||
|
reloadItems();
|
||||||
|
}, [onViewStyleChange, 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} query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
<SelectView getCurrentViewStyle={getCurrentViewStyle} savedViewKey={savedViewKey} query={query} onViewStyleChange={onViewStyleChange} reloadItems={reloadItems} />
|
||||||
|
<Sort SortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
||||||
|
<NewCollection />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemsContainer getCurrentViewStyle={getCurrentViewStyle} query={query} items={itemsResult?.Items} noItemsMessage= 'MessageNoCollectionsAvailable' />
|
||||||
|
|
||||||
|
<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} query={query} reloadItems={reloadItems} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionsView;
|
147
src/view/movies/FavoritesView.tsx
Normal file
147
src/view/movies/FavoritesView.tsx
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import loading from '../../components/loading/loading';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
|
import AlphaPickerContainer from '../components/AlphaPickerContainer';
|
||||||
|
import Filter from '../components/Filter';
|
||||||
|
import ItemsContainer from '../components/ItemsContainer';
|
||||||
|
import Pagination from '../components/Pagination';
|
||||||
|
import SelectView from '../components/SelectView';
|
||||||
|
import Sort from '../components/Sort';
|
||||||
|
import { IQuery } from '../components/type';
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
topParentId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortMenuOptions = () => {
|
||||||
|
return [{
|
||||||
|
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'
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
const FavoritesView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
|
||||||
|
const savedQueryKey = topParentId + '-favorites';
|
||||||
|
const savedViewKey = savedQueryKey + '-view';
|
||||||
|
|
||||||
|
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const query = useMemo<IQuery>(() => ({
|
||||||
|
SortBy: 'SortName,ProductionYear',
|
||||||
|
SortOrder: 'Ascending',
|
||||||
|
IncludeItemTypes: 'Movie',
|
||||||
|
Recursive: true,
|
||||||
|
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||||
|
Limit: userSettings.libraryPageSize(undefined),
|
||||||
|
IsFavorite: true,
|
||||||
|
StartIndex: 0,
|
||||||
|
ParentId: topParentId }), [topParentId]);
|
||||||
|
|
||||||
|
userSettings.loadQuerySettings(savedQueryKey, query);
|
||||||
|
|
||||||
|
const getCurrentViewStyle = useCallback(() => {
|
||||||
|
return userSettings.get(savedViewKey, false) || 'Poster';
|
||||||
|
}, [savedViewKey]);
|
||||||
|
|
||||||
|
const reloadItems = useCallback(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => {
|
||||||
|
setItemsResult(result);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
loading.hide();
|
||||||
|
|
||||||
|
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
|
autoFocuser.autoFocus(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
const onViewStyleChange = useCallback(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewStyle = getCurrentViewStyle();
|
||||||
|
const itemsContainer = page.querySelector('.itemsContainer') as HTMLDivElement;
|
||||||
|
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 = '';
|
||||||
|
}, [getCurrentViewStyle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onViewStyleChange();
|
||||||
|
reloadItems();
|
||||||
|
}, [onViewStyleChange, query, 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} query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
<SelectView getCurrentViewStyle={getCurrentViewStyle} savedViewKey={savedViewKey} query={query} onViewStyleChange={onViewStyleChange} reloadItems={reloadItems} />
|
||||||
|
<Sort SortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
||||||
|
<Filter query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlphaPickerContainer query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
<ItemsContainer getCurrentViewStyle={getCurrentViewStyle} query={query} items={itemsResult?.Items} noItemsMessage= 'MessageNoFavoritesAvailable' />
|
||||||
|
|
||||||
|
<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} query={query} reloadItems={reloadItems} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FavoritesView;
|
61
src/view/movies/GenresView.tsx
Normal file
61
src/view/movies/GenresView.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import loading from '../../components/loading/loading';
|
||||||
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
|
import GenresItemsContainer from '../components/GenresItemsContainer';
|
||||||
|
import { IQuery } from '../components/type';
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
topParentId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenresView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
|
||||||
|
const savedQueryKey = topParentId + '-moviegenres';
|
||||||
|
const savedViewKey = savedQueryKey + '-view';
|
||||||
|
|
||||||
|
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const query = useMemo<IQuery>(() => ({
|
||||||
|
SortBy: 'SortName',
|
||||||
|
SortOrder: 'Ascending',
|
||||||
|
IncludeItemTypes: 'Movie',
|
||||||
|
Recursive: true,
|
||||||
|
EnableTotalRecordCount: false,
|
||||||
|
Limit: userSettings.libraryPageSize(undefined),
|
||||||
|
StartIndex: 0,
|
||||||
|
ParentId: topParentId }), [topParentId]);
|
||||||
|
|
||||||
|
userSettings.loadQuerySettings(savedQueryKey, query);
|
||||||
|
|
||||||
|
const getCurrentViewStyle = useCallback(() => {
|
||||||
|
return userSettings.get(savedViewKey, false) || 'Poster';
|
||||||
|
}, [savedViewKey]);
|
||||||
|
|
||||||
|
const reloadItems = useCallback(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.getGenres(window.ApiClient.getCurrentUserId(), query).then((result) => {
|
||||||
|
setItemsResult(result);
|
||||||
|
loading.hide();
|
||||||
|
});
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadItems();
|
||||||
|
}, [reloadItems]);
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<GenresItemsContainer topParentId={topParentId} getCurrentViewStyle={getCurrentViewStyle} query={query} itemsResult={itemsResult} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenresView;
|
153
src/view/movies/MoviesView.tsx
Normal file
153
src/view/movies/MoviesView.tsx
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import loading from '../../components/loading/loading';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
|
import AlphaPickerContainer from '../components/AlphaPickerContainer';
|
||||||
|
import Filter from '../components/Filter';
|
||||||
|
import ItemsContainer from '../components/ItemsContainer';
|
||||||
|
import Pagination from '../components/Pagination';
|
||||||
|
import SelectView from '../components/SelectView';
|
||||||
|
import Shuffle from '../components/Shuffle';
|
||||||
|
import Sort from '../components/Sort';
|
||||||
|
import { IQuery } from '../components/type';
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
topParentId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortMenuOptions = () => {
|
||||||
|
return [{
|
||||||
|
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'
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
const MoviesView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
|
||||||
|
const savedQueryKey = topParentId + '-movies';
|
||||||
|
const savedViewKey = savedQueryKey + '-view';
|
||||||
|
|
||||||
|
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>();
|
||||||
|
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const query = useMemo<IQuery>(() => ({
|
||||||
|
SortBy: 'SortName,ProductionYear',
|
||||||
|
SortOrder: 'Ascending',
|
||||||
|
IncludeItemTypes: 'Movie',
|
||||||
|
Recursive: true,
|
||||||
|
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||||
|
Limit: userSettings.libraryPageSize(undefined),
|
||||||
|
StartIndex: 0,
|
||||||
|
ParentId: topParentId }), [topParentId]);
|
||||||
|
|
||||||
|
userSettings.loadQuerySettings(savedQueryKey, query);
|
||||||
|
|
||||||
|
const getCurrentViewStyle = useCallback(() => {
|
||||||
|
return userSettings.get(savedViewKey, false) || 'Poster';
|
||||||
|
}, [savedViewKey]);
|
||||||
|
|
||||||
|
const reloadItems = useCallback(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => {
|
||||||
|
setItemsResult(result);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
|
||||||
|
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
|
autoFocuser.autoFocus(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
const onViewStyleChange = useCallback(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewStyle = getCurrentViewStyle();
|
||||||
|
const itemsContainer = page.querySelector('.itemsContainer') as HTMLDivElement;
|
||||||
|
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 = '';
|
||||||
|
}, [getCurrentViewStyle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onViewStyleChange();
|
||||||
|
}, [onViewStyleChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadItems();
|
||||||
|
}, [onViewStyleChange, query, 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} query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
<Shuffle itemsResult= {itemsResult} topParentId={topParentId} />
|
||||||
|
|
||||||
|
<SelectView getCurrentViewStyle={getCurrentViewStyle} savedViewKey={savedViewKey} query={query} onViewStyleChange={onViewStyleChange} reloadItems={reloadItems} />
|
||||||
|
<Sort SortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
||||||
|
<Filter query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlphaPickerContainer query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
<ItemsContainer getCurrentViewStyle={getCurrentViewStyle} query={query} items={itemsResult?.Items} noItemsMessage= 'MessageNoMoviesAvailable' />
|
||||||
|
|
||||||
|
<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} query={query} reloadItems={reloadItems} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MoviesView;
|
9
src/view/movies/ResumableItems.tsx
Normal file
9
src/view/movies/ResumableItems.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function ResumableItems() {
|
||||||
|
return (
|
||||||
|
<div>ResumableItems</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResumableItems;
|
294
src/view/movies/SuggestionsView.tsx
Normal file
294
src/view/movies/SuggestionsView.tsx
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
import escapeHtml from 'escape-html';
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||||
|
import imageLoader from '../../components/images/imageLoader';
|
||||||
|
import layoutManager from '../../components/layoutManager';
|
||||||
|
import loading from '../../components/loading/loading';
|
||||||
|
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
||||||
|
import dom from '../../scripts/dom';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
topParentId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SuggestionsView: FunctionComponent<IProps> = (props: IProps) => {
|
||||||
|
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 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 => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}, [autoFocus, enableScrollX, getPortraitShape]);
|
||||||
|
|
||||||
|
const loadResume = useCallback((page, userId, parentId) => {
|
||||||
|
loading.show();
|
||||||
|
const screenWidth: any = dom.getWindowSize();
|
||||||
|
const options = {
|
||||||
|
SortBy: 'DatePlayed',
|
||||||
|
SortOrder: 'Descending',
|
||||||
|
IncludeItemTypes: 'Movie',
|
||||||
|
Filters: 'IsResumable',
|
||||||
|
Limit: screenWidth.innerWidth >= 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 => {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
loading.hide();
|
||||||
|
// FIXME: Wait for all sections to load
|
||||||
|
autoFocus(page);
|
||||||
|
});
|
||||||
|
}, [autoFocus, enableScrollX, getThumbShape]);
|
||||||
|
|
||||||
|
const getRecommendationHtml = useCallback((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;
|
||||||
|
}, [enableScrollX, getPortraitShape]);
|
||||||
|
|
||||||
|
const loadSuggestions = useCallback((page, userId) => {
|
||||||
|
const screenWidth: any = dom.getWindowSize();
|
||||||
|
let itemLimit = 5;
|
||||||
|
if (screenWidth.innerWidth >= 1600) {
|
||||||
|
itemLimit = 8;
|
||||||
|
} else if (screenWidth.innerWidth >= 1200) {
|
||||||
|
itemLimit = 6;
|
||||||
|
}
|
||||||
|
const url = window.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(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);
|
||||||
|
});
|
||||||
|
}, [autoFocus, getRecommendationHtml]);
|
||||||
|
|
||||||
|
const loadSuggestionsTab = useCallback((view) => {
|
||||||
|
const parentId = props.topParentId;
|
||||||
|
const userId = window.ApiClient.getCurrentUserId();
|
||||||
|
loadResume(view, userId, parentId);
|
||||||
|
loadLatest(view, userId, parentId);
|
||||||
|
loadSuggestions(view, userId);
|
||||||
|
}, [loadLatest, loadResume, loadSuggestions, props.topParentId]);
|
||||||
|
|
||||||
|
const initSuggestedTab = useCallback((tabContent) => {
|
||||||
|
function setScrollClasses(elem: { classList: { add: (arg0: string) => void; remove: (arg0: string) => void; }; }, scrollX: boolean) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const containers = tabContent.querySelectorAll('.itemsContainer');
|
||||||
|
|
||||||
|
for (const container of containers) {
|
||||||
|
setScrollClasses(container, enableScrollX());
|
||||||
|
}
|
||||||
|
}, [enableScrollX]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initSuggestedTab(page);
|
||||||
|
}, [initSuggestedTab]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadSuggestionsTab(page);
|
||||||
|
}, [loadSuggestionsTab]);
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<div id='resumableSection' className='verticalSection hide'>
|
||||||
|
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
||||||
|
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
||||||
|
{globalize.translate('HeaderContinueWatching')}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemsContainerElement
|
||||||
|
id='resumableItems'
|
||||||
|
className='itemsContainer padded-left padded-right'
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
||||||
|
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
||||||
|
{globalize.translate('HeaderLatestMovies')}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemsContainerElement
|
||||||
|
id='recentlyAddedItems'
|
||||||
|
className='itemsContainer padded-left padded-right'
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='recommendations'>
|
||||||
|
</div>
|
||||||
|
<div className='noItemsMessage hide padded-left padded-right'>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{globalize.translate('MessageNoMovieSuggestionsAvailable')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SuggestionsView;
|
132
src/view/movies/TrailersView.tsx
Normal file
132
src/view/movies/TrailersView.tsx
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
|
import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||||
|
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import loading from '../../components/loading/loading';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import * as userSettings from '../../scripts/settings/userSettings';
|
||||||
|
import AlphaPickerContainer from '../components/AlphaPickerContainer';
|
||||||
|
import Filter from '../components/Filter';
|
||||||
|
import ItemsContainer from '../components/ItemsContainer';
|
||||||
|
import Pagination from '../components/Pagination';
|
||||||
|
import Sort from '../components/Sort';
|
||||||
|
import { IQuery } from '../components/type';
|
||||||
|
|
||||||
|
const SortMenuOptions = () => {
|
||||||
|
return [{
|
||||||
|
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'
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
topParentId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrailersView: FunctionComponent<IProps> = ({ topParentId }: IProps) => {
|
||||||
|
const savedQueryKey = topParentId + '-trailers';
|
||||||
|
const savedViewKey = savedQueryKey + '-view';
|
||||||
|
|
||||||
|
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>();
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const query = useMemo<IQuery>(() => ({
|
||||||
|
SortBy: 'SortName',
|
||||||
|
SortOrder: 'Ascending',
|
||||||
|
IncludeItemTypes: 'Trailer',
|
||||||
|
Recursive: true,
|
||||||
|
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo',
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||||
|
Limit: userSettings.libraryPageSize(undefined),
|
||||||
|
StartIndex: 0,
|
||||||
|
ParentId: topParentId }), [topParentId]);
|
||||||
|
|
||||||
|
userSettings.loadQuerySettings(savedQueryKey, query);
|
||||||
|
|
||||||
|
const getCurrentViewStyle = useCallback(() => {
|
||||||
|
return userSettings.get(savedViewKey, false) || 'Poster';
|
||||||
|
}, [savedViewKey]);
|
||||||
|
|
||||||
|
const reloadItems = useCallback(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => {
|
||||||
|
setItemsResult(result);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
});
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
const onViewStyleChange = useCallback(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const viewStyle = getCurrentViewStyle();
|
||||||
|
const itemsContainer = page.querySelector('.itemsContainer') as HTMLDivElement;
|
||||||
|
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 = '';
|
||||||
|
}, [getCurrentViewStyle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onViewStyleChange();
|
||||||
|
reloadItems();
|
||||||
|
}, [onViewStyleChange, query, 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} query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
<Sort SortMenuOptions={SortMenuOptions} query={query} savedQueryKey={savedQueryKey} reloadItems={reloadItems} />
|
||||||
|
<Filter query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlphaPickerContainer query={query} reloadItems={reloadItems} />
|
||||||
|
|
||||||
|
<ItemsContainer getCurrentViewStyle={getCurrentViewStyle} query={query} items={itemsResult?.Items} noItemsMessage= 'MessageNoTrailersFound' />
|
||||||
|
|
||||||
|
<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} query={query} reloadItems={reloadItems} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrailersView;
|
Loading…
Add table
Add a link
Reference in a new issue