From c92ebe373e84bbe640b0d505124eba09cfba5fa6 Mon Sep 17 00:00:00 2001 From: Matthew Sigal Date: Tue, 4 Aug 2026 21:56:36 -0700 Subject: [PATCH 1/2] Add server profile sync parity mappings for PR 217/218 fields and prevent customHomeRows drop --- packages/app/src/context/SettingsContext.js | 17 +++++++++++++---- packages/app/src/context/defaultSettings.js | 9 +++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/app/src/context/SettingsContext.js b/packages/app/src/context/SettingsContext.js index f95d07e..7b91441 100644 --- a/packages/app/src/context/SettingsContext.js +++ b/packages/app/src/context/SettingsContext.js @@ -49,7 +49,11 @@ const SERVER_TO_LOCAL = { enableFolderView: 'folderViewMode', homeRowInfoOverlay: 'homeRowOverlay', autoplayNextEpisode: 'autoPlay', - mediaSegmentCountdown: 'nextUpCountdownStyle' + mediaSegmentCountdown: 'nextUpCountdownStyle', + detailButtonOrder: 'detailButtonOrderTv', + hiddenDetailButtons: 'hiddenDetailButtonsTv', + osdButtonOrderDesktop: 'osdButtonOrderTv', + hiddenOsdButtonsDesktop: 'hiddenOsdButtonsTv' }; const LOCAL_TO_SERVER = Object.fromEntries( Object.entries(SERVER_TO_LOCAL).map(([s, l]) => [l, s]) @@ -152,7 +156,7 @@ const SYNCABLE_KEYS = [ 'visualTheme', 'customThemeId', 'showRatingLabels', 'showRatingBadges', - 'themeMusicEnabled', 'themeMusicVolume', 'themeMusicOnHomeRows', + 'themeMusicEnabled', 'themeMusicVolume', 'themeMusicOnHomeRows', 'themeMusicLoop', 'homeRowsImageType', 'showClock', 'clockDisplay', 'homeRowOverlay', 'folderViewMode', 'excludedGenres', @@ -181,7 +185,12 @@ const SYNCABLE_KEYS = [ 'mergeRadarrSonarrCalendars', 'radarrCalendarShowCinema', 'radarrCalendarShowDigital', 'radarrCalendarShowPhysical', 'radarrCalendarShowDate', 'sonarrCalendarShowDate', 'sonarrCalendarShowEpisodeInfo', - 'showSeerrButton', + 'showSeerrButton', 'showCastButton', + 'classicHomeRowsPadding', 'modernHomeRowsPadding', + 'detailShowTechnicalDetails', + 'recommendationSystemSource', 'recommendationsApplyParentalRatingCap', + 'screensaverMode', + 'seerrRowOrder', 'hiddenSeerrRows', 'detailButtonOrderTv', 'hiddenDetailButtonsTv', 'osdButtonOrderTv', 'hiddenOsdButtonsTv', 'focusBorderColor', 'navbarOpacity', @@ -214,7 +223,7 @@ const profileToLocal = (serverProfile) => { const localToProfile = (localSettings) => { const profile = {}; for (const key of SYNCABLE_KEYS) { - if (key === 'homeRows') continue; + if (key === 'homeRows' || key === 'customHomeRows') continue; const value = localSettings[key]; if (value === undefined || value === null) continue; const serverKey = LOCAL_TO_SERVER[key] || key; diff --git a/packages/app/src/context/defaultSettings.js b/packages/app/src/context/defaultSettings.js index 4ed312a..9960bb5 100644 --- a/packages/app/src/context/defaultSettings.js +++ b/packages/app/src/context/defaultSettings.js @@ -188,6 +188,15 @@ export const defaultSettings = { truehdPassthrough: true, blockedRatings: [], showSeerrButton: true, + themeMusicLoop: true, + showCastButton: true, + classicHomeRowsPadding: 'default', + modernHomeRowsPadding: 'default', + detailShowTechnicalDetails: false, + recommendationSystemSource: 'mixed', + recommendationsApplyParentalRatingCap: true, + seerrRowOrder: null, + hiddenSeerrRows: null, performanceMode: 'auto', focusBorderColor: '', navbarOpacity: 100, From 5db8de4e4c852eb8b4b299a619e9305b32965353 Mon Sep 17 00:00:00 2001 From: RadicalMuffinMan <103554043+RadicalMuffinMan@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:35:35 -0400 Subject: [PATCH 2/2] Correct the server field mapping for the synced profile --- packages/app/src/context/SettingsContext.js | 30 +++---- packages/app/src/context/defaultSettings.js | 18 ++-- packages/app/src/context/settingsSync.test.js | 84 +++++++++++++++++++ 3 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 packages/app/src/context/settingsSync.test.js diff --git a/packages/app/src/context/SettingsContext.js b/packages/app/src/context/SettingsContext.js index 7b91441..df609e4 100644 --- a/packages/app/src/context/SettingsContext.js +++ b/packages/app/src/context/SettingsContext.js @@ -49,11 +49,7 @@ const SERVER_TO_LOCAL = { enableFolderView: 'folderViewMode', homeRowInfoOverlay: 'homeRowOverlay', autoplayNextEpisode: 'autoPlay', - mediaSegmentCountdown: 'nextUpCountdownStyle', - detailButtonOrder: 'detailButtonOrderTv', - hiddenDetailButtons: 'hiddenDetailButtonsTv', - osdButtonOrderDesktop: 'osdButtonOrderTv', - hiddenOsdButtonsDesktop: 'hiddenOsdButtonsTv' + mediaSegmentCountdown: 'nextUpCountdownStyle' }; const LOCAL_TO_SERVER = Object.fromEntries( Object.entries(SERVER_TO_LOCAL).map(([s, l]) => [l, s]) @@ -117,6 +113,11 @@ const VALUE_CONVERSIONS = { toServer: v => v ? 'always' : 'never', fromServer: v => v !== 'never' }, + // The other clients offer off, logo or library, where this app keeps a separate toggle for + // turning it off. There is nothing to draw for "off", so leave the mode we already have. + screensaverMode: { + fromServer: v => (v === 'off' ? undefined : v) + }, // Three states here against a boolean elsewhere. "Per Library" has no equivalent, so it // declines to push and leaves whatever the server holds. folderViewMode: { @@ -141,7 +142,7 @@ const VALUE_CONVERSIONS = { // move together, so it gets resolved whole rather than a key at a time. }; -const SYNCABLE_KEYS = [ +export const SYNCABLE_KEYS = [ 'showShuffleButton', 'shuffleContentType', 'showGenresButton', 'showFavoritesButton', 'showLibrariesInToolbar', 'mergeContinueWatchingNextUp', 'nextUpMaxDays', @@ -156,7 +157,7 @@ const SYNCABLE_KEYS = [ 'visualTheme', 'customThemeId', 'showRatingLabels', 'showRatingBadges', - 'themeMusicEnabled', 'themeMusicVolume', 'themeMusicOnHomeRows', 'themeMusicLoop', + 'themeMusicEnabled', 'themeMusicVolume', 'themeMusicOnHomeRows', 'homeRowsImageType', 'showClock', 'clockDisplay', 'homeRowOverlay', 'folderViewMode', 'excludedGenres', @@ -181,23 +182,24 @@ const SYNCABLE_KEYS = [ 'diagnosticLoggingEnabled', 'uiLanguage', 'blockedRatings', - 'customHomeRows', 'mergeRadarrSonarrCalendars', 'radarrCalendarShowCinema', 'radarrCalendarShowDigital', 'radarrCalendarShowPhysical', 'radarrCalendarShowDate', 'sonarrCalendarShowDate', 'sonarrCalendarShowEpisodeInfo', - 'showSeerrButton', 'showCastButton', + 'showSeerrButton', + 'screensaverMode', + // Settings this app has no screen for. They ride along so a value set on another client + // survives the profile the TV writes back. + 'showCastButton', 'themeMusicLoop', 'classicHomeRowsPadding', 'modernHomeRowsPadding', 'detailShowTechnicalDetails', 'recommendationSystemSource', 'recommendationsApplyParentalRatingCap', - 'screensaverMode', - 'seerrRowOrder', 'hiddenSeerrRows', 'detailButtonOrderTv', 'hiddenDetailButtonsTv', 'osdButtonOrderTv', 'hiddenOsdButtonsTv', 'focusBorderColor', 'navbarOpacity', 'navbarColor', ]; -const profileToLocal = (serverProfile) => { +export const profileToLocal = (serverProfile) => { if (!serverProfile) return {}; const local = {}; for (const [key, value] of Object.entries(serverProfile)) { @@ -220,10 +222,10 @@ const profileToLocal = (serverProfile) => { return local; }; -const localToProfile = (localSettings) => { +export const localToProfile = (localSettings) => { const profile = {}; for (const key of SYNCABLE_KEYS) { - if (key === 'homeRows' || key === 'customHomeRows') continue; + if (key === 'homeRows') continue; const value = localSettings[key]; if (value === undefined || value === null) continue; const serverKey = LOCAL_TO_SERVER[key] || key; diff --git a/packages/app/src/context/defaultSettings.js b/packages/app/src/context/defaultSettings.js index 9960bb5..3239528 100644 --- a/packages/app/src/context/defaultSettings.js +++ b/packages/app/src/context/defaultSettings.js @@ -188,15 +188,15 @@ export const defaultSettings = { truehdPassthrough: true, blockedRatings: [], showSeerrButton: true, - themeMusicLoop: true, - showCastButton: true, - classicHomeRowsPadding: 'default', - modernHomeRowsPadding: 'default', - detailShowTechnicalDetails: false, - recommendationSystemSource: 'mixed', - recommendationsApplyParentalRatingCap: true, - seerrRowOrder: null, - hiddenSeerrRows: null, + // Synced but not shown anywhere in this app, so null keeps the TV from stamping a default + // of its own over what another client set. The padding pair is a pixel count on the server. + themeMusicLoop: null, + showCastButton: null, + classicHomeRowsPadding: null, + modernHomeRowsPadding: null, + detailShowTechnicalDetails: null, + recommendationSystemSource: null, + recommendationsApplyParentalRatingCap: null, performanceMode: 'auto', focusBorderColor: '', navbarOpacity: 100, diff --git a/packages/app/src/context/settingsSync.test.js b/packages/app/src/context/settingsSync.test.js new file mode 100644 index 0000000..119c86d --- /dev/null +++ b/packages/app/src/context/settingsSync.test.js @@ -0,0 +1,84 @@ +// The server profile is typed, so a key this app spells differently isn't refused, it's +// quietly dropped. Nothing on this side can prove a field name is right, but these lock down +// what gets sent and what gets taken. + +// Nothing renders here. The provider only needs React to exist while the module loads, and +// the real one can't be pulled in because the CLI ships a second copy that disagrees with it. +jest.mock('react/jsx-dev-runtime', () => ({})); +jest.mock('react', () => ({createContext: () => ({})})); +// Storage picks its platform module through a dynamic import that jest can't transform. +jest.mock('../services/storage', () => ({})); + +import {SYNCABLE_KEYS, defaultSettings, profileToLocal, localToProfile} from './SettingsContext'; + +describe('profileToLocal', () => { + test('takes the TV button fields under their own names', () => { + const local = profileToLocal({ + detailButtonOrderTv: ['play', 'trailer'], + hiddenDetailButtonsTv: ['shuffle'], + osdButtonOrderTv: ['subtitles'], + hiddenOsdButtonsTv: ['audio'] + }); + + expect(local.detailButtonOrderTv).toEqual(['play', 'trailer']); + expect(local.hiddenDetailButtonsTv).toEqual(['shuffle']); + expect(local.osdButtonOrderTv).toEqual(['subtitles']); + expect(local.hiddenOsdButtonsTv).toEqual(['audio']); + }); + + test('leaves the desktop and mobile button fields alone', () => { + const local = profileToLocal({ + osdButtonOrderDesktop: ['desktop-order'], + hiddenDetailButtonsMobile: ['mobile-hidden'] + }); + + expect(local).toEqual({}); + }); + + test('ignores a screensaver mode this app has no way to draw', () => { + expect(profileToLocal({screensaverMode: 'off'}).screensaverMode).toBeUndefined(); + expect(profileToLocal({screensaverMode: 'logo'}).screensaverMode).toBe('logo'); + }); +}); + +describe('localToProfile', () => { + test('says nothing about settings this app has no screen for', () => { + const profile = localToProfile(defaultSettings); + + for (const key of ['showCastButton', 'themeMusicLoop', 'classicHomeRowsPadding', + 'modernHomeRowsPadding', 'detailShowTechnicalDetails', 'recommendationSystemSource', + 'recommendationsApplyParentalRatingCap']) { + expect(profile).not.toHaveProperty(key); + } + }); + + test('writes those settings back once the server has supplied one', () => { + const profile = localToProfile({...defaultSettings, ...profileToLocal({ + showCastButton: false, + classicHomeRowsPadding: 12 + })}); + + expect(profile.showCastButton).toBe(false); + expect(profile.classicHomeRowsPadding).toBe(12); + }); + + test('keeps the local only home row list out of the profile', () => { + const profile = localToProfile({...defaultSettings, customHomeRows: [{id: 'row'}]}); + + expect(profile).not.toHaveProperty('customHomeRows'); + }); + + // Some synced keys have no default at all, which is how a screen asks for its built in + // order rather than a stored one. + test('invents nothing when there is no local value to send', () => { + expect(localToProfile({})).toEqual({}); + }); +}); + +describe('SYNCABLE_KEYS', () => { + test('no key is listed twice', () => { + const repeated = SYNCABLE_KEYS.filter((key, i) => SYNCABLE_KEYS.indexOf(key) !== i); + + expect(repeated).toEqual([]); + }); +});