Compare commits

..

8 Commits

Author SHA1 Message Date
Fredrik Burmester
6c051f6f61 first commit 2024-10-09 07:49:22 +02:00
Fredrik Burmester
0a098bf26e chore 2024-10-09 07:28:53 +02:00
Fredrik Burmester
f6cb90e5dc feat: add logo to login 2024-10-09 07:28:49 +02:00
Fredrik Burmester
b878e93dec Merge pull request #163 from Alexk2309/master
Removed resumable items from next up
2024-10-08 19:55:28 +02:00
Fredrik Burmester
66cd36a899 feat: native selectable text for titles 2024-10-08 19:53:48 +02:00
Fredrik Burmester
91b926e6c2 fix: larger tap area 2024-10-08 19:53:38 +02:00
Fredrik Burmester
d4cc7499c0 Merge pull request #165 from fredrikburmester/refactor/player
Refactor: Update the player logic (video, music, live-tv)
2024-10-08 18:54:43 +02:00
Alex Kim
6012f8c8d2 Removed resumable items from next up 2024-10-09 02:40:11 +11:00
20 changed files with 391 additions and 85 deletions

View File

@@ -42,11 +42,17 @@ export default function IndexLayout() {
}}
/>
<Stack.Screen
name="settings"
name="settings/index"
options={{
title: "Settings",
}}
/>
<Stack.Screen
name="settings/audio-language"
options={{
title: "Audio Language",
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
<Stack.Screen key={name} name={name} options={options} />
))}

View File

@@ -264,6 +264,7 @@ export default function index() {
fields: ["MediaSourceCount"],
limit: 20,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
enableResumable: false,
})
).data.Items || [],
type: "ScrollingCollectionList",

View File

@@ -0,0 +1,61 @@
import { ScrollView, View, ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { LANGUAGES } from "@/constants/Languages";
import { ListItem } from "@/components/list/ListItem";
import { ListSection } from "@/components/list/ListSection";
import { TAB_HEIGHT } from "@/constants/Values";
import { DefaultLanguageOption, useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons";
import { Colors } from "@/constants/Colors";
interface Props extends ViewProps {}
export default function page() {
const insets = useSafeAreaInsets();
const [settings, updateSettings] = useSettings();
return (
<ScrollView
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
}}
style={{
marginBottom: TAB_HEIGHT,
}}
>
<View className="py-4 px-4">
<ListSection title="LANGUAGES">
{LANGUAGES.sort(sortByName).map((l) => (
<ListItem
key={l.value}
title={l.label}
onPress={() => {
updateSettings({
...settings,
defaultAudioLanguage: l,
});
}}
iconAfter={
settings?.defaultAudioLanguage?.value === l.value ? (
<Ionicons name="checkmark" size={24} color={Colors.primary} />
) : null
}
/>
))}
</ListSection>
</View>
</ScrollView>
);
}
const sortByName = (a: DefaultLanguageOption, b: DefaultLanguageOption) => {
if (a.label < b.label) {
return -1;
}
if (a.label > b.label) {
return 1;
}
return 0;
};

View File

