mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
fix(android): convert intro modal to bottom sheet bc broken on smaller screens
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||||
import { Stack, useRouter } from "expo-router";
|
import { Stack, useRouter } from "expo-router";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, Text, TouchableOpacity, View } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||||
|
|
||||||
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
|
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
|
||||||
@@ -328,33 +328,6 @@ export default function IndexLayout() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
|
||||||
name='intro/page'
|
|
||||||
options={{
|
|
||||||
headerShown: true,
|
|
||||||
title: "",
|
|
||||||
headerBlurEffect: "none",
|
|
||||||
headerTransparent: Platform.OS === "ios",
|
|
||||||
headerShadowVisible: false,
|
|
||||||
headerLeft: () => null,
|
|
||||||
headerRight: () => (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => _router.back()}
|
|
||||||
style={{
|
|
||||||
marginRight: Platform.OS === "android" ? 16 : 0,
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{ color: "#9333ea", fontSize: 17, fontWeight: "600" }}
|
|
||||||
>
|
|
||||||
{t("home.intro.done_button")}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
),
|
|
||||||
presentation: "modal",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
||||||
<Stack.Screen key={name} name={name} options={options} />
|
<Stack.Screen key={name} name={name} options={options} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
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, Platform, 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();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
storage.set("hasShownIntro", true);
|
|
||||||
}, []),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
className={`bg-neutral-900 h-full ${Platform.isTV ? "py-5 space-y-4" : "py-16 space-y-8"} px-4`}
|
|
||||||
>
|
|
||||||
<View>
|
|
||||||
<Text className='text-3xl font-bold text-center mb-2'>
|
|
||||||
{t("home.intro.welcome_to_streamyfin")}
|
|
||||||
</Text>
|
|
||||||
<Text className='text-center'>
|
|
||||||
{t("home.intro.a_free_and_open_source_client_for_jellyfin")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<Text className='text-lg font-bold'>
|
|
||||||
{t("home.intro.features_title")}
|
|
||||||
</Text>
|
|
||||||
<Text className='text-xs'>{t("home.intro.features_description")}</Text>
|
|
||||||
<View className='flex flex-row items-center mt-4'>
|
|
||||||
<Image
|
|
||||||
source={require("@/assets/icons/jellyseerr-logo.svg")}
|
|
||||||
style={{
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<View className='shrink ml-2'>
|
|
||||||
<Text className='font-bold mb-1'>Jellyseerr</Text>
|
|
||||||
<Text className='shrink text-xs'>
|
|
||||||
{t("home.intro.jellyseerr_feature_description")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{!Platform.isTV && (
|
|
||||||
<>
|
|
||||||
<View className='flex flex-row items-center mt-4'>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
}}
|
|
||||||
className='flex items-center justify-center'
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name='cloud-download-outline'
|
|
||||||
size={32}
|
|
||||||
color='white'
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View className='shrink ml-2'>
|
|
||||||
<Text className='font-bold mb-1'>
|
|
||||||
{t("home.intro.downloads_feature_title")}
|
|
||||||
</Text>
|
|
||||||
<Text className='shrink text-xs'>
|
|
||||||
{t("home.intro.downloads_feature_description")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View className='flex flex-row items-center mt-4'>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
}}
|
|
||||||
className='flex items-center justify-center'
|
|
||||||
>
|
|
||||||
<Feather name='cast' size={28} color={"white"} />
|
|
||||||
</View>
|
|
||||||
<View className='shrink ml-2'>
|
|
||||||
<Text className='font-bold mb-1'>Chromecast</Text>
|
|
||||||
<Text className='shrink text-xs'>
|
|
||||||
{t("home.intro.chromecast_feature_description")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<View className='flex flex-row items-center mt-4'>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
}}
|
|
||||||
className='flex items-center justify-center'
|
|
||||||
>
|
|
||||||
<Feather name='settings' size={28} color={"white"} />
|
|
||||||
</View>
|
|
||||||
<View className='shrink ml-2'>
|
|
||||||
<Text className='font-bold mb-1'>
|
|
||||||
{t("home.intro.centralised_settings_plugin_title")}
|
|
||||||
</Text>
|
|
||||||
<View className='flex-row flex-wrap items-baseline'>
|
|
||||||
<Text className='shrink text-xs'>
|
|
||||||
{t("home.intro.centralised_settings_plugin_description")}{" "}
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
Linking.openURL(
|
|
||||||
"https://github.com/streamyfin/jellyfin-plugin-streamyfin",
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text className='text-xs text-purple-600 underline'>
|
|
||||||
{t("home.intro.read_more")}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<Button
|
|
||||||
onPress={() => {
|
|
||||||
router.back();
|
|
||||||
}}
|
|
||||||
className='mt-4'
|
|
||||||
>
|
|
||||||
{t("home.intro.done_button")}
|
|
||||||
</Button>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
router.back();
|
|
||||||
router.push("/settings");
|
|
||||||
}}
|
|
||||||
className='mt-4'
|
|
||||||
>
|
|
||||||
<Text className='text-purple-600 text-center'>
|
|
||||||
{t("home.intro.go_to_settings_button")}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, ScrollView, View } from "react-native";
|
import { Platform, ScrollView, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
|
import { useIntroSheet } from "@/providers/IntroSheetProvider";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
|
|
||||||
export default function IntroPage() {
|
export default function IntroPage() {
|
||||||
const router = useRouter();
|
const { showIntro } = useIntroSheet();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export default function IntroPage() {
|
|||||||
<ListGroup title={t("home.settings.intro.title")}>
|
<ListGroup title={t("home.settings.intro.title")}>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.push("/intro/page");
|
showIntro();
|
||||||
}}
|
}}
|
||||||
title={t("home.settings.intro.show_intro")}
|
title={t("home.settings.intro.show_intro")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
ParamListBase,
|
ParamListBase,
|
||||||
TabNavigationState,
|
TabNavigationState,
|
||||||
} from "@react-navigation/native";
|
} from "@react-navigation/native";
|
||||||
import { useFocusEffect, useRouter, withLayoutContext } from "expo-router";
|
import { useFocusEffect, withLayoutContext } from "expo-router";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
@@ -15,6 +15,7 @@ import { SystemBars } from "react-native-edge-to-edge";
|
|||||||
import { MiniPlayerBar } from "@/components/music/MiniPlayerBar";
|
import { MiniPlayerBar } from "@/components/music/MiniPlayerBar";
|
||||||
import { MusicPlaybackEngine } from "@/components/music/MusicPlaybackEngine";
|
import { MusicPlaybackEngine } from "@/components/music/MusicPlaybackEngine";
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
|
import { useIntroSheet } from "@/providers/IntroSheetProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { eventBus } from "@/utils/eventBus";
|
import { eventBus } from "@/utils/eventBus";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
@@ -31,21 +32,21 @@ export const NativeTabs = withLayoutContext<
|
|||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const { showIntro } = useIntroSheet();
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const hasShownIntro = storage.getBoolean("hasShownIntro");
|
const hasShownIntro = storage.getBoolean("hasShownIntro");
|
||||||
if (!hasShownIntro) {
|
if (!hasShownIntro) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
router.push("/intro/page");
|
showIntro();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, []),
|
}, [showIntro]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
105
app/_layout.tsx
105
app/_layout.tsx
@@ -13,6 +13,7 @@ import { GlobalModal } from "@/components/GlobalModal";
|
|||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import { DownloadProvider } from "@/providers/DownloadProvider";
|
import { DownloadProvider } from "@/providers/DownloadProvider";
|
||||||
import { GlobalModalProvider } from "@/providers/GlobalModalProvider";
|
import { GlobalModalProvider } from "@/providers/GlobalModalProvider";
|
||||||
|
import { IntroSheetProvider } from "@/providers/IntroSheetProvider";
|
||||||
import {
|
import {
|
||||||
apiAtom,
|
apiAtom,
|
||||||
getOrSetDeviceId,
|
getOrSetDeviceId,
|
||||||
@@ -391,59 +392,61 @@ function Layout() {
|
|||||||
<MusicPlayerProvider>
|
<MusicPlayerProvider>
|
||||||
<GlobalModalProvider>
|
<GlobalModalProvider>
|
||||||
<BottomSheetModalProvider>
|
<BottomSheetModalProvider>
|
||||||
<ThemeProvider value={DarkTheme}>
|
<IntroSheetProvider>
|
||||||
<SystemBars style='light' hidden={false} />
|
<ThemeProvider value={DarkTheme}>
|
||||||
<Stack initialRouteName='(auth)/(tabs)'>
|
<SystemBars style='light' hidden={false} />
|
||||||
<Stack.Screen
|
<Stack initialRouteName='(auth)/(tabs)'>
|
||||||
name='(auth)/(tabs)'
|
<Stack.Screen
|
||||||
options={{
|
name='(auth)/(tabs)'
|
||||||
headerShown: false,
|
options={{
|
||||||
title: "",
|
headerShown: false,
|
||||||
header: () => null,
|
title: "",
|
||||||
|
header: () => null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/player'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
title: "",
|
||||||
|
header: () => null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/now-playing'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "modal",
|
||||||
|
gestureEnabled: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='login'
|
||||||
|
options={{
|
||||||
|
headerShown: true,
|
||||||
|
title: "",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen name='+not-found' />
|
||||||
|
</Stack>
|
||||||
|
<Toaster
|
||||||
|
duration={4000}
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
backgroundColor: "#262626",
|
||||||
|
borderColor: "#363639",
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
titleStyle: {
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
closeButton
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<GlobalModal />
|
||||||
name='(auth)/player'
|
</ThemeProvider>
|
||||||
options={{
|
</IntroSheetProvider>
|
||||||
headerShown: false,
|
|
||||||
title: "",
|
|
||||||
header: () => null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='(auth)/now-playing'
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: "modal",
|
|
||||||
gestureEnabled: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='login'
|
|
||||||
options={{
|
|
||||||
headerShown: true,
|
|
||||||
title: "",
|
|
||||||
headerTransparent: Platform.OS === "ios",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name='+not-found' />
|
|
||||||
</Stack>
|
|
||||||
<Toaster
|
|
||||||
duration={4000}
|
|
||||||
toastOptions={{
|
|
||||||
style: {
|
|
||||||
backgroundColor: "#262626",
|
|
||||||
borderColor: "#363639",
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
titleStyle: {
|
|
||||||
color: "white",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
closeButton
|
|
||||||
/>
|
|
||||||
<GlobalModal />
|
|
||||||
</ThemeProvider>
|
|
||||||
</BottomSheetModalProvider>
|
</BottomSheetModalProvider>
|
||||||
</GlobalModalProvider>
|
</GlobalModalProvider>
|
||||||
</MusicPlayerProvider>
|
</MusicPlayerProvider>
|
||||||
|
|||||||
203
components/IntroSheet.tsx
Normal file
203
components/IntroSheet.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||||
|
import {
|
||||||
|
BottomSheetBackdrop,
|
||||||
|
type BottomSheetBackdropProps,
|
||||||
|
BottomSheetModal,
|
||||||
|
BottomSheetScrollView,
|
||||||
|
} from "@gorhom/bottom-sheet";
|
||||||
|
import { Image } from "expo-image";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Linking, 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 { storage } from "@/utils/mmkv";
|
||||||
|
|
||||||
|
export interface IntroSheetRef {
|
||||||
|
present: () => void;
|
||||||
|
dismiss: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
|
||||||
|
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
present: () => {
|
||||||
|
storage.set("hasShownIntro", true);
|
||||||
|
bottomSheetRef.current?.present();
|
||||||
|
},
|
||||||
|
dismiss: () => {
|
||||||
|
bottomSheetRef.current?.dismiss();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const renderBackdrop = useCallback(
|
||||||
|
(props: BottomSheetBackdropProps) => (
|
||||||
|
<BottomSheetBackdrop
|
||||||
|
{...props}
|
||||||
|
disappearsOnIndex={-1}
|
||||||
|
appearsOnIndex={0}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDismiss = useCallback(() => {
|
||||||
|
bottomSheetRef.current?.dismiss();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGoToSettings = useCallback(() => {
|
||||||
|
bottomSheetRef.current?.dismiss();
|
||||||
|
router.push("/settings");
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BottomSheetModal
|
||||||
|
ref={bottomSheetRef}
|
||||||
|
enableDynamicSizing
|
||||||
|
backdropComponent={renderBackdrop}
|
||||||
|
backgroundStyle={{ backgroundColor: "#171717" }}
|
||||||
|
handleIndicatorStyle={{ backgroundColor: "#737373" }}
|
||||||
|
>
|
||||||
|
<BottomSheetScrollView
|
||||||
|
style={{
|
||||||
|
paddingLeft: Math.max(16, insets.left),
|
||||||
|
paddingRight: Math.max(16, insets.right),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className={Platform.isTV ? "py-5 space-y-4" : "py-4 space-y-6"}>
|
||||||
|
<View>
|
||||||
|
<Text className='text-3xl font-bold text-center mb-2'>
|
||||||
|
{t("home.intro.welcome_to_streamyfin")}
|
||||||
|
</Text>
|
||||||
|
<Text className='text-center'>
|
||||||
|
{t("home.intro.a_free_and_open_source_client_for_jellyfin")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Text className='text-lg font-bold'>
|
||||||
|
{t("home.intro.features_title")}
|
||||||
|
</Text>
|
||||||
|
<Text className='text-xs'>
|
||||||
|
{t("home.intro.features_description")}
|
||||||
|
</Text>
|
||||||
|
<View className='flex flex-row items-center mt-4'>
|
||||||
|
<Image
|
||||||
|
source={require("@/assets/icons/jellyseerr-logo.svg")}
|
||||||
|
style={{
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View className='shrink ml-2'>
|
||||||
|
<Text className='font-bold mb-1'>Jellyseerr</Text>
|
||||||
|
<Text className='shrink text-xs'>
|
||||||
|
{t("home.intro.jellyseerr_feature_description")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{!Platform.isTV && (
|
||||||
|
<>
|
||||||
|
<View className='flex flex-row items-center mt-4'>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
}}
|
||||||
|
className='flex items-center justify-center'
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name='cloud-download-outline'
|
||||||
|
size={32}
|
||||||
|
color='white'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className='shrink ml-2'>
|
||||||
|
<Text className='font-bold mb-1'>
|
||||||
|
{t("home.intro.downloads_feature_title")}
|
||||||
|
</Text>
|
||||||
|
<Text className='shrink text-xs'>
|
||||||
|
{t("home.intro.downloads_feature_description")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className='flex flex-row items-center mt-4'>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
}}
|
||||||
|
className='flex items-center justify-center'
|
||||||
|
>
|
||||||
|
<Feather name='cast' size={28} color={"white"} />
|
||||||
|
</View>
|
||||||
|
<View className='shrink ml-2'>
|
||||||
|
<Text className='font-bold mb-1'>Chromecast</Text>
|
||||||
|
<Text className='shrink text-xs'>
|
||||||
|
{t("home.intro.chromecast_feature_description")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<View className='flex flex-row items-center mt-4'>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
}}
|
||||||
|
className='flex items-center justify-center'
|
||||||
|
>
|
||||||
|
<Feather name='settings' size={28} color={"white"} />
|
||||||
|
</View>
|
||||||
|
<View className='shrink ml-2'>
|
||||||
|
<Text className='font-bold mb-1'>
|
||||||
|
{t("home.intro.centralised_settings_plugin_title")}
|
||||||
|
</Text>
|
||||||
|
<View className='flex-row flex-wrap items-baseline'>
|
||||||
|
<Text className='shrink text-xs'>
|
||||||
|
{t(
|
||||||
|
"home.intro.centralised_settings_plugin_description",
|
||||||
|
)}{" "}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
Linking.openURL(
|
||||||
|
"https://github.com/streamyfin/jellyfin-plugin-streamyfin",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className='text-xs text-purple-600 underline'>
|
||||||
|
{t("home.intro.read_more")}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Button onPress={handleDismiss} className='mt-4'>
|
||||||
|
{t("home.intro.done_button")}
|
||||||
|
</Button>
|
||||||
|
<TouchableOpacity onPress={handleGoToSettings} className='mt-4'>
|
||||||
|
<Text className='text-purple-600 text-center'>
|
||||||
|
{t("home.intro.go_to_settings_button")}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={{ height: insets.bottom }} />
|
||||||
|
</View>
|
||||||
|
</BottomSheetScrollView>
|
||||||
|
</BottomSheetModal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
IntroSheet.displayName = "IntroSheet";
|
||||||
55
providers/IntroSheetProvider.tsx
Normal file
55
providers/IntroSheetProvider.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
import { IntroSheet, type IntroSheetRef } from "@/components/IntroSheet";
|
||||||
|
|
||||||
|
interface IntroSheetContextType {
|
||||||
|
showIntro: () => void;
|
||||||
|
hideIntro: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntroSheetContext = createContext<IntroSheetContextType | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useIntroSheet = () => {
|
||||||
|
const context = useContext(IntroSheetContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useIntroSheet must be used within IntroSheetProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IntroSheetProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IntroSheetProvider: React.FC<IntroSheetProviderProps> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const sheetRef = useRef<IntroSheetRef>(null);
|
||||||
|
|
||||||
|
const showIntro = useCallback(() => {
|
||||||
|
sheetRef.current?.present();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hideIntro = useCallback(() => {
|
||||||
|
sheetRef.current?.dismiss();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
showIntro,
|
||||||
|
hideIntro,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntroSheetContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
<IntroSheet ref={sheetRef} />
|
||||||
|
</IntroSheetContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user