This commit is contained in:
Fredrik Burmester
2024-08-02 08:47:39 +02:00
parent 98880e05ec
commit 634f28ac28
35 changed files with 1488 additions and 546 deletions

View File

@@ -19,7 +19,6 @@ export default function TabLayout() {
options={{
headerShown: true,
headerStyle: { backgroundColor: "black" },
title: "Home",
tabBarIcon: ({ color, focused }) => (
<TabBarIcon

View File

@@ -1,7 +1,7 @@
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Text } from "@/components/common/Text";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { ItemCardText } from "@/components/ItemCardText";
import { Loading } from "@/components/Loading";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi, getSuggestionsApi } from "@jellyfin/sdk/lib/utils/api";
@@ -26,6 +26,9 @@ export default function index() {
if (!api || !user?.Id) {
return [];
}
console.log("[2] Items");
const response = await getItemsApi(api).getResumeItems({
userId: user.Id,
});
@@ -48,6 +51,18 @@ export default function index() {
})
).data;
const order = ["boxsets", "tvshows", "movies"];
const cs = data.Items?.sort((a, b) => {
if (
order.indexOf(a.CollectionType!) < order.indexOf(b.CollectionType!)
) {
return 1;
}
return -1;
});
return data.Items || [];
},
enabled: !!api && !!user?.Id,
@@ -85,56 +100,53 @@ export default function index() {
return (
<ScrollView nestedScrollEnabled>
<View className="py-4 px-4 gap-y-2">
<Text className="text-2xl font-bold">Continue Watching</Text>
<ScrollView horizontal>
<View className="flex flex-row gap-x-2">
{data.map((item) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<View>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableOpacity>
))}
</View>
</ScrollView>
<Text className="text-2xl font-bold">Collections</Text>
<ScrollView horizontal>
<View className="flex flex-row gap-x-2">
{collections?.map((item) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/collections/${item.Id}/page`)}
className="flex flex-col w-48"
>
<View>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableOpacity>
))}
</View>
</ScrollView>
<Text className="text-2xl font-bold">Suggestions</Text>
<ScrollView horizontal>
<View className="flex flex-row gap-x-2">
{suggestions?.map((item) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<View className="py-4 gap-y-2">
<Text className="px-4 text-2xl font-bold mb-2">Continue Watching</Text>
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<View>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableOpacity>
))}
</View>
</ScrollView>
</View>
</TouchableOpacity>
)}
/>
<Text className="px-4 text-2xl font-bold mb-2">Collections</Text>
<HorizontalScroll<BaseItemDto>
data={collections}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/collections/${item.Id}/page`)}
className="flex flex-col w-48"
>
<View>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableOpacity>
)}
/>
<Text className="px-4 text-2xl font-bold mb-2">Suggestions</Text>
<HorizontalScroll<BaseItemDto>
data={suggestions}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableOpacity>
)}
/>
</View>
</ScrollView>
);

View File

