components_data_jellyfin_JellyfinUserSettings.bs
import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/misc.bs"
sub init()
m.log = log.Logger("JellyfinUserSettings")
m.log.info("Initializing JellyfinUserSettings node")
' Track previous state for change detection
m.previousDisplaySettings = {}
end sub
' Enable automatic syncing of settings changes to registry
' Call this AFTER loading initial settings from registry/server
' This starts field observers that write changes back to registry
sub enableAutoSync()
m.log.info("Enabling auto-sync - setting up field observers")
' Capture current displaySettings state BEFORE enabling observers
' This prevents initial observer fire from writing to registry
if isValid(m.top.displaySettings)
m.previousDisplaySettings = deepCopyAA(m.top.displaySettings)
end if
' Set up observers for all settings fields
observeAllSettings()
m.log.info("Auto-sync enabled - settings changes will be saved to registry")
end sub
' Disable automatic syncing (optional, for cleanup or testing)
sub disableAutoSync()
m.log.info("Disabling auto-sync - removing field observers")
' Get all fields to unobserve
allFields = m.top.getFields()
' Fields that were excluded from observing
excludedFields = [
"id",
"isLoaded",
"loadedAt",
"rawSettings"
]
' Remove observers from all observed fields
for each fieldName in allFields
if not inArray(excludedFields, fieldName)
m.top.unobserveField(fieldName)
end if
end for
m.log.info("Auto-sync disabled")
end sub
' Dynamically set up observers for all user setting fields
' This automatically handles new fields added to the XML without code changes
sub observeAllSettings()
' IMPORTANT: displaySettings must be observed explicitly
m.top.observeField("displaySettings", "onDisplaySettingsChanged")
' Get ALL field names defined in the XML interface (not just fields with values)
allFieldNames = m.top.getFields()
' Fields that should NOT be observed (internal/system fields)
excludedFields = [
"id",
"isLoaded",
"loadedAt",
"rawSettings", ' Debug-only field, don't observe
"displaySettings" ' Already observed above with custom handler
]
' Loop through all field names
for each fieldName in allFieldNames
' Skip excluded fields
if inArray(excludedFields, fieldName)
continue for
end if
' All other fields use the standard handler
m.top.observeField(fieldName, "onSettingChanged")
end for
end sub
' Called when any individual setting field changes
' Observer is the single source of truth for registry sync
' IMPORTANT: Only syncs to registry - does NOT modify the node (prevents recursion)
'
' DESIGN: Always save settings to registry, never delete
' This preserves user intent - if user explicitly sets a value to match the default,
' that's different from "never touched this setting". This is important for:
' - Future multi-device sync
' - Understanding what users have customized
' - "Reset all settings" feature can delete entire registry section
sub onSettingChanged(event as object)
fieldName = event.getField()
newValue = event.getData()
' Route to appropriate registry section based on setting type
if isGlobalSetting(fieldName)
' Global settings apply to all users - save to "JellyRock" or "test-global" section
globalSection = getGlobalRegistrySection()
registry_write(fieldName, valueToString(newValue), globalSection)
else
' User-specific settings - save to user's section
' Get local reference to minimize rendezvous
localUser = m.global.user
if not isValid(localUser.id)
m.log.warn("Cannot sync setting to registry - user ID is invalid", { field: fieldName })
return
end if
' Always save to registry - track all user changes
registry_write(fieldName, valueToString(newValue), localUser.id)
end if
end sub
' Called when displaySettings field changes
' Only syncs the specific settings that changed, not the entire object
sub onDisplaySettingsChanged()
newDisplaySettings = m.top.displaySettings
if not isValid(newDisplaySettings)
m.log.warn("onDisplaySettingsChanged called but newDisplaySettings is invalid")
return
end if
' Compare with previous state to find what changed BEFORE updating previous state
syncDisplaySettingsChanges(m.previousDisplaySettings, newDisplaySettings)
' Update previous state for next comparison
m.previousDisplaySettings = deepCopyAA(newDisplaySettings)
end sub
' Sync only the display settings that changed
sub syncDisplaySettingsChanges(oldSettings as object, newSettings as object)
if not isValid(oldSettings)
oldSettings = {}
end if
if not isValid(newSettings)
return
end if
' Check for new or modified libraries
for each libraryId in newSettings
newLibSettings = newSettings[libraryId]
if not oldSettings.DoesExist(libraryId)
' New library - sync all its settings
syncLibrarySettings(libraryId, newLibSettings)
else
' Existing library - check for changed settings
oldLibSettings = oldSettings[libraryId]
syncChangedLibrarySettings(libraryId, oldLibSettings, newLibSettings)
end if
end for
' Check for deleted libraries
for each libraryId in oldSettings
if not newSettings.DoesExist(libraryId)
' Library was deleted - remove from registry
deleteLibraryFromRegistry(libraryId)
end if
end for
end sub
' Sync all settings for a library (new library case)
sub syncLibrarySettings(libraryId as string, settings as object)
if not isValid(settings)
return
end if
' Get local reference to minimize rendezvous
localUser = m.global.user
if not isValid(localUser.id)
m.log.warn("Cannot write to registry - user ID is invalid")
return
end if
m.log.info("Syncing all settings for new library", { libraryId: libraryId, settingsCount: settings.Count() })
' Display settings are nested structures stored in the displaySettings field
' We write directly to registry using registry_write() instead of set_user_setting()
' because set_user_setting() tries to create node fields with dots in the name, which is invalid
for each key in settings
registryKey = "display." + libraryId + "." + key
m.log.debug("Writing to registry", { key: registryKey, value: settings[key] })
' Write directly to registry - data is already in m.top.displaySettings (source of truth)
registry_write(registryKey, valueToString(settings[key]), localUser.id)
end for
end sub
' Sync only changed settings for a library
sub syncChangedLibrarySettings(libraryId as string, oldSettings as object, newSettings as object)
if not isValid(newSettings)
return
end if
if not isValid(oldSettings)
oldSettings = {}
end if
' Get local reference to minimize rendezvous
localUser = m.global.user
if not isValid(localUser.id)
m.log.warn("Cannot sync display settings - user ID is invalid")
return
end if
changedCount = 0
' Check for new or modified settings
for each key in newSettings
newValue = newSettings[key]
' Sync if: key is new OR value changed
' Convert to strings for comparison (registry stores strings)
' This handles type mismatches when displaySettings contains mixed types
oldValueStr = valueToString(oldSettings[key])
newValueStr = valueToString(newValue)
if not oldSettings.DoesExist(key) or oldValueStr <> newValueStr
registryKey = "display." + libraryId + "." + key
m.log.debug("Writing changed setting to registry", { key: registryKey, oldValue: oldValueStr, newValue: newValueStr })
' Write directly to registry - data is already in m.top.displaySettings (source of truth)
registry_write(registryKey, valueToString(newValue), localUser.id)
changedCount++
end if
end for
' Check for deleted settings
for each key in oldSettings
if not newSettings.DoesExist(key)
registryKey = "display." + libraryId + "." + key
m.log.debug("Deleting setting from registry", { key: registryKey })
' Delete directly from registry - data was already removed from m.top.displaySettings
registry_delete(registryKey, localUser.id)
changedCount++
end if
end for
if changedCount > 0
m.log.info("Synced changed settings for library", { libraryId: libraryId, changedCount: changedCount })
end if
end sub
' Delete all settings for a library from registry
sub deleteLibraryFromRegistry(libraryId as string)
' Get local reference to minimize rendezvous
localUser = m.global.user
if not isValid(localUser.id)
m.log.warn("Cannot delete library from registry - user ID is invalid")
return
end if
' Known display setting keys
possibleKeys = ["sortField", "filter", "landing", "sortAscending", "filterOptions", "view"]
for each key in possibleKeys
registryKey = "display." + libraryId + "." + key
' Delete directly from registry - data was already removed from m.top.displaySettings
registry_delete(registryKey, localUser.id)
end for
end sub
' Deep copy an associative array (for state tracking)
function deepCopyAA(source as object) as object
if not isValid(source)
return {}
end if
result = {}
for each key in source
value = source[key]
' If value is an AA, recursively copy it
if type(value) = "roAssociativeArray"
result[key] = deepCopyAA(value)
else
result[key] = value
end if
end for
return result
end function