diff --git a/src/apps/experimental/components/library/NewCollectionButton.tsx b/src/apps/experimental/components/library/NewCollectionButton.tsx
new file mode 100644
index 00000000000..e337de7ddd2
--- /dev/null
+++ b/src/apps/experimental/components/library/NewCollectionButton.tsx
@@ -0,0 +1,34 @@
+import React, { FC, useCallback } from 'react';
+import { IconButton } from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import globalize from 'scripts/globalize';
+
+const NewCollectionButton: FC = () => {
+ const showCollectionEditor = useCallback(() => {
+ import('components/collectionEditor/collectionEditor').then(
+ ({ default: CollectionEditor }) => {
+ const serverId = window.ApiClient.serverId();
+ const collectionEditor = new CollectionEditor();
+ collectionEditor.show({
+ items: [],
+ serverId: serverId
+ }).catch(() => {
+ // closed collection editor
+ });
+ }).catch(err => {
+ console.error('[NewCollection] failed to load collection editor', err);
+ });
+ }, []);
+
+ return (
+
+
+
+ );
+};
+
+export default NewCollectionButton;
diff --git a/src/apps/experimental/components/library/PlayAllButton.tsx b/src/apps/experimental/components/library/PlayAllButton.tsx
new file mode 100644
index 00000000000..d7fb0903807
--- /dev/null
+++ b/src/apps/experimental/components/library/PlayAllButton.tsx
@@ -0,0 +1,57 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
+import React, { FC, useCallback } from 'react';
+import { IconButton } from '@mui/material';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+
+import { playbackManager } from 'components/playback/playbackmanager';
+import globalize from 'scripts/globalize';
+import { getFiltersQuery } from 'utils/items';
+import { LibraryViewSettings } from 'types/library';
+import { LibraryTab } from 'types/libraryTab';
+
+interface PlayAllButtonProps {
+ item: BaseItemDto | undefined;
+ items: BaseItemDto[];
+ viewType: LibraryTab;
+ hasFilters: boolean;
+ libraryViewSettings: LibraryViewSettings
+}
+
+const PlayAllButton: FC = ({ item, items, viewType, hasFilters, libraryViewSettings }) => {
+ const play = useCallback(() => {
+ if (item && !hasFilters) {
+ playbackManager.play({
+ items: [item],
+ autoplay: true,
+ queryOptions: {
+ SortBy: [libraryViewSettings.SortBy],
+ SortOrder: [libraryViewSettings.SortOrder]
+ }
+ });
+ } else {
+ playbackManager.play({
+ items: items,
+ autoplay: true,
+ queryOptions: {
+ ParentId: item?.Id ?? undefined,
+ ...getFiltersQuery(viewType, libraryViewSettings),
+ SortBy: [libraryViewSettings.SortBy],
+ SortOrder: [libraryViewSettings.SortOrder]
+ }
+
+ });
+ }
+ }, [hasFilters, item, items, libraryViewSettings, viewType]);
+
+ return (
+
+
+
+ );
+};
+
+export default PlayAllButton;
diff --git a/src/apps/experimental/components/library/QueueButton.tsx b/src/apps/experimental/components/library/QueueButton.tsx
new file mode 100644
index 00000000000..fdc6a7666b2
--- /dev/null
+++ b/src/apps/experimental/components/library/QueueButton.tsx
@@ -0,0 +1,39 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
+import React, { FC, useCallback } from 'react';
+import { IconButton } from '@mui/material';
+import QueueIcon from '@mui/icons-material/Queue';
+
+import { playbackManager } from 'components/playback/playbackmanager';
+import globalize from 'scripts/globalize';
+
+interface QueueButtonProps {
+ item: BaseItemDto | undefined
+ items: BaseItemDto[];
+ hasFilters: boolean;
+}
+
+const QueueButton: FC = ({ item, items, hasFilters }) => {
+ const queue = useCallback(() => {
+ if (item && !hasFilters) {
+ playbackManager.queue({
+ items: [item]
+ });
+ } else {
+ playbackManager.queue({
+ items: items
+ });
+ }
+ }, [hasFilters, item, items]);
+
+ return (
+
+
+
+ );
+};
+
+export default QueueButton;
diff --git a/src/apps/experimental/components/library/ShuffleButton.tsx b/src/apps/experimental/components/library/ShuffleButton.tsx
new file mode 100644
index 00000000000..c81ee4c4bac
--- /dev/null
+++ b/src/apps/experimental/components/library/ShuffleButton.tsx
@@ -0,0 +1,49 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
+import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
+import React, { FC, useCallback } from 'react';
+import { IconButton } from '@mui/material';
+import ShuffleIcon from '@mui/icons-material/Shuffle';
+
+import { playbackManager } from 'components/playback/playbackmanager';
+import globalize from 'scripts/globalize';
+import { getFiltersQuery } from 'utils/items';
+import { LibraryViewSettings } from 'types/library';
+import { LibraryTab } from 'types/libraryTab';
+
+interface ShuffleButtonProps {
+ item: BaseItemDto | undefined;
+ items: BaseItemDto[];
+ viewType: LibraryTab
+ hasFilters: boolean;
+ libraryViewSettings: LibraryViewSettings
+}
+
+const ShuffleButton: FC = ({ item, items, viewType, hasFilters, libraryViewSettings }) => {
+ const shuffle = useCallback(() => {
+ if (item && !hasFilters) {
+ playbackManager.shuffle(item);
+ } else {
+ playbackManager.play({
+ items: items,
+ autoplay: true,
+ queryOptions: {
+ ParentId: item?.Id ?? undefined,
+ ...getFiltersQuery(viewType, libraryViewSettings),
+ SortBy: [ItemSortBy.Random]
+ }
+ });
+ }
+ }, [hasFilters, item, items, libraryViewSettings, viewType]);
+
+ return (
+
+
+
+ );
+};
+
+export default ShuffleButton;
diff --git a/src/apps/experimental/components/library/SortButton.tsx b/src/apps/experimental/components/library/SortButton.tsx
index 7deeae349b0..2c7425f0dea 100644
--- a/src/apps/experimental/components/library/SortButton.tsx
+++ b/src/apps/experimental/components/library/SortButton.tsx
@@ -98,7 +98,7 @@ const SortButton: FC = ({
title={globalize.translate('Sort')}
sx={{ ml: 2 }}
aria-describedby={id}
- className='paper-icon-button-light btnShuffle autoSize'
+ className='paper-icon-button-light btnSort autoSize'
onClick={handleClick}
>
diff --git a/src/apps/experimental/components/library/ViewSettingsButton.tsx b/src/apps/experimental/components/library/ViewSettingsButton.tsx
index cec5090accb..b1ca1679e00 100644
--- a/src/apps/experimental/components/library/ViewSettingsButton.tsx
+++ b/src/apps/experimental/components/library/ViewSettingsButton.tsx
@@ -100,7 +100,7 @@ const ViewSettingsButton: FC = ({
title={globalize.translate('ButtonSelectView')}
sx={{ ml: 2 }}
aria-describedby={id}
- className='paper-icon-button-light btnShuffle autoSize'
+ className='paper-icon-button-light btnSelectView autoSize'
onClick={handleClick}
>
diff --git a/src/utils/items.ts b/src/utils/items.ts
new file mode 100644
index 00000000000..08936a3fde0
--- /dev/null
+++ b/src/utils/items.ts
@@ -0,0 +1,158 @@
+import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
+import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
+import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
+import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
+import * as userSettings from 'scripts/settings/userSettings';
+import { EpisodeFilter, FeatureFilters, LibraryViewSettings, ParentId, VideoBasicFilter, ViewMode } from '../types/library';
+import { LibraryTab } from 'types/libraryTab';
+
+export const getVideoBasicFilter = (libraryViewSettings: LibraryViewSettings) => {
+ let isHd;
+
+ if (libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.IsHD)) {
+ isHd = true;
+ }
+
+ if (libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.IsSD)) {
+ isHd = false;
+ }
+
+ return {
+ isHd,
+ is4K: libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.Is4K) ?
+ true :
+ undefined,
+ is3D: libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.Is3D) ?
+ true :
+ undefined
+ };
+};
+
+export const getFeatureFilters = (libraryViewSettings: LibraryViewSettings) => {
+ return {
+ hasSubtitles: libraryViewSettings.Filters?.Features?.includes(FeatureFilters.HasSubtitles) ?
+ true :
+ undefined,
+ hasTrailer: libraryViewSettings.Filters?.Features?.includes(FeatureFilters.HasTrailer) ?
+ true :
+ undefined,
+ hasSpecialFeature: libraryViewSettings.Filters?.Features?.includes(
+ FeatureFilters.HasSpecialFeature
+ ) ?
+ true :
+ undefined,
+ hasThemeSong: libraryViewSettings.Filters?.Features?.includes(FeatureFilters.HasThemeSong) ?
+ true :
+ undefined,
+ hasThemeVideo: libraryViewSettings.Filters?.Features?.includes(
+ FeatureFilters.HasThemeVideo
+ ) ?
+ true :
+ undefined
+ };
+};
+
+export const getEpisodeFilter = (
+ viewType: LibraryTab,
+ libraryViewSettings: LibraryViewSettings
+) => {
+ return {
+ parentIndexNumber: libraryViewSettings.Filters?.EpisodeFilter?.includes(
+ EpisodeFilter.ParentIndexNumber
+ ) ?
+ 0 :
+ undefined,
+ isMissing:
+ viewType === LibraryTab.Episodes ?
+ !!libraryViewSettings.Filters?.EpisodeFilter?.includes(EpisodeFilter.IsMissing) :
+ undefined,
+ isUnaired: libraryViewSettings.Filters?.EpisodeFilter?.includes(EpisodeFilter.IsUnaired) ?
+ true :
+ undefined
+ };
+};
+
+const getItemFieldsEnum = (
+ viewType: LibraryTab,
+ libraryViewSettings: LibraryViewSettings
+) => {
+ const itemFields: ItemFields[] = [];
+
+ if (viewType !== LibraryTab.Networks) {
+ itemFields.push(ItemFields.BasicSyncInfo, ItemFields.MediaSourceCount);
+ }
+
+ if (libraryViewSettings.ImageType === ImageType.Primary) {
+ itemFields.push(ItemFields.PrimaryImageAspectRatio);
+ }
+
+ if (viewType === LibraryTab.Networks) {
+ itemFields.push(
+ ItemFields.DateCreated,
+ ItemFields.PrimaryImageAspectRatio
+ );
+ }
+
+ return itemFields;
+};
+
+export const getFieldsQuery = (
+ viewType: LibraryTab,
+ libraryViewSettings: LibraryViewSettings
+) => {
+ return {
+ fields: getItemFieldsEnum(viewType, libraryViewSettings)
+ };
+};
+
+export const getLimitQuery = () => {
+ return {
+ limit: userSettings.libraryPageSize(undefined) || undefined
+ };
+};
+
+export const getAlphaPickerQuery = (libraryViewSettings: LibraryViewSettings) => {
+ const alphabetValue = libraryViewSettings.Alphabet !== null ?
+ libraryViewSettings.Alphabet : undefined;
+
+ return {
+ nameLessThan: alphabetValue === '#' ? 'A' : undefined,
+ nameStartsWith: alphabetValue === '#' ? undefined : alphabetValue
+ };
+};
+
+export const getFiltersQuery = (
+ viewType: LibraryTab,
+ libraryViewSettings: LibraryViewSettings
+) => {
+ return {
+ ...getFeatureFilters(libraryViewSettings),
+ ...getEpisodeFilter(viewType, libraryViewSettings),
+ ...getVideoBasicFilter(libraryViewSettings),
+ seriesStatus: libraryViewSettings?.Filters?.SeriesStatus,
+ videoTypes: libraryViewSettings?.Filters?.VideoTypes,
+ filters: libraryViewSettings?.Filters?.Status,
+ genres: libraryViewSettings?.Filters?.Genres,
+ officialRatings: libraryViewSettings?.Filters?.OfficialRatings,
+ tags: libraryViewSettings?.Filters?.Tags,
+ years: libraryViewSettings?.Filters?.Years,
+ studioIds: libraryViewSettings?.Filters?.StudioIds
+ };
+};
+
+export const getSettingsKey = (viewType: LibraryTab, parentId: ParentId) => {
+ return `${viewType} - ${parentId}`;
+};
+
+export const getDefaultLibraryViewSettings = (): LibraryViewSettings => {
+ return {
+ ShowTitle: true,
+ ShowYear: false,
+ ViewMode: ViewMode.GridView,
+ ImageType: ImageType.Primary,
+ CardLayout: false,
+ SortBy: ItemSortBy.SortName,
+ SortOrder: SortOrder.Ascending,
+ StartIndex: 0
+ };
+};