mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #3792 from grafixeyehero/Convert-Movies-Page-to-react
This commit is contained in:
commit
00d9c6d71d
43 changed files with 1887 additions and 1692 deletions
|
@ -206,8 +206,8 @@ import toast from '../toast/toast';
|
|||
});
|
||||
}
|
||||
|
||||
export class showEditor {
|
||||
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;
|
||||
|
|
53
src/components/common/AlphaPickerContainer.tsx
Normal file
53
src/components/common/AlphaPickerContainer.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import AlphaPicker from '../alphaPicker/alphaPicker';
|
||||
import { ViewQuerySettings } from '../../types/interface';
|
||||
|
||||
interface AlphaPickerContainerProps {
|
||||
viewQuerySettings: ViewQuerySettings;
|
||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
||||
}
|
||||
|
||||
const AlphaPickerContainer: FC<AlphaPickerContainerProps> = ({ viewQuerySettings, setViewQuerySettings }) => {
|
||||
const [ alphaPicker, setAlphaPicker ] = useState<AlphaPicker>();
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
alphaPicker?.updateControls(viewQuerySettings);
|
||||
|
||||
const onAlphaPickerChange = useCallback((e) => {
|
||||
const newValue = (e as CustomEvent).detail.value;
|
||||
let updatedValue: React.SetStateAction<ViewQuerySettings>;
|
||||
if (newValue === '#') {
|
||||
updatedValue = {NameLessThan: 'A'};
|
||||
} else {
|
||||
updatedValue = {NameStartsWith: newValue};
|
||||
}
|
||||
setViewQuerySettings((prevState) => ({
|
||||
...prevState,
|
||||
StartIndex: 0,
|
||||
...updatedValue
|
||||
}));
|
||||
}, [setViewQuerySettings]);
|
||||
|
||||
useEffect(() => {
|
||||
const alphaPickerElement = element.current;
|
||||
|
||||
setAlphaPicker(new AlphaPicker({
|
||||
element: alphaPickerElement,
|
||||
valueChangeEvent: 'click'
|
||||
}));
|
||||
|
||||
if (alphaPickerElement) {
|
||||
alphaPickerElement.addEventListener('alphavaluechanged', onAlphaPickerChange);
|
||||
}
|
||||
|
||||
return () => {
|
||||
alphaPickerElement?.removeEventListener('alphavaluechanged', onAlphaPickerChange);
|
||||
};
|
||||
}, [onAlphaPickerChange]);
|
||||
|
||||
return (
|
||||
<div ref={element} className='alphaPicker alphaPicker-fixed alphaPicker-fixed-right alphaPicker-vertical alphabetPicker-right' />
|
||||
);
|
||||
};
|
||||
|
||||
export default AlphaPickerContainer;
|
61
src/components/common/Filter.tsx
Normal file
61
src/components/common/Filter.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
import IconButtonElement from '../../elements/IconButtonElement';
|
||||
import { ViewQuerySettings } from '../../types/interface';
|
||||
|
||||
interface FilterProps {
|
||||
topParentId?: string | null;
|
||||
getItemTypes: () => string[];
|
||||
getFilterMenuOptions: () => Record<string, never>;
|
||||
getVisibleFilters: () => string[];
|
||||
viewQuerySettings: ViewQuerySettings;
|
||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
||||
}
|
||||
|
||||
const Filter: FC<FilterProps> = ({
|
||||
topParentId,
|
||||
getItemTypes,
|
||||
getVisibleFilters,
|
||||
getFilterMenuOptions,
|
||||
viewQuerySettings,
|
||||
setViewQuerySettings
|
||||
}) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showFilterMenu = useCallback(() => {
|
||||
import('../filtermenu/filtermenu').then(({default: FilterMenu}) => {
|
||||
const filterMenu = new FilterMenu();
|
||||
filterMenu.show({
|
||||
settings: viewQuerySettings,
|
||||
visibleSettings: getVisibleFilters(),
|
||||
parentId: topParentId,
|
||||
itemTypes: getItemTypes(),
|
||||
serverId: window.ApiClient.serverId(),
|
||||
filterMenuOptions: getFilterMenuOptions(),
|
||||
setfilters: setViewQuerySettings
|
||||
});
|
||||
});
|
||||
}, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]);
|
||||
|
||||
useEffect(() => {
|
||||
const btnFilter = element.current?.querySelector('.btnFilter');
|
||||
|
||||
btnFilter?.addEventListener('click', showFilterMenu);
|
||||
|
||||
return () => {
|
||||
btnFilter?.removeEventListener('click', showFilterMenu);
|
||||
};
|
||||
}, [showFilterMenu]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnFilter autoSize'
|
||||
title='Filter'
|
||||
icon='material-icons filter_list'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filter;
|
126
src/components/common/GenresItemsContainer.tsx
Normal file
126
src/components/common/GenresItemsContainer.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import escapeHTML from 'escape-html';
|
||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { appRouter } from '../appRouter';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
import layoutManager from '../layoutManager';
|
||||
import lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement';
|
||||
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
||||
|
||||
const createLinkElement = ({ className, title, href }: { className?: string, title?: string | null, href?: string }) => ({
|
||||
__html: `<a
|
||||
is="emby-linkbutton"
|
||||
class="${className}"
|
||||
href="${href}"
|
||||
>
|
||||
<h2 class='sectionTitle sectionTitle-cards'>
|
||||
${title}
|
||||
</h2>
|
||||
<span class='material-icons chevron_right' aria-hidden='true'></span>
|
||||
</a>`
|
||||
});
|
||||
|
||||
interface GenresItemsContainerProps {
|
||||
topParentId?: string | null;
|
||||
itemsResult: BaseItemDtoQueryResult;
|
||||
}
|
||||
|
||||
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
||||
topParentId,
|
||||
itemsResult = {}
|
||||
}) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const enableScrollX = useCallback(() => {
|
||||
return !layoutManager.desktop;
|
||||
}, []);
|
||||
|
||||
const getPortraitShape = useCallback(() => {
|
||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
||||
}, [enableScrollX]);
|
||||
|
||||
const fillItemsContainer = useCallback((entry) => {
|
||||
const elem = entry.target;
|
||||
const id = elem.getAttribute('data-id');
|
||||
|
||||
const query = {
|
||||
SortBy: 'Random',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Movie',
|
||||
Recursive: true,
|
||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary',
|
||||
Limit: 12,
|
||||
GenreIds: id,
|
||||
EnableTotalRecordCount: false,
|
||||
ParentId: topParentId
|
||||
};
|
||||
window.ApiClient.getItems(window.ApiClient.getCurrentUserId(), query).then((result) => {
|
||||
cardBuilder.buildCards(result.Items || [], {
|
||||
itemsContainer: elem,
|
||||
shape: getPortraitShape(),
|
||||
scalable: true,
|
||||
overlayMoreButton: true,
|
||||
allowBottomPadding: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
showYear: true
|
||||
});
|
||||
});
|
||||
}, [getPortraitShape, topParentId]);
|
||||
|
||||
useEffect(() => {
|
||||
const elem = element.current;
|
||||
lazyLoader.lazyChildren(elem, fillItemsContainer);
|
||||
}, [itemsResult.Items, fillItemsContainer]);
|
||||
|
||||
const items = itemsResult.Items || [];
|
||||
return (
|
||||
<div ref={element}>
|
||||
{
|
||||
!items.length ? (
|
||||
<div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>{globalize.translate('MessageNoGenresAvailable')}</p>
|
||||
</div>
|
||||
) : items.map((item, index) => (
|
||||
<div key={index} className='verticalSection'>
|
||||
<div
|
||||
className='sectionTitleContainer sectionTitleContainer-cards padded-left'
|
||||
dangerouslySetInnerHTML={createLinkElement({
|
||||
className: 'more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre',
|
||||
title: escapeHTML(item.Name),
|
||||
href: appRouter.getRouteUrl(item, {
|
||||
context: 'movies',
|
||||
parentId: topParentId
|
||||
})
|
||||
})}
|
||||
/>
|
||||
|
||||
{enableScrollX() ?
|
||||
<ItemsScrollerContainerElement
|
||||
scrollerclassName='padded-top-focusscale padded-bottom-focusscale'
|
||||
dataMousewheel='false'
|
||||
dataCenterfocus='true'
|
||||
className='itemsContainer scrollSlider focuscontainer-x lazy'
|
||||
dataId={item.Id}
|
||||
/> : <ItemsContainerElement
|
||||
className='itemsContainer vertical-wrap lazy padded-left padded-right'
|
||||
dataId={item.Id}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenresItemsContainer;
|
33
src/components/common/ItemsContainer.tsx
Normal file
33
src/components/common/ItemsContainer.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React, { FC, useEffect, useRef } from 'react';
|
||||
|
||||
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import { ViewQuerySettings } from '../../types/interface';
|
||||
|
||||
interface ItemsContainerI {
|
||||
viewQuerySettings: ViewQuerySettings;
|
||||
getItemsHtml: () => string
|
||||
}
|
||||
|
||||
const ItemsContainer: FC<ItemsContainerI> = ({ viewQuerySettings, getItemsHtml }) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement;
|
||||
itemsContainer.innerHTML = getItemsHtml();
|
||||
imageLoader.lazyChildren(itemsContainer);
|
||||
}, [getItemsHtml]);
|
||||
|
||||
const cssClass = viewQuerySettings.imageType == 'list' ? 'vertical-list' : 'vertical-wrap';
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<ItemsContainerElement
|
||||
className={`itemsContainer ${cssClass} centered padded-left padded-right padded-right-withalphapicker`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemsContainer;
|
38
src/components/common/NewCollection.tsx
Normal file
38
src/components/common/NewCollection.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import IconButtonElement from '../../elements/IconButtonElement';
|
||||
|
||||
const NewCollection: FC = () => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showCollectionEditor = useCallback(() => {
|
||||
import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => {
|
||||
const serverId = window.ApiClient.serverId();
|
||||
const collectionEditor = new CollectionEditor();
|
||||
collectionEditor.show({
|
||||
items: [],
|
||||
serverId: serverId
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const btnNewCollection = element.current?.querySelector('.btnNewCollection');
|
||||
if (btnNewCollection) {
|
||||
btnNewCollection.addEventListener('click', showCollectionEditor);
|
||||
}
|
||||
}, [showCollectionEditor]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnNewCollection autoSize'
|
||||
title='Add'
|
||||
icon='material-icons add'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewCollection;
|
96
src/components/common/Pagination.tsx
Normal file
96
src/components/common/Pagination.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
import IconButtonElement from '../../elements/IconButtonElement';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import { ViewQuerySettings } from '../../types/interface';
|
||||
|
||||
interface PaginationProps {
|
||||
viewQuerySettings: ViewQuerySettings;
|
||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
||||
itemsResult?: BaseItemDtoQueryResult;
|
||||
}
|
||||
|
||||
const Pagination: FC<PaginationProps> = ({ viewQuerySettings, setViewQuerySettings, itemsResult = {} }) => {
|
||||
const limit = userSettings.libraryPageSize(undefined);
|
||||
const totalRecordCount = itemsResult.TotalRecordCount || 0;
|
||||
const startIndex = viewQuerySettings.StartIndex || 0;
|
||||
const recordsEnd = Math.min(startIndex + limit, totalRecordCount);
|
||||
const showControls = limit < totalRecordCount;
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onNextPageClick = useCallback(() => {
|
||||
if (limit > 0) {
|
||||
const newIndex = startIndex + limit;
|
||||
setViewQuerySettings((prevState) => ({
|
||||
...prevState,
|
||||
StartIndex: newIndex
|
||||
}));
|
||||
}
|
||||
}, [limit, setViewQuerySettings, startIndex]);
|
||||
|
||||
const onPreviousPageClick = useCallback(() => {
|
||||
if (limit > 0) {
|
||||
const newIndex = Math.max(0, startIndex - limit);
|
||||
setViewQuerySettings((prevState) => ({
|
||||
...prevState,
|
||||
StartIndex: newIndex
|
||||
}));
|
||||
}
|
||||
}, [limit, setViewQuerySettings, startIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const btnNextPage = element.current?.querySelector('.btnNextPage') as HTMLButtonElement;
|
||||
if (btnNextPage) {
|
||||
if (startIndex + limit >= totalRecordCount) {
|
||||
btnNextPage.disabled = true;
|
||||
} else {
|
||||
btnNextPage.disabled = false;
|
||||
}
|
||||
btnNextPage.addEventListener('click', onNextPageClick);
|
||||
}
|
||||
|
||||
const btnPreviousPage = element.current?.querySelector('.btnPreviousPage') as HTMLButtonElement;
|
||||
if (btnPreviousPage) {
|
||||
if (startIndex) {
|
||||
btnPreviousPage.disabled = false;
|
||||
} else {
|
||||
btnPreviousPage.disabled = true;
|
||||
}
|
||||
btnPreviousPage.addEventListener('click', onPreviousPageClick);
|
||||
}
|
||||
|
||||
return () => {
|
||||
btnNextPage?.removeEventListener('click', onNextPageClick);
|
||||
btnPreviousPage?.removeEventListener('click', onPreviousPageClick);
|
||||
};
|
||||
}, [totalRecordCount, onNextPageClick, onPreviousPageClick, limit, startIndex]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<div className='paging'>
|
||||
{showControls && (
|
||||
<div className='listPaging' style={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
||||
<span>
|
||||
{globalize.translate('ListPaging', (totalRecordCount ? startIndex + 1 : 0), recordsEnd, totalRecordCount)}
|
||||
</span>
|
||||
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnPreviousPage autoSize'
|
||||
icon='material-icons arrow_back'
|
||||
/>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnNextPage autoSize'
|
||||
icon='material-icons arrow_forward'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
48
src/components/common/RecommendationContainer.tsx
Normal file
48
src/components/common/RecommendationContainer.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import type { RecommendationDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import globalize from '../../scripts/globalize';
|
||||
import escapeHTML from 'escape-html';
|
||||
import SectionContainer from './SectionContainer';
|
||||
|
||||
interface RecommendationContainerProps {
|
||||
getPortraitShape: () => string;
|
||||
enableScrollX: () => boolean;
|
||||
recommendation?: RecommendationDto;
|
||||
}
|
||||
|
||||
const RecommendationContainer: FC<RecommendationContainerProps> = ({ getPortraitShape, enableScrollX, recommendation = {} }) => {
|
||||
let title = '';
|
||||
|
||||
switch (recommendation.RecommendationType) {
|
||||
case 'SimilarToRecentlyPlayed':
|
||||
title = globalize.translate('RecommendationBecauseYouWatched', recommendation.BaselineItemName);
|
||||
break;
|
||||
|
||||
case 'SimilarToLikedItem':
|
||||
title = globalize.translate('RecommendationBecauseYouLike', recommendation.BaselineItemName);
|
||||
break;
|
||||
|
||||
case 'HasDirectorFromRecentlyPlayed':
|
||||
case 'HasLikedDirector':
|
||||
title = globalize.translate('RecommendationDirectedBy', recommendation.BaselineItemName);
|
||||
break;
|
||||
|
||||
case 'HasActorFromRecentlyPlayed':
|
||||
case 'HasLikedActor':
|
||||
title = globalize.translate('RecommendationStarring', recommendation.BaselineItemName);
|
||||
break;
|
||||
}
|
||||
|
||||
return <SectionContainer
|
||||
sectionTitle={escapeHTML(title)}
|
||||
enableScrollX={enableScrollX}
|
||||
items={recommendation.Items || []}
|
||||
cardOptions={{
|
||||
shape: getPortraitShape(),
|
||||
showYear: true
|
||||
}}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default RecommendationContainer;
|
62
src/components/common/SectionContainer.tsx
Normal file
62
src/components/common/SectionContainer.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useEffect, useRef } from 'react';
|
||||
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
import ItemsContainerElement from '../../elements/ItemsContainerElement';
|
||||
import ItemsScrollerContainerElement from '../../elements/ItemsScrollerContainerElement';
|
||||
import { CardOptions } from '../../types/interface';
|
||||
|
||||
interface SectionContainerProps {
|
||||
sectionTitle: string;
|
||||
enableScrollX: () => boolean;
|
||||
items?: BaseItemDto[];
|
||||
cardOptions?: CardOptions;
|
||||
}
|
||||
|
||||
const SectionContainer: FC<SectionContainerProps> = ({
|
||||
sectionTitle,
|
||||
enableScrollX,
|
||||
items = [],
|
||||
cardOptions = {}
|
||||
}) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
cardBuilder.buildCards(items, {
|
||||
itemsContainer: element.current?.querySelector('.itemsContainer'),
|
||||
parentContainer: element.current?.querySelector('.verticalSection'),
|
||||
scalable: true,
|
||||
overlayPlayButton: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
cardLayout: false,
|
||||
...cardOptions
|
||||
});
|
||||
}, [cardOptions, enableScrollX, items]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<div className='verticalSection hide'>
|
||||
<div className='sectionTitleContainer sectionTitleContainer-cards'>
|
||||
<h2 className='sectionTitle sectionTitle-cards padded-left'>
|
||||
{sectionTitle}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{enableScrollX() ? <ItemsScrollerContainerElement
|
||||
scrollerclassName='padded-top-focusscale padded-bottom-focusscale'
|
||||
dataMousewheel='false'
|
||||
dataCenterfocus='true'
|
||||
className='itemsContainer scrollSlider focuscontainer-x'
|
||||
/> : <ItemsContainerElement
|
||||
className='itemsContainer focuscontainer-x padded-left padded-right vertical-wrap'
|
||||
/>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionContainer;
|
50
src/components/common/SelectView.tsx
Normal file
50
src/components/common/SelectView.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
import IconButtonElement from '../../elements/IconButtonElement';
|
||||
import { ViewQuerySettings } from '../../types/interface';
|
||||
|
||||
interface SelectViewProps {
|
||||
getVisibleViewSettings: () => string[];
|
||||
viewQuerySettings: ViewQuerySettings;
|
||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
||||
}
|
||||
|
||||
const SelectView: FC<SelectViewProps> = ({
|
||||
getVisibleViewSettings,
|
||||
viewQuerySettings,
|
||||
setViewQuerySettings
|
||||
}) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showViewSettingsMenu = useCallback(() => {
|
||||
import('../viewSettings/viewSettings').then(({default: ViewSettings}) => {
|
||||
const viewsettings = new ViewSettings();
|
||||
viewsettings.show({
|
||||
settings: viewQuerySettings,
|
||||
visibleSettings: getVisibleViewSettings(),
|
||||
setviewsettings: setViewQuerySettings
|
||||
});
|
||||
});
|
||||
}, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]);
|
||||
|
||||
useEffect(() => {
|
||||
const btnSelectView = element.current?.querySelector('.btnSelectView') as HTMLButtonElement;
|
||||
btnSelectView?.addEventListener('click', showViewSettingsMenu);
|
||||
|
||||
return () => {
|
||||
btnSelectView?.removeEventListener('click', showViewSettingsMenu);
|
||||
};
|
||||
}, [showViewSettingsMenu]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnSelectView autoSize'
|
||||
title='ButtonSelectView'
|
||||
icon='material-icons view_comfy'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectView;
|
43
src/components/common/Shuffle.tsx
Normal file
43
src/components/common/Shuffle.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import IconButtonElement from '../../elements/IconButtonElement';
|
||||
|
||||
interface ShuffleProps {
|
||||
itemsResult?: BaseItemDtoQueryResult;
|
||||
topParentId: string | null;
|
||||
}
|
||||
|
||||
const Shuffle: FC<ShuffleProps> = ({ itemsResult = {}, topParentId }) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const shuffle = useCallback(() => {
|
||||
window.ApiClient.getItem(
|
||||
window.ApiClient.getCurrentUserId(),
|
||||
topParentId as string
|
||||
).then((item) => {
|
||||
playbackManager.shuffle(item);
|
||||
});
|
||||
}, [topParentId]);
|
||||
|
||||
useEffect(() => {
|
||||
const btnShuffle = element.current?.querySelector('.btnShuffle');
|
||||
if (btnShuffle) {
|
||||
btnShuffle.addEventListener('click', shuffle);
|
||||
}
|
||||
}, [itemsResult.TotalRecordCount, shuffle]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnShuffle autoSize'
|
||||
title='Shuffle'
|
||||
icon='material-icons shuffle'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Shuffle;
|
54
src/components/common/Sort.tsx
Normal file
54
src/components/common/Sort.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
import IconButtonElement from '../../elements/IconButtonElement';
|
||||
import { ViewQuerySettings } from '../../types/interface';
|
||||
|
||||
interface SortProps {
|
||||
getSortMenuOptions: () => {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
viewQuerySettings: ViewQuerySettings;
|
||||
setViewQuerySettings: React.Dispatch<React.SetStateAction<ViewQuerySettings>>;
|
||||
}
|
||||
|
||||
const Sort: FC<SortProps> = ({
|
||||
getSortMenuOptions,
|
||||
viewQuerySettings,
|
||||
setViewQuerySettings
|
||||
}) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showSortMenu = useCallback(() => {
|
||||
import('../sortmenu/sortmenu').then(({default: SortMenu}) => {
|
||||
const sortMenu = new SortMenu();
|
||||
sortMenu.show({
|
||||
settings: viewQuerySettings,
|
||||
sortOptions: getSortMenuOptions(),
|
||||
setSortValues: setViewQuerySettings
|
||||
});
|
||||
});
|
||||
}, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]);
|
||||
|
||||
useEffect(() => {
|
||||
const btnSort = element.current?.querySelector('.btnSort');
|
||||
|
||||
btnSort?.addEventListener('click', showSortMenu);
|
||||
|
||||
return () => {
|
||||
btnSort?.removeEventListener('click', showSortMenu);
|
||||
};
|
||||
}, [showSortMenu]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnSort autoSize'
|
||||
title='Sort'
|
||||
icon='material-icons sort_by_alpha'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sort;
|
417
src/components/common/ViewItemsContainer.tsx
Normal file
417
src/components/common/ViewItemsContainer.tsx
Normal file
|
@ -0,0 +1,417 @@
|
|||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import loading from '../loading/loading';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import AlphaPickerContainer from './AlphaPickerContainer';
|
||||
import Filter from './Filter';
|
||||
import ItemsContainer from './ItemsContainer';
|
||||
import Pagination from './Pagination';
|
||||
import SelectView from './SelectView';
|
||||
import Shuffle from './Shuffle';
|
||||
import Sort from './Sort';
|
||||
import NewCollection from './NewCollection';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { CardOptions, ViewQuerySettings } from '../../types/interface';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||
import listview from '../listview/listview';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
|
||||
interface ViewItemsContainerProps {
|
||||
topParentId: string | null;
|
||||
isBtnShuffleEnabled?: boolean;
|
||||
isBtnFilterEnabled?: boolean;
|
||||
isBtnNewCollectionEnabled?: boolean;
|
||||
isAlphaPickerEnabled?: boolean;
|
||||
getBasekey: () => string;
|
||||
getItemTypes: () => string[];
|
||||
getNoItemsMessage: () => string;
|
||||
}
|
||||
|
||||
const getDefaultSortBy = () => {
|
||||
return 'SortName';
|
||||
};
|
||||
|
||||
const getVisibleViewSettings = () => {
|
||||
return [
|
||||
'showTitle',
|
||||
'showYear',
|
||||
'imageType',
|
||||
'cardLayout'
|
||||
];
|
||||
};
|
||||
|
||||
const getFilterMenuOptions = () => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const getVisibleFilters = () => {
|
||||
return [
|
||||
'IsUnplayed',
|
||||
'IsPlayed',
|
||||
'IsFavorite',
|
||||
'IsResumable',
|
||||
'VideoType',
|
||||
'HasSubtitles',
|
||||
'HasTrailer',
|
||||
'HasSpecialFeature',
|
||||
'HasThemeSong',
|
||||
'HasThemeVideo'
|
||||
];
|
||||
};
|
||||
|
||||
const getSortMenuOptions = () => {
|
||||
return [{
|
||||
name: globalize.translate('Name'),
|
||||
value: 'SortName,ProductionYear'
|
||||
}, {
|
||||
name: globalize.translate('OptionRandom'),
|
||||
value: 'Random'
|
||||
}, {
|
||||
name: globalize.translate('OptionImdbRating'),
|
||||
value: 'CommunityRating,SortName,ProductionYear'
|
||||
}, {
|
||||
name: globalize.translate('OptionCriticRating'),
|
||||
value: 'CriticRating,SortName,ProductionYear'
|
||||
}, {
|
||||
name: globalize.translate('OptionDateAdded'),
|
||||
value: 'DateCreated,SortName,ProductionYear'
|
||||
}, {
|
||||
name: globalize.translate('OptionDatePlayed'),
|
||||
value: 'DatePlayed,SortName,ProductionYear'
|
||||
}, {
|
||||
name: globalize.translate('OptionParentalRating'),
|
||||
value: 'OfficialRating,SortName,ProductionYear'
|
||||
}, {
|
||||
name: globalize.translate('OptionPlayCount'),
|
||||
value: 'PlayCount,SortName,ProductionYear'
|
||||
}, {
|
||||
name: globalize.translate('OptionReleaseDate'),
|
||||
value: 'PremiereDate,SortName,ProductionYear'
|
||||
}, {
|
||||
name: globalize.translate('Runtime'),
|
||||
value: 'Runtime,SortName,ProductionYear'
|
||||
}];
|
||||
};
|
||||
|
||||
const defaultViewQuerySettings: ViewQuerySettings = {
|
||||
showTitle: true,
|
||||
showYear: true,
|
||||
imageType: 'primary',
|
||||
viewType: '',
|
||||
cardLayout: false,
|
||||
SortBy: getDefaultSortBy(),
|
||||
SortOrder: 'Ascending',
|
||||
IsPlayed: false,
|
||||
IsUnplayed: false,
|
||||
IsFavorite: false,
|
||||
IsResumable: false,
|
||||
Is4K: null,
|
||||
IsHD: null,
|
||||
IsSD: null,
|
||||
Is3D: null,
|
||||
VideoTypes: '',
|
||||
SeriesStatus: '',
|
||||
HasSubtitles: null,
|
||||
HasTrailer: null,
|
||||
HasSpecialFeature: null,
|
||||
HasThemeSong: null,
|
||||
HasThemeVideo: null,
|
||||
GenreIds: '',
|
||||
StartIndex: 0
|
||||
};
|
||||
|
||||
const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
||||
topParentId,
|
||||
isBtnShuffleEnabled = false,
|
||||
isBtnFilterEnabled = true,
|
||||
isBtnNewCollectionEnabled = false,
|
||||
isAlphaPickerEnabled = true,
|
||||
getBasekey,
|
||||
getItemTypes,
|
||||
getNoItemsMessage
|
||||
}) => {
|
||||
const getSettingsKey = useCallback(() => {
|
||||
return `${topParentId} - ${getBasekey()}`;
|
||||
}, [getBasekey, topParentId]);
|
||||
|
||||
const [isLoading, setisLoading] = useState(false);
|
||||
|
||||
const [viewQuerySettings, setViewQuerySettings] = useLocalStorage<ViewQuerySettings>(
|
||||
`viewQuerySettings - ${getSettingsKey()}`,
|
||||
defaultViewQuerySettings
|
||||
);
|
||||
|
||||
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
|
||||
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getContext = useCallback(() => {
|
||||
const itemType = getItemTypes().join(',');
|
||||
if (itemType === 'Movie' || itemType === 'BoxSet') {
|
||||
return 'movies';
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [getItemTypes]);
|
||||
|
||||
const getCardOptions = useCallback(() => {
|
||||
let shape;
|
||||
let preferThumb;
|
||||
let preferDisc;
|
||||
let preferLogo;
|
||||
|
||||
if (viewQuerySettings.imageType === 'banner') {
|
||||
shape = 'banner';
|
||||
} else if (viewQuerySettings.imageType === 'disc') {
|
||||
shape = 'square';
|
||||
preferDisc = true;
|
||||
} else if (viewQuerySettings.imageType === 'logo') {
|
||||
shape = 'backdrop';
|
||||
preferLogo = true;
|
||||
} else if (viewQuerySettings.imageType === 'thumb') {
|
||||
shape = 'backdrop';
|
||||
preferThumb = true;
|
||||
} else {
|
||||
shape = 'autoVertical';
|
||||
}
|
||||
|
||||
const cardOptions: CardOptions = {
|
||||
shape: shape,
|
||||
showTitle: viewQuerySettings.showTitle,
|
||||
showYear: viewQuerySettings.showYear,
|
||||
cardLayout: viewQuerySettings.cardLayout,
|
||||
centerText: true,
|
||||
context: getContext(),
|
||||
coverImage: true,
|
||||
preferThumb: preferThumb,
|
||||
preferDisc: preferDisc,
|
||||
preferLogo: preferLogo,
|
||||
overlayPlayButton: false,
|
||||
overlayMoreButton: true,
|
||||
overlayText: !viewQuerySettings.showTitle
|
||||
};
|
||||
|
||||
cardOptions.items = itemsResult.Items || [];
|
||||
|
||||
return cardOptions;
|
||||
}, [
|
||||
getContext,
|
||||
itemsResult.Items,
|
||||
viewQuerySettings.cardLayout,
|
||||
viewQuerySettings.imageType,
|
||||
viewQuerySettings.showTitle,
|
||||
viewQuerySettings.showYear
|
||||
]);
|
||||
|
||||
const getItemsHtml = useCallback(() => {
|
||||
let html = '';
|
||||
|
||||
if (viewQuerySettings.imageType === 'list') {
|
||||
html = listview.getListViewHtml({
|
||||
items: itemsResult.Items || [],
|
||||
context: getContext()
|
||||
});
|
||||
} else {
|
||||
html = cardBuilder.getCardsHtml(itemsResult.Items || [], getCardOptions());
|
||||
}
|
||||
|
||||
if (!itemsResult.Items?.length) {
|
||||
html += '<div class="noItemsMessage centerMessage">';
|
||||
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
|
||||
html += '<p>' + globalize.translate(getNoItemsMessage()) + '</p>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]);
|
||||
|
||||
const getQuery = useCallback(() => {
|
||||
let fields = 'BasicSyncInfo,MediaSourceCount';
|
||||
|
||||
if (viewQuerySettings.imageType === 'primary') {
|
||||
fields += ',PrimaryImageAspectRatio';
|
||||
}
|
||||
|
||||
if (viewQuerySettings.showYear) {
|
||||
fields += ',ProductionYear';
|
||||
}
|
||||
|
||||
const queryFilters: string[] = [];
|
||||
|
||||
if (viewQuerySettings.IsPlayed) {
|
||||
queryFilters.push('IsPlayed');
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsUnplayed) {
|
||||
queryFilters.push('IsUnplayed');
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsFavorite) {
|
||||
queryFilters.push('IsFavorite');
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsResumable) {
|
||||
queryFilters.push('IsResumable');
|
||||
}
|
||||
|
||||
let queryIsHD;
|
||||
|
||||
if (viewQuerySettings.IsHD) {
|
||||
queryIsHD = true;
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsSD) {
|
||||
queryIsHD = false;
|
||||
}
|
||||
|
||||
return {
|
||||
SortBy: viewQuerySettings.SortBy,
|
||||
SortOrder: viewQuerySettings.SortOrder,
|
||||
IncludeItemTypes: getItemTypes().join(','),
|
||||
Recursive: true,
|
||||
Fields: fields,
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo',
|
||||
Limit: userSettings.libraryPageSize(undefined),
|
||||
IsFavorite: getBasekey() === 'favorites' ? true : null,
|
||||
VideoTypes: viewQuerySettings.VideoTypes,
|
||||
GenreIds: viewQuerySettings.GenreIds,
|
||||
Is4K: viewQuerySettings.Is4K ? true : null,
|
||||
IsHD: queryIsHD,
|
||||
Is3D: viewQuerySettings.Is3D ? true : null,
|
||||
HasSubtitles: viewQuerySettings.HasSubtitles ? true : null,
|
||||
HasTrailer: viewQuerySettings.HasTrailer ? true : null,
|
||||
HasSpecialFeature: viewQuerySettings.HasSpecialFeature ? true : null,
|
||||
HasThemeSong: viewQuerySettings.HasThemeSong ? true : null,
|
||||
HasThemeVideo: viewQuerySettings.HasThemeVideo ? true : null,
|
||||
Filters: queryFilters.length ? queryFilters.join(',') : null,
|
||||
StartIndex: viewQuerySettings.StartIndex,
|
||||
NameLessThan: viewQuerySettings.NameLessThan,
|
||||
NameStartsWith: viewQuerySettings.NameStartsWith,
|
||||
ParentId: topParentId
|
||||
};
|
||||
}, [
|
||||
viewQuerySettings.imageType,
|
||||
viewQuerySettings.showYear,
|
||||
viewQuerySettings.IsPlayed,
|
||||
viewQuerySettings.IsUnplayed,
|
||||
viewQuerySettings.IsFavorite,
|
||||
viewQuerySettings.IsResumable,
|
||||
viewQuerySettings.IsHD,
|
||||
viewQuerySettings.IsSD,
|
||||
viewQuerySettings.SortBy,
|
||||
viewQuerySettings.SortOrder,
|
||||
viewQuerySettings.VideoTypes,
|
||||
viewQuerySettings.GenreIds,
|
||||
viewQuerySettings.Is4K,
|
||||
viewQuerySettings.Is3D,
|
||||
viewQuerySettings.HasSubtitles,
|
||||
viewQuerySettings.HasTrailer,
|
||||
viewQuerySettings.HasSpecialFeature,
|
||||
viewQuerySettings.HasThemeSong,
|
||||
viewQuerySettings.HasThemeVideo,
|
||||
viewQuerySettings.StartIndex,
|
||||
viewQuerySettings.NameLessThan,
|
||||
viewQuerySettings.NameStartsWith,
|
||||
getItemTypes,
|
||||
getBasekey,
|
||||
topParentId
|
||||
]);
|
||||
|
||||
const fetchData = useCallback(() => {
|
||||
loading.show();
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(window.ApiClient.serverId());
|
||||
return apiClient.getItems(
|
||||
apiClient.getCurrentUserId(),
|
||||
{
|
||||
...getQuery()
|
||||
}
|
||||
);
|
||||
}, [getQuery]);
|
||||
|
||||
const reloadItems = useCallback(() => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
setisLoading(false);
|
||||
fetchData().then((result) => {
|
||||
setItemsResult(result);
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
loading.hide();
|
||||
setisLoading(true);
|
||||
});
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadItems();
|
||||
}, [reloadItems]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||
<Pagination
|
||||
itemsResult= {itemsResult}
|
||||
viewQuerySettings={viewQuerySettings}
|
||||
setViewQuerySettings={setViewQuerySettings}
|
||||
/>
|
||||
|
||||
{isBtnShuffleEnabled && <Shuffle itemsResult={itemsResult} topParentId={topParentId} />}
|
||||
|
||||
<SelectView
|
||||
getVisibleViewSettings={getVisibleViewSettings}
|
||||
viewQuerySettings={viewQuerySettings}
|
||||
setViewQuerySettings={setViewQuerySettings}
|
||||
/>
|
||||
|
||||
<Sort
|
||||
getSortMenuOptions={getSortMenuOptions}
|
||||
viewQuerySettings={viewQuerySettings}
|
||||
setViewQuerySettings={setViewQuerySettings}
|
||||
/>
|
||||
|
||||
{isBtnFilterEnabled && <Filter
|
||||
topParentId={topParentId}
|
||||
getItemTypes={getItemTypes}
|
||||
getVisibleFilters={getVisibleFilters}
|
||||
getFilterMenuOptions={getFilterMenuOptions}
|
||||
viewQuerySettings={viewQuerySettings}
|
||||
setViewQuerySettings={setViewQuerySettings}
|
||||
/>}
|
||||
|
||||
{isBtnNewCollectionEnabled && <NewCollection />}
|
||||
|
||||
</div>
|
||||
|
||||
{isAlphaPickerEnabled && <AlphaPickerContainer
|
||||
viewQuerySettings={viewQuerySettings}
|
||||
setViewQuerySettings={setViewQuerySettings}
|
||||
/>}
|
||||
|
||||
{isLoading && <ItemsContainer
|
||||
viewQuerySettings={viewQuerySettings}
|
||||
getItemsHtml={getItemsHtml}
|
||||
/>}
|
||||
|
||||
<div className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||
<Pagination
|
||||
itemsResult= {itemsResult}
|
||||
viewQuerySettings={viewQuerySettings}
|
||||
setViewQuerySettings={setViewQuerySettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewItemsContainer;
|
|
@ -102,15 +102,8 @@ function onInputCommand(e) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
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();
|
||||
//}
|
||||
}
|
||||
|
|
|
@ -5,19 +5,19 @@
|
|||
<div class="verticalSection verticalSection-extrabottompadding basicFilterSection focuscontainer-x" style="margin-top:2em;">
|
||||
<div class="checkboxList checkboxList-verticalwrap">
|
||||
<label class="viewSetting simpleFilter" data-settingname="IsUnplayed">
|
||||
<input type="checkbox" is="emby-checkbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkUnplayed" />
|
||||
<span>${Unplayed}</span>
|
||||
</label>
|
||||
<label class="viewSetting simpleFilter" data-settingname="IsPlayed">
|
||||
<input type="checkbox" is="emby-checkbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkPlayed" />
|
||||
<span>${Played}</span>
|
||||
</label>
|
||||
<label class="viewSetting simpleFilter" data-settingname="IsFavorite">
|
||||
<input type="checkbox" is="emby-checkbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFavorite" />
|
||||
<span>${Favorite}</span>
|
||||
</label>
|
||||
<label class="viewSetting simpleFilter" data-settingname="IsResumable">
|
||||
<input type="checkbox" is="emby-checkbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkResumable" />
|
||||
<span>${ContinueWatching}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -49,22 +49,22 @@
|
|||
<div class="checkboxList checkboxList-verticalwrap">
|
||||
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter" data-settingname="IsHD" />
|
||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter chkHDFilter" data-settingname="IsHD" />
|
||||
<span>HD</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter" data-settingname="Is4K" />
|
||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter chk4KFilter" data-settingname="Is4K" />
|
||||
<span>4K</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter" data-settingname="IsSD" />
|
||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter chkSDFilter" data-settingname="IsSD" />
|
||||
<span>SD</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter" data-settingname="Is3D" />
|
||||
<input type="checkbox" is="emby-checkbox" class="simpleFilter chk3DFilter" data-settingname="Is3D" />
|
||||
<span>3D</span>
|
||||
</label>
|
||||
<label>
|
||||
|
@ -82,23 +82,23 @@
|
|||
<h2 class="checkboxListLabel">${Features}</h2>
|
||||
<div class="checkboxList checkboxList-verticalwrap">
|
||||
<label class="viewSetting simpleFilter" data-settingname="HasSubtitles">
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkSubtitle" />
|
||||
<span>${Subtitles}</span>
|
||||
</label>
|
||||
<label class="viewSetting simpleFilter" data-settingname="HasTrailer">
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkTrailer" />
|
||||
<span>${Trailers}</span>
|
||||
</label>
|
||||
<label class="viewSetting simpleFilter" data-settingname="HasSpecialFeature">
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkSpecialFeature" />
|
||||
<span>${Extras}</span>
|
||||
</label>
|
||||
<label class="viewSetting simpleFilter" data-settingname="HasThemeSong">
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkThemeSong" />
|
||||
<span>${ThemeSongs}</span>
|
||||
</label>
|
||||
<label class="viewSetting simpleFilter" data-settingname="HasThemeVideo">
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFeatureFilter chkThemeVideo" />
|
||||
<span>${ThemeVideos}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -18,8 +18,8 @@ function onSubmit(e) {
|
|||
function initEditor(context, settings) {
|
||||
context.querySelector('form').addEventListener('submit', onSubmit);
|
||||
|
||||
context.querySelector('.selectSortOrder').value = settings.sortOrder;
|
||||
context.querySelector('.selectSortBy').value = settings.sortBy;
|
||||
context.querySelector('.selectSortOrder').value = settings.SortOrder;
|
||||
context.querySelector('.selectSortBy').value = settings.SortBy;
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
|
@ -37,9 +37,18 @@ function fillSortBy(context, options) {
|
|||
}).join('');
|
||||
}
|
||||
|
||||
function saveValues(context, settingsKey) {
|
||||
userSettings.setFilter(settingsKey + '-sortorder', context.querySelector('.selectSortOrder').value);
|
||||
userSettings.setFilter(settingsKey + '-sortby', context.querySelector('.selectSortBy').value);
|
||||
function saveValues(context, settingsKey, setSortValues) {
|
||||
if (setSortValues) {
|
||||
setSortValues((prevState) => ({
|
||||
...prevState,
|
||||
StartIndex: 0,
|
||||
SortBy: context.querySelector('.selectSortBy').value,
|
||||
SortOrder: context.querySelector('.selectSortOrder').value
|
||||
}));
|
||||
} else {
|
||||
userSettings.setFilter(settingsKey + '-sortorder', context.querySelector('.selectSortOrder').value);
|
||||
userSettings.setFilter(settingsKey + '-sortby', context.querySelector('.selectSortBy').value);
|
||||
}
|
||||
}
|
||||
|
||||
class SortMenu {
|
||||
|
@ -95,7 +104,7 @@ class SortMenu {
|
|||
}
|
||||
|
||||
if (submitted) {
|
||||
saveValues(dlg, options.settingsKey);
|
||||
saveValues(dlg, options.settingsKey, options.setSortValues);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -29,13 +29,24 @@ function initEditor(context, settings) {
|
|||
context.querySelector('.selectImageType').value = settings.imageType || 'primary';
|
||||
}
|
||||
|
||||
function saveValues(context, settings, settingsKey) {
|
||||
const elems = context.querySelectorAll('.viewSetting-checkboxContainer');
|
||||
for (const elem of elems) {
|
||||
userSettings.set(settingsKey + '-' + elem.getAttribute('data-settingname'), elem.querySelector('input').checked);
|
||||
}
|
||||
function saveValues(context, settings, settingsKey, setviewsettings) {
|
||||
if (setviewsettings) {
|
||||
setviewsettings((prevState) => ({
|
||||
...prevState,
|
||||
StartIndex: 0,
|
||||
imageType: context.querySelector('.selectImageType').value,
|
||||
showTitle: context.querySelector('.chkShowTitle').checked || false,
|
||||
showYear: context.querySelector('.chkShowYear').checked || false,
|
||||
cardLayout: context.querySelector('.chkEnableCardLayout').checked || false
|
||||
}));
|
||||
} else {
|
||||
const elems = context.querySelectorAll('.viewSetting-checkboxContainer');
|
||||
for (const elem of elems) {
|
||||
userSettings.set(settingsKey + '-' + elem.getAttribute('data-settingname'), elem.querySelector('input').checked);
|
||||
}
|
||||
|
||||
userSettings.set(settingsKey + '-imageType', context.querySelector('.selectImageType').value);
|
||||
userSettings.set(settingsKey + '-imageType', context.querySelector('.selectImageType').value);
|
||||
}
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
|
@ -57,7 +68,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 +110,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 () {
|
||||
|
@ -125,12 +137,11 @@ class ViewSettings {
|
|||
}
|
||||
|
||||
if (submitted) {
|
||||
saveValues(dlg, options.settings, options.settingsKey);
|
||||
resolve();
|
||||
return;
|
||||
saveValues(dlg, options.settings, options.settingsKey, options.setviewsettings);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
reject();
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,24 +17,31 @@
|
|||
|
||||
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide chkTitleContainer" data-settingname="showTitle">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" />
|
||||
<input is="emby-checkbox" type="checkbox" class="chkShowTitle" />
|
||||
<span>${ShowTitle}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide chkYearContainer" data-settingname="showYear">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" />
|
||||
<input is="emby-checkbox" type="checkbox" class="chkShowYear" />
|
||||
<span>${ShowYear}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide" data-settingname="groupBySeries">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" />
|
||||
<input is="emby-checkbox" type="checkbox" class="chkGroupBySeries" />
|
||||
<span>${GroupBySeries}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer viewSetting viewSetting-checkboxContainer hide chkCardLayoutContainer" data-settingname="cardLayout">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" class="chkEnableCardLayout" />
|
||||
<span>${EnableCardLayout}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue