diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index fcca2498..f61b00e4 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -204,7 +204,10 @@ export const OtherSettings: React.FC = () => { } /> - + { { + if (typeof value !== "object" || value === null) { + const defaultVal = defaultValues[settingsKey]; + if ( + typeof defaultVal === "object" && + defaultVal !== null && + "key" in defaultVal && + "value" in defaultVal + ) { + // defaultBitrate needs a lookup because its keys are human-readable + // (e.g. "8 Mb/s") that can't be derived from the raw value (e.g. 8000000). + // Other { key, value } settings like maxAutoPlayEpisodeCount work with + // the fallback because their keys are just String(value) (e.g. "5"). + if (settingsKey === "defaultBitrate") { + const match = BITRATES.find( + (b) => b.key === value || b.value === value, + ); + if (match) return match; + } + // maxAutoPlayEpisodeCount: 0 is invalid (breaks autoplay), clamp to -1 + // -1 key must match the translated dropdown label so the UI shows "Disabled" + if ( + settingsKey === "maxAutoPlayEpisodeCount" && + (value === 0 || value === -1) + ) { + return { key: t("home.settings.other.disabled"), value: -1 }; + } + return { key: String(value), value }; + } + } + return value; +}; + export type HomeSectionLatestResolver = { parentId?: string; limit?: number; @@ -362,7 +403,7 @@ export const useSettings = () => { ); const refreshStreamyfinPluginSettings = useCallback( - async (forceOverride = false) => { + async (_forceOverride = false) => { if (!api) { return; } @@ -375,20 +416,17 @@ export const useSettings = () => { ); setPluginSettings(newPluginSettings); - // Apply plugin values to settings + // Apply locked plugin values to settings (unlocked values are handled + // by the settings memo, which respects user customizations) if (newPluginSettings && _settings) { const updates: Partial = {}; for (const [key, setting] of Object.entries(newPluginSettings)) { - if (setting && !setting.locked && setting.value !== undefined) { + if (setting?.locked) { const settingsKey = key as keyof Settings; - // Apply if forceOverride is true, or if user hasn't explicitly set this value - if ( - forceOverride || - _settings[settingsKey] === undefined || - _settings[settingsKey] === "" - ) { - (updates as any)[settingsKey] = setting.value; - } + (updates as any)[settingsKey] = normalizePluginValue( + settingsKey, + setting.value, + ); } } @@ -440,26 +478,31 @@ export const useSettings = () => { // If admin sets locked to false but provides a value, // use user settings first and fallback on admin setting if required. const settings: Settings = useMemo(() => { - const unlockedPluginDefaults: Partial = {}; + const _unlockedPluginDefaults: Partial = {}; const overrideSettings = Object.entries(pluginSettings ?? {}).reduce< Partial >((acc, [key, setting]) => { if (setting) { - const { value, locked } = setting; + let { value } = setting; + const { locked } = setting; const settingsKey = key as keyof Settings; - // Make sure we override default settings with plugin settings when they are not locked. - if ( - !locked && - value !== undefined && - _settings?.[settingsKey] !== value - ) { - (unlockedPluginDefaults as any)[settingsKey] = value; - } + // Normalize object-typed settings from plugin (plain primitive → { key, value }) + value = normalizePluginValue(settingsKey, value); + + // For unlocked settings: use plugin value unless user explicitly + // customized (their saved value differs from the default) + const userVal = _settings?.[settingsKey]; + const defaultVal = defaultValues[settingsKey]; + const userCustomized = + userVal !== undefined && + JSON.stringify(userVal) !== JSON.stringify(defaultVal); (acc as any)[settingsKey] = locked ? value - : (_settings?.[settingsKey] ?? value); + : userCustomized + ? userVal + : value; } return acc; }, {});