Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions components/ItemGrid/GridItem.bs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ sub itemContentChanged()
else
end if

useDetailedSubHeadings = get_user_setting_bool("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
Expand Down
4 changes: 2 additions & 2 deletions components/ItemGrid/GridItemMedium.bs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ sub itemContentChanged()
if isValid(m.topParent.showItemTitles)
if LCase(m.topParent.showItemTitles) = "showalways"
m.title.visible = true
m.itemTextExtra.visible = true
m.itemTextExtra.visible = get_user_setting_bool("ui.library.useDetailedSubHeadings", true)
end if
end if

Expand Down Expand Up @@ -228,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
Expand Down
12 changes: 8 additions & 4 deletions components/Libraries/VisualLibraryScene.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = get_user_setting_bool("ui.library.showMediaDetails", true)
m.infoGroup.visible = showDetails
m.selectedItemName.visible = showDetails

posterOrientation = getPosterOrientation()

Expand Down Expand Up @@ -1122,8 +1123,9 @@ sub onItemFocused()
SetName(string.EMPTY)
end if

' Show the selected item name
m.selectedItemName.visible = true
' 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))
Expand All @@ -1143,6 +1145,8 @@ sub onItemFocused()
SetOfficialRating(string.EMPTY)
end if

if not showDetails then m.infoGroup.visible = false

updateStatusBar()
end sub

Expand Down
88 changes: 84 additions & 4 deletions components/home/HomeRows.bs
Original file line number Diff line number Diff line change
Expand Up @@ -666,10 +666,22 @@ function getExpectedRowContribution(sectionName as string) as integer
end if

m.filteredLatest = filterNodeArray(m.libraryData, "id", excludeList)

' 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"
rowCount++
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 if
end for

Expand Down Expand Up @@ -1306,7 +1318,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 get_user_setting_bool("ui.home.mergeRecentRowsByType", false)
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: ["<p>" + `Error Message: ${e.message}` + "</p>"] })
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}`
Expand Down Expand Up @@ -1335,7 +1415,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")
Expand Down Expand Up @@ -2453,7 +2533,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)
Expand Down
89 changes: 78 additions & 11 deletions components/home/LoadItemsTask.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -57,18 +60,53 @@ end function

function loadLatestMedia() as object
results = []
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: LATEST_MEDIA_LIMIT,
parentId: pId,
enableImageTypes: `${ImageType.PRIMARY}, ${ImageType.BACKDROP}, ${ImageType.THUMB}`,
imageTypeLimit: 1,
enableTotalRecordCount: false,
fields: "Genres,DateCreated"
}
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

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)
' 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

Expand Down Expand Up @@ -2056,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 = []

Expand Down
1 change: 1 addition & 0 deletions components/home/LoadItemsTask.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<interface>
<field id="itemsToLoad" type="string" value="" />
<field id="itemId" type="string" />
<field id="itemIds" type="array" />
<field id="showID" type="string" />
<field id="seasonID" type="string" />
<field id="metadata" type="assocarray" />
Expand Down
28 changes: 28 additions & 0 deletions settings/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -2727,6 +2734,27 @@
"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.",
Expand Down
12 changes: 12 additions & 0 deletions source/utils/config.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions source/utils/misc.bs
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,11 @@ end sub
' Usage:
' setBackgroundWithQueue(uri, m.newBackdrop, m.swapAnimation)
function setBackgroundWithQueue(backgroundUri as string, newBackdropNode as object, swapAnimationNode as object) as boolean
if get_user_setting_bool("ui.library.hideBackdrops", false)
newBackdropNode.uri = ""
return true
end if

animationState = LCase(swapAnimationNode.state)
loadStatus = LCase(newBackdropNode.loadStatus)

Expand Down
4 changes: 4 additions & 0 deletions source/utils/settingsSync.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down