mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-16 09:23:07 +01:00
Compare commits
1 Commits
chore/i18n
...
feat/episo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
820604180d |
@@ -645,7 +645,7 @@ export default function SettingsTV() {
|
|||||||
formatValue={(v) => `${v.toFixed(1)}x`}
|
formatValue={(v) => `${v.toFixed(1)}x`}
|
||||||
/>
|
/>
|
||||||
<TVSettingsStepper
|
<TVSettingsStepper
|
||||||
label={t("home.settings.subtitles.mpv_subtitle_margin_y")}
|
label='Vertical Margin'
|
||||||
value={settings.mpvSubtitleMarginY ?? 0}
|
value={settings.mpvSubtitleMarginY ?? 0}
|
||||||
onDecrease={() => {
|
onDecrease={() => {
|
||||||
const newValue = Math.max(
|
const newValue = Math.max(
|
||||||
@@ -663,11 +663,11 @@ export default function SettingsTV() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TVSettingsOptionButton
|
<TVSettingsOptionButton
|
||||||
label={t("home.settings.subtitles.mpv_subtitle_align_x")}
|
label='Horizontal Alignment'
|
||||||
value={alignXLabel}
|
value={alignXLabel}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
showOptions({
|
showOptions({
|
||||||
title: t("home.settings.subtitles.mpv_subtitle_align_x"),
|
title: "Horizontal Alignment",
|
||||||
options: alignXOptions,
|
options: alignXOptions,
|
||||||
onSelect: (value) =>
|
onSelect: (value) =>
|
||||||
updateSettings({
|
updateSettings({
|
||||||
@@ -677,11 +677,11 @@ export default function SettingsTV() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TVSettingsOptionButton
|
<TVSettingsOptionButton
|
||||||
label={t("home.settings.subtitles.mpv_subtitle_align_y")}
|
label='Vertical Alignment'
|
||||||
value={alignYLabel}
|
value={alignYLabel}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
showOptions({
|
showOptions({
|
||||||
title: t("home.settings.subtitles.mpv_subtitle_align_y"),
|
title: "Vertical Alignment",
|
||||||
options: alignYOptions,
|
options: alignYOptions,
|
||||||
onSelect: (value) =>
|
onSelect: (value) =>
|
||||||
updateSettings({
|
updateSettings({
|
||||||
@@ -854,6 +854,13 @@ export default function SettingsTV() {
|
|||||||
updateSettings({ mergeNextUpAndContinueWatching: value })
|
updateSettings({ mergeNextUpAndContinueWatching: value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<TVSettingsToggle
|
||||||
|
label={t("home.settings.appearance.use_episode_images_next_up")}
|
||||||
|
value={settings.useEpisodeImagesForNextUp}
|
||||||
|
onToggle={(value) =>
|
||||||
|
updateSettings({ useEpisodeImagesForNextUp: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
<TVSettingsToggle
|
<TVSettingsToggle
|
||||||
label={t("home.settings.appearance.show_home_backdrop")}
|
label={t("home.settings.appearance.show_home_backdrop")}
|
||||||
value={settings.showHomeBackdrop}
|
value={settings.showHomeBackdrop}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function ArtistsScreen() {
|
|||||||
return (
|
return (
|
||||||
<View className='flex-1 justify-center items-center bg-black px-6'>
|
<View className='flex-1 justify-center items-center bg-black px-6'>
|
||||||
<Text className='text-neutral-500 text-center'>
|
<Text className='text-neutral-500 text-center'>
|
||||||
{t("music.missing_library_id")}
|
Missing music library id.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default function PlaylistsScreen() {
|
|||||||
return (
|
return (
|
||||||
<View className='flex-1 justify-center items-center bg-black px-6'>
|
<View className='flex-1 justify-center items-center bg-black px-6'>
|
||||||
<Text className='text-neutral-500 text-center'>
|
<Text className='text-neutral-500 text-center'>
|
||||||
{t("music.missing_library_id")}
|
Missing music library id.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export default function SuggestionsScreen() {
|
|||||||
return (
|
return (
|
||||||
<View className='flex-1 justify-center items-center bg-black px-6'>
|
<View className='flex-1 justify-center items-center bg-black px-6'>
|
||||||
<Text className='text-neutral-500 text-center'>
|
<Text className='text-neutral-500 text-center'>
|
||||||
{t("music.missing_library_id")}
|
Missing music library id.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
@@ -73,7 +72,6 @@ const ARTWORK_SIZE = SCREEN_WIDTH - 80;
|
|||||||
type ViewMode = "player" | "queue";
|
type ViewMode = "player" | "queue";
|
||||||
|
|
||||||
export default function NowPlayingScreen() {
|
export default function NowPlayingScreen() {
|
||||||
const { t } = useTranslation();
|
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -232,9 +230,7 @@ export default function NowPlayingScreen() {
|
|||||||
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
|
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text className='text-neutral-500'>
|
<Text className='text-neutral-500'>No track playing</Text>
|
||||||
{t("music.no_track_playing")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</BottomSheetModalProvider>
|
</BottomSheetModalProvider>
|
||||||
);
|
);
|
||||||
@@ -271,7 +267,7 @@ export default function NowPlayingScreen() {
|
|||||||
: "text-neutral-500"
|
: "text-neutral-500"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t("music.now_playing")}
|
Now Playing
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -722,7 +718,6 @@ const QueueView: React.FC<QueueViewProps> = ({
|
|||||||
onRemoveFromQueue,
|
onRemoveFromQueue,
|
||||||
onReorderQueue,
|
onReorderQueue,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const renderQueueItem = useCallback(
|
const renderQueueItem = useCallback(
|
||||||
({ item, drag, isActive, getIndex }: RenderItemParams<BaseItemDto>) => {
|
({ item, drag, isActive, getIndex }: RenderItemParams<BaseItemDto>) => {
|
||||||
const index = getIndex() ?? 0;
|
const index = getIndex() ?? 0;
|
||||||
@@ -836,15 +831,13 @@ const QueueView: React.FC<QueueViewProps> = ({
|
|||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<View className='px-4 py-2'>
|
<View className='px-4 py-2'>
|
||||||
<Text className='text-neutral-400 text-xs uppercase tracking-wider'>
|
<Text className='text-neutral-400 text-xs uppercase tracking-wider'>
|
||||||
{history.length > 0
|
{history.length > 0 ? "Playing from queue" : "Up next"}
|
||||||
? t("music.playing_from_queue")
|
|
||||||
: t("music.up_next")}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View className='flex-1 items-center justify-center py-20'>
|
<View className='flex-1 items-center justify-center py-20'>
|
||||||
<Text className='text-neutral-500'>{t("music.queue_empty")}</Text>
|
<Text className='text-neutral-500'>Queue is empty</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -192,7 +192,6 @@ const SubtitleResultCard = React.forwardRef<
|
|||||||
>(({ result, hasTVPreferredFocus, isDownloading, onPress }, ref) => {
|
>(({ result, hasTVPreferredFocus, isDownloading, onPress }, ref) => {
|
||||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||||
useTVFocusAnimation({ scaleAmount: 1.03 });
|
useTVFocusAnimation({ scaleAmount: 1.03 });
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -329,7 +328,7 @@ const SubtitleResultCard = React.forwardRef<
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={[styles.flagText, { fontSize: scaleSize(10) }]}>
|
<Text style={[styles.flagText, { fontSize: scaleSize(10) }]}>
|
||||||
{t("player.hash_match")}
|
Hash Match
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import { Link, Stack } from "expo-router";
|
import { Link, Stack } from "expo-router";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
import { ThemedText } from "@/components/ThemedText";
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
import { ThemedView } from "@/components/ThemedView";
|
import { ThemedView } from "@/components/ThemedView";
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
export default function NotFoundScreen() {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ title: t("home.oops") }} />
|
<Stack.Screen options={{ title: "Oops!" }} />
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
<ThemedText type='title'>{t("not_found.title")}</ThemedText>
|
<ThemedText type='title'>This screen doesn't exist.</ThemedText>
|
||||||
<Link href={"/home"} style={styles.link}>
|
<Link href={"/home"} style={styles.link}>
|
||||||
<ThemedText type='link'>{t("not_found.go_home")}</ThemedText>
|
<ThemedText type='link'>Go to home screen!</ThemedText>
|
||||||
</Link>
|
</Link>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Platform, StyleSheet, TouchableOpacity, View } from "react-native";
|
import { Platform, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
@@ -210,7 +209,6 @@ const PlatformDropdownComponent = ({
|
|||||||
expoUIConfig,
|
expoUIConfig,
|
||||||
bottomSheetConfig,
|
bottomSheetConfig,
|
||||||
}: PlatformDropdownProps) => {
|
}: PlatformDropdownProps) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { showModal, hideModal, isVisible } = useGlobalModal();
|
const { showModal, hideModal, isVisible } = useGlobalModal();
|
||||||
|
|
||||||
// Handle controlled open state for Android
|
// Handle controlled open state for Android
|
||||||
@@ -382,7 +380,7 @@ const PlatformDropdownComponent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={handlePress} activeOpacity={0.7}>
|
<TouchableOpacity onPress={handlePress} activeOpacity={0.7}>
|
||||||
{trigger || <Text className='text-white'>{t("common.open_menu")}</Text>}
|
{trigger || <Text className='text-white'>Open Menu</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -502,8 +502,8 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
disabled={!item}
|
disabled={!item}
|
||||||
accessibilityLabel={t("accessibility.play_button")}
|
accessibilityLabel='Play button'
|
||||||
accessibilityHint={t("accessibility.play_hint")}
|
accessibilityHint='Tap to play the media'
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
className={"relative flex-1"}
|
className={"relative flex-1"}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
@@ -37,7 +36,6 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
colors,
|
colors,
|
||||||
...props
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const [globalColorAtom] = useAtom(itemThemeColorAtom);
|
const [globalColorAtom] = useAtom(itemThemeColorAtom);
|
||||||
|
|
||||||
// Use colors prop if provided, otherwise fallback to global atom
|
// Use colors prop if provided, otherwise fallback to global atom
|
||||||
@@ -170,8 +168,8 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityLabel={t("accessibility.play_button")}
|
accessibilityLabel='Play button'
|
||||||
accessibilityHint={t("accessibility.play_hint")}
|
accessibilityHint='Tap to play the media'
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
className={"relative"}
|
className={"relative"}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
import {
|
||||||
FlatList,
|
FlatList,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -32,7 +31,6 @@ export const PlayInRemoteSessionButton: React.FC<Props> = ({
|
|||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const { sessions, isLoading } = useAllSessions({} as useSessionsProps);
|
const { sessions, isLoading } = useAllSessions({} as useSessionsProps);
|
||||||
const { t } = useTranslation();
|
|
||||||
const handlePlayInSession = async (sessionId: string) => {
|
const handlePlayInSession = async (sessionId: string) => {
|
||||||
if (!api || !item.Id) return;
|
if (!api || !item.Id) return;
|
||||||
|
|
||||||
@@ -67,9 +65,7 @@ export const PlayInRemoteSessionButton: React.FC<Props> = ({
|
|||||||
<View style={styles.centeredView}>
|
<View style={styles.centeredView}>
|
||||||
<View style={styles.modalView}>
|
<View style={styles.modalView}>
|
||||||
<View style={styles.modalHeader}>
|
<View style={styles.modalHeader}>
|
||||||
<Text style={styles.modalTitle}>
|
<Text style={styles.modalTitle}>Select Session</Text>
|
||||||
{t("home.sessions.select_session")}
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||||
<Ionicons name='close' size={24} color='white' />
|
<Ionicons name='close' size={24} color='white' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -82,7 +78,7 @@ export const PlayInRemoteSessionButton: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
) : !sessions || sessions.length === 0 ? (
|
) : !sessions || sessions.length === 0 ? (
|
||||||
<Text style={styles.noSessionsText}>
|
<Text style={styles.noSessionsText}>
|
||||||
{t("home.sessions.no_active_sessions")}
|
No active sessions found
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
@@ -102,7 +98,7 @@ export const PlayInRemoteSessionButton: React.FC<Props> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
{session.NowPlayingItem && (
|
{session.NowPlayingItem && (
|
||||||
<Text style={styles.nowPlaying} numberOfLines={1}>
|
<Text style={styles.nowPlaying} numberOfLines={1}>
|
||||||
{t("home.sessions.now_playing")}{" "}
|
Now playing:{" "}
|
||||||
{session.NowPlayingItem.SeriesName
|
{session.NowPlayingItem.SeriesName
|
||||||
? `${session.NowPlayingItem.SeriesName} :`
|
? `${session.NowPlayingItem.SeriesName} :`
|
||||||
: ""}
|
: ""}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
|
import { t } from "i18next";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -35,7 +35,6 @@ interface DownloadCardProps extends TouchableOpacityProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { cancelDownload } = useDownload();
|
const { cancelDownload } = useDownload();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useNetworkAwareQueryClient();
|
const queryClient = useNetworkAwareQueryClient();
|
||||||
@@ -174,9 +173,7 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
|||||||
|
|
||||||
{isTranscoding && (
|
{isTranscoding && (
|
||||||
<View className='bg-purple-600/20 px-2 py-0.5 rounded-md mt-1 self-start'>
|
<View className='bg-purple-600/20 px-2 py-0.5 rounded-md mt-1 self-start'>
|
||||||
<Text className='text-xs text-purple-400'>
|
<Text className='text-xs text-purple-400'>Transcoding</Text>
|
||||||
{t("home.downloads.transcoding")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { SectionHeader } from "@/components/common/SectionHeader";
|
import { SectionHeader } from "@/components/common/SectionHeader";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import MoviePoster from "@/components/posters/MoviePoster";
|
import MoviePoster from "@/components/posters/MoviePoster";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { Colors } from "../../constants/Colors";
|
import { Colors } from "../../constants/Colors";
|
||||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||||
@@ -85,6 +86,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
}, [isSuccess, onLoaded]);
|
}, [isSuccess, onLoaded]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { settings } = useSettings();
|
||||||
|
|
||||||
// Flatten all pages into a single array (and de-dupe by Id to avoid UI duplicates)
|
// Flatten all pages into a single array (and de-dupe by Id to avoid UI duplicates)
|
||||||
const allItems = useMemo(() => {
|
const allItems = useMemo(() => {
|
||||||
@@ -186,7 +188,10 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{item.Type === "Episode" && orientation === "horizontal" && (
|
{item.Type === "Episode" && orientation === "horizontal" && (
|
||||||
<ContinueWatchingPoster item={item} />
|
<ContinueWatchingPoster
|
||||||
|
item={item}
|
||||||
|
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{item.Type === "Episode" && orientation === "vertical" && (
|
{item.Type === "Episode" && orientation === "vertical" && (
|
||||||
<SeriesPoster item={item} />
|
<SeriesPoster item={item} />
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useScaledTVTypography } from "@/constants/TVTypography";
|
|||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||||
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { scaleSize } from "@/utils/scaleSize";
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
|
|
||||||
// Extra padding to accommodate scale animation (1.05x) and glow shadow
|
// Extra padding to accommodate scale animation (1.05x) and glow shadow
|
||||||
@@ -165,6 +166,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { settings } = useSettings();
|
||||||
|
|
||||||
const allItems = useMemo(() => {
|
const allItems = useMemo(() => {
|
||||||
const items = data?.pages.flat() ?? [];
|
const items = data?.pages.flat() ?? [];
|
||||||
@@ -231,6 +233,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
hasTVPreferredFocus={isFirstItem}
|
hasTVPreferredFocus={isFirstItem}
|
||||||
onFocus={() => handleItemFocus(item)}
|
onFocus={() => handleItemFocus(item)}
|
||||||
width={itemWidth}
|
width={itemWidth}
|
||||||
|
preferEpisodeImage={settings?.useEpisodeImagesForNextUp}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -243,6 +246,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
showItemActions,
|
showItemActions,
|
||||||
handleItemFocus,
|
handleItemFocus,
|
||||||
ITEM_GAP,
|
ITEM_GAP,
|
||||||
|
settings?.useEpisodeImagesForNextUp,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ScrollView, View, type ViewProps } from "react-native";
|
|||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import MoviePoster from "@/components/posters/MoviePoster";
|
import MoviePoster from "@/components/posters/MoviePoster";
|
||||||
import { useInView } from "@/hooks/useInView";
|
import { useInView } from "@/hooks/useInView";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||||
import { ItemCardText } from "../ItemCardText";
|
import { ItemCardText } from "../ItemCardText";
|
||||||
@@ -50,6 +51,7 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { settings } = useSettings();
|
||||||
|
|
||||||
// Show skeleton if loading OR if lazy loading is enabled and not in view yet
|
// Show skeleton if loading OR if lazy loading is enabled and not in view yet
|
||||||
const shouldShowSkeleton = isLoading || (enableLazyLoading && !isInView);
|
const shouldShowSkeleton = isLoading || (enableLazyLoading && !isInView);
|
||||||
@@ -108,7 +110,10 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{item.Type === "Episode" && orientation === "horizontal" && (
|
{item.Type === "Episode" && orientation === "horizontal" && (
|
||||||
<ContinueWatchingPoster item={item} />
|
<ContinueWatchingPoster
|
||||||
|
item={item}
|
||||||
|
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{item.Type === "Episode" && orientation === "vertical" && (
|
{item.Type === "Episode" && orientation === "vertical" && (
|
||||||
<SeriesPoster item={item} />
|
<SeriesPoster item={item} />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Animated, Pressable, StyleSheet, View } from "react-native";
|
import { Animated, Pressable, StyleSheet, 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";
|
||||||
@@ -23,7 +22,6 @@ export const TVGuideProgramCell: React.FC<TVGuideProgramCellProps> = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
refSetter,
|
refSetter,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
const { focused, handleFocus, handleBlur } = useTVFocusAnimation({
|
const { focused, handleFocus, handleBlur } = useTVFocusAnimation({
|
||||||
scaleAmount: 1,
|
scaleAmount: 1,
|
||||||
@@ -70,7 +68,7 @@ export const TVGuideProgramCell: React.FC<TVGuideProgramCellProps> = ({
|
|||||||
<Text
|
<Text
|
||||||
style={[styles.liveBadgeText, { fontSize: typography.callout }]}
|
style={[styles.liveBadgeText, { fontSize: typography.callout }]}
|
||||||
>
|
>
|
||||||
{t("player.live")}
|
LIVE
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export const TVLiveTVPage: React.FC = () => {
|
|||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("live_tv.title")}
|
Live TV
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Tab Bar */}
|
{/* Tab Bar */}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Animated, Pressable, View } from "react-native";
|
import { Animated, 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";
|
||||||
@@ -89,8 +88,6 @@ export const TVSearchTabBadges: React.FC<TVSearchTabBadgesProps> = ({
|
|||||||
showDiscover,
|
showDiscover,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (!showDiscover) {
|
if (!showDiscover) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -104,13 +101,13 @@ export const TVSearchTabBadges: React.FC<TVSearchTabBadgesProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TVSearchTabBadge
|
<TVSearchTabBadge
|
||||||
label={t("search.library")}
|
label='Library'
|
||||||
isSelected={searchType === "Library"}
|
isSelected={searchType === "Library"}
|
||||||
onPress={() => setSearchType("Library")}
|
onPress={() => setSearchType("Library")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<TVSearchTabBadge
|
<TVSearchTabBadge
|
||||||
label={t("search.discover")}
|
label='Discover'
|
||||||
isSelected={searchType === "Discover"}
|
isSelected={searchType === "Discover"}
|
||||||
onPress={() => setSearchType("Discover")}
|
onPress={() => setSearchType("Discover")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
<ListGroup title={t("home.settings.appearance.title")} className=''>
|
<ListGroup title={t("home.settings.appearance.title")} className=''>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.show_custom_menu_links")}
|
title={t("home.settings.other.show_custom_menu_links")}
|
||||||
|
subtitle={t("home.settings.other.show_custom_menu_links_hint")}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
Linking.openURL(
|
Linking.openURL(
|
||||||
@@ -44,6 +45,9 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.appearance.merge_next_up_continue_watching")}
|
title={t("home.settings.appearance.merge_next_up_continue_watching")}
|
||||||
|
subtitle={t(
|
||||||
|
"home.settings.appearance.merge_next_up_continue_watching_hint",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.mergeNextUpAndContinueWatching}
|
value={settings.mergeNextUpAndContinueWatching}
|
||||||
@@ -52,15 +56,32 @@ export const AppearanceSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.appearance.use_episode_images_next_up")}
|
||||||
|
subtitle={t(
|
||||||
|
"home.settings.appearance.use_episode_images_next_up_hint",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
value={settings.useEpisodeImagesForNextUp}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateSettings({ useEpisodeImagesForNextUp: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
router.push("/settings/appearance/hide-libraries/page")
|
router.push("/settings/appearance/hide-libraries/page")
|
||||||
}
|
}
|
||||||
title={t("home.settings.other.hide_libraries")}
|
title={t("home.settings.other.hide_libraries")}
|
||||||
|
subtitle={t("home.settings.other.select_libraries_you_want_to_hide")}
|
||||||
showArrow
|
showArrow
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.appearance.hide_remote_session_button")}
|
title={t("home.settings.appearance.hide_remote_session_button")}
|
||||||
|
subtitle={t(
|
||||||
|
"home.settings.appearance.hide_remote_session_button_hint",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.hideRemoteSessionButton}
|
value={settings.hideRemoteSessionButton}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Platform, Switch, View, type ViewProps } from "react-native";
|
import { Platform, Switch, View, type ViewProps } from "react-native";
|
||||||
import { Stepper } from "@/components/inputs/Stepper";
|
import { Stepper } from "@/components/inputs/Stepper";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
@@ -18,21 +17,20 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
const isTv = Platform.isTV;
|
const isTv = Platform.isTV;
|
||||||
const media = useMedia();
|
const media = useMedia();
|
||||||
const { settings, updateSettings } = media;
|
const { settings, updateSettings } = media;
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const alignXOptions: AlignX[] = ["left", "center", "right"];
|
const alignXOptions: AlignX[] = ["left", "center", "right"];
|
||||||
const alignYOptions: AlignY[] = ["top", "center", "bottom"];
|
const alignYOptions: AlignY[] = ["top", "center", "bottom"];
|
||||||
|
|
||||||
const alignXLabels: Record<AlignX, string> = {
|
const alignXLabels: Record<AlignX, string> = {
|
||||||
left: t("home.settings.subtitles.align.left"),
|
left: "Left",
|
||||||
center: t("home.settings.subtitles.align.center"),
|
center: "Center",
|
||||||
right: t("home.settings.subtitles.align.right"),
|
right: "Right",
|
||||||
};
|
};
|
||||||
|
|
||||||
const alignYLabels: Record<AlignY, string> = {
|
const alignYLabels: Record<AlignY, string> = {
|
||||||
top: t("home.settings.subtitles.align.top"),
|
top: "Top",
|
||||||
center: t("home.settings.subtitles.align.center"),
|
center: "Center",
|
||||||
bottom: t("home.settings.subtitles.align.bottom"),
|
bottom: "Bottom",
|
||||||
};
|
};
|
||||||
|
|
||||||
const alignXOptionGroups = useMemo(() => {
|
const alignXOptionGroups = useMemo(() => {
|
||||||
@@ -62,18 +60,16 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup
|
<ListGroup
|
||||||
title={t("home.settings.subtitles.mpv_settings_title")}
|
title='MPV Subtitle Settings'
|
||||||
description={
|
description={
|
||||||
<Text className='text-[#8E8D91] text-xs'>
|
<Text className='text-[#8E8D91] text-xs'>
|
||||||
{t("home.settings.subtitles.mpv_settings_description")}
|
Advanced subtitle customization for MPV player
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!isTv && (
|
{!isTv && (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem title='Vertical Margin'>
|
||||||
title={t("home.settings.subtitles.mpv_subtitle_margin_y")}
|
|
||||||
>
|
|
||||||
<Stepper
|
<Stepper
|
||||||
value={settings.mpvSubtitleMarginY ?? 0}
|
value={settings.mpvSubtitleMarginY ?? 0}
|
||||||
step={5}
|
step={5}
|
||||||
@@ -85,7 +81,7 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem title={t("home.settings.subtitles.mpv_subtitle_align_x")}>
|
<ListItem title='Horizontal Alignment'>
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={alignXOptionGroups}
|
groups={alignXOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
@@ -100,11 +96,11 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title={t("home.settings.subtitles.mpv_subtitle_align_x")}
|
title='Horizontal Alignment'
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem title={t("home.settings.subtitles.mpv_subtitle_align_y")}>
|
<ListItem title='Vertical Alignment'>
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={alignYOptionGroups}
|
groups={alignYOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
@@ -119,13 +115,13 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title={t("home.settings.subtitles.mpv_subtitle_align_y")}
|
title='Vertical Alignment'
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ListItem title={t("home.settings.subtitles.opaque_background")}>
|
<ListItem title='Opaque Background'>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.mpvSubtitleBackgroundEnabled ?? false}
|
value={settings.mpvSubtitleBackgroundEnabled ?? false}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -135,7 +131,7 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{settings.mpvSubtitleBackgroundEnabled && (
|
{settings.mpvSubtitleBackgroundEnabled && (
|
||||||
<ListItem title={t("home.settings.subtitles.background_opacity")}>
|
<ListItem title='Background Opacity'>
|
||||||
<Stepper
|
<Stepper
|
||||||
value={settings.mpvSubtitleBackgroundOpacity ?? 75}
|
value={settings.mpvSubtitleBackgroundOpacity ?? 75}
|
||||||
step={5}
|
step={5}
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ export interface TVPosterCardProps {
|
|||||||
|
|
||||||
/** Custom image URL getter - if not provided, uses smart URL logic */
|
/** Custom image URL getter - if not provided, uses smart URL logic */
|
||||||
imageUrlGetter?: (item: BaseItemDto) => string | undefined;
|
imageUrlGetter?: (item: BaseItemDto) => string | undefined;
|
||||||
|
|
||||||
|
/** For horizontal episodes, prefer the episode's own image over the series thumb */
|
||||||
|
preferEpisodeImage?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,6 +111,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
glowColor = "white",
|
glowColor = "white",
|
||||||
scaleAmount = 1.05,
|
scaleAmount = 1.05,
|
||||||
imageUrlGetter,
|
imageUrlGetter,
|
||||||
|
preferEpisodeImage = false,
|
||||||
}) => {
|
}) => {
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const posterSizes = useScaledTVPosterSizes();
|
const posterSizes = useScaledTVPosterSizes();
|
||||||
@@ -139,6 +143,10 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
if (orientation === "horizontal") {
|
if (orientation === "horizontal") {
|
||||||
// Episode: prefer series thumb image for consistent look (like hero section)
|
// Episode: prefer series thumb image for consistent look (like hero section)
|
||||||
if (item.Type === "Episode") {
|
if (item.Type === "Episode") {
|
||||||
|
// Opt-in: use the episode's own image instead of the series thumb.
|
||||||
|
if (preferEpisodeImage && item.ImageTags?.Primary) {
|
||||||
|
return `${api.basePath}/Items/${item.Id}/Images/Primary?fillHeight=600&quality=80&tag=${item.ImageTags.Primary}`;
|
||||||
|
}
|
||||||
// First try parent/series thumb (horizontal series artwork)
|
// First try parent/series thumb (horizontal series artwork)
|
||||||
if (item.ParentBackdropItemId && item.ParentThumbImageTag) {
|
if (item.ParentBackdropItemId && item.ParentThumbImageTag) {
|
||||||
return `${api.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=700&quality=80&tag=${item.ParentThumbImageTag}`;
|
return `${api.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=700&quality=80&tag=${item.ParentThumbImageTag}`;
|
||||||
@@ -173,7 +181,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
item,
|
item,
|
||||||
width: width * 2, // 2x for quality on large screens
|
width: width * 2, // 2x for quality on large screens
|
||||||
});
|
});
|
||||||
}, [api, item, orientation, width, imageUrlGetter]);
|
}, [api, item, orientation, width, imageUrlGetter, preferEpisodeImage]);
|
||||||
|
|
||||||
// Progress calculation
|
// Progress calculation
|
||||||
const progress = useMemo(() => {
|
const progress = useMemo(() => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Animated,
|
Animated,
|
||||||
@@ -29,7 +28,6 @@ export const TVSubtitleResultCard = React.forwardRef<
|
|||||||
const styles = createStyles(typography);
|
const styles = createStyles(typography);
|
||||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||||
useTVFocusAnimation({ scaleAmount: 1.03 });
|
useTVFocusAnimation({ scaleAmount: 1.03 });
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -154,7 +152,7 @@ export const TVSubtitleResultCard = React.forwardRef<
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={styles.flagText}>{t("player.hash_match")}</Text>
|
<Text style={styles.flagText}>Hash Match</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{result.hearingImpaired && (
|
{result.hearingImpaired && (
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
<SkipButton
|
<SkipButton
|
||||||
showButton={showSkipButton}
|
showButton={showSkipButton}
|
||||||
onPress={skipIntro}
|
onPress={skipIntro}
|
||||||
buttonText={t("player.skip_intro")}
|
buttonText='Skip Intro'
|
||||||
/>
|
/>
|
||||||
{/* Smart Skip Credits behavior:
|
{/* Smart Skip Credits behavior:
|
||||||
- Show "Skip Credits" if there's content after credits OR no next episode
|
- Show "Skip Credits" if there's content after credits OR no next episode
|
||||||
@@ -193,7 +193,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
showSkipCreditButton && (hasContentAfterCredits || !nextItem)
|
showSkipCreditButton && (hasContentAfterCredits || !nextItem)
|
||||||
}
|
}
|
||||||
onPress={skipCredit}
|
onPress={skipCredit}
|
||||||
buttonText={t("player.skip_credits")}
|
buttonText='Skip Credits'
|
||||||
/>
|
/>
|
||||||
{settings.autoPlayNextEpisode !== false &&
|
{settings.autoPlayNextEpisode !== false &&
|
||||||
(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
(settings.maxAutoPlayEpisodeCount.value === -1 ||
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text className='text-2xl font-bold text-white py-4 '>
|
<Text className='text-2xl font-bold text-white py-4 '>
|
||||||
{t("player.still_watching")}
|
Are you still watching ?
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type {
|
|||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { type FC, useCallback, useState } from "react";
|
import { type FC, useCallback, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Platform, TouchableOpacity, View } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
|
||||||
@@ -58,7 +57,6 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
|||||||
showTechnicalInfo = false,
|
showTechnicalInfo = false,
|
||||||
onToggleTechnicalInfo,
|
onToggleTechnicalInfo,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useControlsSafeAreaInsets();
|
const insets = useControlsSafeAreaInsets();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
@@ -129,8 +127,8 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
|||||||
onPress={toggleOrientation}
|
onPress={toggleOrientation}
|
||||||
disabled={isTogglingOrientation}
|
disabled={isTogglingOrientation}
|
||||||
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||||
accessibilityLabel={t("accessibility.toggle_orientation")}
|
accessibilityLabel='Toggle screen orientation'
|
||||||
accessibilityHint={t("accessibility.toggle_orientation_hint")}
|
accessibilityHint='Toggles the screen orientation between portrait and landscape'
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name='screen-rotation'
|
name='screen-rotation'
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Platform, StyleSheet, Text, View } from "react-native";
|
import { Platform, StyleSheet, Text, View } from "react-native";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
@@ -185,7 +184,6 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
|
|||||||
currentAudioIndex,
|
currentAudioIndex,
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
const { t } = useTranslation();
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const safeInsets = useControlsSafeAreaInsets();
|
const safeInsets = useControlsSafeAreaInsets();
|
||||||
const [info, setInfo] = useState<TechnicalInfo | null>(null);
|
const [info, setInfo] = useState<TechnicalInfo | null>(null);
|
||||||
@@ -314,13 +312,13 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
|
|||||||
)}
|
)}
|
||||||
{info?.videoCodec && (
|
{info?.videoCodec && (
|
||||||
<Text style={textStyle}>
|
<Text style={textStyle}>
|
||||||
{t("player.technical_info.video")} {formatCodec(info.videoCodec)}
|
Video: {formatCodec(info.videoCodec)}
|
||||||
{info.fps ? ` @ ${formatFps(info.fps)} fps` : ""}
|
{info.fps ? ` @ ${formatFps(info.fps)} fps` : ""}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{info?.audioCodec && (
|
{info?.audioCodec && (
|
||||||
<Text style={textStyle}>
|
<Text style={textStyle}>
|
||||||
{t("player.technical_info.audio")} {formatCodec(info.audioCodec)}
|
Audio: {formatCodec(info.audioCodec)}
|
||||||
{streamInfo?.audioChannels
|
{streamInfo?.audioChannels
|
||||||
? ` ${formatAudioChannels(streamInfo.audioChannels)}`
|
? ` ${formatAudioChannels(streamInfo.audioChannels)}`
|
||||||
: ""}
|
: ""}
|
||||||
@@ -328,13 +326,12 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
|
|||||||
)}
|
)}
|
||||||
{streamInfo?.subtitleCodec && (
|
{streamInfo?.subtitleCodec && (
|
||||||
<Text style={textStyle}>
|
<Text style={textStyle}>
|
||||||
{t("player.technical_info.subtitle")}{" "}
|
Subtitle: {formatCodec(streamInfo.subtitleCodec)}
|
||||||
{formatCodec(streamInfo.subtitleCodec)}
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{(info?.videoBitrate || info?.audioBitrate) && (
|
{(info?.videoBitrate || info?.audioBitrate) && (
|
||||||
<Text style={textStyle}>
|
<Text style={textStyle}>
|
||||||
{t("player.technical_info.bitrate")}{" "}
|
Bitrate:{" "}
|
||||||
{info.videoBitrate
|
{info.videoBitrate
|
||||||
? formatBitrate(info.videoBitrate)
|
? formatBitrate(info.videoBitrate)
|
||||||
: info.audioBitrate
|
: info.audioBitrate
|
||||||
@@ -344,9 +341,7 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
|
|||||||
)}
|
)}
|
||||||
{info?.cacheSeconds !== undefined && (
|
{info?.cacheSeconds !== undefined && (
|
||||||
<Text style={textStyle}>
|
<Text style={textStyle}>
|
||||||
{t("player.technical_info.buffer_seconds", {
|
Buffer: {info.cacheSeconds.toFixed(1)}s
|
||||||
seconds: info.cacheSeconds.toFixed(1),
|
|
||||||
})}
|
|
||||||
{info?.demuxerMaxBytes !== undefined
|
{info?.demuxerMaxBytes !== undefined
|
||||||
? ` (cap ${info.demuxerMaxBytes}MB` +
|
? ` (cap ${info.demuxerMaxBytes}MB` +
|
||||||
`${info.demuxerMaxBackBytes !== undefined ? ` / ${info.demuxerMaxBackBytes}MB back` : ""}` +
|
`${info.demuxerMaxBackBytes !== undefined ? ` / ${info.demuxerMaxBackBytes}MB back` : ""}` +
|
||||||
@@ -357,7 +352,7 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
|
|||||||
)}
|
)}
|
||||||
{info?.voDriver && (
|
{info?.voDriver && (
|
||||||
<Text style={textStyle}>
|
<Text style={textStyle}>
|
||||||
{t("player.technical_info.vo")} {info.voDriver}
|
VO: {info.voDriver}
|
||||||
{info.hwdec ? ` / ${info.hwdec}` : ""}
|
{info.hwdec ? ` / ${info.hwdec}` : ""}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -369,14 +364,10 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
|
|||||||
)}
|
)}
|
||||||
{info?.droppedFrames !== undefined && info.droppedFrames > 0 && (
|
{info?.droppedFrames !== undefined && info.droppedFrames > 0 && (
|
||||||
<Text style={[textStyle, styles.warningText]}>
|
<Text style={[textStyle, styles.warningText]}>
|
||||||
{t("player.technical_info.dropped_frames", {
|
Dropped: {info.droppedFrames} frames
|
||||||
count: info.droppedFrames,
|
|
||||||
})}
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{!info && !playMethod && (
|
{!info && !playMethod && <Text style={textStyle}>Loading...</Text>}
|
||||||
<Text style={textStyle}>{t("player.technical_info.loading")}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
type OptionGroup,
|
type OptionGroup,
|
||||||
@@ -55,7 +54,6 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
|||||||
onRatioChange,
|
onRatioChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const handleRatioSelect = (ratio: AspectRatio) => {
|
const handleRatioSelect = (ratio: AspectRatio) => {
|
||||||
@@ -68,10 +66,7 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
|||||||
{
|
{
|
||||||
options: ASPECT_RATIO_OPTIONS.map((option) => ({
|
options: ASPECT_RATIO_OPTIONS.map((option) => ({
|
||||||
type: "radio" as const,
|
type: "radio" as const,
|
||||||
label:
|
label: option.label,
|
||||||
option.id === "default"
|
|
||||||
? t("player.aspect_ratio_original")
|
|
||||||
: option.label,
|
|
||||||
value: option.id,
|
value: option.id,
|
||||||
selected: option.id === currentRatio,
|
selected: option.id === currentRatio,
|
||||||
onPress: () => handleRatioSelect(option.id),
|
onPress: () => handleRatioSelect(option.id),
|
||||||
@@ -99,7 +94,7 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
title={t("player.aspect_ratio")}
|
title='Aspect Ratio'
|
||||||
groups={optionGroups}
|
groups={optionGroups}
|
||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
bottomSheetConfig={{
|
bottomSheetConfig={{
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +47,6 @@ const DropdownView = ({
|
|||||||
const { settings, updateSettings } = useSettings();
|
const { settings, updateSettings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isOffline = useOfflineMode();
|
const isOffline = useOfflineMode();
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition } =
|
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition } =
|
||||||
useLocalSearchParams<{
|
useLocalSearchParams<{
|
||||||
@@ -103,7 +101,7 @@ const DropdownView = ({
|
|||||||
// Quality Section
|
// Quality Section
|
||||||
if (!isOffline) {
|
if (!isOffline) {
|
||||||
groups.push({
|
groups.push({
|
||||||
title: t("player.menu.quality"),
|
title: "Quality",
|
||||||
options:
|
options:
|
||||||
BITRATES?.map((bitrate) => ({
|
BITRATES?.map((bitrate) => ({
|
||||||
type: "radio" as const,
|
type: "radio" as const,
|
||||||
@@ -118,7 +116,7 @@ const DropdownView = ({
|
|||||||
// Subtitle Section
|
// Subtitle Section
|
||||||
if (subtitleTracks && subtitleTracks.length > 0) {
|
if (subtitleTracks && subtitleTracks.length > 0) {
|
||||||
groups.push({
|
groups.push({
|
||||||
title: t("player.menu.subtitles"),
|
title: "Subtitles",
|
||||||
options: subtitleTracks.map((sub) => ({
|
options: subtitleTracks.map((sub) => ({
|
||||||
type: "radio" as const,
|
type: "radio" as const,
|
||||||
label: sub.name,
|
label: sub.name,
|
||||||
@@ -130,7 +128,7 @@ const DropdownView = ({
|
|||||||
|
|
||||||
// Subtitle Scale Section
|
// Subtitle Scale Section
|
||||||
groups.push({
|
groups.push({
|
||||||
title: t("player.menu.subtitle_scale"),
|
title: "Subtitle Scale",
|
||||||
options: SUBTITLE_SCALE_PRESETS.map((preset) => ({
|
options: SUBTITLE_SCALE_PRESETS.map((preset) => ({
|
||||||
type: "radio" as const,
|
type: "radio" as const,
|
||||||
label: preset.label,
|
label: preset.label,
|
||||||
@@ -144,7 +142,7 @@ const DropdownView = ({
|
|||||||
// Audio Section
|
// Audio Section
|
||||||
if (audioTracks && audioTracks.length > 0) {
|
if (audioTracks && audioTracks.length > 0) {
|
||||||
groups.push({
|
groups.push({
|
||||||
title: t("player.menu.audio"),
|
title: "Audio",
|
||||||
options: audioTracks.map((track) => ({
|
options: audioTracks.map((track) => ({
|
||||||
type: "radio" as const,
|
type: "radio" as const,
|
||||||
label: track.name,
|
label: track.name,
|
||||||
@@ -158,7 +156,7 @@ const DropdownView = ({
|
|||||||
// Speed Section
|
// Speed Section
|
||||||
if (setPlaybackSpeed) {
|
if (setPlaybackSpeed) {
|
||||||
groups.push({
|
groups.push({
|
||||||
title: t("player.menu.speed"),
|
title: "Speed",
|
||||||
options: PLAYBACK_SPEEDS.map((speed) => ({
|
options: PLAYBACK_SPEEDS.map((speed) => ({
|
||||||
type: "radio" as const,
|
type: "radio" as const,
|
||||||
label: speed.label,
|
label: speed.label,
|
||||||
@@ -176,8 +174,8 @@ const DropdownView = ({
|
|||||||
{
|
{
|
||||||
type: "action" as const,
|
type: "action" as const,
|
||||||
label: showTechnicalInfo
|
label: showTechnicalInfo
|
||||||
? t("player.menu.hide_technical_info")
|
? "Hide Technical Info"
|
||||||
: t("player.menu.show_technical_info"),
|
: "Show Technical Info",
|
||||||
onPress: onToggleTechnicalInfo,
|
onPress: onToggleTechnicalInfo,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -187,7 +185,6 @@ const DropdownView = ({
|
|||||||
return groups;
|
return groups;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
t,
|
|
||||||
isOffline,
|
isOffline,
|
||||||
bitrateValue,
|
bitrateValue,
|
||||||
changeBitrate,
|
changeBitrate,
|
||||||
@@ -220,7 +217,7 @@ const DropdownView = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
title={t("player.menu.playback_options")}
|
title='Playback Options'
|
||||||
groups={optionGroups}
|
groups={optionGroups}
|
||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
expoUIConfig={{}}
|
expoUIConfig={{}}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Alert } from "react-native";
|
|||||||
import { type SharedValue, useSharedValue } from "react-native-reanimated";
|
import { type SharedValue, useSharedValue } from "react-native-reanimated";
|
||||||
import { useTVBackPress } from "@/hooks/useTVBackPress";
|
import { useTVBackPress } from "@/hooks/useTVBackPress";
|
||||||
import { useTVEventHandler } from "@/hooks/useTVEventHandler";
|
import { useTVEventHandler } from "@/hooks/useTVEventHandler";
|
||||||
import i18n from "@/i18n";
|
|
||||||
|
|
||||||
interface UseRemoteControlProps {
|
interface UseRemoteControlProps {
|
||||||
showControls: boolean;
|
showControls: boolean;
|
||||||
@@ -125,23 +124,17 @@ export function useRemoteControl({
|
|||||||
|
|
||||||
// Controls are hidden, so confirm before leaving playback.
|
// Controls are hidden, so confirm before leaving playback.
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
i18n.t("player.stopPlayback"),
|
"Stop Playback",
|
||||||
videoTitleRef.current
|
videoTitleRef.current
|
||||||
? i18n.t("player.stopPlayingTitle", {
|
? `Stop playing "${videoTitleRef.current}"?`
|
||||||
title: videoTitleRef.current,
|
: "Are you sure you want to stop playback?",
|
||||||
})
|
|
||||||
: i18n.t("player.stopPlayingConfirm"),
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: i18n.t("common.cancel"),
|
text: "Cancel",
|
||||||
style: "cancel",
|
style: "cancel",
|
||||||
onPress: () => onCancelExitRef.current?.(),
|
onPress: () => onCancelExitRef.current?.(),
|
||||||
},
|
},
|
||||||
{
|
{ text: "Stop", style: "destructive", onPress: onBackRef.current },
|
||||||
text: i18n.t("common.stop"),
|
|
||||||
style: "destructive",
|
|
||||||
onPress: onBackRef.current,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export class JellyseerrApi {
|
|||||||
if (inRange(status, 200, 299)) {
|
if (inRange(status, 200, 299)) {
|
||||||
if (data.version < "2.0.0") {
|
if (data.version < "2.0.0") {
|
||||||
const error = t(
|
const error = t(
|
||||||
"jellyseerr.toasts.jellyseerr_does_not_meet_requirements",
|
"jellyseerr.toasts.jellyseer_does_not_meet_requirements",
|
||||||
);
|
);
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
throw Error(error);
|
throw Error(error);
|
||||||
|
|||||||
@@ -137,7 +137,11 @@
|
|||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"merge_next_up_continue_watching": "Merge Continue watching & Next up",
|
"merge_next_up_continue_watching": "Merge Continue watching & Next up",
|
||||||
|
"merge_next_up_continue_watching_hint": "Combine Continue Watching and Next Up into a single home row.",
|
||||||
|
"use_episode_images_next_up": "Use episode images for Next Up & Continue Watching",
|
||||||
|
"use_episode_images_next_up_hint": "Show each episode's own thumbnail in the Next Up and Continue Watching rows instead of the series image.",
|
||||||
"hide_remote_session_button": "Hide remote session button",
|
"hide_remote_session_button": "Hide remote session button",
|
||||||
|
"hide_remote_session_button_hint": "Hide the remote-sessions button from the home header.",
|
||||||
"show_home_backdrop": "Dynamic home backdrop",
|
"show_home_backdrop": "Dynamic home backdrop",
|
||||||
"show_hero_carousel": "Hero carousel",
|
"show_hero_carousel": "Hero carousel",
|
||||||
"show_series_poster_on_episode": "Show series poster on episodes",
|
"show_series_poster_on_episode": "Show series poster on episodes",
|
||||||
@@ -270,10 +274,6 @@
|
|||||||
"mpv_subtitle_margin_y": "Vertical margin",
|
"mpv_subtitle_margin_y": "Vertical margin",
|
||||||
"mpv_subtitle_align_x": "Horizontal align",
|
"mpv_subtitle_align_x": "Horizontal align",
|
||||||
"mpv_subtitle_align_y": "Vertical align",
|
"mpv_subtitle_align_y": "Vertical align",
|
||||||
"mpv_settings_title": "MPV Subtitle Settings",
|
|
||||||
"mpv_settings_description": "Advanced subtitle customization for MPV player",
|
|
||||||
"opaque_background": "Opaque Background",
|
|
||||||
"background_opacity": "Background Opacity",
|
|
||||||
"align": {
|
"align": {
|
||||||
"left": "Left",
|
"left": "Left",
|
||||||
"center": "Center",
|
"center": "Center",
|
||||||
@@ -300,6 +300,7 @@
|
|||||||
},
|
},
|
||||||
"safe_area_in_controls": "Safe area in controls",
|
"safe_area_in_controls": "Safe area in controls",
|
||||||
"show_custom_menu_links": "Show custom menu links",
|
"show_custom_menu_links": "Show custom menu links",
|
||||||
|
"show_custom_menu_links_hint": "Show the custom links your server administrator added in the web config.",
|
||||||
"show_large_home_carousel": "Show large home carousel (beta)",
|
"show_large_home_carousel": "Show large home carousel (beta)",
|
||||||
"hide_libraries": "Hide libraries",
|
"hide_libraries": "Hide libraries",
|
||||||
"select_libraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
"select_libraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||||
@@ -441,13 +442,10 @@
|
|||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
"title": "Sessions",
|
"title": "Sessions",
|
||||||
"no_active_sessions": "No active sessions",
|
"no_active_sessions": "No active sessions"
|
||||||
"select_session": "Select Session",
|
|
||||||
"now_playing": "Now playing:"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"transcoding": "Transcoding",
|
|
||||||
"series": "Series",
|
"series": "Series",
|
||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -503,8 +501,6 @@
|
|||||||
"none": "None",
|
"none": "None",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"stop": "Stop",
|
|
||||||
"open_menu": "Open Menu",
|
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
@@ -606,31 +602,7 @@
|
|||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"live": "LIVE",
|
"live": "LIVE",
|
||||||
"menu": {
|
|
||||||
"quality": "Quality",
|
|
||||||
"subtitles": "Subtitles",
|
|
||||||
"subtitle_scale": "Subtitle Scale",
|
|
||||||
"audio": "Audio",
|
|
||||||
"speed": "Speed",
|
|
||||||
"playback_options": "Playback Options",
|
|
||||||
"show_technical_info": "Show Technical Info",
|
|
||||||
"hide_technical_info": "Hide Technical Info"
|
|
||||||
},
|
|
||||||
"technical_info": {
|
|
||||||
"video": "Video:",
|
|
||||||
"audio": "Audio:",
|
|
||||||
"subtitle": "Subtitle:",
|
|
||||||
"bitrate": "Bitrate:",
|
|
||||||
"buffer_seconds": "Buffer: {{seconds}}s",
|
|
||||||
"vo": "VO:",
|
|
||||||
"dropped_frames": "Dropped: {{count}} frames",
|
|
||||||
"loading": "Loading..."
|
|
||||||
},
|
|
||||||
"mpv_player_title": "MPV player",
|
"mpv_player_title": "MPV player",
|
||||||
"aspect_ratio": "Aspect Ratio",
|
|
||||||
"aspect_ratio_original": "Original",
|
|
||||||
"hash_match": "Hash Match",
|
|
||||||
"still_watching": "Are you still watching?",
|
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"failed_to_get_stream_url": "Failed to get the stream URL",
|
"failed_to_get_stream_url": "Failed to get the stream URL",
|
||||||
"an_error_occurred_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
"an_error_occurred_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
||||||
@@ -730,7 +702,6 @@
|
|||||||
"no_data_available": "No data available"
|
"no_data_available": "No data available"
|
||||||
},
|
},
|
||||||
"live_tv": {
|
"live_tv": {
|
||||||
"title": "Live TV",
|
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"coming_soon": "Coming soon",
|
"coming_soon": "Coming soon",
|
||||||
@@ -802,7 +773,7 @@
|
|||||||
"request_selected": "Request selected",
|
"request_selected": "Request selected",
|
||||||
"n_selected": "{{count}} selected",
|
"n_selected": "{{count}} selected",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"jellyseerr_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0",
|
"jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0",
|
||||||
"jellyseerr_test_failed": "Seerr test failed. Please try again.",
|
"jellyseerr_test_failed": "Seerr test failed. Please try again.",
|
||||||
"failed_to_test_jellyseerr_server_url": "Failed to test Seerr server url",
|
"failed_to_test_jellyseerr_server_url": "Failed to test Seerr server url",
|
||||||
"issue_submitted": "Issue submitted!",
|
"issue_submitted": "Issue submitted!",
|
||||||
@@ -815,16 +786,6 @@
|
|||||||
"failed_to_decline_request": "Failed to decline request"
|
"failed_to_decline_request": "Failed to decline request"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"accessibility": {
|
|
||||||
"play_button": "Play button",
|
|
||||||
"play_hint": "Tap to play the media",
|
|
||||||
"toggle_orientation": "Toggle screen orientation",
|
|
||||||
"toggle_orientation_hint": "Toggles the screen orientation between portrait and landscape"
|
|
||||||
},
|
|
||||||
"not_found": {
|
|
||||||
"title": "This screen doesn't exist.",
|
|
||||||
"go_home": "Go to home screen!"
|
|
||||||
},
|
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
@@ -835,12 +796,6 @@
|
|||||||
},
|
},
|
||||||
"music": {
|
"music": {
|
||||||
"title": "Music",
|
"title": "Music",
|
||||||
"no_track_playing": "No track playing",
|
|
||||||
"queue_empty": "Queue is empty",
|
|
||||||
"playing_from_queue": "Playing from queue",
|
|
||||||
"up_next": "Up next",
|
|
||||||
"now_playing": "Now Playing",
|
|
||||||
"missing_library_id": "Missing music library id.",
|
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"suggestions": "Suggestions",
|
"suggestions": "Suggestions",
|
||||||
"albums": "Albums",
|
"albums": "Albums",
|
||||||
|
|||||||
@@ -274,6 +274,9 @@ export type Settings = {
|
|||||||
hideBrightnessSlider: boolean;
|
hideBrightnessSlider: boolean;
|
||||||
usePopularPlugin: boolean;
|
usePopularPlugin: boolean;
|
||||||
mergeNextUpAndContinueWatching: boolean;
|
mergeNextUpAndContinueWatching: boolean;
|
||||||
|
// Use the episode's own image (instead of the series thumb) for the
|
||||||
|
// "Next Up" and "Continue Watching" home rows.
|
||||||
|
useEpisodeImagesForNextUp: boolean;
|
||||||
// TV-specific settings
|
// TV-specific settings
|
||||||
showHomeBackdrop: boolean;
|
showHomeBackdrop: boolean;
|
||||||
showTVHeroCarousel: boolean;
|
showTVHeroCarousel: boolean;
|
||||||
@@ -382,6 +385,7 @@ export const defaultValues: Settings = {
|
|||||||
hideBrightnessSlider: false,
|
hideBrightnessSlider: false,
|
||||||
usePopularPlugin: true,
|
usePopularPlugin: true,
|
||||||
mergeNextUpAndContinueWatching: false,
|
mergeNextUpAndContinueWatching: false,
|
||||||
|
useEpisodeImagesForNextUp: false,
|
||||||
// TV-specific settings
|
// TV-specific settings
|
||||||
showHomeBackdrop: true,
|
showHomeBackdrop: true,
|
||||||
showTVHeroCarousel: true,
|
showTVHeroCarousel: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user