@@ -1,7 +1,12 @@
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { ItemCardText } from "@/components/ItemCardText";
import MoviePoster from "@/components/MoviePoster";
import Poster from "@/components/Poster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import { getUserItemData } from "@/utils/jellyfin";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getSearchApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
@@ -12,29 +17,12 @@ import { ScrollView, TouchableOpacity, View } from "react-native";
export default function search() {
const [search, setSearch] = useState<string>("");
const [totalResults, setTotalResults] = useState<number>(0);
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
// useEffect(() => {
// (async () => {
// if (!api || search.length === 0) return;
// const searchApi = await getSearchApi(api).getSearchHints({
// searchTerm: search,
// limit: 10,
// includeItemTypes: ["Movie"],
// });
// const data = searchApi.data;
// setTotalResults(data.TotalRecordCount || 0);
// setData(data.SearchHints || []);
// })();
// }, [search]);
const { data } = useQuery({
queryKey: ["search", search],
const { data: movies } = useQuery({
queryKey: ["search-movies", search],
queryFn: async () => {
if (!api || !user || search.length === 0) return [];
@@ -48,44 +36,166 @@ export default function search() {
},
});
return (
<View className="p-4">
<View className="mb-4">
<Input
placeholder="Search here..."
value={search}
onChangeText={(text) => setSearch(text)}
/>
</View>
const { data: series } = useQuery({
queryKey: ["search-series", search],
queryFn: async () => {
if (!api || !user || search.length === 0) return [];
<ScrollView>
<View className="rounded-xl overflow-hidden">
{data?.map((item, index) => (
<RenderItem item={item} key={index} />
))}
const searchApi = await getSearchApi(api).getSearchHints({
searchTerm: search,
limit: 10,
includeItemTypes: ["Series"],
});
return searchApi.data.SearchHints;
},
});
const { data: episodes } = useQuery({
queryKey: ["search-episodes", search],
queryFn: async () => {
if (!api || !user || search.length === 0) return [];
const searchApi = await getSearchApi(api).getSearchHints({
searchTerm: search,
limit: 10,
includeItemTypes: ["Episode"],
});
return searchApi.data.SearchHints;
},
});
return (
<ScrollView keyboardDismissMode="on-drag">
<View className="p-4 flex flex-col">
<View className="mb-4">
<Input
autoCorrect={false}
returnKeyType="done"
keyboardType="web-search"
placeholder="Search here..."
value={search}
onChangeText={(text) => setSearch(text)}
/>
</View>
</ScrollView>
</View>
<Text className="font-bold text-2xl mb-2">Movies</Text>
<SearchItemWrapper
ids={movies?.map((m) => m.Id!)}
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item) => (
<TouchableOpacity
key={item.Id}
className="flex flex-col w-32"
onPress={() => router.push(`/items/${item.Id}/page`)}
>
<MoviePoster item={item} key={item.Id} />
<Text className="mt-2">{item.Name}</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
</TouchableOpacity>
)}
/>
)}
/>
<Text className="font-bold text-2xl my-2">Series</Text>
<SearchItemWrapper
ids={series?.map((m) => m.Id!)}
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/series/${item.Id}/page`)}
className="flex flex-col w-32"
>
<Poster itemId={item.Id} key={item.Id} />
<Text className="mt-2">{item.Name}</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}
</Text>
</TouchableOpacity>
)}
/>
)}
/>
<Text className="font-bold text-2xl my-2">Episodes</Text>
<SearchItemWrapper
ids={episodes?.map((m) => m.Id!)}
renderItem={(data) => (
<HorizontalScroll<BaseItemDto>
data={data}
renderItem={(item) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</TouchableOpacity>
)}
/>
)}
/>
{/* <Text>Series</Text>
<HorizontalScroll
data={series}
renderItem={(item, index) => <Poster itemId={item.Id} key={index} />}
/>
<Text>Episodes</Text>
<HorizontalScroll
data={episodes}
renderItem={(item, index) => (
<ContinueWatchingPoster item={item} key={index} />
)}
/> */}
</View>
</ScrollView>
);
}
type RenderItemProps = {
item: BaseItemDto;
type Props = {
ids?: string[] | null;
renderItem: (data: BaseItemDto[]) => React.ReactNode;
};
const RenderItem: React.FC<RenderItemProps> = ({ item }) => {
return (
<TouchableOpacity
onPress={() => router.push(`/(auth)/items/${item.Id}/page`)}
className="flex flex-row items-center justify-between p-4 bg-neutral-900 border-neutral-800"
>
<View className="flex flex-col">
<Text className="font-bold">{item.Name}</Text>
{item.Type === "Movie" && (
<Text className="opacity-50">{item.ProductionYear}</Text>
)}
</View>
<Ionicons name="arrow-forward" size={24} color="white" />
</TouchableOpacity>
);
const SearchItemWrapper: React.FC<Props> = ({ ids, renderItem }) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data, isLoading: l1 } = useQuery({
queryKey: ["items", ids],
queryFn: async () => {
if (!user?.Id || !api || !ids || ids.length === 0) {
return [];
}
const itemPromises = ids.map((id) =>
getUserItemData({
api,
userId: user.Id,
itemId: id,
})
);
const results = await Promise.all(itemPromises);
// Filter out null items
return results.filter((item) => item !== null);
},
enabled: !!ids && ids.length > 0 && !!api && !!user?.Id,
staleTime: Infinity,
});
if (!data) return <Text className="opacity-50 text-xs">No results</Text>;
return renderItem(data);
};

View File

@@ -36,8 +36,6 @@ const page: React.FC = () => {
})
).data;
console.log(data.Items?.find((item) => item.Id == collectionId));
return data.Items?.find((item) => item.Id == collectionId);
},
enabled: !!api && !!user?.Id,
@@ -143,6 +141,8 @@ const page: React.FC = () => {
onPress={() => {
if (collection?.CollectionType === "movies") {
router.push(`/items/${item.Id}/page`);
} else if (collection?.CollectionType === "tvshows") {
router.push(`/series/${item.Id}/page`);
}
}}
>

View File

@@ -1,5 +1,9 @@
import { LargePoster } from "@/components/common/LargePoster";
import { Text } from "@/components/common/Text";
import { DownloadItem } from "@/components/DownloadItem";
import { PlayedStatus } from "@/components/PlayedStatus";
import { CastAndCrew } from "@/components/series/CastAndCrew";
import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SimilarItems } from "@/components/SimilarItems";
import { VideoPlayer } from "@/components/VideoPlayer";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
@@ -7,17 +11,10 @@ import { getBackdrop, getStreamUrl, getUserItemData } from "@/utils/jellyfin";
import { Ionicons } from "@expo/vector-icons";
import {} from "@jellyfin/sdk/lib/utils/url";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { Stack, useLocalSearchParams, useNavigation } from "expo-router";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect } from "react";
import {
ActivityIndicator,
Dimensions,
SafeAreaView,
ScrollView,
View,
} from "react-native";
import { ActivityIndicator, ScrollView, View } from "react-native";
const page: React.FC = () => {
const local = useLocalSearchParams();
@@ -40,8 +37,6 @@ const page: React.FC = () => {
staleTime: Infinity,
});
const screenWidth = Dimensions.get("window").width;
const { data: playbackURL, isLoading: l2 } = useQuery({
queryKey: ["playbackUrl", id],
queryFn: async () => {
@@ -57,13 +52,6 @@ const page: React.FC = () => {
staleTime: Infinity,
});
const { data: url } = useQuery({
queryKey: ["backdrop", item?.Id],
queryFn: async () => getBackdrop(api, item),
enabled: !!api && !!item?.Id,
staleTime: Infinity,
});
useEffect(() => {
navigation.setOptions({
headerRight: () => {
@@ -90,16 +78,9 @@ const page: React.FC = () => {
if (!playbackURL) return null;
return (
<ScrollView style={[{ flex: 1 }]}>
{posterUrl && (
<View className="p-4 rounded-xl overflow-hidden ">
<Image
source={{ uri: posterUrl }}
className="w-full aspect-video rounded-xl overflow-hidden border border-neutral-800"
/>
</View>
)}
<View className="flex flex-col text-center px-4 mb-4">
<ScrollView style={[{ flex: 1 }]} keyboardDismissMode="on-drag">
<LargePoster url={posterUrl} />
<View className="flex flex-col px-4 mb-4">
<View className="flex flex-col">
{item.Type === "Episode" ? (
<>
@@ -107,6 +88,13 @@ const page: React.FC = () => {
<Text className="text-center font-bold text-2xl">
{item?.Name}
</Text>
<Text className="text-center opacity-50">
{`S${item?.SeasonName?.replace("Season ", "")}:E${(
item.IndexNumber || 0
).toString()}`}
{" - "}
{item.ProductionYear}
</Text>
</>
) : (
<>
@@ -120,15 +108,54 @@ const page: React.FC = () => {
)}
</View>
<View className="justify-center items-center w-full my-4">
<View className="flex flex-row justify-center items-center w-full my-4 space-x-4">
{playbackURL && <DownloadItem item={item} url={playbackURL} />}
<View className="ml-4">
<PlayedStatus item={item} />
</View>
</View>
<Text>{item.Overview}</Text>
</View>
<View className="flex flex-col p-4">
<VideoPlayer itemId={item.Id} />
</View>
<ScrollView horizontal className="flex px-4 mb-4">
<View className="flex flex-row space-x-2 ">
<View className="flex flex-col">
<Text className="text-sm opacity-70">Video</Text>
<Text className="text-sm opacity-70">Audio</Text>
<Text className="text-sm opacity-70">Subtitles</Text>
</View>
<View className="flex flex-col">
<Text className="text-sm opacity-70">
{item.MediaStreams?.find((i) => i.Type === "Video")?.DisplayTitle}
</Text>
<Text className="text-sm opacity-70">
{item.MediaStreams?.find((i) => i.Type === "Audio")?.DisplayTitle}
</Text>
<Text className="text-sm opacity-70">
{
item.MediaStreams?.find((i) => i.Type === "Subtitle")
?.DisplayTitle
}
</Text>
</View>
</View>
</ScrollView>
<View className="px-4 mb-4">
<CastAndCrew item={item} />
</View>
{item.Type === "Episode" && (
<View className="px-4 mb-4">
<CurrentSeries item={item} />
</View>
)}
<SimilarItems itemId={item.Id} />
<View className="h-12"></View>
</ScrollView>
);
};

View File

@@ -0,0 +1,61 @@
import { Text } from "@/components/common/Text";
import MoviePoster from "@/components/MoviePoster";
import { NextUp } from "@/components/series/NextUp";
import { SeasonPicker } from "@/components/series/SeasonPicker";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageById, getUserItemData, nextUp } from "@/utils/jellyfin";
import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useEffect } from "react";
import { ScrollView, View } from "react-native";
const page: React.FC = () => {
const params = useLocalSearchParams();
const { id: seriesId } = params as { id: string };
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data: item } = useQuery({
queryKey: ["item", seriesId],
queryFn: async () =>
await getUserItemData({
api,
userId: user?.Id,
itemId: seriesId,
}),
enabled: !!seriesId && !!api,
staleTime: Infinity,
});
const { data: next } = useQuery({
queryKey: ["nextUp", seriesId],
queryFn: async () =>
await nextUp({
userId: user?.Id,
api,
itemId: seriesId,
}),
enabled: !!api && !!seriesId && !!user?.Id,
staleTime: 0,
});
if (!item) return null;
return (
<ScrollView>
<View className="flex flex-col px-4 pt-4 pb-8">
<MoviePoster item={item} />
<View className="my-4">
<Text className="text-3xl font-bold">{item?.Name}</Text>
<Text className="">{item?.Overview}</Text>
</View>
<SeasonPicker item={item} />
<NextUp items={next} />
</View>
</ScrollView>
);
};
export default page;

View File

@@ -4,15 +4,17 @@ import { runningProcesses } from "@/components/DownloadItem";
import { ListItem } from "@/components/ListItem";
import ProgressCircle from "@/components/ProgressCircle";
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import { readFromLog } from "@/utils/log";
import { Ionicons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useQuery } from "@tanstack/react-query";
import * as FileSystem from "expo-file-system";
import { useRouter } from "expo-router";
import { FFmpegKit } from "ffmpeg-kit-react-native";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import { ScrollView, TouchableOpacity, View } from "react-native";
const deleteAllFiles = async () => {
const directoryUri = FileSystem.documentDirectory;
@@ -83,90 +85,118 @@ export default function settings() {
})();
}, [key]);
const { data: logs } = useQuery({
queryKey: ["logs"],
queryFn: async () => readFromLog(),
});
return (
<View className="p-4 flex flex-col gap-y-4">
<Text className="font-bold text-2xl">Information</Text>
<ScrollView>
<View className="p-4 flex flex-col gap-y-4 pb-12">
<Text className="font-bold text-2xl">Information</Text>
<View className="rounded-xl mb-4 overflow-hidden border-neutral-800 divide-y-2 divide-neutral-900">
<ListItem title="User" subTitle={user?.Name} />
<ListItem title="Server" subTitle={api?.basePath} />
</View>
<Button onPress={logout}>Log out</Button>
<View className="rounded-xl mb-4 overflow-hidden border-neutral-800 divide-y-2 divide-neutral-900">
<ListItem title="User" subTitle={user?.Name} />
<ListItem title="Server" subTitle={api?.basePath} />
</View>
<View className="mb-4">
<Text className="font-bold text-2xl">Downloads</Text>
<Button onPress={logout}>Log out</Button>
{files.length > 0 ? (
<View>
{files.map((file) => (
<TouchableOpacity
key={file.Id}
className="rounded-xl overflow-hidden"
onPress={() => {
router.back();
router.push(
`/(auth)/player/offline/page?url=${file.Id}.mp4&itemId=${file.Id}`
);
}}
>
<ListItem
title={file.Name}
subTitle={file.ProductionYear?.toString()}
iconAfter={
<TouchableOpacity
onPress={() => {
deleteFile(file.Id);
setKey((prevKey) => prevKey + 1);
}}
>
<Ionicons
name="close-circle-outline"
size={24}
color="white"
/>
</TouchableOpacity>
}
<View className="mb-4">
<Text className="font-bold text-2xl mb-4">Downloads</Text>
{files.length > 0 ? (
<View>
{files.map((file) => (
<TouchableOpacity
key={file.Id}
className="rounded-xl overflow-hidden mb-2"
onPress={() => {
router.back();
router.push(
`/(auth)/player/offline/page?url=${file.Id}.mp4&itemId=${file.Id}`
);
}}
>
<ListItem
title={file.Name}
subTitle={file.ProductionYear?.toString()}
iconAfter={
<TouchableOpacity
onPress={() => {
deleteFile(file.Id);
setKey((prevKey) => prevKey + 1);
}}
>
<Ionicons
name="close-circle-outline"
size={24}
color="white"
/>
</TouchableOpacity>
}
/>
</TouchableOpacity>
))}
</View>
) : activeProcess ? (
<ListItem
title={activeProcess.item.Name}
iconAfter={
<ProgressCircle
size={22}
fill={activeProcess.progress}
width={3}
tintColor="#3498db"
backgroundColor="#bdc3c7"
/>
</TouchableOpacity>
))}
</View>
) : activeProcess ? (
<ListItem
title={activeProcess.item.Name}
iconAfter={
<ProgressCircle
size={22}
fill={activeProcess.progress}
width={3}
tintColor="#3498db"
backgroundColor="#bdc3c7"
/>
}
/>
) : (
<Text className="opacity-50">No downloaded files</Text>
)}
</View>
}
/>
) : (
<Text className="opacity-50">No downloaded files</Text>
)}
</View>
<Button
onPress={() => {
deleteAllFiles();
setKey((prevKey) => prevKey + 1);
}}
>
Clear files data
</Button>
{session?.item.Id && (
<Button
className="mb-2"
color="red"
onPress={() => {
FFmpegKit.cancel();
setSession(null);
deleteAllFiles();
setKey((prevKey) => prevKey + 1);
}}
>
Cancel all downloads
Clear downloads
</Button>
)}
</View>
{session?.item.Id && (
<Button
className="mb-2"
onPress={() => {
FFmpegKit.cancel();
setSession(null);
}}
>
Cancel all downloads
</Button>
)}
<Text className="font-bold">Logs</Text>
<View className="flex flex-col space-y-2">
{logs?.map((l) => (
<View className="bg-neutral-800 border border-neutral-900 rounded p-2">
<Text
className={`
${l.level === "INFO" && "text-blue-500"}
${l.level === "ERROR" && "text-red-500"}
`}
>
{l.level}
</Text>
<Text>{l.message}</Text>
</View>
))}
</View>
</View>
</ScrollView>
);
}

View File

@@ -1,145 +0,0 @@
import { Button } from "@/components/Button";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
import { useAtom } from "jotai";
import React, { useMemo, useState } from "react";
import { KeyboardAvoidingView, Platform, View } from "react-native";
import { z } from "zod";
const CredentialsSchema = z.object({
username: z.string().min(1, "Username is required"),
password: z.string().min(1, "Password is required"),
});
const Login: React.FC = () => {
const { setServer, login, removeServer } = useJellyfin();
const [api] = useAtom(apiAtom);
const [serverURL, setServerURL] = useState<string>("");
const [credentials, setCredentials] = useState<{
username: string;
password: string;
}>({
username: "",
password: "",
});
const [loading, setLoading] = useState<boolean>(false);
const handleLogin = async () => {
setLoading(true);
const result = CredentialsSchema.safeParse(credentials);
if (result.success) {
await login(credentials.username, credentials.password);
}
setLoading(false);
};
const parsedServerURL = useMemo(() => {
let parsedServerURL = serverURL.trim();
if (parsedServerURL) {
parsedServerURL = parsedServerURL.endsWith("/")
? parsedServerURL.replace("/", "")
: parsedServerURL;
parsedServerURL = parsedServerURL.startsWith("http")
? parsedServerURL
: "http://" + parsedServerURL;
return parsedServerURL;
}
return "";
}, [serverURL]);
const handleConnect = (url: string) => {
setServer({ address: url });
};
if (api?.basePath) {
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View className="flex flex-col px-4 justify-center h-full gap-y-2">
<Text className="text-3xl font-bold">Jellyfin</Text>
<Text className="opacity-50 mb-2">Server: {api.basePath}</Text>
<Button
onPress={() => {
removeServer();
setServerURL("");
}}
>
Change server
</Button>
<Text className="text-2xl font-bold">Log in</Text>
<Input
placeholder="Username"
onChangeText={(text) =>
setCredentials({ ...credentials, username: text })
}
value={credentials.username}
autoFocus
secureTextEntry={false}
keyboardType="default"
returnKeyType="done"
autoCapitalize="none"
textContentType="username"
clearButtonMode="while-editing"
maxLength={500}
/>
<Input
className="mb-2"
placeholder="Password"
onChangeText={(text) =>
setCredentials({ ...credentials, password: text })
}
value={credentials.password}
secureTextEntry
keyboardType="default"
returnKeyType="done"
autoCapitalize="none"
textContentType="password"
clearButtonMode="while-editing"
maxLength={500}
/>
<Button onPress={handleLogin} loading={loading}>
Log in
</Button>
</View>
</KeyboardAvoidingView>
);
}
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View className="flex flex-col px-4 justify-center h-full gap-y-2">
<Text className="text-3xl font-bold">Jellyfin</Text>
<Text className="opacity-50">Enter a server adress</Text>
<Input
className="mb-2"
placeholder="http(s)://..."
onChangeText={setServerURL}
value={serverURL}
autoFocus
secureTextEntry={false}
keyboardType="url"
returnKeyType="done"
autoCapitalize="none"
textContentType="URL"
clearButtonMode="while-editing"
maxLength={500}
/>
<Button onPress={() => handleConnect(parsedServerURL)}>Connect</Button>
</View>
</KeyboardAvoidingView>
);
};
export default Login;

View File

@@ -69,7 +69,6 @@ export default function RootLayout() {
name="(auth)/settings"
options={{
headerShown: true,
title: "Settings",
presentation: "modal",
headerLeft: () => (
@@ -106,7 +105,16 @@ export default function RootLayout() {
}}
/>
<Stack.Screen
name="(public)/login"
name="(auth)/series/[id]/page"
options={{
title: "",
headerShown: true,
headerStyle: { backgroundColor: "transparent" },
headerShadowVisible: true,
}}
/>
<Stack.Screen
name="login"
options={{ headerShown: false, title: "Login" }}
/>
<Stack.Screen name="+not-found" />