mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-16 08:08:18 +00:00
Compare commits
3 Commits
remove-opt
...
no-tv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83a264d5a1 | ||
|
|
2d434a0125 | ||
|
|
0d7edca1ad |
22
.claude/settings.local.json
Normal file
22
.claude/settings.local.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
4
app.json
4
app.json
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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={() =>
|
||||||
|
|||||||
@@ -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} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
252
app/login.tsx
252
app/login.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 & {
|
||||||
|
|||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user