mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-07 06:28:39 +01:00
Compare commits
1 Commits
fix/androi
...
fix/tv-see
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36d18e2bec |
@@ -3,7 +3,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import { Directory, Paths } from "expo-file-system";
|
import { Directory, Paths } from "expo-file-system";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Alert, ScrollView, View } from "react-native";
|
import { Alert, ScrollView, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
TVSettingsToggle,
|
TVSettingsToggle,
|
||||||
} from "@/components/tv";
|
} from "@/components/tv";
|
||||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||||
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
|
import { useJellyseerrConnect } from "@/hooks/useJellyseerrConnect";
|
||||||
import { useTVOptionModal } from "@/hooks/useTVOptionModal";
|
import { useTVOptionModal } from "@/hooks/useTVOptionModal";
|
||||||
import { useTVUserSwitchModal } from "@/hooks/useTVUserSwitchModal";
|
import { useTVUserSwitchModal } from "@/hooks/useTVUserSwitchModal";
|
||||||
import { APP_LANGUAGES } from "@/i18n";
|
import { APP_LANGUAGES } from "@/i18n";
|
||||||
@@ -59,6 +61,37 @@ export default function SettingsTV() {
|
|||||||
const { showUserSwitchModal } = useTVUserSwitchModal();
|
const { showUserSwitchModal } = useTVUserSwitchModal();
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { jellyseerrApi, clearAllJellyseerData } = useJellyseerr();
|
||||||
|
const { connecting: jellyseerrConnecting, connect: jellyseerrConnect } =
|
||||||
|
useJellyseerrConnect();
|
||||||
|
|
||||||
|
// Jellyseerr state
|
||||||
|
const [jellyseerrServerUrl, setJellyseerrServerUrl] = useState(
|
||||||
|
settings.jellyseerrServerUrl || "",
|
||||||
|
);
|
||||||
|
const [jellyseerrPassword, setJellyseerrPassword] = useState("");
|
||||||
|
const { pluginSettings } = useSettings();
|
||||||
|
|
||||||
|
const isJellyseerrLocked =
|
||||||
|
pluginSettings?.jellyseerrServerUrl?.locked === true;
|
||||||
|
const isJellyseerrConnected = !!jellyseerrApi;
|
||||||
|
|
||||||
|
const handleJellyseerrUrlBlur = useCallback(() => {
|
||||||
|
const url = jellyseerrServerUrl.trim();
|
||||||
|
updateSettings({ jellyseerrServerUrl: url || undefined });
|
||||||
|
}, [jellyseerrServerUrl, updateSettings]);
|
||||||
|
|
||||||
|
const handleJellyseerrConnect = useCallback(async () => {
|
||||||
|
const url = jellyseerrServerUrl.trim();
|
||||||
|
if (!url) return;
|
||||||
|
await jellyseerrConnect(url, jellyseerrPassword);
|
||||||
|
}, [jellyseerrServerUrl, jellyseerrPassword, jellyseerrConnect]);
|
||||||
|
|
||||||
|
const handleDisconnectJellyseerr = useCallback(() => {
|
||||||
|
clearAllJellyseerData();
|
||||||
|
setJellyseerrServerUrl("");
|
||||||
|
setJellyseerrPassword("");
|
||||||
|
}, [clearAllJellyseerData]);
|
||||||
|
|
||||||
// Local state for OpenSubtitles API key (only commit on blur)
|
// Local state for OpenSubtitles API key (only commit on blur)
|
||||||
const [openSubtitlesApiKey, setOpenSubtitlesApiKey] = useState(
|
const [openSubtitlesApiKey, setOpenSubtitlesApiKey] = useState(
|
||||||
@@ -883,6 +916,81 @@ export default function SettingsTV() {
|
|||||||
onToggle={(value) => updateSettings({ tvThemeMusicEnabled: value })}
|
onToggle={(value) => updateSettings({ tvThemeMusicEnabled: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* seerr Section */}
|
||||||
|
<TVSectionHeader title='seerr' />
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "#9CA3AF",
|
||||||
|
fontSize: typography.callout - 2,
|
||||||
|
marginBottom: 16,
|
||||||
|
marginLeft: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("home.settings.plugins.jellyseerr.server_url_hint") ||
|
||||||
|
"Enter your Jellyseerr server URL to enable discover and request features."}
|
||||||
|
</Text>
|
||||||
|
<TVSettingsTextInput
|
||||||
|
label={
|
||||||
|
t("home.settings.plugins.jellyseerr.server_url") || "Server URL"
|
||||||
|
}
|
||||||
|
value={jellyseerrServerUrl}
|
||||||
|
placeholder={
|
||||||
|
t("home.settings.plugins.jellyseerr.server_url_placeholder") ||
|
||||||
|
"https://jellyseerr.example.com"
|
||||||
|
}
|
||||||
|
onChangeText={setJellyseerrServerUrl}
|
||||||
|
onBlur={handleJellyseerrUrlBlur}
|
||||||
|
disabled={isJellyseerrLocked || jellyseerrConnecting}
|
||||||
|
/>
|
||||||
|
{!isJellyseerrConnected && !isJellyseerrLocked && (
|
||||||
|
<>
|
||||||
|
<TVSettingsTextInput
|
||||||
|
label={
|
||||||
|
t("home.settings.plugins.jellyseerr.password") || "Password"
|
||||||
|
}
|
||||||
|
value={jellyseerrPassword}
|
||||||
|
placeholder={
|
||||||
|
t("home.settings.plugins.jellyseerr.password_placeholder", {
|
||||||
|
username: user?.Name,
|
||||||
|
}) || `Jellyfin password`
|
||||||
|
}
|
||||||
|
onChangeText={setJellyseerrPassword}
|
||||||
|
secureTextEntry
|
||||||
|
disabled={jellyseerrConnecting}
|
||||||
|
/>
|
||||||
|
<TVSettingsOptionButton
|
||||||
|
label={
|
||||||
|
jellyseerrConnecting
|
||||||
|
? t("common.connecting", "Connecting...") || "Connecting..."
|
||||||
|
: t("common.connect", "Connect") || "Connect"
|
||||||
|
}
|
||||||
|
value=''
|
||||||
|
onPress={handleJellyseerrConnect}
|
||||||
|
disabled={jellyseerrConnecting}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<TVSettingsRow
|
||||||
|
label={
|
||||||
|
isJellyseerrConnected
|
||||||
|
? t("common.connected", "Connected") || "Connected"
|
||||||
|
: t("common.not_connected", "Not connected") || "Not connected"
|
||||||
|
}
|
||||||
|
value=''
|
||||||
|
showChevron={false}
|
||||||
|
/>
|
||||||
|
{isJellyseerrConnected && !isJellyseerrLocked && (
|
||||||
|
<TVSettingsOptionButton
|
||||||
|
label={
|
||||||
|
t(
|
||||||
|
"home.settings.plugins.jellyseerr.reset_jellyseerr_config_button",
|
||||||
|
) || "Disconnect"
|
||||||
|
}
|
||||||
|
value=''
|
||||||
|
onPress={handleDisconnectJellyseerr}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Storage Section */}
|
{/* Storage Section */}
|
||||||
<TVSectionHeader title={t("home.settings.storage.storage_title")} />
|
<TVSectionHeader title={t("home.settings.storage.storage_title")} />
|
||||||
<TVSettingsOptionButton
|
<TVSettingsOptionButton
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Animated, FlatList, Pressable, View } from "react-native";
|
import { Animated, FlatList, Pressable, View } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
|
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
|
||||||
|
import { useScaledTVSizes } from "@/constants/TVSizes";
|
||||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import {
|
import {
|
||||||
@@ -22,8 +23,6 @@ import type {
|
|||||||
TvResult,
|
TvResult,
|
||||||
} from "@/utils/jellyseerr/server/models/Search";
|
} from "@/utils/jellyseerr/server/models/Search";
|
||||||
|
|
||||||
const SCALE_PADDING = 20;
|
|
||||||
|
|
||||||
interface TVDiscoverPosterProps {
|
interface TVDiscoverPosterProps {
|
||||||
item: MovieResult | TvResult;
|
item: MovieResult | TvResult;
|
||||||
isFirstItem?: boolean;
|
isFirstItem?: boolean;
|
||||||
@@ -34,6 +33,7 @@ const TVDiscoverPoster: React.FC<TVDiscoverPosterProps> = ({
|
|||||||
isFirstItem = false,
|
isFirstItem = false,
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
|
const sizes = useScaledTVSizes();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { jellyseerrApi, getTitle, getYear } = useJellyseerr();
|
const { jellyseerrApi, getTitle, getYear } = useJellyseerr();
|
||||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||||
@@ -50,6 +50,8 @@ const TVDiscoverPoster: React.FC<TVDiscoverPosterProps> = ({
|
|||||||
item.mediaInfo?.status === MediaStatus.AVAILABLE ||
|
item.mediaInfo?.status === MediaStatus.AVAILABLE ||
|
||||||
item.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE;
|
item.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE;
|
||||||
|
|
||||||
|
const posterWidth = sizes.posters.poster;
|
||||||
|
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/(auth)/(tabs)/(search)/jellyseerr/page",
|
pathname: "/(auth)/(tabs)/(search)/jellyseerr/page",
|
||||||
@@ -71,7 +73,7 @@ const TVDiscoverPoster: React.FC<TVDiscoverPosterProps> = ({
|
|||||||
style={[
|
style={[
|
||||||
animatedStyle,
|
animatedStyle,
|
||||||
{
|
{
|
||||||
width: 210,
|
width: posterWidth,
|
||||||
shadowColor: "#fff",
|
shadowColor: "#fff",
|
||||||
shadowOffset: { width: 0, height: 0 },
|
shadowOffset: { width: 0, height: 0 },
|
||||||
shadowOpacity: focused ? 0.6 : 0,
|
shadowOpacity: focused ? 0.6 : 0,
|
||||||
@@ -81,9 +83,9 @@ const TVDiscoverPoster: React.FC<TVDiscoverPosterProps> = ({
|
|||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: 210,
|
width: posterWidth,
|
||||||
aspectRatio: 10 / 15,
|
aspectRatio: 10 / 15,
|
||||||
borderRadius: 24,
|
borderRadius: sizes.gaps.small,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: "rgba(255,255,255,0.1)",
|
backgroundColor: "rgba(255,255,255,0.1)",
|
||||||
}}
|
}}
|
||||||
@@ -140,12 +142,12 @@ const TVDiscoverPoster: React.FC<TVDiscoverPosterProps> = ({
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{year && (
|
{year != null && (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
color: "#9CA3AF",
|
color: "#9CA3AF",
|
||||||
marginTop: 2,
|
marginTop: sizes.gaps.small,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{year}
|
{year}
|
||||||
@@ -166,6 +168,7 @@ export const TVDiscoverSlide: React.FC<TVDiscoverSlideProps> = ({
|
|||||||
isFirstSlide = false,
|
isFirstSlide = false,
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
|
const sizes = useScaledTVSizes();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
|
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
|
||||||
|
|
||||||
@@ -231,14 +234,14 @@ export const TVDiscoverSlide: React.FC<TVDiscoverSlideProps> = ({
|
|||||||
if (!flatData || flatData.length === 0) return null;
|
if (!flatData || flatData.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ marginBottom: 24 }}>
|
<View style={{ marginBottom: sizes.gaps.section }}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: typography.heading,
|
fontSize: typography.heading,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
marginBottom: 16,
|
marginBottom: sizes.gaps.small,
|
||||||
marginLeft: SCALE_PADDING,
|
marginLeft: sizes.padding.scale,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{slideTitle}
|
{slideTitle}
|
||||||
@@ -249,9 +252,9 @@ export const TVDiscoverSlide: React.FC<TVDiscoverSlideProps> = ({
|
|||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingHorizontal: SCALE_PADDING,
|
paddingHorizontal: sizes.padding.scale,
|
||||||
paddingVertical: SCALE_PADDING,
|
paddingVertical: sizes.padding.scale,
|
||||||
gap: 20,
|
gap: sizes.gaps.item,
|
||||||
}}
|
}}
|
||||||
style={{ overflow: "visible" }}
|
style={{ overflow: "visible" }}
|
||||||
onEndReached={() => {
|
onEndReached={() => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Animated, FlatList, Pressable, View } from "react-native";
|
import { Animated, FlatList, Pressable, View } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
|
import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
|
||||||
|
import { useScaledTVSizes } from "@/constants/TVSizes";
|
||||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
|
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
|
||||||
@@ -14,8 +15,6 @@ import type {
|
|||||||
TvResult,
|
TvResult,
|
||||||
} from "@/utils/jellyseerr/server/models/Search";
|
} from "@/utils/jellyseerr/server/models/Search";
|
||||||
|
|
||||||
const SCALE_PADDING = 20;
|
|
||||||
|
|
||||||
interface TVJellyseerrPosterProps {
|
interface TVJellyseerrPosterProps {
|
||||||
item: MovieResult | TvResult;
|
item: MovieResult | TvResult;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
@@ -28,6 +27,7 @@ const TVJellyseerrPoster: React.FC<TVJellyseerrPosterProps> = ({
|
|||||||
isFirstItem = false,
|
isFirstItem = false,
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
|
const sizes = useScaledTVSizes();
|
||||||
const { jellyseerrApi, getTitle, getYear } = useJellyseerr();
|
const { jellyseerrApi, getTitle, getYear } = useJellyseerr();
|
||||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||||
useTVFocusAnimation({ scaleAmount: 1.05 });
|
useTVFocusAnimation({ scaleAmount: 1.05 });
|
||||||
@@ -43,6 +43,8 @@ const TVJellyseerrPoster: React.FC<TVJellyseerrPosterProps> = ({
|
|||||||
item.mediaInfo?.status === MediaStatus.AVAILABLE ||
|
item.mediaInfo?.status === MediaStatus.AVAILABLE ||
|
||||||
item.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE;
|
item.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE;
|
||||||
|
|
||||||
|
const posterWidth = sizes.posters.poster;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
@@ -54,7 +56,7 @@ const TVJellyseerrPoster: React.FC<TVJellyseerrPosterProps> = ({
|
|||||||
style={[
|
style={[
|
||||||
animatedStyle,
|
animatedStyle,
|
||||||
{
|
{
|
||||||
width: 210,
|
width: posterWidth,
|
||||||
shadowColor: "#fff",
|
shadowColor: "#fff",
|
||||||
shadowOffset: { width: 0, height: 0 },
|
shadowOffset: { width: 0, height: 0 },
|
||||||
shadowOpacity: focused ? 0.6 : 0,
|
shadowOpacity: focused ? 0.6 : 0,
|
||||||
@@ -64,9 +66,9 @@ const TVJellyseerrPoster: React.FC<TVJellyseerrPosterProps> = ({
|
|||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: 210,
|
width: posterWidth,
|
||||||
aspectRatio: 10 / 15,
|
aspectRatio: 10 / 15,
|
||||||
borderRadius: 24,
|
borderRadius: sizes.gaps.small,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: "rgba(255,255,255,0.1)",
|
backgroundColor: "rgba(255,255,255,0.1)",
|
||||||
}}
|
}}
|
||||||
@@ -117,13 +119,13 @@ const TVJellyseerrPoster: React.FC<TVJellyseerrPosterProps> = ({
|
|||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
marginTop: 12,
|
marginTop: sizes.gaps.small,
|
||||||
}}
|
}}
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{year && (
|
{year != null && (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
@@ -149,6 +151,7 @@ const TVJellyseerrPersonPoster: React.FC<TVJellyseerrPersonPosterProps> = ({
|
|||||||
onPress,
|
onPress,
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
|
const sizes = useScaledTVSizes();
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi } = useJellyseerr();
|
||||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||||
useTVFocusAnimation();
|
useTVFocusAnimation();
|
||||||
@@ -157,13 +160,15 @@ const TVJellyseerrPersonPoster: React.FC<TVJellyseerrPersonPosterProps> = ({
|
|||||||
? jellyseerrApi?.imageProxy(item.profilePath, "w185")
|
? jellyseerrApi?.imageProxy(item.profilePath, "w185")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const avatarSize = Math.round(sizes.posters.poster * 0.67);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={onPress} onFocus={handleFocus} onBlur={handleBlur}>
|
<Pressable onPress={onPress} onFocus={handleFocus} onBlur={handleBlur}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
animatedStyle,
|
animatedStyle,
|
||||||
{
|
{
|
||||||
width: 160,
|
width: avatarSize,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
shadowColor: "#fff",
|
shadowColor: "#fff",
|
||||||
shadowOffset: { width: 0, height: 0 },
|
shadowOffset: { width: 0, height: 0 },
|
||||||
@@ -174,9 +179,9 @@ const TVJellyseerrPersonPoster: React.FC<TVJellyseerrPersonPosterProps> = ({
|
|||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: 140,
|
width: avatarSize,
|
||||||
height: 140,
|
height: avatarSize,
|
||||||
borderRadius: 70,
|
borderRadius: avatarSize / 2,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: "rgba(255,255,255,0.1)",
|
backgroundColor: "rgba(255,255,255,0.1)",
|
||||||
borderWidth: focused ? 3 : 0,
|
borderWidth: focused ? 3 : 0,
|
||||||
@@ -198,7 +203,11 @@ const TVJellyseerrPersonPoster: React.FC<TVJellyseerrPersonPosterProps> = ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name='person' size={56} color='rgba(255,255,255,0.4)' />
|
<Ionicons
|
||||||
|
name='person'
|
||||||
|
size={Math.round(avatarSize * 0.35)}
|
||||||
|
color='rgba(255,255,255,0.4)'
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -207,7 +216,7 @@ const TVJellyseerrPersonPoster: React.FC<TVJellyseerrPersonPosterProps> = ({
|
|||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
color: focused ? "#fff" : "rgba(255,255,255,0.9)",
|
color: focused ? "#fff" : "rgba(255,255,255,0.9)",
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
marginTop: 12,
|
marginTop: sizes.gaps.small,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
}}
|
}}
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
@@ -233,17 +242,18 @@ const TVJellyseerrMovieSection: React.FC<TVJellyseerrMovieSectionProps> = ({
|
|||||||
onItemPress,
|
onItemPress,
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
|
const sizes = useScaledTVSizes();
|
||||||
if (!items || items.length === 0) return null;
|
if (!items || items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ marginBottom: 24 }}>
|
<View style={{ marginBottom: sizes.gaps.section }}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: typography.heading,
|
fontSize: typography.heading,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
marginBottom: 16,
|
marginBottom: sizes.gaps.small,
|
||||||
marginLeft: SCALE_PADDING,
|
marginLeft: sizes.padding.scale,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -254,9 +264,9 @@ const TVJellyseerrMovieSection: React.FC<TVJellyseerrMovieSectionProps> = ({
|
|||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingHorizontal: SCALE_PADDING,
|
paddingHorizontal: sizes.padding.scale,
|
||||||
paddingVertical: SCALE_PADDING,
|
paddingVertical: sizes.padding.scale,
|
||||||
gap: 20,
|
gap: sizes.gaps.item,
|
||||||
}}
|
}}
|
||||||
style={{ overflow: "visible" }}
|
style={{ overflow: "visible" }}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
@@ -285,17 +295,18 @@ const TVJellyseerrTvSection: React.FC<TVJellyseerrTvSectionProps> = ({
|
|||||||
onItemPress,
|
onItemPress,
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
|
const sizes = useScaledTVSizes();
|
||||||
if (!items || items.length === 0) return null;
|
if (!items || items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ marginBottom: 24 }}>
|
<View style={{ marginBottom: sizes.gaps.section }}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: typography.heading,
|
fontSize: typography.heading,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
marginBottom: 16,
|
marginBottom: sizes.gaps.small,
|
||||||
marginLeft: SCALE_PADDING,
|
marginLeft: sizes.padding.scale,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -306,9 +317,9 @@ const TVJellyseerrTvSection: React.FC<TVJellyseerrTvSectionProps> = ({
|
|||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingHorizontal: SCALE_PADDING,
|
paddingHorizontal: sizes.padding.scale,
|
||||||
paddingVertical: SCALE_PADDING,
|
paddingVertical: sizes.padding.scale,
|
||||||
gap: 20,
|
gap: sizes.gaps.item,
|
||||||
}}
|
}}
|
||||||
style={{ overflow: "visible" }}
|
style={{ overflow: "visible" }}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
@@ -337,17 +348,18 @@ const TVJellyseerrPersonSection: React.FC<TVJellyseerrPersonSectionProps> = ({
|
|||||||
onItemPress,
|
onItemPress,
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
|
const sizes = useScaledTVSizes();
|
||||||
if (!items || items.length === 0) return null;
|
if (!items || items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ marginBottom: 24 }}>
|
<View style={{ marginBottom: sizes.gaps.section }}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: typography.heading,
|
fontSize: typography.heading,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
marginBottom: 16,
|
marginBottom: sizes.gaps.small,
|
||||||
marginLeft: SCALE_PADDING,
|
marginLeft: sizes.padding.scale,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -358,9 +370,9 @@ const TVJellyseerrPersonSection: React.FC<TVJellyseerrPersonSectionProps> = ({
|
|||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingHorizontal: SCALE_PADDING,
|
paddingHorizontal: sizes.padding.scale,
|
||||||
paddingVertical: SCALE_PADDING,
|
paddingVertical: sizes.padding.scale,
|
||||||
gap: 20,
|
gap: sizes.gaps.item,
|
||||||
}}
|
}}
|
||||||
style={{ overflow: "visible" }}
|
style={{ overflow: "visible" }}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
@@ -400,6 +412,7 @@ export const TVJellyseerrSearchResults: React.FC<
|
|||||||
onPersonPress,
|
onPersonPress,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const typography = useScaledTVTypography();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return null;
|
return null;
|
||||||
@@ -410,7 +423,7 @@ export const TVJellyseerrSearchResults: React.FC<
|
|||||||
<View style={{ alignItems: "center", paddingTop: 40 }}>
|
<View style={{ alignItems: "center", paddingTop: 40 }}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 24,
|
fontSize: typography.heading,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
@@ -418,7 +431,9 @@ export const TVJellyseerrSearchResults: React.FC<
|
|||||||
>
|
>
|
||||||
{t("search.no_results_found_for")}
|
{t("search.no_results_found_for")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 18, color: "rgba(255,255,255,0.6)" }}>
|
<Text
|
||||||
|
style={{ fontSize: typography.body, color: "rgba(255,255,255,0.6)" }}
|
||||||
|
>
|
||||||
"{searchQuery}"
|
"{searchQuery}"
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, ScrollView, TextInput, View } from "react-native";
|
import { Platform, ScrollView, TextInput, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
@@ -166,6 +166,7 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
|
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||||
|
|
||||||
// Image URL getter for music items
|
// Image URL getter for music items
|
||||||
const getImageUrl = useMemo(() => {
|
const getImageUrl = useMemo(() => {
|
||||||
@@ -270,6 +271,9 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
|||||||
onChangeText={setSearch}
|
onChangeText={setSearch}
|
||||||
defaultValue=''
|
defaultValue=''
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
|
onFocus={() => setIsSearchFocused(true)}
|
||||||
|
onBlur={() => setIsSearchFocused(false)}
|
||||||
|
hasTVPreferredFocus
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -290,6 +294,7 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
|||||||
searchType={searchType}
|
searchType={searchType}
|
||||||
setSearchType={setSearchType}
|
setSearchType={setSearchType}
|
||||||
showDiscover={showDiscover}
|
showDiscover={showDiscover}
|
||||||
|
disabled={isSearchFocused}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -316,6 +321,7 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
|||||||
// every keystroke as results re-render. User navigates down to the
|
// every keystroke as results re-render. User navigates down to the
|
||||||
// grid manually.
|
// grid manually.
|
||||||
isFirstSection={false}
|
isFirstSection={false}
|
||||||
|
disabled={isSearchFocused}
|
||||||
onItemPress={onItemPress}
|
onItemPress={onItemPress}
|
||||||
onItemLongPress={onItemLongPress}
|
onItemLongPress={onItemLongPress}
|
||||||
imageUrlGetter={
|
imageUrlGetter={
|
||||||
|
|||||||
@@ -592,7 +592,8 @@
|
|||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"verifying": "Verifying...",
|
"verifying": "Verifying...",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh",
|
||||||
|
"loading": "Loading..."
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"search": "Search...",
|
"search": "Search...",
|
||||||
|
|||||||
Reference in New Issue
Block a user