From 23cf0604533e8d2c39052b5596a27423c8104edf Mon Sep 17 00:00:00 2001 From: dkanada Date: Wed, 18 Mar 2026 23:13:15 +0900 Subject: [PATCH 1/2] initial sections for book library view --- .../components/library/SortButton.tsx | 7 +++ .../library/filter/FilterButton.tsx | 2 + .../library/filter/FiltersStatus.tsx | 10 +++- .../libraries/constants/libraryRoutes.ts | 26 ++++++++ .../experimental/routes/asyncRoutes/user.ts | 1 + src/apps/experimental/routes/books/index.tsx | 60 +++++++++++++++++++ src/components/router/appRouter.js | 8 ++- src/strings/en-us.json | 2 + src/types/sections.ts | 9 +++ src/utils/sections.ts | 26 ++++++++ 10 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 src/apps/experimental/routes/books/index.tsx diff --git a/src/apps/experimental/components/library/SortButton.tsx b/src/apps/experimental/components/library/SortButton.tsx index f46d2ca5bf..7f470db1ac 100644 --- a/src/apps/experimental/components/library/SortButton.tsx +++ b/src/apps/experimental/components/library/SortButton.tsx @@ -83,6 +83,13 @@ const sortOptionsMapping: SortOptionsMapping = { { label: 'OptionReleaseDate', value: ItemSortBy.ProductionYear }, { label: 'OptionDateAdded', value: ItemSortBy.DateCreated } ], + [LibraryTab.Books]: [ + { label: 'Name', value: ItemSortBy.SortName }, + { label: 'OptionReleaseDate', value: ItemSortBy.ProductionYear }, + { label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, + { label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed }, + { label: 'OptionRandom', value: ItemSortBy.Random } + ], [LibraryTab.Songs]: [ { label: 'Name', value: ItemSortBy.SortName }, { label: 'Album', value: ItemSortBy.Album }, diff --git a/src/apps/experimental/components/library/filter/FilterButton.tsx b/src/apps/experimental/components/library/filter/FilterButton.tsx index 86f05fd8a7..70e82f6e67 100644 --- a/src/apps/experimental/components/library/filter/FilterButton.tsx +++ b/src/apps/experimental/components/library/filter/FilterButton.tsx @@ -135,6 +135,7 @@ const FilterButton: FC = ({ || viewType === LibraryTab.Artists || viewType === LibraryTab.Songs || viewType === LibraryTab.Episodes + || viewType === LibraryTab.Books ); }; @@ -142,6 +143,7 @@ const FilterButton: FC = ({ return ( viewType === LibraryTab.Movies || viewType === LibraryTab.Series + || viewType === LibraryTab.Books ); }; diff --git a/src/apps/experimental/components/library/filter/FiltersStatus.tsx b/src/apps/experimental/components/library/filter/FiltersStatus.tsx index f946cdc911..2230d21a05 100644 --- a/src/apps/experimental/components/library/filter/FiltersStatus.tsx +++ b/src/apps/experimental/components/library/filter/FiltersStatus.tsx @@ -8,7 +8,7 @@ import globalize from 'lib/globalize'; import { LibraryViewSettings } from 'types/library'; import { LibraryTab } from 'types/libraryTab'; -const statusFiltersOptions = [ +const defaultFiltersOptions = [ { label: 'Played', value: ItemFilter.IsPlayed }, { label: 'Unplayed', value: ItemFilter.IsUnplayed }, { label: 'Favorite', value: ItemFilter.IsFavorite }, @@ -26,6 +26,14 @@ const FiltersStatus: FC = ({ libraryViewSettings, setLibraryViewSettings }) => { + let statusFiltersOptions = defaultFiltersOptions; + + if (viewType === LibraryTab.Books) { + statusFiltersOptions = defaultFiltersOptions.map((option) => ( + option.value === ItemFilter.IsResumable ? { ...option, label: 'ContinueReading' } : option + )); + } + const onFiltersStatusChange = useCallback( (event: React.ChangeEvent) => { event.preventDefault(); diff --git a/src/apps/experimental/features/libraries/constants/libraryRoutes.ts b/src/apps/experimental/features/libraries/constants/libraryRoutes.ts index c4d833e4a1..42d0e80e81 100644 --- a/src/apps/experimental/features/libraries/constants/libraryRoutes.ts +++ b/src/apps/experimental/features/libraries/constants/libraryRoutes.ts @@ -39,6 +39,32 @@ export const LibraryRoutes: LibraryRoute[] = [ } ] }, + { + path: '/books', + views: [ + { + index: 0, + label: 'Books', + view: LibraryTab.Books, + isDefault: true + }, + { + index: 1, + label: 'Suggestions', + view: LibraryTab.Suggestions + }, + { + index: 2, + label: 'Genres', + view: LibraryTab.Genres + }, + { + index: 3, + label: 'Favorites', + view: LibraryTab.Favorites + } + ] + }, { path: '/movies', views: [ diff --git a/src/apps/experimental/routes/asyncRoutes/user.ts b/src/apps/experimental/routes/asyncRoutes/user.ts index 997182a6a3..abf5672212 100644 --- a/src/apps/experimental/routes/asyncRoutes/user.ts +++ b/src/apps/experimental/routes/asyncRoutes/user.ts @@ -7,6 +7,7 @@ export const ASYNC_USER_ROUTES: AsyncRoute[] = [ { path: 'livetv', type: AppType.Experimental }, { path: 'movies', type: AppType.Experimental }, { path: 'music', type: AppType.Experimental }, + { path: 'books', type: AppType.Experimental }, { path: 'mypreferencesdisplay', page: 'user/display', type: AppType.Experimental }, { path: 'mypreferencesmenu', page: 'user/settings' }, { path: 'quickconnect', page: 'quickConnect' }, diff --git a/src/apps/experimental/routes/books/index.tsx b/src/apps/experimental/routes/books/index.tsx new file mode 100644 index 0000000000..49415d043f --- /dev/null +++ b/src/apps/experimental/routes/books/index.tsx @@ -0,0 +1,60 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import React, { FC } from 'react'; +import useCurrentTab from 'hooks/useCurrentTab'; +import Page from 'components/Page'; +import PageTabContent from '../../components/library/PageTabContent'; +import { LibraryTab } from 'types/libraryTab'; +import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; +import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent'; +import { BookSuggestionsSectionsView } from 'types/sections'; + +const booksTabContent: LibraryTabContent = { + viewType: LibraryTab.Books, + collectionType: CollectionType.Books, + itemType: [BaseItemKind.Book] +}; + +const suggestionsTabContent: LibraryTabContent = { + viewType: LibraryTab.Suggestions, + collectionType: CollectionType.Books, + sectionsView: BookSuggestionsSectionsView +}; + +const genresTabContent: LibraryTabContent = { + viewType: LibraryTab.Genres, + collectionType: CollectionType.Books, + itemType: [BaseItemKind.Book] +}; + +const favoritesTabContent: LibraryTabContent = { + viewType: LibraryTab.Favorites, + collectionType: CollectionType.Books, + itemType: [BaseItemKind.Book] +}; + +const booksTabMapping: LibraryTabMapping = { + 0: booksTabContent, + 1: suggestionsTabContent, + 2: genresTabContent, + 3: favoritesTabContent +}; + +const Books: FC = () => { + const { libraryId, activeTab } = useCurrentTab(); + const currentTab = booksTabMapping[activeTab]; + + return ( + + + + ); +}; + +export default Books; diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index e9332c9947..ace0a7a9a5 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -401,6 +401,12 @@ class AppRouter { } if (context !== 'folders' && !itemHelper.isLocalItem(item)) { + const layoutMode = localStorage.getItem('layout') ?? LayoutMode.Experimental; + + if (layoutMode === LayoutMode.Experimental && item.CollectionType == CollectionType.Books) { + return `#/books?topParentId=${item.Id}`; + } + if (item.CollectionType == CollectionType.Movies) { url = `#/movies?topParentId=${item.Id}&collectionType=${item.CollectionType}`; @@ -431,8 +437,6 @@ class AppRouter { return url; } - const layoutMode = localStorage.getItem('layout'); - if (layoutMode === LayoutMode.Experimental && item.CollectionType == CollectionType.Homevideos) { url = '#/homevideos?topParentId=' + item.Id; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 9f28a86859..60fc6f1f35 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -193,6 +193,7 @@ "ConnectAnyway": "Connect Anyway", "Console": "Console", "ContinueWatching": "Continue watching", + "ContinueReading": "Continue reading", "Continuing": "Continuing", "Copied": "Copied", "Copy": "Copy", @@ -458,6 +459,7 @@ "HeaderKeepRecording": "Keep Recording", "HeaderKeepSeries": "Keep Series", "HeaderKodiMetadataHelp": "To enable or disable NFO metadata, edit a library and find the 'Metadata savers' section.", + "HeaderLatestBooks": "Recently Added Books", "HeaderLatestEpisodes": "Recently Added Episodes", "HeaderLatestMedia": "Recently Added Media", "HeaderLatestMovies": "Recently Added Movies", diff --git a/src/types/sections.ts b/src/types/sections.ts index 4f5d072336..4a666bbdbe 100644 --- a/src/types/sections.ts +++ b/src/types/sections.ts @@ -51,6 +51,8 @@ export enum SectionType { LatestRecordings = 'LatestRecordings', RecordingFolders = 'RecordingFolders', ActiveRecordings = 'ActiveRecordings', + ContinueReading = 'ContinueReading', + LatestBooks = 'LatestBooks', UpcomingRecordings = 'UpcomingRecordings' } @@ -63,6 +65,13 @@ export interface Section { cardOptions: CardOptions; } +export const BookSuggestionsSectionsView: SectionsView = { + suggestionSections: [ + SectionType.LatestBooks, + SectionType.ContinueReading + ] +}; + export const MovieSuggestionsSectionsView: SectionsView = { suggestionSections: [ SectionType.ContinueWatchingMovies, diff --git a/src/utils/sections.ts b/src/utils/sections.ts index c3a25980e0..755bea7697 100644 --- a/src/utils/sections.ts +++ b/src/utils/sections.ts @@ -38,6 +38,32 @@ export const getSuggestionSections = (): Section[] => { showYear: true } }, + { + name: 'HeaderContinueReading', + apiMethod: SectionApiMethod.ResumeItems, + itemTypes: 'Book', + type: SectionType.ContinueReading, + parametersOptions: { + includeItemTypes: [BaseItemKind.Book] + }, + cardOptions: { + overlayPlayButton: true, + shape: CardShape.PortraitOverflow + } + }, + { + name: 'HeaderLatestBooks', + apiMethod: SectionApiMethod.LatestMedia, + itemTypes: 'Book', + type: SectionType.LatestBooks, + parametersOptions: { + includeItemTypes: [BaseItemKind.Book] + }, + cardOptions: { + overlayPlayButton: true, + shape: CardShape.PortraitOverflow + } + }, { name: 'HeaderLatestMovies', apiMethod: SectionApiMethod.LatestMedia, From a204f30fca5f45f4fcefc32eaae43d46ca06f72c Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 20 Mar 2026 02:25:45 +0900 Subject: [PATCH 2/2] change operator for layout mode check Co-authored-by: Bill Thornton --- src/components/router/appRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index ace0a7a9a5..fd0898d3cd 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -401,7 +401,7 @@ class AppRouter { } if (context !== 'folders' && !itemHelper.isLocalItem(item)) { - const layoutMode = localStorage.getItem('layout') ?? LayoutMode.Experimental; + const layoutMode = localStorage.getItem('layout') || LayoutMode.Experimental; if (layoutMode === LayoutMode.Experimental && item.CollectionType == CollectionType.Books) { return `#/books?topParentId=${item.Id}`;