Compare commits

..

1 Commits

Author SHA1 Message Date
Lance Chant
4f9aa0b7d0 fix: maxAutoPlayCount
Fixed the app using config from the plugin for {key, value} objects in
the app

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-05-08 13:45:40 +02:00
5 changed files with 539 additions and 312 deletions

692
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -204,7 +204,10 @@ export const OtherSettings: React.FC = () => {
}
/>
</ListItem>
<ListItem title={t("home.settings.other.max_auto_play_episode_count")}>
<ListItem
title={t("home.settings.other.max_auto_play_episode_count")}
disabled={pluginSettings?.maxAutoPlayEpisodeCount?.locked}
>
<PlatformDropdown
groups={autoPlayEpisodeOptions}
trigger={

View File

@@ -229,7 +229,10 @@ export const PlaybackControlsSettings: React.FC = () => {
<ListItem
title={t("home.settings.other.max_auto_play_episode_count")}
disabled={!settings.autoPlayNextEpisode}
disabled={
!settings.autoPlayNextEpisode ||
pluginSettings?.maxAutoPlayEpisodeCount?.locked
}
>
<PlatformDropdown
groups={autoPlayEpisodeOptions}

View File

@@ -27,9 +27,9 @@
"dependencies": {
"@bottom-tabs/react-navigation": "1.1.0",
"@douglowder/expo-av-route-picker-view": "^0.0.5",
"@expo/metro-runtime": "~55.0.0",
"@expo/metro-runtime": "~6.1.1",
"@expo/react-native-action-sheet": "^4.1.1",
"@expo/ui": "55.0.15",
"@expo/ui": "0.2.0-beta.9",
"@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "5.2.8",
"@jellyfin/sdk": "^0.13.0",
@@ -42,34 +42,34 @@
"@tanstack/react-query": "5.90.20",
"@tanstack/react-query-persist-client": "^5.90.18",
"axios": "^1.7.9",
"expo": "~55.0.0",
"expo-application": "~55.0.0",
"expo-asset": "~55.0.0",
"expo-background-task": "~55.0.0",
"expo-blur": "~55.0.0",
"expo-brightness": "~55.0.0",
"expo-build-properties": "~55.0.0",
"expo-constants": "55.0.16",
"expo-crypto": "^55.0.0",
"expo-dev-client": "~55.0.0",
"expo-device": "~55.0.0",
"expo-font": "~55.0.0",
"expo-haptics": "~55.0.0",
"expo-image": "~55.0.0",
"expo-linear-gradient": "~55.0.0",
"expo-linking": "~55.0.0",
"expo-localization": "~55.0.0",
"expo-location": "^55.0.0",
"expo-notifications": "~55.0.0",
"expo-router": "~55.0.0",
"expo-screen-orientation": "~55.0.0",
"expo-secure-store": "^55.0.0",
"expo-sharing": "~55.0.0",
"expo-splash-screen": "~55.0.0",
"expo-status-bar": "~55.0.0",
"expo-system-ui": "~55.0.0",
"expo-task-manager": "55.0.15",
"expo-web-browser": "~55.0.0",
"expo": "~54.0.31",
"expo-application": "~7.0.8",
"expo-asset": "~12.0.12",
"expo-background-task": "~1.0.10",
"expo-blur": "~15.0.8",
"expo-brightness": "~14.0.8",
"expo-build-properties": "~1.0.10",
"expo-constants": "18.0.13",
"expo-crypto": "^15.0.8",
"expo-dev-client": "~6.0.20",
"expo-device": "~8.0.10",
"expo-font": "~14.0.10",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linear-gradient": "~15.0.8",
"expo-linking": "~8.0.11",
"expo-localization": "~17.0.8",
"expo-location": "^19.0.8",
"expo-notifications": "~0.32.16",
"expo-router": "~6.0.21",
"expo-screen-orientation": "~9.0.8",
"expo-secure-store": "^15.0.8",
"expo-sharing": "~14.0.8",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-system-ui": "~6.0.9",
"expo-task-manager": "14.0.9",
"expo-web-browser": "~15.0.10",
"i18next": "^25.0.0",
"jotai": "2.16.2",
"lodash": "4.17.23",
@@ -160,6 +160,6 @@
"unrs-resolver"
],
"resolutions": {
"expo-constants": "55.0.16"
"expo-constants": "18.0.13"
}
}

View File

@@ -6,6 +6,7 @@ import {
type SortOrder,
SubtitlePlaybackMode,
} from "@jellyfin/sdk/lib/generated-client";
import { t } from "i18next";
import { atom, useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo } from "react";
import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
@@ -121,6 +122,46 @@ export interface MaxAutoPlayEpisodeCount {
value: number;
}
/**
* The plugin may send object-typed settings as plain primitives.
* Resolve to the proper option object from the available choices.
*/
const normalizePluginValue = (
settingsKey: keyof Settings,
value: unknown,
): unknown => {
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<Settings> = {};
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<Settings> = {};
const _unlockedPluginDefaults: Partial<Settings> = {};
const overrideSettings = Object.entries(pluginSettings ?? {}).reduce<
Partial<Settings>
>((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;
}, {});