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