Add a setting
How to add a setting to JellyRock’s Settings screen: the entry in settings/settings.json, the field that holds it, its strings, and the code that reads it. To rename or remove a setting users already have, see registry-migrations.md. The /new-setting skill walks an agent through the same steps.
Three kinds of setting
Section titled “Three kinds of setting”| Kind | Where it lives | Applies to | Named |
|---|---|---|---|
| User setting | The user’s registry section | One user | By area: playback*, ui*, itemGrid* |
| Global setting | The JellyRock registry section | Everyone on the device | global* |
| User configuration | The Jellyfin server | The user, on every client | Never stored by JellyRock |
The prefix decides where a value is saved. When a setting changes, onSettingChanged() in JellyfinUserSettings.bs checks isGlobalSetting() (source/utils/config.bs: the name starts with global). A global setting goes to the global section and every other setting to the user’s. Keep global settings for behavior that belongs to the device, such as Remember Me? (globalRememberMe).
Add a setting when a user needs a different value on the Roku than on their other clients, or for a feature only JellyRock has. Don’t add one for data the server owns, for state that lasts one session, or for something the app can detect on the device.
How a setting loads
Section titled “How a setting loads”- At start-up,
user.settings.SaveDefaults()fills every field from thedefaultvalues insettings/settings.json, andenableAutoSyncturns on saving. - At sign-in,
user.Login()(source/utils/session.bs) runsSaveDefaults()again, then loads each key in the user’s registry section into the field with the same name.user.settings.Save()converts the stored string to the field’s type. Global settings load throughuser.settings.LoadGlobals(). - Code reads the value from
m.global.user.settings.
Only a value the user changed is written to the registry. A default stays in settings.json, so changing a default reaches every user who never changed the setting.
1. Add the entry to settings.json
Section titled “1. Add the entry to settings.json”Put the entry in the right category’s children, in order of title. This is playbackPlayDefaultAudioTrack:
{ "title": "Play Default Audio Track", "description": "Override web client audio preference. When enabled, use the IsDefault flag to select audio track. When disabled, prefer language match and ignore IsDefault.", "settingName": "playbackPlayDefaultAudioTrack", "type": "radio", "default": "webclient", "options": [ { "title": "Use Web Client Setting", "id": "webclient", "titleKey": "LabelUseWebClientSetting" }, { "title": "Enabled", "id": "enabled", "titleKey": "LabelEnabled" }, { "title": "Disabled", "id": "disabled", "titleKey": "LabelDisabled" } ], "titleKey": "LabelPlayDefaultAudioTrack", "descriptionKey": "MessageOverrideWebClientAudioPreferenceWhen"}settingNameis the field and registry key:lowerCamelCase, with its area’s prefix, and specific (playbackPlayDefaultAudioTrack, notplaybackAudio).titleKeyanddescriptionKeyname the strings the Settings screen shows, translated; so does each option’stitleKey.titleanddescriptionare the English text thatdocs/user/app-settings.mdis generated from.defaultis the value a user has until they change it.
The types the Settings screen handles:
type | The user | Field type |
|---|---|---|
bool | Turns it on or off | boolean |
radio | Picks one of options | string |
integer | Enters a number; with min and max, a value outside them asks for confirmation | integer |
text | Types a value, such as a hex color | string |
alpha | Types letters on the on-screen keyboard | string |
languagePicker | Picks a language | string |
Two optional keys:
visibleWhenshows an entry only while another setting has a value, with no code. The Custom Theme Colors group uses"visibleWhen": { "settingName": "uiTheme", "value": "custom" }.LoadMenu()incomponents/settings/settings.bsfilters the menu withisSettingVisible(), and the menu redraws when the controlling setting changes.presetValues, on a radio option, holds values the app reads while that option is chosen. EachuiThemeoption lists the theme’s colors, andapplyThemeColorOverrides()reads them from there; picking a theme saves onlyuiThemeand never writes theuiThemeColor*settings, so the Custom colors stay as the user set them.
An override of a web client setting uses three options: webclient (the default: use the server’s value), enabled and disabled.
2. Add the strings
Section titled “2. Add the strings”Add each new titleKey and descriptionKey to locale/custom/en_US.json. The rules for keys and their order are in translations.md, and the /translation-add skill applies them. npm run lint:translations checks the keys settings.json uses.
3. Add the field
Section titled “3. Add the field”Add a field to components/data/jellyfin/JellyfinUserSettings.xml, in its area’s section:
<field id="playbackPlayDefaultAudioTrack" type="string" alwaysNotify="true" />- The
idmatchessettingNameexactly. The value is saved under the field’sidand loaded back by the same key. - The type comes from the table above. A
radiosetting stored in abooleanfield never holds"webclient". - Give it no
value. The default comes fromsettings.json, and a second one here would disagree with it one day.
Nothing else is needed to load or save it: JellyfinUserSettings.bs observes every field, and user.Login() loads every key that names one.
4. Use it
Section titled “4. Use it”Read the node into a local once, then read the setting from it:
localUser = m.global.userplayDefault = resolvePlayDefaultAudioTrack(localUser.settings, localUser.config)A plain setting needs no more than localUser.settings.<settingName>. When a setting overrides a server value, write one function that decides between them, and call it everywhere. This is resolvePlayDefaultAudioTrack() in source/utils/streamSelection.bs:
function resolvePlayDefaultAudioTrack(userSettings as object, userConfig as object) as boolean ' Default to true if we can't determine the value defaultValue = true
' Try to get web client setting if isValid(userConfig) and isValid(userConfig.playDefaultAudioTrack) ' Ensure it's actually a boolean before using it valueType = Type(userConfig.playDefaultAudioTrack) if valueType = "roBoolean" or valueType = "Boolean" defaultValue = userConfig.playDefaultAudioTrack end if end if
' Check for JellyRock override setting if isValid(userSettings) and isValid(userSettings.playbackPlayDefaultAudioTrack) and userSettings.playbackPlayDefaultAudioTrack <> "" if userSettings.playbackPlayDefaultAudioTrack = "enabled" return true else if userSettings.playbackPlayDefaultAudioTrack = "disabled" return false end if end if
return defaultValueend functionIt returns a safe value when either node or field is missing. Search for every place the behavior lives (git grep -n "<related term>" -- source components), so the setting changes all of them.
5. Test it
Section titled “5. Test it”Unit-test a resolving function in tests/source/unit/<area>/. tests/source/unit/userSettings/PlaybackPreserveDovi.spec.bs is an example, and unit-tests.md covers writing one. Cover:
- each option (
enabled,disabled,webclient); - the server’s value when the override is
webclientor empty; invalidsettings or configuration nodes, and an unexpected value.
Run the specs on a Roku: npm run device:check, then npm run test:tdd with your spec listed (unit-tests-tdd.md) or npm run test:unit.
Then on the device:
- Open the setting in Settings and check its title, description and options.
- Change it, close the app, reopen it, and check the value stayed.
- Check the behavior changes for each value, and for each server value if it overrides one.
- Sign in as a user who never changed it, and check the default applies.
Reset User Settings restores every user setting to its default through SaveDefaults(), so a new setting resets with no more code.
After the change merges, the jellyrock-bot workflow regenerates docs/user/app-settings.md from settings.json (npm run docs:settings does the same locally). Never edit that file by hand.