From f3c65f09e6d2f468789123f29ff1ff2abf94748b Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Tue, 4 Aug 2026 09:10:54 -0700 Subject: [PATCH 1/4] feat: Port library settings schema and server sync mappings --- settings/settings.json | 28 ++++++++++++++++++++++++++++ source/utils/settingsSync.bs | 4 ++++ 2 files changed, 32 insertions(+) diff --git a/settings/settings.json b/settings/settings.json index 3d5108fdd..34037e7f9 100644 --- a/settings/settings.json +++ b/settings/settings.json @@ -2727,6 +2727,34 @@ "type": "bool", "default": "false" }, + { + "title": "Merge Recently Added Libraries by Type", + "description": "Combine separate libraries of the same type into merged rows on the home page.", + "settingName": "ui.home.mergeRecentRowsByType", + "type": "bool", + "default": "false" + }, + { + "title": "Show Media Details", + "description": "Show details of the selected item at the top of Library pages.", + "settingName": "ui.library.showMediaDetails", + "type": "bool", + "default": "true" + }, + { + "title": "Use Detailed Sub-Headings", + "description": "Show detailed sub-row metadata on Library pages.", + "settingName": "ui.library.useDetailedSubHeadings", + "type": "bool", + "default": "true" + }, + { + "title": "Hide Backdrops while Browsing", + "description": "Hide fanart backdrops when browsing libraries.", + "settingName": "ui.library.hideBackdrops", + "type": "bool", + "default": "false" + }, { "title": "Forget Filters", "description": "Forget applied library filters when Jellyfin is closed.", diff --git a/source/utils/settingsSync.bs b/source/utils/settingsSync.bs index 33cdb3c89..efc5849fe 100644 --- a/source/utils/settingsSync.bs +++ b/source/utils/settingsSync.bs @@ -26,6 +26,10 @@ namespace settingsSync { pluginKey: "homeImageUseSeriesImage", rokuKey: "ui.home.preferThumbnails", type: "boolInvert" }, { pluginKey: "mergeContinueWatchingNextUp", rokuKey: "ui.home.mergeContinueWatchingNextUp", type: "bool" }, { pluginKey: "enableMultiServerLibraries", rokuKey: "ui.home.enableMultiServer", type: "bool" }, + { pluginKey: "mergeRecentRowsByType", rokuKey: "ui.home.mergeRecentRowsByType", type: "bool" }, + { pluginKey: "showMediaDetailsOnLibraryPage", rokuKey: "ui.library.showMediaDetails", type: "bool" }, + { pluginKey: "useDetailedSubHeadings", rokuKey: "ui.library.useDetailedSubHeadings", type: "bool" }, + { pluginKey: "hideBackdropsInLibraries", rokuKey: "ui.library.hideBackdrops", type: "bool" }, { pluginKey: "mediaBarMode", rokuKey: "ui.home.mediaBarMode", type: "direct" }, { pluginKey: "mediaBarSourceType", rokuKey: "ui.home.mediaBarSourceType", type: "direct" }, { pluginKey: "mediaBarLibraryIds", rokuKey: "ui.home.mediaBarLibraryIds", type: "jsonArray" }, From e97bcf87e73f1b3bfc49365eb9dfada3aaa6943b Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Tue, 4 Aug 2026 12:02:35 -0700 Subject: [PATCH 2/4] Wire up library view toggles for media details and backdrops Implement UI handling for ported library settings in Roku components: - Wire ui.library.showMediaDetails and ui.library.hideBackdrops in VisualLibraryScene.bs - Wire ui.library.useDetailedSubHeadings in GridItem.bs --- components/ItemGrid/GridItem.bs | 5 +++++ components/Libraries/VisualLibraryScene.bs | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/components/ItemGrid/GridItem.bs b/components/ItemGrid/GridItem.bs index 26edf42e0..eb36833d2 100644 --- a/components/ItemGrid/GridItem.bs +++ b/components/ItemGrid/GridItem.bs @@ -154,6 +154,11 @@ sub itemContentChanged() else end if + useDetailedSubHeadings = toBoolean(chainLookupReturn(m.global.session, "user.settings.ui.library.useDetailedSubHeadings", true)) + if not useDetailedSubHeadings and isValid(itemData.Title) + m.itemText.text = itemData.Title + end if + 'If Poster not loaded, ensure "blue box" is shown until loaded if m.itemPoster.loadStatus <> "ready" m.posterText.visible = true diff --git a/components/Libraries/VisualLibraryScene.bs b/components/Libraries/VisualLibraryScene.bs index 1d0dc7dad..fe920f207 100644 --- a/components/Libraries/VisualLibraryScene.bs +++ b/components/Libraries/VisualLibraryScene.bs @@ -1143,6 +1143,17 @@ sub onItemFocused() SetOfficialRating(string.EMPTY) end if + showMediaDetails = toBoolean(chainLookupReturn(m.global.session, "user.settings.ui.library.showMediaDetails", true)) + if not showMediaDetails or isStringEqual(m.view, "Genres") + m.infoGroup.visible = false + m.selectedItemName.visible = false + end if + + hideBackdrops = toBoolean(chainLookupReturn(m.global.session, "user.settings.ui.library.hideBackdrops", false)) + if hideBackdrops and isValid(m.overhang) and m.overhang.hasField("backgroundUri") + m.overhang.backgroundUri = "" + end if + updateStatusBar() end sub From 4564b987a537c6f77f7f70d12a4e0eb9f49f46d0 Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Tue, 4 Aug 2026 15:37:54 -0700 Subject: [PATCH 3/4] feat: Implement mergeRecentRowsByType, showMediaDetails, useDetailedSubHeadings, and hideBackdrops logic - Added mergeRecentRowsByType support to group home screen Recently Added rows by collection type - Added itemIds support to LoadItemsTask for fetching and merging items across multiple libraries - Added showMediaDetails toggling for library detail header bars in VisualLibraryScene - Added useDetailedSubHeadings toggling for grid poster sub-headings in GridItemMedium - Added hideBackdrops support to suppress fanart images while browsing in setBackgroundWithQueue --- components/ItemGrid/GridItemMedium.bs | 8 +- components/Libraries/VisualLibraryScene.bs | 16 ++- components/home/HomeRows.bs | 111 +++++++++++++++++++-- components/home/LoadItemsTask.bs | 45 ++++++--- components/home/LoadItemsTask.xml | 1 + source/utils/misc.bs | 6 ++ 6 files changed, 162 insertions(+), 25 deletions(-) diff --git a/components/ItemGrid/GridItemMedium.bs b/components/ItemGrid/GridItemMedium.bs index 0b9aaa12b..a423f2d8b 100644 --- a/components/ItemGrid/GridItemMedium.bs +++ b/components/ItemGrid/GridItemMedium.bs @@ -85,10 +85,16 @@ sub itemContentChanged() m.title.visible = false m.itemTextExtra.visible = false + useSubheadings = get_user_setting("ui.library.useDetailedSubHeadings") + showSubheadings = true + if isValid(useSubheadings) and ((type(useSubheadings) = "Boolean" and useSubheadings = false) or (type(useSubheadings) = "String" and LCase(useSubheadings) = "false")) + showSubheadings = false + end if + if isValid(m.topParent.showItemTitles) if LCase(m.topParent.showItemTitles) = "showalways" m.title.visible = true - m.itemTextExtra.visible = true + m.itemTextExtra.visible = showSubheadings end if end if diff --git a/components/Libraries/VisualLibraryScene.bs b/components/Libraries/VisualLibraryScene.bs index fe920f207..0d683b883 100644 --- a/components/Libraries/VisualLibraryScene.bs +++ b/components/Libraries/VisualLibraryScene.bs @@ -461,8 +461,9 @@ sub loadInitialItems() ' Presentation view: show info panel, hide item titles m.top.showItemTitles = "hidealways" - m.infoGroup.visible = true - m.selectedItemName.visible = true + showDetails = isShowMediaDetails() + m.infoGroup.visible = showDetails + m.selectedItemName.visible = showDetails posterOrientation = getPosterOrientation() @@ -1123,7 +1124,7 @@ sub onItemFocused() end if ' Show the selected item name - m.selectedItemName.visible = true + m.selectedItemName.visible = isShowMediaDetails() if isValid(itemData.ProductionYear) SetProductionYear(str(itemData.ProductionYear)) @@ -1767,6 +1768,15 @@ sub reclaimResources() m.genreGrid.content = m.genreData end sub +function isShowMediaDetails() as boolean + showDetails = get_user_setting("ui.library.showMediaDetails") + if not isValid(showDetails) then return true + if type(showDetails) = "String" or type(showDetails) = "roString" + return LCase(showDetails) = "true" + end if + return showDetails = true +end function + diff --git a/components/home/HomeRows.bs b/components/home/HomeRows.bs index 80f3d6970..698a30f7e 100644 --- a/components/home/HomeRows.bs +++ b/components/home/HomeRows.bs @@ -58,6 +58,15 @@ function isFeaturedMediaEnabled() as boolean return mediaBarEnabled end function +function isMergeRecentRowsByType() as boolean + mergeRecent = get_user_setting("ui.home.mergeRecentRowsByType") + if not isValid(mergeRecent) then return false + if type(mergeRecent) = "String" or type(mergeRecent) = "roString" + return LCase(mergeRecent) = "true" + end if + return mergeRecent = true +end function + sub init() m.scene = m.top.getScene() @@ -666,12 +675,28 @@ function getExpectedRowContribution(sectionName as string) as integer end if m.filteredLatest = filterNodeArray(m.libraryData, "id", excludeList) - rowCount = 0 - for each latestLibrary in m.filteredLatest - if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program" - rowCount++ - end if - end for + if isMergeRecentRowsByType() + seenTypes = {} + rowCount = 0 + for each latestLibrary in m.filteredLatest + if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program" + colType = LCase(toNormalizedString(latestLibrary.json.CollectionType ?? latestLibrary.collectionType ?? "other")) + if not seenTypes.DoesExist(colType) + seenTypes[colType] = true + rowCount++ + end if + end if + end for + return rowCount + else + rowCount = 0 + for each latestLibrary in m.filteredLatest + if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program" + rowCount++ + end if + end for + return rowCount + end if return rowCount end if @@ -1306,7 +1331,75 @@ sub createLatestInRows() end if - ' create a Recently Added in row for each library + ' create a Recently Added row for each library (or merged by type) + if isMergeRecentRowsByType() + groupedLibs = {} + groupKeys = [] + for each lib in m.filteredLatest + if not isStringEqual(lib.collectionType, "boxsets") and not isStringEqual(lib.collectionType, "livetv") and not isStringEqual(lib.json.CollectionType, "Program") + colType = LCase(toNormalizedString(lib.json.CollectionType ?? lib.collectionType ?? "other")) + if not groupedLibs.DoesExist(colType) + groupedLibs[colType] = [] + groupKeys.push(colType) + end if + groupedLibs[colType].push(lib) + end if + end for + + for each colType in groupKeys + libsInGroup = groupedLibs[colType] + libIds = [] + for each lib in libsInGroup + libIds.push(lib.id) + end for + + firstLib = libsInGroup[0] + sectionName = "" + if colType = "movies" + sectionName = `${tr("Recently Added")} ${tr("Movies")}` + else if colType = "tvshows" + sectionName = `${tr("Recently Added")} ${tr("TV Shows")}` + else if colType = "music" + sectionName = `${tr("Recently Added")} ${tr("Music")}` + else if libsInGroup.Count() = 1 + sectionName = `${tr("Recently Added in")} ${firstLib.name}` + else + sectionName = `${tr("Recently Added")} ${colType}` + end if + + imagesize = getPosterSize(false) + if colType = "movies" + imagesize = getPosterSize(false) + else if colType = "music" + imagesize = homeRowItemSizes.MUSIC_ALBUM + end if + + if not sectionExists(sectionName) + row = m.top.content.CreateChild("HomeRow") + row.title = sectionName + row.imageWidth = imagesize[0] + row.cursorSize = imagesize + end if + + try + loadLatest = createObject("roSGNode", "LoadItemsTask") + loadLatest.itemsToLoad = "latest" + loadLatest.itemIds = libIds + loadLatest.itemId = firstLib.id + + metadata = { "title": firstLib.name, "contentType": colType, "sectionName": sectionName } + loadLatest.metadata = metadata + + loadLatest.observeField("content", "updateLatestItems") + loadLatest.control = TaskControl.RUN + catch e + removeHomeSection(sectionName) + m.global.sceneManager.callFunc("standardDialog", `Error creating Recently Added rows`, { data: ["

" + `Error Message: ${e.message}` + "

"] }) + end try + end for + return + end if + for each lib in m.filteredLatest if not isStringEqual(lib.collectionType, "boxsets") and not isStringEqual(lib.collectionType, "livetv") and not isStringEqual(lib.json.CollectionType, "Program") sectionName = `${tr("Recently Added in")} ${lib.name}` @@ -1335,7 +1428,7 @@ sub createLatestInRows() loadLatest.itemId = lib.id metadata = { "title": lib.name } - metadata.Append({ "contentType": lib.json.CollectionType }) + metadata.Append({ "contentType": lib.json.CollectionType, "sectionName": sectionName }) loadLatest.metadata = metadata loadLatest.observeField("content", "updateLatestItems") @@ -2453,7 +2546,7 @@ sub updateLatestItems(msg) node.unobserveField("content") node.content = [] - sectionName = tr("Recently Added in") + " " + node.metadata.title + sectionName = node.metadata.sectionName ?? (tr("Recently Added in") + " " + node.metadata.title) if not isValidAndNotEmpty(itemData) removeHomeSection(sectionName) diff --git a/components/home/LoadItemsTask.bs b/components/home/LoadItemsTask.bs index 1957e1028..277e73807 100644 --- a/components/home/LoadItemsTask.bs +++ b/components/home/LoadItemsTask.bs @@ -57,18 +57,39 @@ end function function loadLatestMedia() as object results = [] - - params = { - userId: m.global.session.user.id, - limit: 25, - parentId: m.top.itemId, - enableImageTypes: `${ImageType.PRIMARY}, ${ImageType.BACKDROP}, ${ImageType.THUMB}`, - imageTypeLimit: 1, - enableTotalRecordCount: false, - fields: "Genres" - } - - data = api.items.GetLatest(params) + data = [] + + targetParentIds = [] + if isValidAndNotEmpty(m.top.itemIds) + targetParentIds = m.top.itemIds + else if isValidAndNotEmpty(m.top.itemId) + targetParentIds = [m.top.itemId] + end if + + if targetParentIds.Count() > 0 + seenIds = {} + for each pId in targetParentIds + params = { + userId: m.global.session.user.id, + limit: 25, + parentId: pId, + enableImageTypes: `${ImageType.PRIMARY}, ${ImageType.BACKDROP}, ${ImageType.THUMB}`, + imageTypeLimit: 1, + enableTotalRecordCount: false, + fields: "Genres" + } + libData = api.items.GetLatest(params) + if isValidAndNotEmpty(libData) + for each rawItem in libData + itemIdStr = LCase(toNormalizedString(rawItem.LookupCI("Id"))) + if isValidAndNotEmpty(itemIdStr) and not seenIds.DoesExist(itemIdStr) + seenIds[itemIdStr] = true + data.push(rawItem) + end if + end for + end if + end for + end if if not isValidAndNotEmpty(data) then return results diff --git a/components/home/LoadItemsTask.xml b/components/home/LoadItemsTask.xml index 4b697bd9a..06cc89cfd 100644 --- a/components/home/LoadItemsTask.xml +++ b/components/home/LoadItemsTask.xml @@ -4,6 +4,7 @@ + diff --git a/source/utils/misc.bs b/source/utils/misc.bs index 08d81b458..f5ff2fb89 100644 --- a/source/utils/misc.bs +++ b/source/utils/misc.bs @@ -1052,6 +1052,12 @@ end sub ' Usage: ' setBackgroundWithQueue(uri, m.newBackdrop, m.swapAnimation) function setBackgroundWithQueue(backgroundUri as string, newBackdropNode as object, swapAnimationNode as object) as boolean + hideBackdrop = get_user_setting("ui.library.hideBackdrops") + if isValid(hideBackdrop) and ((type(hideBackdrop) = "Boolean" and hideBackdrop = true) or (type(hideBackdrop) = "String" and LCase(hideBackdrop) = "true")) + newBackdropNode.uri = "" + return true + end if + animationState = LCase(swapAnimationNode.state) loadStatus = LCase(newBackdropNode.loadStatus) From bcf1affb556bd22f81df0864875fb4f3ae2e7a76 Mon Sep 17 00:00:00 2001 From: RadicalMuffinMan <103554043+RadicalMuffinMan@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:32:17 -0400 Subject: [PATCH 4/4] Fix library setting reads and merge Recently Added rows by date --- components/ItemGrid/GridItem.bs | 2 +- components/ItemGrid/GridItemMedium.bs | 10 +---- components/Libraries/VisualLibraryScene.bs | 27 +++--------- components/home/HomeRows.bs | 39 ++++++----------- components/home/LoadItemsTask.bs | 50 +++++++++++++++++++++- settings/settings.json | 14 +++--- source/utils/config.bs | 12 ++++++ source/utils/misc.bs | 3 +- 8 files changed, 89 insertions(+), 68 deletions(-) diff --git a/components/ItemGrid/GridItem.bs b/components/ItemGrid/GridItem.bs index eb36833d2..7d17957ce 100644 --- a/components/ItemGrid/GridItem.bs +++ b/components/ItemGrid/GridItem.bs @@ -154,7 +154,7 @@ sub itemContentChanged() else end if - useDetailedSubHeadings = toBoolean(chainLookupReturn(m.global.session, "user.settings.ui.library.useDetailedSubHeadings", true)) + useDetailedSubHeadings = get_user_setting_bool("ui.library.useDetailedSubHeadings", true) if not useDetailedSubHeadings and isValid(itemData.Title) m.itemText.text = itemData.Title end if diff --git a/components/ItemGrid/GridItemMedium.bs b/components/ItemGrid/GridItemMedium.bs index a423f2d8b..69d76cc71 100644 --- a/components/ItemGrid/GridItemMedium.bs +++ b/components/ItemGrid/GridItemMedium.bs @@ -85,16 +85,10 @@ sub itemContentChanged() m.title.visible = false m.itemTextExtra.visible = false - useSubheadings = get_user_setting("ui.library.useDetailedSubHeadings") - showSubheadings = true - if isValid(useSubheadings) and ((type(useSubheadings) = "Boolean" and useSubheadings = false) or (type(useSubheadings) = "String" and LCase(useSubheadings) = "false")) - showSubheadings = false - end if - if isValid(m.topParent.showItemTitles) if LCase(m.topParent.showItemTitles) = "showalways" m.title.visible = true - m.itemTextExtra.visible = showSubheadings + m.itemTextExtra.visible = get_user_setting_bool("ui.library.useDetailedSubHeadings", true) end if end if @@ -234,7 +228,7 @@ sub focusChanged() if isValid(m.topParent.showItemTitles) if LCase(m.topParent.showItemTitles) = "showonhover" m.title.visible = m.top.itemHasFocus - m.itemTextExtra.visible = m.top.itemHasFocus + m.itemTextExtra.visible = m.top.itemHasFocus and get_user_setting_bool("ui.library.useDetailedSubHeadings", true) end if end if end sub diff --git a/components/Libraries/VisualLibraryScene.bs b/components/Libraries/VisualLibraryScene.bs index 0d683b883..d59660fcb 100644 --- a/components/Libraries/VisualLibraryScene.bs +++ b/components/Libraries/VisualLibraryScene.bs @@ -461,7 +461,7 @@ sub loadInitialItems() ' Presentation view: show info panel, hide item titles m.top.showItemTitles = "hidealways" - showDetails = isShowMediaDetails() + showDetails = get_user_setting_bool("ui.library.showMediaDetails", true) m.infoGroup.visible = showDetails m.selectedItemName.visible = showDetails @@ -1123,8 +1123,9 @@ sub onItemFocused() SetName(string.EMPTY) end if - ' Show the selected item name - m.selectedItemName.visible = isShowMediaDetails() + ' Genres rows carry no per-item metadata, so the detail bar stays hidden on that view + showDetails = get_user_setting_bool("ui.library.showMediaDetails", true) and not isStringEqual(m.view, "Genres") + m.selectedItemName.visible = showDetails if isValid(itemData.ProductionYear) SetProductionYear(str(itemData.ProductionYear)) @@ -1144,16 +1145,7 @@ sub onItemFocused() SetOfficialRating(string.EMPTY) end if - showMediaDetails = toBoolean(chainLookupReturn(m.global.session, "user.settings.ui.library.showMediaDetails", true)) - if not showMediaDetails or isStringEqual(m.view, "Genres") - m.infoGroup.visible = false - m.selectedItemName.visible = false - end if - - hideBackdrops = toBoolean(chainLookupReturn(m.global.session, "user.settings.ui.library.hideBackdrops", false)) - if hideBackdrops and isValid(m.overhang) and m.overhang.hasField("backgroundUri") - m.overhang.backgroundUri = "" - end if + if not showDetails then m.infoGroup.visible = false updateStatusBar() end sub @@ -1768,15 +1760,6 @@ sub reclaimResources() m.genreGrid.content = m.genreData end sub -function isShowMediaDetails() as boolean - showDetails = get_user_setting("ui.library.showMediaDetails") - if not isValid(showDetails) then return true - if type(showDetails) = "String" or type(showDetails) = "roString" - return LCase(showDetails) = "true" - end if - return showDetails = true -end function - diff --git a/components/home/HomeRows.bs b/components/home/HomeRows.bs index 698a30f7e..80abf2c4e 100644 --- a/components/home/HomeRows.bs +++ b/components/home/HomeRows.bs @@ -58,15 +58,6 @@ function isFeaturedMediaEnabled() as boolean return mediaBarEnabled end function -function isMergeRecentRowsByType() as boolean - mergeRecent = get_user_setting("ui.home.mergeRecentRowsByType") - if not isValid(mergeRecent) then return false - if type(mergeRecent) = "String" or type(mergeRecent) = "roString" - return LCase(mergeRecent) = "true" - end if - return mergeRecent = true -end function - sub init() m.scene = m.top.getScene() @@ -675,28 +666,24 @@ function getExpectedRowContribution(sectionName as string) as integer end if m.filteredLatest = filterNodeArray(m.libraryData, "id", excludeList) - if isMergeRecentRowsByType() - seenTypes = {} - rowCount = 0 - for each latestLibrary in m.filteredLatest - if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program" + + ' When merging, libraries sharing a collection type collapse into a single row + mergeByType = get_user_setting_bool("ui.home.mergeRecentRowsByType", false) + seenTypes = {} + rowCount = 0 + for each latestLibrary in m.filteredLatest + if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program" + if not mergeByType + rowCount++ + else colType = LCase(toNormalizedString(latestLibrary.json.CollectionType ?? latestLibrary.collectionType ?? "other")) if not seenTypes.DoesExist(colType) seenTypes[colType] = true rowCount++ end if end if - end for - return rowCount - else - rowCount = 0 - for each latestLibrary in m.filteredLatest - if latestLibrary.collectionType <> "boxsets" and latestLibrary.collectionType <> "livetv" and latestLibrary.json.CollectionType <> "Program" - rowCount++ - end if - end for - return rowCount - end if + end if + end for return rowCount end if @@ -1332,7 +1319,7 @@ sub createLatestInRows() ' create a Recently Added row for each library (or merged by type) - if isMergeRecentRowsByType() + if get_user_setting_bool("ui.home.mergeRecentRowsByType", false) groupedLibs = {} groupKeys = [] for each lib in m.filteredLatest diff --git a/components/home/LoadItemsTask.bs b/components/home/LoadItemsTask.bs index 277e73807..ddeaf8649 100644 --- a/components/home/LoadItemsTask.bs +++ b/components/home/LoadItemsTask.bs @@ -11,6 +11,9 @@ import "pkg:/source/utils/HomeRecommendations.bs" import "pkg:/source/utils/misc.bs" import "pkg:/source/utils/SeerrUtils.bs" +' Items a "Recently Added" row holds, both per library and after libraries are merged +const LATEST_MEDIA_LIMIT = 25 + sub init() m.top.itemsToLoad = "libraries" m.top.functionName = "loadItems" @@ -71,12 +74,12 @@ function loadLatestMedia() as object for each pId in targetParentIds params = { userId: m.global.session.user.id, - limit: 25, + limit: LATEST_MEDIA_LIMIT, parentId: pId, enableImageTypes: `${ImageType.PRIMARY}, ${ImageType.BACKDROP}, ${ImageType.THUMB}`, imageTypeLimit: 1, enableTotalRecordCount: false, - fields: "Genres" + fields: "Genres,DateCreated" } libData = api.items.GetLatest(params) if isValidAndNotEmpty(libData) @@ -89,6 +92,20 @@ function loadLatestMedia() as object end for end if end for + + ' Each library returns its own newest-first page, so a merged row arrives grouped by + ' library. Without this an old item from the first library outranks a new one from the + ' second, and the row ends up holding a page per library instead of a single page. + if targetParentIds.Count() > 1 + data = sortByDateCreatedDescending(data) + if data.Count() > LATEST_MEDIA_LIMIT + trimmed = [] + for i = 0 to LATEST_MEDIA_LIMIT - 1 + trimmed.push(data[i]) + end for + data = trimmed + end if + end if end if if not isValidAndNotEmpty(data) then return results @@ -2077,6 +2094,35 @@ function toNormalizedString(val as dynamic) as string return "" end function +' Orders raw API items newest first. DateCreated is ISO 8601, so comparing the strings gives +' chronological order, and items missing the field settle at the end instead of the front. +' roArray.SortBy() matches the field name case-sensitively, so it can't be used here. +function sortByDateCreatedDescending(items as object) as object + sorted = [] + dates = [] + + for each item in items + itemDate = toNormalizedString(item.LookupCI("DateCreated")) + placed = false + + for i = 0 to sorted.Count() - 1 + if itemDate > dates[i] + sorted.Insert(i, item) + dates.Insert(i, itemDate) + placed = true + exit for + end if + end for + + if not placed + sorted.push(item) + dates.push(itemDate) + end if + end for + + return sorted +end function + function loadCustomDynamicList(listName as string, additionalData as dynamic) as object results = [] diff --git a/settings/settings.json b/settings/settings.json index 34037e7f9..d4032b2c2 100644 --- a/settings/settings.json +++ b/settings/settings.json @@ -173,6 +173,13 @@ "type": "bool", "default": "false" }, + { + "title": "Merge Recently Added Libraries by Type", + "description": "Combine separate libraries of the same type into merged rows on the home page.", + "settingName": "ui.home.mergeRecentRowsByType", + "type": "bool", + "default": "false" + }, { "title": "Prefer Thumbnails", "description": "If thumbnails are selected in Home Row Image, show series instead of episode thumbnails.", @@ -2727,13 +2734,6 @@ "type": "bool", "default": "false" }, - { - "title": "Merge Recently Added Libraries by Type", - "description": "Combine separate libraries of the same type into merged rows on the home page.", - "settingName": "ui.home.mergeRecentRowsByType", - "type": "bool", - "default": "false" - }, { "title": "Show Media Details", "description": "Show details of the selected item at the top of Library pages.", diff --git a/source/utils/config.bs b/source/utils/config.bs index 56875fe23..651227d64 100644 --- a/source/utils/config.bs +++ b/source/utils/config.bs @@ -72,6 +72,18 @@ function get_user_setting(key as string) as dynamic return value end function +' Read a user setting as a boolean, falling back to defaultValue when it is unset or unreadable. +' Values reach the session as real booleans from settings.json defaults and as "true"/"false" +' strings from a plugin sync, so both spellings have to be accepted. +function get_user_setting_bool(key as string, defaultValue as boolean) as boolean + value = toBoolean(session.user.settings.Read(key)) + + valueType = LCase(Type(value)) + if valueType = "boolean" or valueType = "roboolean" then return value + + return defaultValue +end function + sub set_user_setting(key as string, value as dynamic) if m.global.session.user.id = invalid then return session.user.settings.Save(key, value) diff --git a/source/utils/misc.bs b/source/utils/misc.bs index f5ff2fb89..294c1764f 100644 --- a/source/utils/misc.bs +++ b/source/utils/misc.bs @@ -1052,8 +1052,7 @@ end sub ' Usage: ' setBackgroundWithQueue(uri, m.newBackdrop, m.swapAnimation) function setBackgroundWithQueue(backgroundUri as string, newBackdropNode as object, swapAnimationNode as object) as boolean - hideBackdrop = get_user_setting("ui.library.hideBackdrops") - if isValid(hideBackdrop) and ((type(hideBackdrop) = "Boolean" and hideBackdrop = true) or (type(hideBackdrop) = "String" and LCase(hideBackdrop) = "true")) + if get_user_setting_bool("ui.library.hideBackdrops", false) newBackdropNode.uri = "" return true end if