mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
feat: major refactor of navigation
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Chromecast } from "@/components/Chromecast";
|
||||
import { HeaderBackButton } from "@/components/common/HeaderBackButton";
|
||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { Platform, View } from "react-native";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
|
||||
export default function IndexLayout() {
|
||||
const router = useRouter();
|
||||
@@ -45,6 +46,9 @@ export default function IndexLayout() {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
||||
<Stack.Screen key={name} name={name} options={options} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,23 @@
|
||||
import { Bitrate } from "@/components/BitrateSelector";
|
||||
import { ItemCardText } from "@/components/ItemCardText";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { OverviewText } from "@/components/OverviewText";
|
||||
import { Ratings } from "@/components/Ratings";
|
||||
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
||||
import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorrizontalScroll";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||
import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader";
|
||||
import { SeriesTitleHeader } from "@/components/series/SeriesTitleHeader";
|
||||
import MoviePoster from "@/components/posters/MoviePoster";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
||||
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||
import ios from "@/utils/profiles/ios";
|
||||
import native from "@/utils/profiles/native";
|
||||
import old from "@/utils/profiles/old";
|
||||
import { getItemsApi, getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { BaseItemDtoQueryResult } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useCastDevice } from "react-native-google-cast";
|
||||
import { ParallaxScrollView } from "../../../components/ParallaxPage";
|
||||
import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorrizontalScroll";
|
||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||
import MoviePoster from "@/components/posters/MoviePoster";
|
||||
import { ItemCardText } from "@/components/ItemCardText";
|
||||
import { BaseItemDtoQueryResult } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
|
||||
const page: React.FC = () => {
|
||||
const local = useLocalSearchParams();
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Chromecast } from "@/components/Chromecast";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||
import { SongsList } from "@/components/music/SongsList";
|
||||
import ArtistPoster from "@/components/posters/ArtistPoster";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
@@ -102,16 +103,11 @@ export default function page() {
|
||||
|
||||
<View className="flex flex-row space-x-2 mt-1">
|
||||
{album.AlbumArtists?.map((a) => (
|
||||
<TouchableOpacity
|
||||
key={a.Id}
|
||||
onPress={() => {
|
||||
router.push(`/artists/${a.Id}/page`);
|
||||
}}
|
||||
>
|
||||
<TouchableItemRouter key={a.Id} item={album}>
|
||||
<Text className="font-bold text-purple-600">
|
||||
{album?.AlbumArtist}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableItemRouter>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||
import ArtistPoster from "@/components/posters/ArtistPoster";
|
||||
import MoviePoster from "@/components/posters/MoviePoster";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
@@ -90,15 +91,13 @@ export default function page() {
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
renderItem={({ item, index }) => (
|
||||
<TouchableOpacity
|
||||
<TouchableItemRouter
|
||||
style={{
|
||||
maxWidth: "30%",
|
||||
width: "100%",
|
||||
}}
|
||||
key={index}
|
||||
onPress={() => {
|
||||
router.push(`/artists/${item.Id}/page`);
|
||||
}}
|
||||
item={item}
|
||||
>
|
||||
<View className="flex flex-col gap-y-2">
|
||||
{collection?.CollectionType === "movies" && (
|
||||
@@ -110,7 +109,7 @@ export default function page() {
|
||||
<Text>{item.Name}</Text>
|
||||
<Text className="opacity-50 text-xs">{item.ProductionYear}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableItemRouter>
|
||||
)}
|
||||
keyExtractor={(item) => item.Id || ""}
|
||||
/>
|
||||
@@ -3,7 +3,6 @@ import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||
import { FilterButton } from "@/components/filters/FilterButton";
|
||||
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
|
||||
import { ItemCardText } from "@/components/ItemCardText";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import MoviePoster from "@/components/posters/MoviePoster";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemDtoQueryResult,
|
||||
BaseItemKind,
|
||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import {
|
||||
getFilterApi,
|
||||
@@ -27,7 +25,8 @@ import {
|
||||
} from "@jellyfin/sdk/lib/utils/api";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { Stack, useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import * as ScreenOrientation from "expo-screen-orientation";
|
||||
import { useAtom } from "jotai";
|
||||
import React, {
|
||||
useCallback,
|
||||
@@ -36,8 +35,7 @@ import React, {
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FlatList, NativeScrollEvent, ScrollView, View } from "react-native";
|
||||
import * as ScreenOrientation from "expo-screen-orientation";
|
||||
import { FlatList, View } from "react-native";
|
||||
|
||||
const MemoizedTouchableItemRouter = React.memo(TouchableItemRouter);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Bitrate, BitrateSelector } from "@/components/BitrateSelector";
|
||||
import { DownloadItem } from "@/components/DownloadItem";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { OverviewText } from "@/components/OverviewText";
|
||||
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
||||
import { PlayButton } from "@/components/PlayButton";
|
||||
import { PlayedStatus } from "@/components/PlayedStatus";
|
||||
import { Ratings } from "@/components/Ratings";
|
||||
@@ -32,7 +33,6 @@ import { useAtom } from "jotai";
|
||||
import { useMemo, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useCastDevice } from "react-native-google-cast";
|
||||
import { ParallaxScrollView } from "../../../components/ParallaxPage";
|
||||
|
||||
const page: React.FC = () => {
|
||||
const local = useLocalSearchParams();
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListItem } from "@/components/ListItem";
|
||||
import { SettingToggles } from "@/components/settings/SettingToggles";
|
||||
import { useFiles } from "@/hooks/useFiles";
|
||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { clearLogs, readFromLog } from "@/utils/log";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useAtom } from "jotai";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useFiles } from "@/hooks/useFiles";
|
||||
import { SettingToggles } from "@/components/settings/SettingToggles";
|
||||
import { WebSocketsTest } from "@/components/settings/WebsocketsText";
|
||||
|
||||
export default function settings() {
|
||||
const { logout } = useJellyfin();
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||
import { Stack } from "expo-router";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
export default function IndexLayout() {
|
||||
@@ -25,6 +26,9 @@ export default function IndexLayout() {
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
||||
<Stack.Screen key={name} name={name} options={options} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
@@ -62,8 +63,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const LibraryItemCard: React.FC<Props> = ({ library }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
|
||||
const url = useMemo(
|
||||
@@ -78,11 +77,7 @@ const LibraryItemCard: React.FC<Props> = ({ library }) => {
|
||||
if (!url) return null;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push(`/libraries/${library.Id}`);
|
||||
}}
|
||||
>
|
||||
<TouchableItemRouter item={library}>
|
||||
<View className="flex justify-center rounded-xl w-full relative border border-neutral-900 h-20 ">
|
||||
<Image
|
||||
source={{ uri: url }}
|
||||
@@ -99,6 +94,6 @@ const LibraryItemCard: React.FC<Props> = ({ library }) => {
|
||||
{library.Name}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableItemRouter>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||
import { Stack } from "expo-router";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
@@ -15,6 +16,9 @@ export default function SearchLayout() {
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
||||
<Stack.Screen key={name} name={name} options={options} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -257,10 +257,10 @@ export default function search() {
|
||||
<HorizontalScroll<BaseItemDto>
|
||||
data={data}
|
||||
renderItem={(item) => (
|
||||
<TouchableOpacity
|
||||
<TouchableItemRouter
|
||||
key={item.Id}
|
||||
className="flex flex-col w-28"
|
||||
onPress={() => router.push(`/items/${item.Id}`)}
|
||||
item={item}
|
||||
>
|
||||
<MoviePoster item={item} key={item.Id} />
|
||||
<Text numberOfLines={2} className="mt-2">
|
||||
@@ -269,7 +269,7 @@ export default function search() {
|
||||
<Text className="opacity-50 text-xs">
|
||||
{item.ProductionYear}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableItemRouter>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -1,13 +1,10 @@
|
||||
import { router, Tabs } from "expo-router";
|
||||
import React, { useEffect } from "react";
|
||||
import * as NavigationBar from "expo-navigation-bar";
|
||||
import { TabBarIcon } from "@/components/navigation/TabBarIcon";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Chromecast } from "@/components/Chromecast";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { StyleSheet } from "react-native";
|
||||
import * as NavigationBar from "expo-navigation-bar";
|
||||
import { Tabs } from "expo-router";
|
||||
import React, { useEffect } from "react";
|
||||
import { Platform, StyleSheet } from "react-native";
|
||||
|
||||
export default function TabLayout() {
|
||||
useEffect(() => {
|
||||
@@ -50,7 +47,7 @@ export default function TabLayout() {
|
||||
>
|
||||
<Tabs.Screen redirect name="index" />
|
||||
<Tabs.Screen
|
||||
name="home"
|
||||
name="(home)"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "Home",
|
||||
@@ -63,7 +60,7 @@ export default function TabLayout() {
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="search"
|
||||
name="(search)"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "Search",
|
||||
@@ -73,7 +70,7 @@ export default function TabLayout() {
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="libraries"
|
||||
name="(libraries)"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "Library",
|
||||
|
||||
@@ -18,11 +18,10 @@ import { useEffect, useRef } from "react";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import "react-native-reanimated";
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
export const unstable_settings = {
|
||||
initialRouteName: "/index",
|
||||
initialRouteName: "/(auth)/(tabs)/(home)/",
|
||||
};
|
||||
|
||||
export default function RootLayout() {
|
||||
@@ -93,88 +92,6 @@ function Layout() {
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/settings"
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "Settings",
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/downloads"
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "Downloads",
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/items/[id]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/actors/[actorId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/collections/[collectionId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/artists/page"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/artists/[artistId]/page"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/albums/[albumId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/songs/[songId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/series/[id]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="login"
|
||||
options={{ headerShown: false, title: "Login" }}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import {
|
||||
import GoogleCast, {
|
||||
CastButton,
|
||||
useCastDevice,
|
||||
useDevices,
|
||||
useRemoteMediaClient,
|
||||
} from "react-native-google-cast";
|
||||
import GoogleCast from "react-native-google-cast";
|
||||
|
||||
type Props = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
background?: "blur" | "transparent";
|
||||
};
|
||||
|
||||
export const Chromecast: React.FC<Props> = ({ width = 48, height = 48 }) => {
|
||||
export const Chromecast: React.FC<Props> = ({
|
||||
width = 48,
|
||||
height = 48,
|
||||
background = "transparent",
|
||||
}) => {
|
||||
const client = useRemoteMediaClient();
|
||||
const castDevice = useCastDevice();
|
||||
const devices = useDevices();
|
||||
@@ -31,9 +35,19 @@ export const Chromecast: React.FC<Props> = ({ width = 48, height = 48 }) => {
|
||||
})();
|
||||
}, [client, devices, castDevice, sessionManager, discoveryManager]);
|
||||
|
||||
if (background === "transparent")
|
||||
return (
|
||||
<View className=" rounded h-10 aspect-square flex items-center justify-center">
|
||||
<CastButton style={{ tintColor: "white", height, width }} />
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="rounded h-10 aspect-square flex items-center justify-center">
|
||||
<BlurView
|
||||
intensity={100}
|
||||
className="rounded-full overflow-hidden h-10 aspect-square flex items-center justify-center"
|
||||
>
|
||||
<CastButton style={{ tintColor: "white", height, width }} />
|
||||
</View>
|
||||
</BlurView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,14 +33,14 @@ export const ParallaxScrollView: React.FC<Props> = ({
|
||||
translateY: interpolate(
|
||||
scrollOffset.value,
|
||||
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
|
||||
[-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75],
|
||||
[-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
|
||||
),
|
||||
},
|
||||
{
|
||||
scale: interpolate(
|
||||
scrollOffset.value,
|
||||
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
|
||||
[2, 1, 1],
|
||||
[2, 1, 1]
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -58,30 +58,6 @@ export const ParallaxScrollView: React.FC<Props> = ({
|
||||
ref={scrollRef}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
className="absolute left-4 z-50 bg-black rounded-full p-2 border border-neutral-900"
|
||||
style={{
|
||||
top: inset.top + 17,
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
className="drop-shadow-2xl"
|
||||
name="arrow-back"
|
||||
size={24}
|
||||
color="#077DF2"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View
|
||||
className="absolute right-4 z-50 bg-black rounded-full p-0.5"
|
||||
style={{
|
||||
top: inset.top + 17,
|
||||
}}
|
||||
>
|
||||
<Chromecast width={22} height={22} />
|
||||
</View>
|
||||
|
||||
{logo && (
|
||||
<View className="absolute top-[250px] h-[130px] left-0 w-full z-40 px-4 flex justify-center items-center">
|
||||
{logo}
|
||||
|
||||
59
components/common/HeaderBackButton.tsx
Normal file
59
components/common/HeaderBackButton.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
TouchableOpacity,
|
||||
TouchableOpacityProps,
|
||||
View,
|
||||
ViewProps,
|
||||
} from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useRouter } from "expo-router";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { BlurView, BlurViewProps } from "expo-blur";
|
||||
|
||||
interface Props extends BlurViewProps {
|
||||
background?: "blur" | "transparent";
|
||||
touchableOpacityProps?: TouchableOpacityProps;
|
||||
}
|
||||
|
||||
export const HeaderBackButton: React.FC<Props> = ({
|
||||
background = "transparent",
|
||||
touchableOpacityProps,
|
||||
...props
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
if (background === "transparent")
|
||||
return (
|
||||
<BlurView
|
||||
{...props}
|
||||
intensity={100}
|
||||
className="overflow-hidden rounded-full p-2"
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
{...touchableOpacityProps}
|
||||
>
|
||||
<Ionicons
|
||||
className="drop-shadow-2xl"
|
||||
name="arrow-back"
|
||||
size={24}
|
||||
color="#077DF2"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</BlurView>
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
className=" bg-black rounded-full p-2 border border-neutral-900"
|
||||
{...touchableOpacityProps}
|
||||
>
|
||||
<Ionicons
|
||||
className="drop-shadow-2xl"
|
||||
name="arrow-back"
|
||||
size={24}
|
||||
color="#077DF2"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,8 @@
|
||||
import {
|
||||
TouchableOpacity,
|
||||
TouchableOpacityProps,
|
||||
View,
|
||||
ViewProps,
|
||||
} from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { useRouter } from "expo-router";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useRouter, useSegments } from "expo-router";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { Alert, TouchableOpacity, TouchableOpacityProps } from "react-native";
|
||||
|
||||
interface Props extends TouchableOpacityProps {
|
||||
item: BaseItemDto;
|
||||
@@ -20,46 +14,69 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
...props
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
const segments = useSegments();
|
||||
|
||||
if (item.Type === "Series") {
|
||||
router.push(`/series/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
if (item.Type === "Episode") {
|
||||
router.push(`/items/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
if (item.Type === "MusicAlbum") {
|
||||
router.push(`/albums/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
if (item.Type === "Audio") {
|
||||
router.push(`/albums/${item.AlbumId}`);
|
||||
return;
|
||||
}
|
||||
if (item.Type === "MusicArtist") {
|
||||
router.push(`/artists/${item.Id}/page`);
|
||||
return;
|
||||
}
|
||||
if (item.Type === "Person") {
|
||||
router.push(`/actors/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
const from = segments[2];
|
||||
|
||||
if (item.Type === "BoxSet") {
|
||||
router.push(`/collections/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
if (from === "(home)" || from === "(search)" || from === "(libraries)")
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
console.log("[0]", item.Type);
|
||||
|
||||
router.push(`/items/${item.Id}`);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
|
||||
if (item.Type === "Series") {
|
||||
router.push(`/(auth)/(tabs)/${from}/series/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "MusicAlbum") {
|
||||
router.push(`/(auth)/(tabs)/${from}/albums/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "Audio") {
|
||||
router.push(`/(auth)/(tabs)/${from}/albums/${item.AlbumId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "MusicArtist") {
|
||||
router.push(`/(auth)/(tabs)/${from}/artists/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "Person") {
|
||||
router.push(`/(auth)/(tabs)/${from}/actors/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "BoxSet") {
|
||||
router.push(`/(auth)/(tabs)/${from}/collections/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "UserView") {
|
||||
Alert.alert("Not implemented");
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Type === "CollectionFolder") {
|
||||
router.push(`/(auth)/(tabs)/(libraries)/${item.Id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Same as default
|
||||
// if (item.Type === "Episode") {
|
||||
// router.push(`/items/${item.Id}`);
|
||||
// return;
|
||||
// }
|
||||
|
||||
router.push(`/(auth)/(tabs)/${from}/items/${item.Id}`);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||
import {
|
||||
sortByAtom,
|
||||
sortOptions,
|
||||
sortOrderAtom,
|
||||
sortOrderOptions,
|
||||
} from "@/utils/atoms/filters";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const SortButton: React.FC<Props> = ({ title, ...props }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const [sortBy, setSortBy] = useAtom(sortByAtom);
|
||||
const [sortOrder, setSortOrder] = useAtom(sortOrderAtom);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<TouchableOpacity>
|
||||
<View
|
||||
className={`
|
||||
px-3 py-2 rounded-full flex flex-row items-center space-x-2 bg-neutral-900
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<Text>Sort by</Text>
|
||||
<Ionicons
|
||||
name="filter"
|
||||
size={16}
|
||||
color="white"
|
||||
style={{ opacity: 0.5 }}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
loop={true}
|
||||
side="bottom"
|
||||
align="start"
|
||||
alignOffset={0}
|
||||
avoidCollisions={true}
|
||||
collisionPadding={8}
|
||||
sideOffset={8}
|
||||
>
|
||||
{sortOptions?.map((g) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
value={sortBy.key === g.key ? "on" : "off"}
|
||||
onValueChange={(next, previous) => {
|
||||
if (next === "on") {
|
||||
setSortBy(g);
|
||||
} else {
|
||||
setSortBy(sortOptions[0]);
|
||||
}
|
||||
}}
|
||||
key={g.key}
|
||||
textValue={g.value}
|
||||
>
|
||||
<DropdownMenu.ItemIndicator />
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
{sortOrderOptions.map((g) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
value={sortOrder.key === g.key ? "on" : "off"}
|
||||
onValueChange={(next, previous) => {
|
||||
if (next === "on") {
|
||||
setSortOrder(g);
|
||||
} else {
|
||||
setSortOrder(sortOrderOptions[0]);
|
||||
}
|
||||
}}
|
||||
key={g.key}
|
||||
textValue={g.value}
|
||||
>
|
||||
<DropdownMenu.ItemIndicator />
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
};
|
||||
@@ -90,7 +90,7 @@ export const NextEpisodeButton: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={() => router.replace(`/items/${nextEpisode?.Id}`)}
|
||||
onPress={() => router.push(`/items/${nextEpisode?.Id}`)}
|
||||
className={`h-12 aspect-square`}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
|
||||
26
components/stacks/NestedTabPageStack.tsx
Normal file
26
components/stacks/NestedTabPageStack.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Stack } from "expo-router";
|
||||
import { Chromecast } from "../Chromecast";
|
||||
import { HeaderBackButton } from "../common/HeaderBackButton";
|
||||
|
||||
const commonScreenOptions = {
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerTransparent: true,
|
||||
headerShadowVisible: false,
|
||||
headerLeft: () => <HeaderBackButton />,
|
||||
headerRight: () => <Chromecast background="blur" width={22} height={22} />,
|
||||
};
|
||||
|
||||
const routes = [
|
||||
"actors/[actorId]",
|
||||
"albums/[albumId]",
|
||||
"artists/index",
|
||||
"artists/[artistId]",
|
||||
"collections/[collectionId]",
|
||||
"items/[id]",
|
||||
"songs/[songId]",
|
||||
"series/[id]",
|
||||
];
|
||||
|
||||
export const nestedTabPageScreenOptions: { [key: string]: any } =
|
||||
Object.fromEntries(routes.map((route) => [route, commonScreenOptions]));
|
||||
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
currentlyPlayingItemAtom,
|
||||
playingAtom,
|
||||
showCurrentlyPlayingBarAtom,
|
||||
} from "@/utils/atoms/playState";
|
||||
import { Api, Jellyfin } from "@jellyfin/sdk";
|
||||
import { UserDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
@@ -233,7 +228,7 @@ function useProtectedRoute(user: UserDto | null, loading = false) {
|
||||
if (!user?.Id && inAuthGroup) {
|
||||
router.replace("/login");
|
||||
} else if (user?.Id && !inAuthGroup) {
|
||||
router.replace("/home");
|
||||
router.replace("/(auth)/(tabs)/(home)/");
|
||||
}
|
||||
}, [user, segments, loading]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user