Compare commits

...

3 Commits

Author SHA1 Message Date
Fredrik Burmester
83a264d5a1 chore 2025-07-16 19:51:17 +02:00
Fredrik Burmester
2d434a0125 wip 2025-07-15 11:23:38 +02:00
Fredrik Burmester
0d7edca1ad wip 2025-07-15 11:23:35 +02:00
238 changed files with 9820 additions and 4229 deletions

View File

@@ -0,0 +1,22 @@
{
"permissions": {
"allow": [
"Bash(rm:*)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(for file in /Users/fredrikburmester/Documents/GitHub/streamyfin/translations/*.json)",
"Bash(do)",
"Bash(if grep -q \"live_tv\" \"$file\")",
"Bash(then)",
"Bash(echo \"Processing $file\")",
"Bash(fi)",
"Bash(done)",
"Bash(bun run:*)",
"Bash(pod install:*)",
"Bash(bun install:*)",
"Bash(ls:*)",
"Bash(cat:*)"
],
"deny": []
}
}

View File

@@ -1,10 +1,8 @@
module.exports = ({ config }) => { module.exports = ({ config }) => {
if (process.env.EXPO_TV !== "1") { config.plugins.push([
config.plugins.push([ "react-native-google-cast",
"react-native-google-cast", { useDefaultExpandedMediaControls: true },
{ useDefaultExpandedMediaControls: true }, ]);
]);
}
return { return {
android: { android: {
googleServicesFile: process.env.GOOGLE_SERVICES_JSON, googleServicesFile: process.env.GOOGLE_SERVICES_JSON,

View File

@@ -51,7 +51,6 @@
"googleServicesFile": "./google-services.json" "googleServicesFile": "./google-services.json"
}, },
"plugins": [ "plugins": [
"@react-native-tvos/config-tv",
"expo-router", "expo-router",
"expo-font", "expo-font",
[ [
@@ -139,7 +138,8 @@
{ {
"useDefaultExpandedMediaControls": true "useDefaultExpandedMediaControls": true
} }
] ],
"expo-background-task"
], ],
"experiments": { "experiments": {
"typedRoutes": true "typedRoutes": true

View File

@@ -1,13 +1,12 @@
import Ionicons from "@expo/vector-icons/Ionicons";
import { useAtom } from "jotai/index";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { ListItem } from "@/components/list/ListItem"; import { ListItem } from "@/components/list/ListItem";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import Ionicons from "@expo/vector-icons/Ionicons";
import { useAtom } from "jotai/index";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { FlatList, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const WebBrowser = !Platform.isTV ? require("expo-web-browser") : null; const WebBrowser = !Platform.isTV ? require("expo-web-browser") : null;

View File

@@ -1,7 +1,7 @@
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
export default function SearchLayout() { export default function SearchLayout() {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -1,8 +1,8 @@
import { Favorites } from "@/components/home/Favorites"; import { useCallback, useState } from "react";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
import React, { useCallback, useState } from "react";
import { RefreshControl, ScrollView, View } from "react-native"; import { RefreshControl, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Favorites } from "@/components/home/Favorites";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
export default function favorites() { export default function favorites() {
const invalidateCache = useInvalidatePlaybackProgressCache(); const invalidateCache = useInvalidatePlaybackProgressCache();

View File

@@ -1,15 +1,17 @@
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { Feather, Ionicons } from "@expo/vector-icons"; import { Feather, Ionicons } from "@expo/vector-icons";
import { Stack, useRouter } from "expo-router"; import { Stack, useRouter } from "expo-router";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native"; import { Platform, TouchableOpacity, View } from "react-native";
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast"); const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
import { useAtom } from "jotai";
import { useSessions, type useSessionsProps } from "@/hooks/useSessions"; import { useSessions, type useSessionsProps } from "@/hooks/useSessions";
import { userAtom } from "@/providers/JellyfinProvider"; import { userAtom } from "@/providers/JellyfinProvider";
import { useAtom } from "jotai";
export default function IndexLayout() { export default function IndexLayout() {
const router = useRouter(); const _router = useRouter();
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -1,3 +1,8 @@
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { EpisodeCard } from "@/components/downloads/EpisodeCard"; import { EpisodeCard } from "@/components/downloads/EpisodeCard";
import { import {
@@ -6,11 +11,6 @@ import {
} from "@/components/series/SeasonDropdown"; } from "@/components/series/SeasonDropdown";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
export default function page() { export default function page() {
const navigation = useNavigation(); const navigation = useNavigation();

View File

@@ -1,13 +1,3 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { ActiveDownloads } from "@/components/downloads/ActiveDownloads";
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { MovieCard } from "@/components/downloads/MovieCard";
import { SeriesCard } from "@/components/downloads/SeriesCard";
import { type DownloadedItem, useDownload } from "@/providers/DownloadProvider";
import { queueAtom } from "@/utils/atoms/queue";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { writeToLog } from "@/utils/log";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import {
BottomSheetBackdrop, BottomSheetBackdrop,
@@ -18,11 +8,21 @@ import {
import { useNavigation, useRouter } from "expo-router"; import { useNavigation, useRouter } from "expo-router";
import { t } from "i18next"; import { t } from "i18next";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { ActiveDownloads } from "@/components/downloads/ActiveDownloads";
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { MovieCard } from "@/components/downloads/MovieCard";
import { SeriesCard } from "@/components/downloads/SeriesCard";
import { type DownloadedItem, useDownload } from "@/providers/DownloadProvider";
import { queueAtom } from "@/utils/atoms/queue";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { writeToLog } from "@/utils/log";
export default function page() { export default function page() {
const navigation = useNavigation(); const navigation = useNavigation();

View File

@@ -1,12 +1,12 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { storage } from "@/utils/mmkv";
import { Feather, Ionicons } from "@expo/vector-icons"; import { Feather, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useFocusEffect, useRouter } from "expo-router"; import { useFocusEffect, useRouter } from "expo-router";
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Linking, TouchableOpacity, View } from "react-native"; import { Linking, TouchableOpacity, View } from "react-native";
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { storage } from "@/utils/mmkv";
export default function page() { export default function page() {
const router = useRouter(); const router = useRouter();

View File

@@ -1,19 +1,4 @@
import { Badge } from "@/components/Badge"; import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import Poster from "@/components/posters/Poster";
import { useInterval } from "@/hooks/useInterval";
import { useSessions, type useSessionsProps } from "@/hooks/useSessions";
import { apiAtom } from "@/providers/JellyfinProvider";
import { formatBitrate } from "@/utils/bitrate";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { formatTimeString } from "@/utils/time";
import {
AntDesign,
Entypo,
Ionicons,
MaterialCommunityIcons,
} from "@expo/vector-icons";
import { import {
HardwareAccelerationType, HardwareAccelerationType,
type SessionInfoDto, type SessionInfoDto,
@@ -26,10 +11,19 @@ import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { get } from "lodash"; import { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import { Badge } from "@/components/Badge";
import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader";
import Poster from "@/components/posters/Poster";
import { useInterval } from "@/hooks/useInterval";
import { useSessions, type useSessionsProps } from "@/hooks/useSessions";
import { apiAtom } from "@/providers/JellyfinProvider";
import { formatBitrate } from "@/utils/bitrate";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { formatTimeString } from "@/utils/time";
export default function page() { export default function page() {
const { sessions, isLoading } = useSessions({} as useSessionsProps); const { sessions, isLoading } = useSessions({} as useSessionsProps);
@@ -454,20 +448,18 @@ const TranscodingStreamView = ({
</Text> </Text>
</View> </View>
{isTranscoding && transcodeProperties ? ( {isTranscoding && transcodeProperties ? (
<> <View className='flex flex-row'>
<View className='flex flex-row'> <Text className='-mt-0 text-xs opacity-50 w-20 font-bold text-right pr-4'>
<Text className='-mt-0 text-xs opacity-50 w-20 font-bold text-right pr-4'> <MaterialCommunityIcons
<MaterialCommunityIcons name='arrow-right-bottom'
name='arrow-right-bottom' size={14}
size={14} color='white'
color='white' />
/> </Text>
</Text> <Text className='flex-1 text-sm mt-1'>
<Text className='flex-1 text-sm mt-1'> <TranscodingBadges properties={transcodeProperties} />
<TranscodingBadges properties={transcodeProperties} /> </Text>
</Text> </View>
</View>
</>
) : null} ) : null}
</View> </View>
); );

View File

@@ -1,3 +1,9 @@
import { useNavigation, useRouter } from "expo-router";
import { t } from "i18next";
import { useAtom } from "jotai";
import { useEffect } from "react";
import { ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup"; import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem"; import { ListItem } from "@/components/list/ListItem";
@@ -14,21 +20,14 @@ import { StorageSettings } from "@/components/settings/StorageSettings";
import { SubtitleToggles } from "@/components/settings/SubtitleToggles"; import { SubtitleToggles } from "@/components/settings/SubtitleToggles";
import { UserInfo } from "@/components/settings/UserInfo"; import { UserInfo } from "@/components/settings/UserInfo";
import { useHaptic } from "@/hooks/useHaptic"; import { useHaptic } from "@/hooks/useHaptic";
import { useJellyfin } from "@/providers/JellyfinProvider"; import { useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import { userAtom } from "@/providers/JellyfinProvider";
import { clearLogs } from "@/utils/log"; import { clearLogs } from "@/utils/log";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
import { useNavigation, useRouter } from "expo-router";
import { t } from "i18next";
import { useAtom } from "jotai";
import React, { useEffect } from "react";
import { ScrollView, Switch, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function settings() { export default function settings() {
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [user] = useAtom(userAtom); const [_user] = useAtom(userAtom);
const { logout } = useJellyfin(); const { logout } = useJellyfin();
const successHapticFeedback = useHaptic("success"); const successHapticFeedback = useHaptic("success");

View File

@@ -1,15 +1,15 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api"; import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Switch, View } from "react-native"; import { Switch, View } from "react-native";
import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
export default function page() { export default function page() {
const [settings, updateSettings, pluginSettings] = useSettings(); const [settings, updateSettings, pluginSettings] = useSettings();

View File

@@ -3,7 +3,7 @@ import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
export default function page() { export default function page() {
const [settings, updateSettings, pluginSettings] = useSettings(); const [_settings, _updateSettings, pluginSettings] = useSettings();
return ( return (
<DisabledSetting <DisabledSetting

View File

@@ -1,14 +1,14 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { FilterButton } from "@/components/filters/FilterButton";
import { LogLevel, useLog, writeErrorLog } from "@/utils/log";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import * as Sharing from "expo-sharing"; import * as Sharing from "expo-sharing";
import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ScrollView, TouchableOpacity, View } from "react-native"; import { ScrollView, TouchableOpacity, View } from "react-native";
import Collapsible from "react-native-collapsible"; import Collapsible from "react-native-collapsible";
import { Text } from "@/components/common/Text";
import { FilterButton } from "@/components/filters/FilterButton";
import { Loader } from "@/components/Loader";
import { LogLevel, useLog, writeErrorLog } from "@/utils/log";
export default function page() { export default function page() {
const navigation = useNavigation(); const navigation = useNavigation();

View File

@@ -1,13 +1,7 @@
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import { useSettings } from "@/utils/atoms/settings";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import DisabledSetting from "@/components/settings/DisabledSetting";
import React, { useEffect, useMemo, useState } from "react";
import { import {
Linking, Linking,
Switch, Switch,
@@ -16,6 +10,11 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { useSettings } from "@/utils/atoms/settings";
export default function page() { export default function page() {
const navigation = useNavigation(); const navigation = useNavigation();

View File

@@ -1,3 +1,10 @@
import { useMutation } from "@tanstack/react-query";
import { useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity } from "react-native";
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import DisabledSetting from "@/components/settings/DisabledSetting"; import DisabledSetting from "@/components/settings/DisabledSetting";
import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm"; import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm";
@@ -5,13 +12,6 @@ import { apiAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getOrSetDeviceId } from "@/utils/device"; import { getOrSetDeviceId } from "@/utils/device";
import { getStatistics } from "@/utils/optimize-server"; import { getStatistics } from "@/utils/optimize-server";
import { useMutation } from "@tanstack/react-query";
import { useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import { toast } from "sonner-native";
export default function page() { export default function page() {
const navigation = useNavigation(); const navigation = useNavigation();

View File

@@ -1,15 +1,3 @@
import { ItemCardText } from "@/components/ItemCardText";
import { Loader } from "@/components/Loader";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorrizontalScroll";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import type { BaseItemDtoQueryResult } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDtoQueryResult } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@@ -19,6 +7,18 @@ import { useAtom } from "jotai";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorrizontalScroll";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { ItemCardText } from "@/components/ItemCardText";
import { Loader } from "@/components/Loader";
import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
const page: React.FC = () => { const page: React.FC = () => {
const local = useLocalSearchParams(); const local = useLocalSearchParams();

View File

@@ -1,22 +1,3 @@
import { ItemCardText } from "@/components/ItemCardText";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { ItemPoster } from "@/components/posters/ItemPoster";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
SortByOption,
SortOrderOption,
genreFilterAtom,
sortByAtom,
sortOptions,
sortOrderAtom,
sortOrderOptions,
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
import type { import type {
BaseItemDto, BaseItemDto,
BaseItemDtoQueryResult, BaseItemDtoQueryResult,
@@ -35,6 +16,25 @@ import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FlatList, View } from "react-native"; import { FlatList, View } from "react-native";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { ItemCardText } from "@/components/ItemCardText";
import { ItemPoster } from "@/components/posters/ItemPoster";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
genreFilterAtom,
SortByOption,
SortOrderOption,
sortByAtom,
sortOptions,
sortOrderAtom,
sortOrderOptions,
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
const page: React.FC = () => { const page: React.FC = () => {
const searchParams = useLocalSearchParams(); const searchParams = useLocalSearchParams();
@@ -43,7 +43,7 @@ const page: React.FC = () => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const navigation = useNavigation(); const navigation = useNavigation();
const [orientation, setOrientation] = useState( const [orientation, _setOrientation] = useState(
ScreenOrientation.Orientation.PORTRAIT_UP, ScreenOrientation.Orientation.PORTRAIT_UP,
); );

View File

@@ -1,6 +1,3 @@
import { ItemContent } from "@/components/ItemContent";
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
@@ -15,6 +12,9 @@ import Animated, {
useSharedValue, useSharedValue,
withTiming, withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { Text } from "@/components/common/Text";
import { ItemContent } from "@/components/ItemContent";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
const Page: React.FC = () => { const Page: React.FC = () => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);

View File

@@ -1,18 +1,17 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { uniqBy } from "lodash";
import { useMemo } from "react";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import { import {
type MovieResult, type MovieResult,
Results,
type TvResult, type TvResult,
} from "@/utils/jellyseerr/server/models/Search"; } from "@/utils/jellyseerr/server/models/Search";
import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { uniqBy } from "lodash";
import React, { useMemo } from "react";
export default function page() { export default function page() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
@@ -99,7 +98,7 @@ export default function page() {
}} }}
/> />
} }
renderItem={(item, index) => ( renderItem={(item, _index) => (
<JellyseerrPoster item={item as MovieResult | TvResult} /> <JellyseerrPoster item={item as MovieResult | TvResult} />
)} )}
/> />

View File

@@ -1,21 +1,17 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
import { uniqBy } from "lodash";
import { useMemo } from "react";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import Poster from "@/components/posters/Poster";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import { import {
type MovieResult, type MovieResult,
Results,
type TvResult, type TvResult,
} from "@/utils/jellyseerr/server/models/Search"; } from "@/utils/jellyseerr/server/models/Search";
import { useInfiniteQuery } from "@tanstack/react-query";
import { router, useLocalSearchParams, useSegments } from "expo-router";
import { uniqBy } from "lodash";
import React, { useMemo } from "react";
import { TouchableOpacity } from "react-native";
export default function page() { export default function page() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
@@ -96,7 +92,7 @@ export default function page() {
{name} {name}
</Text> </Text>
} }
renderItem={(item, index) => ( renderItem={(item, _index) => (
<JellyseerrPoster item={item as MovieResult | TvResult} /> <JellyseerrPoster item={item as MovieResult | TvResult} />
)} )}
/> />

View File

@@ -1,25 +1,3 @@
import { Button } from "@/components/Button";
import { GenreTags } from "@/components/GenreTags";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { JellyserrRatings } from "@/components/Ratings";
import { Text } from "@/components/common/Text";
import Cast from "@/components/jellyseerr/Cast";
import DetailFacts from "@/components/jellyseerr/DetailFacts";
import JellyseerrSeasons from "@/components/series/JellyseerrSeasons";
import { ItemActions } from "@/components/series/SeriesActions";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
import {
type IssueType,
IssueTypeName,
} from "@/utils/jellyseerr/server/constants/issue";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type {
MovieResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import {
BottomSheetBackdrop, BottomSheetBackdrop,
@@ -36,7 +14,31 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native"; import { Platform, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { GenreTags } from "@/components/GenreTags";
import Cast from "@/components/jellyseerr/Cast";
import DetailFacts from "@/components/jellyseerr/DetailFacts";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { JellyserrRatings } from "@/components/Ratings";
import JellyseerrSeasons from "@/components/series/JellyseerrSeasons";
import { ItemActions } from "@/components/series/SeriesActions";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
import {
type IssueType,
IssueTypeName,
} from "@/utils/jellyseerr/server/constants/issue";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type {
MovieResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import RequestModal from "@/components/jellyseerr/RequestModal"; import RequestModal from "@/components/jellyseerr/RequestModal";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
@@ -380,7 +382,7 @@ const Page: React.FC = () => {
</DropdownMenu.Label> </DropdownMenu.Label>
{Object.entries(IssueTypeName) {Object.entries(IssueTypeName)
.reverse() .reverse()
.map(([key, value], idx) => ( .map(([key, value], _idx) => (
<DropdownMenu.Item <DropdownMenu.Item
key={value} key={value}
onSelect={() => onSelect={() =>

View File

@@ -1,6 +1,12 @@
import { OverviewText } from "@/components/OverviewText"; import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { orderBy, uniqBy } from "lodash";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import { OverviewText } from "@/components/OverviewText";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
@@ -8,12 +14,6 @@ import type {
MovieResult, MovieResult,
TvResult, TvResult,
} from "@/utils/jellyseerr/server/models/Search"; } from "@/utils/jellyseerr/server/models/Search";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams, useSegments } from "expo-router";
import { orderBy, uniqBy } from "lodash";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
export default function page() { export default function page() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
@@ -107,7 +107,7 @@ export default function page() {
MainContent={() => ( MainContent={() => (
<OverviewText text={data?.details?.biography} className='mt-4' /> <OverviewText text={data?.details?.biography} className='mt-4' />
)} )}
renderItem={(item, index) => ( renderItem={(item, _index) => (
<JellyseerrPoster item={item as MovieResult | TvResult} /> <JellyseerrPoster item={item as MovieResult | TvResult} />
)} )}
/> />

View File

@@ -1,52 +0,0 @@
import type {
MaterialTopTabNavigationEventMap,
MaterialTopTabNavigationOptions,
} from "@react-navigation/material-top-tabs";
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";
import type {
ParamListBase,
TabNavigationState,
} from "@react-navigation/native";
import { Stack, withLayoutContext } from "expo-router";
import React from "react";
const { Navigator } = createMaterialTopTabNavigator();
export const Tab = withLayoutContext<
MaterialTopTabNavigationOptions,
typeof Navigator,
TabNavigationState<ParamListBase>,
MaterialTopTabNavigationEventMap
>(Navigator);
const Layout = () => {
return (
<>
<Stack.Screen options={{ title: "Live TV" }} />
<Tab
initialRouteName='programs'
keyboardDismissMode='none'
screenOptions={{
tabBarBounces: true,
tabBarLabelStyle: { fontSize: 10 },
tabBarItemStyle: {
width: 100,
},
tabBarStyle: { backgroundColor: "black" },
animationEnabled: true,
lazy: true,
swipeEnabled: true,
tabBarIndicatorStyle: { backgroundColor: "#9334E9" },
tabBarScrollEnabled: true,
}}
>
<Tab.Screen name='programs' />
<Tab.Screen name='guide' />
<Tab.Screen name='channels' />
<Tab.Screen name='recordings' />
</Tab>
</>
);
};
export default Layout;

View File

@@ -1,56 +0,0 @@
import { ItemImage } from "@/components/common/ItemImage";
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import React from "react";
import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function page() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
const { data: channels } = useQuery({
queryKey: ["livetv", "channels"],
queryFn: async () => {
const res = await getLiveTvApi(api!).getLiveTvChannels({
startIndex: 0,
limit: 500,
enableFavoriteSorting: true,
userId: user?.Id,
addCurrentProgram: false,
enableUserData: false,
enableImageTypes: ["Primary"],
});
return res.data;
},
});
return (
<View className='flex flex-1'>
<FlashList
data={channels?.Items}
estimatedItemSize={76}
renderItem={({ item }) => (
<View className='flex flex-row items-center px-4 mb-2'>
<View className='w-22 mr-4 rounded-lg overflow-hidden'>
<ItemImage
style={{
aspectRatio: "1/1",
width: 60,
borderRadius: 8,
}}
item={item}
/>
</View>
<Text className='font-bold'>{item.Name}</Text>
</View>
)}
/>
</View>
);
}

View File

@@ -1,221 +0,0 @@
import { ItemImage } from "@/components/common/ItemImage";
import { Text } from "@/components/common/Text";
import { HourHeader } from "@/components/livetv/HourHeader";
import { LiveTVGuideRow } from "@/components/livetv/LiveTVGuideRow";
import { TAB_HEIGHT } from "@/constants/Values";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import React, { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Dimensions,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const HOUR_HEIGHT = 30;
const ITEMS_PER_PAGE = 20;
const MemoizedLiveTVGuideRow = React.memo(LiveTVGuideRow);
export default function page() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
const [date, setDate] = useState<Date>(new Date());
const [currentPage, setCurrentPage] = useState(1);
const { data: guideInfo } = useQuery({
queryKey: ["livetv", "guideInfo"],
queryFn: async () => {
const res = await getLiveTvApi(api!).getGuideInfo();
return res.data;
},
});
const { data: channels } = useQuery({
queryKey: ["livetv", "channels", currentPage],
queryFn: async () => {
const res = await getLiveTvApi(api!).getLiveTvChannels({
startIndex: (currentPage - 1) * ITEMS_PER_PAGE,
limit: ITEMS_PER_PAGE,
enableFavoriteSorting: true,
userId: user?.Id,
addCurrentProgram: false,
enableUserData: false,
enableImageTypes: ["Primary"],
});
return res.data;
},
});
const { data: programs } = useQuery({
queryKey: ["livetv", "programs", date, currentPage],
queryFn: async () => {
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(date);
endOfDay.setHours(23, 59, 59, 999);
const now = new Date();
const isToday = startOfDay.toDateString() === now.toDateString();
const res = await getLiveTvApi(api!).getPrograms({
getProgramsDto: {
MaxStartDate: endOfDay.toISOString(),
MinEndDate: isToday ? now.toISOString() : startOfDay.toISOString(),
ChannelIds: channels?.Items?.map((c) => c.Id).filter(
Boolean,
) as string[],
ImageTypeLimit: 1,
EnableImages: false,
SortBy: ["StartDate"],
EnableTotalRecordCount: false,
EnableUserData: false,
},
});
return res.data;
},
enabled: !!channels,
});
const screenWidth = Dimensions.get("window").width;
const [scrollX, setScrollX] = useState(0);
const handleNextPage = useCallback(() => {
setCurrentPage((prev) => prev + 1);
}, []);
const handlePrevPage = useCallback(() => {
setCurrentPage((prev) => Math.max(1, prev - 1));
}, []);
return (
<ScrollView
nestedScrollEnabled
contentInsetAdjustmentBehavior='automatic'
key={"home"}
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
}}
>
<PageButtons
currentPage={currentPage}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
isNextDisabled={
!channels || (channels?.Items?.length || 0) < ITEMS_PER_PAGE
}
/>
<View className='flex flex-row'>
<View className='flex flex-col w-[64px]'>
<View
style={{
height: HOUR_HEIGHT,
}}
className='bg-neutral-800'
/>
{channels?.Items?.map((c, i) => (
<View className='h-16 w-16 mr-4 rounded-lg overflow-hidden' key={i}>
<ItemImage
style={{
width: "100%",
height: "100%",
resizeMode: "contain",
}}
item={c}
/>
</View>
))}
</View>
<ScrollView
style={{
width: screenWidth - 64,
}}
horizontal
scrollEnabled
onScroll={(e) => {
setScrollX(e.nativeEvent.contentOffset.x);
}}
>
<View className='flex flex-col'>
<HourHeader height={HOUR_HEIGHT} />
{channels?.Items?.map((c, i) => (
<MemoizedLiveTVGuideRow
channel={c}
programs={programs?.Items}
key={c.Id}
scrollX={scrollX}
/>
))}
</View>
</ScrollView>
</View>
</ScrollView>
);
}
interface PageButtonsProps {
currentPage: number;
onPrevPage: () => void;
onNextPage: () => void;
isNextDisabled: boolean;
}
const PageButtons: React.FC<PageButtonsProps> = ({
currentPage,
onPrevPage,
onNextPage,
isNextDisabled,
}) => {
const { t } = useTranslation();
return (
<View className='flex flex-row justify-between items-center bg-neutral-800 w-full px-4 py-2'>
<TouchableOpacity
onPress={onPrevPage}
disabled={currentPage === 1}
className='flex flex-row items-center'
>
<Ionicons
name='chevron-back'
size={24}
color={currentPage === 1 ? "gray" : "white"}
/>
<Text
className={`ml-1 ${
currentPage === 1 ? "text-gray-500" : "text-white"
}`}
>
{t("live_tv.previous")}
</Text>
</TouchableOpacity>
<Text className='text-white'>Page {currentPage}</Text>
<TouchableOpacity
onPress={onNextPage}
disabled={isNextDisabled}
className='flex flex-row items-center'
>
<Text
className={`mr-1 ${isNextDisabled ? "text-gray-500" : "text-white"}`}
>
{t("live_tv.next")}
</Text>
<Ionicons
name='chevron-forward'
size={24}
color={isNextDisabled ? "gray" : "white"}
/>
</TouchableOpacity>
</View>
);
};

View File

@@ -1,147 +0,0 @@
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
import { TAB_HEIGHT } from "@/constants/Values";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api";
import { useAtom } from "jotai";
import React from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function page() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
const { t } = useTranslation();
return (
<ScrollView
nestedScrollEnabled
contentInsetAdjustmentBehavior='automatic'
key={"home"}
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
paddingTop: 8,
}}
>
<View className='flex flex-col space-y-2'>
<ScrollingCollectionList
queryKey={["livetv", "recommended"]}
title={t("live_tv.on_now")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getRecommendedPrograms({
userId: user?.Id,
isAiring: true,
limit: 24,
imageTypeLimit: 1,
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
enableTotalRecordCount: false,
fields: ["ChannelInfo", "PrimaryImageAspectRatio"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "shows"]}
title={t("live_tv.shows")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isMovie: false,
isSeries: true,
isSports: false,
isNews: false,
isKids: false,
enableTotalRecordCount: false,
fields: ["ChannelInfo", "PrimaryImageAspectRatio"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "movies"]}
title={t("live_tv.movies")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isMovie: true,
enableTotalRecordCount: false,
fields: ["ChannelInfo"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "sports"]}
title={t("live_tv.sports")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isSports: true,
enableTotalRecordCount: false,
fields: ["ChannelInfo"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "kids"]}
title={t("live_tv.for_kids")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isKids: true,
enableTotalRecordCount: false,
fields: ["ChannelInfo"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
<ScrollingCollectionList
queryKey={["livetv", "news"]}
title={t("live_tv.news")}
queryFn={async () => {
if (!api) return [] as BaseItemDto[];
const res = await getLiveTvApi(api).getLiveTvPrograms({
userId: user?.Id,
hasAired: false,
limit: 9,
isNews: true,
enableTotalRecordCount: false,
fields: ["ChannelInfo"],
enableImageTypes: ["Primary", "Thumb", "Backdrop"],
});
return res.data.Items || [];
}}
orientation='horizontal'
/>
</View>
</ScrollView>
);
}

View File

@@ -1,13 +0,0 @@
import { Text } from "@/components/common/Text";
import React from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
export default function page() {
const { t } = useTranslation();
return (
<View className='flex items-center justify-center h-full -mt-12'>
<Text>{t("live_tv.coming_soon")}</Text>
</View>
);
}

View File

@@ -1,13 +1,3 @@
import { AddToFavorites } from "@/components/AddToFavorites";
import { DownloadItems } from "@/components/DownloadItem";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { NextUp } from "@/components/series/NextUp";
import { SeasonPicker } from "@/components/series/SeasonPicker";
import { SeriesHeader } from "@/components/series/SeriesHeader";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@@ -18,6 +8,16 @@ import type React from "react";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { AddToFavorites } from "@/components/AddToFavorites";
import { DownloadItems } from "@/components/DownloadItem";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { NextUp } from "@/components/series/NextUp";
import { SeasonPicker } from "@/components/series/SeasonPicker";
import { SeriesHeader } from "@/components/series/SeriesHeader";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
const page: React.FC = () => { const page: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();

View File

@@ -1,34 +1,3 @@
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import React, { useCallback, useEffect, useMemo } from "react";
import { FlatList, View, useWindowDimensions } from "react-native";
import { ItemCardText } from "@/components/ItemCardText";
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { ItemPoster } from "@/components/posters/ItemPoster";
import { useOrientation } from "@/hooks/useOrientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
SortByOption,
SortOrderOption,
genreFilterAtom,
getSortByPreference,
getSortOrderPreference,
sortByAtom,
sortByPreferenceAtom,
sortOptions,
sortOrderAtom,
sortOrderOptions,
sortOrderPreferenceAtom,
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
import type { import type {
BaseItemDto, BaseItemDto,
BaseItemDtoQueryResult, BaseItemDtoQueryResult,
@@ -40,8 +9,38 @@ import {
getUserLibraryApi, getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api"; } from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import React, { useCallback, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FlatList, useWindowDimensions, 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 { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { ItemCardText } from "@/components/ItemCardText";
import { Loader } from "@/components/Loader";
import { ItemPoster } from "@/components/posters/ItemPoster";
import { useOrientation } from "@/hooks/useOrientation";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
genreFilterAtom,
getSortByPreference,
getSortOrderPreference,
SortByOption,
SortOrderOption,
sortByAtom,
sortByPreferenceAtom,
sortOptions,
sortOrderAtom,
sortOrderOptions,
sortOrderPreferenceAtom,
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
const Page = () => { const Page = () => {
const searchParams = useLocalSearchParams(); const searchParams = useLocalSearchParams();

View File

@@ -1,9 +1,11 @@
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { useSettings } from "@/utils/atoms/settings";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function IndexLayout() { export default function IndexLayout() {

View File

@@ -1,8 +1,3 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { LibraryItemCard } from "@/components/library/LibraryItemCard";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { import {
getUserLibraryApi, getUserLibraryApi,
getUserViewsApi, getUserViewsApi,
@@ -14,6 +9,11 @@ import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native"; import { StyleSheet, 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 { Loader } from "@/components/Loader";
import { LibraryItemCard } from "@/components/library/LibraryItemCard";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
export default function index() { export default function index() {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);

View File

@@ -1,10 +1,10 @@
import { Stack } from "expo-router";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { import {
commonScreenOptions, commonScreenOptions,
nestedTabPageScreenOptions, nestedTabPageScreenOptions,
} from "@/components/stacks/NestedTabPageStack"; } from "@/components/stacks/NestedTabPageStack";
import { Stack } from "expo-router";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
export default function SearchLayout() { export default function SearchLayout() {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -1,9 +1,30 @@
import type {
BaseItemDto,
BaseItemKind,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useDebounce } from "use-debounce";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { Tag } from "@/components/GenreTags";
import { ItemCardText } from "@/components/ItemCardText";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton"; import { FilterButton } from "@/components/filters/FilterButton";
import { Tag } from "@/components/GenreTags";
import { ItemCardText } from "@/components/ItemCardText";
import { import {
JellyseerrSearchSort, JellyseerrSearchSort,
JellyserrIndexPage, JellyserrIndexPage,
@@ -16,27 +37,6 @@ import { useJellyseerr } from "@/hooks/useJellyseerr";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus"; import { eventBus } from "@/utils/eventBus";
import type {
BaseItemDto,
BaseItemKind,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi, getSearchApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useDebounce } from "use-debounce";
type SearchType = "Library" | "Discover"; type SearchType = "Library" | "Discover";

View File

@@ -1,26 +1,24 @@
import React, { useCallback, useRef } from "react"; import {
createNativeBottomTabNavigator,
type NativeBottomTabNavigationEventMap,
} from "@bottom-tabs/react-navigation";
import { useFocusEffect, useRouter, withLayoutContext } from "expo-router";
import { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { useFocusEffect, useRouter, withLayoutContext } from "expo-router";
import {
type NativeBottomTabNavigationEventMap,
createNativeBottomTabNavigator,
} from "@bottom-tabs/react-navigation";
const { Navigator } = createNativeBottomTabNavigator(); const { Navigator } = createNativeBottomTabNavigator();
import type { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs";
import { Colors } from "@/constants/Colors"; import type { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
import { storage } from "@/utils/mmkv";
import type { import type {
ParamListBase, ParamListBase,
TabNavigationState, TabNavigationState,
} from "@react-navigation/native"; } from "@react-navigation/native";
import { SystemBars } from "react-native-edge-to-edge"; import { SystemBars } from "react-native-edge-to-edge";
import { Colors } from "@/constants/Colors";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
import { storage } from "@/utils/mmkv";
export const NativeTabs = withLayoutContext< export const NativeTabs = withLayoutContext<
BottomTabNavigationOptions, BottomTabNavigationOptions,
@@ -64,7 +62,7 @@ export default function TabLayout() {
<NativeTabs.Screen redirect name='index' /> <NativeTabs.Screen redirect name='index' />
<NativeTabs.Screen <NativeTabs.Screen
listeners={({ navigation }) => ({ listeners={({ navigation }) => ({
tabPress: (e) => { tabPress: (_e) => {
eventBus.emit("scrollToTop"); eventBus.emit("scrollToTop");
}, },
})} })}
@@ -83,7 +81,7 @@ export default function TabLayout() {
/> />
<NativeTabs.Screen <NativeTabs.Screen
listeners={({ navigation }) => ({ listeners={({ navigation }) => ({
tabPress: (e) => { tabPress: (_e) => {
eventBus.emit("searchTabPressed"); eventBus.emit("searchTabPressed");
}, },
})} })}

View File

@@ -1,5 +1,4 @@
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import React from "react";
import { SystemBars } from "react-native-edge-to-edge"; import { SystemBars } from "react-native-edge-to-edge";
export default function Layout() { export default function Layout() {

View File

@@ -15,7 +15,7 @@ import { router, useGlobalSearchParams, useNavigation } from "expo-router";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Alert, Platform, View } from "react-native"; import { Alert, View } from "react-native";
import { useSharedValue } from "react-native-reanimated"; import { useSharedValue } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { BITRATES } from "@/components/BitrateSelector"; import { BITRATES } from "@/components/BitrateSelector";
@@ -41,9 +41,7 @@ import { storage } from "@/utils/mmkv";
import generateDeviceProfile from "@/utils/profiles/native"; import generateDeviceProfile from "@/utils/profiles/native";
import { msToTicks, ticksToSeconds } from "@/utils/time"; import { msToTicks, ticksToSeconds } from "@/utils/time";
const downloadProvider = !Platform.isTV const downloadProvider = require("@/providers/DownloadProvider");
? require("@/providers/DownloadProvider")
: { useDownload: () => null };
const IGNORE_SAFE_AREAS_KEY = "video_player_ignore_safe_areas"; const IGNORE_SAFE_AREAS_KEY = "video_player_ignore_safe_areas";
@@ -70,9 +68,7 @@ export default function page() {
const progress = useSharedValue(0); const progress = useSharedValue(0);
const isSeeking = useSharedValue(false); const isSeeking = useSharedValue(false);
const cacheProgress = useSharedValue(0); const cacheProgress = useSharedValue(0);
const VolumeManager = Platform.isTV const VolumeManager = require("react-native-volume-manager");
? null
: require("react-native-volume-manager");
const getDownloadedItem = downloadProvider.useDownload(); const getDownloadedItem = downloadProvider.useDownload();
@@ -141,7 +137,7 @@ export default function page() {
setItemStatus({ isLoading: true, isError: false }); setItemStatus({ isLoading: true, isError: false });
try { try {
let fetchedItem: BaseItemDto | null = null; let fetchedItem: BaseItemDto | null = null;
if (offline && !Platform.isTV) { if (offline) {
const data = await getDownloadedItem.getDownloadedItem(itemId); const data = await getDownloadedItem.getDownloadedItem(itemId);
if (data) fetchedItem = data.item as BaseItemDto; if (data) fetchedItem = data.item as BaseItemDto;
} else { } else {
@@ -182,7 +178,7 @@ export default function page() {
const native = await generateDeviceProfile(); const native = await generateDeviceProfile();
try { try {
let result: Stream | null = null; let result: Stream | null = null;
if (offline && !Platform.isTV) { if (offline) {
const data = await getDownloadedItem.getDownloadedItem(itemId); const data = await getDownloadedItem.getDownloadedItem(itemId);
if (!data?.mediaSource) return; if (!data?.mediaSource) return;
const url = await getDownloadedFileUrl(data.item.Id!); const url = await getDownloadedFileUrl(data.item.Id!);
@@ -363,8 +359,6 @@ export default function page() {
}, [offline, getInitialPlaybackTicks]); }, [offline, getInitialPlaybackTicks]);
const volumeUpCb = useCallback(async () => { const volumeUpCb = useCallback(async () => {
if (Platform.isTV) return;
try { try {
const { volume: currentVolume } = await VolumeManager.getVolume(); const { volume: currentVolume } = await VolumeManager.getVolume();
const newVolume = Math.min(currentVolume + 0.1, 1.0); const newVolume = Math.min(currentVolume + 0.1, 1.0);
@@ -377,8 +371,6 @@ export default function page() {
const [previousVolume, setPreviousVolume] = useState<number | null>(null); const [previousVolume, setPreviousVolume] = useState<number | null>(null);
const toggleMuteCb = useCallback(async () => { const toggleMuteCb = useCallback(async () => {
if (Platform.isTV) return;
try { try {
const { volume: currentVolume } = await VolumeManager.getVolume(); const { volume: currentVolume } = await VolumeManager.getVolume();
const currentVolumePercent = currentVolume * 100; const currentVolumePercent = currentVolume * 100;
@@ -400,8 +392,6 @@ export default function page() {
} }
}, [previousVolume]); }, [previousVolume]);
const volumeDownCb = useCallback(async () => { const volumeDownCb = useCallback(async () => {
if (Platform.isTV) return;
try { try {
const { volume: currentVolume } = await VolumeManager.getVolume(); const { volume: currentVolume } = await VolumeManager.getVolume();
const newVolume = Math.max(currentVolume - 0.1, 0); // Decrease by 10% const newVolume = Math.max(currentVolume - 0.1, 0); // Decrease by 10%
@@ -418,8 +408,6 @@ export default function page() {
}, []); }, []);
const setVolumeCb = useCallback(async (newVolume: number) => { const setVolumeCb = useCallback(async (newVolume: number) => {
if (Platform.isTV) return;
try { try {
const clampedVolume = Math.max(0, Math.min(newVolume, 100)); const clampedVolume = Math.max(0, Math.min(newVolume, 100));
console.log("Setting volume to", clampedVolume); console.log("Setting volume to", clampedVolume);
@@ -446,14 +434,14 @@ export default function page() {
if (state === "Playing") { if (state === "Playing") {
setIsPlaying(true); setIsPlaying(true);
reportPlaybackProgress(); reportPlaybackProgress();
if (!Platform.isTV) await activateKeepAwakeAsync(); await activateKeepAwakeAsync();
return; return;
} }
if (state === "Paused") { if (state === "Paused") {
setIsPlaying(false); setIsPlaying(false);
reportPlaybackProgress(); reportPlaybackProgress();
if (!Platform.isTV) await deactivateKeepAwake(); await deactivateKeepAwake();
return; return;
} }

View File

@@ -1,11 +1,15 @@
import "@/augmentations"; import "@/augmentations";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { Platform } from "react-native";
import i18n from "@/i18n"; import i18n from "@/i18n";
import { DownloadProvider } from "@/providers/DownloadProvider"; import { DownloadProvider } from "@/providers/DownloadProvider";
import { import {
JellyfinProvider,
apiAtom, apiAtom,
getOrSetDeviceId, getOrSetDeviceId,
getTokenFromStorage, getTokenFromStorage,
JellyfinProvider,
} from "@/providers/JellyfinProvider"; } from "@/providers/JellyfinProvider";
import { JobQueueProvider } from "@/providers/JobQueueProvider"; import { JobQueueProvider } from "@/providers/JobQueueProvider";
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider"; import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
@@ -24,35 +28,33 @@ import {
} from "@/utils/log"; } from "@/utils/log";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server"; import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { Platform } from "react-native";
const BackGroundDownloader = !Platform.isTV const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader") ? require("@kesha-antonov/react-native-background-downloader")
: null; : null;
import { DarkTheme, ThemeProvider } from "@react-navigation/native"; import { DarkTheme, ThemeProvider } from "@react-navigation/native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const BackgroundFetch = !Platform.isTV
? require("expo-background-fetch")
: null;
import * as Device from "expo-device"; import * as Device from "expo-device";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
const Notifications = !Platform.isTV ? require("expo-notifications") : null; const Notifications = !Platform.isTV ? require("expo-notifications") : null;
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { Stack, router, useSegments } from "expo-router"; import { router, Stack, useSegments } from "expo-router";
import * as SplashScreen from "expo-splash-screen"; import * as SplashScreen from "expo-splash-screen";
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import * as TaskManager from "expo-task-manager";
import { getLocales } from "expo-localization"; import { getLocales } from "expo-localization";
import { Provider as JotaiProvider } from "jotai"; import { Provider as JotaiProvider } from "jotai";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import { AppState, Appearance } from "react-native"; import { Appearance, AppState } from "react-native";
import { SystemBars } from "react-native-edge-to-edge"; import { SystemBars } from "react-native-edge-to-edge";
import { GestureHandlerRootView } from "react-native-gesture-handler"; import { GestureHandlerRootView } from "react-native-gesture-handler";
import "react-native-reanimated"; import "react-native-reanimated";
import { userAtom } from "@/providers/JellyfinProvider";
import { store } from "@/utils/store";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import type { EventSubscription } from "expo-modules-core"; import type { EventSubscription } from "expo-modules-core";
import type { import type {
@@ -62,6 +64,8 @@ import type {
import type { ExpoPushToken } from "expo-notifications/build/Tokens.types"; import type { ExpoPushToken } from "expo-notifications/build/Tokens.types";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { Toaster } from "sonner-native"; import { Toaster } from "sonner-native";
import { userAtom } from "@/providers/JellyfinProvider";
import { store } from "@/utils/store";
if (!Platform.isTV) { if (!Platform.isTV) {
Notifications.setNotificationHandler({ Notifications.setNotificationHandler({
@@ -122,7 +126,9 @@ if (!Platform.isTV) {
console.log("TaskManager ~ sessions trigger"); console.log("TaskManager ~ sessions trigger");
const api = store.get(apiAtom); const api = store.get(apiAtom);
if (api === null || api === undefined) return; if (api === null || api === undefined) {
return { value: null };
}
const response = await getSessionApi(api).getSessions({ const response = await getSessionApi(api).getSessions({
activeWithinSeconds: 360, activeWithinSeconds: 360,
@@ -131,7 +137,7 @@ if (!Platform.isTV) {
const result = response.data.filter((s) => s.NowPlayingItem); const result = response.data.filter((s) => s.NowPlayingItem);
Notifications.setBadgeCountAsync(result.length); Notifications.setBadgeCountAsync(result.length);
return BackgroundFetch.BackgroundFetchResult.NewData; return { value: "success" };
}); });
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
@@ -141,20 +147,18 @@ if (!Platform.isTV) {
const settingsData = storage.getString("settings"); const settingsData = storage.getString("settings");
if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData; if (!settingsData) return { value: null };
const settings: Partial<Settings> = JSON.parse(settingsData); const settings: Partial<Settings> = JSON.parse(settingsData);
const url = settings?.optimizedVersionsServerUrl; const url = settings?.optimizedVersionsServerUrl;
if (!settings?.autoDownload || !url) if (!settings?.autoDownload || !url) return { value: null };
return BackgroundFetch.BackgroundFetchResult.NoData;
const token = getTokenFromStorage(); const token = getTokenFromStorage();
const deviceId = getOrSetDeviceId(); const deviceId = getOrSetDeviceId();
const baseDirectory = FileSystem.documentDirectory; const baseDirectory = FileSystem.documentDirectory;
if (!token || !deviceId || !baseDirectory) if (!token || !deviceId || !baseDirectory) return { value: null };
return BackgroundFetch.BackgroundFetchResult.NoData;
const jobs = await getAllJobsByDeviceId({ const jobs = await getAllJobsByDeviceId({
deviceId, deviceId,
@@ -187,7 +191,7 @@ if (!Platform.isTV) {
}) })
.done(() => { .done(() => {
console.log("TaskManager ~ Download completed: ", job.id); console.log("TaskManager ~ Download completed: ", job.id);
saveDownloadedItemInfo(job.item); _saveDownloadedItemInfo(job.item);
BackGroundDownloader.completeHandler(job.id); BackGroundDownloader.completeHandler(job.id);
cancelJobById({ cancelJobById({
authHeader: token, authHeader: token,
@@ -225,7 +229,7 @@ if (!Platform.isTV) {
console.log(`Auto download started: ${new Date(now).toISOString()}`); console.log(`Auto download started: ${new Date(now).toISOString()}`);
// Be sure to return the successful result type! // Be sure to return the successful result type!
return BackgroundFetch.BackgroundFetchResult.NewData; return { value: "success" };
}); });
} }
@@ -441,26 +445,25 @@ function Layout() {
segments, segments,
]); ]);
useEffect(() => { useEffect(() => {
const subscription = AppState.addEventListener( const subscription = AppState.addEventListener(
"change", "change",
(nextAppState) => { (nextAppState) => {
if ( if (
appState.current.match(/inactive|background/) && appState.current.match(/inactive|background/) &&
nextAppState === "active" nextAppState === "active"
) { ) {
BackGroundDownloader.checkForExistingDownloads(); BackGroundDownloader.checkForExistingDownloads();
} }
}, },
); );
BackGroundDownloader.checkForExistingDownloads(); BackGroundDownloader.checkForExistingDownloads();
return () => { return () => {
subscription.remove(); subscription.remove();
}; };
}, []); }, []);
}
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@@ -526,7 +529,7 @@ function Layout() {
); );
} }
function saveDownloadedItemInfo(item: BaseItemDto) { function _saveDownloadedItemInfo(item: BaseItemDto) {
try { try {
const downloadedItems = storage.getString("downloadedItems"); const downloadedItems = storage.getString("downloadedItems");
const items: BaseItemDto[] = downloadedItems const items: BaseItemDto[] = downloadedItems

View File

@@ -1,29 +1,29 @@
import { Button } from "@/components/Button";
import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery";
import { PreviousServersList } from "@/components/PreviousServersList";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
import { Colors } from "@/constants/Colors";
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import type { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client"; import type { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router"; import { useLocalSearchParams, useNavigation } from "expo-router";
import { t } from "i18next";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { import {
Alert, Alert,
Keyboard,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
SafeAreaView, SafeAreaView,
TouchableOpacity, TouchableOpacity,
View, View,
} from "react-native"; } from "react-native";
import { Keyboard } from "react-native";
import { t } from "i18next";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/Button";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery";
import { PreviousServersList } from "@/components/PreviousServersList";
import { Colors } from "@/constants/Colors";
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
const CredentialsSchema = z.object({ const CredentialsSchema = z.object({
username: z.string().min(1, t("login.username_required")), username: z.string().min(1, t("login.username_required")),
}); });
@@ -199,7 +199,7 @@ const Login: React.FC = () => {
], ],
); );
} }
} catch (error) { } catch (_error) {
Alert.alert( Alert.alert(
t("login.error_title"), t("login.error_title"),
t("login.failed_to_initiate_quick_connect"), t("login.failed_to_initiate_quick_connect"),
@@ -213,133 +213,127 @@ const Login: React.FC = () => {
behavior={Platform.OS === "ios" ? "padding" : "height"} behavior={Platform.OS === "ios" ? "padding" : "height"}
> >
{api?.basePath ? ( {api?.basePath ? (
<> <View className='flex flex-col h-full relative items-center justify-center'>
<View className='flex flex-col h-full relative items-center justify-center'> <View className='px-4 -mt-20 w-full'>
<View className='px-4 -mt-20 w-full'> <View className='flex flex-col space-y-2'>
<View className='flex flex-col space-y-2'> <Text className='text-2xl font-bold -mb-2'>
<Text className='text-2xl font-bold -mb-2'> {serverName ? (
{serverName ? ( <>
<> {`${t("login.login_to_title")} `}
{`${t("login.login_to_title")} `} <Text className='text-purple-600'>{serverName}</Text>
<Text className='text-purple-600'>{serverName}</Text> </>
</> ) : (
) : ( t("login.login_title")
t("login.login_title") )}
)}
</Text>
<Text className='text-xs text-neutral-400'>
{api.basePath}
</Text>
<Input
placeholder={t("login.username_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, username: text })
}
value={credentials.username}
keyboardType='default'
returnKeyType='done'
autoCapitalize='none'
// Changed from username to oneTimeCode because it is a known issue in RN
// https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037
textContentType='oneTimeCode'
clearButtonMode='while-editing'
maxLength={500}
/>
<Input
placeholder={t("login.password_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, password: text })
}
value={credentials.password}
secureTextEntry
keyboardType='default'
returnKeyType='done'
autoCapitalize='none'
textContentType='password'
clearButtonMode='while-editing'
maxLength={500}
/>
<View className='flex flex-row items-center justify-between'>
<Button
onPress={handleLogin}
loading={loading}
className='flex-1 mr-2'
>
{t("login.login_button")}
</Button>
<TouchableOpacity
onPress={handleQuickConnect}
className='p-2 bg-neutral-900 rounded-xl h-12 w-12 flex items-center justify-center'
>
<MaterialCommunityIcons
name='cellphone-lock'
size={24}
color='white'
/>
</TouchableOpacity>
</View>
</View>
</View>
<View className='absolute bottom-0 left-0 w-full px-4 mb-2' />
</View>
</>
) : (
<>
<View className='flex flex-col h-full items-center justify-center w-full'>
<View className='flex flex-col gap-y-2 px-4 w-full -mt-36'>
<Image
style={{
width: 100,
height: 100,
marginLeft: -23,
marginBottom: -20,
}}
source={require("@/assets/images/StreamyFinFinal.png")}
/>
<Text className='text-3xl font-bold'>Streamyfin</Text>
<Text className='text-neutral-500'>
{t("server.enter_url_to_jellyfin_server")}
</Text> </Text>
<Text className='text-xs text-neutral-400'>{api.basePath}</Text>
<Input <Input
aria-label='Server URL' placeholder={t("login.username_placeholder")}
placeholder={t("server.server_url_placeholder")} onChangeText={(text) =>
onChangeText={setServerURL} setCredentials({ ...credentials, username: text })
value={serverURL} }
keyboardType='url' value={credentials.username}
keyboardType='default'
returnKeyType='done' returnKeyType='done'
autoCapitalize='none' autoCapitalize='none'
textContentType='URL' // Changed from username to oneTimeCode because it is a known issue in RN
// https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037
textContentType='oneTimeCode'
clearButtonMode='while-editing'
maxLength={500} maxLength={500}
/> />
<Button
loading={loadingServerCheck} <Input
disabled={loadingServerCheck} placeholder={t("login.password_placeholder")}
onPress={async () => { onChangeText={(text) =>
await handleConnect(serverURL); setCredentials({ ...credentials, password: text })
}} }
className='w-full grow' value={credentials.password}
> secureTextEntry
{t("server.connect_button")} keyboardType='default'
</Button> returnKeyType='done'
<JellyfinServerDiscovery autoCapitalize='none'
onServerSelect={async (server) => { textContentType='password'
setServerURL(server.address); clearButtonMode='while-editing'
if (server.serverName) { maxLength={500}
setServerName(server.serverName);
}
await handleConnect(server.address);
}}
/>
<PreviousServersList
onServerSelect={async (s) => {
await handleConnect(s.address);
}}
/> />
<View className='flex flex-row items-center justify-between'>
<Button
onPress={handleLogin}
loading={loading}
className='flex-1 mr-2'
>
{t("login.login_button")}
</Button>
<TouchableOpacity
onPress={handleQuickConnect}
className='p-2 bg-neutral-900 rounded-xl h-12 w-12 flex items-center justify-center'
>
<MaterialCommunityIcons
name='cellphone-lock'
size={24}
color='white'
/>
</TouchableOpacity>
</View>
</View> </View>
</View> </View>
</>
<View className='absolute bottom-0 left-0 w-full px-4 mb-2' />
</View>
) : (
<View className='flex flex-col h-full items-center justify-center w-full'>
<View className='flex flex-col gap-y-2 px-4 w-full -mt-36'>
<Image
style={{
width: 100,
height: 100,
marginLeft: -23,
marginBottom: -20,
}}
source={require("@/assets/images/StreamyFinFinal.png")}
/>
<Text className='text-3xl font-bold'>Streamyfin</Text>
<Text className='text-neutral-500'>
{t("server.enter_url_to_jellyfin_server")}
</Text>
<Input
aria-label='Server URL'
placeholder={t("server.server_url_placeholder")}
onChangeText={setServerURL}
value={serverURL}
keyboardType='url'
returnKeyType='done'
autoCapitalize='none'
textContentType='URL'
maxLength={500}
/>
<Button
loading={loadingServerCheck}
disabled={loadingServerCheck}
onPress={async () => {
await handleConnect(serverURL);
}}
className='w-full grow'
>
{t("server.connect_button")}
</Button>
<JellyfinServerDiscovery
onServerSelect={async (server) => {
setServerURL(server.address);
if (server.serverName) {
setServerName(server.serverName);
}
await handleConnect(server.address);
}}
/>
<PreviousServersList
onServerSelect={async (s) => {
await handleConnect(s.address);
}}
/>
</View>
</View>
)} )}
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>

View File

@@ -1,6 +1,6 @@
import type { StreamyfinPluginConfig } from "@/utils/atoms/settings"; import { Api, AUTHORIZATION_HEADER } from "@jellyfin/sdk";
import { AUTHORIZATION_HEADER, Api } from "@jellyfin/sdk";
import type { AxiosRequestConfig, AxiosResponse } from "axios"; import type { AxiosRequestConfig, AxiosResponse } from "axios";
import type { StreamyfinPluginConfig } from "@/utils/atoms/settings";
declare module "@jellyfin/sdk" { declare module "@jellyfin/sdk" {
interface Api { interface Api {

1483
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import { RoundButton } from "@/components/RoundButton";
import { useFavorite } from "@/hooks/useFavorite";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { FC } from "react"; import type { FC } from "react";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { RoundButton } from "@/components/RoundButton";
import { useFavorite } from "@/hooks/useFavorite";
interface Props extends ViewProps { interface Props extends ViewProps {
item: BaseItemDto; item: BaseItemDto;

View File

@@ -1,7 +1,9 @@
import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"; import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react"; import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
const DropdownMenu = require("zeego/dropdown-menu");
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
@@ -17,7 +19,6 @@ export const AudioTrackSelector: React.FC<Props> = ({
selected, selected,
...props ...props
}) => { }) => {
if (Platform.isTV) return null;
const audioStreams = useMemo( const audioStreams = useMemo(
() => source?.MediaStreams?.filter((x) => x.Type === "Audio"), () => source?.MediaStreams?.filter((x) => x.Type === "Audio"),
[source], [source],

View File

@@ -1,5 +1,7 @@
import { Platform, TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
const DropdownMenu = require("zeego/dropdown-menu");
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
@@ -58,7 +60,6 @@ export const BitrateSelector: React.FC<Props> = ({
inverted, inverted,
...props ...props
}) => { }) => {
if (Platform.isTV) return null;
const sorted = useMemo(() => { const sorted = useMemo(() => {
if (inverted) if (inverted)
return BITRATES.sort( return BITRATES.sort(

View File

@@ -1,7 +1,7 @@
import { useHaptic } from "@/hooks/useHaptic";
import type React from "react"; import type React from "react";
import { type PropsWithChildren, type ReactNode, useMemo } from "react"; import { type PropsWithChildren, type ReactNode, useMemo } from "react";
import { Platform, Text, TouchableOpacity, View } from "react-native"; import { Text, TouchableOpacity, View } from "react-native";
import { useHaptic } from "@/hooks/useHaptic";
import { Loader } from "./Loader"; import { Loader } from "./Loader";
export interface ButtonProps export interface ButtonProps

View File

@@ -1,6 +1,6 @@
import { Feather } from "@expo/vector-icons"; import { Feather } from "@expo/vector-icons";
import React, { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import { Platform, TouchableOpacity, type ViewProps } from "react-native"; import { Platform, type ViewProps } from "react-native";
import GoogleCast, { import GoogleCast, {
CastButton, CastButton,
CastContext, CastContext,

View File

@@ -1,11 +1,11 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useMemo } from "react";
import type React from "react"; import type React from "react";
import { useMemo } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider";
import { WatchedIndicator } from "./WatchedIndicator"; import { WatchedIndicator } from "./WatchedIndicator";
type ContinueWatchingPosterProps = { type ContinueWatchingPosterProps = {

View File

@@ -1,11 +1,3 @@
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueActions, queueAtom } from "@/utils/atoms/queue";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server";
import download from "@/utils/profiles/download";
import Ionicons from "@expo/vector-icons/Ionicons"; import Ionicons from "@expo/vector-icons/Ionicons";
import { import {
BottomSheetBackdrop, BottomSheetBackdrop,
@@ -24,15 +16,23 @@ import type React from "react";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
import { Alert, Platform, View, type ViewProps } from "react-native"; import { Alert, Platform, View, type ViewProps } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueAtom } from "@/utils/atoms/queue";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server";
import download from "@/utils/profiles/download";
import { AudioTrackSelector } from "./AudioTrackSelector"; import { AudioTrackSelector } from "./AudioTrackSelector";
import { type Bitrate, BitrateSelector } from "./BitrateSelector"; import { type Bitrate, BitrateSelector } from "./BitrateSelector";
import { Button } from "./Button"; import { Button } from "./Button";
import { Text } from "./common/Text";
import { Loader } from "./Loader"; import { Loader } from "./Loader";
import { MediaSourceSelector } from "./MediaSourceSelector"; import { MediaSourceSelector } from "./MediaSourceSelector";
import ProgressCircle from "./ProgressCircle"; import ProgressCircle from "./ProgressCircle";
import { RoundButton } from "./RoundButton"; import { RoundButton } from "./RoundButton";
import { SubtitleTrackSelector } from "./SubtitleTrackSelector"; import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
import { Text } from "./common/Text";
interface DownloadProps extends ViewProps { interface DownloadProps extends ViewProps {
items: BaseItemDto[]; items: BaseItemDto[];
@@ -88,7 +88,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
bottomSheetModalRef.current?.present(); bottomSheetModalRef.current?.present();
}, []); }, []);
const handleSheetChanges = useCallback((index: number) => {}, []); const handleSheetChanges = useCallback((_index: number) => {}, []);
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
bottomSheetModalRef.current?.dismiss(); bottomSheetModalRef.current?.dismiss();

View File

@@ -1,4 +1,3 @@
import { tc } from "@/utils/textTools";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type React from "react"; import type React from "react";
import { View } from "react-native"; import { View } from "react-native";

View File

@@ -1,25 +1,3 @@
import { AudioTrackSelector } from "@/components/AudioTrackSelector";
import { type Bitrate, BitrateSelector } from "@/components/BitrateSelector";
import { DownloadSingleItem } from "@/components/DownloadItem";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
// const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null;
import { PlayButton } from "@/components/PlayButton";
import { PlayedStatus } from "@/components/PlayedStatus";
import { SimilarItems } from "@/components/SimilarItems";
import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector";
import { ItemImage } from "@/components/common/ItemImage";
import { CastAndCrew } from "@/components/series/CastAndCrew";
import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColors } from "@/hooks/useImageColors";
import { useOrientation } from "@/hooks/useOrientation";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom } from "@/providers/JellyfinProvider";
import { userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import type { import type {
BaseItemDto, BaseItemDto,
MediaSourceInfo, MediaSourceInfo,
@@ -28,15 +6,37 @@ import { Image } from "expo-image";
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { Platform, View } from "react-native"; import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AudioTrackSelector } from "@/components/AudioTrackSelector";
import { type Bitrate, BitrateSelector } from "@/components/BitrateSelector";
import { ItemImage } from "@/components/common/ItemImage";
import { DownloadSingleItem } from "@/components/DownloadItem";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
// const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null;
import { PlayButton } from "@/components/PlayButton";
import { PlayedStatus } from "@/components/PlayedStatus";
import { SimilarItems } from "@/components/SimilarItems";
import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector";
import { CastAndCrew } from "@/components/series/CastAndCrew";
import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColors } from "@/hooks/useImageColors";
import { useOrientation } from "@/hooks/useOrientation";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { AddToFavorites } from "./AddToFavorites"; import { AddToFavorites } from "./AddToFavorites";
import { ItemHeader } from "./ItemHeader"; import { ItemHeader } from "./ItemHeader";
import { ItemTechnicalDetails } from "./ItemTechnicalDetails"; import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
import { MediaSourceSelector } from "./MediaSourceSelector"; import { MediaSourceSelector } from "./MediaSourceSelector";
import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
import { PlayInRemoteSessionButton } from "./PlayInRemoteSession"; import { PlayInRemoteSessionButton } from "./PlayInRemoteSession";
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
const Chromecast = require("./Chromecast");
export type SelectedOptions = { export type SelectedOptions = {
bitrate: Bitrate; bitrate: Bitrate;
@@ -85,35 +85,27 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
defaultMediaSource, defaultMediaSource,
]); ]);
if (!Platform.isTV) { useEffect(() => {
useEffect(() => { navigation.setOptions({
navigation.setOptions({ headerRight: () =>
headerRight: () => item && (
item && ( <View className='flex flex-row items-center space-x-2'>
<View className='flex flex-row items-center space-x-2'> <Chromecast.Chromecast background='blur' width={22} height={22} />
<Chromecast.Chromecast {item.Type !== "Program" && (
background='blur' <View className='flex flex-row items-center space-x-2'>
width={22} <DownloadSingleItem item={item} size='large' />
height={22} {user?.Policy?.IsAdministrator && (
/> <PlayInRemoteSessionButton item={item} size='large' />
{item.Type !== "Program" && ( )}
<View className='flex flex-row items-center space-x-2'>
{!Platform.isTV && (
<DownloadSingleItem item={item} size='large' />
)}
{user?.Policy?.IsAdministrator && (
<PlayInRemoteSessionButton item={item} size='large' />
)}
<PlayedStatus items={[item]} size='large' /> <PlayedStatus items={[item]} size='large' />
<AddToFavorites item={item} /> <AddToFavorites item={item} />
</View> </View>
)} )}
</View> </View>
), ),
}); });
}, [item]); }, [item]);
}
useEffect(() => { useEffect(() => {
if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP) if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP)
@@ -174,7 +166,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
<View className='flex flex-col bg-transparent shrink'> <View className='flex flex-col bg-transparent shrink'>
<View className='flex flex-col px-4 w-full space-y-2 pt-2 mb-2 shrink'> <View className='flex flex-col px-4 w-full space-y-2 pt-2 mb-2 shrink'>
<ItemHeader item={item} className='mb-4' /> <ItemHeader item={item} className='mb-4' />
{item.Type !== "Program" && !Platform.isTV && ( {item.Type !== "Program" && (
<View className='flex flex-row items-center justify-start w-full h-16'> <View className='flex flex-row items-center justify-start w-full h-16'>
<BitrateSelector <BitrateSelector
className='mr-1' className='mr-1'

View File

@@ -2,8 +2,8 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type React from "react"; import type React from "react";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { GenreTags } from "./GenreTags"; import { GenreTags } from "./GenreTags";
import { Ratings } from "./Ratings";
import { MoviesTitleHeader } from "./movies/MoviesTitleHeader"; import { MoviesTitleHeader } from "./movies/MoviesTitleHeader";
import { Ratings } from "./Ratings";
import { EpisodeTitleHeader } from "./series/EpisodeTitleHeader"; import { EpisodeTitleHeader } from "./series/EpisodeTitleHeader";
import { ItemActions } from "./series/SeriesActions"; import { ItemActions } from "./series/SeriesActions";

View File

@@ -1,11 +1,9 @@
import { formatBitrate } from "@/utils/bitrate";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import {
BottomSheetBackdrop, BottomSheetBackdrop,
type BottomSheetBackdropProps, type BottomSheetBackdropProps,
BottomSheetModal, BottomSheetModal,
BottomSheetScrollView, BottomSheetScrollView,
BottomSheetView,
} from "@gorhom/bottom-sheet"; } from "@gorhom/bottom-sheet";
import type { import type {
MediaSourceInfo, MediaSourceInfo,
@@ -15,8 +13,8 @@ import type React from "react";
import { useMemo, useRef } from "react"; import { useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import { formatBitrate } from "@/utils/bitrate";
import { Badge } from "./Badge"; import { Badge } from "./Badge";
import { Button } from "./Button";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
interface Props { interface Props {
@@ -103,7 +101,7 @@ const SubtitleStreamInfo = ({
}) => { }) => {
return ( return (
<View className='flex flex-col'> <View className='flex flex-col'>
{subtitleStreams.map((stream, index) => ( {subtitleStreams.map((stream, _index) => (
<View key={stream.Index} className='flex flex-col'> <View key={stream.Index} className='flex flex-col'>
<Text className='text-xs mb-3 text-neutral-400'> <Text className='text-xs mb-3 text-neutral-400'>
{stream.DisplayTitle} {stream.DisplayTitle}

View File

@@ -1,7 +1,7 @@
import { useJellyfinDiscovery } from "@/hooks/useJellyfinDiscovery";
import type React from "react"; import type React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Text, TouchableOpacity, View } from "react-native"; import { Text, View } from "react-native";
import { useJellyfinDiscovery } from "@/hooks/useJellyfinDiscovery";
import { Button } from "./Button"; import { Button } from "./Button";
import { ListGroup } from "./list/ListGroup"; import { ListGroup } from "./list/ListGroup";
import { ListItem } from "./list/ListItem"; import { ListItem } from "./list/ListItem";

View File

@@ -2,7 +2,6 @@ import {
ActivityIndicator, ActivityIndicator,
type ActivityIndicatorProps, type ActivityIndicatorProps,
Platform, Platform,
View,
} from "react-native"; } from "react-native";
interface Props extends ActivityIndicatorProps {} interface Props extends ActivityIndicatorProps {}

View File

@@ -4,7 +4,9 @@ import type {
} from "@jellyfin/sdk/lib/generated-client/models"; } from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react"; import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native"; import { Platform, TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Text } from "./common/Text"; import { Text } from "./common/Text";

View File

@@ -1,10 +1,3 @@
import { ItemCardText } from "@/components/ItemCardText";
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@@ -12,6 +5,13 @@ import { useAtom } from "jotai";
import type React from "react"; import type React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { ItemCardText } from "@/components/ItemCardText";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
interface Props extends ViewProps { interface Props extends ViewProps {
actorId: string; actorId: string;

View File

@@ -1,8 +1,8 @@
import { Text } from "@/components/common/Text";
import { tc } from "@/utils/textTools";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TouchableOpacity, View, type ViewProps } from "react-native"; import { TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { tc } from "@/utils/textTools";
interface Props extends ViewProps { interface Props extends ViewProps {
text?: string | null; text?: string | null;

View File

@@ -1,11 +1,6 @@
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import type { PropsWithChildren, ReactElement } from "react"; import type { PropsWithChildren, ReactElement } from "react";
import { import { type NativeScrollEvent, View, type ViewProps } from "react-native";
type NativeScrollEvent,
NativeSyntheticEvent,
View,
type ViewProps,
} from "react-native";
import Animated, { import Animated, {
interpolate, interpolate,
useAnimatedRef, useAnimatedRef,

View File

@@ -1,6 +1,7 @@
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import type React from "react"; import type React from "react";
import { Platform, View, type ViewProps } from "react-native"; import { Platform, View, type ViewProps } from "react-native";
interface Props extends ViewProps { interface Props extends ViewProps {
blurAmount?: number; blurAmount?: number;
blurType?: "light" | "dark" | "xlight"; blurType?: "light" | "dark" | "xlight";

View File

@@ -1,13 +1,3 @@
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings";
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { chromecast } from "@/utils/profiles/chromecast";
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
import { runtimeTicksToMinutes } from "@/utils/time";
import { useActionSheet } from "@expo/react-native-action-sheet"; import { useActionSheet } from "@expo/react-native-action-sheet";
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
@@ -15,7 +5,6 @@ import { useRouter } from "expo-router";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, Pressable } from "react-native";
import { Alert, TouchableOpacity, View } from "react-native"; import { Alert, TouchableOpacity, View } from "react-native";
import CastContext, { import CastContext, {
CastButton, CastButton,
@@ -33,6 +22,16 @@ import Animated, {
useSharedValue, useSharedValue,
withTiming, withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings";
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { chromecast } from "@/utils/profiles/chromecast";
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
import { runtimeTicksToMinutes } from "@/utils/time";
import type { Button } from "./Button"; import type { Button } from "./Button";
import type { SelectedOptions } from "./ItemContent"; import type { SelectedOptions } from "./ItemContent";

View File

@@ -1,226 +0,0 @@
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings";
import { runtimeTicksToMinutes } from "@/utils/time";
import { useActionSheet } from "@expo/react-native-action-sheet";
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useRouter } from "expo-router";
import { useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { Alert, TouchableOpacity, View } from "react-native";
import Animated, {
Easing,
interpolate,
interpolateColor,
useAnimatedReaction,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import type { Button } from "./Button";
import type { SelectedOptions } from "./ItemContent";
interface Props extends React.ComponentProps<typeof Button> {
item: BaseItemDto;
selectedOptions: SelectedOptions;
}
const ANIMATION_DURATION = 500;
const MIN_PLAYBACK_WIDTH = 15;
export const PlayButton: React.FC<Props> = ({
item,
selectedOptions,
...props
}: Props) => {
const { showActionSheetWithOptions } = useActionSheet();
const { t } = useTranslation();
const [colorAtom] = useAtom(itemThemeColorAtom);
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const router = useRouter();
const startWidth = useSharedValue(0);
const targetWidth = useSharedValue(0);
const endColor = useSharedValue(colorAtom);
const startColor = useSharedValue(colorAtom);
const widthProgress = useSharedValue(0);
const colorChangeProgress = useSharedValue(0);
const [settings] = useSettings();
const lightHapticFeedback = useHaptic("light");
const goToPlayer = useCallback(
(q: string) => {
router.push(`/player/direct-player?${q}`);
},
[router],
);
const onPress = () => {
console.log("onpress");
if (!item) return;
lightHapticFeedback();
const queryParams = new URLSearchParams({
itemId: item.Id!,
audioIndex: selectedOptions.audioIndex?.toString() ?? "",
subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "",
mediaSourceId: selectedOptions.mediaSource?.Id ?? "",
bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "",
});
const queryString = queryParams.toString();
goToPlayer(queryString);
return;
};
const derivedTargetWidth = useDerivedValue(() => {
if (!item || !item.RunTimeTicks) return 0;
const userData = item.UserData;
if (userData?.PlaybackPositionTicks) {
return userData.PlaybackPositionTicks > 0
? Math.max(
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
MIN_PLAYBACK_WIDTH,
)
: 0;
}
return 0;
}, [item]);
useAnimatedReaction(
() => derivedTargetWidth.value,
(newWidth) => {
targetWidth.value = newWidth;
widthProgress.value = 0;
widthProgress.value = withTiming(1, {
duration: ANIMATION_DURATION,
easing: Easing.bezier(0.7, 0, 0.3, 1.0),
});
},
[item],
);
useAnimatedReaction(
() => colorAtom,
(newColor) => {
endColor.value = newColor;
colorChangeProgress.value = 0;
colorChangeProgress.value = withTiming(1, {
duration: ANIMATION_DURATION,
easing: Easing.bezier(0.9, 0, 0.31, 0.99),
});
},
[colorAtom],
);
useEffect(() => {
const timeout_2 = setTimeout(() => {
startColor.value = colorAtom;
startWidth.value = targetWidth.value;
}, ANIMATION_DURATION);
return () => {
clearTimeout(timeout_2);
};
}, [colorAtom, item]);
/**
* ANIMATED STYLES
*/
const animatedAverageStyle = useAnimatedStyle(() => ({
backgroundColor: interpolateColor(
colorChangeProgress.value,
[0, 1],
[startColor.value.primary, endColor.value.primary],
),
}));
const animatedPrimaryStyle = useAnimatedStyle(() => ({
backgroundColor: interpolateColor(
colorChangeProgress.value,
[0, 1],
[startColor.value.primary, endColor.value.primary],
),
}));
const animatedWidthStyle = useAnimatedStyle(() => ({
width: `${interpolate(
widthProgress.value,
[0, 1],
[startWidth.value, targetWidth.value],
)}%`,
}));
const animatedTextStyle = useAnimatedStyle(() => ({
color: interpolateColor(
colorChangeProgress.value,
[0, 1],
[startColor.value.text, endColor.value.text],
),
}));
/**
* *********************
*/
return (
<TouchableOpacity
accessibilityLabel='Play button'
accessibilityHint='Tap to play the media'
onPress={onPress}
className={"relative"}
{...props}
>
<View className='absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden'>
<Animated.View
style={[
animatedPrimaryStyle,
animatedWidthStyle,
{
height: "100%",
},
]}
/>
</View>
<Animated.View
style={[animatedAverageStyle, { opacity: 0.5 }]}
className='absolute w-full h-full top-0 left-0 rounded-xl'
/>
<View
style={{
borderWidth: 1,
borderColor: colorAtom.primary,
borderStyle: "solid",
}}
className='flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full '
>
<View className='flex flex-row items-center space-x-2'>
<Animated.Text style={[animatedTextStyle, { fontWeight: "bold" }]}>
{runtimeTicksToMinutes(item?.RunTimeTicks)}
</Animated.Text>
<Animated.Text style={animatedTextStyle}>
<Ionicons name='play-circle' size={24} />
</Animated.Text>
{settings?.openInVLC && (
<Animated.Text style={animatedTextStyle}>
<MaterialCommunityIcons
name='vlc'
size={18}
color={animatedTextStyle.color}
/>
</Animated.Text>
)}
</View>
</View>
</TouchableOpacity>
);
};

View File

@@ -1,5 +1,3 @@
import { useAllSessions, type useSessionsProps } from "@/hooks/useSessions";
import { apiAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import {
type BaseItemDto, type BaseItemDto,
@@ -15,9 +13,11 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
} from "react-native"; } from "react-native";
import { useAllSessions, type useSessionsProps } from "@/hooks/useSessions";
import { apiAtom } from "@/providers/JellyfinProvider";
import { Text } from "./common/Text";
import { Loader } from "./Loader"; import { Loader } from "./Loader";
import { RoundButton } from "./RoundButton"; import { RoundButton } from "./RoundButton";
import { Text } from "./common/Text";
interface Props extends React.ComponentProps<typeof View> { interface Props extends React.ComponentProps<typeof View> {
item: BaseItemDto; item: BaseItemDto;

View File

@@ -1,8 +1,8 @@
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import type React from "react"; import type React from "react";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
import { RoundButton } from "./RoundButton"; import { RoundButton } from "./RoundButton";
interface Props extends ViewProps { interface Props extends ViewProps {
@@ -13,7 +13,7 @@ interface Props extends ViewProps {
export const PlayedStatus: React.FC<Props> = ({ items, ...props }) => { export const PlayedStatus: React.FC<Props> = ({ items, ...props }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const invalidateQueries = () => { const _invalidateQueries = () => {
items.forEach((item) => { items.forEach((item) => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["item", item.Id], queryKey: ["item", item.Id],

View File

@@ -1,5 +1,4 @@
import type React from "react"; import type React from "react";
import { StyleSheet, View } from "react-native";
import { AnimatedCircularProgress } from "react-native-circular-progress"; import { AnimatedCircularProgress } from "react-native-circular-progress";
type ProgressCircleProps = { type ProgressCircleProps = {

View File

@@ -1,3 +1,9 @@
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useMemo } from "react";
import { View, type ViewProps } from "react-native";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
@@ -6,12 +12,6 @@ import type {
TvResult, TvResult,
} from "@/utils/jellyseerr/server/models/Search"; } from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useMemo } from "react";
import { View, type ViewProps } from "react-native";
import { Badge } from "./Badge"; import { Badge } from "./Badge";
interface Props extends ViewProps { interface Props extends ViewProps {

View File

@@ -1,4 +1,3 @@
import { useHaptic } from "@/hooks/useHaptic";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
@@ -7,6 +6,7 @@ import {
TouchableOpacity, TouchableOpacity,
type TouchableOpacityProps, type TouchableOpacityProps,
} from "react-native"; } from "react-native";
import { useHaptic } from "@/hooks/useHaptic";
interface Props extends TouchableOpacityProps { interface Props extends TouchableOpacityProps {
onPress?: () => void; onPress?: () => void;

View File

@@ -1,23 +1,16 @@
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { getLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { View, type ViewProps } from "react-native";
ScrollView, import MoviePoster from "@/components/posters/MoviePoster";
TouchableOpacity, import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
View,
type ViewProps,
} from "react-native";
import { ItemCardText } from "./ItemCardText";
import { Loader } from "./Loader";
import { HorizontalScroll } from "./common/HorrizontalScroll"; import { HorizontalScroll } from "./common/HorrizontalScroll";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { TouchableItemRouter } from "./common/TouchableItemRouter"; import { TouchableItemRouter } from "./common/TouchableItemRouter";
import { ItemCardText } from "./ItemCardText";
interface SimilarItemsProps extends ViewProps { interface SimilarItemsProps extends ViewProps {
itemId?: string | null; itemId?: string | null;

View File

@@ -1,8 +1,10 @@
import { tc } from "@/utils/textTools";
import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"; import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react"; import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native"; import { Platform, TouchableOpacity, View } from "react-native";
import { tc } from "@/utils/textTools";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Text } from "./common/Text"; import { Text } from "./common/Text";

View File

@@ -1,4 +1,3 @@
import * as React from "react";
import renderer from "react-test-renderer"; import renderer from "react-test-renderer";
import { ThemedText } from "../ThemedText"; import { ThemedText } from "../ThemedText";

View File

@@ -1,5 +1,5 @@
import { Text } from "@/components/common/Text";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
interface Props extends ViewProps {} interface Props extends ViewProps {}

View File

@@ -1,7 +1,6 @@
import { useMemo } from "react"; import { View, type ViewProps } from "react-native";
import { StyleSheet, View, type ViewProps } from "react-native";
const getItemStyle = (index: number, numColumns: number) => { const _getItemStyle = (index: number, numColumns: number) => {
const alignItems = (() => { const alignItems = (() => {
if (numColumns < 2 || index % numColumns === 0) return "flex-start"; if (numColumns < 2 || index % numColumns === 0) return "flex-start";
if ((index + 1) % numColumns === 0) return "flex-end"; if ((index + 1) % numColumns === 0) return "flex-end";

View File

@@ -1,13 +1,14 @@
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { Text } from "@/components/common/Text";
import DisabledSetting from "@/components/settings/DisabledSetting"; import {
import React, {
type PropsWithChildren, type PropsWithChildren,
type ReactNode, type ReactNode,
useEffect, useEffect,
useState, useState,
} from "react"; } from "react";
import { Platform, TouchableOpacity, View, type ViewProps } from "react-native"; import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import DisabledSetting from "@/components/settings/DisabledSetting";
interface Props<T> { interface Props<T> {
data: T[]; data: T[];
@@ -58,7 +59,7 @@ const Dropdown = <T,>({
</TouchableOpacity> </TouchableOpacity>
</View> </View>
) : ( ) : (
<>{title}</> title
)} )}
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content <DropdownMenu.Content
@@ -71,7 +72,7 @@ const Dropdown = <T,>({
sideOffset={0} sideOffset={0}
> >
<DropdownMenu.Label>{label}</DropdownMenu.Label> <DropdownMenu.Label>{label}</DropdownMenu.Label>
{data.map((item, idx) => {data.map((item, _idx) =>
multiple ? ( multiple ? (
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
value={ value={
@@ -80,7 +81,10 @@ const Dropdown = <T,>({
: "off" : "off"
} }
key={keyExtractor(item)} key={keyExtractor(item)}
onValueChange={(next: "on" | "off", previous: "on" | "off") => { onValueChange={(
next: "on" | "off",
_previous: "on" | "off",
) => {
setSelected((p) => { setSelected((p) => {
const prev = p || []; const prev = p || [];
if (next === "on") { if (next === "on") {

View File

@@ -1,4 +1,3 @@
import { Text } from "@/components/common/Text";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { BlurView, type BlurViewProps } from "expo-blur"; import { BlurView, type BlurViewProps } from "expo-blur";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
@@ -6,8 +5,6 @@ import {
Platform, Platform,
TouchableOpacity, TouchableOpacity,
type TouchableOpacityProps, type TouchableOpacityProps,
View,
ViewProps,
} from "react-native"; } from "react-native";
interface Props extends BlurViewProps { interface Props extends BlurViewProps {

View File

@@ -1,4 +1,3 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type { import type {
BaseItemDto, BaseItemDto,
BaseItemDtoQueryResult, BaseItemDtoQueryResult,
@@ -14,6 +13,7 @@ import Animated, {
useSharedValue, useSharedValue,
withTiming, withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Loader } from "../Loader"; import { Loader } from "../Loader";
import { Text } from "./Text"; import { Text } from "./Text";

View File

@@ -1,11 +1,11 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { getItemImage } from "@/utils/getItemImage";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image, type ImageProps } from "expo-image"; import { Image, type ImageProps } from "expo-image";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { type FC, useMemo } from "react"; import { type FC, useMemo } from "react";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getItemImage } from "@/utils/getItemImage";
interface Props extends ImageProps { interface Props extends ImageProps {
item: BaseItemDto; item: BaseItemDto;

View File

@@ -1,9 +1,13 @@
import { useRouter, useSegments } from "expo-router";
import type React from "react";
import { type PropsWithChildren, useCallback, useMemo } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import * as ContextMenu from "@/components/ContextMenu"; import * as ContextMenu from "@/components/ContextMenu";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import { import {
Permission,
hasPermission, hasPermission,
Permission,
} from "@/utils/jellyseerr/server/lib/permissions"; } from "@/utils/jellyseerr/server/lib/permissions";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { import type {
@@ -11,10 +15,6 @@ import type {
TvResult, TvResult,
} from "@/utils/jellyseerr/server/models/Search"; } from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { useRouter, useSegments } from "expo-router";
import type React from "react";
import { type PropsWithChildren, useCallback, useMemo } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
interface Props extends TouchableOpacityProps { interface Props extends TouchableOpacityProps {
result?: MovieResult | TvResult | MovieDetails | TvDetails; result?: MovieResult | TvResult | MovieDetails | TvDetails;

View File

@@ -1,6 +1,4 @@
import React from "react"; import { Platform, Text as RNText, type TextProps } from "react-native";
import { Platform, type TextProps } from "react-native";
import { Text as RNText } from "react-native";
import { UITextView } from "react-native-uitextview"; import { UITextView } from "react-native-uitextview";
export function Text( export function Text(
props: TextProps & { props: TextProps & {

View File

@@ -1,5 +1,3 @@
import { useFavorite } from "@/hooks/useFavorite";
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
import { useActionSheet } from "@expo/react-native-action-sheet"; import { useActionSheet } from "@expo/react-native-action-sheet";
import type { import type {
BaseItemDto, BaseItemDto,
@@ -8,6 +6,8 @@ import type {
import { useRouter, useSegments } from "expo-router"; import { useRouter, useSegments } from "expo-router";
import { type PropsWithChildren, useCallback } from "react"; import { type PropsWithChildren, useCallback } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import { useFavorite } from "@/hooks/useFavorite";
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
interface Props extends TouchableOpacityProps { interface Props extends TouchableOpacityProps {
item: BaseItemDto; item: BaseItemDto;
@@ -17,10 +17,6 @@ export const itemRouter = (
item: BaseItemDto | BaseItemPerson, item: BaseItemDto | BaseItemPerson,
from: string, from: string,
) => { ) => {
if ("CollectionType" in item && item.CollectionType === "livetv") {
return `/(auth)/(tabs)/${from}/livetv`;
}
if (item.Type === "Series") { if (item.Type === "Series") {
return `/(auth)/(tabs)/${from}/series/${item.Id}`; return `/(auth)/(tabs)/${from}/series/${item.Id}`;
} }

View File

@@ -1,4 +1,3 @@
import { Text } from "@/components/common/Text";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
interface Props extends ViewProps { interface Props extends ViewProps {

View File

@@ -1,9 +1,3 @@
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { storage } from "@/utils/mmkv";
import type { JobStatus } from "@/utils/optimize-server";
import { formatTimeString } from "@/utils/time";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Image } from "expo-image"; import { Image } from "expo-image";
@@ -19,7 +13,14 @@ import {
type ViewProps, type ViewProps,
} from "react-native"; } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { storage } from "@/utils/mmkv";
import type { JobStatus } from "@/utils/optimize-server";
import { formatTimeString } from "@/utils/time";
import { Button } from "../Button"; import { Button } from "../Button";
const BackGroundDownloader = !Platform.isTV const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader") ? require("@kesha-antonov/react-native-background-downloader")
: null; : null;

View File

@@ -1,9 +1,9 @@
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type React from "react"; import type React from "react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import type { TextProps } from "react-native"; import type { TextProps } from "react-native";
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
interface DownloadSizeProps extends TextProps { interface DownloadSizeProps extends TextProps {
items: BaseItemDto[]; items: BaseItemDto[];

View File

@@ -1,4 +1,3 @@
import { useHaptic } from "@/hooks/useHaptic";
import { import {
ActionSheetProvider, ActionSheetProvider,
useActionSheet, useActionSheet,
@@ -11,17 +10,14 @@ import {
type TouchableOpacityProps, type TouchableOpacityProps,
View, View,
} from "react-native"; } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { DownloadSize } from "@/components/downloads/DownloadSize"; import { DownloadSize } from "@/components/downloads/DownloadSize";
import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener"; import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener";
import { useHaptic } from "@/hooks/useHaptic";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
import { runtimeTicksToSeconds } from "@/utils/time"; import { runtimeTicksToSeconds } from "@/utils/time";
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import ContinueWatchingPoster from "../ContinueWatchingPoster"; import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
interface EpisodeCardProps extends TouchableOpacityProps { interface EpisodeCardProps extends TouchableOpacityProps {
item: BaseItemDto; item: BaseItemDto;
@@ -33,7 +29,7 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item, ...props }) => {
const { showActionSheetWithOptions } = useActionSheet(); const { showActionSheetWithOptions } = useActionSheet();
const successHapticFeedback = useHaptic("success"); const successHapticFeedback = useHaptic("success");
const base64Image = useMemo(() => { const _base64Image = useMemo(() => {
return storage.getString(item.Id!); return storage.getString(item.Id!);
}, [item]); }, [item]);

View File

@@ -1,19 +1,18 @@
import { useHaptic } from "@/hooks/useHaptic";
import { import {
ActionSheetProvider, ActionSheetProvider,
useActionSheet, useActionSheet,
} from "@expo/react-native-action-sheet"; } from "@expo/react-native-action-sheet";
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image";
import type React from "react"; import type React from "react";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import { DownloadSize } from "@/components/downloads/DownloadSize"; import { DownloadSize } from "@/components/downloads/DownloadSize";
import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener"; import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener";
import { useHaptic } from "@/hooks/useHaptic";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { ItemCardText } from "../ItemCardText"; import { ItemCardText } from "../ItemCardText";
interface MovieCardProps { interface MovieCardProps {

View File

@@ -1,6 +1,3 @@
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { useActionSheet } from "@expo/react-native-action-sheet"; import { useActionSheet } from "@expo/react-native-action-sheet";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
@@ -9,6 +6,9 @@ import { router } from "expo-router";
import type React from "react"; import type React from "react";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => { export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {

View File

@@ -1,8 +1,8 @@
import { Text } from "@/components/common/Text";
import { FontAwesome, Ionicons } from "@expo/vector-icons"; import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native"; import { TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { FilterSheet } from "./FilterSheet"; import { FilterSheet } from "./FilterSheet";
interface FilterButtonProps<T> extends ViewProps { interface FilterButtonProps<T> extends ViewProps {

View File

@@ -1,16 +1,12 @@
import { Ionicons } from "@expo/vector-icons";
import { import {
BottomSheetBackdrop, BottomSheetBackdrop,
type BottomSheetBackdropProps, type BottomSheetBackdropProps,
BottomSheetFlatList,
BottomSheetModal, BottomSheetModal,
BottomSheetScrollView, BottomSheetScrollView,
BottomSheetView,
} from "@gorhom/bottom-sheet"; } from "@gorhom/bottom-sheet";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Text } from "@/components/common/Text";
import { Ionicons } from "@expo/vector-icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
StyleSheet, StyleSheet,
@@ -18,6 +14,7 @@ import {
View, View,
type ViewProps, type ViewProps,
} from "react-native"; } from "react-native";
import { Text } from "@/components/common/Text";
import { Button } from "../Button"; import { Button } from "../Button";
import { Input } from "../common/Input"; import { Input } from "../common/Input";

View File

@@ -1,11 +1,11 @@
import { Ionicons } from "@expo/vector-icons";
import { useAtom } from "jotai";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import { import {
genreFilterAtom, genreFilterAtom,
tagsFilterAtom, tagsFilterAtom,
yearFilterAtom, yearFilterAtom,
} from "@/utils/atoms/filters"; } from "@/utils/atoms/filters";
import { Ionicons } from "@expo/vector-icons";
import { useAtom } from "jotai";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
interface Props extends TouchableOpacityProps {} interface Props extends TouchableOpacityProps {}

View File

@@ -1,5 +1,3 @@
import { Colors } from "@/constants/Colors";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import type { Api } from "@jellyfin/sdk"; import type { Api } from "@jellyfin/sdk";
import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client"; import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
@@ -7,10 +5,11 @@ import { t } from "i18next";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Image, Text, View } from "react-native"; import { Image, Text, View } from "react-native";
import { ScrollingCollectionList } from "./ScrollingCollectionList";
// PNG ASSET // PNG ASSET
import heart from "@/assets/icons/heart.fill.png"; import heart from "@/assets/icons/heart.fill.png";
import { Colors } from "@/constants/Colors";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { ScrollingCollectionList } from "./ScrollingCollectionList";
type FavoriteTypes = type FavoriteTypes =
| "Series" | "Series"

View File

@@ -1,8 +1,3 @@
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@@ -21,6 +16,11 @@ import Carousel, {
type ICarouselInstance, type ICarouselInstance,
Pagination, Pagination,
} from "react-native-reanimated-carousel"; } from "react-native-reanimated-carousel";
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { itemRouter } from "../common/TouchableItemRouter"; import { itemRouter } from "../common/TouchableItemRouter";
interface Props extends ViewProps {} interface Props extends ViewProps {}

View File

@@ -1,5 +1,3 @@
import { Text } from "@/components/common/Text";
import MoviePoster from "@/components/posters/MoviePoster";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { import {
type QueryFunction, type QueryFunction,
@@ -8,9 +6,11 @@ import {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ScrollView, View, type ViewProps } from "react-native"; import { ScrollView, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import MoviePoster from "@/components/posters/MoviePoster";
import ContinueWatchingPoster from "../ContinueWatchingPoster"; import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { ItemCardText } from "../ItemCardText";
import { TouchableItemRouter } from "../common/TouchableItemRouter"; import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { ItemCardText } from "../ItemCardText";
import SeriesPoster from "../posters/SeriesPoster"; import SeriesPoster from "../posters/SeriesPoster";
interface Props extends ViewProps { interface Props extends ViewProps {

View File

@@ -1,6 +1,6 @@
import { TouchableOpacity } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import DisabledSetting from "@/components/settings/DisabledSetting"; import DisabledSetting from "@/components/settings/DisabledSetting";
import { TouchableOpacity, View } from "react-native";
interface StepperProps { interface StepperProps {
value: number; value: number;

View File

@@ -1,11 +1,11 @@
import { Text } from "@/components/common/Text";
import PersonPoster from "@/components/jellyseerr/PersonPoster";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import type React from "react"; import type React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import PersonPoster from "@/components/jellyseerr/PersonPoster";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
const CastSlide: React.FC< const CastSlide: React.FC<
{ details?: MovieDetails | TvDetails } & ViewProps { details?: MovieDetails | TvDetails } & ViewProps

View File

@@ -1,15 +1,15 @@
import { Text } from "@/components/common/Text";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import type { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { uniqBy } from "lodash"; import { uniqBy } from "lodash";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import CountryFlag from "react-native-country-flag"; import CountryFlag from "react-native-country-flag";
import { Text } from "@/components/common/Text";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import type { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
interface Release { interface Release {
certification: string; certification: string;

View File

@@ -1,3 +1,13 @@
import { orderBy, uniqBy } from "lodash";
import type React from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import {
useAnimatedReaction,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import Discover from "@/components/jellyseerr/discover/Discover"; import Discover from "@/components/jellyseerr/discover/Discover";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import { MediaType } from "@/utils/jellyseerr/server/constants/media";
@@ -7,17 +17,6 @@ import type {
TvResult, TvResult,
} from "@/utils/jellyseerr/server/models/Search"; } from "@/utils/jellyseerr/server/models/Search";
import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery"; import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery";
import { orderBy, uniqBy } from "lodash";
import type React from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import {
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import JellyseerrPoster from "../posters/JellyseerrPoster"; import JellyseerrPoster from "../posters/JellyseerrPoster";
import { LoadingSkeleton } from "../search/LoadingSkeleton"; import { LoadingSkeleton } from "../search/LoadingSkeleton";

View File

@@ -1,7 +1,7 @@
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import { Feather, MaterialCommunityIcons } from "@expo/vector-icons"; import { Feather, MaterialCommunityIcons } from "@expo/vector-icons";
import { useMemo } from "react"; import { useMemo } from "react";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
const JellyseerrMediaIcon: React.FC< const JellyseerrMediaIcon: React.FC<
{ mediaType: "tv" | "movie" } & ViewProps { mediaType: "tv" | "movie" } & ViewProps

View File

@@ -1,7 +1,7 @@
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native"; import { TouchableOpacity, View, type ViewProps } from "react-native";
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
interface Props { interface Props {
mediaStatus?: MediaStatus; mediaStatus?: MediaStatus;

Some files were not shown because too many files have changed in this diff Show More