Compare commits

...

3 Commits

Author SHA1 Message Date
Gauvino
3d6f12ceb2 chore: git hygiene — enforce LF, drop dead .gitattributes/.gitignore rules
- .gitattributes: 'text=auto eol=lf' so files are stored and checked out as LF
  on every OS (no more CRLF churn on Windows, regardless of core.autocrlf);
  mark common binaries; keep .bat/.cmd as CRLF.
- Remove the stale Git LFS rule for modules/vlc-player (that module no longer
  exists and no LFS objects are tracked).
- .gitignore: stop ignoring bun.lock (it's committed and checked by the lockfile
  workflow), and replace the dead/duplicate per-module build paths (player,
  hls-downloader, sf-player, music-controls, duplicate mpv-player) with a single
  modules/*/android/build/ glob.
- Renormalise the one CRLF-stored file (crowdin.yml) to LF (EOL only).
2026-06-01 17:22:45 +02:00
lance chant
338fb9713b fix: qr code scanning not working ios (#1619)
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-06-01 12:38:54 +02:00
lance chant
939fd2512d fix: max episodes count (#1554)
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-06-01 12:38:34 +02:00
10 changed files with 182 additions and 131 deletions

29
.gitattributes vendored
View File

@@ -1 +1,28 @@
.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text
# Normalise line endings to LF for everyone. Files are stored as LF in git and
# checked out as LF on every OS, so Windows clones stop producing CRLF churn
# (no more "LF will be replaced by CRLF" warnings) regardless of core.autocrlf.
* text=auto eol=lf
# Windows-only scripts must stay CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
# Binary assets — never touched / never normalised
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.ico binary
*.icns binary
*.ttf binary
*.otf binary
*.woff binary
*.woff2 binary
*.mp3 binary
*.mp4 binary
*.mov binary
*.pdf binary
*.keystore binary
*.jks binary
*.p12 binary

11
.gitignore vendored
View File

@@ -1,6 +1,5 @@
# Dependencies and Package Managers
node_modules/
bun.lock
bun.lockb
package-lock.json
@@ -21,10 +20,8 @@ web-build/
# Gradle caches (top-level + per-module native projects)
**/.gradle/
# Module-specific Builds
modules/mpv-player/android/build
modules/player/android
modules/hls-downloader/android/build
# Native module build outputs (any module)
modules/*/android/build/
# Generated Applications
Streamyfin.app
@@ -69,10 +66,6 @@ certs/
# Version and Backup Files
/version-backup-*
/modules/sf-player/android/build
/modules/music-controls/android/build
modules/background-downloader/android/build/*
/modules/mpv-player/android/build
# ios:unsigned-build Artifacts
build/

View File

@@ -59,17 +59,19 @@ function SettingsMobile() {
<QuickConnect className='mb-4' />
<View className='mb-4'>
<ListGroup title={t("pairing.pair_with_phone_title")}>
<ListItem
onPress={() =>
router.push("/(auth)/(tabs)/(home)/companion-login")
}
title={t("pairing.pair_with_phone")}
textColor='blue'
/>
</ListGroup>
</View>
{Platform.OS !== "ios" && (
<View className='mb-4'>
<ListGroup title={t("pairing.pair_with_phone_title")}>
<ListItem
onPress={() =>
router.push("/(auth)/(tabs)/(home)/companion-login")
}
title={t("pairing.pair_with_phone")}
textColor='blue'
/>
</ListGroup>
</View>
)}
<View className='mb-4'>
<AppLanguageSelector />

View File

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

View File

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

View File

@@ -196,7 +196,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

@@ -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;
@@ -427,61 +468,37 @@ export const useSettings = () => {
[_setPluginSettings],
);
const refreshStreamyfinPluginSettings = useCallback(
async (forceOverride = false) => {
if (!api) {
return;
const refreshStreamyfinPluginSettings = useCallback(async () => {
if (!api) {
return;
}
const newPluginSettings = await api.getStreamyfinPluginConfig().then(
({ data }) => {
writeInfoLog("Got plugin settings", data?.settings);
return data?.settings;
},
(_err) => undefined,
);
setPluginSettings(newPluginSettings);
// 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) {
const streamyStatsUrl = newPluginSettings.streamyStatsServerUrl;
if (streamyStatsUrl?.value && _settings.searchEngine !== "Streamystats") {
const newSettings = {
...defaultValues,
..._settings,
searchEngine: "Streamystats",
} as Settings;
setSettings(newSettings);
saveSettings(newSettings);
}
const newPluginSettings = await api.getStreamyfinPluginConfig().then(
({ data }) => {
writeInfoLog("Got plugin settings", data?.settings);
return data?.settings;
},
(_err) => undefined,
);
setPluginSettings(newPluginSettings);
}
// Apply plugin values to 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;
if (
streamyStatsUrl?.value &&
_settings.searchEngine !== "Streamystats"
) {
updates.searchEngine = "Streamystats";
}
if (Object.keys(updates).length > 0) {
const newSettings = {
...defaultValues,
..._settings,
...updates,
} as Settings;
setSettings(newSettings);
saveSettings(newSettings);
}
}
return newPluginSettings;
},
[api, _settings],
);
return newPluginSettings;
}, [api, _settings]);
const updateSettings = (update: Partial<Settings>) => {
if (!_settings) {
@@ -512,8 +529,13 @@ export const useSettings = () => {
Partial<Settings>
>((acc, [key, setting]) => {
if (setting) {
const { value, locked } = setting;
let { value } = setting;
const { locked } = setting;
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);
(acc as any)[settingsKey] = locked

View File

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