@@ -1,13 +1,18 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { ListItem } from "@/components/ListItem";
import { ListInputItem } from "@/components/list/ListInputItem";
import { ListItem } from "@/components/list/ListItem";
import { ListSection } from "@/components/list/ListSection";
import { SettingToggles } from "@/components/settings/SettingToggles";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { clearLogs, readFromLog } from "@/utils/log";
import { Ionicons } from "@expo/vector-icons";
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import * as Haptics from "expo-haptics";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
import { Alert, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -16,6 +21,7 @@ import { toast } from "sonner-native";
export default function settings() {
const { logout } = useJellyfin();
const { deleteAllFiles } = useDownload();
const [settings, updateSettings] = useSettings();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -57,6 +63,8 @@ export default function settings() {
);
};
const router = useRouter();
return (
<ScrollView
contentContainerStyle={{
@@ -73,15 +81,46 @@ export default function settings() {
>
registerBackgroundFetchAsync
</Button> */}
<View>
<Text className="font-bold text-lg mb-2">User Info</Text>
<ListSection title="USER INFO">
<ListItem title="User" text={user?.Name} />
<ListItem title="Server" text={api?.basePath} />
<ListItem title="Token" text={api?.accessToken} />
</ListSection>
<View className="flex flex-col rounded-xl overflow-hidden border-neutral-800 divide-y-2 divide-solid divide-neutral-800 ">
<ListItem title="User" subTitle={user?.Name} />
<ListItem title="Server" subTitle={api?.basePath} />
<ListItem title="Token" subTitle={api?.accessToken} />
</View>
</View>
<ListSection title="MEDIA">
<ListItem
title="Audio language"
iconAfter={
<Ionicons name="chevron-forward" size={20} color="white" />
}
onPress={() => router.push("/settings/audio-language")}
/>
<ListItem
title="Subtitle language"
iconAfter={
<Ionicons name="chevron-forward" size={20} color="white" />
}
onPress={() => router.push("/settings/subtitle-language")}
/>
<ListInputItem
textInputProps={{
placeholder: "30",
clearButtonMode: "never",
returnKeyType: "done",
}}
defaultValue={(settings?.forwardSkipTime || "").toString()}
title={"Forward skip"}
onChange={(val) => {
// 1. validate positive number
// 2. save settings
if (val.length === 0) return;
if (val.match(/^\d+$/)) {
} else {
toast.error("Invalid number");
}
}}
/>
</ListSection>
<View>
<Text className="font-bold text-lg mb-2">Quick connect</Text>
@@ -143,7 +182,9 @@ export default function settings() {
>
{log.level}
</Text>
<Text className="text-xs">{log.message}</Text>
<Text uiTextView selectable className="text-xs">
{log.message}
</Text>
</View>
))}
{logs?.length === 0 && (

View File

@@ -0,0 +1,61 @@
import { ScrollView, View, ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { LANGUAGES } from "@/constants/Languages";
import { ListItem } from "@/components/list/ListItem";
import { ListSection } from "@/components/list/ListSection";
import { TAB_HEIGHT } from "@/constants/Values";
import { DefaultLanguageOption, useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons";
import { Colors } from "@/constants/Colors";
interface Props extends ViewProps {}
export default function page() {
const insets = useSafeAreaInsets();
const [settings, updateSettings] = useSettings();
return (
<ScrollView
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
}}
style={{
marginBottom: TAB_HEIGHT,
}}
>
<View className="py-4 px-4">
<ListSection title="LANGUAGES">
{LANGUAGES.sort(sortByName).map((l) => (
<ListItem
key={l.value}
title={l.label}
onPress={() => {
updateSettings({
...settings,
defaultSubtitleLanguage: l,
});
}}
iconAfter={
settings?.defaultSubtitleLanguage?.value === l.value ? (
<Ionicons name="checkmark" size={24} color={Colors.primary} />
) : null
}
/>
))}
</ListSection>
</View>
</ScrollView>
);
}
const sortByName = (a: DefaultLanguageOption, b: DefaultLanguageOption) => {
if (a.label < b.label) {
return -1;
}
if (a.label > b.label) {
return 1;
}
return 0;
};

View File

@@ -5,6 +5,7 @@ import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
import { getSystemApi } from "@jellyfin/sdk/lib/utils/api";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import React, { useEffect, useState } from "react";
@@ -129,7 +130,7 @@ const Login: React.FC = () => {
if (error.name === "AbortError") {
console.log(`Request to ${protocol}${url} timed out`);
} else {
console.error(`Error checking ${protocol}${url}:`, error);
console.log(`Error checking ${protocol}${url}:`, error);
}
}
}
@@ -195,16 +196,18 @@ const Login: React.FC = () => {
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1, height: "100%" }}
>
<View className="flex flex-col justify-between px-4 h-full gap-y-2">
<View></View>
<View>
<View className="flex flex-col w-full h-full relative items-center justify-center">
<View className="px-4 -mt-20">
<View className="mb-4">
<Text className="text-3xl font-bold mb-1">
{serverName || "Streamyfin"}
</Text>
<Text className="text-neutral-500 mb-2">
Server: {api.basePath}
</Text>
<View className="bg-neutral-900 rounded-xl p-4 mb-2 flex flex-row items-center justify-between">
<Text className="">URL</Text>
<Text numberOfLines={1} className="shrink">
{api.basePath}
</Text>
</View>
<Button
color="black"
onPress={() => {
@@ -261,11 +264,11 @@ const Login: React.FC = () => {
<Text className="text-red-600 mb-2">{error}</Text>
</View>
<View className="mt-auto mb-2">
<View className="absolute bottom-0 left-0 w-full px-4 mb-2">
<Button
color="black"
onPress={handleQuickConnect}
className="mb-2"
className="w-full mb-2"
>
Use Quick Connect
</Button>
@@ -285,9 +288,17 @@ const Login: React.FC = () => {
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View className="flex flex-col px-4 justify-between h-full">
<View></View>
<View className="flex flex-col gap-y-2">
<View className="flex flex-col h-full relative 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">
Connect to your Jellyfin server
@@ -303,14 +314,16 @@ const Login: React.FC = () => {
maxLength={500}
/>
</View>
<Button
loading={loadingServerCheck}
disabled={loadingServerCheck}
onPress={async () => await handleConnect(serverURL)}
className="mb-2"
>
Connect
</Button>
<View className="mb-2 absolute bottom-0 left-0 w-full px-4">
<Button
loading={loadingServerCheck}
disabled={loadingServerCheck}
onPress={async () => await handleConnect(serverURL)}
className="w-full grow"
>
Connect
</Button>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,35 +0,0 @@
import { PropsWithChildren, ReactNode } from "react";
import { View, ViewProps } from "react-native";
import { Text } from "./common/Text";
interface Props extends ViewProps {
title?: string | null | undefined;
subTitle?: string | null | undefined;
children?: ReactNode;
iconAfter?: ReactNode;
}
export const ListItem: React.FC<PropsWithChildren<Props>> = ({
title,
subTitle,
iconAfter,
children,
...props
}) => {
return (
<View
className="flex flex-row items-center justify-between bg-neutral-900 p-4"
{...props}
>
<View className="flex flex-col">
<Text className="font-bold ">{title}</Text>
{subTitle && (
<Text className="text-xs" selectable>
{subTitle}
</Text>
)}
</View>
{iconAfter}
</View>
);
};

View File

@@ -0,0 +1,22 @@
import { ScrollView, View, ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { TAB_HEIGHT } from "@/constants/Values";
interface Props extends ViewProps {}
export default function page() {
const insets = useSafeAreaInsets();
return (
<ScrollView
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: 16,
}}
style={{
marginBottom: TAB_HEIGHT,
}}
></ScrollView>
);
}

View File

@@ -24,14 +24,14 @@ export const HeaderBackButton: React.FC<Props> = ({
if (background === "transparent" && Platform.OS !== "android")
return (
<BlurView
{...props}
intensity={100}
className="overflow-hidden rounded-full p-2"
<TouchableOpacity
onPress={() => router.back()}
{...touchableOpacityProps}
>
<TouchableOpacity
onPress={() => router.back()}
{...touchableOpacityProps}
<BlurView
{...props}
intensity={100}
className="overflow-hidden rounded-full p-2"
>
<Ionicons
className="drop-shadow-2xl"
@@ -39,8 +39,8 @@ export const HeaderBackButton: React.FC<Props> = ({
size={24}
color="white"
/>
</TouchableOpacity>
</BlurView>
</BlurView>
</TouchableOpacity>
);
return (

View File

@@ -10,9 +10,9 @@ export function Input(props: TextInputProps) {
className="p-4 border border-neutral-800 rounded-xl bg-neutral-900"
allowFontScaling={false}
style={[{ color: "white" }, style]}
{...otherProps}
placeholderTextColor={"#9CA3AF"}
clearButtonMode="while-editing"
{...otherProps}
/>
);
}

View File

@@ -1,11 +1,16 @@
import React from "react";
import { TextProps } from "react-native";
import { Text as DefaultText } from "react-native";
export function Text(props: TextProps) {
import { UITextView } from "react-native-uitextview";
export function Text(
props: TextProps & {
uiTextView?: boolean;
}
) {
const { style, ...otherProps } = props;
return (
<DefaultText
<UITextView
allowFontScaling={false}
style={[{ color: "white" }, style]}
{...otherProps}

View File

@@ -0,0 +1,64 @@
import { PropsWithChildren, ReactNode, useEffect, useState } from "react";
import {
Pressable,
TextInput,
TextInputProps,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewProps,
} from "react-native";
import { Text } from "../common/Text";
interface Props extends ViewProps {
title?: string | null | undefined;
text?: string | null | undefined;
children?: ReactNode;
iconAfter?: ReactNode;
iconBefore?: ReactNode;
textInputProps?: TextInputProps;
defaultValue?: string;
onChange: (text: string) => void;
}
export const ListInputItem: React.FC<PropsWithChildren<Props>> = ({
title,
text,
iconAfter,
iconBefore,
children,
onChange,
textInputProps,
defaultValue,
...props
}) => {
const [value, setValue] = useState<string>(defaultValue || "");
useEffect(() => {
onChange(value);
}, [value]);
return (
<View
className={`flex flex-row items-center justify-between px-4 h-12 bg-neutral-900`}
{...props}
>
{iconBefore && <View className="mr-2">{iconBefore}</View>}
<View>
<Text className="">{title}</Text>
</View>
<View className="ml-auto">
<TextInput
inputMode="numeric"
keyboardType="decimal-pad"
style={{ color: "white" }}
value={value}
onChangeText={setValue}
className=""
{...textInputProps}
/>
</View>
{iconAfter && <View className="ml-2">{iconAfter}</View>}
</View>
);
};

View File

@@ -0,0 +1,44 @@
import { PropsWithChildren, ReactNode, useState } from "react";
import {
Pressable,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewProps,
} from "react-native";
import { Text } from "../common/Text";
interface Props extends TouchableOpacityProps {
title?: string | null | undefined;
text?: string | null | undefined;
children?: ReactNode;
iconAfter?: ReactNode;
iconBefore?: ReactNode;
}
export const ListItem: React.FC<PropsWithChildren<Props>> = ({
title,
text,
iconAfter,
iconBefore,
children,
...props
}) => {
return (
<TouchableOpacity
className={`flex flex-row items-center justify-between px-4 h-12 bg-neutral-900`}
{...props}
>
{iconBefore && <View className="mr-2">{iconBefore}</View>}
<View>
<Text className="">{title}</Text>
</View>
<View className="ml-auto">
<Text selectable className="">
{text}
</Text>
</View>
{iconAfter && <View className="ml-2">{iconAfter}</View>}
</TouchableOpacity>
);
};

View File

@@ -0,0 +1,24 @@
import { View, ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { Children, PropsWithChildren } from "react";
interface Props extends ViewProps {
title: string;
}
export const ListSection: React.FC<PropsWithChildren<Props>> = ({
children,
title,
...props
}) => {
return (
<View {...props}>
<Text className="ml-4 mb-1 text-xs text-neutral-500 uppercase">
{title}
</Text>
<View className="flex flex-col rounded-xl overflow-hidden border-neutral-800 divide-y-2 divide-solid divide-neutral-800">
{children}
</View>
</View>
);
};

View File

@@ -9,10 +9,10 @@ interface Props extends ViewProps {
export const MoviesTitleHeader: React.FC<Props> = ({ item, ...props }) => {
return (
<View {...props}>
<Text className=" font-bold text-2xl mb-1" selectable>
<Text uiTextView selectable className="font-bold text-2xl mb-1">
{item?.Name}
</Text>
<Text className=" opacity-50">{item?.ProductionYear}</Text>
<Text className="opacity-50">{item?.ProductionYear}</Text>
</View>
);
};

View File

@@ -12,7 +12,7 @@ export const EpisodeTitleHeader: React.FC<Props> = ({ item, ...props }) => {
return (
<View {...props}>
<Text className="font-bold text-2xl" selectable>
<Text uiTextView className="font-bold text-2xl" selectable>
{item?.Name}
</Text>
<View className="flex flex-row items-center mb-1">

View File

@@ -10,6 +10,7 @@ import {
registerBackgroundFetchAsync,
unregisterBackgroundFetchAsync,
} from "@/utils/background-tasks";
import { getStatistics } from "@/utils/optimize-server";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import * as BackgroundFetch from "expo-background-fetch";
@@ -18,7 +19,6 @@ import * as TaskManager from "expo-task-manager";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import {
ActivityIndicator,
Linking,
Switch,
TouchableOpacity,
@@ -32,8 +32,6 @@ import { Input } from "../common/Input";
import { Text } from "../common/Text";
import { Loader } from "../Loader";
import { MediaToggles } from "./MediaToggles";
import axios from "axios";
import { getStatistics } from "@/utils/optimize-server";
interface Props extends ViewProps {}

View File

@@ -82,6 +82,7 @@
"react-native-screens": "3.31.1",
"react-native-svg": "15.2.0",
"react-native-tab-view": "^3.5.2",
"react-native-uitextview": "^1.4.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.2",
"react-native-video": "^6.6.4",