diff --git a/components/JFOverhang.bs b/components/JFOverhang.bs
index c7574c64b..5ac550c30 100644
--- a/components/JFOverhang.bs
+++ b/components/JFOverhang.bs
@@ -673,7 +673,10 @@ function handleDownKey() as boolean
mediaBar.visible = true
mediaBar.hasFocus = true
mediaBar.setFocus(true)
- homeRows.visible = false
+ mediaBarMode = m.global.session.user.settings["ui.home.mediaBarMode"] ?? "moonfin"
+ if mediaBarMode <> "banner"
+ homeRows.visible = false
+ end if
mediaBar.callFunc("refreshCurrentDisplay")
return true
end if
diff --git a/components/home/BookshelfItem.bs b/components/home/BookshelfItem.bs
new file mode 100644
index 000000000..a6d1459ec
--- /dev/null
+++ b/components/home/BookshelfItem.bs
@@ -0,0 +1,33 @@
+import "pkg:/source/utils/misc.bs"
+
+sub init()
+ m.itemNumber = m.top.findNode("itemNumber")
+ m.narrowContent = m.top.findNode("narrowContent")
+ m.narrowTitle = m.top.findNode("narrowTitle")
+ m.wideContent = m.top.findNode("wideContent")
+ m.itemPoster = m.top.findNode("itemPoster")
+ m.narrowSpine = m.top.findNode("narrowSpine")
+ m.wideSpine = m.top.findNode("wideSpine")
+end sub
+
+sub contentChanged()
+ data = m.top.itemContent
+ if not isValid(data) then return
+
+ blendColor = data.blendColor <> invalid ? data.blendColor : "0x7A5334FF"
+
+ m.narrowSpine.blendColor = blendColor
+ m.wideSpine.blendColor = blendColor
+
+ isActive = data.isActive <> invalid ? data.isActive : false
+ title = data.title <> invalid ? data.title : ""
+
+ m.itemNumber.text = data.itemNumber <> invalid ? right("0" + data.itemNumber.toStr(), 2) : ""
+
+ m.narrowContent.visible = not isActive
+ m.wideContent.visible = isActive
+
+ m.narrowTitle.text = title
+
+ if isActive then m.itemPoster.uri = data.backdropUrl
+end sub
diff --git a/components/home/BookshelfItem.xml b/components/home/BookshelfItem.xml
new file mode 100644
index 000000000..84f512e0e
--- /dev/null
+++ b/components/home/BookshelfItem.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/home/GalleryItem.bs b/components/home/GalleryItem.bs
new file mode 100644
index 000000000..5c80c16d2
--- /dev/null
+++ b/components/home/GalleryItem.bs
@@ -0,0 +1,130 @@
+import "pkg:/source/utils/misc.bs"
+
+sub init()
+ m.itemNumber = m.top.findNode("itemNumber")
+ m.narrowContent = m.top.findNode("narrowContent")
+ m.wideContent = m.top.findNode("wideContent")
+ m.narrowTitle = m.top.findNode("narrowTitle")
+ m.narrowPoster = m.top.findNode("narrowPoster")
+ m.itemBackdrop = m.top.findNode("itemBackdrop")
+ m.itemTitle = m.top.findNode("itemTitle")
+ m.gYear = m.top.findNode("gYear")
+ m.gSep1 = m.top.findNode("gSep1")
+ m.gRating = m.top.findNode("gRating")
+ m.gRatingBg = m.top.findNode("gRatingBg")
+ m.gRatingGroup = m.top.findNode("gRatingGroup")
+ m.gSep2 = m.top.findNode("gSep2")
+ m.gRuntime = m.top.findNode("gRuntime")
+ m.gSep3 = m.top.findNode("gSep3")
+ m.gStar = m.top.findNode("gStar")
+ m.gCommunityRating = m.top.findNode("gCommunityRating")
+ m.gSep4 = m.top.findNode("gSep4")
+ m.gGenres = m.top.findNode("gGenres")
+ m.gOverview = m.top.findNode("gOverview")
+ m.gStarring = m.top.findNode("gStarring")
+ m.galleryMetaRow = m.top.findNode("galleryMetaRow")
+end sub
+
+sub contentChanged()
+ data = m.top.itemContent
+ if not isValid(data) then return
+
+ isActive = data.isActive <> invalid ? data.isActive : false
+ title = data.title <> invalid ? data.title : ""
+
+ m.itemNumber.text = data.itemNumber <> invalid ? right("0" + data.itemNumber.toStr(), 2) : ""
+
+ ' Narrow content
+ m.narrowTitle.text = title
+
+ posterUrl = data.backdropUrl
+ hasPoster = isValid(posterUrl) and posterUrl <> ""
+
+ m.narrowPoster.uri = ""
+ m.narrowPoster.visible = false
+ m.itemBackdrop.uri = ""
+ if isActive and hasPoster
+ m.itemBackdrop.uri = posterUrl
+ else if hasPoster
+ m.narrowPoster.uri = posterUrl
+ m.narrowPoster.visible = true
+ end if
+
+ m.itemTitle.text = title
+
+ hasYear = false
+ hasRating = false
+ hasRuntime = false
+ hasStarRating = false
+
+ year = data.year
+ if year <> invalid and year > 0
+ m.gYear.text = year.toStr()
+ hasYear = true
+ else
+ m.gYear.text = ""
+ end if
+
+ rating = data.rating
+ if rating <> invalid and rating <> ""
+ m.gRating.text = rating
+ hasRating = true
+ labelRect = m.gRating.boundingRect()
+ if isValid(labelRect)
+ m.gRatingBg.width = labelRect.width + 16
+ m.gRatingBg.height = labelRect.height + 4
+ m.gRatingBg.translation = [labelRect.x - 16 / 2, labelRect.y - 4 / 2 - 2]
+ m.gRatingBg.visible = true
+ end if
+ else
+ m.gRating.text = ""
+ m.gRatingBg.visible = false
+ end if
+
+ runtimeDisplay = data.runtimeDisplay
+ itemType = data.itemType
+ if itemType = "Movie" and runtimeDisplay <> "" and runtimeDisplay <> invalid
+ m.gRuntime.text = runtimeDisplay
+ hasRuntime = true
+ else
+ m.gRuntime.text = ""
+ end if
+
+ communityRatingDisplay = data.communityRatingDisplay
+ if isValid(communityRatingDisplay) and communityRatingDisplay <> ""
+ m.gStar.text = "★ "
+ m.gStar.visible = true
+ m.gCommunityRating.text = communityRatingDisplay
+ hasStarRating = true
+ else
+ m.gStar.text = ""
+ m.gStar.visible = false
+ m.gCommunityRating.text = ""
+ end if
+
+ m.gSep1.text = (hasYear and hasRating) ? " • " : ""
+ m.gSep1.visible = (hasYear and hasRating)
+ m.gSep2.text = (hasRating) ? " • " : ""
+ m.gSep2.visible = hasRating
+
+ m.gSep3.text = (hasRuntime and hasStarRating) ? " • " : ""
+ m.gSep3.visible = (hasRuntime and hasStarRating)
+
+ genresDisplay = data.genresDisplay
+ hasGenres = isValid(genresDisplay) and genresDisplay <> ""
+ m.gGenres.text = genresDisplay
+ m.gSep4.text = ((hasYear or hasRating or hasRuntime or hasStarRating) and hasGenres) ? " • " : ""
+ m.gSep4.visible = ((hasYear or hasRating or hasRuntime or hasStarRating) and hasGenres)
+
+ overview = data.overview
+ if overview <> invalid and overview <> ""
+ m.gOverview.text = overview
+ else
+ m.gOverview.text = ""
+ end if
+
+ m.gStarring.text = data.starring
+
+ m.wideContent.visible = isActive
+ m.narrowContent.visible = not isActive
+end sub
diff --git a/components/home/GalleryItem.xml b/components/home/GalleryItem.xml
new file mode 100644
index 000000000..e884356d5
--- /dev/null
+++ b/components/home/GalleryItem.xml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/home/Home.bs b/components/home/Home.bs
index a999042fc..977b20dcc 100644
--- a/components/home/Home.bs
+++ b/components/home/Home.bs
@@ -17,10 +17,6 @@ sub init()
hasRunBefore = get_setting(firstRunKey)
m.isFirstRun = (hasRunBefore = invalid or hasRunBefore = "false")
- ' Track if we should focus media bar on initial load of this screen
- ' Set to true initially - will be set to false after first focus is set
- m.shouldFocusMediaBarOnLoad = true
-
' m.isMediaBarEnabled will be set after m.homeRows is initialized below
m.top.optionsAvailable = false
@@ -113,31 +109,16 @@ sub init()
' Show MediaBar if enabled via toggle
if m.isMediaBarEnabled
- m.mediaBar.visible = true
- m.homeRows.visible = false
- ' Set focus to MediaBar
- m.mediaBar.hasFocus = true
+ applyMediaBarLayout(true)
m.mediaBar.setFocus(true)
m.homeRows.setFocus(false)
- ' Hide home rows and details when showing media bar
- if isValid(m.detailsSection)
- m.detailsSection.visible = false
- end if
- if isValid(m.backdropTint)
- m.backdropTint.opacity = 0
- end if
- if isValid(m.backdrop)
- m.backdrop.opacity = 1.0
- end if
- m.backdropTargetOpacity = 1.0
else
- ' Make sure media bar is hidden if disabled
- m.mediaBar.visible = false
- ' Make sure home rows are visible if media bar is not shown
- m.homeRows.visible = true
+ applyMediaBarLayout(false)
end if
end if
+ m.global.observeFieldScoped("mediaBarModeChanged", "onMediaBarModeChanged")
+
scene = m.top.getScene()
activeNav = getActiveNav(scene)
if isValid(activeNav)
@@ -190,6 +171,32 @@ sub onLibrarySelected(event as object)
handleLibraryNavigation(libraryNode)
end sub
+' Handle media bar backdrop when style changes
+sub onMediaBarModeChanged()
+ if not isValid(m.mediaBar) or not isValid(m.homeRows) then return
+
+ mode = m.global.session.user.settings["ui.home.mediaBarMode"] ?? "moonfin"
+
+ if mode = "off" or mode = ""
+ applyMediaBarLayout(false)
+ m.isMediaBarEnabled = false
+ return
+ end if
+
+ m.isMediaBarEnabled = true
+ applyMediaBarLayout(true)
+
+ if mode = "gallery"
+ if isValid(m.backdrop) then m.backdrop.uri = ""
+ if isValid(m.backdropPrev) then m.backdropPrev.uri = ""
+ m.backdropCrossfading = false
+ else if mode = "bookshelf"
+ if isValid(m.backdrop) then m.backdrop.uri = "pkg:/images/bookshelfBg.png"
+ if isValid(m.backdropPrev) then m.backdropPrev.uri = ""
+ m.backdropCrossfading = false
+ end if
+end sub
+
' Handle media bar backdrop changes
sub onMediaBarBackdropChanged(event as object)
backdropUrl = event.getData()
@@ -197,6 +204,31 @@ sub onMediaBarBackdropChanged(event as object)
return
end if
+ mode = m.global.session.user.settings["ui.home.mediaBarMode"] ?? "moonfin"
+ if mode = "gallery"
+ if isValid(m.backdrop)
+ m.backdrop.uri = ""
+ m.backdrop.opacity = 0
+ end if
+ if isValid(m.backdropPrev)
+ m.backdropPrev.uri = ""
+ m.backdropPrev.opacity = 0
+ end if
+ m.backdropCrossfading = false
+ return
+ else if mode = "bookshelf"
+ if isValid(m.backdrop)
+ m.backdrop.uri = "pkg:/images/bookshelfBg.png"
+ m.backdrop.opacity = 1
+ end if
+ if isValid(m.backdropPrev)
+ m.backdropPrev.uri = ""
+ m.backdropPrev.opacity = 0
+ end if
+ m.backdropCrossfading = false
+ return
+ end if
+
' Handle clearing the backdrop when MediaBar loses focus
if backdropUrl = ""
if isValid(m.backdrop)
@@ -210,31 +242,16 @@ sub onMediaBarBackdropChanged(event as object)
end if
' Only update if MediaBar has focus
- if isValid(m.mediaBar) and m.mediaBar.hasFocus and m.mediaBar.visible
+ if isValid(m.mediaBar) and m.mediaBar.visible
if isValid(m.backdrop)
- ' Stop any running fade animations
- if isValid(m.backdropFadeIn) then m.backdropFadeIn.control = "stop"
- if isValid(m.backdropPrevFadeOut) then m.backdropPrevFadeOut.control = "stop"
-
- ' Copy current image to prev layer
- if isValid(m.backdropPrev) and m.backdrop.uri <> ""
- m.backdropPrev.uri = m.backdrop.uri
- m.backdropPrev.opacity = m.backdropTargetOpacity
- end if
-
- ' Hide new backdrop until it's ready
- m.backdrop.opacity = 0.0
- m.backdrop.uri = backdropUrl
- m.backdropCrossfading = true
+ startBackdropCrossfade(backdropUrl)
end if
end if
end sub
' Check if Featured Media should be shown when content loads
sub onHomeRowsContentLoaded()
- if not isValid(m.mediaBar) or not isValid(m.homeRows)
- return
- end if
+ if not isValid(m.mediaBar) or not isValid(m.homeRows) then return
' Check if Media Bar is enabled via toggle (no longer checking homesection0)
if isValid(m.homeRows)
@@ -246,41 +263,9 @@ sub onHomeRowsContentLoaded()
' Only show/focus Media Bar if it's enabled
if m.isMediaBarEnabled
- ' On first run only, set focus to media bar
- ' DON'T set focus if we're returning from another screen (OnScreenShown handles focus restoration)
- if m.shouldFocusMediaBarOnLoad = true and isValid(m.mediaBar)
- m.currentFocusedItemId = ""
- m.currentFocusedItemServerUrl = invalid
-
- ' Show MediaBar and hide HomeRows only when setting initial focus
- m.mediaBar.visible = true
- m.homeRows.visible = false
- m.mediaBar.hasFocus = true
- m.mediaBar.setFocus(true)
-
- ' Update backdrop for media bar
- if isValid(m.detailsSection)
- m.detailsSection.visible = false
- end if
- if isValid(m.backdropTint)
- m.backdropTint.opacity = 0
- end if
- if isValid(m.backdrop)
- m.backdrop.opacity = 1.0
- end if
- m.backdropTargetOpacity = 1.0
-
- ' Store focus state
- group = m.global.sceneManager.callFunc("getActiveScene")
- if isValid(group) then group.lastFocus = m.mediaBar
-
- ' Only set to false after successfully setting focus
- m.shouldFocusMediaBarOnLoad = false
- end if
+ applyMediaBarLayout(true)
else
- ' Make sure media bar is hidden if disabled
- m.mediaBar.visible = false
- m.homeRows.visible = true
+ applyMediaBarLayout(false)
end if
end sub
@@ -521,6 +506,69 @@ sub loadLibraries()
m.homeRows.callFunc("loadLibraries")
end sub
+' Apply layout for media bar visibility (handles banner vs fullscreen vs hidden)
+sub applyMediaBarLayout(mediaBarVisible as boolean)
+ if not isValid(m.mediaBar) or not isValid(m.homeRows) then return
+
+ m.backdrop.width = "1920"
+ m.backdrop.height = "1080"
+ m.backdrop.translation = "[0, 0]"
+ m.backdrop.loadDisplayMode = "scaleToFill"
+ m.backdropPrev.width = "1920"
+ m.backdropPrev.height = "1080"
+ m.backdropPrev.translation = "[0, 0]"
+ m.backdropPrev.loadDisplayMode = "scaleToFill"
+
+ if mediaBarVisible
+ m.mediaBar.visible = true
+ if (m.global.session.user.settings["ui.home.mediaBarMode"] ?? "moonfin") = "banner"
+ ' Banner media bar style
+ m.homeRows.visible = true
+ m.homeRows.translation = [111, 450]
+ if isValid(m.detailsSection) then m.detailsSection.visible = false
+
+ m.backdrop.width = "1710"
+ m.backdrop.height = "300"
+ m.backdrop.translation = "[105, 135]"
+ m.backdrop.loadDisplayMode = "scaleToZoom"
+
+ m.backdropPrev.width = "1710"
+ m.backdropPrev.height = "300"
+ m.backdropPrev.translation = "[105, 135]"
+ m.backdropPrev.loadDisplayMode = "scaleToZoom"
+
+ if isValid(m.backdropTint) then m.backdropTint.opacity = 0
+ if isValid(m.backdrop) then m.backdrop.opacity = 0.6
+ m.backdropTargetOpacity = 0.6
+ else
+ ' Fullscreen media bar style
+ m.homeRows.visible = false
+
+ if isValid(m.detailsSection) then m.detailsSection.visible = false
+ mediaBarMode = m.global.session.user.settings["ui.home.mediaBarMode"] ?? "moonfin"
+ if mediaBarMode = "makd"
+ if isValid(m.backdropTint) then m.backdropTint.opacity = 1.0
+ if isValid(m.backdrop) then m.backdrop.opacity = 0.3
+ m.backdropTargetOpacity = 0.3
+ else
+ if isValid(m.backdropTint) then m.backdropTint.opacity = 0
+ if isValid(m.backdrop) then m.backdrop.opacity = 1.0
+ m.backdropTargetOpacity = 1.0
+ end if
+ end if
+ else
+ ' Media bar off, or focus on homeRows
+ m.mediaBar.visible = false
+ m.homeRows.visible = true
+ m.homeRows.translation = [111, 450]
+
+ if isValid(m.detailsSection) then m.detailsSection.visible = true
+ if isValid(m.backdropTint) then m.backdropTint.opacity = 1.0
+ if isValid(m.backdrop) then m.backdrop.opacity = 0.3
+ m.backdropTargetOpacity = 0.3
+ end if
+end sub
+
' JFScreen hook called when the screen is displayed by the screen manager
sub OnScreenShown(_isReturning = false as boolean)
m.homeRows.rowLabelColor = chainLookupReturn(m.global.session, "user.settings.colorHomeRowHeaders", ColorPalette.WHITE)
@@ -554,43 +602,38 @@ sub OnScreenShown(_isReturning = false as boolean)
if isValid(scenePreviousFocus)
' Check if the saved focus was homeRows itself or a child of homeRows
if scenePreviousFocus.isSameNode(m.homeRows) or isChildOf(scenePreviousFocus, m.homeRows)
- m.homeRows.setFocus(true)
- focusRestored = true
-
- ' Hide MediaBar and show HomeRows content when restoring to HomeRows
- if isValid(m.mediaBar)
- m.mediaBar.visible = false
- end if
- if isValid(m.homeRows)
- m.homeRows.visible = true
- end if
- if isValid(m.detailsSection)
- m.detailsSection.visible = true
+ mediaBarCheck = m.homeRows.callFunc("isFeaturedMediaEnabled")
+ if isValid(mediaBarCheck) and type(mediaBarCheck) = "roBoolean"
+ m.isMediaBarEnabled = mediaBarCheck
end if
- ' Trigger backdrop update for current focused item
- if _isReturning
- onItemFocusChanged()
+ if m.isMediaBarEnabled and isValid(m.mediaBar)
+ m.currentFocusedItemId = ""
+ m.currentFocusedItemServerUrl = invalid
+
+ m.mediaBar.setFocus(true)
+ focusRestored = true
+
+ applyMediaBarLayout(true)
+ else
+ m.homeRows.setFocus(true)
+ focusRestored = true
+
+ applyMediaBarLayout(false)
+
+ if _isReturning
+ onItemFocusChanged()
+ end if
end if
' Check if the saved focus was mediaBar itself or a child of mediaBar
else if isValid(m.mediaBar) and (scenePreviousFocus.isSameNode(m.mediaBar) or isChildOf(scenePreviousFocus, m.mediaBar))
m.currentFocusedItemId = ""
m.currentFocusedItemServerUrl = invalid
- m.mediaBar.hasFocus = true
m.mediaBar.setFocus(true)
focusRestored = true
- ' Show MediaBar and hide HomeRows when restoring to MediaBar
- if isValid(m.mediaBar)
- m.mediaBar.visible = true
- end if
- if isValid(m.homeRows)
- m.homeRows.visible = false
- end if
- if isValid(m.detailsSection)
- m.detailsSection.visible = false
- end if
+ applyMediaBarLayout(true)
else
' lastFocus exists but isn't in homeRows or mediaBar (e.g., from settings/avatar)
' Default to homeRows since we're coming back to Home screen
@@ -599,14 +642,7 @@ sub OnScreenShown(_isReturning = false as boolean)
m.top.lastFocus = m.homeRows
focusRestored = true
- ' Show homeRows, hide mediaBar
- if isValid(m.mediaBar)
- m.mediaBar.visible = false
- end if
- m.homeRows.visible = true
- if isValid(m.detailsSection)
- m.detailsSection.visible = true
- end if
+ applyMediaBarLayout(false)
' Trigger backdrop update for current focused item
if _isReturning
@@ -622,9 +658,8 @@ sub OnScreenShown(_isReturning = false as boolean)
' Home rows already have focus, keep it there
m.homeRows.setFocus(true)
focusRestored = true
- else if isValid(m.mediaBar) and m.mediaBar.visible and m.mediaBar.hasFocus()
+ else if isValid(m.mediaBar) and m.mediaBar.visible and m.mediaBar.isInFocusChain()
' Media bar already has focus, keep it there
- m.mediaBar.hasFocus = true
m.mediaBar.setFocus(true)
focusRestored = true
end if
@@ -646,35 +681,16 @@ sub OnScreenShown(_isReturning = false as boolean)
m.currentFocusedItemId = ""
m.currentFocusedItemServerUrl = invalid
- m.mediaBar.visible = true
- m.mediaBar.hasFocus = true
+ applyMediaBarLayout(true)
m.mediaBar.setFocus(true)
m.top.lastFocus = m.mediaBar
- ' Hide details section and show full brightness backdrop for media bar
- if isValid(m.detailsSection)
- m.detailsSection.visible = false
- end if
- if isValid(m.backdropTint)
- m.backdropTint.opacity = 0
- end if
- if isValid(m.backdrop)
- m.backdrop.opacity = 1.0
- end if
- m.backdropTargetOpacity = 1.0
else
' Default to home rows
if isValid(m.homeRows)
m.homeRows.setFocus(true)
m.top.lastFocus = m.homeRows
- ' Ensure homeRows is visible and mediaBar is hidden
- m.homeRows.visible = true
- if isValid(m.mediaBar)
- m.mediaBar.visible = false
- end if
- if isValid(m.detailsSection)
- m.detailsSection.visible = true
- end if
+ applyMediaBarLayout(false)
' Trigger backdrop update
if _isReturning
@@ -848,24 +864,11 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.currentFocusedItemId = ""
m.currentFocusedItemServerUrl = invalid
- m.homeRows.visible = false
m.homeRows.setFocus(false)
- m.mediaBar.visible = true
- m.mediaBar.hasFocus = true
+ applyMediaBarLayout(true)
m.mediaBar.setFocus(true)
- m.shouldFocusMediaBarOnLoad = false
m.mediaBar.callFunc("refreshCurrentDisplay")
- if isValid(m.detailsSection)
- m.detailsSection.visible = false
- end if
- if isValid(m.backdropTint)
- m.backdropTint.opacity = 0
- end if
- if isValid(m.backdrop)
- m.backdrop.opacity = 1.0
- end if
- m.backdropTargetOpacity = 1.0
return true
end if
end if
@@ -879,11 +882,8 @@ function onKeyEvent(key as string, press as boolean) as boolean
' Handle navigation between MediaBar and HomeRows
if isStringEqual(key, KeyCode.DOWN)
- if isValid(m.mediaBar) and m.mediaBar.hasFocus and m.mediaBar.visible
- ' Move focus from MediaBar to first row in HomeRows
- m.mediaBar.visible = false
- m.mediaBar.hasFocus = false
- m.homeRows.visible = true
+ if isValid(m.mediaBar) and m.mediaBar.visible and m.mediaBar.isInFocusChain()
+ applyMediaBarLayout(false)
' Jump to first row (no placeholder to skip)
if m.homeRows.content.getChildCount() > 0
@@ -891,28 +891,15 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
m.homeRows.setFocus(true)
-
- ' Restore details section and normal backdrop
- if isValid(m.detailsSection)
- m.detailsSection.visible = true
- end if
- if isValid(m.backdropTint)
- m.backdropTint.opacity = 1.0
- end if
- if isValid(m.backdrop)
- m.backdrop.opacity = 0.3
- end if
- m.backdropTargetOpacity = 0.3
return true
end if
end if
if isStringEqual(key, KeyCode.UP)
- if isValid(m.mediaBar) and m.mediaBar.hasFocus and m.mediaBar.visible
+ if isValid(m.mediaBar) and m.mediaBar.visible and m.mediaBar.isInFocusChain()
scene = m.top.getScene()
activeNav = getActiveNav(scene)
if isValid(activeNav)
- m.mediaBar.hasFocus = false
activeNav.setFocus(true)
return true
end if
@@ -1057,6 +1044,20 @@ sub clearFocusedItemBackdrop()
m.backdropCrossfading = false
end sub
+sub startBackdropCrossfade(uri as string)
+ if isValid(m.backdropFadeIn) then m.backdropFadeIn.control = "stop"
+ if isValid(m.backdropPrevFadeOut) then m.backdropPrevFadeOut.control = "stop"
+
+ if isValid(m.backdropPrev) and m.backdrop.uri <> ""
+ m.backdropPrev.uri = m.backdrop.uri
+ m.backdropPrev.opacity = m.backdropTargetOpacity
+ end if
+
+ m.backdrop.opacity = 0.0
+ m.backdrop.uri = uri
+ m.backdropCrossfading = true
+end sub
+
sub onItemFocusChanged()
if not isValid(m.homeRows.content) then return
if m.homeRows.rowItemFocused[0] < 0 or m.homeRows.rowItemFocused[1] < 0 then return
@@ -1071,17 +1072,7 @@ sub onItemFocusChanged()
resetFocusedItemDetails()
if isValid(m.backdrop) and isValidAndNotEmpty(focusedItem.backdropURL)
- if isValid(m.backdropFadeIn) then m.backdropFadeIn.control = "stop"
- if isValid(m.backdropPrevFadeOut) then m.backdropPrevFadeOut.control = "stop"
-
- if isValid(m.backdropPrev) and m.backdrop.uri <> ""
- m.backdropPrev.uri = m.backdrop.uri
- m.backdropPrev.opacity = m.backdropTargetOpacity
- end if
-
- m.backdrop.opacity = 0.0
- m.backdrop.uri = focusedItem.backdropURL
- m.backdropCrossfading = true
+ startBackdropCrossfade(focusedItem.backdropURL)
else
if isValid(m.backdropFadeIn) then m.backdropFadeIn.control = "stop"
if isValid(m.backdropPrevFadeOut) then m.backdropPrevFadeOut.control = "stop"
@@ -1154,7 +1145,7 @@ sub onItemDetailsLoaded()
isEpisode = isValid(json.Type) and json.Type = "Episode"
- if isValid(m.backdrop) and not isValidAndNotEmpty(m.currentFocusedItemServerUrl)
+ if isValid(m.backdrop)
backdropUri = ""
if isEpisode and isValidAndNotEmpty(json.SeriesId)
@@ -1172,13 +1163,7 @@ sub onItemDetailsLoaded()
end if
if isValidAndNotEmpty(backdropUri)
- if isValid(m.backdropPrev) and m.backdrop.uri <> ""
- m.backdropPrev.uri = m.backdrop.uri
- m.backdropPrev.opacity = m.backdropTargetOpacity
- end if
- m.backdrop.opacity = 0.0
- m.backdrop.uri = backdropUri
- m.backdropCrossfading = true
+ startBackdropCrossfade(backdropUri)
m.backdropTargetOpacity = 0.3
if isValid(m.backdropTint)
diff --git a/components/home/MediaBar.bs b/components/home/MediaBar.bs
index ef7e4bdae..f7bb81f62 100644
--- a/components/home/MediaBar.bs
+++ b/components/home/MediaBar.bs
@@ -35,6 +35,15 @@ sub init()
m.ratingsRow = m.top.findNode("ratingsRow")
+ m.galleryGroup = m.top.findNode("galleryGroup")
+ m.galleryGrid = m.top.findNode("galleryGrid")
+
+ m.bookshelfGroup = m.top.findNode("bookshelfGroup")
+ m.bookshelfGrid = m.top.findNode("bookshelfGrid")
+
+ m.global.observeFieldScoped("mediaBarModeChanged", "onMediaBarModeChanged")
+ applyCurrentStyle()
+
' Content fade animations
m.contentFadeOut = m.top.findNode("contentFadeOut")
m.contentFadeIn = m.top.findNode("contentFadeIn")
@@ -51,6 +60,17 @@ sub init()
m.isPaused = false
m.clearBackdrop = false
+ ' Gallery mode state
+ m.galleryPages = []
+ m.galleryPageContentNodes = []
+ m.galleryCurrentPage = 0
+ m.galleryActiveIndex = 0
+ m.galleryTotalPages = 0
+
+ ' Bookshelf mode state
+ m.bookshelfContentNode = invalid
+ m.bookshelfActiveIndex = 0
+
' Make component focusable
m.top.focusable = true
@@ -72,13 +92,275 @@ sub init()
loadMediaBarItems()
end sub
+' React to media bar mode changes by re-applying style and refreshing display
+sub onMediaBarModeChanged(_event as object)
+ applyCurrentStyle()
+ applyOverlaySettings()
+ refreshMediaBarBackdropUrls()
+
+ if m.mediaBarItems.count() > 0
+ if m.mediaBarMode = "gallery"
+ setupGalleryPages()
+ showGalleryPage(0)
+ m.top.currentBackdrop = ""
+ if m.mediaBarItems.count() > 1
+ startAutoAdvanceTimer()
+ end if
+ else if m.mediaBarMode = "bookshelf"
+ setupBookshelfPages()
+ showBookshelfPage()
+ m.top.currentBackdrop = "pkg:/images/bookshelfBg.png"
+ if m.mediaBarItems.count() > 1
+ startAutoAdvanceTimer()
+ end if
+ else
+ if m.indicatorDots.getChildCount() = 0
+ createIndicatorDots()
+ end if
+ showMediaBarItem(m.currentIndex)
+ m.top.currentBackdrop = m.mediaBarItems[m.currentIndex].backdropUrl
+ end if
+ end if
+end sub
+
+' Configure visibility, translations, and font sizes for current media bar mode
+sub applyCurrentStyle()
+ mode = m.global.session.user.settings["ui.home.mediaBarMode"] ?? "moonfin"
+ m.mediaBarMode = mode
+ if mode = "off" or mode = ""
+ m.galleryGroup.visible = false
+ m.bookshelfGroup.visible = false
+ m.infoOverlayContent.visible = false
+ m.logoImage.visible = false
+ m.logoShadow.visible = false
+ m.titleText.visible = false
+ m.infoBackground.visible = false
+ m.leftArrow.visible = false
+ m.leftArrowBg.visible = false
+ m.rightArrow.visible = false
+ m.rightArrowBg.visible = false
+ m.dotsBackground.visible = false
+ m.indicatorDots.visible = false
+ m.overviewLabel.visible = false
+ m.pauseIndicator.visible = false
+ if isValid(m.autoAdvanceTimer) then m.autoAdvanceTimer.control = "stop"
+ end if
+
+ if mode <> "moonfin" and mode <> "makd" and mode <> "gallery" and mode <> "banner" and mode <> "bookshelf" ' Only accept available styles
+ mode = "moonfin"
+ m.mediaBarMode = mode
+ end if
+
+ if mode = "moonfin"
+ m.infoBottomAnchor = 720
+
+ m.galleryGroup.visible = false
+ m.bookshelfGroup.visible = false
+ m.infoOverlayContent.visible = true
+
+ ' Logo Shadow
+ m.logoShadow.width = "400"
+ m.logoShadow.height = "120"
+ m.logoShadow.translation = "[153, -12]"
+ ' Logo Image
+ m.logoImage.width = "400"
+ m.logoImage.height = "120"
+ m.logoImage.translation = "[150, -15]"
+ ' Title Text
+ m.titleText.translation = "[150, -15]"
+ m.titleText.horizAlign = "left"
+ ' Info Background
+ m.infoBackground.visible = true
+ ' Info Overlay Content
+ m.infoOverlayContent.translation = "[170, 475]"
+ m.infoOverlayContent.horizAlignment = "left"
+
+ m.yearLabel.font = "font:SmallestBoldSystemFont"
+ m.runtimeLabel.font = "font:SmallestBoldSystemFont"
+ m.starLabel.font = "font:SmallestBoldSystemFont"
+ m.communityRatingLabel.font = "font:SmallestBoldSystemFont"
+ m.genresLabel.font = "font:SmallestBoldSystemFont"
+ m.ratingsRow.labelFont = "font:SmallBoldSystemFont"
+ m.overviewLabel.visible = true
+ ' Left Arrow
+ m.leftArrow.visible = true
+ m.leftArrowBg.visible = true
+ ' Right Arrow
+ m.rightArrow.visible = true
+ m.rightArrowBg.visible = true
+ ' Dots Background
+ m.dotsBackground.visible = true
+ ' Dots
+ m.indicatorDots.visible = true
+ m.dotsY = 810
+ m.dotsBgY = 793
+ ' Paused Indicator
+ m.pauseIndicator.translation = "[1772, 808]"
+ else if mode = "makd"
+ m.infoBottomAnchor = 800
+
+ m.galleryGroup.visible = false
+ m.bookshelfGroup.visible = false
+ m.infoOverlayContent.visible = true
+
+ ' Logo Shadow
+ m.logoShadow.width = "500"
+ m.logoShadow.height = "220"
+ m.logoShadow.translation = "[710, 200]"
+ ' Logo Image
+ m.logoImage.width = "500"
+ m.logoImage.height = "220"
+ m.logoImage.translation = "[710, 200]"
+ ' Title Text
+ m.titleText.translation = "[560, 285]"
+ m.titleText.horizAlign = "center"
+ ' Info Background
+ m.infoBackground.visible = false
+ ' Info Overlay Content
+ m.infoOverlayContent.translation = "[960, 660]"
+ m.infoOverlayContent.horizAlignment = "center"
+
+ m.yearLabel.font = "font:TinyBoldSystemFont"
+ m.runtimeLabel.font = "font:TinyBoldSystemFont"
+ m.starLabel.font = "font:TinyBoldSystemFont"
+ m.communityRatingLabel.font = "font:TinyBoldSystemFont"
+ m.genresLabel.font = "font:TinyBoldSystemFont"
+ m.ratingsRow.labelFont = "font:TinyBoldSystemFont"
+ m.overviewLabel.visible = false
+ ' Left Arrow
+ m.leftArrow.visible = true
+ m.leftArrowBg.visible = true
+ ' Right Arrow
+ m.rightArrow.visible = true
+ m.rightArrowBg.visible = true
+ ' Dots Background
+ m.dotsBackground.visible = false
+ ' Dots
+ m.indicatorDots.visible = true
+ m.dotsY = 810
+ m.dotsBgY = 793
+ ' Paused Indicator
+ m.pauseIndicator.translation = "[1772, 808]"
+ else if mode = "bookshelf"
+ m.infoBottomAnchor = 720
+
+ if isValid(m.contentFadeOut) then m.contentFadeOut.control = "stop"
+ if isValid(m.contentFadeIn) then m.contentFadeIn.control = "stop"
+ m.pendingDisplayIndex = -1
+
+ m.logoShadow.visible = false
+ m.logoImage.visible = false
+ m.titleText.visible = false
+ m.infoBackground.visible = false
+ m.infoOverlayContent.visible = false
+ m.leftArrow.visible = false
+ m.leftArrowBg.visible = false
+ m.rightArrow.visible = false
+ m.rightArrowBg.visible = false
+ m.dotsBackground.visible = false
+ m.indicatorDots.visible = false
+ m.overviewLabel.visible = false
+
+ m.galleryGroup.visible = false
+
+ m.bookshelfGroup.visible = true
+
+ m.dotsY = 810
+ m.dotsBgY = 793
+ ' Paused Indicator
+ m.pauseIndicator.translation = "[1772, 808]"
+ else if mode = "gallery"
+ m.infoBottomAnchor = 720
+
+ if isValid(m.contentFadeOut) then m.contentFadeOut.control = "stop"
+ if isValid(m.contentFadeIn) then m.contentFadeIn.control = "stop"
+ m.pendingDisplayIndex = -1
+
+ m.logoShadow.visible = false
+ m.logoImage.visible = false
+ m.titleText.visible = false
+ m.infoBackground.visible = false
+ m.infoOverlayContent.visible = false
+ m.leftArrow.visible = false
+ m.leftArrowBg.visible = false
+ m.rightArrow.visible = false
+ m.rightArrowBg.visible = false
+ m.dotsBackground.visible = false
+ m.indicatorDots.visible = false
+ m.overviewLabel.visible = false
+
+ m.galleryGroup.visible = true
+ m.bookshelfGroup.visible = false
+
+ m.dotsY = 810
+ m.dotsBgY = 793
+ ' Paused Indicator
+ m.pauseIndicator.translation = "[1722, 808]"
+ else if mode = "banner"
+ m.infoBottomAnchor = 240
+
+ m.galleryGroup.visible = false
+ m.bookshelfGroup.visible = false
+ m.infoOverlayContent.visible = true
+
+ ' Logo Shadow
+ m.logoShadow.width = "400"
+ m.logoShadow.height = "120"
+ m.logoShadow.translation = "[153, -12]"
+ ' Logo Image
+ m.logoImage.width = "400"
+ m.logoImage.height = "120"
+ m.logoImage.translation = "[150, -15]"
+ ' Title Text
+ m.titleText.translation = "[150, -15]"
+ m.titleText.horizAlign = "left"
+ ' Info Background
+ m.infoBackground.visible = false
+ ' Info Overlay Content
+ m.infoOverlayContent.translation = "[153, 140]"
+ m.infoOverlayContent.horizAlignment = "left"
+
+ m.yearLabel.font = "font:TinyBoldSystemFont"
+ m.runtimeLabel.font = "font:TinyBoldSystemFont"
+ m.starLabel.font = "font:TinyBoldSystemFont"
+ m.communityRatingLabel.font = "font:TinyBoldSystemFont"
+ m.genresLabel.font = "font:TinyBoldSystemFont"
+ m.ratingsRow.labelFont = "font:TinyBoldSystemFont"
+ m.overviewLabel.visible = false
+ ' Left Arrow
+ m.leftArrow.visible = false
+ m.leftArrowBg.visible = false
+ ' Right Arrow
+ m.rightArrow.visible = false
+ m.rightArrowBg.visible = false
+ ' Dots Background
+ m.dotsBackground.visible = false
+ ' Dots
+ m.indicatorDots.visible = true
+ m.dotsY = 190
+ m.dotsBgY = 173
+ ' Paused Indicator
+ m.pauseIndicator.translation = "[1667, 163]"
+ end if
+
+ updateInfoBackgroundSize()
+ updateDotsBackgroundSize()
+end sub
+
+' Rebuild backdrop URLs for all items after mode change affects URL params
+sub refreshMediaBarBackdropUrls()
+ for each item in m.mediaBarItems
+ item.backdropUrl = buildBackdropUrl(item)
+ end for
+end sub
+
' Handle focus changes - ensure backdrop is set when component gets focus
sub onFocusChanged(event as object)
hasFocus = event.getData()
if hasFocus and m.mediaBarItems.count() > 0
' Immediately refresh display when focus is gained
- displayMediaBarItem(m.currentIndex)
+ refreshDisplay()
' Resume auto-advance timer
if isValid(m.autoAdvanceTimer)
m.autoAdvanceTimer.control = "start"
@@ -101,7 +383,18 @@ sub onVisibilityChanged(event as object)
isVisible = event.getData()
if isVisible and m.mediaBarItems.count() > 0
- ' Refresh the current item display when component becomes visible
+ refreshDisplay()
+ end if
+end sub
+
+' Mode-aware display refresh
+sub refreshDisplay()
+ if m.mediaBarItems.count() = 0 then return
+ if m.mediaBarMode = "gallery"
+ showGalleryPage(m.galleryCurrentPage)
+ else if m.mediaBarMode = "bookshelf"
+ showBookshelfPage()
+ else
displayMediaBarItem(m.currentIndex)
end if
end sub
@@ -138,6 +431,7 @@ sub loadMediaBarItems()
m.mediaBarTask.contentType = m.global.session.user.settings["ui.home.mediaBarContentType"] ?? "both"
m.mediaBarTask.sourceType = m.global.session.user.settings["ui.home.mediaBarSourceType"] ?? "library"
m.mediaBarTask.selectedLibraryIds = m.global.session.user.settings["ui.home.mediaBarLibraryIds"] ?? "[]"
+ m.mediaBarTask.featuredCount = m.global.session.user.settings["ui.home.featuredMediaItems"] ?? 15
m.mediaBarTask.selectedCollectionIds = m.global.session.user.settings["ui.home.mediaBarCollectionIds"] ?? "[]"
m.mediaBarTask.excludedGenreIds = m.global.session.user.settings["ui.home.mediaBarExcludedGenres"] ?? "[]"
m.mediaBarTask.observeFieldScoped("mediaBarData", "onMediaBarDataLoaded")
@@ -158,59 +452,46 @@ sub onMediaBarDataLoaded(event as object)
allItems = isChainValid(data, "items") ? data.items : []
- ' Client-side type filter - API may not reliably exclude collection types
- itemsWithBackdrops = []
- seenMediaBarItems = {}
+ m.mediaBarItems = []
for each item in allItems
- itemType = LCase(item.LookupCI("Type") ?? "")
- if itemType <> "boxset" and itemType <> "collectionfolder" and isChainValid(item, "backdropImageTags") and item.backdropImageTags.count() > 0
- processedItem = processItem(item)
-
- if isValidAndNotEmpty(processedItem.tmdbId)
- dedupeKey = `${itemType}|tmdb|${LCase(processedItem.tmdbId)}`
- else if isValidAndNotEmpty(processedItem.id)
- dedupeKey = `${itemType}|id|${processedItem.id}`
- else
- nameKey = LCase(item.LookupCI("Name") ?? "")
- yearKey = isValid(item.LookupCI("ProductionYear")) ? item.LookupCI("ProductionYear").toStr() : ""
- dedupeKey = `${itemType}|name|${nameKey}|${yearKey}`
- end if
-
- if not seenMediaBarItems.DoesExist(dedupeKey)
- seenMediaBarItems[dedupeKey] = true
- itemsWithBackdrops.push(processedItem)
- end if
- end if
+ processed = processItem(item)
+ buildItemUrls(processed)
+ m.mediaBarItems.push(processed)
end for
- settingValue = m.global.session.user.settings["ui.home.featuredMediaItems"] ?? invalid
- carouselItemCount = isValid(settingValue) ? settingValue.toInt() : 15
-
- ' Shuffle and take configured number of items
- shuffled = shuffleArray(itemsWithBackdrops)
- if shuffled.count() > carouselItemCount
- m.mediaBarItems = shuffled.slice(0, carouselItemCount)
- else
- m.mediaBarItems = shuffled
- end if
-
' Initialize display if we have items
if m.mediaBarItems.count() > 0
- createIndicatorDots()
- if m.mediaBarItems.count() > 1
- scene = m.top.getScene()
- sidebarActive = false
- if isValid(scene)
- sidebar = scene.findNode("sidebar")
- sidebarActive = isValid(sidebar) and sidebar.visible
+ if m.mediaBarMode = "gallery"
+ setupGalleryPages()
+ showGalleryPage(0)
+ if m.mediaBarItems.count() > 1
+ startAutoAdvanceTimer()
+ end if
+ else if m.mediaBarMode = "bookshelf"
+ setupBookshelfPages()
+ showBookshelfPage()
+ m.top.currentBackdrop = "pkg:/images/bookshelfBg.png"
+ if m.mediaBarItems.count() > 1
+ startAutoAdvanceTimer()
+ end if
+ else
+ createIndicatorDots()
+ if m.mediaBarItems.count() > 1
+ scene = m.top.getScene()
+ sidebarActive = false
+ if isValid(scene)
+ sidebar = scene.findNode("sidebar")
+ sidebarActive = isValid(sidebar) and sidebar.visible
+ end if
+ showArrows = (m.mediaBarMode <> "banner") and not sidebarActive
+ m.leftArrow.visible = showArrows
+ if isValid(m.leftArrowBg) then m.leftArrowBg.visible = showArrows
+ m.rightArrow.visible = showArrows
+ startAutoAdvanceTimer()
end if
- m.leftArrow.visible = not sidebarActive
- if isValid(m.leftArrowBg) then m.leftArrowBg.visible = not sidebarActive
- m.rightArrow.visible = true
- startAutoAdvanceTimer()
+ ' Display first item (single backdrop update)
+ displayMediaBarItem(0)
end if
- ' Display first item (single backdrop update)
- displayMediaBarItem(0)
end if
end sub
@@ -220,48 +501,32 @@ function processItem(item as object) as object
id: item.id,
title: item.name,
itemType: item.type,
- overview: invalid,
- year: invalid,
- rating: invalid,
+ overview: item.overview,
+ year: item.productionYear,
+ rating: item.officialRating,
runtime: invalid,
runtimeDisplay: "",
- communityRating: invalid,
+ communityRating: item.communityRating,
communityRatingDisplay: "",
genres: [],
genresDisplay: "",
- backdropUrl: "",
+ backdropTag: "",
logoUrl: "",
- tmdbId: ""
+ hasLogo: false,
+ tmdbId: "",
+ people: invalid
}
if isChainValid(item, "backdropImageTags") and item.backdropImageTags.count() > 0
- processed.backdropUrl = ImageURL(item.id, "Backdrop", {
- maxHeight: 1080,
- maxWidth: 1920,
- quality: 100,
- Tag: item.backdropImageTags[0]
- })
+ processed.backdropTag = item.backdropImageTags[0]
end if
if isChainValid(item, "imageTags.Logo")
- processed.logoUrl = ImageURL(item.id, "Logo", {
- maxHeight: 150,
- maxWidth: 500,
- quality: 100
- })
+ processed.hasLogo = true
end if
- ' Extract metadata
- if isValid(item.overview)
- processed.overview = item.overview
- end if
-
- if isValid(item.productionYear)
- processed.year = item.productionYear
- end if
-
- if isValid(item.officialRating)
- processed.rating = item.officialRating
+ if isChainValid(item, "providerIds")
+ processed.tmdbId = moonfinPlugin.GetTmdbId(item.providerIds)
end if
if isValid(item.runTimeTicks)
@@ -277,36 +542,265 @@ function processItem(item as object) as object
end if
if isValid(item.communityRating)
- processed.communityRating = item.communityRating
processed.communityRatingDisplay = formatFloat(item.communityRating, 1)
end if
- if isChainValid(item, "providerIds")
- processed.tmdbId = moonfinPlugin.GetTmdbId(item.providerIds)
- end if
-
if isChainValid(item, "genres") and item.genres.count() > 0
- processed.genres = item.genres
' Cache genres display string (first 3 genres joined)
displayGenres = item.genres
if displayGenres.count() > 3
displayGenres = [displayGenres[0], displayGenres[1], displayGenres[2]]
end if
processed.genresDisplay = displayGenres.join(" • ")
+ processed.genres = item.genres
+ end if
+
+ if isChainValid(item, "People") and item.People.count() > 0
+ peopleList = []
+ for each person in item.People
+ personType = LCase(person.Type)
+ if personType <> "director" and personType <> "writer"
+ peopleList.push(person.Name)
+ if peopleList.count() >= 6 then exit for
+ end if
+ end for
+ if peopleList.count() > 0
+ processed.people = peopleList.join(", ")
+ end if
end if
return processed
end function
+' Build a single backdrop image URL with mode-specific sizing and quality
+function buildBackdropUrl(item as object) as string
+ if not isValid(item.backdropTag) or item.backdropTag = "" then return ""
+ if m.mediaBarMode = "makd"
+ return ImageURL(item.id, "Backdrop", {maxHeight: 1080, maxWidth: 1920, quality: 60, blur: 15, Tag: item.backdropTag})
+ else if m.mediaBarMode = "bookshelf"
+ return ImageURL(item.id, "Primary", {maxHeight: 850, maxWidth: 420, quality: 90})
+ else
+ return ImageURL(item.id, "Backdrop", {maxHeight: 1080, maxWidth: 1920, quality: 90, Tag: item.backdropTag})
+ end if
+end function
+
+' Build backdrop and logo URLs for a single media item
+sub buildItemUrls(item as object)
+ item.backdropUrl = buildBackdropUrl(item)
+ if item.hasLogo
+ item.logoUrl = ImageURL(item.id, "Logo", {maxHeight: 150, maxWidth: 500, quality: 100})
+ end if
+end sub
+
+' Create a ContentNode from an item dict with optional extra fields
+function createItemContentNode(item as object, extraFields = {} as object) as object
+ child = createObject("roSGNode", "ContentNode")
+ fields = {
+ title: item.title,
+ backdropUrl: item.backdropUrl,
+ logoUrl: item.logoUrl,
+ overview: item.overview,
+ starring: item.people,
+ year: item.year,
+ rating: item.rating,
+ runtimeDisplay: item.runtimeDisplay,
+ communityRatingDisplay: item.communityRatingDisplay,
+ genresDisplay: item.genresDisplay,
+ itemType: item.itemType
+ }
+ for each key in extraFields
+ fields[key] = extraFields[key]
+ end for
+ child.addFields(fields)
+ return child
+end function
+
+' Split items into pages of 5 for gallery display
+sub setupGalleryPages()
+ pageSize = 5
+ m.galleryPages = []
+ m.galleryPageContentNodes = []
+ allItems = m.mediaBarItems
+
+ for pageIndex = 0 to (allItems.count() - 1) step pageSize
+ finish = pageIndex + pageSize - 1
+ if finish >= allItems.count() then finish = allItems.count() - 1
+ page = []
+ content = createObject("roSGNode", "ContentNode")
+ for i = pageIndex to finish
+ item = allItems[i]
+ page.push(item)
+ child = createItemContentNode(item, { isActive: false, itemNumber: i + 1 })
+ content.appendChild(child)
+ end for
+ m.galleryPages.push(page)
+ m.galleryPageContentNodes.push(content)
+ end for
+
+ m.galleryTotalPages = m.galleryPages.count()
+ m.galleryCurrentPage = 0
+ m.galleryActiveIndex = 0
+end sub
+
+' Show a specific gallery page
+sub showGalleryPage(pageIndex as integer)
+ if pageIndex < 0 or pageIndex >= m.galleryTotalPages then return
+
+ pageContent = m.galleryPageContentNodes[pageIndex]
+
+ for i = 0 to pageContent.getChildCount() - 1
+ pageContent.getChild(i).isActive = false
+ end for
+
+ pageContent.getChild(m.galleryActiveIndex).isActive = true
+
+ m.galleryGrid.content = pageContent
+ setGalleryColumnWidths(m.galleryActiveIndex)
+end sub
+
+' Set column widths based on active index
+sub setGalleryColumnWidths(activeIndex as integer)
+ widths = [110, 110, 110, 110, 110]
+ widths[activeIndex] = 1400
+ m.galleryGrid.columnWidths = widths
+end sub
+
+' Navigate between gallery items
+sub galleryAdvance(direction as integer)
+ if m.galleryTotalPages = 0 then return
+ itemsOnPage = m.galleryPages[m.galleryCurrentPage].count()
+
+ if direction > 0
+ if m.galleryActiveIndex < itemsOnPage - 1
+ m.galleryActiveIndex++
+ else
+ m.galleryCurrentPage = (m.galleryCurrentPage + 1) mod m.galleryTotalPages
+ m.galleryActiveIndex = 0
+ end if
+ else
+ if m.galleryActiveIndex > 0
+ m.galleryActiveIndex--
+ else
+ m.galleryCurrentPage = m.galleryCurrentPage - 1
+ if m.galleryCurrentPage < 0 then m.galleryCurrentPage = m.galleryTotalPages - 1
+ m.galleryActiveIndex = m.galleryPages[m.galleryCurrentPage].count() - 1
+ end if
+ end if
+
+ showGalleryPage(m.galleryCurrentPage)
+end sub
+
+' Get the currently active item in gallery mode
+function getGalleryActiveItem() as object
+ if m.galleryTotalPages = 0 then return invalid
+ page = m.galleryPages[m.galleryCurrentPage]
+ if m.galleryActiveIndex < page.count()
+ return page[m.galleryActiveIndex]
+ end if
+ return invalid
+end function
+
+' Build the bookshelf ContentNode with spine colours and initial column widths
+sub setupBookshelfPages()
+ allItems = m.mediaBarItems
+ totalItems = allItems.count()
+ if totalItems = 0 then return
+
+ blendColors = [
+ "0xBF441BFF", ' orange
+ "0xae792dFF", ' gold
+ "0xD98934FF", ' yellow
+ "0x870F19FF", ' burgundy
+ "0x7A5334FF", ' brown
+ "0x592B2BFF", ' purpleish brown
+ "0x30A49CFF", ' cyan/light blue
+ "0x6C7851FF" ' army green
+ ]
+
+ content = createObject("roSGNode", "ContentNode")
+ for i = 0 to totalItems - 1
+ item = allItems[i]
+
+ colorIndex = Rnd(8) - 1
+
+ child = createItemContentNode(item, { isActive: (i = 0), itemNumber: i + 1, blendColor: blendColors[colorIndex] })
+ content.appendChild(child)
+ end for
+
+ m.bookshelfContentNode = content
+ m.bookshelfActiveIndex = m.currentIndex
+
+ m.bookshelfGrid.numColumns = totalItems
+
+ updateBookshelfLayout(m.bookshelfActiveIndex)
+end sub
+
+' Render the current bookshelf page, marking the active item and recentering
+sub showBookshelfPage()
+ m.bookshelfGroup.visible = true
+
+ pageContent = m.bookshelfContentNode
+ totalItems = pageContent.getChildCount()
+
+ for i = 0 to totalItems - 1
+ pageContent.getChild(i).isActive = (i = m.bookshelfActiveIndex)
+ end for
+
+ m.bookshelfGrid.content = pageContent
+
+ updateBookshelfLayout(m.bookshelfActiveIndex)
+end sub
+
+' Navigate between gallery items
+sub bookshelfAdvance(direction as integer)
+ totalItems = m.mediaBarItems.count()
+ if totalItems <= 1 then return
+
+ if direction > 0
+ m.bookshelfActiveIndex = (m.bookshelfActiveIndex + 1) mod totalItems
+ else
+ m.bookshelfActiveIndex = m.bookshelfActiveIndex - 1
+ if m.bookshelfActiveIndex < 0 then m.bookshelfActiveIndex = totalItems - 1
+ end if
+ m.currentIndex = m.bookshelfActiveIndex
+ showBookshelfPage()
+end sub
+
+sub updateBookshelfLayout(activeIndex as integer)
+ totalItems = m.mediaBarItems.count()
+ if totalItems = 0 then return
+
+ ' Set column widths: wide for active item, narrow for the rest
+ widths = []
+ for i = 0 to totalItems - 1
+ if i = activeIndex
+ widths.push(690)
+ else
+ widths.push(60)
+ end if
+ end for
+ m.bookshelfGrid.columnWidths = widths
+
+ ' Translate the bookshelf group to the active book is horizontally centered
+ spineWidth = 60
+ bookWidth = 778
+ spacing = 5
+ screenCenterX = 960
+
+ itemX = activeIndex * (spineWidth + spacing)
+ centerX = itemX + bookWidth / 2
+ offsetX = screenCenterX - centerX
+
+ m.bookshelfGroup.translation = [offsetX, 30]
+end sub
+
' Create indicator dots for slides
sub createIndicatorDots()
' Clear existing dots
m.indicatorDots.removeChildrenIndex(m.indicatorDots.getChildCount(), 0)
- totalItems = m.mediaBarItems.count()
-
' Show one dot for each item
- numDots = totalItems
+ numDots = m.mediaBarItems.count()
' Create dot for each visible indicator
for i = 0 to numDots - 1
@@ -349,13 +843,17 @@ sub updateDotsBackgroundSize()
totalDotsWidth = (numDots - 1) * inactiveDotSize + activeDotSize
totalSpacingWidth = (numDots - 1) * spacing
calculatedWidth = totalDotsWidth + totalSpacingWidth + padding
- m.dotsBackground.width = calculatedWidth
' Center the background and dots on screen
' Screen width is 1920, so center point is 960
screenCenter = 960
- m.indicatorDots.translation = [screenCenter - (totalDotsWidth + totalSpacingWidth) / 2, 809]
- m.dotsBackground.translation = [screenCenter - calculatedWidth / 2, 792]
+
+ m.indicatorDots.translation = [screenCenter - (totalDotsWidth + totalSpacingWidth) / 2, m.dotsY]
+
+ if not m.dotsBackground.visible then return
+
+ m.dotsBackground.width = calculatedWidth
+ m.dotsBackground.translation = [screenCenter - calculatedWidth / 2, m.dotsBgY]
end sub
' Update indicator dots to show current slide
@@ -521,7 +1019,9 @@ sub showMediaBarItem(index as integer)
end if
' Overview (full text, no line limit)
- if item.overview <> invalid
+ if m.mediaBarMode = "makd" or m.mediaBarMode = "banner" or m.mediaBarMode = "off"
+ m.overviewLabel.text = ""
+ else if item.overview <> invalid
m.overviewLabel.text = item.overview
else
m.overviewLabel.text = ""
@@ -537,17 +1037,19 @@ end sub
' Update info background to match content height
sub updateInfoBackgroundSize()
if not isValid(m.infoOverlayContent) or not isValid(m.infoBackground) then return
+ if not m.infoBackground.visible then return
contentHeight = m.infoOverlayContent.boundingRect().height
if contentHeight <= 0 then return
totalHeight = contentHeight + 30
- newBgY = 720 - totalHeight
+ anchor = m.infoBottomAnchor ?? 720
+ newBgY = anchor - totalHeight
m.infoBackground.height = totalHeight
- m.infoBackground.translation = [150, newBgY]
+ m.infoBackground.translation = [m.infoBackground.translation[0], newBgY]
- m.infoOverlayContent.translation = [170, newBgY + 15]
+ m.infoOverlayContent.translation = [m.infoOverlayContent.translation[0], newBgY + 15]
end sub
sub fetchMediaBarRatings(item as object)
@@ -628,7 +1130,13 @@ end function
' Auto-advance handler
sub onAutoAdvance()
if not m.isPaused and m.mediaBarItems.count() > 1
- nextSlide()
+ if m.mediaBarMode = "gallery"
+ galleryAdvance(1)
+ else if m.mediaBarMode = "bookshelf"
+ bookshelfAdvance(1)
+ else
+ nextSlide()
+ end if
end if
end sub
@@ -677,7 +1185,13 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
end if
end if
- previousSlide()
+ if m.mediaBarMode = "gallery"
+ galleryAdvance(-1)
+ else if m.mediaBarMode = "bookshelf"
+ bookshelfAdvance(-1)
+ else
+ previousSlide()
+ end if
' Reset auto-advance timer
if isValid(m.autoAdvanceTimer)
m.autoAdvanceTimer.control = "stop"
@@ -685,7 +1199,13 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
return true
else if key = KeyCode.RIGHT
- nextSlide()
+ if m.mediaBarMode = "gallery"
+ galleryAdvance(1)
+ else if m.mediaBarMode = "bookshelf"
+ bookshelfAdvance(1)
+ else
+ nextSlide()
+ end if
' Reset auto-advance timer
if isValid(m.autoAdvanceTimer)
m.autoAdvanceTimer.control = "stop"
@@ -694,15 +1214,23 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
else if key = KeyCode.OK
' Navigate to item details
- if m.mediaBarItems.count() > 0 and m.currentIndex >= 0 and m.currentIndex < m.mediaBarItems.count()
- currentItem = m.mediaBarItems[m.currentIndex]
- if isValid(currentItem) and isValid(currentItem.id) and isValid(currentItem.itemType)
- itemNode = createObject("roSGNode", "ContentNode")
- itemNode.id = currentItem.id
- itemNode.title = currentItem.title
- itemNode.addFields({ type: currentItem.itemType })
- m.top.itemSelected = itemNode
+ activeItem = invalid
+ if m.mediaBarMode = "gallery"
+ activeItem = getGalleryActiveItem()
+ else if m.mediaBarMode = "bookshelf"
+ if m.bookshelfActiveIndex >= 0 and m.bookshelfActiveIndex < m.mediaBarItems.count()
+ activeItem = m.mediaBarItems[m.bookshelfActiveIndex]
end if
+ else if m.currentIndex >= 0 and m.currentIndex < m.mediaBarItems.count()
+ activeItem = m.mediaBarItems[m.currentIndex]
+ end if
+
+ if isValid(activeItem) and isValid(activeItem.id) and isValid(activeItem.itemType)
+ itemNode = createObject("roSGNode", "ContentNode")
+ itemNode.id = activeItem.id
+ itemNode.title = activeItem.title
+ itemNode.addFields({ type: activeItem.itemType })
+ m.top.itemSelected = itemNode
end if
return true
else if key = KeyCode.PLAY or key = "pause"
@@ -727,6 +1255,14 @@ end function
' Public function to refresh the current display from parent
sub refreshCurrentDisplay()
if m.mediaBarItems.count() > 0
- displayMediaBarItem(m.currentIndex)
+ if m.mediaBarMode = "gallery"
+ m.top.currentBackdrop = ""
+ refreshDisplay()
+ else if m.mediaBarMode = "bookshelf"
+ m.top.currentBackdrop = ""
+ refreshDisplay()
+ else
+ refreshDisplay()
+ end if
end if
end sub
diff --git a/components/home/MediaBar.xml b/components/home/MediaBar.xml
index 995dae864..4d9ad7941 100644
--- a/components/home/MediaBar.xml
+++ b/components/home/MediaBar.xml
@@ -160,26 +160,6 @@
/>
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -217,5 +244,6 @@
+
\ No newline at end of file
diff --git a/components/home/MediaBarTask.bs b/components/home/MediaBarTask.bs
index 9755cf653..f489891e5 100644
--- a/components/home/MediaBarTask.bs
+++ b/components/home/MediaBarTask.bs
@@ -1,5 +1,6 @@
import "pkg:/source/api/sdk.bs"
import "pkg:/source/utils/misc.bs"
+import "pkg:/source/api/baserequest.bs"
sub init()
m.top.functionName = "getMediaBarData"
@@ -34,29 +35,91 @@ sub getMediaBarData()
return
end if
- allItems = []
+ fetchLimit = m.top.featuredCount + 5
+ port = CreateObject("roMessagePort")
+ requests = []
for each parentId in parentIds
- data = api.items.Get({
+ req = APIRequest("/Items/", {
userId: m.top.userId,
parentId: parentId,
includeItemTypes: includeTypes,
+ excludeItemTypes: "BoxSet,CollectionFolder",
recursive: true,
sortBy: "Random",
hasBackdrop: true,
- limit: 50,
- fields: "Overview,Genres,CommunityRating,OfficialRating,ProductionYear,RunTimeTicks,ProviderIds"
+ imageTypes: "Backdrop",
+ limit: fetchLimit,
+ fields: "Overview,People,Genres,CommunityRating,OfficialRating,ProductionYear,RunTimeTicks,ProviderIds",
+ personTypes: "Actor,GuestStar"
})
-
- items = isChainValid(data, "items") ? data.items : []
- if excludedGenreNames.count() > 0
- items = filterItemsByExcludedGenres(items, excludedGenreNames)
+ if req <> invalid
+ req.setMessagePort(port)
+ if req.AsyncGetToString()
+ requests.push(req)
+ end if
end if
- allItems.append(items)
end for
+ ' Parallelize
+ allItems = []
+ completed = 0
+ totalRequests = requests.count()
+ while completed < totalRequests
+ msg = wait(30000, port)
+ if type(msg) <> "roUrlEvent" then exit while
+
+ if msg.GetResponseCode() = 200
+ json = ParseJson(msg.GetString())
+ items = isChainValid(json, "items") ? json.items : []
+ if excludedGenreNames.count() > 0
+ items = filterItemsByExcludedGenres(items, excludedGenreNames)
+ end if
+ allItems.append(items)
+ end if
+ completed++
+ end while
+
+ filteredItems = filterAndDedupeItems(allItems)
+ shuffled = shuffleArray(filteredItems)
+ featuredCount = m.top.featuredCount
+ if shuffled.count() > featuredCount
+ allItems = shuffled.slice(0, featuredCount)
+ else
+ allItems = shuffled
+ end if
+
m.top.mediaBarData = { items: allItems }
end sub
+function filterAndDedupeItems(items as object) as object
+ result = []
+ seen = {}
+ for each item in items
+ itemType = LCase(item.LookupCI("Type") ?? "")
+
+ tmdbId = ""
+ if isChainValid(item, "providerIds")
+ tmdbId = moonfinPlugin.GetTmdbId(item.providerIds)
+ end if
+
+ if isValidAndNotEmpty(tmdbId)
+ dedupeKey = `${itemType}|tmdb|${LCase(tmdbId)}`
+ else if isValidAndNotEmpty(item.id)
+ dedupeKey = `${itemType}|id|${item.id}`
+ else
+ nameKey = LCase(item.LookupCI("Name") ?? "")
+ yearKey = isValid(item.LookupCI("ProductionYear")) ? item.LookupCI("ProductionYear").toStr() : ""
+ dedupeKey = `${itemType}|name|${nameKey}|${yearKey}`
+ end if
+
+ if not seen.DoesExist(dedupeKey)
+ seen[dedupeKey] = true
+ result.push(item)
+ end if
+ end for
+ return result
+end function
+
function getAllowedCollectionTypes() as object
contentType = m.top.contentType
if contentType = "movies"
diff --git a/components/home/MediaBarTask.xml b/components/home/MediaBarTask.xml
index f84a72a31..fc5fb992f 100644
--- a/components/home/MediaBarTask.xml
+++ b/components/home/MediaBarTask.xml
@@ -8,5 +8,6 @@
+
\ No newline at end of file
diff --git a/components/settings/settings.bs b/components/settings/settings.bs
index f31694543..8252ee68e 100644
--- a/components/settings/settings.bs
+++ b/components/settings/settings.bs
@@ -1954,6 +1954,12 @@ sub radioSettingChanged()
end if
end if
+ if isStringEqual(selectedSetting.settingName, "ui.home.mediaBarMode")
+ if isValid(m.global) and m.global.hasField("mediaBarModeChanged")
+ m.global.mediaBarModeChanged = m.global.mediaBarModeChanged + 1
+ end if
+ end if
+
if isStringEqual(selectedSetting.settingName, "navbar.position")
selectedPosition = m.radioSetting.content.getChild(m.radioSetting.checkedItem).id
scene = m.top.getScene()
diff --git a/images/bookSpine.png b/images/bookSpine.png
new file mode 100644
index 000000000..5a5cf5d79
Binary files /dev/null and b/images/bookSpine.png differ
diff --git a/images/bookWide.png b/images/bookWide.png
new file mode 100644
index 000000000..fa4a7800c
Binary files /dev/null and b/images/bookWide.png differ
diff --git a/images/bookshelfBg.png b/images/bookshelfBg.png
new file mode 100644
index 000000000..9e4012c9b
Binary files /dev/null and b/images/bookshelfBg.png differ
diff --git a/settings/settings.json b/settings/settings.json
index 7a291205b..6477398ca 100644
--- a/settings/settings.json
+++ b/settings/settings.json
@@ -293,6 +293,22 @@
"title": "Moonfin",
"id": "moonfin"
},
+ {
+ "title": "MakD",
+ "id": "makd"
+ },
+ {
+ "title": "Bookshelf",
+ "id": "bookshelf"
+ },
+ {
+ "title": "Gallery",
+ "id": "gallery"
+ },
+ {
+ "title": "Banner",
+ "id": "banner"
+ },
{
"title": "Off",
"id": "off"
diff --git a/source/utils/globals.bs b/source/utils/globals.bs
index d0548d404..d0a3cf983 100644
--- a/source/utils/globals.bs
+++ b/source/utils/globals.bs
@@ -24,7 +24,8 @@ sub setConstants()
check_white: "pkg:/images/icons/check_white.png"
}
},
- overlaySettingsChanged: 0
+ overlaySettingsChanged: 0,
+ mediaBarModeChanged: 0
})
end sub