From 479c53eb8b16a4dd92055d89260a4ed618155fbc Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sat, 6 Aug 2022 01:36:13 +0300 Subject: [PATCH 01/23] Migrate Movies --- src/elements/ItemsContainerElement.tsx | 28 ++ src/routes/index.tsx | 2 + src/routes/movies.tsx | 139 +++++++++ src/scripts/routes.js | 7 - src/view/components/AlphaPickerContainer.tsx | 48 +++ src/view/components/Filter.tsx | 51 ++++ src/view/components/GenresItemsContainer.tsx | 165 +++++++++++ src/view/components/ItemsContainer.tsx | 110 +++++++ src/view/components/NewCollection.tsx | 35 +++ src/view/components/Pagination.tsx | 65 ++++ src/view/components/SelectView.tsx | 45 +++ src/view/components/Shuffle.tsx | 44 +++ src/view/components/Sort.tsx | 48 +++ src/view/components/type.ts | 15 + src/view/movies/CollectionsView.tsx | 122 ++++++++ src/view/movies/FavoritesView.tsx | 147 ++++++++++ src/view/movies/GenresView.tsx | 61 ++++ src/view/movies/MoviesView.tsx | 153 ++++++++++ src/view/movies/ResumableItems.tsx | 9 + src/view/movies/SuggestionsView.tsx | 294 +++++++++++++++++++ src/view/movies/TrailersView.tsx | 132 +++++++++ 21 files changed, 1713 insertions(+), 7 deletions(-) create mode 100644 src/elements/ItemsContainerElement.tsx create mode 100644 src/routes/movies.tsx create mode 100644 src/view/components/AlphaPickerContainer.tsx create mode 100644 src/view/components/Filter.tsx create mode 100644 src/view/components/GenresItemsContainer.tsx create mode 100644 src/view/components/ItemsContainer.tsx create mode 100644 src/view/components/NewCollection.tsx create mode 100644 src/view/components/Pagination.tsx create mode 100644 src/view/components/SelectView.tsx create mode 100644 src/view/components/Shuffle.tsx create mode 100644 src/view/components/Sort.tsx create mode 100644 src/view/components/type.ts create mode 100644 src/view/movies/CollectionsView.tsx create mode 100644 src/view/movies/FavoritesView.tsx create mode 100644 src/view/movies/GenresView.tsx create mode 100644 src/view/movies/MoviesView.tsx create mode 100644 src/view/movies/ResumableItems.tsx create mode 100644 src/view/movies/SuggestionsView.tsx create mode 100644 src/view/movies/TrailersView.tsx diff --git a/src/elements/ItemsContainerElement.tsx b/src/elements/ItemsContainerElement.tsx new file mode 100644 index 000000000..14f519590 --- /dev/null +++ b/src/elements/ItemsContainerElement.tsx @@ -0,0 +1,28 @@ +import React, { FunctionComponent } from 'react'; + +const createButtonElement = ({ id, className }: IProps) => ({ + __html: `
+
` +}); + +type IProps = { + id?: string; + className?: string; +} + +const ItemsContainerElement: FunctionComponent = ({ id, className }: IProps) => { + return ( +
+ ); +}; + +export default ItemsContainerElement; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index d37fa9a7a..d18ce561e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -11,6 +11,7 @@ import UserPassword from './user/userpassword'; import UserProfile from './user/userprofile'; import UserProfiles from './user/userprofiles'; import Home from './home'; +import Movies from './movies'; const AppRoutes = () => ( @@ -20,6 +21,7 @@ const AppRoutes = () => ( } /> } /> } /> + } /> {/* Admin routes */} diff --git a/src/routes/movies.tsx b/src/routes/movies.tsx new file mode 100644 index 000000000..bf9686c0d --- /dev/null +++ b/src/routes/movies.tsx @@ -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(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 = ; + break; + + case 1: + component = ; + break; + + case 2: + component = ; + break; + + case 3: + component = ; + break; + + case 4: + component = ; + break; + + case 5: + component = ; + 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 ( +
+ + {getTabComponent(selectedIndex)} + + +
+ ); +}; + +export default Movies; diff --git a/src/scripts/routes.js b/src/scripts/routes.js index 0e8251376..a92ef28e1 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -345,13 +345,6 @@ import { appRouter } from '../components/appRouter'; controller: 'livetvtuner' }); - defineRoute({ - alias: '/movies.html', - path: 'movies/movies.html', - autoFocus: false, - controller: 'movies/moviesrecommended' - }); - defineRoute({ alias: '/music.html', path: 'music/music.html', diff --git a/src/view/components/AlphaPickerContainer.tsx b/src/view/components/AlphaPickerContainer.tsx new file mode 100644 index 000000000..dd4cc36f2 --- /dev/null +++ b/src/view/components/AlphaPickerContainer.tsx @@ -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 = ({ query, reloadItems }: AlphaPickerProps) => { + const [ alphaPicker, setAlphaPicker ] = useState(); + const element = useRef(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 ( +
+
+
+ ); +}; + +export default AlphaPickerContainer; diff --git a/src/view/components/Filter.tsx b/src/view/components/Filter.tsx new file mode 100644 index 000000000..d0fc0274f --- /dev/null +++ b/src/view/components/Filter.tsx @@ -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 = ({ query, reloadItems }: FilterProps) => { + const element = useRef(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 ( +
+ +
+ ); +}; + +export default Filter; diff --git a/src/view/components/GenresItemsContainer.tsx b/src/view/components/GenresItemsContainer.tsx new file mode 100644 index 000000000..65ca30448 --- /dev/null +++ b/src/view/components/GenresItemsContainer.tsx @@ -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 = ({ topParentId, getCurrentViewStyle, query, itemsResult = {} }: GenresItemsContainerProps) => { + const element = useRef(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 += '
'; + html += ''; + if (enableScrollX()) { + let scrollXClass = 'scrollX hiddenScrollX'; + + if (layoutManager.tv) { + scrollXClass += 'smoothScrollX padded-top-focusscale padded-bottom-focusscale'; + } + + html += '
'; + } else { + html += '
'; + } + + html += '
'; + html += '
'; + } + + if (!itemsResult.Items?.length) { + html = ''; + + html += '
'; + html += '

' + globalize.translate('MessageNothingHere') + '

'; + html += '

' + globalize.translate('MessageNoGenresAvailable') + '

'; + html += '
'; + } + + elem.innerHTML = html; + lazyLoader.lazyChildren(elem, fillItemsContainer); + }, [getCurrentViewStyle, query.SortBy, itemsResult.Items, fillItemsContainer, topParentId, enableScrollX]); + + return ( +
+
+
+ ); +}; + +export default GenresItemsContainer; diff --git a/src/view/components/ItemsContainer.tsx b/src/view/components/ItemsContainer.tsx new file mode 100644 index 000000000..ef3ef01fd --- /dev/null +++ b/src/view/components/ItemsContainer.tsx @@ -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 = ({ getCurrentViewStyle, query, items = [], noItemsMessage }: ItemsContainerProps) => { + const element = useRef(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 += '
'; + html += '

' + globalize.translate('MessageNothingHere') + '

'; + html += '

' + globalize.translate(noItemsMessage) + '

'; + html += '
'; + } + + const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement; + itemsContainer.innerHTML = html; + imageLoader.lazyChildren(itemsContainer); + }, [getCurrentViewStyle, query.SortBy, items, noItemsMessage]); + + return ( +
+ +
+ ); +}; + +export default ItemsContainer; diff --git a/src/view/components/NewCollection.tsx b/src/view/components/NewCollection.tsx new file mode 100644 index 000000000..a68fa2c85 --- /dev/null +++ b/src/view/components/NewCollection.tsx @@ -0,0 +1,35 @@ +import React, { FunctionComponent, useEffect, useRef } from 'react'; + +import IconButtonElement from '../../elements/IconButtonElement'; + +const NewCollection: FunctionComponent = () => { + const element = useRef(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 ( +
+ +
+ ); +}; + +export default NewCollection; diff --git a/src/view/components/Pagination.tsx b/src/view/components/Pagination.tsx new file mode 100644 index 000000000..e315e53f3 --- /dev/null +++ b/src/view/components/Pagination.tsx @@ -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 = ({ query, itemsResult = {}, reloadItems }: PaginationProps) => { + const element = useRef(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 ( +
+
+
+ + ); +}; + +export default Pagination; diff --git a/src/view/components/SelectView.tsx b/src/view/components/SelectView.tsx new file mode 100644 index 000000000..8c74f13ab --- /dev/null +++ b/src/view/components/SelectView.tsx @@ -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 = ({ getCurrentViewStyle, savedViewKey, query, onViewStyleChange, reloadItems }: SelectViewProps) => { + const element = useRef(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 ( +
+ +
+ ); +}; + +export default SelectView; diff --git a/src/view/components/Shuffle.tsx b/src/view/components/Shuffle.tsx new file mode 100644 index 000000000..fe8d92023 --- /dev/null +++ b/src/view/components/Shuffle.tsx @@ -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 = ({ itemsResult = {}, topParentId }: ShuffleProps) => { + const element = useRef(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 ( +
+ +
+ ); +}; + +export default Shuffle; diff --git a/src/view/components/Sort.tsx b/src/view/components/Sort.tsx new file mode 100644 index 000000000..5b9a2e58c --- /dev/null +++ b/src/view/components/Sort.tsx @@ -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 = ({ SortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => { + const element = useRef(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 ( +
+ +
+ ); +}; + +export default Sort; diff --git a/src/view/components/type.ts b/src/view/components/type.ts new file mode 100644 index 000000000..2dde6bb24 --- /dev/null +++ b/src/view/components/type.ts @@ -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; +} diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx new file mode 100644 index 000000000..46b030ac1 --- /dev/null +++ b/src/view/movies/CollectionsView.tsx @@ -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 = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-moviecollections'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState({}); + const element = useRef(null); + + const query = useMemo(() => ({ + 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 ( +
+
+ + + + + + +
+ + + +
+ +
+
+ ); +}; + +export default CollectionsView; diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx new file mode 100644 index 000000000..b94767b45 --- /dev/null +++ b/src/view/movies/FavoritesView.tsx @@ -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 = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-favorites'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState({}); + const element = useRef(null); + + const query = useMemo(() => ({ + 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 ( +
+
+ + + + + + +
+ + + + + +
+ +
+
+ ); +}; + +export default FavoritesView; diff --git a/src/view/movies/GenresView.tsx b/src/view/movies/GenresView.tsx new file mode 100644 index 000000000..b37d5fbec --- /dev/null +++ b/src/view/movies/GenresView.tsx @@ -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 = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-moviegenres'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState({}); + const element = useRef(null); + + const query = useMemo(() => ({ + 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 ( +
+ +
+ ); +}; + +export default GenresView; diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx new file mode 100644 index 000000000..74e72da16 --- /dev/null +++ b/src/view/movies/MoviesView.tsx @@ -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 = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-movies'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState(); + + const element = useRef(null); + + const query = useMemo(() => ({ + 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 ( +
+
+ + + + + + + + +
+ + + + + +
+ +
+
+ ); +}; + +export default MoviesView; diff --git a/src/view/movies/ResumableItems.tsx b/src/view/movies/ResumableItems.tsx new file mode 100644 index 000000000..692c0fdd5 --- /dev/null +++ b/src/view/movies/ResumableItems.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +function ResumableItems() { + return ( +
ResumableItems
+ ); +} + +export default ResumableItems; diff --git a/src/view/movies/SuggestionsView.tsx b/src/view/movies/SuggestionsView.tsx new file mode 100644 index 000000000..1de301f2a --- /dev/null +++ b/src/view/movies/SuggestionsView.tsx @@ -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 = (props: IProps) => { + const element = useRef(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 += '
'; + html += `

${escapeHtml(title)}

`; + const allowBottomPadding = true; + + if (enableScrollX()) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + html += cardBuilder.getCardsHtml(recommendation.Items, { + shape: getPortraitShape(), + scalable: true, + overlayPlayButton: true, + allowBottomPadding: allowBottomPadding, + showTitle: true, + showYear: true, + centerText: true + }); + + if (enableScrollX()) { + html += '
'; + } + html += '
'; + html += '
'; + 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 ( +
+
+
+

+ {globalize.translate('HeaderContinueWatching')} +

+
+ + + +
+ +
+
+

+ {globalize.translate('HeaderLatestMovies')} +

+
+ + + +
+ +
+
+
+
+

+ {globalize.translate('MessageNoMovieSuggestionsAvailable')} +

+
+
+ ); +}; + +export default SuggestionsView; diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx new file mode 100644 index 000000000..3eb6909b7 --- /dev/null +++ b/src/view/movies/TrailersView.tsx @@ -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 = ({ topParentId }: IProps) => { + const savedQueryKey = topParentId + '-trailers'; + const savedViewKey = savedQueryKey + '-view'; + + const [ itemsResult, setItemsResult ] = useState(); + const element = useRef(null); + + const query = useMemo(() => ({ + 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 ( +
+
+ + + + + +
+ + + + + +
+ +
+
+ ); +}; + +export default TrailersView; From 9741470c8c4375c1e0d87aaba62844728e051163 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sat, 6 Aug 2022 01:42:36 +0300 Subject: [PATCH 02/23] =?UTF-8?q?=EF=BB=BFremove=20unused=20movies=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/movies/moviecollections.js | 267 ------------ src/controllers/movies/moviegenres.js | 224 ---------- src/controllers/movies/movies.html | 92 ----- src/controllers/movies/movies.js | 327 --------------- src/controllers/movies/moviesrecommended.js | 428 -------------------- src/controllers/movies/movietrailers.js | 279 ------------- 6 files changed, 1617 deletions(-) delete mode 100644 src/controllers/movies/moviecollections.js delete mode 100644 src/controllers/movies/moviegenres.js delete mode 100644 src/controllers/movies/movies.html delete mode 100644 src/controllers/movies/movies.js delete mode 100644 src/controllers/movies/moviesrecommended.js delete mode 100644 src/controllers/movies/movietrailers.js diff --git a/src/controllers/movies/moviecollections.js b/src/controllers/movies/moviecollections.js deleted file mode 100644 index d1b909511..000000000 --- a/src/controllers/movies/moviecollections.js +++ /dev/null @@ -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 += '
'; - html += '

' + globalize.translate('MessageNothingHere') + '

'; - html += '

' + globalize.translate('MessageNoCollectionsAvailable') + '

'; - html += '
'; - } - - 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 */ diff --git a/src/controllers/movies/moviegenres.js b/src/controllers/movies/moviegenres.js deleted file mode 100644 index 62320fa3a..000000000 --- a/src/controllers/movies/moviegenres.js +++ /dev/null @@ -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 += '
'; - html += ''; - if (enableScrollX()) { - let scrollXClass = 'scrollX hiddenScrollX'; - - if (layoutManager.tv) { - scrollXClass += 'smoothScrollX padded-top-focusscale padded-bottom-focusscale'; - } - - html += '
'; - } else { - html += '
'; - } - - html += '
'; - html += '
'; - } - - if (!result.Items.length) { - html = ''; - - html += '
'; - html += '

' + globalize.translate('MessageNothingHere') + '

'; - html += '

' + globalize.translate('MessageNoGenresAvailable') + '

'; - html += '
'; - } - - 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 */ diff --git a/src/controllers/movies/movies.html b/src/controllers/movies/movies.html deleted file mode 100644 index 7a08694b2..000000000 --- a/src/controllers/movies/movies.html +++ /dev/null @@ -1,92 +0,0 @@ -
- -
-
-
- - - - -
- -
-
- -
-
-
-
-
-
-
-
-
-

${HeaderContinueWatching}

-
- -
-
-
- -
-
-

${HeaderLatestMovies}

-
- -
-
-
- -
-
-
-
-

${MessageNoMovieSuggestionsAvailable}

-
-
-
-
-
- - -
- -
-
- -
-
-
-
-
-
-
-
-
- -
- -
-
-
-
-
-
-
-
-
- - - -
- -
-
-
-
-
-
-
-
-
-
diff --git a/src/controllers/movies/movies.js b/src/controllers/movies/movies.js deleted file mode 100644 index 89e086ce7..000000000 --- a/src/controllers/movies/movies.js +++ /dev/null @@ -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 */ diff --git a/src/controllers/movies/moviesrecommended.js b/src/controllers/movies/moviesrecommended.js deleted file mode 100644 index b1b010331..000000000 --- a/src/controllers/movies/moviesrecommended.js +++ /dev/null @@ -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 += '
'; - html += '

' + escapeHtml(title) + '

'; - const allowBottomPadding = true; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - html += cardBuilder.getCardsHtml(recommendation.Items, { - shape: getPortraitShape(), - scalable: true, - overlayPlayButton: true, - allowBottomPadding: allowBottomPadding, - showTitle: true, - showYear: true, - centerText: true - }); - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - html += '
'; - 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 */ diff --git a/src/controllers/movies/movietrailers.js b/src/controllers/movies/movietrailers.js deleted file mode 100644 index 2e55e6eea..000000000 --- a/src/controllers/movies/movietrailers.js +++ /dev/null @@ -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 += '
'; - html += '

' + globalize.translate('MessageNothingHere') + '

'; - html += '

' + globalize.translate('MessageNoTrailersFound') + '

'; - html += '
'; - } - - 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 */ From 87ebe89196a87d8275b672abb4d4fc88e28f7f32 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sat, 6 Aug 2022 02:38:27 +0300 Subject: [PATCH 03/23] add MessageNoFavoritesAvailable string --- src/components/maintabsmanager.js | 26 +++++++++++--------- src/strings/en-us.json | 1 + src/view/components/GenresItemsContainer.tsx | 19 +++++++------- src/view/movies/MoviesView.tsx | 2 +- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/components/maintabsmanager.js b/src/components/maintabsmanager.js index 36aa17b49..5ce5b55c9 100644 --- a/src/components/maintabsmanager.js +++ b/src/components/maintabsmanager.js @@ -138,21 +138,23 @@ import '../elements/emby-button/emby-button'; configureSwipeTabs(view, tabsElem); - tabsElem.addEventListener('beforetabchange', function (e) { - const tabContainers = getTabContainersFn(); - if (e.detail.previousIndex != null) { - const previousPanel = tabContainers[e.detail.previousIndex]; - if (previousPanel) { - previousPanel.classList.remove('is-active'); + if (getTabContainersFn) { + tabsElem.addEventListener('beforetabchange', function (e) { + const tabContainers = getTabContainersFn(); + if (e.detail.previousIndex != null) { + const previousPanel = tabContainers[e.detail.previousIndex]; + if (previousPanel) { + previousPanel.classList.remove('is-active'); + } } - } - const newPanel = tabContainers[e.detail.selectedTabIndex]; + const newPanel = tabContainers[e.detail.selectedTabIndex]; - if (newPanel) { - newPanel.classList.add('is-active'); - } - }); + if (newPanel) { + newPanel.classList.add('is-active'); + } + }); + } if (onBeforeTabChange) { tabsElem.addEventListener('beforetabchange', onBeforeTabChange); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 4fa437b46..37e3c5bb4 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1065,6 +1065,7 @@ "MessageItemsAdded": "Items added.", "MessageItemSaved": "Item saved.", "MessageLeaveEmptyToInherit": "Leave empty to inherit settings from a parent item or the global default value.", + "MessageNoFavoritesAvailable": "No favorites are currently available.", "MessageNoAvailablePlugins": "No available plugins.", "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.", diff --git a/src/view/components/GenresItemsContainer.tsx b/src/view/components/GenresItemsContainer.tsx index 65ca30448..2a616f0a8 100644 --- a/src/view/components/GenresItemsContainer.tsx +++ b/src/view/components/GenresItemsContainer.tsx @@ -1,14 +1,15 @@ -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 { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import escapeHTML from 'escape-html'; +import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; + +import { appRouter } from '../../components/appRouter'; +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import layoutManager from '../../components/layoutManager'; +import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; +import globalize from '../../scripts/globalize'; import { IQuery } from './type'; type GenresItemsContainerProps = { diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx index 74e72da16..4a6d19be6 100644 --- a/src/view/movies/MoviesView.tsx +++ b/src/view/movies/MoviesView.tsx @@ -141,7 +141,7 @@ const MoviesView: FunctionComponent = ({ topParentId }: IProps) => { - +
From bdff9b14f14f0aa6b5e8cbfb479393ca5657800a Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sat, 6 Aug 2022 02:50:41 +0300 Subject: [PATCH 04/23] Rename class name for collectionEditor & add method show --- src/components/collectionEditor/collectionEditor.js | 6 +++--- src/components/itemContextMenu.js | 5 +++-- src/components/multiSelect/multiSelect.js | 5 +++-- src/view/components/NewCollection.tsx | 5 +++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/collectionEditor/collectionEditor.js b/src/components/collectionEditor/collectionEditor.js index 946ddfebb..8af78fede 100644 --- a/src/components/collectionEditor/collectionEditor.js +++ b/src/components/collectionEditor/collectionEditor.js @@ -206,8 +206,8 @@ import toast from '../toast/toast'; }); } - export class showEditor { - constructor(options) { + class CollectionEditor { + show(options) { const items = options.items || {}; currentServerId = options.serverId; @@ -266,4 +266,4 @@ import toast from '../toast/toast'; } /* eslint-enable indent */ -export default showEditor; +export default CollectionEditor; diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index 6974f609e..15980b23a 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -318,8 +318,9 @@ import toast from './toast/toast'; return new Promise(function (resolve, reject) { switch (id) { case 'addtocollection': - import('./collectionEditor/collectionEditor').then(({default: collectionEditor}) => { - new collectionEditor({ + import('./collectionEditor/collectionEditor').then(({default: CollectionEditor}) => { + const collectionEditor = new CollectionEditor(); + collectionEditor.show({ items: [itemId], serverId: serverId }).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id)); diff --git a/src/components/multiSelect/multiSelect.js b/src/components/multiSelect/multiSelect.js index 370d306d4..6632ef628 100644 --- a/src/components/multiSelect/multiSelect.js +++ b/src/components/multiSelect/multiSelect.js @@ -267,8 +267,9 @@ import datetime from '../../scripts/datetime'; } break; case 'addtocollection': - import('../collectionEditor/collectionEditor').then(({default: collectionEditor}) => { - new collectionEditor({ + import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => { + const collectionEditor = new CollectionEditor(); + collectionEditor.show({ items: items, serverId: serverId }); diff --git a/src/view/components/NewCollection.tsx b/src/view/components/NewCollection.tsx index a68fa2c85..1dbd625e6 100644 --- a/src/view/components/NewCollection.tsx +++ b/src/view/components/NewCollection.tsx @@ -9,9 +9,10 @@ const NewCollection: FunctionComponent = () => { const btnNewCollection = element.current?.querySelector('.btnNewCollection') as HTMLButtonElement; if (btnNewCollection) { btnNewCollection.addEventListener('click', () => { - import('../../components/collectionEditor/collectionEditor').then(({ default: collectionEditor }) => { + import('../../components/collectionEditor/collectionEditor').then(({default: CollectionEditor}) => { const serverId = window.ApiClient.serverId(); - new collectionEditor({ + const collectionEditor = new CollectionEditor(); + collectionEditor.show({ items: [], serverId: serverId }); From f4b878bea2b8105d37d44664702f9d56f6a25f82 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sat, 6 Aug 2022 03:06:41 +0300 Subject: [PATCH 05/23] remove query form GenresItemsContainer --- src/view/components/GenresItemsContainer.tsx | 6 ++---- src/view/movies/GenresView.tsx | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/view/components/GenresItemsContainer.tsx b/src/view/components/GenresItemsContainer.tsx index 2a616f0a8..c6c608aab 100644 --- a/src/view/components/GenresItemsContainer.tsx +++ b/src/view/components/GenresItemsContainer.tsx @@ -10,16 +10,14 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder'; import layoutManager from '../../components/layoutManager'; import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; import globalize from '../../scripts/globalize'; -import { IQuery } from './type'; type GenresItemsContainerProps = { topParentId?: string | null; getCurrentViewStyle: () => string; - query: IQuery; itemsResult?: BaseItemDtoQueryResult; } -const GenresItemsContainer: FunctionComponent = ({ topParentId, getCurrentViewStyle, query, itemsResult = {} }: GenresItemsContainerProps) => { +const GenresItemsContainer: FunctionComponent = ({ topParentId, getCurrentViewStyle, itemsResult = {} }: GenresItemsContainerProps) => { const element = useRef(null); const enableScrollX = useCallback(() => { @@ -154,7 +152,7 @@ const GenresItemsContainer: FunctionComponent = ({ to elem.innerHTML = html; lazyLoader.lazyChildren(elem, fillItemsContainer); - }, [getCurrentViewStyle, query.SortBy, itemsResult.Items, fillItemsContainer, topParentId, enableScrollX]); + }, [getCurrentViewStyle, itemsResult.Items, fillItemsContainer, topParentId, enableScrollX]); return (
diff --git a/src/view/movies/GenresView.tsx b/src/view/movies/GenresView.tsx index b37d5fbec..d442def73 100644 --- a/src/view/movies/GenresView.tsx +++ b/src/view/movies/GenresView.tsx @@ -53,7 +53,7 @@ const GenresView: FunctionComponent = ({ topParentId }: IProps) => { }, [reloadItems]); return (
- +
); }; From 7543e494c941835c2329a5ff56045cdfbfc5bac4 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sun, 7 Aug 2022 02:33:25 +0300 Subject: [PATCH 06/23] splitting SuggestionsView component --- src/elements/ItemsContainerElement.tsx | 8 +- .../ItemsScrollerContainerElement.tsx | 43 ++++ .../RecentlyAddedItemsContainer.tsx | 60 +++++ .../components/RecommendationContainer.tsx | 80 +++++++ .../components/ResumableItemsContainer.tsx | 62 +++++ src/view/components/Sort.tsx | 8 +- src/view/movies/CollectionsView.tsx | 2 +- src/view/movies/FavoritesView.tsx | 2 +- src/view/movies/MoviesView.tsx | 2 +- src/view/movies/ResumableItems.tsx | 9 - src/view/movies/SuggestionsView.tsx | 220 +++++------------- src/view/movies/TrailersView.tsx | 2 +- 12 files changed, 311 insertions(+), 187 deletions(-) create mode 100644 src/elements/ItemsScrollerContainerElement.tsx create mode 100644 src/view/components/RecentlyAddedItemsContainer.tsx create mode 100644 src/view/components/RecommendationContainer.tsx create mode 100644 src/view/components/ResumableItemsContainer.tsx delete mode 100644 src/view/movies/ResumableItems.tsx diff --git a/src/elements/ItemsContainerElement.tsx b/src/elements/ItemsContainerElement.tsx index 14f519590..44ea3de2d 100644 --- a/src/elements/ItemsContainerElement.tsx +++ b/src/elements/ItemsContainerElement.tsx @@ -1,9 +1,9 @@ import React, { FunctionComponent } from 'react'; -const createButtonElement = ({ id, className }: IProps) => ({ +const createElement = ({ id, className }: IProps) => ({ __html: `
` @@ -17,8 +17,8 @@ type IProps = { const ItemsContainerElement: FunctionComponent = ({ id, className }: IProps) => { return (
diff --git a/src/elements/ItemsScrollerContainerElement.tsx b/src/elements/ItemsScrollerContainerElement.tsx new file mode 100644 index 000000000..438944c26 --- /dev/null +++ b/src/elements/ItemsScrollerContainerElement.tsx @@ -0,0 +1,43 @@ +import React, { FunctionComponent } from 'react'; + +const createScroller = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, id, className }: IProps) => ({ + __html: `
+
+
+
` +}); + +type IProps = { + scrollerclassName?: string; + dataHorizontal?: string; + dataMousewheel?: string; + dataCenterfocus?: string; + id?: string; + className?: string; +} + +const ItemsScrollerContainerElement: FunctionComponent = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, id, className }: IProps) => { + return ( +
+ ); +}; + +export default ItemsScrollerContainerElement; diff --git a/src/view/components/RecentlyAddedItemsContainer.tsx b/src/view/components/RecentlyAddedItemsContainer.tsx new file mode 100644 index 000000000..c252b917c --- /dev/null +++ b/src/view/components/RecentlyAddedItemsContainer.tsx @@ -0,0 +1,60 @@ +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useEffect, useRef } from 'react'; + +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import globalize from '../../scripts/globalize'; +import ItemsContainerElement from '../../elements/ItemsContainerElement'; + +type RecentlyAddedItemsContainerProps = { + getPortraitShape: () => string; + enableScrollX: () => boolean; + items?: BaseItemDto[]; +} + +const RecentlyAddedItemsContainer: FunctionComponent = ({ getPortraitShape, enableScrollX, items = [] }: RecentlyAddedItemsContainerProps) => { + const element = useRef(null); + + useEffect(() => { + const section = element.current?.querySelector('#recentlyAddedItemsSection') as HTMLDivElement; + if (items?.length) { + section.classList.remove('hide'); + } else { + section.classList.add('hide'); + } + + const allowBottomPadding = !enableScrollX(); + const container = element.current?.querySelector('#recentlyAddedItems'); + cardBuilder.buildCards(items, { + itemsContainer: container, + shape: getPortraitShape(), + scalable: true, + overlayPlayButton: true, + allowBottomPadding: allowBottomPadding, + showTitle: true, + showYear: true, + centerText: true + }); + }, [enableScrollX, getPortraitShape, items]); + + return ( +
+
+
+

+ {globalize.translate('HeaderLatestMovies')} +

+
+ + + +
+
+ ); +}; + +export default RecentlyAddedItemsContainer; diff --git a/src/view/components/RecommendationContainer.tsx b/src/view/components/RecommendationContainer.tsx new file mode 100644 index 000000000..2fbef7b20 --- /dev/null +++ b/src/view/components/RecommendationContainer.tsx @@ -0,0 +1,80 @@ +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import { RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useEffect, useRef } from 'react'; + +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import globalize from '../../scripts/globalize'; +import ItemsContainerElement from '../../elements/ItemsContainerElement'; +import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; +import escapeHTML from 'escape-html'; + +type RecommendationContainerProps = { + getPortraitShape: () => string; + enableScrollX: () => boolean; + recommendation?: RecommendationDto; +} + +const RecommendationContainer: FunctionComponent = ({ getPortraitShape, enableScrollX, recommendation = {} }: RecommendationContainerProps) => { + const element = useRef(null); + + 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; + } + + useEffect(() => { + cardBuilder.buildCards(recommendation.Items || [], { + itemsContainer: element.current?.querySelector('.itemsContainer'), + parentContainer: element.current, + shape: getPortraitShape(), + scalable: true, + overlayPlayButton: true, + allowBottomPadding: true, + showTitle: true, + showYear: true, + centerText: true + }); + }, [enableScrollX, getPortraitShape, recommendation]); + + return ( +
+
+
+

+ {escapeHTML(title)} +

+
+ + {enableScrollX() ? : } + +
+
+ ); +}; + +export default RecommendationContainer; diff --git a/src/view/components/ResumableItemsContainer.tsx b/src/view/components/ResumableItemsContainer.tsx new file mode 100644 index 000000000..cb11ea7d4 --- /dev/null +++ b/src/view/components/ResumableItemsContainer.tsx @@ -0,0 +1,62 @@ +import '../../elements/emby-itemscontainer/emby-itemscontainer'; + +import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useEffect, useRef } from 'react'; + +import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import globalize from '../../scripts/globalize'; +import ItemsContainerElement from '../../elements/ItemsContainerElement'; + +type ResumableItemsContainerProps = { + getThumbShape: () => string; + enableScrollX: () => boolean; + itemsResult?: BaseItemDtoQueryResult; +} + +const ResumableItemsContainer: FunctionComponent = ({ getThumbShape, enableScrollX, itemsResult = {} }: ResumableItemsContainerProps) => { + const element = useRef(null); + + useEffect(() => { + const section = element.current?.querySelector('#resumableSection') as HTMLDivElement; + if (itemsResult.Items?.length) { + section.classList.remove('hide'); + } else { + section.classList.add('hide'); + } + + const allowBottomPadding = !enableScrollX(); + const container = element.current?.querySelector('#resumableItems') as HTMLDivElement; + cardBuilder.buildCards(itemsResult.Items || [], { + itemsContainer: container, + preferThumb: true, + shape: getThumbShape(), + scalable: true, + overlayPlayButton: true, + allowBottomPadding: allowBottomPadding, + cardLayout: false, + showTitle: true, + showYear: true, + centerText: true + }); + }, [enableScrollX, getThumbShape, itemsResult.Items]); + + return ( +
+
+
+

+ {globalize.translate('HeaderContinueWatching')} +

+
+ + + +
+
+ ); +}; + +export default ResumableItemsContainer; diff --git a/src/view/components/Sort.tsx b/src/view/components/Sort.tsx index 5b9a2e58c..9cdb12777 100644 --- a/src/view/components/Sort.tsx +++ b/src/view/components/Sort.tsx @@ -5,13 +5,13 @@ import * as userSettings from '../../scripts/settings/userSettings'; import { IQuery } from './type'; type SortProps = { - SortMenuOptions: () => { name: string; id: string}[]; + sortMenuOptions: () => { name: string; id: string}[]; query: IQuery; savedQueryKey: string; reloadItems: () => void; } -const Sort: FunctionComponent = ({ SortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => { +const Sort: FunctionComponent = ({ sortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => { const element = useRef(null); useEffect(() => { @@ -20,7 +20,7 @@ const Sort: FunctionComponent = ({ SortMenuOptions, query, savedQuery if (btnSort) { btnSort.addEventListener('click', (e) => { libraryBrowser.showSortMenu({ - items: SortMenuOptions(), + items: sortMenuOptions(), callback: () => { query.StartIndex = 0; userSettings.saveQuerySettings(savedQueryKey, query); @@ -31,7 +31,7 @@ const Sort: FunctionComponent = ({ SortMenuOptions, query, savedQuery }); }); } - }, [SortMenuOptions, query, reloadItems, savedQueryKey]); + }, [sortMenuOptions, query, reloadItems, savedQueryKey]); return (
diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx index 46b030ac1..17ddbe4d1 100644 --- a/src/view/movies/CollectionsView.tsx +++ b/src/view/movies/CollectionsView.tsx @@ -105,7 +105,7 @@ const CollectionsView: FunctionComponent = ({ topParentId }: IProps) => - +
diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx index b94767b45..81388f2c1 100644 --- a/src/view/movies/FavoritesView.tsx +++ b/src/view/movies/FavoritesView.tsx @@ -128,7 +128,7 @@ const FavoritesView: FunctionComponent = ({ topParentId }: IProps) => { - +
diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx index 4a6d19be6..0db029c6e 100644 --- a/src/view/movies/MoviesView.tsx +++ b/src/view/movies/MoviesView.tsx @@ -134,7 +134,7 @@ const MoviesView: FunctionComponent = ({ topParentId }: IProps) => { - +
diff --git a/src/view/movies/ResumableItems.tsx b/src/view/movies/ResumableItems.tsx deleted file mode 100644 index 692c0fdd5..000000000 --- a/src/view/movies/ResumableItems.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -function ResumableItems() { - return ( -
ResumableItems
- ); -} - -export default ResumableItems; diff --git a/src/view/movies/SuggestionsView.tsx b/src/view/movies/SuggestionsView.tsx index 1de301f2a..10d965570 100644 --- a/src/view/movies/SuggestionsView.tsx +++ b/src/view/movies/SuggestionsView.tsx @@ -1,19 +1,22 @@ -import escapeHtml from 'escape-html'; -import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; +import { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import React, { FunctionComponent, useCallback, useEffect, useRef, useState } 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'; +import RecentlyAddedItemsContainer from '../components/RecentlyAddedItemsContainer'; +import RecommendationContainer from '../components/RecommendationContainer'; +import ResumableItemsContainer from '../components/ResumableItemsContainer'; type IProps = { topParentId: string | null; } const SuggestionsView: FunctionComponent = (props: IProps) => { + const [ latestItems, setLatestItems ] = useState([]); + const [ resumeItemsResult, setResumeItemsResult ] = useState(); + const [ recommendations, setRecommendations ] = useState([]); const element = useRef(null); const enableScrollX = useCallback(() => { @@ -45,23 +48,12 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { 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 - }); + setLatestItems(items); // FIXME: Wait for all sections to load autoFocus(page); }); - }, [autoFocus, enableScrollX, getPortraitShape]); + }, [autoFocus]); const loadResume = useCallback((page, userId, parentId) => { loading.show(); @@ -81,84 +73,13 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { 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'); - } + setResumeItemsResult(result); - 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 += '
'; - html += `

${escapeHtml(title)}

`; - const allowBottomPadding = true; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - html += cardBuilder.getCardsHtml(recommendation.Items, { - shape: getPortraitShape(), - scalable: true, - overlayPlayButton: true, - allowBottomPadding: allowBottomPadding, - showTitle: true, - showYear: true, - centerText: true - }); - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - html += '
'; - return html; - }, [enableScrollX, getPortraitShape]); + }, [autoFocus]); const loadSuggestions = useCallback((page, userId) => { const screenWidth: any = dom.getWindowSize(); @@ -177,22 +98,12 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { 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); + setRecommendations(recommendations); // FIXME: Wait for all sections to load autoFocus(page); }); - }, [autoFocus, getRecommendationHtml]); + }, [autoFocus]); const loadSuggestionsTab = useCallback((view) => { const parentId = props.topParentId; @@ -202,32 +113,40 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { 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'); + const setScrollClasses = useCallback((elem, scrollX) => { + const page = element.current; - 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'); - } + if (!page) { + console.error('Unexpected null reference'); + return; } - const containers = tabContent.querySelectorAll('.itemsContainer'); + + 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 initSuggestedTab = useCallback((view) => { + const containers = view.querySelectorAll('.itemsContainer'); for (const container of containers) { setScrollClasses(container, enableScrollX()); } - }, [enableScrollX]); + }, [enableScrollX, setScrollClasses]); useEffect(() => { const page = element.current; @@ -238,54 +157,23 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { } initSuggestedTab(page); - }, [initSuggestedTab]); - - useEffect(() => { - const page = element.current; - - if (!page) { - console.error('Unexpected null reference'); - return; - } loadSuggestionsTab(page); - }, [loadSuggestionsTab]); + }, [initSuggestedTab, loadSuggestionsTab]); + return (
-
-
-

- {globalize.translate('HeaderContinueWatching')} -

-
+ - + -
- -
-
-

- {globalize.translate('HeaderLatestMovies')} -

-
- - - -
- -
-
-
-
-

- {globalize.translate('MessageNoMovieSuggestionsAvailable')} -

+
+ {!recommendations.length ?
+

{globalize.translate('MessageNothingHere')}

+

{globalize.translate('MessageNoMovieSuggestionsAvailable')}

+
: recommendations.map((recommendation, index) => { + return ; + })} + {}
); diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx index 3eb6909b7..f856b142e 100644 --- a/src/view/movies/TrailersView.tsx +++ b/src/view/movies/TrailersView.tsx @@ -113,7 +113,7 @@ const TrailersView: FunctionComponent = ({ topParentId }: IProps) => {
- +
From 111cc430db08564a1d6e7139c956226aceef4e8d Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 10 Aug 2022 22:08:49 +0300 Subject: [PATCH 07/23] Refactoring duplicates onViewStyleChange --- src/view/components/GenresItemsContainer.tsx | 2 +- src/view/components/ItemsContainer.tsx | 9 ++++-- src/view/components/Pagination.tsx | 5 +--- src/view/components/SelectView.tsx | 6 ++-- src/view/movies/CollectionsView.tsx | 26 ++---------------- src/view/movies/FavoritesView.tsx | 26 ++---------------- src/view/movies/MoviesView.tsx | 29 ++------------------ src/view/movies/SuggestionsView.tsx | 4 +-- src/view/movies/TrailersView.tsx | 23 +--------------- 9 files changed, 19 insertions(+), 111 deletions(-) diff --git a/src/view/components/GenresItemsContainer.tsx b/src/view/components/GenresItemsContainer.tsx index c6c608aab..cb003ba05 100644 --- a/src/view/components/GenresItemsContainer.tsx +++ b/src/view/components/GenresItemsContainer.tsx @@ -156,7 +156,7 @@ const GenresItemsContainer: FunctionComponent = ({ to return (
-
+
); }; diff --git a/src/view/components/ItemsContainer.tsx b/src/view/components/ItemsContainer.tsx index ef3ef01fd..1d389eda4 100644 --- a/src/view/components/ItemsContainer.tsx +++ b/src/view/components/ItemsContainer.tsx @@ -18,10 +18,11 @@ type ItemsContainerProps = { const ItemsContainer: FunctionComponent = ({ getCurrentViewStyle, query, items = [], noItemsMessage }: ItemsContainerProps) => { const element = useRef(null); + const viewStyle = getCurrentViewStyle(); useEffect(() => { let html; - const viewStyle = getCurrentViewStyle(); + if (viewStyle == 'Thumb') { html = cardBuilder.getCardsHtml(items, { items: items, @@ -95,13 +96,15 @@ const ItemsContainer: FunctionComponent = ({ getCurrentView const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement; itemsContainer.innerHTML = html; imageLoader.lazyChildren(itemsContainer); - }, [getCurrentViewStyle, query.SortBy, items, noItemsMessage]); + }, [query.SortBy, items, noItemsMessage, viewStyle]); + + const cssClass = viewStyle == 'List' ? 'vertical-list' : 'vertical-wrap'; return (
); diff --git a/src/view/components/Pagination.tsx b/src/view/components/Pagination.tsx index e315e53f3..bdb1fecfd 100644 --- a/src/view/components/Pagination.tsx +++ b/src/view/components/Pagination.tsx @@ -54,11 +54,8 @@ const Pagination: FunctionComponent = ({ query, itemsResult = { return (
-
+
- ); }; diff --git a/src/view/components/SelectView.tsx b/src/view/components/SelectView.tsx index 8c74f13ab..eb857ca12 100644 --- a/src/view/components/SelectView.tsx +++ b/src/view/components/SelectView.tsx @@ -9,11 +9,10 @@ type SelectViewProps = { getCurrentViewStyle: () => string; query: IQuery; savedViewKey: string; - onViewStyleChange: () => void; reloadItems: () => void; } -const SelectView: FunctionComponent = ({ getCurrentViewStyle, savedViewKey, query, onViewStyleChange, reloadItems }: SelectViewProps) => { +const SelectView: FunctionComponent = ({ getCurrentViewStyle, savedViewKey, query, reloadItems }: SelectViewProps) => { const element = useRef(null); useEffect(() => { @@ -25,10 +24,9 @@ const SelectView: FunctionComponent = ({ getCurrentViewStyle, s const viewStyle = (e as CustomEvent).detail.viewStyle; userSettings.set(savedViewKey, viewStyle, false); query.StartIndex = 0; - onViewStyleChange(); reloadItems(); }); - }, [getCurrentViewStyle, onViewStyleChange, query, reloadItems, savedViewKey]); + }, [getCurrentViewStyle, query, reloadItems, savedViewKey]); return (
diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx index 17ddbe4d1..612df7dee 100644 --- a/src/view/movies/CollectionsView.tsx +++ b/src/view/movies/CollectionsView.tsx @@ -73,38 +73,16 @@ const CollectionsView: FunctionComponent = ({ topParentId }: IProps) => }); }, [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]); + }, [reloadItems]); return (
- + diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx index 81388f2c1..713b02376 100644 --- a/src/view/movies/FavoritesView.tsx +++ b/src/view/movies/FavoritesView.tsx @@ -96,38 +96,16 @@ const FavoritesView: FunctionComponent = ({ topParentId }: IProps) => { }); }, [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]); + }, [query, reloadItems]); return (
- + diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx index 0db029c6e..e0fee74a5 100644 --- a/src/view/movies/MoviesView.tsx +++ b/src/view/movies/MoviesView.tsx @@ -97,34 +97,9 @@ const MoviesView: FunctionComponent = ({ topParentId }: IProps) => { }); }, [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]); + }, [query, reloadItems]); return (
@@ -133,7 +108,7 @@ const MoviesView: FunctionComponent = ({ topParentId }: IProps) => { - + diff --git a/src/view/movies/SuggestionsView.tsx b/src/view/movies/SuggestionsView.tsx index 10d965570..7cecd5616 100644 --- a/src/view/movies/SuggestionsView.tsx +++ b/src/view/movies/SuggestionsView.tsx @@ -97,8 +97,8 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { ImageTypeLimit: 1, EnableImageTypes: 'Primary,Backdrop,Banner,Thumb' }); - window.ApiClient.getJSON(url).then(recommendations => { - setRecommendations(recommendations); + window.ApiClient.getJSON(url).then(result => { + setRecommendations(result); // FIXME: Wait for all sections to load autoFocus(page); diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx index f856b142e..e776c0117 100644 --- a/src/view/movies/TrailersView.tsx +++ b/src/view/movies/TrailersView.tsx @@ -83,30 +83,9 @@ const TrailersView: FunctionComponent = ({ topParentId }: IProps) => { }); }, [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]); + }, [query, reloadItems]); return (
From 368a6064c28c6ca43a3efea087d02149cc273179 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 10 Aug 2022 22:19:26 +0300 Subject: [PATCH 08/23] Add ItemsScrollerContainerElement for ResumableItemsContainer & RecentlyAddedItemsContainer --- src/view/components/ItemsContainer.tsx | 1 - .../RecentlyAddedItemsContainer.tsx | 27 ++++++------- .../components/RecommendationContainer.tsx | 1 - .../components/ResumableItemsContainer.tsx | 24 ++++++------ src/view/movies/SuggestionsView.tsx | 39 +------------------ 5 files changed, 24 insertions(+), 68 deletions(-) diff --git a/src/view/components/ItemsContainer.tsx b/src/view/components/ItemsContainer.tsx index 1d389eda4..8cc2992cc 100644 --- a/src/view/components/ItemsContainer.tsx +++ b/src/view/components/ItemsContainer.tsx @@ -103,7 +103,6 @@ const ItemsContainer: FunctionComponent = ({ getCurrentView return (
diff --git a/src/view/components/RecentlyAddedItemsContainer.tsx b/src/view/components/RecentlyAddedItemsContainer.tsx index c252b917c..60c6571bd 100644 --- a/src/view/components/RecentlyAddedItemsContainer.tsx +++ b/src/view/components/RecentlyAddedItemsContainer.tsx @@ -6,6 +6,7 @@ import React, { FunctionComponent, useEffect, useRef } from 'react'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; import globalize from '../../scripts/globalize'; import ItemsContainerElement from '../../elements/ItemsContainerElement'; +import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; type RecentlyAddedItemsContainerProps = { getPortraitShape: () => string; @@ -17,21 +18,13 @@ const RecentlyAddedItemsContainer: FunctionComponent(null); useEffect(() => { - const section = element.current?.querySelector('#recentlyAddedItemsSection') as HTMLDivElement; - if (items?.length) { - section.classList.remove('hide'); - } else { - section.classList.add('hide'); - } - - const allowBottomPadding = !enableScrollX(); - const container = element.current?.querySelector('#recentlyAddedItems'); cardBuilder.buildCards(items, { - itemsContainer: container, + itemsContainer: element.current?.querySelector('.itemsContainer'), + parentContainer: element.current?.querySelector('#recentlyAddedItemsSection'), shape: getPortraitShape(), scalable: true, overlayPlayButton: true, - allowBottomPadding: allowBottomPadding, + allowBottomPadding: true, showTitle: true, showYear: true, centerText: true @@ -47,10 +40,14 @@ const RecentlyAddedItemsContainer: FunctionComponent
- + {enableScrollX() ? : }
diff --git a/src/view/components/RecommendationContainer.tsx b/src/view/components/RecommendationContainer.tsx index 2fbef7b20..7501585a5 100644 --- a/src/view/components/RecommendationContainer.tsx +++ b/src/view/components/RecommendationContainer.tsx @@ -43,7 +43,6 @@ const RecommendationContainer: FunctionComponent = useEffect(() => { cardBuilder.buildCards(recommendation.Items || [], { itemsContainer: element.current?.querySelector('.itemsContainer'), - parentContainer: element.current, shape: getPortraitShape(), scalable: true, overlayPlayButton: true, diff --git a/src/view/components/ResumableItemsContainer.tsx b/src/view/components/ResumableItemsContainer.tsx index cb11ea7d4..b26b8613f 100644 --- a/src/view/components/ResumableItemsContainer.tsx +++ b/src/view/components/ResumableItemsContainer.tsx @@ -6,6 +6,7 @@ import React, { FunctionComponent, useEffect, useRef } from 'react'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; import globalize from '../../scripts/globalize'; import ItemsContainerElement from '../../elements/ItemsContainerElement'; +import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; type ResumableItemsContainerProps = { getThumbShape: () => string; @@ -17,17 +18,10 @@ const ResumableItemsContainer: FunctionComponent = const element = useRef(null); useEffect(() => { - const section = element.current?.querySelector('#resumableSection') as HTMLDivElement; - if (itemsResult.Items?.length) { - section.classList.remove('hide'); - } else { - section.classList.add('hide'); - } - const allowBottomPadding = !enableScrollX(); - const container = element.current?.querySelector('#resumableItems') as HTMLDivElement; cardBuilder.buildCards(itemsResult.Items || [], { - itemsContainer: container, + itemsContainer: element.current?.querySelector('.itemsContainer'), + parentContainer: element.current?.querySelector('#resumableSection'), preferThumb: true, shape: getThumbShape(), scalable: true, @@ -49,10 +43,14 @@ const ResumableItemsContainer: FunctionComponent =
- + {enableScrollX() ? : }
diff --git a/src/view/movies/SuggestionsView.tsx b/src/view/movies/SuggestionsView.tsx index 7cecd5616..34a049783 100644 --- a/src/view/movies/SuggestionsView.tsx +++ b/src/view/movies/SuggestionsView.tsx @@ -113,41 +113,6 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { loadSuggestions(view, userId); }, [loadLatest, loadResume, loadSuggestions, props.topParentId]); - const setScrollClasses = useCallback((elem, scrollX) => { - const page = element.current; - - if (!page) { - console.error('Unexpected null reference'); - return; - } - - 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 initSuggestedTab = useCallback((view) => { - const containers = view.querySelectorAll('.itemsContainer'); - - for (const container of containers) { - setScrollClasses(container, enableScrollX()); - } - }, [enableScrollX, setScrollClasses]); - useEffect(() => { const page = element.current; @@ -156,9 +121,8 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { return; } - initSuggestedTab(page); loadSuggestionsTab(page); - }, [initSuggestedTab, loadSuggestionsTab]); + }, [loadSuggestionsTab]); return (
@@ -173,7 +137,6 @@ const SuggestionsView: FunctionComponent = (props: IProps) => {
: recommendations.map((recommendation, index) => { return ; })} - {}
); From cf137497a093006d98c4e759fa82508612dff87b Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sun, 21 Aug 2022 03:09:22 +0300 Subject: [PATCH 09/23] Refactoring duplicates code --- src/scripts/dom.js | 8 +- src/scripts/settings/userSettings.js | 8 +- src/strings/en-us.json | 1 + src/view/components/AlphaPickerContainer.tsx | 43 ++--- src/view/components/Filter.tsx | 11 +- src/view/components/GenresItemsContainer.tsx | 9 +- src/view/components/ItemsContainer.tsx | 19 ++- src/view/components/NewCollection.tsx | 30 ++-- src/view/components/Pagination.tsx | 88 ++++++---- src/view/components/SelectView.tsx | 8 +- src/view/components/Shuffle.tsx | 5 +- src/view/components/Sort.tsx | 15 +- src/view/components/ViewItemsContainer.tsx | 144 ++++++++++++++++ src/view/components/type.ts | 1 + src/view/movies/CollectionsView.tsx | 116 ++++--------- src/view/movies/FavoritesView.tsx | 162 ++++++------------ src/view/movies/GenresView.tsx | 26 ++- src/view/movies/MoviesView.tsx | 168 +++++++------------ src/view/movies/SuggestionsView.tsx | 4 +- src/view/movies/TrailersView.tsx | 138 +++++---------- 20 files changed, 491 insertions(+), 513 deletions(-) create mode 100644 src/view/components/ViewItemsContainer.tsx diff --git a/src/scripts/dom.js b/src/scripts/dom.js index e336088c8..06d30235b 100644 --- a/src/scripts/dom.js +++ b/src/scripts/dom.js @@ -145,9 +145,15 @@ windowSize = null; } + /** + * @typedef {Object} windowSize + * @property {number} innerHeight - window innerHeight. + * @property {number} innerWidth - window innerWidth. + */ + /** * Returns window size. - * @returns {Object} Window size. + * @returns {windowSize} Window size. */ export function getWindowSize() { if (!windowSize) { diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js index 062749020..d670bb010 100644 --- a/src/scripts/settings/userSettings.js +++ b/src/scripts/settings/userSettings.js @@ -467,11 +467,17 @@ export class UserSettings { return this.get('soundeffects', false); } + /** + * @typedef {Object} Query + * @property {number} StartIndex - query StartIndex. + * @property {number} Limit - query Limit. + */ + /** * Load query settings. * @param {string} key - Query key. * @param {Object} query - Query base. - * @return {Object} Query. + * @return {Query} Query. */ loadQuerySettings(key, query) { let values = this.get(key); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 37e3c5bb4..7106cbcee 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1065,6 +1065,7 @@ "MessageItemsAdded": "Items added.", "MessageItemSaved": "Item saved.", "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.", "MessageNoCollectionsAvailable": "Collections allow you to enjoy personalized groupings of Movies, Series, and Albums. Click the '+' button to start creating collections.", diff --git a/src/view/components/AlphaPickerContainer.tsx b/src/view/components/AlphaPickerContainer.tsx index dd4cc36f2..b85e99e0b 100644 --- a/src/view/components/AlphaPickerContainer.tsx +++ b/src/view/components/AlphaPickerContainer.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; +import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'; import AlphaPicker from '../../components/alphaPicker/alphaPicker'; import { IQuery } from './type'; @@ -13,34 +13,35 @@ const AlphaPickerContainer: FunctionComponent = ({ query, relo alphaPicker?.updateControls(query); + const onAlphaPickerChange = useCallback((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(); + }, [query, reloadItems]); + 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' - })); + setAlphaPicker(new AlphaPicker({ + element: alphaPickerElement, + valueChangeEvent: 'click' + })); - alphaPickerElement.classList.add('alphaPicker-fixed-right'); + if (alphaPickerElement) { + alphaPickerElement.addEventListener('alphavaluechanged', onAlphaPickerChange); } - }, [query, reloadItems, setAlphaPicker]); + }, [onAlphaPickerChange]); return (
-
+
); }; diff --git a/src/view/components/Filter.tsx b/src/view/components/Filter.tsx index d0fc0274f..8e0dfd554 100644 --- a/src/view/components/Filter.tsx +++ b/src/view/components/Filter.tsx @@ -5,17 +5,18 @@ import { IQuery } from './type'; type FilterProps = { query: IQuery; + getFilterMode: () => string | null; reloadItems: () => void; } -const Filter: FunctionComponent = ({ query, reloadItems }: FilterProps) => { +const Filter: FunctionComponent = ({ query, getFilterMode, reloadItems }: FilterProps) => { const element = useRef(null); const showFilterMenu = useCallback(() => { import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => { const filterDialog = new filterDialogFactory({ query: query, - mode: 'movies', + mode: getFilterMode(), serverId: window.ApiClient.serverId() }); Events.on(filterDialog, 'filterchange', () => { @@ -24,15 +25,13 @@ const Filter: FunctionComponent = ({ query, reloadItems }: FilterPr }); filterDialog.show(); }); - }, [query, reloadItems]); + }, [getFilterMode, query, reloadItems]); useEffect(() => { const btnFilter = element.current?.querySelector('.btnFilter'); if (btnFilter) { - btnFilter.addEventListener('click', () => { - showFilterMenu(); - }); + btnFilter.addEventListener('click', showFilterMenu); } }, [showFilterMenu]); diff --git a/src/view/components/GenresItemsContainer.tsx b/src/view/components/GenresItemsContainer.tsx index cb003ba05..9abccbee0 100644 --- a/src/view/components/GenresItemsContainer.tsx +++ b/src/view/components/GenresItemsContainer.tsx @@ -126,13 +126,8 @@ const GenresItemsContainer: FunctionComponent = ({ to html += ''; html += '
'; if (enableScrollX()) { - let scrollXClass = 'scrollX hiddenScrollX'; - - if (layoutManager.tv) { - scrollXClass += 'smoothScrollX padded-top-focusscale padded-bottom-focusscale'; - } - - html += '
'; + html += '
'; + html += '
'; } else { html += '
'; } diff --git a/src/view/components/ItemsContainer.tsx b/src/view/components/ItemsContainer.tsx index 8cc2992cc..6a6b24d03 100644 --- a/src/view/components/ItemsContainer.tsx +++ b/src/view/components/ItemsContainer.tsx @@ -12,11 +12,12 @@ import { IQuery } from './type'; type ItemsContainerProps = { getCurrentViewStyle: () => string; query: IQuery; + getContext: () => string | null; items?: BaseItemDto[] | null; noItemsMessage?: string; } -const ItemsContainer: FunctionComponent = ({ getCurrentViewStyle, query, items = [], noItemsMessage }: ItemsContainerProps) => { +const ItemsContainer: FunctionComponent = ({ getCurrentViewStyle, query, getContext, items = [], noItemsMessage }: ItemsContainerProps) => { const element = useRef(null); const viewStyle = getCurrentViewStyle(); @@ -28,7 +29,7 @@ const ItemsContainer: FunctionComponent = ({ getCurrentView items: items, shape: 'backdrop', preferThumb: true, - context: 'movies', + context: getContext(), lazy: true, overlayPlayButton: true, showTitle: true, @@ -40,7 +41,7 @@ const ItemsContainer: FunctionComponent = ({ getCurrentView items: items, shape: 'backdrop', preferThumb: true, - context: 'movies', + context: getContext(), lazy: true, cardLayout: true, showTitle: true, @@ -52,20 +53,20 @@ const ItemsContainer: FunctionComponent = ({ getCurrentView items: items, shape: 'banner', preferBanner: true, - context: 'movies', + context: getContext(), lazy: true }); } else if (viewStyle == 'List') { html = listview.getListViewHtml({ items: items, - context: 'movies', + context: getContext(), sortBy: query.SortBy }); } else if (viewStyle == 'PosterCard') { html = cardBuilder.getCardsHtml(items, { items: items, shape: 'portrait', - context: 'movies', + context: getContext(), showTitle: true, showYear: true, centerText: true, @@ -76,7 +77,7 @@ const ItemsContainer: FunctionComponent = ({ getCurrentView html = cardBuilder.getCardsHtml(items, { items: items, shape: 'portrait', - context: 'movies', + context: getContext(), overlayPlayButton: true, showTitle: true, showYear: true, @@ -96,14 +97,14 @@ const ItemsContainer: FunctionComponent = ({ getCurrentView const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement; itemsContainer.innerHTML = html; imageLoader.lazyChildren(itemsContainer); - }, [query.SortBy, items, noItemsMessage, viewStyle]); + }, [query.SortBy, items, noItemsMessage, viewStyle, getContext]); const cssClass = viewStyle == 'List' ? 'vertical-list' : 'vertical-wrap'; return (
); diff --git a/src/view/components/NewCollection.tsx b/src/view/components/NewCollection.tsx index 1dbd625e6..d77329571 100644 --- a/src/view/components/NewCollection.tsx +++ b/src/view/components/NewCollection.tsx @@ -1,26 +1,28 @@ -import React, { FunctionComponent, useEffect, useRef } from 'react'; +import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; import IconButtonElement from '../../elements/IconButtonElement'; const NewCollection: FunctionComponent = () => { const element = useRef(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(); - const collectionEditor = new CollectionEditor(); - collectionEditor.show({ - items: [], - serverId: serverId - }); - }); + const showCollectionEditor = useCallback(() => { + import('../../components/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 (
= ({ query, itemsResult = {}, reloadItems }: PaginationProps) => { + const startIndex = query.StartIndex; + const limit = query.Limit; + const totalRecordCount = itemsResult.TotalRecordCount || 0; + const recordsEnd = Math.min(startIndex + limit, totalRecordCount); + const showControls = limit < totalRecordCount; const element = useRef(null); + + const onNextPageClick = useCallback(() => { + if (query.Limit > 0) { + query.StartIndex += query.Limit; + } + reloadItems(); + }, [query, reloadItems]); + + const onPreviousPageClick = useCallback(() => { + if (query.Limit > 0) { + query.StartIndex = Math.max(0, query.StartIndex - query.Limit); + } + reloadItems(); + }, [query, reloadItems]); + 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) { + 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); } - }, [itemsResult, query, reloadItems]); + }, [totalRecordCount, onNextPageClick, onPreviousPageClick, limit, startIndex]); return (
-
+
+ {showControls && ( +
+ + + {globalize.translate('ListPaging', (totalRecordCount ? startIndex + 1 : 0), recordsEnd, totalRecordCount)} + + + + +
+ )} +
); }; diff --git a/src/view/components/SelectView.tsx b/src/view/components/SelectView.tsx index eb857ca12..c1f2633c5 100644 --- a/src/view/components/SelectView.tsx +++ b/src/view/components/SelectView.tsx @@ -8,11 +8,11 @@ import { IQuery } from './type'; type SelectViewProps = { getCurrentViewStyle: () => string; query: IQuery; - savedViewKey: string; + getViewSettings: () => string; reloadItems: () => void; } -const SelectView: FunctionComponent = ({ getCurrentViewStyle, savedViewKey, query, reloadItems }: SelectViewProps) => { +const SelectView: FunctionComponent = ({ getCurrentViewStyle, getViewSettings, query, reloadItems }: SelectViewProps) => { const element = useRef(null); useEffect(() => { @@ -22,11 +22,11 @@ const SelectView: FunctionComponent = ({ getCurrentViewStyle, s }); btnSelectView.addEventListener('layoutchange', (e) => { const viewStyle = (e as CustomEvent).detail.viewStyle; - userSettings.set(savedViewKey, viewStyle, false); + userSettings.set(getViewSettings(), viewStyle, false); query.StartIndex = 0; reloadItems(); }); - }, [getCurrentViewStyle, query, reloadItems, savedViewKey]); + }, [getCurrentViewStyle, query, reloadItems, getViewSettings]); return (
diff --git a/src/view/components/Shuffle.tsx b/src/view/components/Shuffle.tsx index fe8d92023..3ff64253f 100644 --- a/src/view/components/Shuffle.tsx +++ b/src/view/components/Shuffle.tsx @@ -22,8 +22,7 @@ const Shuffle: FunctionComponent = ({ itemsResult = {}, topParentI }, [topParentId]); useEffect(() => { - const btnShuffle = element.current?.querySelector('.btnShuffle') as HTMLButtonElement; - btnShuffle.classList.toggle('hide', typeof itemsResult.TotalRecordCount === 'number' && itemsResult.TotalRecordCount < 1); + const btnShuffle = element.current?.querySelector('.btnShuffle'); if (btnShuffle) { btnShuffle.addEventListener('click', shuffle); } @@ -33,7 +32,7 @@ const Shuffle: FunctionComponent = ({ itemsResult = {}, topParentI
diff --git a/src/view/components/Sort.tsx b/src/view/components/Sort.tsx index 9cdb12777..47f155307 100644 --- a/src/view/components/Sort.tsx +++ b/src/view/components/Sort.tsx @@ -5,13 +5,16 @@ import * as userSettings from '../../scripts/settings/userSettings'; import { IQuery } from './type'; type SortProps = { - sortMenuOptions: () => { name: string; id: string}[]; + getSortMenuOptions: () => { + name: string; + id: string; + }[]; query: IQuery; - savedQueryKey: string; + getSettingsKey: () => string; reloadItems: () => void; } -const Sort: FunctionComponent = ({ sortMenuOptions, query, savedQueryKey, reloadItems }: SortProps) => { +const Sort: FunctionComponent = ({ getSortMenuOptions, query, getSettingsKey, reloadItems }: SortProps) => { const element = useRef(null); useEffect(() => { @@ -20,10 +23,10 @@ const Sort: FunctionComponent = ({ sortMenuOptions, query, savedQuery if (btnSort) { btnSort.addEventListener('click', (e) => { libraryBrowser.showSortMenu({ - items: sortMenuOptions(), + items: getSortMenuOptions(), callback: () => { query.StartIndex = 0; - userSettings.saveQuerySettings(savedQueryKey, query); + userSettings.saveQuerySettings(getSettingsKey(), query); reloadItems(); }, query: query, @@ -31,7 +34,7 @@ const Sort: FunctionComponent = ({ sortMenuOptions, query, savedQuery }); }); } - }, [sortMenuOptions, query, reloadItems, savedQueryKey]); + }, [getSortMenuOptions, query, reloadItems, getSettingsKey]); return (
diff --git a/src/view/components/ViewItemsContainer.tsx b/src/view/components/ViewItemsContainer.tsx new file mode 100644 index 000000000..974f2c4fc --- /dev/null +++ b/src/view/components/ViewItemsContainer.tsx @@ -0,0 +1,144 @@ +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 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 { IQuery } from './type'; +import NewCollection from './NewCollection'; + +type IProps = { + topParentId: string | null; + isBtnShuffleEnabled?: boolean; + isBtnFilterEnabled?: boolean; + isBtnNewCollectionEnabled?: boolean; + isAlphaPickerEnabled?: boolean; + getBasekey: () => string; + getFilterMode: () => string; + getItemTypes: () => string; + getSortMenuOptions: () => { + name: string; + id: string; + }[]; + getNoItemsMessage: () => string; +} + +const ViewItemsContainer: FunctionComponent = ({ + topParentId, + isBtnShuffleEnabled = false, + isBtnFilterEnabled = true, + isBtnNewCollectionEnabled = false, + isAlphaPickerEnabled = true, + getBasekey, + getFilterMode, + getItemTypes, + getSortMenuOptions, + getNoItemsMessage +}: IProps) => { + const [ itemsResult, setItemsResult ] = useState({}); + + const element = useRef(null); + + const getSettingsKey = useCallback(() => { + return `${topParentId} - ${getBasekey()}`; + }, [getBasekey, topParentId]); + + const getViewSettings = useCallback(() => { + return `${getSettingsKey()} -view`; + }, [getSettingsKey]); + + let query = useMemo(() => ({ + SortBy: 'SortName,ProductionYear', + SortOrder: 'Ascending', + IncludeItemTypes: getItemTypes(), + Recursive: true, + Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + Limit: userSettings.libraryPageSize(undefined), + StartIndex: 0, + ParentId: topParentId + }), [getItemTypes, topParentId]); + + if (getBasekey() === 'favorites') { + query.IsFavorite = true; + } + + query = userSettings.loadQuerySettings(getSettingsKey(), query); + + const getCurrentViewStyle = useCallback(() => { + return userSettings.get(getViewSettings(), false) || 'Poster'; + }, [getViewSettings]); + + const getContext = useCallback(() => { + const itemType = getItemTypes(); + if (itemType === 'Movie' || itemType === 'BoxSet') { + return 'movies'; + } + + return null; + }, [getItemTypes]); + + 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]); + + useEffect(() => { + reloadItems(); + }, [reloadItems]); + + return ( +
+
+ + + {isBtnShuffleEnabled && } + + + + {isBtnFilterEnabled && } + + {isBtnNewCollectionEnabled && } + +
+ + {isAlphaPickerEnabled && } + + + +
+ +
+
+ ); +}; + +export default ViewItemsContainer; diff --git a/src/view/components/type.ts b/src/view/components/type.ts index 2dde6bb24..b54ad5334 100644 --- a/src/view/components/type.ts +++ b/src/view/components/type.ts @@ -5,6 +5,7 @@ export type IQuery = { Recursive?: boolean; Fields?: string; ImageTypeLimit?: number; + EnableTotalRecordCount?: boolean; EnableImageTypes?: string; StartIndex: number; ParentId?: string | null; diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx index 612df7dee..17bd14cf5 100644 --- a/src/view/movies/CollectionsView.tsx +++ b/src/view/movies/CollectionsView.tsx @@ -1,99 +1,51 @@ -import '../../elements/emby-itemscontainer/emby-itemscontainer'; +import React, { FunctionComponent, useCallback } from 'react'; -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' - }]; -}; +import ViewItemsContainer from '../components/ViewItemsContainer'; type IProps = { topParentId: string | null; } const CollectionsView: FunctionComponent = ({ topParentId }: IProps) => { - const savedQueryKey = topParentId + '-moviecollections'; - const savedViewKey = savedQueryKey + '-view'; + const getBasekey = useCallback(() => { + return 'collections'; + }, []); - const [ itemsResult, setItemsResult ] = useState({}); - const element = useRef(null); + const getFilterMode = useCallback(() => { + return 'movies'; + }, []); - const query = useMemo(() => ({ - 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]); + const getItemTypes = useCallback(() => { + return 'BoxSet'; + }, []); - userSettings.loadQuerySettings(savedQueryKey, query); + const getNoItemsMessage = useCallback(() => { + return 'MessageNoCollectionsAvailable'; + }, []); - 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]); - - useEffect(() => { - reloadItems(); - }, [reloadItems]); + const getSortMenuOptions = useCallback(() => { + return [{ + name: globalize.translate('Name'), + id: 'SortName' + }, { + name: globalize.translate('OptionDateAdded'), + id: 'DateCreated,SortName' + }]; + }, []); return ( -
-
- - - - - - -
- - - -
- -
-
+ ); }; diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx index 713b02376..8ec222af7 100644 --- a/src/view/movies/FavoritesView.tsx +++ b/src/view/movies/FavoritesView.tsx @@ -1,124 +1,72 @@ -import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { FunctionComponent, useCallback } 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'; +import ViewItemsContainer from '../components/ViewItemsContainer'; 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 = ({ topParentId }: IProps) => { - const savedQueryKey = topParentId + '-favorites'; - const savedViewKey = savedQueryKey + '-view'; + const getBasekey = useCallback(() => { + return 'favorites'; + }, []); - const [ itemsResult, setItemsResult ] = useState({}); - const element = useRef(null); + const getFilterMode = useCallback(() => { + return 'movies'; + }, []); - const query = useMemo(() => ({ - 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]); + const getItemTypes = useCallback(() => { + return 'Movie'; + }, []); - userSettings.loadQuerySettings(savedQueryKey, query); + const getNoItemsMessage = useCallback(() => { + return 'MessageNoFavoritesAvailable'; + }, []); - 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]); - - useEffect(() => { - reloadItems(); - }, [query, reloadItems]); + const getSortMenuOptions = useCallback(() => { + 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' + }]; + }, []); return ( -
-
- - - - - - -
- - - - - -
- -
-
+ ); }; diff --git a/src/view/movies/GenresView.tsx b/src/view/movies/GenresView.tsx index d442def73..b75dc9e96 100644 --- a/src/view/movies/GenresView.tsx +++ b/src/view/movies/GenresView.tsx @@ -11,13 +11,18 @@ type IProps = { } const GenresView: FunctionComponent = ({ topParentId }: IProps) => { - const savedQueryKey = topParentId + '-moviegenres'; - const savedViewKey = savedQueryKey + '-view'; - const [ itemsResult, setItemsResult ] = useState({}); const element = useRef(null); - const query = useMemo(() => ({ + const getSettingsKey = useCallback(() => { + return topParentId + '-genres'; + }, [topParentId]); + + const getViewSettings = useCallback(() => { + return getSettingsKey() + '-view'; + }, [getSettingsKey]); + + let query = useMemo(() => ({ SortBy: 'SortName', SortOrder: 'Ascending', IncludeItemTypes: 'Movie', @@ -27,20 +32,13 @@ const GenresView: FunctionComponent = ({ topParentId }: IProps) => { StartIndex: 0, ParentId: topParentId }), [topParentId]); - userSettings.loadQuerySettings(savedQueryKey, query); + query = userSettings.loadQuerySettings(getSettingsKey(), query); const getCurrentViewStyle = useCallback(() => { - return userSettings.get(savedViewKey, false) || 'Poster'; - }, [savedViewKey]); + return userSettings.get(getViewSettings(), false) || 'Poster'; + }, [getViewSettings]); 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); diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx index e0fee74a5..3dd2b3caf 100644 --- a/src/view/movies/MoviesView.tsx +++ b/src/view/movies/MoviesView.tsx @@ -1,127 +1,73 @@ -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 React, { FunctionComponent, useCallback } from 'react'; 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'; + +import ViewItemsContainer from '../components/ViewItemsContainer'; 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 = ({ topParentId }: IProps) => { - const savedQueryKey = topParentId + '-movies'; - const savedViewKey = savedQueryKey + '-view'; + const getBasekey = useCallback(() => { + return 'movies'; + }, []); - const [ itemsResult, setItemsResult ] = useState(); + const getFilterMode = useCallback(() => { + return 'movies'; + }, []); - const element = useRef(null); + const getItemTypes = useCallback(() => { + return 'Movie'; + }, []); - const query = useMemo(() => ({ - 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]); + const getNoItemsMessage = useCallback(() => { + return 'MessageNoItemsAvailable'; + }, []); - 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]); - - useEffect(() => { - reloadItems(); - }, [query, reloadItems]); + const getSortMenuOptions = useCallback(() => { + 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' + }]; + }, []); return ( -
-
- - - - - - - - -
- - - - - -
- -
-
+ ); }; diff --git a/src/view/movies/SuggestionsView.tsx b/src/view/movies/SuggestionsView.tsx index 34a049783..9ff96a50f 100644 --- a/src/view/movies/SuggestionsView.tsx +++ b/src/view/movies/SuggestionsView.tsx @@ -57,7 +57,7 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { const loadResume = useCallback((page, userId, parentId) => { loading.show(); - const screenWidth: any = dom.getWindowSize(); + const screenWidth = dom.getWindowSize(); const options = { SortBy: 'DatePlayed', SortOrder: 'Descending', @@ -82,7 +82,7 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { }, [autoFocus]); const loadSuggestions = useCallback((page, userId) => { - const screenWidth: any = dom.getWindowSize(); + const screenWidth = dom.getWindowSize(); let itemLimit = 5; if (screenWidth.innerWidth >= 1600) { itemLimit = 8; diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx index e776c0117..9cce2ebe4 100644 --- a/src/view/movies/TrailersView.tsx +++ b/src/view/movies/TrailersView.tsx @@ -1,110 +1,64 @@ -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 React, { FunctionComponent, useCallback } 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' - }]; -}; +import ViewItemsContainer from '../components/ViewItemsContainer'; type IProps = { topParentId: string | null; } const TrailersView: FunctionComponent = ({ topParentId }: IProps) => { - const savedQueryKey = topParentId + '-trailers'; - const savedViewKey = savedQueryKey + '-view'; + const getBasekey = useCallback(() => { + return 'trailers'; + }, []); - const [ itemsResult, setItemsResult ] = useState(); - const element = useRef(null); + const getFilterMode = useCallback(() => { + return 'movies'; + }, []); - const query = useMemo(() => ({ - 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]); + const getItemTypes = useCallback(() => { + return 'Trailer'; + }, []); - userSettings.loadQuerySettings(savedQueryKey, query); + const getNoItemsMessage = useCallback(() => { + return 'MessageNoTrailersFound'; + }, []); - 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]); - - useEffect(() => { - reloadItems(); - }, [query, reloadItems]); + const getSortMenuOptions = useCallback(() => { + 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' + }]; + }, []); return ( -
-
- - - - - -
- - - - - -
- -
-
+ ); }; From 9d88af3dfe773764b455e36dfba62d6e09b9c32c Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Fri, 2 Sep 2022 02:46:05 +0300 Subject: [PATCH 10/23] Refactoring Suggestions View --- .../components/RecommendationContainer.tsx | 51 ++-------- .../components/ResumableItemsContainer.tsx | 60 ----------- ...temsContainer.tsx => SectionContainer.tsx} | 31 +++--- src/view/components/type.ts | 33 +++++++ src/view/movies/SuggestionsView.tsx | 99 ++++++++++--------- 5 files changed, 116 insertions(+), 158 deletions(-) delete mode 100644 src/view/components/ResumableItemsContainer.tsx rename src/view/components/{RecentlyAddedItemsContainer.tsx => SectionContainer.tsx} (67%) diff --git a/src/view/components/RecommendationContainer.tsx b/src/view/components/RecommendationContainer.tsx index 7501585a5..75756a8a1 100644 --- a/src/view/components/RecommendationContainer.tsx +++ b/src/view/components/RecommendationContainer.tsx @@ -1,13 +1,9 @@ -import '../../elements/emby-itemscontainer/emby-itemscontainer'; - import { RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useEffect, useRef } from 'react'; +import React, { FunctionComponent } from 'react'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; import globalize from '../../scripts/globalize'; -import ItemsContainerElement from '../../elements/ItemsContainerElement'; -import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; import escapeHTML from 'escape-html'; +import SectionContainer from './SectionContainer'; type RecommendationContainerProps = { getPortraitShape: () => string; @@ -16,8 +12,6 @@ type RecommendationContainerProps = { } const RecommendationContainer: FunctionComponent = ({ getPortraitShape, enableScrollX, recommendation = {} }: RecommendationContainerProps) => { - const element = useRef(null); - let title = ''; switch (recommendation.RecommendationType) { @@ -40,40 +34,15 @@ const RecommendationContainer: FunctionComponent = break; } - useEffect(() => { - cardBuilder.buildCards(recommendation.Items || [], { - itemsContainer: element.current?.querySelector('.itemsContainer'), + return -
-
-

- {escapeHTML(title)} -

-
- - {enableScrollX() ? : } - -
-
- ); + showYear: true + }} + />; }; export default RecommendationContainer; diff --git a/src/view/components/ResumableItemsContainer.tsx b/src/view/components/ResumableItemsContainer.tsx deleted file mode 100644 index b26b8613f..000000000 --- a/src/view/components/ResumableItemsContainer.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import '../../elements/emby-itemscontainer/emby-itemscontainer'; - -import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useEffect, useRef } from 'react'; - -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import globalize from '../../scripts/globalize'; -import ItemsContainerElement from '../../elements/ItemsContainerElement'; -import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; - -type ResumableItemsContainerProps = { - getThumbShape: () => string; - enableScrollX: () => boolean; - itemsResult?: BaseItemDtoQueryResult; -} - -const ResumableItemsContainer: FunctionComponent = ({ getThumbShape, enableScrollX, itemsResult = {} }: ResumableItemsContainerProps) => { - const element = useRef(null); - - useEffect(() => { - const allowBottomPadding = !enableScrollX(); - cardBuilder.buildCards(itemsResult.Items || [], { - itemsContainer: element.current?.querySelector('.itemsContainer'), - parentContainer: element.current?.querySelector('#resumableSection'), - preferThumb: true, - shape: getThumbShape(), - scalable: true, - overlayPlayButton: true, - allowBottomPadding: allowBottomPadding, - cardLayout: false, - showTitle: true, - showYear: true, - centerText: true - }); - }, [enableScrollX, getThumbShape, itemsResult.Items]); - - return ( -
-
-
-

- {globalize.translate('HeaderContinueWatching')} -

-
- - {enableScrollX() ? : } - -
-
- ); -}; - -export default ResumableItemsContainer; diff --git a/src/view/components/RecentlyAddedItemsContainer.tsx b/src/view/components/SectionContainer.tsx similarity index 67% rename from src/view/components/RecentlyAddedItemsContainer.tsx rename to src/view/components/SectionContainer.tsx index 60c6571bd..2268fe292 100644 --- a/src/view/components/RecentlyAddedItemsContainer.tsx +++ b/src/view/components/SectionContainer.tsx @@ -4,39 +4,44 @@ import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; import React, { FunctionComponent, useEffect, useRef } from 'react'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import globalize from '../../scripts/globalize'; import ItemsContainerElement from '../../elements/ItemsContainerElement'; import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; +import { ICardOptions } from './type'; -type RecentlyAddedItemsContainerProps = { - getPortraitShape: () => string; +type SectionContainerProps = { + sectionTitle: string; enableScrollX: () => boolean; items?: BaseItemDto[]; + cardOptions?: ICardOptions; } -const RecentlyAddedItemsContainer: FunctionComponent = ({ getPortraitShape, enableScrollX, items = [] }: RecentlyAddedItemsContainerProps) => { +const SectionContainer: FunctionComponent = ({ + sectionTitle, + enableScrollX, + items = [], + cardOptions = {} +}: SectionContainerProps) => { const element = useRef(null); useEffect(() => { cardBuilder.buildCards(items, { itemsContainer: element.current?.querySelector('.itemsContainer'), - parentContainer: element.current?.querySelector('#recentlyAddedItemsSection'), - shape: getPortraitShape(), + parentContainer: element.current?.querySelector('.verticalSection'), scalable: true, overlayPlayButton: true, - allowBottomPadding: true, showTitle: true, - showYear: true, - centerText: true + centerText: true, + cardLayout: false, + ...cardOptions }); - }, [enableScrollX, getPortraitShape, items]); + }, [cardOptions, enableScrollX, items]); return (
-
+

- {globalize.translate('HeaderLatestMovies')} + {sectionTitle}

@@ -54,4 +59,4 @@ const RecentlyAddedItemsContainer: FunctionComponent = (props: IProps) => { const [ latestItems, setLatestItems ] = useState([]); - const [ resumeItemsResult, setResumeItemsResult ] = useState(); + const [ resumeResult, setResumeResult ] = useState({}); const [ recommendations, setRecommendations ] = useState([]); const element = useRef(null); @@ -37,6 +36,31 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { }); }, []); + 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', @@ -50,43 +74,16 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { window.ApiClient.getJSON(window.ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(items => { setLatestItems(items); - // FIXME: Wait for all sections to load - autoFocus(page); - }); - }, [autoFocus]); - - const loadResume = useCallback((page, userId, parentId) => { - loading.show(); - const screenWidth = 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 => { - setResumeItemsResult(result); - - loading.hide(); - // FIXME: Wait for all sections to load autoFocus(page); }); }, [autoFocus]); const loadSuggestions = useCallback((page, userId) => { - const screenWidth = dom.getWindowSize(); + const screenWidth = dom.getWindowSize().innerWidth; let itemLimit = 5; - if (screenWidth.innerWidth >= 1600) { + if (screenWidth >= 1600) { itemLimit = 8; - } else if (screenWidth.innerWidth >= 1200) { + } else if (screenWidth >= 1200) { itemLimit = 6; } const url = window.window.ApiClient.getUrl('Movies/Recommendations', { @@ -100,7 +97,6 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { window.ApiClient.getJSON(url).then(result => { setRecommendations(result); - // FIXME: Wait for all sections to load autoFocus(page); }); }, [autoFocus]); @@ -126,18 +122,33 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { return (
- + - + -
- {!recommendations.length ?
-

{globalize.translate('MessageNothingHere')}

-

{globalize.translate('MessageNoMovieSuggestionsAvailable')}

-
: recommendations.map((recommendation, index) => { - return ; - })} -
+ {!recommendations.length ?
+

{globalize.translate('MessageNothingHere')}

+

{globalize.translate('MessageNoMovieSuggestionsAvailable')}

+
: recommendations.map((recommendation, index) => { + return ; + })}
); }; From de4a359c98baaa6fda291bfaf317a3d97ec2adfe Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sun, 2 Oct 2022 19:07:42 +0300 Subject: [PATCH 11/23] Use interface over type --- src/elements/ItemsContainerElement.tsx | 11 ++---- .../ItemsScrollerContainerElement.tsx | 21 +++++----- src/routes/movies.tsx | 38 +++++++++---------- src/view/components/Filter.tsx | 10 ++--- src/view/components/GenresItemsContainer.tsx | 6 +-- src/view/components/ItemsContainer.tsx | 4 +- src/view/components/NewCollection.tsx | 4 +- src/view/components/Pagination.tsx | 10 ++--- .../components/RecommendationContainer.tsx | 6 +-- src/view/components/SectionContainer.tsx | 12 +++--- src/view/components/SelectView.tsx | 10 ++--- src/view/components/Shuffle.tsx | 6 +-- src/view/components/Sort.tsx | 10 ++--- src/view/components/ViewItemsContainer.tsx | 12 +++--- src/view/components/{type.ts => interface.ts} | 4 +- src/view/movies/CollectionsView.tsx | 6 +-- src/view/movies/FavoritesView.tsx | 6 +-- src/view/movies/GenresView.tsx | 10 ++--- src/view/movies/MoviesView.tsx | 6 +-- src/view/movies/SuggestionsView.tsx | 8 ++-- src/view/movies/TrailersView.tsx | 6 +-- 21 files changed, 100 insertions(+), 106 deletions(-) rename src/view/components/{type.ts => interface.ts} (96%) diff --git a/src/elements/ItemsContainerElement.tsx b/src/elements/ItemsContainerElement.tsx index 44ea3de2d..0e4fb7b6d 100644 --- a/src/elements/ItemsContainerElement.tsx +++ b/src/elements/ItemsContainerElement.tsx @@ -1,24 +1,21 @@ -import React, { FunctionComponent } from 'react'; +import React, { FC } from 'react'; -const createElement = ({ id, className }: IProps) => ({ +const createElement = ({ className }: IProps) => ({ __html: `
` }); -type IProps = { - id?: string; +interface IProps { className?: string; } -const ItemsContainerElement: FunctionComponent = ({ id, className }: IProps) => { +const ItemsContainerElement: FC = ({ className }) => { return (
diff --git a/src/elements/ItemsScrollerContainerElement.tsx b/src/elements/ItemsScrollerContainerElement.tsx index 438944c26..01f823ae6 100644 --- a/src/elements/ItemsScrollerContainerElement.tsx +++ b/src/elements/ItemsScrollerContainerElement.tsx @@ -1,31 +1,29 @@ -import React, { FunctionComponent } from 'react'; +import React, { FC } from 'react'; -const createScroller = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, id, className }: IProps) => ({ +const createScroller = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, className }: IProps) => ({ __html: `
-
-
+
+
` }); -type IProps = { +interface IProps { scrollerclassName?: string; dataHorizontal?: string; dataMousewheel?: string; dataCenterfocus?: string; - id?: string; className?: string; } -const ItemsScrollerContainerElement: FunctionComponent = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, id, className }: IProps) => { +const ItemsScrollerContainerElement: FC = ({ scrollerclassName, dataHorizontal, dataMousewheel, dataCenterfocus, className }) => { return (
= ({ scrollerclas dataHorizontal: dataHorizontal ? `data-horizontal="${dataHorizontal}"` : '', dataMousewheel: dataMousewheel ? `data-mousewheel="${dataMousewheel}"` : '', dataCenterfocus: dataCenterfocus ? `data-centerfocus="${dataCenterfocus}"` : '', - id: id ? `id='${id}'` : '', className: className })} /> diff --git a/src/routes/movies.tsx b/src/routes/movies.tsx index bf9686c0d..3c4e4344b 100644 --- a/src/routes/movies.tsx +++ b/src/routes/movies.tsx @@ -3,7 +3,7 @@ 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 React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import * as mainTabsManager from '../components/maintabsmanager'; @@ -37,28 +37,28 @@ const getDefaultTabIndex = (folderId: string | null) => { } }; -const Movies: FunctionComponent = () => { +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(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'); @@ -106,7 +106,7 @@ const Movies: FunctionComponent = () => { console.error('Unexpected null reference'); return; } - mainTabsManager.setTabs(element.current, selectedIndex, getTabs, undefined, undefined, onTabChange); + mainTabsManager.setTabs(page, selectedIndex, getTabs, undefined, undefined, onTabChange); if (!page.getAttribute('data-title')) { const parentId = searchParams.get('topParentId'); diff --git a/src/view/components/Filter.tsx b/src/view/components/Filter.tsx index 8e0dfd554..248b6c9fb 100644 --- a/src/view/components/Filter.tsx +++ b/src/view/components/Filter.tsx @@ -1,15 +1,15 @@ -import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; import { Events } from 'jellyfin-apiclient'; import IconButtonElement from '../../elements/IconButtonElement'; -import { IQuery } from './type'; +import { QueryI } from './interface'; -type FilterProps = { - query: IQuery; +interface FilterI { + query: QueryI; getFilterMode: () => string | null; reloadItems: () => void; } -const Filter: FunctionComponent = ({ query, getFilterMode, reloadItems }: FilterProps) => { +const Filter: FC = ({ query, getFilterMode, reloadItems }) => { const element = useRef(null); const showFilterMenu = useCallback(() => { diff --git a/src/view/components/GenresItemsContainer.tsx b/src/view/components/GenresItemsContainer.tsx index 9abccbee0..1b90e49e7 100644 --- a/src/view/components/GenresItemsContainer.tsx +++ b/src/view/components/GenresItemsContainer.tsx @@ -3,7 +3,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer'; import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; import escapeHTML from 'escape-html'; -import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; import { appRouter } from '../../components/appRouter'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; @@ -11,13 +11,13 @@ import layoutManager from '../../components/layoutManager'; import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; import globalize from '../../scripts/globalize'; -type GenresItemsContainerProps = { +interface GenresItemsContainerI { topParentId?: string | null; getCurrentViewStyle: () => string; itemsResult?: BaseItemDtoQueryResult; } -const GenresItemsContainer: FunctionComponent = ({ topParentId, getCurrentViewStyle, itemsResult = {} }: GenresItemsContainerProps) => { +const GenresItemsContainer: FC = ({ topParentId, getCurrentViewStyle, itemsResult = {} }) => { const element = useRef(null); const enableScrollX = useCallback(() => { diff --git a/src/view/components/ItemsContainer.tsx b/src/view/components/ItemsContainer.tsx index 6a6b24d03..84354aafc 100644 --- a/src/view/components/ItemsContainer.tsx +++ b/src/view/components/ItemsContainer.tsx @@ -7,11 +7,11 @@ 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'; +import { QueryI } from './interface'; type ItemsContainerProps = { getCurrentViewStyle: () => string; - query: IQuery; + query: QueryI; getContext: () => string | null; items?: BaseItemDto[] | null; noItemsMessage?: string; diff --git a/src/view/components/NewCollection.tsx b/src/view/components/NewCollection.tsx index d77329571..28ade67fb 100644 --- a/src/view/components/NewCollection.tsx +++ b/src/view/components/NewCollection.tsx @@ -1,8 +1,8 @@ -import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; import IconButtonElement from '../../elements/IconButtonElement'; -const NewCollection: FunctionComponent = () => { +const NewCollection: FC = () => { const element = useRef(null); const showCollectionEditor = useCallback(() => { diff --git a/src/view/components/Pagination.tsx b/src/view/components/Pagination.tsx index 81d8d6770..37eb168ea 100644 --- a/src/view/components/Pagination.tsx +++ b/src/view/components/Pagination.tsx @@ -1,16 +1,16 @@ import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; import IconButtonElement from '../../elements/IconButtonElement'; import globalize from '../../scripts/globalize'; -import { IQuery } from './type'; +import { QueryI } from './interface'; -type PaginationProps = { - query: IQuery; +interface PaginationI { + query: QueryI; itemsResult?: BaseItemDtoQueryResult; reloadItems: () => void; } -const Pagination: FunctionComponent = ({ query, itemsResult = {}, reloadItems }: PaginationProps) => { +const Pagination: FC = ({ query, itemsResult = {}, reloadItems }) => { const startIndex = query.StartIndex; const limit = query.Limit; const totalRecordCount = itemsResult.TotalRecordCount || 0; diff --git a/src/view/components/RecommendationContainer.tsx b/src/view/components/RecommendationContainer.tsx index 75756a8a1..836e2126f 100644 --- a/src/view/components/RecommendationContainer.tsx +++ b/src/view/components/RecommendationContainer.tsx @@ -1,17 +1,17 @@ import { RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent } from 'react'; +import React, { FC } from 'react'; import globalize from '../../scripts/globalize'; import escapeHTML from 'escape-html'; import SectionContainer from './SectionContainer'; -type RecommendationContainerProps = { +interface RecommendationContainerI { getPortraitShape: () => string; enableScrollX: () => boolean; recommendation?: RecommendationDto; } -const RecommendationContainer: FunctionComponent = ({ getPortraitShape, enableScrollX, recommendation = {} }: RecommendationContainerProps) => { +const RecommendationContainer: FC = ({ getPortraitShape, enableScrollX, recommendation = {} }) => { let title = ''; switch (recommendation.RecommendationType) { diff --git a/src/view/components/SectionContainer.tsx b/src/view/components/SectionContainer.tsx index 2268fe292..eb9daacb1 100644 --- a/src/view/components/SectionContainer.tsx +++ b/src/view/components/SectionContainer.tsx @@ -1,26 +1,26 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer'; import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useEffect, useRef } from 'react'; +import React, { FC, useEffect, useRef } from 'react'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; import ItemsContainerElement from '../../elements/ItemsContainerElement'; import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; -import { ICardOptions } from './type'; +import { CardOptionsI } from './interface'; -type SectionContainerProps = { +interface SectionContainerI { sectionTitle: string; enableScrollX: () => boolean; items?: BaseItemDto[]; - cardOptions?: ICardOptions; + cardOptions?: CardOptionsI; } -const SectionContainer: FunctionComponent = ({ +const SectionContainer: FC = ({ sectionTitle, enableScrollX, items = [], cardOptions = {} -}: SectionContainerProps) => { +}) => { const element = useRef(null); useEffect(() => { diff --git a/src/view/components/SelectView.tsx b/src/view/components/SelectView.tsx index c1f2633c5..4a308c11d 100644 --- a/src/view/components/SelectView.tsx +++ b/src/view/components/SelectView.tsx @@ -1,18 +1,18 @@ -import React, { FunctionComponent, useEffect, useRef } from 'react'; +import React, { FC, 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'; +import { QueryI } from './interface'; -type SelectViewProps = { +interface SelectViewI { getCurrentViewStyle: () => string; - query: IQuery; + query: QueryI; getViewSettings: () => string; reloadItems: () => void; } -const SelectView: FunctionComponent = ({ getCurrentViewStyle, getViewSettings, query, reloadItems }: SelectViewProps) => { +const SelectView: FC = ({ getCurrentViewStyle, getViewSettings, query, reloadItems }) => { const element = useRef(null); useEffect(() => { diff --git a/src/view/components/Shuffle.tsx b/src/view/components/Shuffle.tsx index 3ff64253f..35da1f7f9 100644 --- a/src/view/components/Shuffle.tsx +++ b/src/view/components/Shuffle.tsx @@ -1,15 +1,15 @@ import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; import { playbackManager } from '../../components/playback/playbackmanager'; import IconButtonElement from '../../elements/IconButtonElement'; -type ShuffleProps = { +interface ShuffleI { itemsResult?: BaseItemDtoQueryResult; topParentId: string | null; } -const Shuffle: FunctionComponent = ({ itemsResult = {}, topParentId }: ShuffleProps) => { +const Shuffle: FC = ({ itemsResult = {}, topParentId }) => { const element = useRef(null); const shuffle = useCallback(() => { diff --git a/src/view/components/Sort.tsx b/src/view/components/Sort.tsx index 47f155307..770b466df 100644 --- a/src/view/components/Sort.tsx +++ b/src/view/components/Sort.tsx @@ -1,20 +1,20 @@ -import React, { FunctionComponent, useEffect, useRef } from 'react'; +import React, { FC, 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'; +import { QueryI } from './interface'; -type SortProps = { +interface SortI { getSortMenuOptions: () => { name: string; id: string; }[]; - query: IQuery; + query: QueryI; getSettingsKey: () => string; reloadItems: () => void; } -const Sort: FunctionComponent = ({ getSortMenuOptions, query, getSettingsKey, reloadItems }: SortProps) => { +const Sort: FC = ({ getSortMenuOptions, query, getSettingsKey, reloadItems }) => { const element = useRef(null); useEffect(() => { diff --git a/src/view/components/ViewItemsContainer.tsx b/src/view/components/ViewItemsContainer.tsx index 974f2c4fc..50c97c30e 100644 --- a/src/view/components/ViewItemsContainer.tsx +++ b/src/view/components/ViewItemsContainer.tsx @@ -1,5 +1,5 @@ import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import loading from '../../components/loading/loading'; import * as userSettings from '../../scripts/settings/userSettings'; @@ -10,10 +10,10 @@ import Pagination from './Pagination'; import SelectView from './SelectView'; import Shuffle from './Shuffle'; import Sort from './Sort'; -import { IQuery } from './type'; +import { QueryI } from './interface'; import NewCollection from './NewCollection'; -type IProps = { +interface ViewItemsContainerI { topParentId: string | null; isBtnShuffleEnabled?: boolean; isBtnFilterEnabled?: boolean; @@ -29,7 +29,7 @@ type IProps = { getNoItemsMessage: () => string; } -const ViewItemsContainer: FunctionComponent = ({ +const ViewItemsContainer: FC = ({ topParentId, isBtnShuffleEnabled = false, isBtnFilterEnabled = true, @@ -40,7 +40,7 @@ const ViewItemsContainer: FunctionComponent = ({ getItemTypes, getSortMenuOptions, getNoItemsMessage -}: IProps) => { +}) => { const [ itemsResult, setItemsResult ] = useState({}); const element = useRef(null); @@ -53,7 +53,7 @@ const ViewItemsContainer: FunctionComponent = ({ return `${getSettingsKey()} -view`; }, [getSettingsKey]); - let query = useMemo(() => ({ + let query = useMemo(() => ({ SortBy: 'SortName,ProductionYear', SortOrder: 'Ascending', IncludeItemTypes: getItemTypes(), diff --git a/src/view/components/type.ts b/src/view/components/interface.ts similarity index 96% rename from src/view/components/type.ts rename to src/view/components/interface.ts index bcec02111..bb68ff8dd 100644 --- a/src/view/components/type.ts +++ b/src/view/components/interface.ts @@ -1,4 +1,4 @@ -export type IQuery = { +export type QueryI = { SortBy?: string; SortOrder?: string; IncludeItemTypes?: string; @@ -16,7 +16,7 @@ export type IQuery = { NameStartsWith?: string; } -export type ICardOptions = { +export type CardOptionsI = { itemsContainer?: HTMLElement; parentContainer?: HTMLElement; allowBottomPadding?: boolean; diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx index 17bd14cf5..dd4000c05 100644 --- a/src/view/movies/CollectionsView.tsx +++ b/src/view/movies/CollectionsView.tsx @@ -1,13 +1,13 @@ -import React, { FunctionComponent, useCallback } from 'react'; +import React, { FC, useCallback } from 'react'; import globalize from '../../scripts/globalize'; import ViewItemsContainer from '../components/ViewItemsContainer'; -type IProps = { +interface CollectionsViewI { topParentId: string | null; } -const CollectionsView: FunctionComponent = ({ topParentId }: IProps) => { +const CollectionsView: FC = ({ topParentId }) => { const getBasekey = useCallback(() => { return 'collections'; }, []); diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx index 8ec222af7..3e203a544 100644 --- a/src/view/movies/FavoritesView.tsx +++ b/src/view/movies/FavoritesView.tsx @@ -1,13 +1,13 @@ -import React, { FunctionComponent, useCallback } from 'react'; +import React, { FC, useCallback } from 'react'; import globalize from '../../scripts/globalize'; import ViewItemsContainer from '../components/ViewItemsContainer'; -type IProps = { +interface FavoritesViewI { topParentId: string | null; } -const FavoritesView: FunctionComponent = ({ topParentId }: IProps) => { +const FavoritesView: FC = ({ topParentId }) => { const getBasekey = useCallback(() => { return 'favorites'; }, []); diff --git a/src/view/movies/GenresView.tsx b/src/view/movies/GenresView.tsx index b75dc9e96..b61b97b76 100644 --- a/src/view/movies/GenresView.tsx +++ b/src/view/movies/GenresView.tsx @@ -1,16 +1,16 @@ import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { FC, 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'; +import { QueryI } from '../components/interface'; -type IProps = { +interface GenresViewI { topParentId: string | null; } -const GenresView: FunctionComponent = ({ topParentId }: IProps) => { +const GenresView: FC = ({ topParentId }) => { const [ itemsResult, setItemsResult ] = useState({}); const element = useRef(null); @@ -22,7 +22,7 @@ const GenresView: FunctionComponent = ({ topParentId }: IProps) => { return getSettingsKey() + '-view'; }, [getSettingsKey]); - let query = useMemo(() => ({ + let query = useMemo(() => ({ SortBy: 'SortName', SortOrder: 'Ascending', IncludeItemTypes: 'Movie', diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx index 3dd2b3caf..bd92b9d62 100644 --- a/src/view/movies/MoviesView.tsx +++ b/src/view/movies/MoviesView.tsx @@ -1,13 +1,13 @@ -import React, { FunctionComponent, useCallback } from 'react'; +import React, { FC, useCallback } from 'react'; import globalize from '../../scripts/globalize'; import ViewItemsContainer from '../components/ViewItemsContainer'; -type IProps = { +interface MoviesViewI { topParentId: string | null; } -const MoviesView: FunctionComponent = ({ topParentId }: IProps) => { +const MoviesView: FC = ({ topParentId }) => { const getBasekey = useCallback(() => { return 'movies'; }, []); diff --git a/src/view/movies/SuggestionsView.tsx b/src/view/movies/SuggestionsView.tsx index 1d9e77825..5bd73f0ae 100644 --- a/src/view/movies/SuggestionsView.tsx +++ b/src/view/movies/SuggestionsView.tsx @@ -8,11 +8,11 @@ import globalize from '../../scripts/globalize'; import RecommendationContainer from '../components/RecommendationContainer'; import SectionContainer from '../components/SectionContainer'; -type IProps = { +interface SuggestionsViewI { topParentId: string | null; } -const SuggestionsView: FunctionComponent = (props: IProps) => { +const SuggestionsView: FunctionComponent = ({topParentId}) => { const [ latestItems, setLatestItems ] = useState([]); const [ resumeResult, setResumeResult ] = useState({}); const [ recommendations, setRecommendations ] = useState([]); @@ -102,12 +102,12 @@ const SuggestionsView: FunctionComponent = (props: IProps) => { }, [autoFocus]); const loadSuggestionsTab = useCallback((view) => { - const parentId = props.topParentId; + const parentId = topParentId; const userId = window.ApiClient.getCurrentUserId(); loadResume(view, userId, parentId); loadLatest(view, userId, parentId); loadSuggestions(view, userId); - }, [loadLatest, loadResume, loadSuggestions, props.topParentId]); + }, [loadLatest, loadResume, loadSuggestions, topParentId]); useEffect(() => { const page = element.current; diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx index 9cce2ebe4..8ed076e98 100644 --- a/src/view/movies/TrailersView.tsx +++ b/src/view/movies/TrailersView.tsx @@ -1,14 +1,14 @@ -import React, { FunctionComponent, useCallback } from 'react'; +import React, { FC, useCallback } from 'react'; import globalize from '../../scripts/globalize'; import ViewItemsContainer from '../components/ViewItemsContainer'; -type IProps = { +interface TrailersViewI { topParentId: string | null; } -const TrailersView: FunctionComponent = ({ topParentId }: IProps) => { +const TrailersView: FC = ({ topParentId }) => { const getBasekey = useCallback(() => { return 'trailers'; }, []); From df4de415b2418c2f8f70c54304c5a67d32e8ffd8 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Mon, 3 Oct 2022 01:23:59 +0300 Subject: [PATCH 12/23] use sortmenu instead of libraryBrowse .showSortMenu --- src/view/components/ItemsContainer.tsx | 6 +- src/view/components/Sort.tsx | 50 ++++----- src/view/components/ViewItemsContainer.tsx | 113 +++++++++++++++------ src/view/movies/CollectionsView.tsx | 12 --- src/view/movies/FavoritesView.tsx | 36 ------- src/view/movies/MoviesView.tsx | 36 ------- src/view/movies/TrailersView.tsx | 27 ----- 7 files changed, 113 insertions(+), 167 deletions(-) diff --git a/src/view/components/ItemsContainer.tsx b/src/view/components/ItemsContainer.tsx index 84354aafc..806cad279 100644 --- a/src/view/components/ItemsContainer.tsx +++ b/src/view/components/ItemsContainer.tsx @@ -1,5 +1,5 @@ import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useEffect, useRef } from 'react'; +import React, { FC, useEffect, useRef } from 'react'; import ItemsContainerElement from '../../elements/ItemsContainerElement'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; @@ -9,7 +9,7 @@ import imageLoader from '../../components/images/imageLoader'; import '../../elements/emby-itemscontainer/emby-itemscontainer'; import { QueryI } from './interface'; -type ItemsContainerProps = { +interface ItemsContainerI { getCurrentViewStyle: () => string; query: QueryI; getContext: () => string | null; @@ -17,7 +17,7 @@ type ItemsContainerProps = { noItemsMessage?: string; } -const ItemsContainer: FunctionComponent = ({ getCurrentViewStyle, query, getContext, items = [], noItemsMessage }: ItemsContainerProps) => { +const ItemsContainer: FC = ({ getCurrentViewStyle, query, getContext, items = [], noItemsMessage }) => { const element = useRef(null); const viewStyle = getCurrentViewStyle(); diff --git a/src/view/components/Sort.tsx b/src/view/components/Sort.tsx index 770b466df..43c5125f1 100644 --- a/src/view/components/Sort.tsx +++ b/src/view/components/Sort.tsx @@ -1,40 +1,44 @@ -import React, { FC, useEffect, useRef } from 'react'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; import IconButtonElement from '../../elements/IconButtonElement'; -import libraryBrowser from '../../scripts/libraryBrowser'; -import * as userSettings from '../../scripts/settings/userSettings'; -import { QueryI } from './interface'; interface SortI { getSortMenuOptions: () => { name: string; - id: string; - }[]; - query: QueryI; + value: string; + }[] + getSortValues: () => { + sortBy: string; + sortOrder: string; + } getSettingsKey: () => string; reloadItems: () => void; } -const Sort: FC = ({ getSortMenuOptions, query, getSettingsKey, reloadItems }) => { +const Sort: FC = ({ getSortMenuOptions, getSortValues, getSettingsKey, reloadItems }) => { const element = useRef(null); + const showSortMenu = useCallback(() => { + import('../../components/sortmenu/sortmenu').then(({default: SortMenu}) => { + const sortMenu = new SortMenu(); + sortMenu.show({ + settingsKey: getSettingsKey(), + settings: getSortValues(), + sortOptions: getSortMenuOptions() + }).then(() => { + reloadItems(); + }); + }); + }, [getSettingsKey, getSortMenuOptions, getSortValues, reloadItems]); + useEffect(() => { const btnSort = element.current?.querySelector('.btnSort'); - if (btnSort) { - btnSort.addEventListener('click', (e) => { - libraryBrowser.showSortMenu({ - items: getSortMenuOptions(), - callback: () => { - query.StartIndex = 0; - userSettings.saveQuerySettings(getSettingsKey(), query); - reloadItems(); - }, - query: query, - button: e.target - }); - }); - } - }, [getSortMenuOptions, query, reloadItems, getSettingsKey]); + btnSort?.addEventListener('click', showSortMenu); + + return () => { + btnSort?.removeEventListener('click', showSortMenu); + }; + }, [showSortMenu]); return (
diff --git a/src/view/components/ViewItemsContainer.tsx b/src/view/components/ViewItemsContainer.tsx index 50c97c30e..909da7be7 100644 --- a/src/view/components/ViewItemsContainer.tsx +++ b/src/view/components/ViewItemsContainer.tsx @@ -1,5 +1,5 @@ import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import loading from '../../components/loading/loading'; import * as userSettings from '../../scripts/settings/userSettings'; @@ -12,6 +12,7 @@ import Shuffle from './Shuffle'; import Sort from './Sort'; import { QueryI } from './interface'; import NewCollection from './NewCollection'; +import globalize from '../../scripts/globalize'; interface ViewItemsContainerI { topParentId: string | null; @@ -22,10 +23,6 @@ interface ViewItemsContainerI { getBasekey: () => string; getFilterMode: () => string; getItemTypes: () => string; - getSortMenuOptions: () => { - name: string; - id: string; - }[]; getNoItemsMessage: () => string; } @@ -38,7 +35,6 @@ const ViewItemsContainer: FC = ({ getBasekey, getFilterMode, getItemTypes, - getSortMenuOptions, getNoItemsMessage }) => { const [ itemsResult, setItemsResult ] = useState({}); @@ -53,24 +49,74 @@ const ViewItemsContainer: FC = ({ return `${getSettingsKey()} -view`; }, [getSettingsKey]); - let query = useMemo(() => ({ - SortBy: 'SortName,ProductionYear', - SortOrder: 'Ascending', - IncludeItemTypes: getItemTypes(), - Recursive: true, - Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', - Limit: userSettings.libraryPageSize(undefined), - StartIndex: 0, - ParentId: topParentId - }), [getItemTypes, topParentId]); + const getDefaultSortBy = useCallback(() => { + return 'SortName'; + }, []); - if (getBasekey() === 'favorites') { - query.IsFavorite = true; - } + const getSortValues = useCallback(() => { + const basekey = getSettingsKey(); - query = userSettings.loadQuerySettings(getSettingsKey(), query); + return { + sortBy: userSettings.getFilter(basekey + '-sortby') || getDefaultSortBy(), + sortOrder: userSettings.getFilter(basekey + '-sortorder') === 'Descending' ? 'Descending' : 'Ascending' + }; + }, [getDefaultSortBy, getSettingsKey]); + + const getQuery = useCallback(() => { + const query: QueryI = { + SortBy: getSortValues().sortBy, + SortOrder: getSortValues().sortOrder, + IncludeItemTypes: getItemTypes(), + Recursive: true, + Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + Limit: userSettings.libraryPageSize(undefined), + StartIndex: 0, + ParentId: topParentId + }; + + if (getBasekey() === 'favorites') { + query.IsFavorite = true; + } + + userSettings.loadQuerySettings(getSettingsKey(), query); + return query; + }, [getSortValues, getItemTypes, topParentId, getBasekey, getSettingsKey]); + + const getSortMenuOptions = useCallback(() => { + 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 getCurrentViewStyle = useCallback(() => { return userSettings.get(getViewSettings(), false) || 'Poster'; @@ -93,6 +139,7 @@ const ViewItemsContainer: FC = ({ return; } loading.show(); + const query = getQuery(); window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { setItemsResult(result); window.scrollTo(0, 0); @@ -103,7 +150,7 @@ const ViewItemsContainer: FC = ({ autoFocuser.autoFocus(page); }); }); - }, [query]); + }, [getQuery]); useEffect(() => { reloadItems(); @@ -112,30 +159,36 @@ const ViewItemsContainer: FC = ({ return (
- + {isBtnShuffleEnabled && } - - + - {isBtnFilterEnabled && } + + + {isBtnFilterEnabled && } {isBtnNewCollectionEnabled && }
- {isAlphaPickerEnabled && } + {isAlphaPickerEnabled && }
- +
); diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx index dd4000c05..059f6b052 100644 --- a/src/view/movies/CollectionsView.tsx +++ b/src/view/movies/CollectionsView.tsx @@ -1,6 +1,5 @@ import React, { FC, useCallback } from 'react'; -import globalize from '../../scripts/globalize'; import ViewItemsContainer from '../components/ViewItemsContainer'; interface CollectionsViewI { @@ -24,16 +23,6 @@ const CollectionsView: FC = ({ topParentId }) => { return 'MessageNoCollectionsAvailable'; }, []); - const getSortMenuOptions = useCallback(() => { - return [{ - name: globalize.translate('Name'), - id: 'SortName' - }, { - name: globalize.translate('OptionDateAdded'), - id: 'DateCreated,SortName' - }]; - }, []); - return ( = ({ topParentId }) => { getBasekey={getBasekey} getFilterMode={getFilterMode} getItemTypes={getItemTypes} - getSortMenuOptions={getSortMenuOptions} getNoItemsMessage={getNoItemsMessage} /> ); diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx index 3e203a544..4254578b7 100644 --- a/src/view/movies/FavoritesView.tsx +++ b/src/view/movies/FavoritesView.tsx @@ -1,6 +1,5 @@ import React, { FC, useCallback } from 'react'; -import globalize from '../../scripts/globalize'; import ViewItemsContainer from '../components/ViewItemsContainer'; interface FavoritesViewI { @@ -24,47 +23,12 @@ const FavoritesView: FC = ({ topParentId }) => { return 'MessageNoFavoritesAvailable'; }, []); - const getSortMenuOptions = useCallback(() => { - 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' - }]; - }, []); - return ( ); diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx index bd92b9d62..a5f2c69e2 100644 --- a/src/view/movies/MoviesView.tsx +++ b/src/view/movies/MoviesView.tsx @@ -1,5 +1,4 @@ import React, { FC, useCallback } from 'react'; -import globalize from '../../scripts/globalize'; import ViewItemsContainer from '../components/ViewItemsContainer'; @@ -24,40 +23,6 @@ const MoviesView: FC = ({ topParentId }) => { return 'MessageNoItemsAvailable'; }, []); - const getSortMenuOptions = useCallback(() => { - 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' - }]; - }, []); - return ( = ({ topParentId }) => { getBasekey={getBasekey} getFilterMode={getFilterMode} getItemTypes={getItemTypes} - getSortMenuOptions={getSortMenuOptions} getNoItemsMessage={getNoItemsMessage} /> ); diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx index 8ed076e98..4896e6e77 100644 --- a/src/view/movies/TrailersView.tsx +++ b/src/view/movies/TrailersView.tsx @@ -1,7 +1,6 @@ import React, { FC, useCallback } from 'react'; -import globalize from '../../scripts/globalize'; import ViewItemsContainer from '../components/ViewItemsContainer'; interface TrailersViewI { @@ -25,38 +24,12 @@ const TrailersView: FC = ({ topParentId }) => { return 'MessageNoTrailersFound'; }, []); - const getSortMenuOptions = useCallback(() => { - 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' - }]; - }, []); - return ( ); From 0acac1b52d6dbcda84fffea7c12e7691d1d97c6d Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Mon, 3 Oct 2022 01:44:54 +0300 Subject: [PATCH 13/23] use interface export --- src/view/components/interface.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/components/interface.ts b/src/view/components/interface.ts index bb68ff8dd..c05945ef6 100644 --- a/src/view/components/interface.ts +++ b/src/view/components/interface.ts @@ -1,4 +1,4 @@ -export type QueryI = { +export interface QueryI { SortBy?: string; SortOrder?: string; IncludeItemTypes?: string; @@ -16,7 +16,7 @@ export type QueryI = { NameStartsWith?: string; } -export type CardOptionsI = { +export interface CardOptionsI { itemsContainer?: HTMLElement; parentContainer?: HTMLElement; allowBottomPadding?: boolean; From 0dc9ad8904e4c705cddfd836fcf34489a85beffa Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Mon, 3 Oct 2022 02:33:43 +0300 Subject: [PATCH 14/23] use filtermenu --- src/view/components/Filter.tsx | 51 ++++--- src/view/components/ViewItemsContainer.tsx | 156 +++++++++++++++++++-- src/view/components/interface.ts | 31 ++++ src/view/movies/CollectionsView.tsx | 7 +- src/view/movies/FavoritesView.tsx | 7 +- src/view/movies/MoviesView.tsx | 7 +- src/view/movies/TrailersView.tsx | 7 +- 7 files changed, 215 insertions(+), 51 deletions(-) diff --git a/src/view/components/Filter.tsx b/src/view/components/Filter.tsx index 248b6c9fb..becaf0e61 100644 --- a/src/view/components/Filter.tsx +++ b/src/view/components/Filter.tsx @@ -1,38 +1,53 @@ import React, { FC, useCallback, useEffect, useRef } from 'react'; -import { Events } from 'jellyfin-apiclient'; import IconButtonElement from '../../elements/IconButtonElement'; -import { QueryI } from './interface'; +import { FiltersI } from './interface'; interface FilterI { - query: QueryI; - getFilterMode: () => string | null; + topParentId?: string | null; + getItemTypes: () => string[]; + getFilters: () => FiltersI; + getSettingsKey: () => string; + getFilterMenuOptions: () => Record; + getVisibleFilters: () => string[]; reloadItems: () => void; } -const Filter: FC = ({ query, getFilterMode, reloadItems }) => { +const Filter: FC = ({ + topParentId, + getItemTypes, + getSettingsKey, + getFilters, + getVisibleFilters, + getFilterMenuOptions, + reloadItems +}) => { const element = useRef(null); const showFilterMenu = useCallback(() => { - import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => { - const filterDialog = new filterDialogFactory({ - query: query, - mode: getFilterMode(), - serverId: window.ApiClient.serverId() - }); - Events.on(filterDialog, 'filterchange', () => { - query.StartIndex = 0; + import('../../components/filtermenu/filtermenu').then(({default: FilterMenu}) => { + const filterMenu = new FilterMenu(); + filterMenu.show({ + settingsKey: getSettingsKey(), + settings: getFilters(), + visibleSettings: getVisibleFilters(), + parentId: topParentId, + itemTypes: getItemTypes(), + serverId: window.ApiClient.serverId(), + filterMenuOptions: getFilterMenuOptions() + }).then(() => { reloadItems(); }); - filterDialog.show(); }); - }, [getFilterMode, query, reloadItems]); + }, [getSettingsKey, getFilters, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, reloadItems]); useEffect(() => { const btnFilter = element.current?.querySelector('.btnFilter'); - if (btnFilter) { - btnFilter.addEventListener('click', showFilterMenu); - } + btnFilter?.addEventListener('click', showFilterMenu); + + return () => { + btnFilter?.removeEventListener('click', showFilterMenu); + }; }, [showFilterMenu]); return ( diff --git a/src/view/components/ViewItemsContainer.tsx b/src/view/components/ViewItemsContainer.tsx index 909da7be7..10a1046da 100644 --- a/src/view/components/ViewItemsContainer.tsx +++ b/src/view/components/ViewItemsContainer.tsx @@ -21,8 +21,7 @@ interface ViewItemsContainerI { isBtnNewCollectionEnabled?: boolean; isAlphaPickerEnabled?: boolean; getBasekey: () => string; - getFilterMode: () => string; - getItemTypes: () => string; + getItemTypes: () => string[]; getNoItemsMessage: () => string; } @@ -33,7 +32,6 @@ const ViewItemsContainer: FC = ({ isBtnNewCollectionEnabled = false, isAlphaPickerEnabled = true, getBasekey, - getFilterMode, getItemTypes, getNoItemsMessage }) => { @@ -62,11 +60,52 @@ const ViewItemsContainer: FC = ({ }; }, [getDefaultSortBy, getSettingsKey]); + const getFilters = useCallback(() => { + const basekey = getSettingsKey(); + return { + IsPlayed: userSettings.getFilter(basekey + '-filter-IsPlayed') === 'true', + IsUnplayed: userSettings.getFilter(basekey + '-filter-IsUnplayed') === 'true', + IsFavorite: userSettings.getFilter(basekey + '-filter-IsFavorite') === 'true', + IsResumable: userSettings.getFilter(basekey + '-filter-IsResumable') === 'true', + Is4K: userSettings.getFilter(basekey + '-filter-Is4K') === 'true', + IsHD: userSettings.getFilter(basekey + '-filter-IsHD') === 'true', + IsSD: userSettings.getFilter(basekey + '-filter-IsSD') === 'true', + Is3D: userSettings.getFilter(basekey + '-filter-Is3D') === 'true', + VideoTypes: userSettings.getFilter(basekey + '-filter-VideoTypes'), + SeriesStatus: userSettings.getFilter(basekey + '-filter-SeriesStatus'), + HasSubtitles: userSettings.getFilter(basekey + '-filter-HasSubtitles'), + HasTrailer: userSettings.getFilter(basekey + '-filter-HasTrailer'), + HasSpecialFeature: userSettings.getFilter(basekey + '-filter-HasSpecialFeature'), + HasThemeSong: userSettings.getFilter(basekey + '-filter-HasThemeSong'), + HasThemeVideo: userSettings.getFilter(basekey + '-filter-HasThemeVideo'), + GenreIds: userSettings.getFilter(basekey + '-filter-GenreIds') + }; + }, [getSettingsKey]); + + const getFilterMenuOptions = useCallback(() => { + return {}; + }, []); + + const getVisibleFilters = useCallback(() => { + return [ + 'IsUnplayed', + 'IsPlayed', + 'IsFavorite', + 'IsResumable', + 'VideoType', + 'HasSubtitles', + 'HasTrailer', + 'HasSpecialFeature', + 'HasThemeSong', + 'HasThemeVideo' + ]; + }, []); + const getQuery = useCallback(() => { const query: QueryI = { SortBy: getSortValues().sortBy, SortOrder: getSortValues().sortOrder, - IncludeItemTypes: getItemTypes(), + IncludeItemTypes: getItemTypes().join(','), Recursive: true, Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', ImageTypeLimit: 1, @@ -84,6 +123,97 @@ const ViewItemsContainer: FC = ({ return query; }, [getSortValues, getItemTypes, topParentId, getBasekey, getSettingsKey]); + const getQueryWithFilters = useCallback(() => { + const query = getQuery(); + + const queryFilters = []; + let hasFilters; + + const filters = getFilters(); + + if (filters.IsPlayed) { + queryFilters.push('IsPlayed'); + hasFilters = true; + } + + if (filters.IsUnplayed) { + queryFilters.push('IsUnplayed'); + hasFilters = true; + } + + if (filters.IsFavorite) { + queryFilters.push('IsFavorite'); + hasFilters = true; + } + + if (filters.IsResumable) { + queryFilters.push('IsResumable'); + hasFilters = true; + } + + if (filters.VideoTypes) { + hasFilters = true; + query.VideoTypes = filters.VideoTypes; + } + + if (filters.GenreIds) { + hasFilters = true; + query.GenreIds = filters.GenreIds; + } + + if (filters.Is4K) { + query.Is4K = true; + hasFilters = true; + } + + if (filters.IsHD) { + query.IsHD = true; + hasFilters = true; + } + + if (filters.IsSD) { + query.IsHD = false; + hasFilters = true; + } + + if (filters.Is3D) { + query.Is3D = true; + hasFilters = true; + } + + if (filters.HasSubtitles) { + query.HasSubtitles = true; + hasFilters = true; + } + + if (filters.HasTrailer) { + query.HasTrailer = true; + hasFilters = true; + } + + if (filters.HasSpecialFeature) { + query.HasSpecialFeature = true; + hasFilters = true; + } + + if (filters.HasThemeSong) { + query.HasThemeSong = true; + hasFilters = true; + } + + if (filters.HasThemeVideo) { + query.HasThemeVideo = true; + hasFilters = true; + } + + query.Filters = queryFilters.length ? queryFilters.join(',') : null; + + return { + query: query, + hasFilters: hasFilters + }; + }, [getQuery, getFilters]); + const getSortMenuOptions = useCallback(() => { return [{ name: globalize.translate('Name'), @@ -123,7 +253,7 @@ const ViewItemsContainer: FC = ({ }, [getViewSettings]); const getContext = useCallback(() => { - const itemType = getItemTypes(); + const itemType = getItemTypes().join(','); if (itemType === 'Movie' || itemType === 'BoxSet') { return 'movies'; } @@ -139,8 +269,8 @@ const ViewItemsContainer: FC = ({ return; } loading.show(); - const query = getQuery(); - window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { + const querywithfilters = getQueryWithFilters().query; + window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), querywithfilters).then((result) => { setItemsResult(result); window.scrollTo(0, 0); @@ -150,7 +280,7 @@ const ViewItemsContainer: FC = ({ autoFocuser.autoFocus(page); }); }); - }, [getQuery]); + }, [getQueryWithFilters]); useEffect(() => { reloadItems(); @@ -171,7 +301,15 @@ const ViewItemsContainer: FC = ({ reloadItems={reloadItems} /> - {isBtnFilterEnabled && } + {isBtnFilterEnabled && } {isBtnNewCollectionEnabled && } diff --git a/src/view/components/interface.ts b/src/view/components/interface.ts index c05945ef6..ba1e459ce 100644 --- a/src/view/components/interface.ts +++ b/src/view/components/interface.ts @@ -12,8 +12,39 @@ export interface QueryI { IsFavorite?: boolean; IsMissing?: boolean; Limit:number; + NameStartsWithOrGreater?: string; NameLessThan?: string; NameStartsWith?: string; + VideoTypes?: string; + GenreIds?: string; + Is4K?: boolean; + IsHD?: boolean; + Is3D?: boolean; + HasSubtitles?: boolean; + HasTrailer?: boolean; + HasSpecialFeature?: boolean; + HasThemeSong?: boolean; + HasThemeVideo?: boolean; + Filters?: string | null; +} + +export interface FiltersI { + IsPlayed: boolean; + IsUnplayed: boolean; + IsFavorite: boolean; + IsResumable: boolean; + Is4K: boolean; + IsHD: boolean; + IsSD: boolean; + Is3D: boolean; + VideoTypes: string; + SeriesStatus: string; + HasSubtitles: string; + HasTrailer: string; + HasSpecialFeature: string; + HasThemeSong: string; + HasThemeVideo: string; + GenreIds: string; } export interface CardOptionsI { diff --git a/src/view/movies/CollectionsView.tsx b/src/view/movies/CollectionsView.tsx index 059f6b052..81316ed07 100644 --- a/src/view/movies/CollectionsView.tsx +++ b/src/view/movies/CollectionsView.tsx @@ -11,12 +11,8 @@ const CollectionsView: FC = ({ topParentId }) => { return 'collections'; }, []); - const getFilterMode = useCallback(() => { - return 'movies'; - }, []); - const getItemTypes = useCallback(() => { - return 'BoxSet'; + return ['BoxSet']; }, []); const getNoItemsMessage = useCallback(() => { @@ -30,7 +26,6 @@ const CollectionsView: FC = ({ topParentId }) => { isBtnNewCollectionEnabled={true} isAlphaPickerEnabled={false} getBasekey={getBasekey} - getFilterMode={getFilterMode} getItemTypes={getItemTypes} getNoItemsMessage={getNoItemsMessage} /> diff --git a/src/view/movies/FavoritesView.tsx b/src/view/movies/FavoritesView.tsx index 4254578b7..d1619c345 100644 --- a/src/view/movies/FavoritesView.tsx +++ b/src/view/movies/FavoritesView.tsx @@ -11,12 +11,8 @@ const FavoritesView: FC = ({ topParentId }) => { return 'favorites'; }, []); - const getFilterMode = useCallback(() => { - return 'movies'; - }, []); - const getItemTypes = useCallback(() => { - return 'Movie'; + return ['Movie']; }, []); const getNoItemsMessage = useCallback(() => { @@ -27,7 +23,6 @@ const FavoritesView: FC = ({ topParentId }) => { diff --git a/src/view/movies/MoviesView.tsx b/src/view/movies/MoviesView.tsx index a5f2c69e2..30074ac31 100644 --- a/src/view/movies/MoviesView.tsx +++ b/src/view/movies/MoviesView.tsx @@ -11,12 +11,8 @@ const MoviesView: FC = ({ topParentId }) => { return 'movies'; }, []); - const getFilterMode = useCallback(() => { - return 'movies'; - }, []); - const getItemTypes = useCallback(() => { - return 'Movie'; + return ['Movie']; }, []); const getNoItemsMessage = useCallback(() => { @@ -28,7 +24,6 @@ const MoviesView: FC = ({ topParentId }) => { topParentId={topParentId} isBtnShuffleEnabled={true} getBasekey={getBasekey} - getFilterMode={getFilterMode} getItemTypes={getItemTypes} getNoItemsMessage={getNoItemsMessage} /> diff --git a/src/view/movies/TrailersView.tsx b/src/view/movies/TrailersView.tsx index 4896e6e77..958d591ad 100644 --- a/src/view/movies/TrailersView.tsx +++ b/src/view/movies/TrailersView.tsx @@ -12,12 +12,8 @@ const TrailersView: FC = ({ topParentId }) => { return 'trailers'; }, []); - const getFilterMode = useCallback(() => { - return 'movies'; - }, []); - const getItemTypes = useCallback(() => { - return 'Trailer'; + return ['Trailer']; }, []); const getNoItemsMessage = useCallback(() => { @@ -28,7 +24,6 @@ const TrailersView: FC = ({ topParentId }) => { From 1ac97c878a49c458154d3bea279ed981bb57f7e0 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 5 Oct 2022 02:44:28 +0300 Subject: [PATCH 15/23] use viewSettings instead of libraryBrowser.showLayoutMenu --- src/components/viewSettings/viewSettings.js | 12 +- .../viewSettings/viewSettings.template.html | 7 + src/strings/en-us.json | 1 + src/view/components/AlphaPickerContainer.tsx | 39 ++--- src/view/components/GenresItemsContainer.tsx | 2 +- src/view/components/ItemsContainer.tsx | 140 +++++++++--------- src/view/components/Pagination.tsx | 34 +++-- .../components/RecommendationContainer.tsx | 2 +- src/view/components/SectionContainer.tsx | 2 +- src/view/components/SelectView.tsx | 49 +++--- src/view/components/Shuffle.tsx | 2 +- src/view/components/ViewItemsContainer.tsx | 92 +++++++++--- src/view/components/interface.ts | 65 ++++++-- src/view/movies/GenresView.tsx | 2 +- src/view/movies/SuggestionsView.tsx | 6 +- 15 files changed, 287 insertions(+), 168 deletions(-) diff --git a/src/components/viewSettings/viewSettings.js b/src/components/viewSettings/viewSettings.js index c87256130..d6cbf2e92 100644 --- a/src/components/viewSettings/viewSettings.js +++ b/src/components/viewSettings/viewSettings.js @@ -57,7 +57,7 @@ function showIfAllowed(context, selector, visible) { class ViewSettings { show(options) { - return new Promise(function (resolve, reject) { + return new Promise(function (resolve) { const dialogOptions = { removeOnClose: true, scrollY: false @@ -99,8 +99,9 @@ class ViewSettings { initEditor(dlg, options.settings); dlg.querySelector('.selectImageType').addEventListener('change', function () { - showIfAllowed(dlg, '.chkTitleContainer', this.value !== 'list'); - showIfAllowed(dlg, '.chkYearContainer', this.value !== 'list'); + showIfAllowed(dlg, '.chkTitleContainer', this.value !== 'list' && this.value !== 'banner'); + showIfAllowed(dlg, '.chkYearContainer', this.value !== 'list' && this.value !== 'banner'); + showIfAllowed(dlg, '.chkCardLayoutContainer', this.value !== 'list' && this.value !== 'banner'); }); dlg.querySelector('.btnCancel').addEventListener('click', function () { @@ -126,11 +127,10 @@ class ViewSettings { if (submitted) { saveValues(dlg, options.settings, options.settingsKey); - resolve(); - return; + return resolve(); } - reject(); + return resolve(); }); }); } diff --git a/src/components/viewSettings/viewSettings.template.html b/src/components/viewSettings/viewSettings.template.html index 8aea2fac2..5989b3f83 100644 --- a/src/components/viewSettings/viewSettings.template.html +++ b/src/components/viewSettings/viewSettings.template.html @@ -35,6 +35,13 @@ ${GroupBySeries}
+ +
+ +
diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 7106cbcee..1487f8bf2 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -236,6 +236,7 @@ "EnableNextVideoInfoOverlayHelp": "At the end of a video, display info about the next video coming up in the current playlist.", "EnablePhotos": "Display the photos", "EnablePhotosHelp": "Images will be detected and displayed alongside other media files.", + "EnableCardLayout": "Display visual CardBox", "EnableRewatchingNextUp": "Enable Rewatching in Next Up", "EnableRewatchingNextUpHelp": "Enable showing already watched episodes in 'Next Up' sections.", "EnableQuickConnect": "Enable Quick Connect on this server", diff --git a/src/view/components/AlphaPickerContainer.tsx b/src/view/components/AlphaPickerContainer.tsx index b85e99e0b..57b87e0b5 100644 --- a/src/view/components/AlphaPickerContainer.tsx +++ b/src/view/components/AlphaPickerContainer.tsx @@ -1,33 +1,34 @@ -import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'; +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import AlphaPicker from '../../components/alphaPicker/alphaPicker'; -import { IQuery } from './type'; +import { AlphaPickerValueI, QueryI } from './interface'; -type AlphaPickerProps = { - query: IQuery; - reloadItems: () => void; -}; +interface AlphaPickerContainerI { + getQuery: () => QueryI + setAlphaPickerValue: React.Dispatch; + setStartIndex: React.Dispatch>; +} -const AlphaPickerContainer: FunctionComponent = ({ query, reloadItems }: AlphaPickerProps) => { +const AlphaPickerContainer: FC = ({ getQuery, setAlphaPickerValue, setStartIndex }) => { const [ alphaPicker, setAlphaPicker ] = useState(); const element = useRef(null); + const query = getQuery(); alphaPicker?.updateControls(query); const onAlphaPickerChange = useCallback((e) => { const newValue = (e as CustomEvent).detail.value; + let updatedValue; if (newValue === '#') { - query.NameLessThan = 'A'; - delete query.NameStartsWith; + updatedValue = {NameLessThan: 'A'}; } else { - query.NameStartsWith = newValue; - delete query.NameLessThan; + updatedValue = {NameStartsWith: newValue}; } - query.StartIndex = 0; - reloadItems(); - }, [query, reloadItems]); + setAlphaPickerValue(updatedValue); + setStartIndex(0); + }, [setStartIndex, setAlphaPickerValue]); useEffect(() => { - const alphaPickerElement = element.current?.querySelector('.alphaPicker'); + const alphaPickerElement = element.current; setAlphaPicker(new AlphaPicker({ element: alphaPickerElement, @@ -37,12 +38,14 @@ const AlphaPickerContainer: FunctionComponent = ({ query, relo if (alphaPickerElement) { alphaPickerElement.addEventListener('alphavaluechanged', onAlphaPickerChange); } + + return () => { + alphaPickerElement?.removeEventListener('alphavaluechanged', onAlphaPickerChange); + }; }, [onAlphaPickerChange]); return ( -
-
-
+
); }; diff --git a/src/view/components/GenresItemsContainer.tsx b/src/view/components/GenresItemsContainer.tsx index 1b90e49e7..9c8d26839 100644 --- a/src/view/components/GenresItemsContainer.tsx +++ b/src/view/components/GenresItemsContainer.tsx @@ -1,7 +1,7 @@ import '../../elements/emby-button/emby-button'; import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; import escapeHTML from 'escape-html'; import React, { FC, useCallback, useEffect, useRef } from 'react'; diff --git a/src/view/components/ItemsContainer.tsx b/src/view/components/ItemsContainer.tsx index 806cad279..3deb0829b 100644 --- a/src/view/components/ItemsContainer.tsx +++ b/src/view/components/ItemsContainer.tsx @@ -1,5 +1,5 @@ -import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FC, useEffect, useRef } from 'react'; +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; import ItemsContainerElement from '../../elements/ItemsContainerElement'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; @@ -7,82 +7,78 @@ import listview from '../../components/listview/listview'; import globalize from '../../scripts/globalize'; import imageLoader from '../../components/images/imageLoader'; import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import { QueryI } from './interface'; +import { CardOptionsI } from './interface'; interface ItemsContainerI { - getCurrentViewStyle: () => string; - query: QueryI; + getViewSettings: () => { + showTitle: string | boolean; + cardLayout: string | boolean; + showYear: string | boolean; + imageType: string; + viewType: string; + }; getContext: () => string | null; items?: BaseItemDto[] | null; noItemsMessage?: string; } -const ItemsContainer: FC = ({ getCurrentViewStyle, query, getContext, items = [], noItemsMessage }) => { +const ItemsContainer: FC = ({ getViewSettings, getContext, items = [], noItemsMessage }) => { const element = useRef(null); - const viewStyle = getCurrentViewStyle(); + const viewsettings = getViewSettings(); - useEffect(() => { - let html; + const getCardOptions = useCallback(() => { + let shape; + let preferThumb; + let preferDisc; + let preferLogo; - if (viewStyle == 'Thumb') { - html = cardBuilder.getCardsHtml(items, { - items: items, - shape: 'backdrop', - preferThumb: true, - context: getContext(), - 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: getContext(), - 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: getContext(), - lazy: true - }); - } else if (viewStyle == 'List') { + if (viewsettings.imageType === 'banner') { + shape = 'banner'; + } else if (viewsettings.imageType === 'disc') { + shape = 'square'; + preferDisc = true; + } else if (viewsettings.imageType === 'logo') { + shape = 'backdrop'; + preferLogo = true; + } else if (viewsettings.imageType === 'thumb') { + shape = 'backdrop'; + preferThumb = true; + } else { + shape = 'autoVertical'; + } + + const cardOptions: CardOptionsI = { + shape: shape, + showTitle: viewsettings.showTitle, + showYear: viewsettings.showTitle, + cardLayout: viewsettings.cardLayout, + centerText: true, + context: getContext(), + coverImage: true, + preferThumb: preferThumb, + preferDisc: preferDisc, + preferLogo: preferLogo, + overlayPlayButton: false, + overlayMoreButton: true, + overlayText: !viewsettings.showTitle + }; + + cardOptions.items = items; + + return cardOptions; + }, [getContext, items, viewsettings.cardLayout, viewsettings.imageType, viewsettings.showTitle]); + + const getItemsHtml = useCallback(() => { + const settings = getViewSettings(); + + let html = ''; + + if (settings.imageType === 'list') { html = listview.getListViewHtml({ items: items, - context: getContext(), - sortBy: query.SortBy - }); - } else if (viewStyle == 'PosterCard') { - html = cardBuilder.getCardsHtml(items, { - items: items, - shape: 'portrait', - context: getContext(), - showTitle: true, - showYear: true, - centerText: true, - lazy: true, - cardLayout: true - }); + context: getContext()}); } else { - html = cardBuilder.getCardsHtml(items, { - items: items, - shape: 'portrait', - context: getContext(), - overlayPlayButton: true, - showTitle: true, - showYear: true, - centerText: true - }); + html = cardBuilder.getCardsHtml(items, getCardOptions()); } if (!items?.length) { @@ -94,12 +90,16 @@ const ItemsContainer: FC = ({ getCurrentViewStyle, query, getCo html += '
'; } - const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement; - itemsContainer.innerHTML = html; - imageLoader.lazyChildren(itemsContainer); - }, [query.SortBy, items, noItemsMessage, viewStyle, getContext]); + return html; + }, [getCardOptions, getContext, getViewSettings, items, noItemsMessage]); - const cssClass = viewStyle == 'List' ? 'vertical-list' : 'vertical-wrap'; + useEffect(() => { + const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement; + itemsContainer.innerHTML = getItemsHtml(); + imageLoader.lazyChildren(itemsContainer); + }, [getItemsHtml]); + + const cssClass = viewsettings.imageType == 'List' ? 'vertical-list' : 'vertical-wrap'; return (
diff --git a/src/view/components/Pagination.tsx b/src/view/components/Pagination.tsx index 37eb168ea..7a6da59a2 100644 --- a/src/view/components/Pagination.tsx +++ b/src/view/components/Pagination.tsx @@ -1,36 +1,35 @@ -import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +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 { QueryI } from './interface'; +import * as userSettings from '../../scripts/settings/userSettings'; interface PaginationI { - query: QueryI; + startIndex: number + setStartIndex: React.Dispatch>; itemsResult?: BaseItemDtoQueryResult; - reloadItems: () => void; } -const Pagination: FC = ({ query, itemsResult = {}, reloadItems }) => { - const startIndex = query.StartIndex; - const limit = query.Limit; +const Pagination: FC = ({ startIndex, setStartIndex, itemsResult = {} }) => { + const limit = userSettings.libraryPageSize(undefined); const totalRecordCount = itemsResult.TotalRecordCount || 0; const recordsEnd = Math.min(startIndex + limit, totalRecordCount); const showControls = limit < totalRecordCount; const element = useRef(null); const onNextPageClick = useCallback(() => { - if (query.Limit > 0) { - query.StartIndex += query.Limit; + if (limit > 0) { + const newIndex = startIndex + limit; + setStartIndex(newIndex); } - reloadItems(); - }, [query, reloadItems]); + }, [limit, setStartIndex, startIndex]); const onPreviousPageClick = useCallback(() => { - if (query.Limit > 0) { - query.StartIndex = Math.max(0, query.StartIndex - query.Limit); + if (limit > 0) { + const newIndex = Math.max(0, startIndex - limit); + setStartIndex(newIndex); } - reloadItems(); - }, [query, reloadItems]); + }, [limit, setStartIndex, startIndex]); useEffect(() => { const btnNextPage = element.current?.querySelector('.btnNextPage') as HTMLButtonElement; @@ -52,6 +51,11 @@ const Pagination: FC = ({ query, itemsResult = {}, reloadItems }) = } btnPreviousPage.addEventListener('click', onPreviousPageClick); } + + return () => { + btnNextPage?.removeEventListener('click', onNextPageClick); + btnPreviousPage?.removeEventListener('click', onPreviousPageClick); + }; }, [totalRecordCount, onNextPageClick, onPreviousPageClick, limit, startIndex]); return ( diff --git a/src/view/components/RecommendationContainer.tsx b/src/view/components/RecommendationContainer.tsx index 836e2126f..a88cf2f40 100644 --- a/src/view/components/RecommendationContainer.tsx +++ b/src/view/components/RecommendationContainer.tsx @@ -1,4 +1,4 @@ -import { RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import type { RecommendationDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FC } from 'react'; import globalize from '../../scripts/globalize'; diff --git a/src/view/components/SectionContainer.tsx b/src/view/components/SectionContainer.tsx index eb9daacb1..b1ecff871 100644 --- a/src/view/components/SectionContainer.tsx +++ b/src/view/components/SectionContainer.tsx @@ -1,6 +1,6 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useEffect, useRef } from 'react'; import cardBuilder from '../../components/cardbuilder/cardBuilder'; diff --git a/src/view/components/SelectView.tsx b/src/view/components/SelectView.tsx index 4a308c11d..6b48a86b9 100644 --- a/src/view/components/SelectView.tsx +++ b/src/view/components/SelectView.tsx @@ -1,32 +1,43 @@ -import React, { FC, useEffect, useRef } from 'react'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; import IconButtonElement from '../../elements/IconButtonElement'; -import libraryBrowser from '../../scripts/libraryBrowser'; -import * as userSettings from '../../scripts/settings/userSettings'; -import { QueryI } from './interface'; - interface SelectViewI { - getCurrentViewStyle: () => string; - query: QueryI; - getViewSettings: () => string; + getSettingsKey: () => string; + getVisibleViewSettings: () => string[]; + getViewSettings: () => { + showTitle: string | boolean; + cardLayout: string | boolean; + showYear: string | boolean; + imageType: string; + viewType: string; + }; reloadItems: () => void; } -const SelectView: FC = ({ getCurrentViewStyle, getViewSettings, query, reloadItems }) => { +const SelectView: FC = ({ getSettingsKey, getVisibleViewSettings, getViewSettings, reloadItems }) => { const element = useRef(null); + const showViewSettingsMenu = useCallback(() => { + import('../../components/viewSettings/viewSettings').then(({default: ViewSettings}) => { + const viewSettings = new ViewSettings(); + viewSettings.show({ + settingsKey: getSettingsKey(), + settings: getViewSettings(), + visibleSettings: getVisibleViewSettings() + }).then(() => { + reloadItems(); + }); + }); + }, [getSettingsKey, getViewSettings, getVisibleViewSettings, reloadItems]); + 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(getViewSettings(), viewStyle, false); - query.StartIndex = 0; - reloadItems(); - }); - }, [getCurrentViewStyle, query, reloadItems, getViewSettings]); + btnSelectView?.addEventListener('click', showViewSettingsMenu); + + return () => { + btnSelectView?.removeEventListener('click', showViewSettingsMenu); + }; + }, [showViewSettingsMenu]); return (
diff --git a/src/view/components/Shuffle.tsx b/src/view/components/Shuffle.tsx index 35da1f7f9..e808c7f3d 100644 --- a/src/view/components/Shuffle.tsx +++ b/src/view/components/Shuffle.tsx @@ -1,4 +1,4 @@ -import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useCallback, useEffect, useRef } from 'react'; import { playbackManager } from '../../components/playback/playbackmanager'; diff --git a/src/view/components/ViewItemsContainer.tsx b/src/view/components/ViewItemsContainer.tsx index 10a1046da..cb2650a5a 100644 --- a/src/view/components/ViewItemsContainer.tsx +++ b/src/view/components/ViewItemsContainer.tsx @@ -1,5 +1,5 @@ -import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import loading from '../../components/loading/loading'; import * as userSettings from '../../scripts/settings/userSettings'; @@ -10,9 +10,10 @@ import Pagination from './Pagination'; import SelectView from './SelectView'; import Shuffle from './Shuffle'; import Sort from './Sort'; -import { QueryI } from './interface'; import NewCollection from './NewCollection'; import globalize from '../../scripts/globalize'; +import layoutManager from '../../components/layoutManager'; +import { AlphaPickerValueI, QueryI } from './interface'; interface ViewItemsContainerI { topParentId: string | null; @@ -36,15 +37,37 @@ const ViewItemsContainer: FC = ({ getNoItemsMessage }) => { const [ itemsResult, setItemsResult ] = useState({}); + const [ startIndex, setStartIndex ] = useState(0); + const [ alphaPickerValue, setAlphaPickerValue ] = useState({}); const element = useRef(null); + const queryAlphaPickerValue = useMemo(() => ({ + ...alphaPickerValue + }), [alphaPickerValue]); + const getSettingsKey = useCallback(() => { return `${topParentId} - ${getBasekey()}`; }, [getBasekey, topParentId]); + const getVisibleViewSettings = useCallback(() => { + return [ + 'showTitle', + 'showYear', + 'imageType', + 'cardLayout' + ]; + }, []); + const getViewSettings = useCallback(() => { - return `${getSettingsKey()} -view`; + const basekey = getSettingsKey(); + return { + showTitle: userSettings.get(basekey + '-showTitle', false) !== 'false', + showYear: userSettings.get(basekey + '-showYear', false) !== 'false', + imageType: userSettings.get(basekey + '-imageType', false) || 'primary', + viewType: userSettings.get(basekey + '-viewType', false) || 'images', + cardLayout: userSettings.get(basekey + '-cardLayout', false) !== 'false' + }; }, [getSettingsKey]); const getDefaultSortBy = useCallback(() => { @@ -102,16 +125,26 @@ const ViewItemsContainer: FC = ({ }, []); const getQuery = useCallback(() => { + let fields = 'BasicSyncInfo,MediaSourceCount'; + const viewsettings = getViewSettings(); + if (viewsettings.imageType === 'primary') { + fields += ',PrimaryImageAspectRatio'; + } + + if (viewsettings.showYear) { + fields += ',ProductionYear'; + } + const query: QueryI = { SortBy: getSortValues().sortBy, SortOrder: getSortValues().sortOrder, IncludeItemTypes: getItemTypes().join(','), Recursive: true, - Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo', + Fields: fields, ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo', Limit: userSettings.libraryPageSize(undefined), - StartIndex: 0, + StartIndex: startIndex, ParentId: topParentId }; @@ -119,13 +152,13 @@ const ViewItemsContainer: FC = ({ query.IsFavorite = true; } - userSettings.loadQuerySettings(getSettingsKey(), query); - return query; - }, [getSortValues, getItemTypes, topParentId, getBasekey, getSettingsKey]); + const queryInfo: QueryI = Object.assign(query, queryAlphaPickerValue || {}); + + return queryInfo; + }, [getViewSettings, getSortValues, getItemTypes, startIndex, topParentId, getBasekey, queryAlphaPickerValue]); const getQueryWithFilters = useCallback(() => { const query = getQuery(); - const queryFilters = []; let hasFilters; @@ -248,10 +281,6 @@ const ViewItemsContainer: FC = ({ }]; }, []); - const getCurrentViewStyle = useCallback(() => { - return userSettings.get(getViewSettings(), false) || 'Poster'; - }, [getViewSettings]); - const getContext = useCallback(() => { const itemType = getItemTypes().join(','); if (itemType === 'Movie' || itemType === 'BoxSet') { @@ -269,8 +298,8 @@ const ViewItemsContainer: FC = ({ return; } loading.show(); - const querywithfilters = getQueryWithFilters().query; - window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), querywithfilters).then((result) => { + const query = getQueryWithFilters().query; + window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => { setItemsResult(result); window.scrollTo(0, 0); @@ -289,10 +318,20 @@ const ViewItemsContainer: FC = ({ return (
- + {isBtnShuffleEnabled && } - + + {} = ({
- {isAlphaPickerEnabled && } + {isAlphaPickerEnabled && }
- +
); diff --git a/src/view/components/interface.ts b/src/view/components/interface.ts index ba1e459ce..224859e3d 100644 --- a/src/view/components/interface.ts +++ b/src/view/components/interface.ts @@ -1,3 +1,9 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; + +export interface AlphaPickerValueI { + NameLessThan?: string; + NameStartsWith?: string | null; +} export interface QueryI { SortBy?: string; SortOrder?: string; @@ -7,14 +13,14 @@ export interface QueryI { ImageTypeLimit?: number; EnableTotalRecordCount?: boolean; EnableImageTypes?: string; - StartIndex: number; + StartIndex?: number; ParentId?: string | null; IsFavorite?: boolean; IsMissing?: boolean; Limit:number; NameStartsWithOrGreater?: string; NameLessThan?: string; - NameStartsWith?: string; + NameStartsWith?: string | null; VideoTypes?: string; GenreIds?: string; Is4K?: boolean; @@ -48,8 +54,9 @@ export interface FiltersI { } export interface CardOptionsI { - itemsContainer?: HTMLElement; - parentContainer?: HTMLElement; + itemsContainer?: HTMLElement | null; + parentContainer?: HTMLElement | null; + items?: BaseItemDto[] | null; allowBottomPadding?: boolean; centerText?: boolean; coverImage?: boolean; @@ -58,17 +65,19 @@ export interface CardOptionsI { overlayPlayButton?: boolean; overlayText?: boolean; preferThumb?: boolean; + preferDisc?: boolean; + preferLogo?: boolean; scalable?: boolean; - shape?: string; + shape?: string | null; lazy?: boolean; - cardLayout?: boolean; + cardLayout?: boolean | string; showParentTitle?: boolean; showParentTitleOrTitle?: boolean; showAirTime?: boolean; showAirDateTime?: boolean; showChannelName?: boolean; - showTitle?: boolean; - showYear?: boolean; + showTitle?: boolean | string; + showYear?: boolean | string; showDetailsMenu?: boolean; missingIndicator?: boolean; showLocationTypeIndicator?: boolean; @@ -76,5 +85,43 @@ export interface CardOptionsI { showUnplayedIndicator?: boolean; showChildCountIndicator?: boolean; lines?: number; - context?: string; + 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; } diff --git a/src/view/movies/GenresView.tsx b/src/view/movies/GenresView.tsx index b61b97b76..4f8cb1dfa 100644 --- a/src/view/movies/GenresView.tsx +++ b/src/view/movies/GenresView.tsx @@ -1,4 +1,4 @@ -import { BaseItemDtoQueryResult } from '@thornbill/jellyfin-sdk/dist/generated-client'; +import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import loading from '../../components/loading/loading'; diff --git a/src/view/movies/SuggestionsView.tsx b/src/view/movies/SuggestionsView.tsx index 5bd73f0ae..befde0e83 100644 --- a/src/view/movies/SuggestionsView.tsx +++ b/src/view/movies/SuggestionsView.tsx @@ -1,5 +1,5 @@ -import { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; -import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'; +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'; @@ -12,7 +12,7 @@ interface SuggestionsViewI { topParentId: string | null; } -const SuggestionsView: FunctionComponent = ({topParentId}) => { +const SuggestionsView: FC = ({topParentId}) => { const [ latestItems, setLatestItems ] = useState([]); const [ resumeResult, setResumeResult ] = useState({}); const [ recommendations, setRecommendations ] = useState([]); From f40c565e4aae4f2516bda07d9b6750c43366eaad Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Fri, 14 Oct 2022 02:07:54 +0300 Subject: [PATCH 16/23] apply suggestion --- src/view/components/AlphaPickerContainer.tsx | 17 ++++--- src/view/components/Filter.tsx | 11 +++-- src/view/components/GenresItemsContainer.tsx | 4 +- src/view/components/ItemsContainer.tsx | 6 +-- src/view/components/Pagination.tsx | 18 ++++---- .../components/RecommendationContainer.tsx | 4 +- src/view/components/SectionContainer.tsx | 4 +- src/view/components/SelectView.tsx | 9 ++-- src/view/components/Shuffle.tsx | 4 +- src/view/components/Sort.tsx | 9 ++-- src/view/components/ViewItemsContainer.tsx | 46 +++++++++---------- src/view/components/interface.ts | 6 +-- 12 files changed, 70 insertions(+), 68 deletions(-) diff --git a/src/view/components/AlphaPickerContainer.tsx b/src/view/components/AlphaPickerContainer.tsx index 57b87e0b5..2d5d385f5 100644 --- a/src/view/components/AlphaPickerContainer.tsx +++ b/src/view/components/AlphaPickerContainer.tsx @@ -1,14 +1,13 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import AlphaPicker from '../../components/alphaPicker/alphaPicker'; -import { AlphaPickerValueI, QueryI } from './interface'; +import { QueryI } from './interface'; -interface AlphaPickerContainerI { - getQuery: () => QueryI - setAlphaPickerValue: React.Dispatch; - setStartIndex: React.Dispatch>; +interface AlphaPickerContainerProps { + getQuery: () => QueryI; + setQuery: React.Dispatch>; } -const AlphaPickerContainer: FC = ({ getQuery, setAlphaPickerValue, setStartIndex }) => { +const AlphaPickerContainer: FC = ({ getQuery, setQuery }) => { const [ alphaPicker, setAlphaPicker ] = useState(); const element = useRef(null); const query = getQuery(); @@ -23,9 +22,9 @@ const AlphaPickerContainer: FC = ({ getQuery, setAlphaPic } else { updatedValue = {NameStartsWith: newValue}; } - setAlphaPickerValue(updatedValue); - setStartIndex(0); - }, [setStartIndex, setAlphaPickerValue]); + + setQuery({StartIndex: 0, ...updatedValue}); + }, [setQuery]); useEffect(() => { const alphaPickerElement = element.current; diff --git a/src/view/components/Filter.tsx b/src/view/components/Filter.tsx index becaf0e61..366e61f4d 100644 --- a/src/view/components/Filter.tsx +++ b/src/view/components/Filter.tsx @@ -1,24 +1,26 @@ import React, { FC, useCallback, useEffect, useRef } from 'react'; import IconButtonElement from '../../elements/IconButtonElement'; -import { FiltersI } from './interface'; +import { FiltersI, QueryI } from './interface'; -interface FilterI { +interface FilterProps { topParentId?: string | null; getItemTypes: () => string[]; getFilters: () => FiltersI; getSettingsKey: () => string; getFilterMenuOptions: () => Record; getVisibleFilters: () => string[]; + setQuery: React.Dispatch>; reloadItems: () => void; } -const Filter: FC = ({ +const Filter: FC = ({ topParentId, getItemTypes, getSettingsKey, getFilters, getVisibleFilters, getFilterMenuOptions, + setQuery, reloadItems }) => { const element = useRef(null); @@ -35,10 +37,11 @@ const Filter: FC = ({ serverId: window.ApiClient.serverId(), filterMenuOptions: getFilterMenuOptions() }).then(() => { + setQuery({StartIndex: 0}); reloadItems(); }); }); - }, [getSettingsKey, getFilters, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, reloadItems]); + }, [getSettingsKey, getFilters, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setQuery, reloadItems]); useEffect(() => { const btnFilter = element.current?.querySelector('.btnFilter'); diff --git a/src/view/components/GenresItemsContainer.tsx b/src/view/components/GenresItemsContainer.tsx index 9c8d26839..9f3e073ed 100644 --- a/src/view/components/GenresItemsContainer.tsx +++ b/src/view/components/GenresItemsContainer.tsx @@ -11,13 +11,13 @@ import layoutManager from '../../components/layoutManager'; import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; import globalize from '../../scripts/globalize'; -interface GenresItemsContainerI { +interface GenresItemsContainerProps { topParentId?: string | null; getCurrentViewStyle: () => string; itemsResult?: BaseItemDtoQueryResult; } -const GenresItemsContainer: FC = ({ topParentId, getCurrentViewStyle, itemsResult = {} }) => { +const GenresItemsContainer: FC = ({ topParentId, getCurrentViewStyle, itemsResult = {} }) => { const element = useRef(null); const enableScrollX = useCallback(() => { diff --git a/src/view/components/ItemsContainer.tsx b/src/view/components/ItemsContainer.tsx index 3deb0829b..c7d81aa27 100644 --- a/src/view/components/ItemsContainer.tsx +++ b/src/view/components/ItemsContainer.tsx @@ -50,7 +50,7 @@ const ItemsContainer: FC = ({ getViewSettings, getContext, item const cardOptions: CardOptionsI = { shape: shape, showTitle: viewsettings.showTitle, - showYear: viewsettings.showTitle, + showYear: viewsettings.showYear, cardLayout: viewsettings.cardLayout, centerText: true, context: getContext(), @@ -66,7 +66,7 @@ const ItemsContainer: FC = ({ getViewSettings, getContext, item cardOptions.items = items; return cardOptions; - }, [getContext, items, viewsettings.cardLayout, viewsettings.imageType, viewsettings.showTitle]); + }, [getContext, items, viewsettings.cardLayout, viewsettings.imageType, viewsettings.showTitle, viewsettings.showYear]); const getItemsHtml = useCallback(() => { const settings = getViewSettings(); @@ -82,8 +82,6 @@ const ItemsContainer: FC = ({ getViewSettings, getContext, item } if (!items?.length) { - html = ''; - html += '
'; html += '

' + globalize.translate('MessageNothingHere') + '

'; html += '

' + globalize.translate(noItemsMessage) + '

'; diff --git a/src/view/components/Pagination.tsx b/src/view/components/Pagination.tsx index 7a6da59a2..c85f74272 100644 --- a/src/view/components/Pagination.tsx +++ b/src/view/components/Pagination.tsx @@ -3,16 +3,18 @@ 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 { QueryI } from './interface'; -interface PaginationI { - startIndex: number - setStartIndex: React.Dispatch>; +interface PaginationProps { + query: QueryI; + setQuery: React.Dispatch>; itemsResult?: BaseItemDtoQueryResult; } -const Pagination: FC = ({ startIndex, setStartIndex, itemsResult = {} }) => { +const Pagination: FC = ({ query, setQuery, itemsResult = {} }) => { const limit = userSettings.libraryPageSize(undefined); const totalRecordCount = itemsResult.TotalRecordCount || 0; + const startIndex = query.StartIndex || 0; const recordsEnd = Math.min(startIndex + limit, totalRecordCount); const showControls = limit < totalRecordCount; const element = useRef(null); @@ -20,16 +22,16 @@ const Pagination: FC = ({ startIndex, setStartIndex, itemsResult = const onNextPageClick = useCallback(() => { if (limit > 0) { const newIndex = startIndex + limit; - setStartIndex(newIndex); + setQuery({StartIndex: newIndex}); } - }, [limit, setStartIndex, startIndex]); + }, [limit, setQuery, startIndex]); const onPreviousPageClick = useCallback(() => { if (limit > 0) { const newIndex = Math.max(0, startIndex - limit); - setStartIndex(newIndex); + setQuery({StartIndex: newIndex}); } - }, [limit, setStartIndex, startIndex]); + }, [limit, setQuery, startIndex]); useEffect(() => { const btnNextPage = element.current?.querySelector('.btnNextPage') as HTMLButtonElement; diff --git a/src/view/components/RecommendationContainer.tsx b/src/view/components/RecommendationContainer.tsx index a88cf2f40..6c28272c9 100644 --- a/src/view/components/RecommendationContainer.tsx +++ b/src/view/components/RecommendationContainer.tsx @@ -5,13 +5,13 @@ import globalize from '../../scripts/globalize'; import escapeHTML from 'escape-html'; import SectionContainer from './SectionContainer'; -interface RecommendationContainerI { +interface RecommendationContainerProps { getPortraitShape: () => string; enableScrollX: () => boolean; recommendation?: RecommendationDto; } -const RecommendationContainer: FC = ({ getPortraitShape, enableScrollX, recommendation = {} }) => { +const RecommendationContainer: FC = ({ getPortraitShape, enableScrollX, recommendation = {} }) => { let title = ''; switch (recommendation.RecommendationType) { diff --git a/src/view/components/SectionContainer.tsx b/src/view/components/SectionContainer.tsx index b1ecff871..63cfae525 100644 --- a/src/view/components/SectionContainer.tsx +++ b/src/view/components/SectionContainer.tsx @@ -8,14 +8,14 @@ import ItemsContainerElement from '../../elements/ItemsContainerElement'; import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement'; import { CardOptionsI } from './interface'; -interface SectionContainerI { +interface SectionContainerProps { sectionTitle: string; enableScrollX: () => boolean; items?: BaseItemDto[]; cardOptions?: CardOptionsI; } -const SectionContainer: FC = ({ +const SectionContainer: FC = ({ sectionTitle, enableScrollX, items = [], diff --git a/src/view/components/SelectView.tsx b/src/view/components/SelectView.tsx index 6b48a86b9..164bc2da6 100644 --- a/src/view/components/SelectView.tsx +++ b/src/view/components/SelectView.tsx @@ -1,7 +1,8 @@ import React, { FC, useCallback, useEffect, useRef } from 'react'; import IconButtonElement from '../../elements/IconButtonElement'; +import { QueryI } from './interface'; -interface SelectViewI { +interface SelectViewProps { getSettingsKey: () => string; getVisibleViewSettings: () => string[]; getViewSettings: () => { @@ -11,10 +12,11 @@ interface SelectViewI { imageType: string; viewType: string; }; + setQuery: React.Dispatch>; reloadItems: () => void; } -const SelectView: FC = ({ getSettingsKey, getVisibleViewSettings, getViewSettings, reloadItems }) => { +const SelectView: FC = ({ setQuery, getSettingsKey, getVisibleViewSettings, getViewSettings, reloadItems }) => { const element = useRef(null); const showViewSettingsMenu = useCallback(() => { @@ -25,10 +27,11 @@ const SelectView: FC = ({ getSettingsKey, getVisibleViewSettings, g settings: getViewSettings(), visibleSettings: getVisibleViewSettings() }).then(() => { + setQuery({StartIndex: 0}); reloadItems(); }); }); - }, [getSettingsKey, getViewSettings, getVisibleViewSettings, reloadItems]); + }, [getSettingsKey, getViewSettings, getVisibleViewSettings, reloadItems, setQuery]); useEffect(() => { const btnSelectView = element.current?.querySelector('.btnSelectView') as HTMLButtonElement; diff --git a/src/view/components/Shuffle.tsx b/src/view/components/Shuffle.tsx index e808c7f3d..6d1fb3d65 100644 --- a/src/view/components/Shuffle.tsx +++ b/src/view/components/Shuffle.tsx @@ -4,12 +4,12 @@ import React, { FC, useCallback, useEffect, useRef } from 'react'; import { playbackManager } from '../../components/playback/playbackmanager'; import IconButtonElement from '../../elements/IconButtonElement'; -interface ShuffleI { +interface ShuffleProps { itemsResult?: BaseItemDtoQueryResult; topParentId: string | null; } -const Shuffle: FC = ({ itemsResult = {}, topParentId }) => { +const Shuffle: FC = ({ itemsResult = {}, topParentId }) => { const element = useRef(null); const shuffle = useCallback(() => { diff --git a/src/view/components/Sort.tsx b/src/view/components/Sort.tsx index 43c5125f1..d48d0f056 100644 --- a/src/view/components/Sort.tsx +++ b/src/view/components/Sort.tsx @@ -1,7 +1,8 @@ import React, { FC, useCallback, useEffect, useRef } from 'react'; import IconButtonElement from '../../elements/IconButtonElement'; +import { QueryI } from './interface'; -interface SortI { +interface SortProps { getSortMenuOptions: () => { name: string; value: string; @@ -11,10 +12,11 @@ interface SortI { sortOrder: string; } getSettingsKey: () => string; + setQuery: React.Dispatch>; reloadItems: () => void; } -const Sort: FC = ({ getSortMenuOptions, getSortValues, getSettingsKey, reloadItems }) => { +const Sort: FC = ({ getSortMenuOptions, getSortValues, getSettingsKey, setQuery, reloadItems }) => { const element = useRef(null); const showSortMenu = useCallback(() => { @@ -25,10 +27,11 @@ const Sort: FC = ({ getSortMenuOptions, getSortValues, getSettingsKey, re settings: getSortValues(), sortOptions: getSortMenuOptions() }).then(() => { + setQuery({StartIndex: 0}); reloadItems(); }); }); - }, [getSettingsKey, getSortMenuOptions, getSortValues, reloadItems]); + }, [getSettingsKey, getSortMenuOptions, getSortValues, reloadItems, setQuery]); useEffect(() => { const btnSort = element.current?.querySelector('.btnSort'); diff --git a/src/view/components/ViewItemsContainer.tsx b/src/view/components/ViewItemsContainer.tsx index cb2650a5a..e604c248b 100644 --- a/src/view/components/ViewItemsContainer.tsx +++ b/src/view/components/ViewItemsContainer.tsx @@ -1,5 +1,5 @@ import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; -import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import loading from '../../components/loading/loading'; import * as userSettings from '../../scripts/settings/userSettings'; @@ -12,10 +12,9 @@ import Shuffle from './Shuffle'; import Sort from './Sort'; import NewCollection from './NewCollection'; import globalize from '../../scripts/globalize'; -import layoutManager from '../../components/layoutManager'; -import { AlphaPickerValueI, QueryI } from './interface'; +import { QueryI } from './interface'; -interface ViewItemsContainerI { +interface ViewItemsContainerProps { topParentId: string | null; isBtnShuffleEnabled?: boolean; isBtnFilterEnabled?: boolean; @@ -26,7 +25,7 @@ interface ViewItemsContainerI { getNoItemsMessage: () => string; } -const ViewItemsContainer: FC = ({ +const ViewItemsContainer: FC = ({ topParentId, isBtnShuffleEnabled = false, isBtnFilterEnabled = true, @@ -37,15 +36,12 @@ const ViewItemsContainer: FC = ({ getNoItemsMessage }) => { const [ itemsResult, setItemsResult ] = useState({}); - const [ startIndex, setStartIndex ] = useState(0); - const [ alphaPickerValue, setAlphaPickerValue ] = useState({}); + const [ query, setQuery ] = useState({ + StartIndex: 0 + }); const element = useRef(null); - const queryAlphaPickerValue = useMemo(() => ({ - ...alphaPickerValue - }), [alphaPickerValue]); - const getSettingsKey = useCallback(() => { return `${topParentId} - ${getBasekey()}`; }, [getBasekey, topParentId]); @@ -135,7 +131,7 @@ const ViewItemsContainer: FC = ({ fields += ',ProductionYear'; } - const query: QueryI = { + const options: QueryI = { SortBy: getSortValues().sortBy, SortOrder: getSortValues().sortOrder, IncludeItemTypes: getItemTypes().join(','), @@ -144,18 +140,18 @@ const ViewItemsContainer: FC = ({ ImageTypeLimit: 1, EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo', Limit: userSettings.libraryPageSize(undefined), - StartIndex: startIndex, + StartIndex: query.StartIndex, + NameLessThan: query.NameLessThan, + NameStartsWith: query.NameStartsWith, ParentId: topParentId }; if (getBasekey() === 'favorites') { - query.IsFavorite = true; + options.IsFavorite = true; } - const queryInfo: QueryI = Object.assign(query, queryAlphaPickerValue || {}); - - return queryInfo; - }, [getViewSettings, getSortValues, getItemTypes, startIndex, topParentId, getBasekey, queryAlphaPickerValue]); + return options; + }, [getViewSettings, getSortValues, getItemTypes, query.StartIndex, query.NameLessThan, query.NameStartsWith, topParentId, getBasekey]); const getQueryWithFilters = useCallback(() => { const query = getQuery(); @@ -320,8 +316,8 @@ const ViewItemsContainer: FC = ({
{isBtnShuffleEnabled && } @@ -330,6 +326,7 @@ const ViewItemsContainer: FC = ({ getSettingsKey={getSettingsKey} getVisibleViewSettings={getVisibleViewSettings} getViewSettings={getViewSettings} + setQuery={setQuery} reloadItems={reloadItems} />} @@ -337,6 +334,7 @@ const ViewItemsContainer: FC = ({ getSortMenuOptions={getSortMenuOptions} getSortValues={getSortValues} getSettingsKey={getSettingsKey} + setQuery={setQuery} reloadItems={reloadItems} /> @@ -347,6 +345,7 @@ const ViewItemsContainer: FC = ({ getItemTypes={getItemTypes} getVisibleFilters={getVisibleFilters} getFilterMenuOptions={getFilterMenuOptions} + setQuery={setQuery} reloadItems={reloadItems} />} @@ -356,8 +355,7 @@ const ViewItemsContainer: FC = ({ {isAlphaPickerEnabled && } = ({
diff --git a/src/view/components/interface.ts b/src/view/components/interface.ts index 224859e3d..dc971d077 100644 --- a/src/view/components/interface.ts +++ b/src/view/components/interface.ts @@ -1,9 +1,5 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; -export interface AlphaPickerValueI { - NameLessThan?: string; - NameStartsWith?: string | null; -} export interface QueryI { SortBy?: string; SortOrder?: string; @@ -17,7 +13,7 @@ export interface QueryI { ParentId?: string | null; IsFavorite?: boolean; IsMissing?: boolean; - Limit:number; + Limit?:number; NameStartsWithOrGreater?: string; NameLessThan?: string; NameStartsWith?: string | null; From 6341a71fec882726bdffa4ed3460a6cbcf252d6e Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Fri, 21 Oct 2022 21:47:54 +0300 Subject: [PATCH 17/23] use single state and local storage --- src/components/filtermenu/filtermenu.js | 47 +- .../filtermenu/filtermenu.template.html | 26 +- src/components/sortmenu/sortmenu.js | 21 +- src/components/viewSettings/viewSettings.js | 25 +- .../viewSettings/viewSettings.template.html | 8 +- src/controllers/list.js | 10 +- src/view/components/AlphaPickerContainer.tsx | 22 +- src/view/components/Filter.tsx | 25 +- src/view/components/ItemsContainer.tsx | 90 +-- src/view/components/Pagination.tsx | 24 +- src/view/components/SelectView.tsx | 35 +- src/view/components/Sort.tsx | 31 +- src/view/components/ViewItemsContainer.tsx | 541 +++++++++--------- src/view/components/interface.ts | 68 +-- src/view/hook/useLocalStorage.tsx | 20 + 15 files changed, 485 insertions(+), 508 deletions(-) create mode 100644 src/view/hook/useLocalStorage.tsx diff --git a/src/components/filtermenu/filtermenu.js b/src/components/filtermenu/filtermenu.js index 12f535a18..d6b85256a 100644 --- a/src/components/filtermenu/filtermenu.js +++ b/src/components/filtermenu/filtermenu.js @@ -102,15 +102,8 @@ function onInputCommand(e) { break; } } -function saveValues(context, settings, settingsKey) { +function saveValues(context, settings, settingsKey, setfilters) { 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 const videoTypes = []; @@ -121,7 +114,6 @@ function saveValues(context, settings, settingsKey) { videoTypes.push(elems[i].getAttribute('data-filter')); } } - userSettings.setFilter(settingsKey + '-filter-VideoTypes', videoTypes.join(',')); // Series status const seriesStatuses = []; @@ -132,7 +124,6 @@ function saveValues(context, settings, settingsKey) { seriesStatuses.push(elems[i].getAttribute('data-filter')); } } - userSettings.setFilter(`${settingsKey}-filter-SeriesStatus`, seriesStatuses.join(',')); // Genres const genres = []; @@ -143,7 +134,39 @@ function saveValues(context, settings, settingsKey) { genres.push(elems[i].getAttribute('data-filter')); } } - userSettings.setFilter(settingsKey + '-filter-GenreIds', genres.join(',')); + + 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(',')); + } } function bindCheckboxInput(context, on) { const elems = context.querySelectorAll('.checkboxList-verticalwrap'); @@ -275,7 +298,7 @@ class FilterMenu { if (submitted) { //if (!options.onChange) { - saveValues(dlg, options.settings, options.settingsKey); + saveValues(dlg, options.settings, options.settingsKey, options.setfilters); return resolve(); //} } diff --git a/src/components/filtermenu/filtermenu.template.html b/src/components/filtermenu/filtermenu.template.html index 1a5545baa..cacc69cf4 100644 --- a/src/components/filtermenu/filtermenu.template.html +++ b/src/components/filtermenu/filtermenu.template.html @@ -5,19 +5,19 @@
@@ -49,22 +49,22 @@