Merge branch 'develop' into fix/vlc4

This commit is contained in:
Gauvain
2025-07-21 14:10:33 +02:00
committed by GitHub
210 changed files with 4785 additions and 2011 deletions

View File

@@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals"]
}

View File

@@ -43,7 +43,7 @@ body:
label: Version
description: What version of Streamyfin are you running?
options:
- 0.28.1
- 0.29.0
- 0.28.0
- 0.27.0
- 0.26.1

View File

@@ -22,7 +22,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3
- uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
@@ -36,7 +36,7 @@ jobs:
```
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
with:
header: pr-title-lint-error
delete: true

1
.gitignore vendored
View File

@@ -10,7 +10,6 @@ npm-debug.*
*.orig.*
web-build/
modules/vlc-player/android/build
modules/vlc-player/android/.gradle
# macOS
.DS_Store

View File

@@ -107,13 +107,13 @@ Key points of the MPL-2.0:
- You must disclose your source code for any modifications to the covered files
- Larger works may combine MPL code with code under other licenses
- MPL-licensed components must remain under the MPL, but the larger work can be under a different license
- For the full text of the license, please see the LICENSE file in this repository.
- For the full text of the license, please see the LICENSE file in this repository
## 🌐 Connect with Us
Join our Discord: [https://discord.gg/aJvAYeycyY](https://discord.gg/aJvAYeycyY)
If you have questions or need support, feel free to reach out:
Need support or have questions:
- GitHub Issues: Report bugs or request features here.
- Email: [fredrik.burmester@gmail.com](mailto:fredrik.burmester@gmail.com)
@@ -139,77 +139,74 @@ Special shoutout to the JF official clients for being an inspiration to ours.
Thanks to the following contributors for their significant contributions:
<div align="left">
<table>
<tr
style="
display: flex;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
"
>
<tr>
<td align="center">
<a href="https://github.com/Alexk2309">
<img src="https://github.com/Alexk2309.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/Alexk2309.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@Alexk2309</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/herrrta">
<img src="https://github.com/herrrta.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/herrrta.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@herrrta</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lostb1t">
<img src="https://github.com/lostb1t.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/lostb1t.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@lostb1t</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Simon-Eklundh">
<img src="https://github.com/Simon-Eklundh.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/Simon-Eklundh.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@Simon-Eklundh</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/topiga">
<img src="https://github.com/topiga.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/topiga.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@topiga</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://github.com/simoncaron">
<img src="https://github.com/simoncaron.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/simoncaron.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@simoncaron</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jakequade">
<img src="https://github.com/jakequade.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/jakequade.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@jakequade</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Ryan0204">
<img src="https://github.com/Ryan0204.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/Ryan0204.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@Ryan0204</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/retardgerman">
<img src="https://github.com/retardgerman.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/retardgerman.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@retardgerman</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/whoopsi-daisy">
<img src="https://github.com/whoopsi-daisy.png?size=80" width="80" style="border-radius: 50%;" />
<img src="https://github.com/whoopsi-daisy.png?size=55" width="55" style="border-radius: 50%;" />
<br /><sub><b>@whoopsi-daisy</b></sub>
</a>
</td>
</tr>
</table>
</div>
And all other developers who have contributed to Streamyfin, thank you for your contributions.
@@ -228,4 +225,4 @@ I'd also like to thank the following people and projects for their contributions
Streamyfin does not promote, support, or condone piracy in any form. The app is intended solely for streaming media that you personally own and control. It does not provide or include any media content. Any discussions or support requests related to piracy are strictly prohibited across all our channels.
## 🤝 Sponsorship
VPS hosting generously provided by [Hexabyte](https://hexabyte.se/en/vps/?currency=eur)
VPS hosting generously provided by [Hexabyte](https://hexabyte.se/en/vps/?currency=eur) and [SweHosting](https://swehosting.se/en/#tj%C3%A4nster)

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.28.1",
"version": "0.29.1",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
@@ -113,7 +113,6 @@
}
}
],
["react-native-bottom-tabs"],
["./plugins/withChangeNativeAndroidTextToWhite.js"],
["./plugins/withAndroidManifest.js"],
["./plugins/withTrustLocalCerts.js"],

View File

@@ -1,13 +1,12 @@
import Ionicons from "@expo/vector-icons/Ionicons";
import { useAtom } from "jotai/index";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { ListItem } from "@/components/list/ListItem";
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;

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,8 @@
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
import { Text } from "@/components/common/Text";
import { EpisodeCard } from "@/components/downloads/EpisodeCard";
import {
@@ -6,11 +11,6 @@ import {
} from "@/components/series/SeasonDropdown";
import { useDownload } from "@/providers/DownloadProvider";
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() {
const navigation = useNavigation();

View File

@@ -1,3 +1,17 @@
import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import { useNavigation, useRouter } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { toast } from "sonner-native";
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { ActiveDownloads } from "@/components/downloads/ActiveDownloads";
@@ -8,36 +22,47 @@ 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 {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import { useNavigation, useRouter } from "expo-router";
import { t } from "i18next";
import { useAtom } from "jotai";
import React, { useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { toast } from "sonner-native";
export default function page() {
const navigation = useNavigation();
const { t } = useTranslation();
const [queue, setQueue] = useAtom(queueAtom);
const { removeProcess, downloadedFiles, deleteFileByType } = useDownload();
const { removeProcess, downloadedFiles, deleteFileByType, deleteAllFiles } =
useDownload();
const router = useRouter();
const [settings] = useSettings();
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const [showMigration, setShowMigration] = useState(false);
const insets = useSafeAreaInsets();
const migration_20241124 = () => {
Alert.alert(
t("home.downloads.new_app_version_requires_re_download"),
t("home.downloads.new_app_version_requires_re_download_description"),
[
{
text: t("home.downloads.back"),
onPress: () => setShowMigration(false) || router.back(),
},
{
text: t("home.downloads.delete"),
style: "destructive",
onPress: async () => {
await deleteAllFiles();
setShowMigration(false);
},
},
],
);
};
const movies = useMemo(() => {
try {
return downloadedFiles?.filter((f) => f.item.Type === "Movie") || [];
} catch {
migration_20241124();
setShowMigration(true);
return [];
}
}, [downloadedFiles]);
@@ -54,13 +79,11 @@ export default function page() {
});
return Object.values(series);
} catch {
migration_20241124();
setShowMigration(true);
return [];
}
}, [downloadedFiles]);
const insets = useSafeAreaInsets();
useEffect(() => {
navigation.setOptions({
headerRight: () => (
@@ -71,6 +94,12 @@ export default function page() {
});
}, [downloadedFiles]);
useEffect(() => {
if (showMigration) {
migration_20241124();
}
}, [showMigration]);
const deleteMovies = () =>
deleteFileByType("Movie")
.then(() =>
@@ -249,23 +278,3 @@ export default function page() {
</>
);
}
function migration_20241124() {
const router = useRouter();
const { deleteAllFiles } = useDownload();
Alert.alert(
t("home.downloads.new_app_version_requires_re_download"),
t("home.downloads.new_app_version_requires_re_download_description"),
[
{
text: t("home.downloads.back"),
onPress: () => router.back(),
},
{
text: t("home.downloads.delete"),
style: "destructive",
onPress: async () => await deleteAllFiles(),
},
],
);
}

View File

@@ -1,12 +1,12 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { storage } from "@/utils/mmkv";
import { Feather, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { useFocusEffect, useRouter } from "expo-router";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
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() {
const router = useRouter();

View File

@@ -1,19 +1,4 @@
import { Badge } from "@/components/Badge";
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 { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import {
HardwareAccelerationType,
type SessionInfoDto,
@@ -26,10 +11,19 @@ import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import { FlashList } from "@shopify/flash-list";
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { get } from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
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() {
const { sessions, isLoading } = useSessions({} as useSessionsProps);
@@ -454,20 +448,18 @@ const TranscodingStreamView = ({
</Text>
</View>
{isTranscoding && transcodeProperties ? (
<>
<View className='flex flex-row'>
<Text className='-mt-0 text-xs opacity-50 w-20 font-bold text-right pr-4'>
<MaterialCommunityIcons
name='arrow-right-bottom'
size={14}
color='white'
/>
</Text>
<Text className='flex-1 text-sm mt-1'>
<TranscodingBadges properties={transcodeProperties} />
</Text>
</View>
</>
<View className='flex flex-row'>
<Text className='-mt-0 text-xs opacity-50 w-20 font-bold text-right pr-4'>
<MaterialCommunityIcons
name='arrow-right-bottom'
size={14}
color='white'
/>
</Text>
<Text className='flex-1 text-sm mt-1'>
<TranscodingBadges properties={transcodeProperties} />
</Text>
</View>
) : null}
</View>
);

View File

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

View File

@@ -1,15 +1,15 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { useTranslation } from "react-i18next";
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() {
const [settings, updateSettings, pluginSettings] = useSettings();

View File

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

View File

@@ -1,14 +1,14 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { FilterButton } from "@/components/filters/FilterButton";
import { LogLevel, useLog, writeErrorLog } from "@/utils/log";
import * as FileSystem from "expo-file-system";
import { useNavigation } from "expo-router";
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 { ScrollView, TouchableOpacity, View } from "react-native";
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() {
const navigation = useNavigation();

View File

@@ -1,13 +1,7 @@
import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem";
import { useSettings } from "@/utils/atoms/settings";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigation } from "expo-router";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import DisabledSetting from "@/components/settings/DisabledSetting";
import React, { useEffect, useMemo, useState } from "react";
import {
Linking,
Switch,
@@ -16,6 +10,11 @@ import {
View,
} from "react-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() {
const navigation = useNavigation();

View File

@@ -1,3 +1,10 @@
import { useMutation } from "@tanstack/react-query";
import { useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity } from "react-native";
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import DisabledSetting from "@/components/settings/DisabledSetting";
import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm";
@@ -5,13 +12,6 @@ import { apiAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getOrSetDeviceId } from "@/utils/device";
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() {
const navigation = useNavigation();

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
import { ItemContent } from "@/components/ItemContent";
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
@@ -15,6 +12,9 @@ import Animated, {
useSharedValue,
withTiming,
} 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 [api] = useAtom(apiAtom);

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ import type {
TabNavigationState,
} from "@react-navigation/native";
import { Stack, withLayoutContext } from "expo-router";
import React from "react";
const { Navigator } = createMaterialTopTabNavigator();

View File

@@ -1,18 +1,17 @@
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";
import { ItemImage } from "@/components/common/ItemImage";
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
export default function page() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
const _insets = useSafeAreaInsets();
const { data: channels } = useQuery({
queryKey: ["livetv", "channels"],

View File

@@ -1,23 +1,16 @@
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 React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Dimensions,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
import { Dimensions, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
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 { apiAtom, userAtom } from "@/providers/JellyfinProvider";
const HOUR_HEIGHT = 30;
const ITEMS_PER_PAGE = 20;
@@ -28,7 +21,7 @@ export default function page() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
const [date, setDate] = useState<Date>(new Date());
const [date, _setDate] = useState<Date>(new Date());
const [currentPage, setCurrentPage] = useState(1);
const { data: guideInfo } = useQuery({
@@ -150,7 +143,7 @@ export default function page() {
>
<View className='flex flex-col'>
<HourHeader height={HOUR_HEIGHT} />
{channels?.Items?.map((c, i) => (
{channels?.Items?.map((c, _i) => (
<MemoizedLiveTVGuideRow
channel={c}
programs={programs?.Items}

View File

@@ -1,13 +1,11 @@
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";
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
export default function page() {
const [api] = useAtom(apiAtom);

View File

@@ -1,7 +1,6 @@
import { Text } from "@/components/common/Text";
import React from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { Text } from "@/components/common/Text";
export default function page() {
const { t } = useTranslation();

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons";
import { Stack } from "expo-router";
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;
import { useTranslation } from "react-i18next";
export default function IndexLayout() {

View File

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

View File

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

View File

@@ -1,9 +1,30 @@
import type {
BaseItemDto,
BaseItemKind,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useDebounce } from "use-debounce";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { Tag } from "@/components/GenreTags";
import { ItemCardText } from "@/components/ItemCardText";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { Tag } from "@/components/GenreTags";
import { ItemCardText } from "@/components/ItemCardText";
import {
JellyseerrSearchSort,
JellyserrIndexPage,
@@ -16,27 +37,6 @@ import { useJellyseerr } from "@/hooks/useJellyseerr";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
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";
@@ -249,205 +249,203 @@ export default function search() {
}, [l1, l2, l3, l7, l8]);
return (
<>
<ScrollView
keyboardDismissMode='on-drag'
contentInsetAdjustmentBehavior='automatic'
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
<ScrollView
keyboardDismissMode='on-drag'
contentInsetAdjustmentBehavior='automatic'
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
}}
>
<View
className='flex flex-col'
style={{
marginTop: Platform.OS === "android" ? 16 : 0,
}}
>
<View
className='flex flex-col'
style={{
marginTop: Platform.OS === "android" ? 16 : 0,
}}
>
{jellyseerrApi && (
<ScrollView
horizontal
className='flex flex-row flex-wrap space-x-2 px-4 mb-2'
>
<TouchableOpacity onPress={() => setSearchType("Library")}>
<Tag
text={t("search.library")}
textClass='p-1'
className={
searchType === "Library" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSearchType("Discover")}>
<Tag
text={t("search.discover")}
textClass='p-1'
className={
searchType === "Discover" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
{searchType === "Discover" &&
!loading &&
noResults &&
debouncedSearch.length > 0 && (
<View className='flex flex-row justify-end items-center space-x-1'>
<FilterButton
id='search'
queryKey='jellyseerr_search'
queryFn={async () =>
Object.keys(JellyseerrSearchSort).filter((v) =>
Number.isNaN(Number(v)),
)
}
set={(value) => setJellyseerrOrderBy(value[0])}
values={[jellyseerrOrderBy]}
title={t("library.filters.sort_by")}
renderItemLabel={(item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`)
}
showSearch={false}
/>
<FilterButton
id='order'
queryKey='jellysearr_search'
queryFn={async () => ["asc", "desc"]}
set={(value) => setJellyseerrSortOrder(value[0])}
values={[jellyseerrSortOrder]}
title={t("library.filters.sort_order")}
renderItemLabel={(item) => t(`library.filters.${item}`)}
showSearch={false}
/>
</View>
)}
</ScrollView>
)}
{jellyseerrApi && (
<ScrollView
horizontal
className='flex flex-row flex-wrap space-x-2 px-4 mb-2'
>
<TouchableOpacity onPress={() => setSearchType("Library")}>
<Tag
text={t("search.library")}
textClass='p-1'
className={
searchType === "Library" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSearchType("Discover")}>
<Tag
text={t("search.discover")}
textClass='p-1'
className={
searchType === "Discover" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
{searchType === "Discover" &&
!loading &&
noResults &&
debouncedSearch.length > 0 && (
<View className='flex flex-row justify-end items-center space-x-1'>
<FilterButton
id='search'
queryKey='jellyseerr_search'
queryFn={async () =>
Object.keys(JellyseerrSearchSort).filter((v) =>
Number.isNaN(Number(v)),
)
}
set={(value) => setJellyseerrOrderBy(value[0])}
values={[jellyseerrOrderBy]}
title={t("library.filters.sort_by")}
renderItemLabel={(item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`)
}
showSearch={false}
/>
<FilterButton
id='order'
queryKey='jellysearr_search'
queryFn={async () => ["asc", "desc"]}
set={(value) => setJellyseerrSortOrder(value[0])}
values={[jellyseerrSortOrder]}
title={t("library.filters.sort_order")}
renderItemLabel={(item) => t(`library.filters.${item}`)}
showSearch={false}
/>
</View>
)}
</ScrollView>
)}
<View className='mt-2'>
<LoadingSkeleton isLoading={loading} />
</View>
{searchType === "Library" ? (
<View className={l1 || l2 ? "opacity-0" : "opacity-100"}>
<SearchItemWrapper
header={t("search.movies")}
items={movies}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
className='flex flex-col w-28 mr-2'
item={item}
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className='mt-2'>
{item.Name}
</Text>
<Text className='opacity-50 text-xs'>
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={series}
header={t("search.series")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
item={item}
className='flex flex-col w-28 mr-2'
>
<SeriesPoster item={item} key={item.Id} />
<Text numberOfLines={2} className='mt-2'>
{item.Name}
</Text>
<Text className='opacity-50 text-xs'>
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={episodes}
header={t("search.episodes")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
item={item}
key={item.Id}
className='flex flex-col w-44 mr-2'
>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={collections}
header={t("search.collections")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
item={item}
className='flex flex-col w-28 mr-2'
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className='mt-2'>
{item.Name}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={actors}
header={t("search.actors")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
item={item}
key={item.Id}
className='flex flex-col w-28 mr-2'
>
<MoviePoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
</View>
) : (
<JellyserrIndexPage
searchQuery={debouncedSearch}
sortType={jellyseerrOrderBy}
order={jellyseerrSortOrder}
/>
)}
{searchType === "Library" &&
(!loading && noResults && debouncedSearch.length > 0 ? (
<View>
<Text className='text-center text-lg font-bold mt-4'>
{t("search.no_results_found_for")}
</Text>
<Text className='text-xs text-purple-600 text-center'>
"{debouncedSearch}"
</Text>
</View>
) : debouncedSearch.length === 0 ? (
<View className='mt-4 flex flex-col items-center space-y-2'>
{exampleSearches.map((e) => (
<TouchableOpacity
onPress={() => {
setSearch(e);
searchBarRef.current?.setText(e);
}}
key={e}
className='mb-2'
>
<Text className='text-purple-600'>{e}</Text>
</TouchableOpacity>
))}
</View>
) : null)}
<View className='mt-2'>
<LoadingSkeleton isLoading={loading} />
</View>
</ScrollView>
</>
{searchType === "Library" ? (
<View className={l1 || l2 ? "opacity-0" : "opacity-100"}>
<SearchItemWrapper
header={t("search.movies")}
items={movies}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
className='flex flex-col w-28 mr-2'
item={item}
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className='mt-2'>
{item.Name}
</Text>
<Text className='opacity-50 text-xs'>
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={series}
header={t("search.series")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
item={item}
className='flex flex-col w-28 mr-2'
>
<SeriesPoster item={item} key={item.Id} />
<Text numberOfLines={2} className='mt-2'>
{item.Name}
</Text>
<Text className='opacity-50 text-xs'>
{item.ProductionYear}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={episodes}
header={t("search.episodes")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
item={item}
key={item.Id}
className='flex flex-col w-44 mr-2'
>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={collections}
header={t("search.collections")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
item={item}
className='flex flex-col w-28 mr-2'
>
<MoviePoster item={item} key={item.Id} />
<Text numberOfLines={2} className='mt-2'>
{item.Name}
</Text>
</TouchableItemRouter>
)}
/>
<SearchItemWrapper
items={actors}
header={t("search.actors")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
item={item}
key={item.Id}
className='flex flex-col w-28 mr-2'
>
<MoviePoster item={item} />
<ItemCardText item={item} />
</TouchableItemRouter>
)}
/>
</View>
) : (
<JellyserrIndexPage
searchQuery={debouncedSearch}
sortType={jellyseerrOrderBy}
order={jellyseerrSortOrder}
/>
)}
{searchType === "Library" &&
(!loading && noResults && debouncedSearch.length > 0 ? (
<View>
<Text className='text-center text-lg font-bold mt-4'>
{t("search.no_results_found_for")}
</Text>
<Text className='text-xs text-purple-600 text-center'>
"{debouncedSearch}"
</Text>
</View>
) : debouncedSearch.length === 0 ? (
<View className='mt-4 flex flex-col items-center space-y-2'>
{exampleSearches.map((e) => (
<TouchableOpacity
onPress={() => {
setSearch(e);
searchBarRef.current?.setText(e);
}}
key={e}
className='mb-2'
>
<Text className='text-purple-600'>{e}</Text>
</TouchableOpacity>
))}
</View>
) : null)}
</View>
</ScrollView>
);
}

View File

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

View File

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

View File

@@ -1,11 +1,15 @@
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 { DownloadProvider } from "@/providers/DownloadProvider";
import {
JellyfinProvider,
apiAtom,
getOrSetDeviceId,
getTokenFromStorage,
JellyfinProvider,
} from "@/providers/JellyfinProvider";
import { JobQueueProvider } from "@/providers/JobQueueProvider";
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
@@ -24,35 +28,37 @@ import {
} from "@/utils/log";
import { storage } from "@/utils/mmkv";
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
? require("@kesha-antonov/react-native-background-downloader")
: null;
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const BackgroundFetch = !Platform.isTV
? require("expo-background-fetch")
: null;
import * as Device from "expo-device";
import * as FileSystem from "expo-file-system";
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 ScreenOrientation from "@/packages/expo-screen-orientation";
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
import { getLocales } from "expo-localization";
import { Provider as JotaiProvider } from "jotai";
import { useEffect, useRef, useState } from "react";
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 { GestureHandlerRootView } from "react-native-gesture-handler";
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 type { EventSubscription } from "expo-modules-core";
import type {
@@ -62,6 +68,8 @@ import type {
import type { ExpoPushToken } from "expo-notifications/build/Tokens.types";
import { useAtom } from "jotai";
import { Toaster } from "sonner-native";
import { userAtom } from "@/providers/JellyfinProvider";
import { store } from "@/utils/store";
if (!Platform.isTV) {
Notifications.setNotificationHandler({
@@ -83,9 +91,9 @@ SplashScreen.setOptions({
});
function useNotificationObserver() {
if (Platform.isTV) return;
useEffect(() => {
if (Platform.isTV) return;
let isMounted = true;
function redirect(notification: typeof Notifications.Notification) {
@@ -138,14 +146,12 @@ if (!Platform.isTV) {
console.log("TaskManager ~ trigger");
const now = Date.now();
const settingsData = storage.getString("settings");
if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData;
const settings: Partial<Settings> = JSON.parse(settingsData);
const url = settings?.optimizedVersionsServerUrl;
if (!settings?.autoDownload || !url)
return BackgroundFetch.BackgroundFetchResult.NoData;
@@ -223,7 +229,6 @@ if (!Platform.isTV) {
}
console.log(`Auto download started: ${new Date(now).toISOString()}`);
// Be sure to return the successful result type!
return BackgroundFetch.BackgroundFetchResult.NewData;
});
@@ -301,51 +306,51 @@ function Layout() {
);
}, [settings?.preferedLanguage, i18n]);
if (!Platform.isTV) {
useNotificationObserver();
useNotificationObserver();
const [expoPushToken, setExpoPushToken] = useState<ExpoPushToken>();
const notificationListener = useRef<EventSubscription>();
const responseListener = useRef<EventSubscription>();
const [expoPushToken, setExpoPushToken] = useState<ExpoPushToken>();
const notificationListener = useRef<EventSubscription>();
const responseListener = useRef<EventSubscription>();
useEffect(() => {
if (expoPushToken && api && user) {
api
?.post("/Streamyfin/device", {
token: expoPushToken.data,
deviceId: getOrSetDeviceId(),
userId: user.Id,
})
.then((_) => console.log("Posted expo push token"))
.catch((_) =>
writeErrorLog("Failed to push expo push token to plugin"),
);
} else console.log("No token available");
}, [api, expoPushToken, user]);
useEffect(() => {
if (!Platform.isTV && expoPushToken && api && user) {
api
?.post("/Streamyfin/device", {
token: expoPushToken.data,
deviceId: getOrSetDeviceId(),
userId: user.Id,
})
.then((_) => console.log("Posted expo push token"))
.catch((_) =>
writeErrorLog("Failed to push expo push token to plugin"),
);
} else console.log("No token available");
}, [api, expoPushToken, user]);
async function registerNotifications() {
if (Platform.OS === "android") {
console.log("Setting android notification channel 'default'");
await Notifications?.setNotificationChannelAsync("default", {
name: "default",
});
}
await checkAndRequestPermissions();
if (!Platform.isTV && user && user.Policy?.IsAdministrator) {
await registerBackgroundFetchAsyncSessions();
}
// only create push token for real devices (pointless for emulators)
if (Device.isDevice) {
Notifications?.getExpoPushTokenAsync()
.then((token: ExpoPushToken) => token && setExpoPushToken(token))
.catch((reason: any) => console.log("Failed to get token", reason));
}
async function registerNotifications() {
if (Platform.OS === "android") {
console.log("Setting android notification channel 'default'");
await Notifications?.setNotificationChannelAsync("default", {
name: "default",
});
}
useEffect(() => {
await checkAndRequestPermissions();
if (!Platform.isTV && user && user.Policy?.IsAdministrator) {
await registerBackgroundFetchAsyncSessions();
}
// only create push token for real devices (pointless for emulators)
if (Device.isDevice) {
Notifications?.getExpoPushTokenAsync()
.then((token: ExpoPushToken) => token && setExpoPushToken(token))
.catch((reason: any) => console.log("Failed to get token", reason));
}
}
useEffect(() => {
if (!Platform.isTV) {
registerNotifications();
notificationListener.current =
@@ -363,12 +368,10 @@ function Layout() {
(response: NotificationResponse) => {
// Currently the notifications supported by the plugin will send data for deep links.
const { title, data } = response.notification.request.content;
writeDebugLog(
`Notification ${title} opened`,
response.notification.request.content,
);
if (data && Object.keys(data).length > 0) {
const type = data?.type?.toLower?.();
const itemId = data?.id;
@@ -381,12 +384,10 @@ function Layout() {
// We just clicked a notification for an individual episode.
if (itemId) {
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`);
}
// summarized season notification for multiple episodes. Bring them to series season
else {
// summarized season notification for multiple episodes. Bring them to series season
} else {
const seriesId = data.seriesId;
const seasonIndex = data.seasonIndex;
if (seasonIndex) {
router.push(
`/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`,
@@ -411,56 +412,57 @@ function Layout() {
responseListener.current,
);
};
}, []);
}
}, [user, api]);
useEffect(() => {
if (Platform.isTV) {
return;
useEffect(() => {
if (Platform.isTV) {
return;
}
if (segments.includes("direct-player" as never)) {
if (
!settings.followDeviceOrientation &&
settings.defaultVideoOrientation
) {
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
}
return;
}
if (segments.includes("direct-player" as never)) {
if (
!settings.followDeviceOrientation &&
settings.defaultVideoOrientation
) {
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
}
return;
}
if (settings.followDeviceOrientation === true) {
ScreenOrientation.unlockAsync();
} else {
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP,
);
}
}, [
settings.followDeviceOrientation,
settings.defaultVideoOrientation,
segments,
]);
useEffect(() => {
const subscription = AppState.addEventListener(
"change",
(nextAppState) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
BackGroundDownloader.checkForExistingDownloads();
}
},
if (settings.followDeviceOrientation === true) {
ScreenOrientation.unlockAsync();
} else {
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP,
);
}
}, [
settings.followDeviceOrientation,
settings.defaultVideoOrientation,
segments,
]);
BackGroundDownloader.checkForExistingDownloads();
useEffect(() => {
if (Platform.isTV) {
return;
}
return () => {
subscription.remove();
};
}, []);
}
const subscription = AppState.addEventListener("change", (nextAppState) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
BackGroundDownloader.checkForExistingDownloads();
}
});
BackGroundDownloader.checkForExistingDownloads();
return () => {
subscription.remove();
};
}, []);
return (
<QueryClientProvider client={queryClient}>

View File

@@ -1,29 +1,29 @@
import { Button } from "@/components/Button";
import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery";
import { PreviousServersList } from "@/components/PreviousServersList";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
import { Colors } from "@/constants/Colors";
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import type { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { t } from "i18next";
import { useAtomValue } from "jotai";
import type React from "react";
import { useCallback, useEffect, useState } from "react";
import {
Alert,
Keyboard,
KeyboardAvoidingView,
Platform,
SafeAreaView,
TouchableOpacity,
View,
} from "react-native";
import { Keyboard } from "react-native";
import { t } from "i18next";
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({
username: z.string().min(1, t("login.username_required")),
});
@@ -199,7 +199,7 @@ const Login: React.FC = () => {
],
);
}
} catch (error) {
} catch (_error) {
Alert.alert(
t("login.error_title"),
t("login.failed_to_initiate_quick_connect"),
@@ -213,133 +213,127 @@ const Login: React.FC = () => {
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
{api?.basePath ? (
<>
<View className='flex flex-col h-full relative items-center justify-center'>
<View className='px-4 -mt-20 w-full'>
<View className='flex flex-col space-y-2'>
<Text className='text-2xl font-bold -mb-2'>
{serverName ? (
<>
{`${t("login.login_to_title")} `}
<Text className='text-purple-600'>{serverName}</Text>
</>
) : (
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")}
<View className='flex flex-col h-full relative items-center justify-center'>
<View className='px-4 -mt-20 w-full'>
<View className='flex flex-col space-y-2'>
<Text className='text-2xl font-bold -mb-2'>
{serverName ? (
<>
{`${t("login.login_to_title")} `}
<Text className='text-purple-600'>{serverName}</Text>
</>
) : (
t("login.login_title")
)}
</Text>
<Text className='text-xs text-neutral-400'>{api.basePath}</Text>
<Input
aria-label='Server URL'
placeholder={t("server.server_url_placeholder")}
onChangeText={setServerURL}
value={serverURL}
keyboardType='url'
placeholder={t("login.username_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, username: text })
}
value={credentials.username}
keyboardType='default'
returnKeyType='done'
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}
/>
<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);
}}
<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>
<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>
</SafeAreaView>

View File

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

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
"files": {
"includes": [
"**/*",

100
bun.lock
View File

@@ -4,7 +4,6 @@
"": {
"name": "streamyfin",
"dependencies": {
"@bottom-tabs/react-navigation": "0.8.6",
"@expo/config-plugins": "~9.0.15",
"@expo/react-native-action-sheet": "^4.1.1",
"@expo/vector-icons": "^14.0.4",
@@ -14,43 +13,44 @@
"@kesha-antonov/react-native-background-downloader": "3.2.6",
"@react-native-community/netinfo": "11.4.1",
"@react-native-menu/menu": "^1.2.2",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/bottom-tabs": "^7.4.2",
"@react-navigation/material-top-tabs": "^7.1.0",
"@react-navigation/native": "^7.0.14",
"@shopify/flash-list": "1.7.3",
"@tanstack/react-query": "^5.66.0",
"add": "^2.0.6",
"axios": "^1.7.9",
"expo": "~52.0.47",
"expo-asset": "~11.0.5",
"expo-background-fetch": "~13.0.6",
"expo": "~52.0.31",
"expo-asset": "~11.0.3",
"expo-background-fetch": "~13.0.5",
"expo-blur": "~14.0.3",
"expo-brightness": "~13.0.3",
"expo-build-properties": "~0.13.3",
"expo-constants": "~17.0.8",
"expo-build-properties": "~0.13.2",
"expo-constants": "~17.0.5",
"expo-crypto": "~14.0.2",
"expo-dev-client": "~5.0.20",
"expo-device": "~7.0.3",
"expo-dev-client": "~5.0.11",
"expo-device": "~7.0.2",
"expo-font": "~13.0.3",
"expo-haptics": "~14.0.1",
"expo-image": "~2.0.7",
"expo-image": "~2.0.4",
"expo-keep-awake": "~14.0.2",
"expo-linear-gradient": "~14.0.2",
"expo-linking": "~7.0.5",
"expo-localization": "~16.0.1",
"expo-network": "~7.0.5",
"expo-notifications": "~0.29.14",
"expo-router": "~4.0.21",
"expo-notifications": "~0.29.13",
"expo-router": "~4.0.17",
"expo-screen-orientation": "~8.0.4",
"expo-sensors": "~14.0.2",
"expo-sharing": "~13.0.1",
"expo-splash-screen": "~0.29.24",
"expo-splash-screen": "~0.29.22",
"expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.9",
"expo-task-manager": "~12.0.6",
"expo-updates": "~0.27.4",
"expo-system-ui": "~4.0.8",
"expo-task-manager": "~12.0.5",
"expo-updates": "~0.26.17",
"expo-web-browser": "~14.0.2",
"i18next": "^25.0.0",
"install": "^0.13.0",
"jotai": "^2.11.3",
"lodash": "^4.17.21",
"nativewind": "^2.0.11",
@@ -59,14 +59,13 @@
"react-i18next": "^15.4.0",
"react-native": "npm:react-native-tvos@~0.77.2-0",
"react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "0.8.6",
"react-native-circular-progress": "^1.4.1",
"react-native-collapsible": "^1.6.2",
"react-native-compressor": "^1.10.3",
"react-native-country-flag": "^2.0.2",
"react-native-device-info": "^14.0.4",
"react-native-edge-to-edge": "^1.4.3",
"react-native-gesture-handler": "~2.20.2",
"react-native-gesture-handler": "2.22.0",
"react-native-get-random-values": "^1.11.0",
"react-native-google-cast": "^4.8.3",
"react-native-image-colors": "^2.4.0",
@@ -77,9 +76,9 @@
"react-native-progress": "^5.0.1",
"react-native-reanimated": "~3.16.7",
"react-native-reanimated-carousel": "3.5.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-safe-area-context": "5.5.0",
"react-native-screens": "~4.5.0",
"react-native-svg": "15.11.1",
"react-native-tab-view": "^4.0.5",
"react-native-udp": "^4.1.7",
"react-native-uitextview": "^1.4.0",
@@ -88,7 +87,7 @@
"react-native-video": "6.10.0",
"react-native-volume-manager": "^2.0.8",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"react-native-webview": "13.13.2",
"sonner-native": "^0.17.0",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",
@@ -98,7 +97,7 @@
},
"devDependencies": {
"@babel/core": "^7.26.8",
"@biomejs/biome": "^2.0.0",
"@biomejs/biome": "^2.1.2",
"@react-native-community/cli": "18.0.0",
"@react-native-tvos/config-tv": "^0.1.1",
"@types/jest": "^30.0.0",
@@ -116,6 +115,9 @@
},
},
},
"trustedDependencies": [
"postinstall-postinstall",
],
"packages": {
"@0no-co/graphql.web": ["@0no-co/graphql.web@1.1.1", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-F2i3xdycesw78QCOBHmpTn7eaD2iNXGwB2gkfwxcOfBbeauYpr8RBSyJOkDrFtKtVRMclg8Sg3n1ip0ACyUuag=="],
@@ -379,25 +381,23 @@
"@babel/types": ["@babel/types@7.26.9", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw=="],
"@biomejs/biome": ["@biomejs/biome@2.0.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.0.5", "@biomejs/cli-darwin-x64": "2.0.5", "@biomejs/cli-linux-arm64": "2.0.5", "@biomejs/cli-linux-arm64-musl": "2.0.5", "@biomejs/cli-linux-x64": "2.0.5", "@biomejs/cli-linux-x64-musl": "2.0.5", "@biomejs/cli-win32-arm64": "2.0.5", "@biomejs/cli-win32-x64": "2.0.5" }, "bin": { "biome": "bin/biome" } }, "sha512-MztFGhE6cVjf3QmomWu83GpTFyWY8KIcskgRf2AqVEMSH4qI4rNdBLdpAQ11TNK9pUfLGz3IIOC1ZYwgBePtig=="],
"@biomejs/biome": ["@biomejs/biome@2.1.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.2", "@biomejs/cli-darwin-x64": "2.1.2", "@biomejs/cli-linux-arm64": "2.1.2", "@biomejs/cli-linux-arm64-musl": "2.1.2", "@biomejs/cli-linux-x64": "2.1.2", "@biomejs/cli-linux-x64-musl": "2.1.2", "@biomejs/cli-win32-arm64": "2.1.2", "@biomejs/cli-win32-x64": "2.1.2" }, "bin": { "biome": "bin/biome" } }, "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.0.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VIIWQv9Rcj9XresjCf3isBFfWjFStsdGZvm8SmwJzKs/22YQj167ge7DkxuaaZbNf2kmYif0AcjAKvtNedEoEw=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.0.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-DRpGxBgf5Z7HUFcNUB6n66UiD4VlBlMpngNf32wPraxX8vYU6N9cb3xQWOXIQVBBQ64QfsSLJnjNu79i/LNmSg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-FQTfDNMXOknf8+g9Eede2daaduRjTC2SNbfWPNFMadN9K3UKjeZ62jwiYxztPaz9zQQsZU8VbddQIaeQY5CmIA=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.0.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-OpflTCOw/ElEs7QZqN/HFaSViPHjAsAPxFJ22LhWUWvuJgcy/Z8+hRV0/3mk/ZRWy5A6fCDKHZqAxU+xB6W4mA=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-znpfydUDPuDkyBTulnODrQVK2FaG/4hIOPcQSsF2GeauQOYrBAOplj0etGB0NUrr0dFsvaQ15nzDXYb60ACoiw=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-9lmjCnajAzpZXbav2P6D87ugkhnaDpJtDvOH5uQbY2RXeW6Rq18uOUltxgacGBP+d8GusTr+s3IFOu7SN0Ok8g=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.0.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-CP2wKQB+gh8HdJTFKYRFETqReAjxlcN9AlYDEoye8v2eQp+L9v+PUeDql/wsbaUhSsLR0sjj3PtbBtt+02AN3A=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.0.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Sw3rz2m6bBADeQpr3+MD7Ch4E1l15DTt/+dfqKnwkm3cn4BrYwnArmvKeZdVsFRDjMyjlKIP88bw1r7o+9aqzw=="],
"@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.8.6", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-hLlyBAUz4ahaVK2Op2VcJeAkCSpm3KKho4IojkPyXsos4WEHtO44EYWC71TDbVGeOP5HQ9k7FSwAW3IiZs0wHw=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA=="],
"@dominicstop/ts-event-emitter": ["@dominicstop/ts-event-emitter@1.1.0", "", {}, "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw=="],
@@ -653,11 +653,11 @@
"@react-native/normalize-colors": ["@react-native/normalize-colors@0.76.8", "", {}, "sha512-FRjRvs7RgsXjkbGSOjYSxhX5V70c0IzA/jy3HXeYpATMwD9fOR1DbveLW497QGsVdCa0vThbJUtR8rIzAfpHQA=="],
"@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.2.0", "", { "dependencies": { "@react-navigation/elements": "^2.2.5", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-1LxjgnbPyFINyf9Qr5d1YE0pYhuJayg5TCIIFQmbcX4PRhX7FKUXV7cX8OzrKXEdZi/UE/VNXugtozPAR9zgvA=="],
"@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.4.2", "", { "dependencies": { "@react-navigation/elements": "^2.5.2", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.1.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-jyBux5l3qqEucY5M/ZWxVvfA8TQu7DVl2gK+xB6iKqRUfLf7dSumyVxc7HemDwGFoz3Ug8dVZFvSMEs+mfrieQ=="],
"@react-navigation/core": ["@react-navigation/core@7.3.1", "", { "dependencies": { "@react-navigation/routers": "^7.1.2", "escape-string-regexp": "^4.0.0", "nanoid": "3.3.8", "query-string": "^7.1.3", "react-is": "^18.2.0", "use-latest-callback": "^0.2.1", "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-S3KCGvNsoqVk8ErAtQI2EAhg9185lahF5OY01ofrrD4Ij/uk3QEHHjoGQhR5l5DXSCSKr1JbMQA7MEKMsBiWZA=="],
"@react-navigation/elements": ["@react-navigation/elements@2.2.5", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg=="],
"@react-navigation/elements": ["@react-navigation/elements@2.5.2", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-aGC3ukF5+lXuiF5bK7bJyRuWCE+Tk4MZ3GoQpAb7u7+m0KmsquliDhj4UCWEUU5kUoCeoRAUvv+1lKcYKf+WTQ=="],
"@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.1.0", "", { "dependencies": { "@react-navigation/elements": "^2.2.5", "color": "^4.2.3", "react-native-tab-view": "^4.0.5" }, "peerDependencies": { "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-bTFgeHWZmkyE9CAVMTc+lw/b1n2ES3bk0JZoCNSTIrDP+tXfsS8CB4lpOhBybfX1q0C4NQ/i4qMlV7p1iO0eKA=="],
@@ -1193,7 +1193,7 @@
"expo-task-manager": ["expo-task-manager@12.0.6", "", { "dependencies": { "unimodules-app-loader": "~5.0.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-yGbS64OL95z7tAQAvryy0sGHuQgrcpvnJsdyuGL8MA9bcPtr+kytLZ4dOCDac7foQS7+FLDGgtiAR6v/64B5Pg=="],
"expo-updates": ["expo-updates@0.27.4", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.11", "@expo/config-plugins": "~9.0.17", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.3", "expo-manifests": "~0.15.7", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-0rg4L2fFPEjTR/qnZ9Te4Q4irVC8uvNcTZW1pWnWbadG1SLv2PKjS1MYX5BboKzC3ao0H7m++5TP3hWhNg9org=="],
"expo-updates": ["expo-updates@0.26.19", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~10.0.10", "@expo/config-plugins": "~9.0.15", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "expo-eas-client": "~0.13.2", "expo-manifests": "~0.15.6", "expo-structured-headers": "~4.0.0", "expo-updates-interface": "~1.0.0", "fast-glob": "^3.3.2", "fbemitter": "^3.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-h40UrG0n1nCb2na1ffz+mNQtsnr7/BxxK+EtXJSqCaD9PIGaTGe20tasmo1oVskv3s37zfv0x93+6uTjanieQg=="],
"expo-updates-interface": ["expo-updates-interface@1.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ=="],
@@ -1359,6 +1359,8 @@
"inline-style-prefixer": ["inline-style-prefixer@6.0.4", "", { "dependencies": { "css-in-js-utils": "^3.1.0", "fast-loops": "^1.1.3" } }, "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg=="],
"install": ["install@0.13.0", "", {}, "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA=="],
"internal-ip": ["internal-ip@4.3.0", "", { "dependencies": { "default-gateway": "^4.2.0", "ipaddr.js": "^1.9.0" } }, "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg=="],
"invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
@@ -1843,8 +1845,6 @@
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
"react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.6", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-N5b3MoSfsEqlmvFyIyL0X0bd+QAtB+cXH1rl/+R2Kr0BefBTC7ZldGcPhgK3FhBbt0vJDpd3kLb/dvmqZd+Eag=="],
"react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="],
"react-native-collapsible": ["react-native-collapsible@1.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MCOBVJWqHNjnDaGkvxX997VONmJeebh6wyJxnHEgg0L1PrlcXU1e/bo6eK+CDVFuMrCafw8Qh4DOv/C4V/+Iew=="],
@@ -1857,7 +1857,7 @@
"react-native-edge-to-edge": ["react-native-edge-to-edge@1.4.3", "", { "peerDependencies": { "react": ">=18.2.0", "react-native": ">=0.74.0" } }, "sha512-fYchwiQ2D/8NzcvJK1sD9Cm25GFQfsLgYmGpohoSpRxwBwR5UCL0wUf4scoQgYncRh9Hmc2t8ml/sikTwMM3ng=="],
"react-native-gesture-handler": ["react-native-gesture-handler@2.20.2", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4", "prop-types": "^15.7.2" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg=="],
"react-native-gesture-handler": ["react-native-gesture-handler@2.22.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-m5Ps1cOSxSiMP4re+XsbeWcC9DNJuIEjMSmtUxBdyfYEJtdu5iAAiX7KlHHrf2mnK4I/56Ncy4PvPKWBwSpWpQ=="],
"react-native-get-random-values": ["react-native-get-random-values@1.11.0", "", { "dependencies": { "fast-base64-decode": "^1.0.0" }, "peerDependencies": { "react-native": ">=0.56" } }, "sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ=="],
@@ -1883,11 +1883,11 @@
"react-native-reanimated-carousel": ["react-native-reanimated-carousel@3.5.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.6.0", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-9BBQV6JAYSQm2lV7MFtT4mzapXmW4IZO6s38gfiJL84Jg23ivGB1UykcNQauKgtHyhtW2NuZJzItb1s42lM+hA=="],
"react-native-safe-area-context": ["react-native-safe-area-context@4.12.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ=="],
"react-native-safe-area-context": ["react-native-safe-area-context@5.5.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-BQcSvVEJj3T4zBQH9YrnlfcLGHiVOsmeiE10PSBsmI/xyzULSZdJISFOH0HLcLU7/nePC+HsaaVzIsEa1CVBYw=="],
"react-native-screens": ["react-native-screens@4.4.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg=="],
"react-native-screens": ["react-native-screens@4.5.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-yBWeN5EHNeew9f0ia9VE7JSlUQzCZEwkb87r7A7/Sg41OJHuRKHNRhmdCOiMBUqwwQi3F+b4NZGywjeM/gWMyg=="],
"react-native-svg": ["react-native-svg@15.8.0", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw=="],
"react-native-svg": ["react-native-svg@15.11.1", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Qmwx/yJKt+AHUr4zjxx/Q69qwKtRfr1+uIfFMQoq3WFRhqU76aL9db1DyvPiY632DAsVGba1pHf92OZPkpjrdQ=="],
"react-native-tab-view": ["react-native-tab-view@4.0.5", "", { "dependencies": { "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-Xn3TpYo4yvKRC/f4+cOcvsXlitdnSaYkacshckrEI3JiDmFKNFIRVNxtZFggm4MwbJafq2RzuzR6xrgKoxgkTw=="],
@@ -1905,7 +1905,7 @@
"react-native-web": ["react-native-web@0.19.13", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^6.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A=="],
"react-native-webview": ["react-native-webview@13.12.5", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-INOKPom4dFyzkbxbkuQNfeRG9/iYnyRDzrDkJeyvSWgJAW2IDdJkWFJBS2v0RxIL4gqLgHkiIZDOfiLaNnw83Q=="],
"react-native-webview": ["react-native-webview@13.13.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-zACPDTF0WnaEnKZ9mA/r/UpcOpV2gQM06AAIrOOexnO8UJvXL8Pjso0b/wTqKFxUZZnmjKuwd8gHVUosVOdVrw=="],
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
@@ -2479,6 +2479,14 @@
"@react-navigation/core/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"@react-navigation/elements/use-latest-callback": ["use-latest-callback@0.2.4", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg=="],
"@react-navigation/elements/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
"@react-navigation/material-top-tabs/@react-navigation/elements": ["@react-navigation/elements@2.2.5", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg=="],
"@react-navigation/native-stack/@react-navigation/elements": ["@react-navigation/elements@2.2.5", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg=="],
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"ansi-fragments/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
@@ -2527,6 +2535,8 @@
"expo-modules-autolinking/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"expo-router/@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.2.0", "", { "dependencies": { "@react-navigation/elements": "^2.2.5", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-1LxjgnbPyFINyf9Qr5d1YE0pYhuJayg5TCIIFQmbcX4PRhX7FKUXV7cX8OzrKXEdZi/UE/VNXugtozPAR9zgvA=="],
"expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"expo-system-ui/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
@@ -2911,6 +2921,8 @@
"expo-modules-autolinking/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"expo-router/@react-navigation/bottom-tabs/@react-navigation/elements": ["@react-navigation/elements@2.2.5", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.0.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg=="],
"expo-updates/@expo/config-plugins/@expo/config-types": ["@expo/config-types@52.0.5", "", {}, "sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA=="],
"expo-updates/@expo/config-plugins/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
import { Platform, TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Text } from "./common/Text";
@@ -58,23 +60,26 @@ export const BitrateSelector: React.FC<Props> = ({
inverted,
...props
}) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const sorted = useMemo(() => {
if (inverted)
return BITRATES.sort(
return BITRATES.slice().sort(
(a, b) =>
(a.value || Number.POSITIVE_INFINITY) -
(b.value || Number.POSITIVE_INFINITY),
);
return BITRATES.sort(
return BITRATES.slice().sort(
(a, b) =>
(b.value || Number.POSITIVE_INFINITY) -
(a.value || Number.POSITIVE_INFINITY),
);
}, []);
}, [inverted]);
const { t } = useTranslation();
if (isTv) return null;
return (
<View
className='flex shrink'

View File

@@ -1,7 +1,7 @@
import { useHaptic } from "@/hooks/useHaptic";
import type React 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";
export interface ButtonProps

View File

@@ -1,6 +1,6 @@
import { Feather } from "@expo/vector-icons";
import React, { useCallback, useEffect } from "react";
import { Platform, TouchableOpacity, type ViewProps } from "react-native";
import { useCallback, useEffect } from "react";
import { Platform, type ViewProps } from "react-native";
import GoogleCast, {
CastButton,
CastContext,
@@ -44,11 +44,7 @@ export function Chromecast({
// Android requires the cast button to be present for startDiscovery to work
const AndroidCastButton = useCallback(
() =>
Platform.OS === "android" ? (
<CastButton tintColor='transparent' />
) : (
<></>
),
Platform.OS === "android" ? <CastButton tintColor='transparent' /> : null,
[Platform.OS],
);

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,3 @@
import { AudioTrackSelector } from "@/components/AudioTrackSelector";
import { type Bitrate, BitrateSelector } from "@/components/BitrateSelector";
import { DownloadSingleItem } from "@/components/DownloadItem";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
// const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null;
import { PlayButton } from "@/components/PlayButton";
import { PlayedStatus } from "@/components/PlayedStatus";
import { SimilarItems } from "@/components/SimilarItems";
import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector";
import { ItemImage } from "@/components/common/ItemImage";
import { CastAndCrew } from "@/components/series/CastAndCrew";
import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColors } from "@/hooks/useImageColors";
import { useOrientation } from "@/hooks/useOrientation";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom } from "@/providers/JellyfinProvider";
import { userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import type {
BaseItemDto,
MediaSourceInfo,
@@ -30,12 +8,34 @@ import { useAtom } from "jotai";
import React, { useEffect, useMemo, useState } from "react";
import { Platform, View } from "react-native";
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 { ItemHeader } from "./ItemHeader";
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
import { MediaSourceSelector } from "./MediaSourceSelector";
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
import { PlayInRemoteSessionButton } from "./PlayInRemoteSession";
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
export type SelectedOptions = {
@@ -85,8 +85,8 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
defaultMediaSource,
]);
if (!Platform.isTV) {
useEffect(() => {
useEffect(() => {
if (!Platform.isTV) {
navigation.setOptions({
headerRight: () =>
item && (
@@ -112,8 +112,8 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
</View>
),
});
}, [item]);
}
}
}, [item, navigation, user]);
useEffect(() => {
if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP)
@@ -122,12 +122,16 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
else setHeaderHeight(350);
}, [item.Type, orientation]);
const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]);
const logoUrl = useMemo(
() => getLogoImageUrlById({ api, item }),
[api, item],
);
const loading = useMemo(() => {
return Boolean(logoUrl && loadingLogo);
}, [loadingLogo, logoUrl]);
if (!selectedOptions) return null;
if (!selectedOptions) return <View />;
return (
<View
@@ -168,7 +172,9 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
onLoad={() => setLoadingLogo(false)}
onError={() => setLoadingLogo(false)}
/>
) : null
) : (
<View />
)
}
>
<View className='flex flex-col bg-transparent shrink'>

View File

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

View File

@@ -1,11 +1,9 @@
import { formatBitrate } from "@/utils/bitrate";
import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetScrollView,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type {
MediaSourceInfo,
@@ -15,15 +13,15 @@ import type React from "react";
import { useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View } from "react-native";
import { formatBitrate } from "@/utils/bitrate";
import { Badge } from "./Badge";
import { Button } from "./Button";
import { Text } from "./common/Text";
interface Props {
source?: MediaSourceInfo;
}
export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
export const ItemTechnicalDetails: React.FC<Props> = ({ source }) => {
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { t } = useTranslation();
@@ -55,7 +53,7 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
>
<BottomSheetScrollView>
<View className='flex flex-col space-y-2 p-4 mb-4'>
<View className=''>
<View>
<Text className='text-lg font-bold mb-4'>
{t("item_card.video")}
</Text>
@@ -64,7 +62,7 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
</View>
</View>
<View className=''>
<View>
<Text className='text-lg font-bold mb-2'>
{t("item_card.audio")}
</Text>
@@ -77,7 +75,7 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
/>
</View>
<View className=''>
<View>
<Text className='text-lg font-bold mb-2'>
{t("item_card.subtitles")}
</Text>
@@ -103,7 +101,7 @@ const SubtitleStreamInfo = ({
}) => {
return (
<View className='flex flex-col'>
{subtitleStreams.map((stream, index) => (
{subtitleStreams.map((stream, _index) => (
<View key={stream.Index} className='flex flex-col'>
<Text className='text-xs mb-3 text-neutral-400'>
{stream.DisplayTitle}
@@ -177,15 +175,13 @@ const AudioStreamInfo = ({ audioStreams }: { audioStreams: MediaStream[] }) => {
};
const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => {
if (!source) return null;
const videoStream = useMemo(() => {
return source.MediaStreams?.find(
(stream) => stream.Type === "Video",
) as MediaStream;
}, [source.MediaStreams]);
return source?.MediaStreams?.find((stream) => stream.Type === "Video") as
| MediaStream
| undefined;
}, [source?.MediaStreams]);
if (!videoStream) return null;
if (!source || !videoStream) return null;
return (
<View className='flex-row flex-wrap gap-2'>
@@ -223,7 +219,11 @@ const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => {
<Badge
variant='gray'
iconLeft={<Ionicons name='play-outline' size={16} color='white' />}
text={`${videoStream.AverageFrameRate?.toFixed(0)} fps`}
text={
videoStream.AverageFrameRate != null
? `${videoStream.AverageFrameRate.toFixed(0)} fps`
: ""
}
/>
</View>
);

View File

@@ -1,7 +1,7 @@
import { useJellyfinDiscovery } from "@/hooks/useJellyfinDiscovery";
import type React from "react";
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 { ListGroup } from "./list/ListGroup";
import { ListItem } from "./list/ListItem";

View File

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

View File

@@ -4,7 +4,9 @@ import type {
} from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next";
import { Text } from "./common/Text";
@@ -20,7 +22,8 @@ export const MediaSourceSelector: React.FC<Props> = ({
selected,
...props
}) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const selectedName = useMemo(
() =>
item.MediaSources?.find((x) => x.Id === selected?.Id)?.MediaStreams?.find(
@@ -52,6 +55,8 @@ export const MediaSourceSelector: React.FC<Props> = ({
return name?.replace(commonPrefix, "").toLowerCase();
};
if (isTv) return null;
return (
<View
className='flex shrink'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,11 @@
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 { 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 { TouchableOpacity, View } from "react-native";
import Animated, {
Easing,
interpolate,
@@ -22,6 +16,11 @@ import Animated, {
useSharedValue,
withTiming,
} 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 { runtimeTicksToMinutes } from "@/utils/time";
import type { Button } from "./Button";
import type { SelectedOptions } from "./ItemContent";
@@ -42,8 +41,8 @@ export const PlayButton: React.FC<Props> = ({
const { t } = useTranslation();
const [colorAtom] = useAtom(itemThemeColorAtom);
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const _api = useAtomValue(apiAtom);
const _user = useAtomValue(userAtom);
const router = useRouter();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
import { tc } from "@/utils/textTools";
import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native";
import { tc } from "@/utils/textTools";
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next";
import { Text } from "./common/Text";
@@ -18,7 +20,8 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
selected,
...props
}) => {
if (Platform.isTV) return null;
const isTv = Platform.isTV;
const subtitleStreams = useMemo(() => {
return source?.MediaStreams?.filter((x) => x.Type === "Subtitle");
}, [source]);
@@ -28,10 +31,11 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
[subtitleStreams, selected],
);
if (subtitleStreams?.length === 0) return null;
const { t } = useTranslation();
if (isTv) return null;
if (subtitleStreams?.length === 0) return null;
return (
<View
className='flex col shrink justify-start place-self-start items-start'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,13 @@
import { useRouter, useSegments } from "expo-router";
import type React from "react";
import { type PropsWithChildren, useCallback, useMemo } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import * as ContextMenu from "@/components/ContextMenu";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import {
Permission,
hasPermission,
Permission,
} from "@/utils/jellyseerr/server/lib/permissions";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type {
@@ -11,10 +15,6 @@ import type {
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
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 {
result?: MovieResult | TvResult | MovieDetails | TvDetails;
@@ -60,69 +60,67 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
if (from === "(home)" || from === "(search)" || from === "(libraries)")
return (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<TouchableOpacity
onPress={() => {
if (!result) return;
<ContextMenu.Root>
<ContextMenu.Trigger>
<TouchableOpacity
onPress={() => {
if (!result) return;
// @ts-ignore
router.push({
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
params: {
...result,
mediaTitle,
releaseYear,
canRequest,
posterSrc,
mediaType,
},
});
}}
{...props}
>
{children}
</TouchableOpacity>
</ContextMenu.Trigger>
<ContextMenu.Content
avoidCollisions
alignOffset={0}
collisionPadding={0}
loop={false}
key={"content"}
// @ts-ignore
router.push({
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
params: {
...result,
mediaTitle,
releaseYear,
canRequest,
posterSrc,
mediaType,
},
});
}}
{...props}
>
<ContextMenu.Label key='label-1'>Actions</ContextMenu.Label>
{canRequest && mediaType === MediaType.MOVIE && (
<ContextMenu.Item
key='item-1'
onSelect={() => {
if (autoApprove) {
request();
}
{children}
</TouchableOpacity>
</ContextMenu.Trigger>
<ContextMenu.Content
avoidCollisions
alignOffset={0}
collisionPadding={0}
loop={false}
key={"content"}
>
<ContextMenu.Label key='label-1'>Actions</ContextMenu.Label>
{canRequest && mediaType === MediaType.MOVIE && (
<ContextMenu.Item
key='item-1'
onSelect={() => {
if (autoApprove) {
request();
}
}}
shouldDismissMenuOnSelect
>
<ContextMenu.ItemTitle key='item-1-title'>
Request
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon
ios={{
name: "arrow.down.to.line",
pointSize: 18,
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "purple",
light: "purple",
},
}}
shouldDismissMenuOnSelect
>
<ContextMenu.ItemTitle key='item-1-title'>
Request
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon
ios={{
name: "arrow.down.to.line",
pointSize: 18,
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "purple",
light: "purple",
},
}}
androidIconName='download'
/>
</ContextMenu.Item>
)}
</ContextMenu.Content>
</ContextMenu.Root>
</>
androidIconName='download'
/>
</ContextMenu.Item>
)}
</ContextMenu.Content>
</ContextMenu.Root>
);
};

View File

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

View File

@@ -1,5 +1,3 @@
import { useFavorite } from "@/hooks/useFavorite";
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
import { useActionSheet } from "@expo/react-native-action-sheet";
import type {
BaseItemDto,
@@ -8,6 +6,8 @@ import type {
import { useRouter, useSegments } from "expo-router";
import { type PropsWithChildren, useCallback } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import { useFavorite } from "@/hooks/useFavorite";
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
interface Props extends TouchableOpacityProps {
item: BaseItemDto;

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type React from "react";
import { useEffect, useMemo, useState } from "react";
import type { TextProps } from "react-native";
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
interface DownloadSizeProps extends TextProps {
items: BaseItemDto[];
@@ -39,10 +39,8 @@ export const DownloadSize: React.FC<DownloadSizeProps> = ({
}, [size]);
return (
<>
<Text className='text-xs text-neutral-500' {...props}>
{sizeText}
</Text>
</>
<Text className='text-xs text-neutral-500' {...props}>
{sizeText}
</Text>
);
};

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
import { useActionSheet } from "@expo/react-native-action-sheet";
import { Ionicons } from "@expo/vector-icons";
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 { useCallback, useMemo } from "react";
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";
export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {

View File

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

View File

@@ -1,16 +1,12 @@
import { Ionicons } from "@expo/vector-icons";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetFlatList,
BottomSheetModal,
BottomSheetScrollView,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type React 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 {
StyleSheet,
@@ -18,6 +14,7 @@ import {
View,
type ViewProps,
} from "react-native";
import { Text } from "@/components/common/Text";
import { Button } from "../Button";
import { Input } from "../common/Input";

View File

@@ -1,11 +1,11 @@
import { Ionicons } from "@expo/vector-icons";
import { useAtom } from "jotai";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import {
genreFilterAtom,
tagsFilterAtom,
yearFilterAtom,
} 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 {}

View File

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

View File

@@ -1,8 +1,3 @@
import { useHaptic } from "@/hooks/useHaptic";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
@@ -21,6 +16,11 @@ import Carousel, {
type ICarouselInstance,
Pagination,
} 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";
interface Props extends ViewProps {}

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import { Text } from "@/components/common/Text";
import PersonPoster from "@/components/jellyseerr/PersonPoster";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { FlashList } from "@shopify/flash-list";
import type React from "react";
import { useTranslation } from "react-i18next";
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<
{ details?: MovieDetails | TvDetails } & ViewProps

View File

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

View File

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

View File

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

View File

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

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