mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-27 19:51:51 +00:00
Merge branch 'master' into feat/i18n
This commit is contained in:
@@ -1,27 +1,29 @@
|
||||
import {FlatList, TouchableOpacity, View} from "react-native";
|
||||
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import {useAtom} from "jotai/index";
|
||||
import {apiAtom} from "@/providers/JellyfinProvider";
|
||||
import {ListItem} from "@/components/ListItem";
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
import {Text} from "@/components/common/Text";
|
||||
import { FlatList, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useAtom } from "jotai/index";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import { Text } from "@/components/common/Text";
|
||||
|
||||
export interface MenuLink {
|
||||
name: string,
|
||||
url: string,
|
||||
icon: string
|
||||
name: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export default function menuLinks() {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const insets = useSafeAreaInsets()
|
||||
const [menuLinks, setMenuLinks] = useState<MenuLink[]>([])
|
||||
const insets = useSafeAreaInsets();
|
||||
const [menuLinks, setMenuLinks] = useState<MenuLink[]>([]);
|
||||
|
||||
const getMenuLinks = useCallback(async () => {
|
||||
try {
|
||||
const response = await api?.axiosInstance.get(api?.basePath + "/web/config.json")
|
||||
const response = await api?.axiosInstance.get(
|
||||
api?.basePath + "/web/config.json"
|
||||
);
|
||||
const config = response?.data;
|
||||
|
||||
if (!config && !config.hasOwnProperty("menuLinks")) {
|
||||
@@ -29,15 +31,15 @@ export default function menuLinks() {
|
||||
return;
|
||||
}
|
||||
|
||||
setMenuLinks(config?.menuLinks as MenuLink[])
|
||||
} catch (error) {
|
||||
console.error("Failed to retrieve config:", error);
|
||||
}
|
||||
},
|
||||
[api]
|
||||
)
|
||||
setMenuLinks(config?.menuLinks as MenuLink[]);
|
||||
} catch (error) {
|
||||
console.error("Failed to retrieve config:", error);
|
||||
}
|
||||
}, [api]);
|
||||
|
||||
useEffect(() => { getMenuLinks() }, []);
|
||||
useEffect(() => {
|
||||
getMenuLinks();
|
||||
}, []);
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
@@ -47,27 +49,27 @@ export default function menuLinks() {
|
||||
paddingRight: insets.right,
|
||||
}}
|
||||
data={menuLinks}
|
||||
renderItem={({item}) => (
|
||||
<TouchableOpacity onPress={() => WebBrowser.openBrowserAsync(item.url) }>
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity onPress={() => WebBrowser.openBrowserAsync(item.url)}>
|
||||
<ListItem
|
||||
title={item.name}
|
||||
iconAfter={<Ionicons name="link" size={24} color="white"/>}
|
||||
title={item.name}
|
||||
iconAfter={<Ionicons name="link" size={24} color="white" />}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
)}
|
||||
ItemSeparatorComponent={() => (
|
||||
<View
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
}}/>
|
||||
)}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
<View className="flex flex-col items-center justify-center h-full">
|
||||
<Text className="font-bold text-xl text-neutral-500">No links</Text>
|
||||
</View>
|
||||
<View className="flex flex-col items-center justify-center h-full">
|
||||
<Text className="font-bold text-xl text-neutral-500">No links</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ export default function SearchLayout() {
|
||||
headerShown: true,
|
||||
headerLargeTitle: true,
|
||||
headerTitle: t("favorites.favorites_title"),
|
||||
headerLargeStyle: {
|
||||
backgroundColor: "black",
|
||||
},
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerShadowVisible: false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Chromecast } from "@/components/Chromecast";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -17,6 +18,9 @@ export default function IndexLayout() {
|
||||
headerLargeTitle: true,
|
||||
headerTitle: t("home.home"),
|
||||
headerBlurEffect: "prominent",
|
||||
headerLargeStyle: {
|
||||
backgroundColor: "black",
|
||||
},
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerShadowVisible: false,
|
||||
headerRight: () => (
|
||||
@@ -51,6 +55,30 @@ export default function IndexLayout() {
|
||||
title: t("home.settings.settings_title"),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings/optimized-server/page"
|
||||
options={{
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings/marlin-search/page"
|
||||
options={{
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings/jellyseerr/page"
|
||||
options={{
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings/popular-lists/page"
|
||||
options={{
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
||||
<Stack.Screen key={name} name={name} options={options} />
|
||||
))}
|
||||
|
||||
@@ -6,18 +6,23 @@ import { DownloadedItem, useDownload } from "@/providers/DownloadProvider";
|
||||
import { queueAtom } from "@/utils/atoms/queue";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {useNavigation, useRouter} from "expo-router";
|
||||
import { useNavigation, useRouter } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React, {useEffect, useMemo, useRef} from "react";
|
||||
import {Alert, ScrollView, TouchableOpacity, View} from "react-native";
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
|
||||
import { Button } from "@/components/Button";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import {DownloadSize} from "@/components/downloads/DownloadSize";
|
||||
import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet";
|
||||
import {toast} from "sonner-native";
|
||||
import {writeToLog} from "@/utils/log";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { t } from 'i18next';
|
||||
import { DownloadSize } from "@/components/downloads/DownloadSize";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { toast } from "sonner-native";
|
||||
import { writeToLog } from "@/utils/log";
|
||||
|
||||
export default function page() {
|
||||
const navigation = useNavigation();
|
||||
@@ -59,28 +64,29 @@ export default function page() {
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<TouchableOpacity
|
||||
onPress={bottomSheetModalRef.current?.present}
|
||||
>
|
||||
<DownloadSize items={downloadedFiles?.map(f => f.item) || []}/>
|
||||
<TouchableOpacity onPress={bottomSheetModalRef.current?.present}>
|
||||
<DownloadSize items={downloadedFiles?.map((f) => f.item) || []} />
|
||||
</TouchableOpacity>
|
||||
)
|
||||
})
|
||||
),
|
||||
});
|
||||
}, [downloadedFiles]);
|
||||
|
||||
const deleteMovies = () => deleteFileByType("Movie")
|
||||
.then(() => toast.success(t("home.downloads.toasts.deleted_all_movies_successfully")))
|
||||
.catch((reason) => {
|
||||
writeToLog("ERROR", reason);
|
||||
toast.error(t("home.downloads.toasts.failed_to_delete_all_movies"));
|
||||
});
|
||||
const deleteShows = () => deleteFileByType("Episode")
|
||||
.then(() => toast.success(t("home.downloads.toasts.deleted_all_tvseries_successfully")))
|
||||
.catch((reason) => {
|
||||
writeToLog("ERROR", reason);
|
||||
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
|
||||
});
|
||||
const deleteAllMedia = async () => await Promise.all([deleteMovies(), deleteShows()])
|
||||
const deleteMovies = () =>
|
||||
deleteFileByType("Movie")
|
||||
.then(() => toast.success(t("home.downloads.toasts.deleted_all_movies_successfully")))
|
||||
.catch((reason) => {
|
||||
writeToLog("ERROR", reason);
|
||||
toast.error(t("home.downloads.toasts.failed_to_delete_all_movies"));
|
||||
});
|
||||
const deleteShows = () =>
|
||||
deleteFileByType("Episode")
|
||||
.then(() => toast.success(t("home.downloads.toasts.deleted_all_tvseries_successfully")))
|
||||
.catch((reason) => {
|
||||
writeToLog("ERROR", reason);
|
||||
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
|
||||
});
|
||||
const deleteAllMedia = async () =>
|
||||
await Promise.all([deleteMovies(), deleteShows()]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -110,7 +116,9 @@ export default function page() {
|
||||
>
|
||||
<View>
|
||||
<Text className="font-semibold">{q.item.Name}</Text>
|
||||
<Text className="text-xs opacity-50">{q.item.Type}</Text>
|
||||
<Text className="text-xs opacity-50">
|
||||
{q.item.Type}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
@@ -121,7 +129,7 @@ export default function page() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={24} color="red"/>
|
||||
<Ionicons name="close" size={24} color="red" />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -133,7 +141,7 @@ export default function page() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ActiveDownloads/>
|
||||
<ActiveDownloads />
|
||||
</View>
|
||||
|
||||
{movies.length > 0 && (
|
||||
@@ -148,7 +156,7 @@ export default function page() {
|
||||
<View className="px-4 flex flex-row">
|
||||
{movies?.map((item) => (
|
||||
<View className="mb-2 last:mb-0" key={item.item.Id}>
|
||||
<MovieCard item={item.item}/>
|
||||
<MovieCard item={item.item} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
@@ -160,13 +168,18 @@ export default function page() {
|
||||
<View className="flex flex-row items-center justify-between mb-2 px-4">
|
||||
<Text className="text-lg font-bold">{t("home.downloads.tvseries")}</Text>
|
||||
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
|
||||
<Text className="text-xs font-bold">{groupedBySeries?.length}</Text>
|
||||
<Text className="text-xs font-bold">
|
||||
{groupedBySeries?.length}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View className="px-4 flex flex-row">
|
||||
{groupedBySeries?.map((items) => (
|
||||
<View className="mb-2 last:mb-0" key={items[0].item.SeriesId}>
|
||||
<View
|
||||
className="mb-2 last:mb-0"
|
||||
key={items[0].item.SeriesId}
|
||||
>
|
||||
<SeriesCard
|
||||
items={items.map((i) => i.item)}
|
||||
key={items[0].item.SeriesId}
|
||||
@@ -203,9 +216,15 @@ export default function page() {
|
||||
>
|
||||
<BottomSheetView>
|
||||
<View className="p-4 space-y-4 mb-4">
|
||||
<Button color="purple" onPress={deleteMovies}>{t("home.downloads.delete_all_movies_button")}</Button>
|
||||
<Button color="purple" onPress={deleteShows}>{t("home.downloads.delete_all_tvseries_button")}</Button>
|
||||
<Button color="red" onPress={deleteAllMedia}>{t("home.downloads.delete_all_button")}</Button>
|
||||
<Button color="purple" onPress={deleteMovies}>
|
||||
{t("home.downloads.delete_all_movies_button")}
|
||||
</Button>
|
||||
<Button color="purple" onPress={deleteShows}>
|
||||
{t("home.downloads.delete_all_tvseries_button")}
|
||||
</Button>
|
||||
<Button color="red" onPress={deleteAllMedia}>
|
||||
{t("home.downloads.delete_all_button")}
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheetView>
|
||||
</BottomSheetModal>
|
||||
|
||||
@@ -1,179 +1,88 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListItem } from "@/components/ListItem";
|
||||
import { SettingToggles } from "@/components/settings/SettingToggles";
|
||||
import {useDownload} from "@/providers/DownloadProvider";
|
||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { clearLogs, useLog } from "@/utils/log";
|
||||
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
import { AudioToggles } from "@/components/settings/AudioToggles";
|
||||
import { DownloadSettings } from "@/components/settings/DownloadSettings";
|
||||
import { MediaProvider } from "@/components/settings/MediaContext";
|
||||
import { MediaToggles } from "@/components/settings/MediaToggles";
|
||||
import { OtherSettings } from "@/components/settings/OtherSettings";
|
||||
import { PluginSettings } from "@/components/settings/PluginSettings";
|
||||
import { QuickConnect } from "@/components/settings/QuickConnect";
|
||||
import { StorageSettings } from "@/components/settings/StorageSettings";
|
||||
import { SubtitleToggles } from "@/components/settings/SubtitleToggles";
|
||||
import { UserInfo } from "@/components/settings/UserInfo";
|
||||
import { useJellyfin } from "@/providers/JellyfinProvider";
|
||||
import { clearLogs } from "@/utils/log";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useAtom } from "jotai";
|
||||
import { Alert, ScrollView, View } from "react-native";
|
||||
import * as Progress from "react-native-progress";
|
||||
import { useNavigation, useRouter } from "expo-router";
|
||||
import { t } from "i18next";
|
||||
import { useEffect } from "react";
|
||||
import { ScrollView, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { toast } from "sonner-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function settings() {
|
||||
const { logout } = useJellyfin();
|
||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||
const { logs } = useLog();
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: size, isLoading: appSizeLoading } = useQuery({
|
||||
queryKey: ["appSize", appSizeUsage],
|
||||
queryFn: async () => {
|
||||
const app = await appSizeUsage;
|
||||
|
||||
const remaining = await FileSystem.getFreeDiskStorageAsync();
|
||||
const total = await FileSystem.getTotalDiskCapacityAsync();
|
||||
|
||||
return { app, remaining, total, used: (total - remaining) / total };
|
||||
},
|
||||
});
|
||||
|
||||
const openQuickConnectAuthCodeInput = () => {
|
||||
Alert.prompt(
|
||||
t("home.settings.quick_connect.quick_connect_title"),
|
||||
t("home.settings.quick_connect.enter_the_quick_connect_code"),
|
||||
async (text) => {
|
||||
if (text) {
|
||||
try {
|
||||
const res = await getQuickConnectApi(api!).authorizeQuickConnect({
|
||||
code: text,
|
||||
userId: user?.Id,
|
||||
});
|
||||
if (res.status === 200) {
|
||||
Haptics.notificationAsync(
|
||||
Haptics.NotificationFeedbackType.Success
|
||||
);
|
||||
Alert.alert(t("home.settings.quick_connect.success"), t("home.settings.quick_connect.quick_connect_autorized"));
|
||||
} else {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
|
||||
}
|
||||
} catch (e) {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const onDeleteClicked = async () => {
|
||||
try {
|
||||
await deleteAllFiles();
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
} catch (e) {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
toast.error(t("home.settings.toasts.error_deleting_files"));
|
||||
}
|
||||
};
|
||||
const { logout } = useJellyfin();
|
||||
|
||||
const onClearLogsClicked = async () => {
|
||||
clearLogs();
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
};
|
||||
|
||||
const navigation = useNavigation();
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
logout();
|
||||
}}
|
||||
>
|
||||
<Text className="text-red-600">{t("home.settings.log_out_button")}</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
paddingBottom: 100,
|
||||
}}
|
||||
>
|
||||
<View className="p-4 flex flex-col gap-y-4">
|
||||
{/* <Button
|
||||
onPress={() => {
|
||||
registerBackgroundFetchAsync();
|
||||
}}
|
||||
>
|
||||
registerBackgroundFetchAsync
|
||||
</Button> */}
|
||||
<View>
|
||||
<Text className="font-bold text-lg mb-2">{t("home.settings.user_info.user_info_title")}</Text>
|
||||
<UserInfo />
|
||||
<QuickConnect className="mb-4" />
|
||||
|
||||
<View className="flex flex-col rounded-xl overflow-hidden border-neutral-800 divide-y-2 divide-solid divide-neutral-800 ">
|
||||
<ListItem title={t("home.settings.user_info.user")} subTitle={user?.Name} />
|
||||
<ListItem title={t("home.settings.user_info.server")} subTitle={api?.basePath} />
|
||||
<ListItem title={t("home.settings.user_info.token")} subTitle={api?.accessToken} />
|
||||
</View>
|
||||
<Button className="my-2.5" color="black" onPress={logout}>
|
||||
{t("home.settings.user_info.log_out_button")}
|
||||
</Button>
|
||||
</View>
|
||||
<MediaProvider>
|
||||
<MediaToggles className="mb-4" />
|
||||
<AudioToggles className="mb-4" />
|
||||
<SubtitleToggles className="mb-4" />
|
||||
</MediaProvider>
|
||||
|
||||
<View>
|
||||
<Text className="font-bold text-lg mb-2">{t("home.settings.quick_connect.quick_connect_title")}</Text>
|
||||
<Button onPress={openQuickConnectAuthCodeInput} color="black">
|
||||
{t("home.settings.quick_connect.authorize_button")}
|
||||
</Button>
|
||||
</View>
|
||||
<OtherSettings />
|
||||
<DownloadSettings />
|
||||
|
||||
<SettingToggles />
|
||||
<PluginSettings />
|
||||
|
||||
<View className="flex flex-col space-y-2">
|
||||
<Text className="font-bold text-lg mb-2">{t("home.settings.storage.storage_title")}</Text>
|
||||
<View className="mb-4 space-y-2">
|
||||
{size && <Text>{t("home.settings.storage.app_usage", {usedSpace: size.app.bytesToReadable()})}</Text>}
|
||||
<Progress.Bar
|
||||
className="bg-gray-100/10"
|
||||
indeterminate={appSizeLoading}
|
||||
color="#9333ea"
|
||||
width={null}
|
||||
height={10}
|
||||
borderRadius={6}
|
||||
borderWidth={0}
|
||||
progress={size?.used}
|
||||
<View className="mb-4">
|
||||
<ListGroup title={t("home.settings.logs.logs_title")}>
|
||||
<ListItem
|
||||
onPress={() => router.push("/settings/logs/page")}
|
||||
showArrow
|
||||
title={t("home.settings.logs.logs_title")}
|
||||
/>
|
||||
{size && (
|
||||
<Text>
|
||||
{t("home.settings.storage.available_total", {availableSpace: size.remaining?.bytesToReadable(), totalSpace: size.total?.bytesToReadable()})}
|
||||
{}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Button color="red" onPress={onDeleteClicked}>
|
||||
{t("home.settings.storage.delete_all_downloaded_files")}
|
||||
</Button>
|
||||
<Button color="red" onPress={onClearLogsClicked}>
|
||||
{t("home.settings.storage.delete_all_logs")}
|
||||
</Button>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="font-bold text-lg mb-2">{t("home.settings.logs.logs_title")}</Text>
|
||||
<View className="flex flex-col space-y-2">
|
||||
{logs?.map((log, index) => (
|
||||
<View key={index} className="bg-neutral-900 rounded-xl p-3">
|
||||
<Text
|
||||
className={`
|
||||
mb-1
|
||||
${log.level === "INFO" && "text-blue-500"}
|
||||
${log.level === "ERROR" && "text-red-500"}
|
||||
`}
|
||||
>
|
||||
{log.level}
|
||||
</Text>
|
||||
<Text uiTextView selectable className="text-xs">
|
||||
{log.message}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{logs?.length === 0 && (
|
||||
<Text className="opacity-50">{t("home.settings.logs.no_logs_available")}</Text>
|
||||
)}
|
||||
</View>
|
||||
<ListItem
|
||||
textColor="red"
|
||||
onPress={onClearLogsClicked}
|
||||
title={t("home.settings.logs.delete_all_logs")}
|
||||
/>
|
||||
</ListGroup>
|
||||
</View>
|
||||
|
||||
<StorageSettings />
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
78
app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx
Normal file
78
app/(auth)/(tabs)/(home)/settings/jellyseerr/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
|
||||
import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm";
|
||||
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 { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
|
||||
export default function page() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [settings, updateSettings] = useSettings();
|
||||
|
||||
const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] =
|
||||
useState<string>(settings?.optimizedVersionsServerUrl || "");
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async (newVal: string) => {
|
||||
if (newVal.length === 0 || !newVal.startsWith("http")) {
|
||||
toast.error("Invalid URL");
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedUrl = newVal.endsWith("/") ? newVal : newVal + "/";
|
||||
|
||||
updateSettings({
|
||||
optimizedVersionsServerUrl: updatedUrl,
|
||||
});
|
||||
|
||||
return await getStatistics({
|
||||
url: settings?.optimizedVersionsServerUrl,
|
||||
authHeader: api?.accessToken,
|
||||
deviceId: getOrSetDeviceId(),
|
||||
});
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
toast.success("Connected");
|
||||
} else {
|
||||
toast.error("Could not connect");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Could not connect");
|
||||
},
|
||||
});
|
||||
|
||||
const onSave = (newVal: string) => {
|
||||
saveMutation.mutate(newVal);
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// title: "Optimized Server",
|
||||
// headerRight: () =>
|
||||
// saveMutation.isPending ? (
|
||||
// <ActivityIndicator size={"small"} color={"white"} />
|
||||
// ) : (
|
||||
// <TouchableOpacity onPress={() => onSave(optimizedVersionsServerUrl)}>
|
||||
// <Text className="text-blue-500">Save</Text>
|
||||
// </TouchableOpacity>
|
||||
// ),
|
||||
// });
|
||||
// }, [navigation, optimizedVersionsServerUrl, saveMutation.isPending]);
|
||||
|
||||
return (
|
||||
<View className="p-4">
|
||||
<JellyseerrSettings />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
35
app/(auth)/(tabs)/(home)/settings/logs/page.tsx
Normal file
35
app/(auth)/(tabs)/(home)/settings/logs/page.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useLog } from "@/utils/log";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function page() {
|
||||
const { logs } = useLog();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ScrollView className="p-4">
|
||||
<View className="flex flex-col space-y-2">
|
||||
{logs?.map((log, index) => (
|
||||
<View key={index} className="bg-neutral-900 rounded-xl p-3">
|
||||
<Text
|
||||
className={`
|
||||
mb-1
|
||||
${log.level === "INFO" && "text-blue-500"}
|
||||
${log.level === "ERROR" && "text-red-500"}
|
||||
`}
|
||||
>
|
||||
{log.level}
|
||||
</Text>
|
||||
<Text uiTextView selectable className="text-xs">
|
||||
{log.message}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{logs?.length === 0 && (
|
||||
<Text className="opacity-50">{t("home.settings.logs.no_logs_available")}</Text>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
104
app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx
Normal file
104
app/(auth)/(tabs)/(home)/settings/marlin-search/page.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigation } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Linking,
|
||||
Switch,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
|
||||
export default function page() {
|
||||
const navigation = useNavigation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");
|
||||
|
||||
const onSave = (val: string) => {
|
||||
updateSettings({
|
||||
marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1),
|
||||
});
|
||||
toast.success("Saved");
|
||||
};
|
||||
|
||||
const handleOpenLink = () => {
|
||||
Linking.openURL("https://github.com/fredrikburmester/marlin-search");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<TouchableOpacity onPress={() => onSave(value)}>
|
||||
<Text className="text-blue-500">{t("home.settings.plugins.marlin_search.save_button")}</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
}, [navigation, value]);
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
return (
|
||||
<View className="px-4">
|
||||
<ListGroup>
|
||||
<ListItem
|
||||
title={t("home.settings.plugins.marlin_search.enable_marlin_search")}
|
||||
onPress={() => {
|
||||
updateSettings({ searchEngine: "Jellyfin" });
|
||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
value={settings.searchEngine === "Marlin"}
|
||||
onValueChange={(value) => {
|
||||
updateSettings({ searchEngine: value ? "Marlin" : "Jellyfin" });
|
||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
|
||||
<View
|
||||
className={`mt-2 ${
|
||||
settings.searchEngine === "Marlin" ? "" : "opacity-50"
|
||||
}`}
|
||||
>
|
||||
<View className="flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4">
|
||||
<View
|
||||
className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}
|
||||
>
|
||||
<Text className="mr-4">{t("home.settings.plugins.marlin_search.url")}</Text>
|
||||
<TextInput
|
||||
editable={settings.searchEngine === "Marlin"}
|
||||
className="text-white"
|
||||
placeholder={t("home.settings.plugins.marlin_search.url")}
|
||||
value={value}
|
||||
keyboardType="url"
|
||||
returnKeyType="done"
|
||||
autoCapitalize="none"
|
||||
textContentType="URL"
|
||||
onChangeText={(text) => setValue(text)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
||||
{t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "}
|
||||
<Text className="text-blue-500" onPress={handleOpenLink}>
|
||||
{t("home.settings.plugins.marlin_search.read_more_about_marlin")}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
80
app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx
Normal file
80
app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm";
|
||||
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 { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
|
||||
export default function page() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [settings, updateSettings] = useSettings();
|
||||
|
||||
const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] =
|
||||
useState<string>(settings?.optimizedVersionsServerUrl || "");
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async (newVal: string) => {
|
||||
if (newVal.length === 0 || !newVal.startsWith("http")) {
|
||||
toast.error("Invalid URL");
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedUrl = newVal.endsWith("/") ? newVal : newVal + "/";
|
||||
|
||||
updateSettings({
|
||||
optimizedVersionsServerUrl: updatedUrl,
|
||||
});
|
||||
|
||||
return await getStatistics({
|
||||
url: settings?.optimizedVersionsServerUrl,
|
||||
authHeader: api?.accessToken,
|
||||
deviceId: getOrSetDeviceId(),
|
||||
});
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
toast.success("Connected");
|
||||
} else {
|
||||
toast.error("Could not connect");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Could not connect");
|
||||
},
|
||||
});
|
||||
|
||||
const onSave = (newVal: string) => {
|
||||
saveMutation.mutate(newVal);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: "Optimized Server",
|
||||
headerRight: () =>
|
||||
saveMutation.isPending ? (
|
||||
<ActivityIndicator size={"small"} color={"white"} />
|
||||
) : (
|
||||
<TouchableOpacity onPress={() => onSave(optimizedVersionsServerUrl)}>
|
||||
<Text className="text-blue-500">Save</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
}, [navigation, optimizedVersionsServerUrl, saveMutation.isPending]);
|
||||
|
||||
return (
|
||||
<View className="p-4">
|
||||
<OptimizedServerForm
|
||||
value={optimizedVersionsServerUrl}
|
||||
onChangeValue={setOptimizedVersionsServerUrl}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
136
app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx
Normal file
136
app/(auth)/(tabs)/(home)/settings/popular-lists/page.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigation } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { Linking, Switch, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function page() {
|
||||
const navigation = useNavigation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const [settings, updateSettings] = useSettings();
|
||||
|
||||
const handleOpenLink = () => {
|
||||
Linking.openURL(
|
||||
"https://github.com/lostb1t/jellyfin-plugin-collection-import"
|
||||
);
|
||||
};
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: mediaListCollections,
|
||||
isLoading: isLoadingMediaListCollections,
|
||||
} = useQuery({
|
||||
queryKey: ["sf_promoted", user?.Id, settings?.usePopularPlugin],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id) return [];
|
||||
|
||||
const response = await getItemsApi(api).getItems({
|
||||
userId: user.Id,
|
||||
tags: ["sf_promoted"],
|
||||
recursive: true,
|
||||
fields: ["Tags"],
|
||||
includeItemTypes: ["BoxSet"],
|
||||
});
|
||||
|
||||
return response.data.Items ?? [];
|
||||
},
|
||||
enabled: !!api && !!user?.Id && settings?.usePopularPlugin === true,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
return (
|
||||
<View className="px-4 pt-4">
|
||||
<ListGroup title={t("home.settings.plugins.popular_lists.enable_plugin")} className="">
|
||||
<ListItem
|
||||
title={t("home.settings.plugins.popular_lists.enable_popular_lists")}
|
||||
onPress={() => {
|
||||
updateSettings({ usePopularPlugin: true });
|
||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
value={settings.usePopularPlugin}
|
||||
onValueChange={(value) => {
|
||||
updateSettings({ usePopularPlugin: value });
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
||||
{t("home.settings.plugins.popular_lists.enable_popular_hint")}{" "}
|
||||
<Text className="text-blue-500" onPress={handleOpenLink}>
|
||||
{t("home.settings.plugins.popular_lists.read_more_about_popular_lists")}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
{settings.usePopularPlugin && (
|
||||
<>
|
||||
{!isLoadingMediaListCollections ? (
|
||||
<>
|
||||
{mediaListCollections?.length === 0 ? (
|
||||
<Text className="text-xs opacity-50 p-4">
|
||||
{t("home.settings.plugins.popular_lists.no_collections_found")}
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<ListGroup title="Media List Collections" className="mt-4">
|
||||
{mediaListCollections?.map((mlc) => (
|
||||
<ListItem key={mlc.Id} title={mlc.Name}>
|
||||
<Switch
|
||||
value={settings.mediaListCollectionIds?.includes(
|
||||
mlc.Id!
|
||||
)}
|
||||
onValueChange={(value) => {
|
||||
if (!settings.mediaListCollectionIds) {
|
||||
updateSettings({
|
||||
mediaListCollectionIds: [mlc.Id!],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateSettings({
|
||||
mediaListCollectionIds:
|
||||
settings.mediaListCollectionIds.includes(
|
||||
mlc.Id!
|
||||
)
|
||||
? settings.mediaListCollectionIds.filter(
|
||||
(id) => id !== mlc.Id
|
||||
)
|
||||
: [
|
||||
...settings.mediaListCollectionIds,
|
||||
mlc.Id!,
|
||||
],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</ListGroup>
|
||||
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
||||
{t("home.settings.plugins.popular_lists.select_the_lists_you_want_to_display")}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -55,6 +55,7 @@ const Page: React.FC = () => {
|
||||
data: details,
|
||||
isFetching,
|
||||
isLoading,
|
||||
refetch
|
||||
} = useQuery({
|
||||
enabled: !!jellyseerrApi && !!result && !!result.id,
|
||||
queryKey: ["jellyseerr", "detail", result.mediaType, result.id],
|
||||
@@ -63,6 +64,7 @@ const Page: React.FC = () => {
|
||||
refetchOnReconnect: true,
|
||||
refetchOnWindowFocus: true,
|
||||
retryOnMount: true,
|
||||
refetchInterval: 0,
|
||||
queryFn: async () => {
|
||||
return result.mediaType === MediaType.MOVIE
|
||||
? jellyseerrApi?.movieDetails(result.id!!)
|
||||
@@ -94,15 +96,18 @@ const Page: React.FC = () => {
|
||||
}, [jellyseerrApi, details, result, issueType, issueMessage]);
|
||||
|
||||
const request = useCallback(
|
||||
() =>
|
||||
async () => {
|
||||
requestMedia(mediaTitle, {
|
||||
mediaId: Number(result.id!!),
|
||||
mediaType: result.mediaType!!,
|
||||
tvdbId: details?.externalIds?.tvdbId,
|
||||
seasons: (details as TvDetails)?.seasons
|
||||
?.filter?.((s) => s.seasonNumber !== 0)
|
||||
?.map?.((s) => s.seasonNumber),
|
||||
}),
|
||||
mediaId: Number(result.id!!),
|
||||
mediaType: result.mediaType!!,
|
||||
tvdbId: details?.externalIds?.tvdbId,
|
||||
seasons: (details as TvDetails)?.seasons
|
||||
?.filter?.((s) => s.seasonNumber !== 0)
|
||||
?.map?.((s) => s.seasonNumber),
|
||||
},
|
||||
refetch
|
||||
)
|
||||
},
|
||||
[details, result, requestMedia]
|
||||
);
|
||||
|
||||
@@ -205,6 +210,7 @@ const Page: React.FC = () => {
|
||||
isLoading={isLoading || isFetching}
|
||||
result={result as TvResult}
|
||||
details={details as TvDetails}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
} from "@jellyfin/sdk/lib/utils/api";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { colletionTypeToItemType } from "@/utils/collectionTypeToItemType";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Page = () => {
|
||||
@@ -154,6 +153,8 @@ const Page = () => {
|
||||
itemType = "Series";
|
||||
} else if (library.CollectionType === "boxsets") {
|
||||
itemType = "BoxSet";
|
||||
} else if (library.CollectionType === "music") {
|
||||
itemType = "MusicAlbum";
|
||||
}
|
||||
|
||||
const response = await getItemsApi(api).getItems({
|
||||
|
||||
@@ -22,6 +22,9 @@ export default function IndexLayout() {
|
||||
headerLargeTitle: true,
|
||||
headerTitle: t("library.library_title"),
|
||||
headerBlurEffect: "prominent",
|
||||
headerLargeStyle: {
|
||||
backgroundColor: "black",
|
||||
},
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerShadowVisible: false,
|
||||
headerRight: () => (
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {commonScreenOptions, nestedTabPageScreenOptions} from "@/components/stacks/NestedTabPageStack";
|
||||
import {
|
||||
commonScreenOptions,
|
||||
nestedTabPageScreenOptions,
|
||||
} from "@/components/stacks/NestedTabPageStack";
|
||||
import { Stack } from "expo-router";
|
||||
import { Platform } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -13,6 +16,9 @@ export default function SearchLayout() {
|
||||
headerShown: true,
|
||||
headerLargeTitle: true,
|
||||
headerTitle: t("search.search_title"),
|
||||
headerLargeStyle: {
|
||||
backgroundColor: "black",
|
||||
},
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerShadowVisible: false,
|
||||
@@ -31,10 +37,7 @@ export default function SearchLayout() {
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="jellyseerr/page"
|
||||
options={commonScreenOptions}
|
||||
/>
|
||||
<Stack.Screen name="jellyseerr/page" options={commonScreenOptions} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ export default function search() {
|
||||
return (searchApi.data.SearchHints as BaseItemDto[]) || [];
|
||||
} else {
|
||||
if (!settings?.marlinServerUrl) return [];
|
||||
|
||||
const url = `${
|
||||
settings.marlinServerUrl
|
||||
}/search?q=${encodeURIComponent(query)}&includeItemTypes=${types
|
||||
@@ -105,6 +106,7 @@ export default function search() {
|
||||
.join("&includeItemTypes=")}`;
|
||||
|
||||
const response1 = await axios.get(url);
|
||||
|
||||
const ids = response1.data.ids;
|
||||
|
||||
if (!ids || !ids.length) return [];
|
||||
|
||||
@@ -106,7 +106,6 @@ export default function page() {
|
||||
} = useQuery({
|
||||
queryKey: ["item", itemId],
|
||||
queryFn: async () => {
|
||||
console.log("Offline:", offline);
|
||||
if (offline) {
|
||||
const item = await getDownloadedItem(itemId);
|
||||
if (item) return item.item;
|
||||
@@ -130,7 +129,6 @@ export default function page() {
|
||||
} = useQuery({
|
||||
queryKey: ["stream-url", itemId, mediaSourceId, bitrateValue],
|
||||
queryFn: async () => {
|
||||
console.log("Offline:", offline);
|
||||
if (offline) {
|
||||
const data = await getDownloadedItem(itemId);
|
||||
if (!data?.mediaSource) return null;
|
||||
@@ -197,8 +195,6 @@ export default function page() {
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Actually marked as paused");
|
||||
} else {
|
||||
videoRef.current?.play();
|
||||
if (!offline && stream) {
|
||||
@@ -341,7 +337,6 @@ export default function page() {
|
||||
React.useCallback(() => {
|
||||
return async () => {
|
||||
stop();
|
||||
console.log("Unmounted");
|
||||
};
|
||||
}, [])
|
||||
);
|
||||
@@ -351,10 +346,8 @@ export default function page() {
|
||||
useEffect(() => {
|
||||
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
||||
if (appState.match(/inactive|background/) && nextAppState === "active") {
|
||||
console.log("App has come to the foreground!");
|
||||
// Handle app coming to the foreground
|
||||
} else if (nextAppState.match(/inactive|background/)) {
|
||||
console.log("App has gone to the background!");
|
||||
// Handle app going to the background
|
||||
if (videoRef.current && videoRef.current.pause) {
|
||||
videoRef.current.pause();
|
||||
|
||||
@@ -169,18 +169,15 @@ export default function page() {
|
||||
);
|
||||
|
||||
const play = useCallback(() => {
|
||||
console.log("play");
|
||||
videoRef.current?.resume();
|
||||
reportPlaybackStart();
|
||||
}, [videoRef]);
|
||||
|
||||
const pause = useCallback(() => {
|
||||
console.log("play");
|
||||
videoRef.current?.pause();
|
||||
}, [videoRef]);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
console.log("stop");
|
||||
setIsPlaybackStopped(true);
|
||||
videoRef.current?.pause();
|
||||
reportPlaybackStopped();
|
||||
|
||||
@@ -260,13 +260,6 @@ const Player = () => {
|
||||
progress.value = ticks;
|
||||
cacheProgress.value = secondsToTicks(data.playableDuration);
|
||||
|
||||
console.log(
|
||||
"onProgress ~",
|
||||
ticks,
|
||||
isPlaying,
|
||||
`AUDIO index: ${audioIndex} SUB index" ${subtitleIndex}`
|
||||
);
|
||||
|
||||
// TODO: Use this when streaming with HLS url, but NOT when direct playing
|
||||
// TODO: since playable duration is always 0 then.
|
||||
setIsBuffering(data.playableDuration === 0);
|
||||
@@ -339,11 +332,7 @@ const Player = () => {
|
||||
|
||||
// Most likely the subtitle is burned in.
|
||||
if (embeddedTrackIndex === -1) return;
|
||||
console.log(
|
||||
"Setting selected text track",
|
||||
subtitleIndex,
|
||||
embeddedTrackIndex
|
||||
);
|
||||
|
||||
setSelectedTextTrack({
|
||||
type: SelectedTrackType.INDEX,
|
||||
value: embeddedTrackIndex,
|
||||
@@ -439,7 +428,6 @@ const Player = () => {
|
||||
setIsBuffering(e.isBuffering);
|
||||
}}
|
||||
onAudioTracks={(e) => {
|
||||
console.log("onAudioTracks: ", e.audioTracks);
|
||||
setAudioTracks(
|
||||
e.audioTracks.map((t) => ({
|
||||
index: t.index,
|
||||
@@ -493,7 +481,6 @@ const Player = () => {
|
||||
}}
|
||||
getAudioTracks={getAudioTracks}
|
||||
setAudioTrack={(i) => {
|
||||
console.log("setAudioTrack ~", i);
|
||||
setSelectedAudioTrack({
|
||||
type: SelectedTrackType.INDEX,
|
||||
value: i,
|
||||
|
||||
@@ -7,8 +7,6 @@ import { useTranslation } from "react-i18next";
|
||||
export default function page() {
|
||||
const searchParams = useGlobalSearchParams();
|
||||
const { t } = useTranslation();
|
||||
console.log(searchParams);
|
||||
|
||||
const { url } = searchParams as { url: string };
|
||||
|
||||
const videoId = useMemo(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { Input } from "@/components/common/Input";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { PreviousServersList } from "@/components/PreviousServersList";
|
||||
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||
@@ -8,7 +9,7 @@ import { getSystemApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { Image } from "expo-image";
|
||||
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
@@ -119,7 +120,7 @@ const CredentialsSchema = z.object({
|
||||
* - Sets loadingServerCheck state to true at the beginning and false at the end.
|
||||
* - Logs errors and timeout information to the console.
|
||||
*/
|
||||
async function checkUrl(url: string) {
|
||||
const checkUrl = useCallback(async (url: string) => {
|
||||
setLoadingServerCheck(true);
|
||||
|
||||
try {
|
||||
@@ -129,6 +130,7 @@ const CredentialsSchema = z.object({
|
||||
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as PublicSystemInfo;
|
||||
|
||||
setServerName(data.ServerName || "");
|
||||
return url;
|
||||
}
|
||||
@@ -137,7 +139,7 @@ const CredentialsSchema = z.object({
|
||||
} finally {
|
||||
setLoadingServerCheck(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handles the connection attempt to a Jellyfin server.
|
||||
@@ -155,7 +157,7 @@ const CredentialsSchema = z.object({
|
||||
* - Sets the server address using `setServer` if the connection is successful.
|
||||
*
|
||||
*/
|
||||
const handleConnect = async (url: string) => {
|
||||
const handleConnect = useCallback(async (url: string) => {
|
||||
url = url.trim();
|
||||
|
||||
const result = await checkUrl(url);
|
||||
@@ -169,7 +171,7 @@ const CredentialsSchema = z.object({
|
||||
}
|
||||
|
||||
setServer({ address: url });
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleQuickConnect = async () => {
|
||||
try {
|
||||
@@ -206,7 +208,7 @@ const CredentialsSchema = z.object({
|
||||
) : t("login.login_title")}
|
||||
</>
|
||||
</Text>
|
||||
<Text className="text-xs text-neutral-400">{serverURL}</Text>
|
||||
<Text className="text-xs text-neutral-400">{api.basePath}</Text>
|
||||
<Input
|
||||
placeholder={t("login.username_placeholder")}
|
||||
onChangeText={(text) =>
|
||||
@@ -292,9 +294,14 @@ const CredentialsSchema = z.object({
|
||||
textContentType="URL"
|
||||
maxLength={500}
|
||||
/>
|
||||
<Text className="text-xs text-neutral-500">
|
||||
<Text className="text-xs text-neutral-500 ml-4">
|
||||
{t("server.server_url_hint")}
|
||||
</Text>
|
||||
<PreviousServersList
|
||||
onServerSelect={(s) => {
|
||||
handleConnect(s.address);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View className="mb-2 absolute bottom-0 left-0 w-full px-4">
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user