Compare commits

..

8 Commits

Author SHA1 Message Date
lance chant
efc45f56f6 Merge branch 'develop' into fix/maxEpisodes-count 2026-06-01 10:48:17 +02:00
lance chant
51ae9f94e0 Merge branch 'develop' into fix/maxEpisodes-count 2026-06-01 09:51:14 +02:00
Lance Chant
706f0cb185 fix: pr comments
Allowed the user settings to be saved incase admin unlocks settings

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-06-01 09:49:21 +02:00
lance chant
49aa374f03 Merge branch 'develop' into fix/maxEpisodes-count 2026-06-01 09:36:40 +02:00
lance chant
02961bff87 Merge branch 'develop' into fix/maxEpisodes-count 2026-06-01 07:28:54 +02:00
lance chant
16d1a4e91c Merge branch 'develop' into fix/maxEpisodes-count 2026-05-31 22:03:12 +02:00
Lance Chant
eb9bf40e6d Merge remote-tracking branch 'origin/develop' into fix/maxEpisodes-count
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-05-31 11:47:54 +02:00
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
7 changed files with 98 additions and 73 deletions

View File

@@ -59,7 +59,6 @@ function SettingsMobile() {
<QuickConnect className='mb-4' /> <QuickConnect className='mb-4' />
{Platform.OS !== "ios" && (
<View className='mb-4'> <View className='mb-4'>
<ListGroup title={t("pairing.pair_with_phone_title")}> <ListGroup title={t("pairing.pair_with_phone_title")}>
<ListItem <ListItem
@@ -71,7 +70,6 @@ function SettingsMobile() {
/> />
</ListGroup> </ListGroup>
</View> </View>
)}
<View className='mb-4'> <View className='mb-4'>
<AppLanguageSelector /> <AppLanguageSelector />

View File

@@ -114,7 +114,7 @@ export default function StreamystatsPage() {
}; };
const handleRefreshFromServer = useCallback(async () => { const handleRefreshFromServer = useCallback(async () => {
const newPluginSettings = await refreshStreamyfinPluginSettings(true); const newPluginSettings = await refreshStreamyfinPluginSettings();
// Update local state with new values // Update local state with new values
const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || ""; const newUrl = newPluginSettings?.streamyStatsServerUrl?.value || "";
setUrl(newUrl); setUrl(newUrl);

View File

@@ -1,6 +1,6 @@
import { t } from "i18next"; import { t } from "i18next";
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { Platform, ScrollView, View } from "react-native"; import { ScrollView, View } from "react-native";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useScaledTVTypography } from "@/constants/TVTypography"; import { useScaledTVTypography } from "@/constants/TVTypography";
@@ -107,7 +107,7 @@ export const TVAddServerForm: React.FC<TVAddServerFormProps> = ({
</View> </View>
{/* Pair with Phone */} {/* Pair with Phone */}
{Platform.OS !== "ios" && onStartPairing && ( {onStartPairing && (
<View> <View>
<Button <Button
onPress={onStartPairing} onPress={onStartPairing}

View File

@@ -196,7 +196,10 @@ export const OtherSettings: React.FC = () => {
} }
/> />
</ListItem> </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 <PlatformDropdown
groups={autoPlayEpisodeOptions} groups={autoPlayEpisodeOptions}
trigger={ trigger={

View File

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

View File

@@ -6,6 +6,7 @@ import {
type SortOrder, type SortOrder,
SubtitlePlaybackMode, SubtitlePlaybackMode,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { t } from "i18next";
import { atom, useAtom, useAtomValue } from "jotai"; import { atom, useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
@@ -121,6 +122,46 @@ export interface MaxAutoPlayEpisodeCount {
value: number; 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 = { export type HomeSectionLatestResolver = {
parentId?: string; parentId?: string;
limit?: number; limit?: number;
@@ -427,8 +468,7 @@ export const useSettings = () => {
[_setPluginSettings], [_setPluginSettings],
); );
const refreshStreamyfinPluginSettings = useCallback( const refreshStreamyfinPluginSettings = useCallback(async () => {
async (forceOverride = false) => {
if (!api) { if (!api) {
return; return;
} }
@@ -441,37 +481,16 @@ export const useSettings = () => {
); );
setPluginSettings(newPluginSettings); setPluginSettings(newPluginSettings);
// Apply plugin values to settings // Locked/unlocked values are handled by the settings memo, which
// applies locked values at runtime without overwriting user storage.
// We only handle auto-enabling Streamystats here.
if (newPluginSettings && _settings) { if (newPluginSettings && _settings) {
const updates: Partial<Settings> = {};
for (const [key, setting] of Object.entries(newPluginSettings)) {
if (setting && !setting.locked && setting.value !== undefined) {
const settingsKey = key as keyof Settings;
const effectiveValue = getEffectiveSettingValue(
_settings,
settingsKey,
);
// Apply if forceOverride is true, or if neither persisted settings
// nor app defaults provide a meaningful value.
if (forceOverride || !hasMeaningfulSettingValue(effectiveValue)) {
(updates as any)[settingsKey] = setting.value;
}
}
}
// Auto-enable Streamystats if server URL is provided
const streamyStatsUrl = newPluginSettings.streamyStatsServerUrl; const streamyStatsUrl = newPluginSettings.streamyStatsServerUrl;
if ( if (streamyStatsUrl?.value && _settings.searchEngine !== "Streamystats") {
streamyStatsUrl?.value &&
_settings.searchEngine !== "Streamystats"
) {
updates.searchEngine = "Streamystats";
}
if (Object.keys(updates).length > 0) {
const newSettings = { const newSettings = {
...defaultValues, ...defaultValues,
..._settings, ..._settings,
...updates, searchEngine: "Streamystats",
} as Settings; } as Settings;
setSettings(newSettings); setSettings(newSettings);
saveSettings(newSettings); saveSettings(newSettings);
@@ -479,9 +498,7 @@ export const useSettings = () => {
} }
return newPluginSettings; return newPluginSettings;
}, }, [api, _settings]);
[api, _settings],
);
const updateSettings = (update: Partial<Settings>) => { const updateSettings = (update: Partial<Settings>) => {
if (!_settings) { if (!_settings) {
@@ -512,8 +529,13 @@ export const useSettings = () => {
Partial<Settings> Partial<Settings>
>((acc, [key, setting]) => { >((acc, [key, setting]) => {
if (setting) { if (setting) {
const { value, locked } = setting; let { value } = setting;
const { locked } = setting;
const settingsKey = key as keyof Settings; const settingsKey = key as keyof Settings;
// Normalize object-typed settings from plugin (plain primitive → { key, value })
value = normalizePluginValue(settingsKey, value);
const effectiveValue = getEffectiveSettingValue(_settings, settingsKey); const effectiveValue = getEffectiveSettingValue(_settings, settingsKey);
(acc as any)[settingsKey] = locked (acc as any)[settingsKey] = locked

View File

@@ -27,7 +27,6 @@ export function startPairingListener(
}); });
socket.on("error", (err) => { socket.on("error", (err) => {
if (!active) return;
if (__DEV__) console.error("[PairingService] Socket error:", err); if (__DEV__) console.error("[PairingService] Socket error:", err);
onError?.(err.message); onError?.(err.message);
cleanup(); cleanup();