From 847c2160278f231e047d449f537f00641b9a704a Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Tue, 4 Aug 2026 22:04:23 -0700 Subject: [PATCH 01/14] Add server profile sync mappings for PR 217/218 fields in settingsSync.bs --- source/utils/settingsSync.bs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/source/utils/settingsSync.bs b/source/utils/settingsSync.bs index 33cdb3c89..0eff10374 100644 --- a/source/utils/settingsSync.bs +++ b/source/utils/settingsSync.bs @@ -108,6 +108,29 @@ namespace settingsSync { pluginKey: "sonarrCalendarShowDate", rokuKey: "calendar.sonarr.showDate", type: "bool" }, { pluginKey: "mergeRadarrSonarrCalendars", rokuKey: "calendar.merge", type: "bool" }, + { pluginKey: "playbackTimeAboveLeft", rokuKey: "playback.timeAboveLeft", type: "direct" }, + { pluginKey: "playbackTimeAboveCenter", rokuKey: "playback.timeAboveCenter", type: "direct" }, + { pluginKey: "playbackTimeAboveRight", rokuKey: "playback.timeAboveRight", type: "direct" }, + { pluginKey: "playbackTimeBelowLeft", rokuKey: "playback.timeBelowLeft", type: "direct" }, + { pluginKey: "playbackTimeBelowCenter", rokuKey: "playback.timeBelowCenter", type: "direct" }, + { pluginKey: "playbackTimeBelowRight", rokuKey: "playback.timeBelowRight", type: "direct" }, + { pluginKey: "musicPlaybackTimeDisplay", rokuKey: "playback.musicTimeDisplay", type: "direct" }, + + { pluginKey: "collectionsRowShowEpisodes", rokuKey: "ui.home.collectionsRowShowEpisodes", type: "bool" }, + { pluginKey: "playlistsRowShowEpisodes", rokuKey: "ui.home.playlistsRowShowEpisodes", type: "bool" }, + { pluginKey: "mergeRecentRowsByType", rokuKey: "ui.home.mergeRecentRowsByType", type: "bool" }, + { pluginKey: "showCastButton", rokuKey: "navbar.show_cast", type: "bool" }, + { pluginKey: "classicHomeRowsPadding", rokuKey: "ui.home.classicRowsPadding", type: "direct" }, + { pluginKey: "modernHomeRowsPadding", rokuKey: "ui.home.modernRowsPadding", type: "direct" }, + { pluginKey: "detailShowTechnicalDetails", rokuKey: "ui.library.showTechnicalDetails", type: "bool" }, + { pluginKey: "recommendationSystemSource", rokuKey: "ui.home.recommendationSource", type: "direct" }, + { pluginKey: "recommendationsApplyParentalRatingCap", rokuKey: "ui.home.recommendationsParentalCap", type: "bool" }, + { pluginKey: "screensaverMode", rokuKey: "screensaver.mode", type: "direct" }, + { pluginKey: "seerrRowOrder", rokuKey: "seerr.rowOrder", type: "jsonArray" }, + { pluginKey: "hiddenSeerrRows", rokuKey: "seerr.hiddenRows", type: "jsonArray" }, + { pluginKey: "detailButtonOrder", rokuKey: "ui.itemdetail.buttonOrder", type: "jsonArray" }, + { pluginKey: "hiddenDetailButtons", rokuKey: "ui.itemdetail.hiddenButtons", type: "jsonArray" }, + { pluginKey: "customThemeId", rokuKey: "ui.theme.customThemeId", type: "direct" } ] end function From 7bca0105b6da08b19c9ff91a46dcc27bfdc2728a Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Tue, 4 Aug 2026 23:54:42 -0700 Subject: [PATCH 02/14] Sync subtitleMode, subtitle language, and audio preferences to Jellyfin user configuration on Moonbase pull --- source/utils/settingsSync.bs | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/source/utils/settingsSync.bs b/source/utils/settingsSync.bs index 0eff10374..03cb2d674 100644 --- a/source/utils/settingsSync.bs +++ b/source/utils/settingsSync.bs @@ -130,6 +130,10 @@ namespace settingsSync { pluginKey: "hiddenSeerrRows", rokuKey: "seerr.hiddenRows", type: "jsonArray" }, { pluginKey: "detailButtonOrder", rokuKey: "ui.itemdetail.buttonOrder", type: "jsonArray" }, { pluginKey: "hiddenDetailButtons", rokuKey: "ui.itemdetail.hiddenButtons", type: "jsonArray" }, + { pluginKey: "subtitleMode", rokuKey: "playback.subs.mode", type: "subtitleModeFormat" }, + { pluginKey: "defaultSubtitleLanguage", rokuKey: "playback.subs.language", type: "direct" }, + { pluginKey: "defaultAudioLanguage", rokuKey: "playback.audioPreferredLanguage", type: "direct" }, + { pluginKey: "preferDefaultAudioTrack", rokuKey: "playback.playDefaultAudioTrack", type: "bool" }, { pluginKey: "customThemeId", rokuKey: "ui.theme.customThemeId", type: "direct" } ] @@ -207,6 +211,37 @@ namespace settingsSync session.user.Update("settings", tmpSettings) + currentConfig = m.global.session.user.Configuration + if isValid(currentConfig) + configChanged = false + if isValid(profile.subtitleMode) + subMode = settingsSync.PluginToRoku(profile.subtitleMode, "subtitleModeFormat") + if isValid(subMode) + currentConfig.AddReplace("SubtitleMode", subMode) + configChanged = true + end if + end if + if isValid(profile.defaultSubtitleLanguage) + subLang = profile.defaultSubtitleLanguage + if isStringEqual(subLang, "anyLanguage") then subLang = "" + currentConfig.AddReplace("SubtitleLanguagePreference", subLang) + configChanged = true + end if + if isValid(profile.defaultAudioLanguage) + audLang = profile.defaultAudioLanguage + if isStringEqual(audLang, "anyLanguage") then audLang = "" + currentConfig.AddReplace("AudioLanguagePreference", audLang) + configChanged = true + end if + if isValid(profile.preferDefaultAudioTrack) + currentConfig.AddReplace("PlayDefaultAudioTrack", toBoolean(profile.preferDefaultAudioTrack)) + configChanged = true + end if + if configChanged + session.user.Update("Configuration", currentConfig) + end if + end if + if isValid(m.global) and m.global.hasField("overlaySettingsChanged") m.global.overlaySettingsChanged = m.global.overlaySettingsChanged + 1 end if @@ -425,6 +460,13 @@ namespace settingsSync if pluginStr = "movies" then return "movies_only" if pluginStr = "tv" then return "tv_only" return "both" + else if conversionType = "subtitleModeFormat" + valStr = LCase(pluginValue.toStr()) + if valStr = "default" then return "Default" + if valStr = "always" then return "Always" + if valStr = "onlyforced" then return "OnlyForced" + if valStr = "none" then return "None" + return "Default" end if return invalid end function @@ -468,6 +510,13 @@ namespace settingsSync if rokuStr = "movies_only" then return "movies" if rokuStr = "tv_only" then return "tv" return "both" + else if conversionType = "subtitleModeFormat" + valStr = LCase(rokuStr) + if valStr = "default" then return "default" + if valStr = "always" then return "always" + if valStr = "onlyforced" then return "onlyforced" + if valStr = "none" then return "none" + return "default" end if return invalid end function From f4b749aa571ed2accfad87604072851d89ad33cf Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 00:07:00 -0700 Subject: [PATCH 03/14] Implement playback settings: resume rewind, unpause rewind, skip lengths, and show description on pause --- components/ItemGrid/LoadVideoContentTask.bs | 10 ++++++++- components/video/VideoPlayerView.bs | 23 ++++++++++++++++++--- source/utils/settingsSync.bs | 5 +++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index a10ba724c..44b22b5d1 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -270,7 +270,15 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s end if ' PlayStart requires the time to be in seconds - video.content.PlayStart = int(playbackPosition / 10000000) + playStartSec = int(playbackPosition / 10000000) + if playStartSec > 0 + resumeRewindSec = val(m.global.session.user.settings["playback.resumeRewind"] ?? "0") + if resumeRewindSec > 0 + playStartSec = playStartSec - resumeRewindSec + if playStartSec < 0 then playStartSec = 0 + end if + end if + video.content.PlayStart = playStartSec if not isValid(mediaSourceId) then mediaSourceId = video.id if meta.live then mediaSourceId = "" diff --git a/components/video/VideoPlayerView.bs b/components/video/VideoPlayerView.bs index ac1f509d7..57818d742 100644 --- a/components/video/VideoPlayerView.bs +++ b/components/video/VideoPlayerView.bs @@ -1276,6 +1276,7 @@ sub onState() ' Handle OSD visibility based on playback state if isStringEqual(m.top.state, MediaPlaybackState.PAUSED) + m.wasPaused = true ' Keep OSD visible when paused if not m.scrubActive if not m.osd.visible @@ -1285,12 +1286,23 @@ sub onState() ' Disable auto-hide timer when paused m.osd.inactiveTimeout = 0 ' Show dark overlay when paused - m.pauseOverlay.visible = true + showDescOnPause = toBoolean(m.global.session.user.settings["playback.showDescriptionOnPause"] ?? true) + m.pauseOverlay.visible = showDescOnPause else if isStringEqual(m.top.state, MediaPlaybackState.PLAYING) ' Enable auto-hide timer when playing (5 seconds) m.osd.inactiveTimeout = 5 ' Hide dark overlay when playing m.pauseOverlay.visible = false + + if m.wasPaused ?? false + m.wasPaused = false + unpauseRewindSec = val(m.global.session.user.settings["playback.unpauseRewind"] ?? "0") + if unpauseRewindSec > 0 and m.top.position > unpauseRewindSec + newPos = m.top.position - unpauseRewindSec + if newPos < 0 then newPos = 0 + m.top.seek = newPos + end if + end if end if ' When buffering, start timer to monitor buffering process @@ -1906,11 +1918,16 @@ function onKeyEvent(key as string, press as boolean) as boolean if not m.osd.visible and not m.top.trickPlayBar.visible if not stateAllowsOSD() then return true + skipFwd = val(m.global.session.user.settings["playback.skipForwardLength"] ?? "10") + skipBack = val(m.global.session.user.settings["playback.skipBackLength"] ?? "10") + if skipFwd <= 0 then skipFwd = 10 + if skipBack <= 0 then skipBack = 10 + if key = KeyCode.RIGHT - newPosition = m.top.position + 10 + newPosition = m.top.position + skipFwd if newPosition > m.top.duration then newPosition = m.top.duration else - newPosition = m.top.position - 10 + newPosition = m.top.position - skipBack if newPosition < 0 then newPosition = 0 end if diff --git a/source/utils/settingsSync.bs b/source/utils/settingsSync.bs index 03cb2d674..ce029accf 100644 --- a/source/utils/settingsSync.bs +++ b/source/utils/settingsSync.bs @@ -52,6 +52,11 @@ namespace settingsSync ' Settings the server can now store that already had a switch here. { pluginKey: "cinemaModeEnabled", rokuKey: "playback.cinemamode", type: "bool" }, { pluginKey: "autoplayNextEpisode", rokuKey: "playback.showNextUpAfterFinish", type: "bool" }, + { pluginKey: "showDescriptionOnPause", rokuKey: "playback.showDescriptionOnPause", type: "bool" }, + { pluginKey: "resumeSubtractDuration", rokuKey: "playback.resumeRewind", type: "intToStr" }, + { pluginKey: "unpauseRewindDuration", rokuKey: "playback.unpauseRewind", type: "intToStr" }, + { pluginKey: "skipBackLength", rokuKey: "playback.skipBackLength", type: "intToStr" }, + { pluginKey: "skipForwardLength", rokuKey: "playback.skipForwardLength", type: "intToStr" }, { pluginKey: "seerrBlockNsfw", rokuKey: "seerr.blockNsfw", type: "bool" }, { pluginKey: "nextUpMaxDays", rokuKey: "ui.details.maxdaysnextup", type: "intToStr" }, ' Roku registry stores seconds as an integer, matching the server's representation. From b7356f7f9e5250d8388c6fffa404c1d6cb58e0b8 Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 00:28:04 -0700 Subject: [PATCH 04/14] Remove showDescriptionOnPause setting from Roku settingsSync and VideoPlayerView --- components/ItemGrid/LoadVideoContentTask.bs | 18 +++++++++++++++++- components/video/VideoPlayerView.bs | 3 +-- source/utils/settingsSync.bs | 5 ++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index 44b22b5d1..068f713db 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -719,7 +719,10 @@ function sortSubtitles(MediaStreams) trackType = "normal" end if - if prefered_lang <> "" and prefered_lang = stream.Track.Language + preferSdh = toBoolean(m.global.session.user.settings["playback.preferSdhSubtitles"] ?? false) + isSdhTrack = toBoolean(chainLookupReturn(stream, "IsHearingImpaired", false)) or instr(1, LCase(chainLookupReturn(stream, "displaytitle", "")), "sdh") > 0 + + if (prefered_lang <> "" and prefered_lang = stream.Track.Language) or (preferSdh and isSdhTrack) tracks[trackType].unshift(stream) if stream.IsTextSubtitleStream @@ -769,6 +772,19 @@ function FindPreferredAudioStream(streams as dynamic) as integer firstAudioTrack = -1 + preferAudioDesc = toBoolean(m.global.session.user.settings["playback.preferAudioDescription"] ?? false) + if preferAudioDesc + for i = 0 to streams.Count() - 1 + if LCase(streams[i].Type) = "audio" + if toBoolean(chainLookupReturn(streams[i], "IsAudioDescription", false)) or instr(1, LCase(chainLookupReturn(streams[i], "DisplayTitle", "")), "audio description") > 0 + if not isValid(preferredLanguage) or isStringEqual(chainLookupReturn(streams[i], "Language", invalid), preferredLanguage) + return i + end if + end if + end if + end for + end if + ' No user selection and not configured to play the default, how about a preferred language? if isValid(preferredLanguage) for i = 0 to streams.Count() - 1 diff --git a/components/video/VideoPlayerView.bs b/components/video/VideoPlayerView.bs index 57818d742..478d5a971 100644 --- a/components/video/VideoPlayerView.bs +++ b/components/video/VideoPlayerView.bs @@ -1286,8 +1286,7 @@ sub onState() ' Disable auto-hide timer when paused m.osd.inactiveTimeout = 0 ' Show dark overlay when paused - showDescOnPause = toBoolean(m.global.session.user.settings["playback.showDescriptionOnPause"] ?? true) - m.pauseOverlay.visible = showDescOnPause + m.pauseOverlay.visible = true else if isStringEqual(m.top.state, MediaPlaybackState.PLAYING) ' Enable auto-hide timer when playing (5 seconds) m.osd.inactiveTimeout = 5 diff --git a/source/utils/settingsSync.bs b/source/utils/settingsSync.bs index ce029accf..24111ff11 100644 --- a/source/utils/settingsSync.bs +++ b/source/utils/settingsSync.bs @@ -52,11 +52,14 @@ namespace settingsSync ' Settings the server can now store that already had a switch here. { pluginKey: "cinemaModeEnabled", rokuKey: "playback.cinemamode", type: "bool" }, { pluginKey: "autoplayNextEpisode", rokuKey: "playback.showNextUpAfterFinish", type: "bool" }, - { pluginKey: "showDescriptionOnPause", rokuKey: "playback.showDescriptionOnPause", type: "bool" }, { pluginKey: "resumeSubtractDuration", rokuKey: "playback.resumeRewind", type: "intToStr" }, { pluginKey: "unpauseRewindDuration", rokuKey: "playback.unpauseRewind", type: "intToStr" }, { pluginKey: "skipBackLength", rokuKey: "playback.skipBackLength", type: "intToStr" }, { pluginKey: "skipForwardLength", rokuKey: "playback.skipForwardLength", type: "intToStr" }, + { pluginKey: "introAction", rokuKey: "playback.mediasegments.intro", type: "direct" }, + { pluginKey: "outroAction", rokuKey: "playback.mediasegments.outro", type: "direct" }, + { pluginKey: "preferAudioDescription", rokuKey: "playback.preferAudioDescription", type: "bool" }, + { pluginKey: "preferSdhSubtitles", rokuKey: "playback.preferSdhSubtitles", type: "bool" }, { pluginKey: "seerrBlockNsfw", rokuKey: "seerr.blockNsfw", type: "bool" }, { pluginKey: "nextUpMaxDays", rokuKey: "ui.details.maxdaysnextup", type: "intToStr" }, ' Roku registry stores seconds as an integer, matching the server's representation. From 2492baa00ce38623cdd3682f0508057e15edd068 Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 12:13:28 -0700 Subject: [PATCH 05/14] =?UTF-8?q?=EF=BB=BFAdd=20Playback=20category=20and?= =?UTF-8?q?=20UI=20toggles=20to=20settings.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add UI setting toggle entries for Resume Rewind, Unpause Rewind, Skip Forward Length, Skip Back Length, Prefer SDH Subtitles, Prefer Audio Description, and Show Description on Pause. --- settings/settings.json | 148 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/settings/settings.json b/settings/settings.json index 3d5108fdd..805197718 100644 --- a/settings/settings.json +++ b/settings/settings.json @@ -2715,6 +2715,154 @@ } ] }, + { + "title": "Playback", + "description": "Resume rewind, unpause rewind, seek step sizes, and subtitle/audio track preferences.", + "icon": "videonav.png", + "children": [ + { + "title": "Resume Rewind", + "description": "Automatically rewind a few seconds when resuming playback from a saved bookmark position.", + "settingName": "playback.resumeRewind", + "type": "radio", + "default": "0", + "options": [ + { + "title": "Off", + "id": "0" + }, + { + "title": "2 Seconds", + "id": "2" + }, + { + "title": "3 Seconds", + "id": "3" + }, + { + "title": "5 Seconds", + "id": "5" + }, + { + "title": "7 Seconds", + "id": "7" + }, + { + "title": "10 Seconds", + "id": "10" + } + ] + }, + { + "title": "Unpause Rewind", + "description": "Automatically rewind a few seconds when resuming video playback after being paused.", + "settingName": "playback.unpauseRewind", + "type": "radio", + "default": "0", + "options": [ + { + "title": "Off", + "id": "0" + }, + { + "title": "1 Second", + "id": "1" + }, + { + "title": "2 Seconds", + "id": "2" + }, + { + "title": "3 Seconds", + "id": "3" + }, + { + "title": "5 Seconds", + "id": "5" + } + ] + }, + { + "title": "Skip Forward Length", + "description": "The amount of time to jump forward when pressing D-pad Right during playback.", + "settingName": "playback.skipForwardLength", + "type": "radio", + "default": "10", + "options": [ + { + "title": "5 Seconds", + "id": "5" + }, + { + "title": "10 Seconds", + "id": "10" + }, + { + "title": "15 Seconds", + "id": "15" + }, + { + "title": "30 Seconds", + "id": "30" + }, + { + "title": "60 Seconds", + "id": "60" + } + ] + }, + { + "title": "Skip Back Length", + "description": "The amount of time to jump backward when pressing D-pad Left during playback.", + "settingName": "playback.skipBackLength", + "type": "radio", + "default": "10", + "options": [ + { + "title": "5 Seconds", + "id": "5" + }, + { + "title": "10 Seconds", + "id": "10" + }, + { + "title": "15 Seconds", + "id": "15" + }, + { + "title": "30 Seconds", + "id": "30" + }, + { + "title": "60 Seconds", + "id": "60" + } + ] + }, + { + "title": "Prefer SDH Subtitles", + "description": "Automatically prefer Subtitles for the Deaf and Hard of Hearing (SDH) tracks when matching subtitle streams.", + "settingName": "playback.preferSdhSubtitles", + "type": "bool", + "default": "false" + }, + { + "title": "Prefer Audio Description", + "description": "Automatically prefer Audio Description (descriptive audio for visually impaired) tracks when matching audio streams.", + "settingName": "playback.preferAudioDescription", + "type": "bool", + "default": "false" + }, + { + "title": "Show Description on Pause", + "description": "Display the video overview/description on the pause overlay.", + "settingName": "playback.showDescriptionOnPause", + "type": "bool", + "default": "true" + } + ] + }, { "title": "Libraries", "description": "Settings relating to the appearance of Library pages.", From bc54d9a063b21ff26697e681bd3932152f14b204 Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 12:14:15 -0700 Subject: [PATCH 06/14] =?UTF-8?q?=EF=BB=BFRemove=20Show=20Description=20on?= =?UTF-8?q?=20Pause=20from=20settings.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove Show Description on Pause entry as requested. --- settings/settings.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/settings/settings.json b/settings/settings.json index 805197718..7c17f0804 100644 --- a/settings/settings.json +++ b/settings/settings.json @@ -2853,13 +2853,6 @@ "settingName": "playback.preferAudioDescription", "type": "bool", "default": "false" - }, - { - "title": "Show Description on Pause", - "description": "Display the video overview/description on the pause overlay.", - "settingName": "playback.showDescriptionOnPause", - "type": "bool", - "default": "true" } ] }, From 23c4f09ce55d099cc90b4a96a425ce3bdb6a66e3 Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 12:16:56 -0700 Subject: [PATCH 07/14] =?UTF-8?q?=EF=BB=BFOrganize=20new=20playback=20sett?= =?UTF-8?q?ings=20into=20existing=20Playback=20sub-categories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Place Resume Rewind, Unpause Rewind, Skip Forward Length, and Skip Back Length under Video Playback Preferences. - Place Prefer Audio Description under Audio Preferences. - Place Prefer SDH Subtitles under Subtitle Preferences. --- settings/settings.json | 275 ++++++++++++++++++++--------------------- 1 file changed, 134 insertions(+), 141 deletions(-) diff --git a/settings/settings.json b/settings/settings.json index 7c17f0804..23c250bd9 100644 --- a/settings/settings.json +++ b/settings/settings.json @@ -1558,6 +1558,126 @@ "title": "Video Playback Preferences", "description": "Core video engine and streaming quality settings", "children": [ + { + "title": "Resume Rewind", + "description": "Automatically rewind a few seconds when resuming playback from a saved bookmark position.", + "settingName": "playback.resumeRewind", + "type": "radio", + "default": "0", + "options": [ + { + "title": "Off", + "id": "0" + }, + { + "title": "2 Seconds", + "id": "2" + }, + { + "title": "3 Seconds", + "id": "3" + }, + { + "title": "5 Seconds", + "id": "5" + }, + { + "title": "7 Seconds", + "id": "7" + }, + { + "title": "10 Seconds", + "id": "10" + } + ] + }, + { + "title": "Unpause Rewind", + "description": "Automatically rewind a few seconds when resuming video playback after being paused.", + "settingName": "playback.unpauseRewind", + "type": "radio", + "default": "0", + "options": [ + { + "title": "Off", + "id": "0" + }, + { + "title": "1 Second", + "id": "1" + }, + { + "title": "2 Seconds", + "id": "2" + }, + { + "title": "3 Seconds", + "id": "3" + }, + { + "title": "5 Seconds", + "id": "5" + } + ] + }, + { + "title": "Skip Forward Length", + "description": "The amount of time to jump forward when pressing D-pad Right during playback.", + "settingName": "playback.skipForwardLength", + "type": "radio", + "default": "10", + "options": [ + { + "title": "5 Seconds", + "id": "5" + }, + { + "title": "10 Seconds", + "id": "10" + }, + { + "title": "15 Seconds", + "id": "15" + }, + { + "title": "30 Seconds", + "id": "30" + }, + { + "title": "60 Seconds", + "id": "60" + } + ] + }, + { + "title": "Skip Back Length", + "description": "The amount of time to jump backward when pressing D-pad Left during playback.", + "settingName": "playback.skipBackLength", + "type": "radio", + "default": "10", + "options": [ + { + "title": "5 Seconds", + "id": "5" + }, + { + "title": "10 Seconds", + "id": "10" + }, + { + "title": "15 Seconds", + "id": "15" + }, + { + "title": "30 Seconds", + "id": "30" + }, + { + "title": "60 Seconds", + "id": "60" + } + ] + }, { "title": "Bitrate Limit", "description": "Configure the maximum playback bitrate.", @@ -1729,6 +1849,13 @@ "title": "Audio Preferences", "description": "Audio tracks, processing, and related settings", "children": [ + { + "title": "Prefer Audio Description", + "description": "Automatically prefer Audio Description (descriptive audio for visually impaired) tracks when matching audio streams.", + "settingName": "playback.preferAudioDescription", + "type": "bool", + "default": "false" + }, { "title": "Preferred audio language", "description": "Choose the default audio language for playback", @@ -1996,6 +2123,13 @@ "title": "Subtitle Preferences", "description": "Change subtitle modes, default languages, and other options", "children": [ + { + "title": "Prefer SDH Subtitles", + "description": "Automatically prefer Subtitles for the Deaf and Hard of Hearing (SDH) tracks when matching subtitle streams.", + "settingName": "playback.preferSdhSubtitles", + "type": "bool", + "default": "false" + }, { "title": "Subtitle Mode", "description": "Choose the default subtitle mode for playback", @@ -2715,147 +2849,6 @@ } ] }, - { - "title": "Playback", - "description": "Resume rewind, unpause rewind, seek step sizes, and subtitle/audio track preferences.", - "icon": "videonav.png", - "children": [ - { - "title": "Resume Rewind", - "description": "Automatically rewind a few seconds when resuming playback from a saved bookmark position.", - "settingName": "playback.resumeRewind", - "type": "radio", - "default": "0", - "options": [ - { - "title": "Off", - "id": "0" - }, - { - "title": "2 Seconds", - "id": "2" - }, - { - "title": "3 Seconds", - "id": "3" - }, - { - "title": "5 Seconds", - "id": "5" - }, - { - "title": "7 Seconds", - "id": "7" - }, - { - "title": "10 Seconds", - "id": "10" - } - ] - }, - { - "title": "Unpause Rewind", - "description": "Automatically rewind a few seconds when resuming video playback after being paused.", - "settingName": "playback.unpauseRewind", - "type": "radio", - "default": "0", - "options": [ - { - "title": "Off", - "id": "0" - }, - { - "title": "1 Second", - "id": "1" - }, - { - "title": "2 Seconds", - "id": "2" - }, - { - "title": "3 Seconds", - "id": "3" - }, - { - "title": "5 Seconds", - "id": "5" - } - ] - }, - { - "title": "Skip Forward Length", - "description": "The amount of time to jump forward when pressing D-pad Right during playback.", - "settingName": "playback.skipForwardLength", - "type": "radio", - "default": "10", - "options": [ - { - "title": "5 Seconds", - "id": "5" - }, - { - "title": "10 Seconds", - "id": "10" - }, - { - "title": "15 Seconds", - "id": "15" - }, - { - "title": "30 Seconds", - "id": "30" - }, - { - "title": "60 Seconds", - "id": "60" - } - ] - }, - { - "title": "Skip Back Length", - "description": "The amount of time to jump backward when pressing D-pad Left during playback.", - "settingName": "playback.skipBackLength", - "type": "radio", - "default": "10", - "options": [ - { - "title": "5 Seconds", - "id": "5" - }, - { - "title": "10 Seconds", - "id": "10" - }, - { - "title": "15 Seconds", - "id": "15" - }, - { - "title": "30 Seconds", - "id": "30" - }, - { - "title": "60 Seconds", - "id": "60" - } - ] - }, - { - "title": "Prefer SDH Subtitles", - "description": "Automatically prefer Subtitles for the Deaf and Hard of Hearing (SDH) tracks when matching subtitle streams.", - "settingName": "playback.preferSdhSubtitles", - "type": "bool", - "default": "false" - }, - { - "title": "Prefer Audio Description", - "description": "Automatically prefer Audio Description (descriptive audio for visually impaired) tracks when matching audio streams.", - "settingName": "playback.preferAudioDescription", - "type": "bool", - "default": "false" - } - ] - }, { "title": "Libraries", "description": "Settings relating to the appearance of Library pages.", From 6006b89301797656e8c6b8a76014ee2b541ca2ee Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 14:26:03 -0700 Subject: [PATCH 08/14] =?UTF-8?q?=EF=BB=BFAdd=20UI=20settings=20toggle=20e?= =?UTF-8?q?ntries=20for=20all=20PR=20199=20sync=20keys=20in=20settings.jso?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add settings.json definitions for: - showCastButton (navbar.show_cast) - collectionsRowShowEpisodes (ui.home.collectionsRowShowEpisodes) - playlistsRowShowEpisodes (ui.home.playlistsRowShowEpisodes) - classicHomeRowsPadding (ui.home.classicRowsPadding) - modernHomeRowsPadding (ui.home.modernRowsPadding) - recommendationSystemSource (ui.home.recommendationSource) - recommendationsApplyParentalRatingCap (ui.home.recommendationsParentalCap) - detailShowTechnicalDetails (ui.library.showTechnicalDetails) - musicPlaybackTimeDisplay (playback.musicTimeDisplay) - playbackTimeAboveLeft/Center/Right & playbackTimeBelowLeft/Center/Right (playback.timeAbove/Below...) - screensaverMode (screensaver.mode) --- settings/settings.json | 220 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/settings/settings.json b/settings/settings.json index 23c250bd9..556312778 100644 --- a/settings/settings.json +++ b/settings/settings.json @@ -158,6 +158,13 @@ "settingName": "navbar.show_shuffle", "type": "bool", "default": "true" + }, + { + "title": "Show Cast Button", + "description": "Show the cast button in the navigation bar", + "settingName": "navbar.show_cast", + "type": "bool", + "default": "true" } ] }, @@ -294,6 +301,90 @@ "id": "series" } ] + }, + { + "title": "Collections Row Show Episodes", + "description": "Include TV episodes when displaying collections on the Home Screen", + "settingName": "ui.home.collectionsRowShowEpisodes", + "type": "bool", + "default": "false" + }, + { + "title": "Playlists Row Show Episodes", + "description": "Include TV episodes when displaying playlists on the Home Screen", + "settingName": "ui.home.playlistsRowShowEpisodes", + "type": "bool", + "default": "false" + }, + { + "title": "Classic Layout Home Rows Padding", + "description": "Adjust vertical padding between home screen rows in classic layout", + "settingName": "ui.home.classicRowsPadding", + "type": "radio", + "default": "normal", + "options": [ + { + "title": "Compact", + "id": "compact" + }, + { + "title": "Normal", + "id": "normal" + }, + { + "title": "Spacious", + "id": "spacious" + } + ] + }, + { + "title": "Modern Layout Home Rows Padding", + "description": "Adjust vertical padding between home screen rows in modern layout", + "settingName": "ui.home.modernRowsPadding", + "type": "radio", + "default": "normal", + "options": [ + { + "title": "Compact", + "id": "compact" + }, + { + "title": "Normal", + "id": "normal" + }, + { + "title": "Spacious", + "id": "spacious" + } + ] + }, + { + "title": "Recommendation Engine Source", + "description": "Select the recommendation engine source for home screen recommendations", + "settingName": "ui.home.recommendationSource", + "type": "radio", + "default": "server", + "options": [ + { + "title": "Server", + "id": "server" + }, + { + "title": "Local", + "id": "local" + }, + { + "title": "Hybrid", + "id": "hybrid" + } + ] + }, + { + "title": "Apply Parental Rating Cap to Recommendations", + "description": "Filter home screen recommendations according to user parental rating limits", + "settingName": "ui.home.recommendationsParentalCap", + "type": "bool", + "default": "true" } ] }, @@ -1842,6 +1933,90 @@ "default": "false" } ] + }, + { + "title": "Media Bar Playback Time Above Left", + "description": "Select information to display above the progress bar on the left side", + "settingName": "playback.timeAboveLeft", + "type": "radio", + "default": "elapsed", + "options": [ + { "title": "None", "id": "none" }, + { "title": "Current Time", "id": "time" }, + { "title": "Elapsed Time", "id": "elapsed" }, + { "title": "Remaining Time", "id": "remaining" }, + { "title": "Ends At Time", "id": "endsAt" } + ] + }, + { + "title": "Media Bar Playback Time Above Center", + "description": "Select information to display above the progress bar in the center", + "settingName": "playback.timeAboveCenter", + "type": "radio", + "default": "none", + "options": [ + { "title": "None", "id": "none" }, + { "title": "Current Time", "id": "time" }, + { "title": "Elapsed Time", "id": "elapsed" }, + { "title": "Remaining Time", "id": "remaining" }, + { "title": "Ends At Time", "id": "endsAt" } + ] + }, + { + "title": "Media Bar Playback Time Above Right", + "description": "Select information to display above the progress bar on the right side", + "settingName": "playback.timeAboveRight", + "type": "radio", + "default": "remaining", + "options": [ + { "title": "None", "id": "none" }, + { "title": "Current Time", "id": "time" }, + { "title": "Elapsed Time", "id": "elapsed" }, + { "title": "Remaining Time", "id": "remaining" }, + { "title": "Ends At Time", "id": "endsAt" } + ] + }, + { + "title": "Media Bar Playback Time Below Left", + "description": "Select information to display below the progress bar on the left side", + "settingName": "playback.timeBelowLeft", + "type": "radio", + "default": "none", + "options": [ + { "title": "None", "id": "none" }, + { "title": "Current Time", "id": "time" }, + { "title": "Elapsed Time", "id": "elapsed" }, + { "title": "Remaining Time", "id": "remaining" }, + { "title": "Ends At Time", "id": "endsAt" } + ] + }, + { + "title": "Media Bar Playback Time Below Center", + "description": "Select information to display below the progress bar in the center", + "settingName": "playback.timeBelowCenter", + "type": "radio", + "default": "none", + "options": [ + { "title": "None", "id": "none" }, + { "title": "Current Time", "id": "time" }, + { "title": "Elapsed Time", "id": "elapsed" }, + { "title": "Remaining Time", "id": "remaining" }, + { "title": "Ends At Time", "id": "endsAt" } + ] + }, + { + "title": "Media Bar Playback Time Below Right", + "description": "Select information to display below the progress bar on the right side", + "settingName": "playback.timeBelowRight", + "type": "radio", + "default": "endsAt", + "options": [ + { "title": "None", "id": "none" }, + { "title": "Current Time", "id": "time" }, + { "title": "Elapsed Time", "id": "elapsed" }, + { "title": "Remaining Time", "id": "remaining" }, + { "title": "Ends At Time", "id": "endsAt" } + ] } ] }, @@ -1849,6 +2024,23 @@ "title": "Audio Preferences", "description": "Audio tracks, processing, and related settings", "children": [ + { + "title": "Music Playback Time Display", + "description": "Choose whether the music player displays elapsed time or remaining time", + "settingName": "playback.musicTimeDisplay", + "type": "radio", + "default": "elapsed", + "options": [ + { + "title": "Elapsed Time", + "id": "elapsed" + }, + { + "title": "Remaining Time", + "id": "remaining" + } + ] + }, { "title": "Prefer Audio Description", "description": "Automatically prefer Audio Description (descriptive audio for visually impaired) tracks when matching audio streams.", @@ -2786,6 +2978,27 @@ "settingName": "load.allowwhatsnew", "type": "bool", "default": "true" + }, + { + "title": "Screensaver Mode", + "description": "Choose screensaver visual style when the app is idle", + "settingName": "screensaver.mode", + "type": "radio", + "default": "backdrop", + "options": [ + { + "title": "Backdrop Slideshow", + "id": "backdrop" + }, + { + "title": "Logo Bounce", + "id": "logo" + }, + { + "title": "Black Screen", + "id": "black" + } + ] } ] }, @@ -2846,6 +3059,13 @@ "settingName": "ui.itemdetail.expandedTabs", "type": "bool", "default": "true" + }, + { + "title": "Show Technical Media Details", + "description": "Display technical details (codec, bitrate, resolution, container) on item details screens", + "settingName": "ui.library.showTechnicalDetails", + "type": "bool", + "default": "true" } ] }, From e8e2e5c1e72f85a341c5e4bc884ed8c97dba357e Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 14:53:02 -0700 Subject: [PATCH 09/14] =?UTF-8?q?=EF=BB=BF-=20first=20implementation=20of?= =?UTF-8?q?=20missing=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/details/ModernItemDetails.bs | 14 ++++++++++-- components/home/HomeRows.bs | 16 +++++++++++--- components/music/AudioPlayerView.bs | 7 +++++- components/seerr/SeerrDiscoveryScreen.bs | 16 ++++++++++++++ components/video/OSD.bs | 26 ++++++++++++++++++++-- settings/settings.json | 28 ------------------------ 6 files changed, 71 insertions(+), 36 deletions(-) diff --git a/components/details/ModernItemDetails.bs b/components/details/ModernItemDetails.bs index dcd050178..97a5182ad 100644 --- a/components/details/ModernItemDetails.bs +++ b/components/details/ModernItemDetails.bs @@ -1199,9 +1199,10 @@ sub updateCodecPills() end if end for - ' Hide codecRow entirely when no codecs + ' Hide codecRow entirely when no codecs or when technical details are disabled + showTechDetails = toBoolean(chainLookupReturn(m.global.session, "user.settings.ui.library.showTechnicalDetails", true)) codecRow = m.top.findNode("codecRow") - if isValid(codecRow) then codecRow.visible = anyCodec + if isValid(codecRow) then codecRow.visible = anyCodec and showTechDetails end sub sub populateRatings(itemData as object) @@ -2207,6 +2208,15 @@ sub addMusicButtons() end sub sub addButton(config as object) + hiddenButtons = chainLookupReturn(m.global.session, "user.settings.ui.itemdetail.hiddenButtons", []) + if isValid(hiddenButtons) and isArray(hiddenButtons) + for each hiddenId in hiddenButtons + if isStringEqual(toStringValue(hiddenId), config.action) or isStringEqual(toStringValue(hiddenId), config.id) + return + end if + end for + end if + buttonGroup = detailButtons.createModernButton(config, m.buttonGroups.count()) buttonGroup.observeField("focusedChild", "onButtonFocusChanged") m.buttonRow.appendChild(buttonGroup) diff --git a/components/home/HomeRows.bs b/components/home/HomeRows.bs index 80f3d6970..f697eda44 100644 --- a/components/home/HomeRows.bs +++ b/components/home/HomeRows.bs @@ -149,9 +149,19 @@ sub updateSize() m.top.itemSize = [1703, itemHeight] modernRows = isModernHomeRows() - - ' spacing between rows (modern reserves space for the focused card info block) - m.top.itemSpacing = modernRows ? [0, 210] : [0, 60] + if modernRows + modPadding = chainLookupReturn(m.global.session, "user.settings.ui.home.modernRowsPadding", "normal") + verticalSpacing = 210 + if modPadding = "compact" then verticalSpacing = 170 + if modPadding = "spacious" then verticalSpacing = 250 + m.top.itemSpacing = [0, verticalSpacing] + else + classPadding = chainLookupReturn(m.global.session, "user.settings.ui.home.classicRowsPadding", "normal") + verticalSpacing = 60 + if classPadding = "compact" then verticalSpacing = 40 + if classPadding = "spacious" then verticalSpacing = 90 + m.top.itemSpacing = [0, verticalSpacing] + end if ' spacing between items in a row. Modern keeps cards tight, the focused card ' makes its own room by sliding the following cells aside (reflow). diff --git a/components/music/AudioPlayerView.bs b/components/music/AudioPlayerView.bs index 21f792a64..2765257f8 100644 --- a/components/music/AudioPlayerView.bs +++ b/components/music/AudioPlayerView.bs @@ -521,7 +521,12 @@ sub onAudioDataChanged() m.songDuration = currentItem.RunTimeTicks / 10000000.0 ' Update displayed total audio length - m.totalLengthTimestamp.text = ticksToHuman(currentItem.RunTimeTicks) + musicDisplay = chainLookupReturn(m.global.session, "user.settings.playback.musicTimeDisplay", "elapsed") + if musicDisplay = "remaining" + m.totalLengthTimestamp.text = "-" + ticksToHuman(currentItem.RunTimeTicks) + else + m.totalLengthTimestamp.text = ticksToHuman(currentItem.RunTimeTicks) + end if end if ' Validate lyrics data and for now only support timed lyrics diff --git a/components/seerr/SeerrDiscoveryScreen.bs b/components/seerr/SeerrDiscoveryScreen.bs index 322aa5352..85e7d7c72 100644 --- a/components/seerr/SeerrDiscoveryScreen.bs +++ b/components/seerr/SeerrDiscoveryScreen.bs @@ -163,6 +163,22 @@ sub LoadDiscoveryCategories() } ] + hiddenRows = chainLookupReturn(m.global.session, "user.settings.seerr.hiddenRows", []) + if isValid(hiddenRows) and isArray(hiddenRows) and hiddenRows.Count() > 0 + filteredCategories = [] + for each category in categories + isHidden = false + for each hiddenName in hiddenRows + if isStringEqual(toStringValue(hiddenName), category.name) + isHidden = true + exit for + end if + end for + if not isHidden then filteredCategories.push(category) + end for + categories = filteredCategories + end if + m.categoryData = categories contentNode = CreateObject("roSGNode", "ContentNode") diff --git a/components/video/OSD.bs b/components/video/OSD.bs index e33278146..9aab9c830 100644 --- a/components/video/OSD.bs +++ b/components/video/OSD.bs @@ -85,18 +85,40 @@ end sub ' onProgressPercentageChanged: Handler for changes to m.top.progressPercentage param ' +function formatOSDTimeSlot(slotKey as string) as string + slotMode = chainLookupReturn(m.global.session, "user.settings." + slotKey, "none") + if slotMode = "none" or slotMode = "" then return "" + if slotMode = "elapsed" then return secondsToHuman(m.top.positionTime, true) + if slotMode = "remaining" then return "-" + secondsToHuman(m.top.remainingPositionTime, true) + if slotMode = "endsAt" then return "Ends at " + m.top.videoEndingTime + if slotMode = "time" then return m.top.videoEndingTime + return "" +end function + sub onProgressPercentageChanged() totalTime = m.top.positionTime + m.top.remainingPositionTime - m.videoRemainingTime.text = secondsToHuman(m.top.positionTime, true) + " / " + secondsToHuman(totalTime, true) m.progressBar.width = m.progressBarBackground.width * m.top.progressPercentage + aboveLeft = formatOSDTimeSlot("playback.timeAboveLeft") + aboveRight = formatOSDTimeSlot("playback.timeAboveRight") + if aboveLeft <> "" or aboveRight <> "" + m.videoRemainingTime.text = (aboveLeft <> "" ? aboveLeft : "") + (aboveRight <> "" ? " / " + aboveRight : "") + else + m.videoRemainingTime.text = secondsToHuman(m.top.positionTime, true) + " / " + secondsToHuman(totalTime, true) + end if + ' Update circle indicator position (centered on progress, offset by circle radius) indicatorX = 103 + (m.progressBarBackground.width * m.top.progressPercentage) - 10 m.progressIndicator.translation = [indicatorX, 812] end sub sub setVideoEndingTime() - m.videoEndingTime.text = "Ends at " + m.top.videoEndingTime + belowRight = formatOSDTimeSlot("playback.timeBelowRight") + if belowRight <> "" + m.videoEndingTime.text = belowRight + else + m.videoEndingTime.text = "Ends at " + m.top.videoEndingTime + end if end sub sub updateSeekPreview(totalTime as float) diff --git a/settings/settings.json b/settings/settings.json index 556312778..db0f3a5f6 100644 --- a/settings/settings.json +++ b/settings/settings.json @@ -158,13 +158,6 @@ "settingName": "navbar.show_shuffle", "type": "bool", "default": "true" - }, - { - "title": "Show Cast Button", - "description": "Show the cast button in the navigation bar", - "settingName": "navbar.show_cast", - "type": "bool", - "default": "true" } ] }, @@ -2978,27 +2971,6 @@ "settingName": "load.allowwhatsnew", "type": "bool", "default": "true" - }, - { - "title": "Screensaver Mode", - "description": "Choose screensaver visual style when the app is idle", - "settingName": "screensaver.mode", - "type": "radio", - "default": "backdrop", - "options": [ - { - "title": "Backdrop Slideshow", - "id": "backdrop" - }, - { - "title": "Logo Bounce", - "id": "logo" - }, - { - "title": "Black Screen", - "id": "black" - } - ] } ] }, From 43dace9dddd579420d59d2bedc72f9f36756514c Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 14:55:23 -0700 Subject: [PATCH 10/14] =?UTF-8?q?=EF=BB=BF-=20first=20implementation=20of?= =?UTF-8?q?=20missing=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/details/ModernItemDetails.bs | 5 +++-- components/seerr/SeerrDiscoveryScreen.bs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/components/details/ModernItemDetails.bs b/components/details/ModernItemDetails.bs index 97a5182ad..0f9ac3b7f 100644 --- a/components/details/ModernItemDetails.bs +++ b/components/details/ModernItemDetails.bs @@ -2209,9 +2209,10 @@ end sub sub addButton(config as object) hiddenButtons = chainLookupReturn(m.global.session, "user.settings.ui.itemdetail.hiddenButtons", []) - if isValid(hiddenButtons) and isArray(hiddenButtons) + if isValid(hiddenButtons) and type(hiddenButtons) = "roArray" for each hiddenId in hiddenButtons - if isStringEqual(toStringValue(hiddenId), config.action) or isStringEqual(toStringValue(hiddenId), config.id) + hStr = isValid(hiddenId) ? hiddenId.ToStr() : "" + if isStringEqual(hStr, config.action) or isStringEqual(hStr, config.id) return end if end for diff --git a/components/seerr/SeerrDiscoveryScreen.bs b/components/seerr/SeerrDiscoveryScreen.bs index 85e7d7c72..3d2f74dbe 100644 --- a/components/seerr/SeerrDiscoveryScreen.bs +++ b/components/seerr/SeerrDiscoveryScreen.bs @@ -164,12 +164,13 @@ sub LoadDiscoveryCategories() ] hiddenRows = chainLookupReturn(m.global.session, "user.settings.seerr.hiddenRows", []) - if isValid(hiddenRows) and isArray(hiddenRows) and hiddenRows.Count() > 0 + if isValid(hiddenRows) and type(hiddenRows) = "roArray" and hiddenRows.Count() > 0 filteredCategories = [] for each category in categories isHidden = false for each hiddenName in hiddenRows - if isStringEqual(toStringValue(hiddenName), category.name) + hNameStr = isValid(hiddenName) ? hiddenName.ToStr() : "" + if isStringEqual(hNameStr, category.name) isHidden = true exit for end if From ea89d7966193914944d3ba8c179cc7a18c5af380 Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 15:02:39 -0700 Subject: [PATCH 11/14] =?UTF-8?q?=EF=BB=BF-=20first=20implementation=20of?= =?UTF-8?q?=20missing=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/home/LoadItemsTask.bs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/home/LoadItemsTask.bs b/components/home/LoadItemsTask.bs index 1957e1028..ec92074b7 100644 --- a/components/home/LoadItemsTask.bs +++ b/components/home/LoadItemsTask.bs @@ -598,7 +598,8 @@ function loadSinceYouWatched() as object rowIndex = 1 if isValid(m.top.metadata) and isValid(m.top.metadata.rowIndex) then rowIndex = m.top.metadata.rowIndex - sourceMode = getStringSetting(settings, "ui.home.sinceYouWatchedSource", "local") + recSource = getStringSetting(settings, "ui.home.recommendationSource", "") + sourceMode = recSource <> "" ? recSource : getStringSetting(settings, "ui.home.sinceYouWatchedSource", "local") sourceItem = getStringSetting(settings, "ui.home.sinceYouWatchedSourceItem", "recentlyWatched") sourceType = getStringSetting(settings, "ui.home.sinceYouWatchedSourceType", "movies") includeWatched = getBoolSetting(settings, "ui.home.sinceYouWatchedIncludeWatched", false) From bc241f213c05a8067f6a7b032d9c412de5013767 Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 15:11:35 -0700 Subject: [PATCH 12/14] =?UTF-8?q?=EF=BB=BF-=20route=20item=20detail=20scre?= =?UTF-8?q?en=20recommendations=20by=20recommendationSource=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/extras/LoadExtrasTask.bs | 142 ++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 6 deletions(-) diff --git a/components/extras/LoadExtrasTask.bs b/components/extras/LoadExtrasTask.bs index bd7c409d8..ed81803f2 100644 --- a/components/extras/LoadExtrasTask.bs +++ b/components/extras/LoadExtrasTask.bs @@ -5,6 +5,7 @@ import "pkg:/source/api/sdk.bs" import "pkg:/source/enums/ItemType.bs" import "pkg:/source/utils/deviceCapabilities.bs" import "pkg:/source/utils/misc.bs" +import "pkg:/source/utils/HomeRecommendations.bs" ' Fast parallel loading task for TV show details ' Loads seasons, next up, people, and similar items simultaneously @@ -543,9 +544,143 @@ function loadPeople() as object end function function loadSimilar() as object + settings = chainLookupReturn(m.global.session, "user.settings", {}) + recSource = chainLookupReturn(settings, "ui.home.recommendationSource", "") + if recSource = "" + recSource = chainLookupReturn(settings, "ui.home.sinceYouWatchedSource", "local") + end if + + if LCase(recSource) = "online" + onlineItems = loadOnlineSimilar() + if isValidAndNotEmpty(onlineItems) then return onlineItems + else if LCase(recSource) = "local" + localItems = loadLocalSimilar() + if isValidAndNotEmpty(localItems) then return localItems + end if + + return loadJellyfinSimilar() +end function + +function loadOnlineSimilar() as object + baseItem = invalid + if isMultiServer() + serverData = getServerData() + url = Substitute("Items/{0}", m.top.itemId) + req = APIRequestForServer(serverData.serverUrl, serverData.userId, serverData.authToken, url, { "userId": serverData.userId, "fields": "ProviderIds" }) + if isValid(req) then baseItem = getJson(req) + else + baseItem = api.items.GetByID(m.top.itemId, { userId: getUserId(), fields: "ProviderIds" }) + end if + + if not isChainValid(baseItem, "ProviderIds.Tmdb") then return [] + tmdbId = baseItem.ProviderIds.Tmdb.ToStr() + if tmdbId = "" then return [] + + mediaType = "movie" + if isChainValid(baseItem, "Type") and baseItem.Type = "Series" then mediaType = "tv" + + config = GetSeerrConfig() + if not isValid(config) or config.enabled <> true then return [] + + req = APIRequest("/Moonfin/Seerr/Api/" + mediaType + "/" + tmdbId + "/recommendations", {}) + if not isValid(req) then return [] + data = getJson(req) + if not isChainValid(data, "results") then return [] + + results = [] + count = 0 + for each res in data.results + if count >= 25 then exit for + tmp = createSGNode("HomeData") + tmp.json = res + tmp.imageWidth = 180 + tmp.labelText = isChainValid(res, "title") ? res.title : res.name + tmp.type = mediaType = "movie" ? "Movie" : "Series" + if isChainValid(res, "posterPath") and isValidAndNotEmpty(res.posterPath) + tmp.posterURL = "https://image.tmdb.org/t/p/w300" + res.posterPath + end if + results.push(tmp) + count = count + 1 + end for + return results +end function + +function loadLocalSimilar() as object + baseItem = invalid + if isMultiServer() + serverData = getServerData() + url = Substitute("Items/{0}", m.top.itemId) + req = APIRequestForServer(serverData.serverUrl, serverData.userId, serverData.authToken, url, { "userId": serverData.userId, "fields": "Genres,Tags,People,Studios,OfficialRating,ProductionYear" }) + if isValid(req) then baseItem = getJson(req) + else + baseItem = api.items.GetByID(m.top.itemId, { userId: getUserId(), fields: "Genres,Tags,People,Studios,OfficialRating,ProductionYear" }) + end if + + if not isValid(baseItem) then return [] + ctx = HomeRecommendations.buildContext(baseItem) + + candidateTypes = "Movie,Series" + candidateFields = "Genres,Tags,People,UserData,OfficialRating,ProductionYear,CommunityRating,Studios,PremiereDate,ImageTags" + + data = apiItemsGet({ + userId: getUserId(), + includeItemTypes: candidateTypes, + genres: ctx.genres.Join("|"), + recursive: true, + limit: 40, + fields: candidateFields, + EnableTotalRecordCount: false + }) + + candidates = {} + if isChainValid(data, "Items") + for each item in data.Items + if isValid(item.Id) and item.Id <> m.top.itemId then candidates[item.Id] = item + end for + end if + + scored = [] + for each key in candidates + cand = candidates[key] + score = HomeRecommendations.scoreCandidate(cand, ctx) + if score > 0 then scored.push({ item: cand, score: score }) + end for + + if scored.count() = 0 then return [] + + results = [] + count = 0 + for each entry in scored + if count >= 25 then exit for + item = entry.item + tmp = createSGNode("HomeData") + if isMultiServer() + item._serverUrl = m.top.serverUrl + item._userId = m.top.serverUserId + item._authToken = m.top.serverAuthToken + end if + tmp.json = item + tmp.imageWidth = 180 + imgParams = { "maxHeight": 270, "maxWidth": 180 } + if hasImageTagPrimary(item) then imgParams.Tag = item.ImageTags.Primary + if isMultiServer() + normalizedServerUrl = m.top.serverUrl + if normalizedServerUrl.right(1) = "/" then normalizedServerUrl = normalizedServerUrl.left(normalizedServerUrl.len() - 1) + tmp.posterURL = normalizedServerUrl + "/Items/" + item.Id + "/Images/Primary?maxWidth=180&maxHeight=270&quality=90" + else + tmp.posterURL = ImageURL(item.Id, "Primary", imgParams) + end if + tmp.labelText = item.Name + tmp.type = item.Type + results.push(tmp) + count = count + 1 + end for + return results +end function + +function loadJellyfinSimilar() as object similar = [] - ' Normalize server URL once if multi-server normalizedServerUrl = "" if isMultiServer() normalizedServerUrl = m.top.serverUrl @@ -567,7 +702,6 @@ function loadSimilar() as object for each item in data.Items tmp = createSGNode("HomeData") - ' Add server metadata to item JSON for multi-server navigation if isMultiServer() item._serverUrl = m.top.serverUrl item._userId = m.top.serverUserId @@ -576,7 +710,6 @@ function loadSimilar() as object tmp.json = item - ' Use standard poster dimensions tmp.imageWidth = 180 imgParams = { "maxHeight": 270, "maxWidth": 180 } @@ -584,7 +717,6 @@ function loadSimilar() as object imgParams.Tag = item.ImageTags.Primary end if - ' Build image URL using correct server for multi-server items if isMultiServer() tmp.posterURL = normalizedServerUrl + "/Items/" + item.Id + "/Images/Primary?maxWidth=180&maxHeight=270&quality=90" if hasImageTagPrimary(item) @@ -594,11 +726,9 @@ function loadSimilar() as object tmp.posterURL = ImageURL(item.Id, "Primary", imgParams) end if - ' Set display fields tmp.labelText = item.Name tmp.type = item.Type - ' Set year as subtitle if isValid(item.ProductionYear) tmp.subTitle = stri(item.ProductionYear) else if isValid(item.PremiereDate) From bf6a07e06992edf14706b59f75b50053a49b4822 Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Wed, 5 Aug 2026 15:35:32 -0700 Subject: [PATCH 13/14] =?UTF-8?q?=EF=BB=BFConverted=20discrete=20options?= =?UTF-8?q?=20to=20sliders=20to=20match=20core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/home/HomeRows.bs | 24 +++++++++++------ settings/settings.json | 52 ++++++++++++++++++------------------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/components/home/HomeRows.bs b/components/home/HomeRows.bs index f697eda44..f353ecf7b 100644 --- a/components/home/HomeRows.bs +++ b/components/home/HomeRows.bs @@ -150,16 +150,24 @@ sub updateSize() modernRows = isModernHomeRows() if modernRows - modPadding = chainLookupReturn(m.global.session, "user.settings.ui.home.modernRowsPadding", "normal") - verticalSpacing = 210 - if modPadding = "compact" then verticalSpacing = 170 - if modPadding = "spacious" then verticalSpacing = 250 + modPadding = chainLookupReturn(m.global.session, "user.settings.ui.home.modernRowsPadding", 460) + modStr = isValid(modPadding) ? modPadding.ToStr() : "" + verticalSpacing = Val(modStr) + if verticalSpacing <= 0 + if isStringEqual(modStr, "compact") then verticalSpacing = 360 + if isStringEqual(modStr, "spacious") then verticalSpacing = 560 + if verticalSpacing <= 0 then verticalSpacing = 460 + end if m.top.itemSpacing = [0, verticalSpacing] else - classPadding = chainLookupReturn(m.global.session, "user.settings.ui.home.classicRowsPadding", "normal") - verticalSpacing = 60 - if classPadding = "compact" then verticalSpacing = 40 - if classPadding = "spacious" then verticalSpacing = 90 + classPadding = chainLookupReturn(m.global.session, "user.settings.ui.home.classicRowsPadding", 30) + classStr = isValid(classPadding) ? classPadding.ToStr() : "" + verticalSpacing = Val(classStr) + if verticalSpacing <= 0 + if isStringEqual(classStr, "compact") then verticalSpacing = 10 + if isStringEqual(classStr, "spacious") then verticalSpacing = 90 + if verticalSpacing <= 0 then verticalSpacing = 30 + end if m.top.itemSpacing = [0, verticalSpacing] end if diff --git a/settings/settings.json b/settings/settings.json index db0f3a5f6..42c8eb836 100644 --- a/settings/settings.json +++ b/settings/settings.json @@ -314,20 +314,21 @@ "description": "Adjust vertical padding between home screen rows in classic layout", "settingName": "ui.home.classicRowsPadding", "type": "radio", - "default": "normal", + "default": "30", "options": [ - { - "title": "Compact", - "id": "compact" - }, - { - "title": "Normal", - "id": "normal" - }, - { - "title": "Spacious", - "id": "spacious" - } + { "title": "10 px", "id": "10" }, + { "title": "20 px", "id": "20" }, + { "title": "30 px (Default)", "id": "30" }, + { "title": "40 px", "id": "40" }, + { "title": "50 px", "id": "50" }, + { "title": "60 px", "id": "60" }, + { "title": "70 px", "id": "70" }, + { "title": "80 px", "id": "80" }, + { "title": "90 px", "id": "90" }, + { "title": "100 px", "id": "100" }, + { "title": "110 px", "id": "110" }, + { "title": "120 px", "id": "120" }, + { "title": "130 px", "id": "130" } ] }, { @@ -335,20 +336,19 @@ "description": "Adjust vertical padding between home screen rows in modern layout", "settingName": "ui.home.modernRowsPadding", "type": "radio", - "default": "normal", + "default": "460", "options": [ - { - "title": "Compact", - "id": "compact" - }, - { - "title": "Normal", - "id": "normal" - }, - { - "title": "Spacious", - "id": "spacious" - } + { "title": "360 px", "id": "360" }, + { "title": "380 px", "id": "380" }, + { "title": "400 px", "id": "400" }, + { "title": "420 px", "id": "420" }, + { "title": "440 px", "id": "440" }, + { "title": "460 px (Default)", "id": "460" }, + { "title": "480 px", "id": "480" }, + { "title": "500 px", "id": "500" }, + { "title": "520 px", "id": "520" }, + { "title": "540 px", "id": "540" }, + { "title": "560 px", "id": "560" } ] }, { From ef7943639d8939c53fc5c2d96d2e245415dcb5e9 Mon Sep 17 00:00:00 2001 From: RadicalMuffinMan <103554043+RadicalMuffinMan@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:58:38 -0400 Subject: [PATCH 14/14] Fixed inert setting reads and correct sync mappings --- components/ItemGrid/LoadVideoContentTask.bs | 10 +++-- components/details/ModernItemDetails.bs | 21 ++++++----- components/extras/LoadExtrasTask.bs | 12 ++++-- components/home/HomeRows.bs | 42 +++++++++++---------- components/music/AudioPlayerView.bs | 2 +- components/seerr/SeerrDiscoveryScreen.bs | 17 --------- components/video/OSD.bs | 6 +-- source/utils/settingsSync.bs | 9 +---- 8 files changed, 55 insertions(+), 64 deletions(-) diff --git a/components/ItemGrid/LoadVideoContentTask.bs b/components/ItemGrid/LoadVideoContentTask.bs index 068f713db..9efe8d14c 100644 --- a/components/ItemGrid/LoadVideoContentTask.bs +++ b/components/ItemGrid/LoadVideoContentTask.bs @@ -693,6 +693,7 @@ function sortSubtitles(MediaStreams) tracks = { "forced": [], "default": [], "normal": [], "text": [] } 'Too many args for using substitute prefered_lang = getPreferredLanguage("playback.subs.language") ?? "" + preferSdh = toBoolean(m.global.session.user.settings["playback.preferSdhSubtitles"] ?? false) for each stream in MediaStreams if stream.type = "Subtitle" @@ -701,6 +702,9 @@ function sortSubtitles(MediaStreams) url = buildURL(stream.DeliveryUrl) end if + ' stream is rebuilt below without these fields, so read them while they are still here + isSdhTrack = toBoolean(chainLookupReturn(stream, "IsHearingImpaired", false)) or instr(1, LCase(chainLookupReturn(stream, "displaytitle", "")), "sdh") > 0 + stream = { "Track": { "Language": stream.language, "Description": stream.displaytitle, "TrackName": url }, "IsTextSubtitleStream": stream.IsTextSubtitleStream, @@ -719,10 +723,8 @@ function sortSubtitles(MediaStreams) trackType = "normal" end if - preferSdh = toBoolean(m.global.session.user.settings["playback.preferSdhSubtitles"] ?? false) - isSdhTrack = toBoolean(chainLookupReturn(stream, "IsHearingImpaired", false)) or instr(1, LCase(chainLookupReturn(stream, "displaytitle", "")), "sdh") > 0 - - if (prefered_lang <> "" and prefered_lang = stream.Track.Language) or (preferSdh and isSdhTrack) + matchesLanguage = prefered_lang = "" or prefered_lang = stream.Track.Language + if matchesLanguage and (prefered_lang <> "" or (preferSdh and isSdhTrack)) tracks[trackType].unshift(stream) if stream.IsTextSubtitleStream diff --git a/components/details/ModernItemDetails.bs b/components/details/ModernItemDetails.bs index 0f9ac3b7f..d17abdf02 100644 --- a/components/details/ModernItemDetails.bs +++ b/components/details/ModernItemDetails.bs @@ -1200,7 +1200,7 @@ sub updateCodecPills() end for ' Hide codecRow entirely when no codecs or when technical details are disabled - showTechDetails = toBoolean(chainLookupReturn(m.global.session, "user.settings.ui.library.showTechnicalDetails", true)) + showTechDetails = toBoolean(m.global.session.user.settings["ui.library.showTechnicalDetails"] ?? true) codecRow = m.top.findNode("codecRow") if isValid(codecRow) then codecRow.visible = anyCodec and showTechDetails end sub @@ -2208,14 +2208,17 @@ sub addMusicButtons() end sub sub addButton(config as object) - hiddenButtons = chainLookupReturn(m.global.session, "user.settings.ui.itemdetail.hiddenButtons", []) - if isValid(hiddenButtons) and type(hiddenButtons) = "roArray" - for each hiddenId in hiddenButtons - hStr = isValid(hiddenId) ? hiddenId.ToStr() : "" - if isStringEqual(hStr, config.action) or isStringEqual(hStr, config.id) - return - end if - end for + ' Stored as a JSON string, so skip parsing on the common empty case + hiddenRaw = m.global.session.user.settings["ui.itemdetail.hiddenButtons"] ?? "[]" + if hiddenRaw <> "" and hiddenRaw <> "[]" + hiddenButtons = ParseJson(hiddenRaw) + if isValid(hiddenButtons) + for each hiddenId in hiddenButtons + if isStringEqual(hiddenId.ToStr(), config.action) or isStringEqual(hiddenId.ToStr(), config.id) + return + end if + end for + end if end if buttonGroup = detailButtons.createModernButton(config, m.buttonGroups.count()) diff --git a/components/extras/LoadExtrasTask.bs b/components/extras/LoadExtrasTask.bs index ed81803f2..f2a3b6742 100644 --- a/components/extras/LoadExtrasTask.bs +++ b/components/extras/LoadExtrasTask.bs @@ -544,10 +544,10 @@ function loadPeople() as object end function function loadSimilar() as object - settings = chainLookupReturn(m.global.session, "user.settings", {}) - recSource = chainLookupReturn(settings, "ui.home.recommendationSource", "") + settings = m.global.session.user.settings + recSource = settings["ui.home.recommendationSource"] ?? "" if recSource = "" - recSource = chainLookupReturn(settings, "ui.home.sinceYouWatchedSource", "local") + recSource = settings["ui.home.sinceYouWatchedSource"] ?? "local" end if if LCase(recSource) = "online" @@ -681,6 +681,7 @@ end function function loadJellyfinSimilar() as object similar = [] + ' Normalize server URL once if multi-server normalizedServerUrl = "" if isMultiServer() normalizedServerUrl = m.top.serverUrl @@ -702,6 +703,7 @@ function loadJellyfinSimilar() as object for each item in data.Items tmp = createSGNode("HomeData") + ' Add server metadata to item JSON for multi-server navigation if isMultiServer() item._serverUrl = m.top.serverUrl item._userId = m.top.serverUserId @@ -710,6 +712,7 @@ function loadJellyfinSimilar() as object tmp.json = item + ' Use standard poster dimensions tmp.imageWidth = 180 imgParams = { "maxHeight": 270, "maxWidth": 180 } @@ -717,6 +720,7 @@ function loadJellyfinSimilar() as object imgParams.Tag = item.ImageTags.Primary end if + ' Build image URL using correct server for multi-server items if isMultiServer() tmp.posterURL = normalizedServerUrl + "/Items/" + item.Id + "/Images/Primary?maxWidth=180&maxHeight=270&quality=90" if hasImageTagPrimary(item) @@ -726,9 +730,11 @@ function loadJellyfinSimilar() as object tmp.posterURL = ImageURL(item.Id, "Primary", imgParams) end if + ' Set display fields tmp.labelText = item.Name tmp.type = item.Type + ' Set year as subtitle if isValid(item.ProductionYear) tmp.subTitle = stri(item.ProductionYear) else if isValid(item.PremiereDate) diff --git a/components/home/HomeRows.bs b/components/home/HomeRows.bs index f353ecf7b..f93c383ef 100644 --- a/components/home/HomeRows.bs +++ b/components/home/HomeRows.bs @@ -148,29 +148,31 @@ sub updateSize() 'Set width of Rows to cut off at edge of Safe Zone m.top.itemSize = [1703, itemHeight] - modernRows = isModernHomeRows() - if modernRows - modPadding = chainLookupReturn(m.global.session, "user.settings.ui.home.modernRowsPadding", 460) - modStr = isValid(modPadding) ? modPadding.ToStr() : "" - verticalSpacing = Val(modStr) - if verticalSpacing <= 0 - if isStringEqual(modStr, "compact") then verticalSpacing = 360 - if isStringEqual(modStr, "spacious") then verticalSpacing = 560 - if verticalSpacing <= 0 then verticalSpacing = 460 - end if - m.top.itemSpacing = [0, verticalSpacing] + ' spacing between rows (modern reserves space for the focused card info block) + if isModernHomeRows() + paddingKey = "ui.home.modernRowsPadding" + compactSpacing = 360 + spaciousSpacing = 560 + defaultSpacing = 460 else - classPadding = chainLookupReturn(m.global.session, "user.settings.ui.home.classicRowsPadding", 30) - classStr = isValid(classPadding) ? classPadding.ToStr() : "" - verticalSpacing = Val(classStr) - if verticalSpacing <= 0 - if isStringEqual(classStr, "compact") then verticalSpacing = 10 - if isStringEqual(classStr, "spacious") then verticalSpacing = 90 - if verticalSpacing <= 0 then verticalSpacing = 30 - end if - m.top.itemSpacing = [0, verticalSpacing] + paddingKey = "ui.home.classicRowsPadding" + compactSpacing = 10 + spaciousSpacing = 90 + defaultSpacing = 30 end if + ' Padding arrives as either a pixel value or a named preset + padding = m.global.session.user.settings[paddingKey] + paddingStr = isValid(padding) ? padding.ToStr() : "" + verticalSpacing = Val(paddingStr) + if verticalSpacing <= 0 + if isStringEqual(paddingStr, "compact") then verticalSpacing = compactSpacing + if isStringEqual(paddingStr, "spacious") then verticalSpacing = spaciousSpacing + if verticalSpacing <= 0 then verticalSpacing = defaultSpacing + end if + + m.top.itemSpacing = [0, verticalSpacing] + ' spacing between items in a row. Modern keeps cards tight, the focused card ' makes its own room by sliding the following cells aside (reflow). m.top.rowItemSpacing = [20, 0] diff --git a/components/music/AudioPlayerView.bs b/components/music/AudioPlayerView.bs index 2765257f8..b06f85721 100644 --- a/components/music/AudioPlayerView.bs +++ b/components/music/AudioPlayerView.bs @@ -521,7 +521,7 @@ sub onAudioDataChanged() m.songDuration = currentItem.RunTimeTicks / 10000000.0 ' Update displayed total audio length - musicDisplay = chainLookupReturn(m.global.session, "user.settings.playback.musicTimeDisplay", "elapsed") + musicDisplay = m.global.session.user.settings["playback.musicTimeDisplay"] ?? "elapsed" if musicDisplay = "remaining" m.totalLengthTimestamp.text = "-" + ticksToHuman(currentItem.RunTimeTicks) else diff --git a/components/seerr/SeerrDiscoveryScreen.bs b/components/seerr/SeerrDiscoveryScreen.bs index 3d2f74dbe..322aa5352 100644 --- a/components/seerr/SeerrDiscoveryScreen.bs +++ b/components/seerr/SeerrDiscoveryScreen.bs @@ -163,23 +163,6 @@ sub LoadDiscoveryCategories() } ] - hiddenRows = chainLookupReturn(m.global.session, "user.settings.seerr.hiddenRows", []) - if isValid(hiddenRows) and type(hiddenRows) = "roArray" and hiddenRows.Count() > 0 - filteredCategories = [] - for each category in categories - isHidden = false - for each hiddenName in hiddenRows - hNameStr = isValid(hiddenName) ? hiddenName.ToStr() : "" - if isStringEqual(hNameStr, category.name) - isHidden = true - exit for - end if - end for - if not isHidden then filteredCategories.push(category) - end for - categories = filteredCategories - end if - m.categoryData = categories contentNode = CreateObject("roSGNode", "ContentNode") diff --git a/components/video/OSD.bs b/components/video/OSD.bs index 9aab9c830..7e61d0f58 100644 --- a/components/video/OSD.bs +++ b/components/video/OSD.bs @@ -83,10 +83,8 @@ sub init() positionSecondaryControls() end sub -' onProgressPercentageChanged: Handler for changes to m.top.progressPercentage param -' function formatOSDTimeSlot(slotKey as string) as string - slotMode = chainLookupReturn(m.global.session, "user.settings." + slotKey, "none") + slotMode = m.global.session.user.settings[slotKey] ?? "none" if slotMode = "none" or slotMode = "" then return "" if slotMode = "elapsed" then return secondsToHuman(m.top.positionTime, true) if slotMode = "remaining" then return "-" + secondsToHuman(m.top.remainingPositionTime, true) @@ -95,6 +93,8 @@ function formatOSDTimeSlot(slotKey as string) as string return "" end function +' onProgressPercentageChanged: Handler for changes to m.top.progressPercentage param +' sub onProgressPercentageChanged() totalTime = m.top.positionTime + m.top.remainingPositionTime m.progressBar.width = m.progressBarBackground.width * m.top.progressPercentage diff --git a/source/utils/settingsSync.bs b/source/utils/settingsSync.bs index 24111ff11..85ca0057f 100644 --- a/source/utils/settingsSync.bs +++ b/source/utils/settingsSync.bs @@ -56,8 +56,6 @@ namespace settingsSync { pluginKey: "unpauseRewindDuration", rokuKey: "playback.unpauseRewind", type: "intToStr" }, { pluginKey: "skipBackLength", rokuKey: "playback.skipBackLength", type: "intToStr" }, { pluginKey: "skipForwardLength", rokuKey: "playback.skipForwardLength", type: "intToStr" }, - { pluginKey: "introAction", rokuKey: "playback.mediasegments.intro", type: "direct" }, - { pluginKey: "outroAction", rokuKey: "playback.mediasegments.outro", type: "direct" }, { pluginKey: "preferAudioDescription", rokuKey: "playback.preferAudioDescription", type: "bool" }, { pluginKey: "preferSdhSubtitles", rokuKey: "playback.preferSdhSubtitles", type: "bool" }, { pluginKey: "seerrBlockNsfw", rokuKey: "seerr.blockNsfw", type: "bool" }, @@ -126,7 +124,6 @@ namespace settingsSync { pluginKey: "collectionsRowShowEpisodes", rokuKey: "ui.home.collectionsRowShowEpisodes", type: "bool" }, { pluginKey: "playlistsRowShowEpisodes", rokuKey: "ui.home.playlistsRowShowEpisodes", type: "bool" }, - { pluginKey: "mergeRecentRowsByType", rokuKey: "ui.home.mergeRecentRowsByType", type: "bool" }, { pluginKey: "showCastButton", rokuKey: "navbar.show_cast", type: "bool" }, { pluginKey: "classicHomeRowsPadding", rokuKey: "ui.home.classicRowsPadding", type: "direct" }, { pluginKey: "modernHomeRowsPadding", rokuKey: "ui.home.modernRowsPadding", type: "direct" }, @@ -134,10 +131,8 @@ namespace settingsSync { pluginKey: "recommendationSystemSource", rokuKey: "ui.home.recommendationSource", type: "direct" }, { pluginKey: "recommendationsApplyParentalRatingCap", rokuKey: "ui.home.recommendationsParentalCap", type: "bool" }, { pluginKey: "screensaverMode", rokuKey: "screensaver.mode", type: "direct" }, - { pluginKey: "seerrRowOrder", rokuKey: "seerr.rowOrder", type: "jsonArray" }, - { pluginKey: "hiddenSeerrRows", rokuKey: "seerr.hiddenRows", type: "jsonArray" }, - { pluginKey: "detailButtonOrder", rokuKey: "ui.itemdetail.buttonOrder", type: "jsonArray" }, - { pluginKey: "hiddenDetailButtons", rokuKey: "ui.itemdetail.hiddenButtons", type: "jsonArray" }, + { pluginKey: "detailButtonOrderTv", rokuKey: "ui.itemdetail.buttonOrder", type: "jsonArray" }, + { pluginKey: "hiddenDetailButtonsTv", rokuKey: "ui.itemdetail.hiddenButtons", type: "jsonArray" }, { pluginKey: "subtitleMode", rokuKey: "playback.subs.mode", type: "subtitleModeFormat" }, { pluginKey: "defaultSubtitleLanguage", rokuKey: "playback.subs.language", type: "direct" }, { pluginKey: "defaultAudioLanguage", rokuKey: "playback.audioPreferredLanguage", type: "direct" },