mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-26 18:44:41 +01:00
Merge pull request #419 from streamyfin/fix/remove-music
fix: remove everything related to music
This commit is contained in:
@@ -1,128 +0,0 @@
|
|||||||
import { Chromecast } from "@/components/Chromecast";
|
|
||||||
import { ItemImage } from "@/components/common/ItemImage";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
|
||||||
import { SongsList } from "@/components/music/SongsList";
|
|
||||||
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
|
||||||
import ArtistPoster from "@/components/posters/ArtistPoster";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { router, useLocalSearchParams, useNavigation } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { ScrollView, TouchableOpacity, View } from "react-native";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
|
|
||||||
export default function page() {
|
|
||||||
const searchParams = useLocalSearchParams();
|
|
||||||
const { collectionId, artistId, albumId } = searchParams as {
|
|
||||||
collectionId: string;
|
|
||||||
artistId: string;
|
|
||||||
albumId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
|
||||||
const [user] = useAtom(userAtom);
|
|
||||||
|
|
||||||
const navigation = useNavigation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
navigation.setOptions({
|
|
||||||
headerRight: () => (
|
|
||||||
<View className="">
|
|
||||||
<Chromecast />
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: album } = useQuery({
|
|
||||||
queryKey: ["album", albumId, artistId],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api) return null;
|
|
||||||
const response = await getItemsApi(api).getItems({
|
|
||||||
userId: user?.Id,
|
|
||||||
ids: [albumId],
|
|
||||||
});
|
|
||||||
const data = response.data.Items?.[0];
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
enabled: !!api && !!user?.Id && !!albumId,
|
|
||||||
staleTime: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: songs,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
} = useQuery<{
|
|
||||||
Items: BaseItemDto[];
|
|
||||||
TotalRecordCount: number;
|
|
||||||
}>({
|
|
||||||
queryKey: ["songs", artistId, albumId],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api)
|
|
||||||
return {
|
|
||||||
Items: [],
|
|
||||||
TotalRecordCount: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await getItemsApi(api).getItems({
|
|
||||||
userId: user?.Id,
|
|
||||||
parentId: albumId,
|
|
||||||
fields: [
|
|
||||||
"ItemCounts",
|
|
||||||
"PrimaryImageAspectRatio",
|
|
||||||
"CanDelete",
|
|
||||||
"MediaSourceCount",
|
|
||||||
],
|
|
||||||
sortBy: ["ParentIndexNumber", "IndexNumber", "SortName"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = response.data.Items;
|
|
||||||
|
|
||||||
return {
|
|
||||||
Items: data || [],
|
|
||||||
TotalRecordCount: response.data.TotalRecordCount || 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
enabled: !!api && !!user?.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
|
|
||||||
if (!album) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ParallaxScrollView
|
|
||||||
headerHeight={400}
|
|
||||||
headerImage={
|
|
||||||
<ItemImage
|
|
||||||
variant={"Primary"}
|
|
||||||
item={album}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<View className="px-4 mb-8">
|
|
||||||
<Text className="font-bold text-2xl mb-2">{album?.Name}</Text>
|
|
||||||
<Text className="text-neutral-500">
|
|
||||||
{songs?.TotalRecordCount} songs
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className="px-4">
|
|
||||||
<SongsList
|
|
||||||
albumId={albumId}
|
|
||||||
songs={songs?.Items}
|
|
||||||
collectionId={collectionId}
|
|
||||||
artistId={artistId}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</ParallaxScrollView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import ArtistPoster from "@/components/posters/ArtistPoster";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { router, useLocalSearchParams, useNavigation } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { FlatList, ScrollView, TouchableOpacity, View } from "react-native";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import { ItemImage } from "@/components/common/ItemImage";
|
|
||||||
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
|
||||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
|
||||||
|
|
||||||
export default function page() {
|
|
||||||
const searchParams = useLocalSearchParams();
|
|
||||||
const { artistId } = searchParams as {
|
|
||||||
artistId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
|
||||||
const [user] = useAtom(userAtom);
|
|
||||||
|
|
||||||
const navigation = useNavigation();
|
|
||||||
|
|
||||||
const [startIndex, setStartIndex] = useState<number>(0);
|
|
||||||
|
|
||||||
const { data: artist } = useQuery({
|
|
||||||
queryKey: ["album", artistId],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api) return null;
|
|
||||||
const response = await getItemsApi(api).getItems({
|
|
||||||
userId: user?.Id,
|
|
||||||
ids: [artistId],
|
|
||||||
});
|
|
||||||
const data = response.data.Items?.[0];
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
enabled: !!api && !!user?.Id && !!artistId,
|
|
||||||
staleTime: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: albums,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
} = useQuery<{
|
|
||||||
Items: BaseItemDto[];
|
|
||||||
TotalRecordCount: number;
|
|
||||||
}>({
|
|
||||||
queryKey: ["albums", artistId, startIndex],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api)
|
|
||||||
return {
|
|
||||||
Items: [],
|
|
||||||
TotalRecordCount: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await getItemsApi(api).getItems({
|
|
||||||
userId: user?.Id,
|
|
||||||
parentId: artistId,
|
|
||||||
sortOrder: ["Descending", "Descending", "Ascending"],
|
|
||||||
includeItemTypes: ["MusicAlbum"],
|
|
||||||
recursive: true,
|
|
||||||
fields: [
|
|
||||||
"ParentId",
|
|
||||||
"PrimaryImageAspectRatio",
|
|
||||||
"ParentId",
|
|
||||||
"PrimaryImageAspectRatio",
|
|
||||||
],
|
|
||||||
collapseBoxSetItems: false,
|
|
||||||
albumArtistIds: [artistId],
|
|
||||||
startIndex,
|
|
||||||
limit: 100,
|
|
||||||
sortBy: ["PremiereDate", "ProductionYear", "SortName"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = response.data.Items;
|
|
||||||
|
|
||||||
return {
|
|
||||||
Items: data || [],
|
|
||||||
TotalRecordCount: response.data.TotalRecordCount || 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
enabled: !!api && !!user?.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
|
|
||||||
if (!artist || !albums) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ParallaxScrollView
|
|
||||||
headerHeight={400}
|
|
||||||
headerImage={
|
|
||||||
<ItemImage
|
|
||||||
variant={"Primary"}
|
|
||||||
item={artist}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<View className="px-4 mb-8">
|
|
||||||
<Text className="font-bold text-2xl mb-2">{artist?.Name}</Text>
|
|
||||||
<Text className="text-neutral-500">
|
|
||||||
{albums.TotalRecordCount} albums
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex flex-row flex-wrap justify-between px-4">
|
|
||||||
{albums.Items.map((item, idx) => (
|
|
||||||
<TouchableItemRouter
|
|
||||||
item={item}
|
|
||||||
style={{ width: "30%", marginBottom: 20 }}
|
|
||||||
key={idx}
|
|
||||||
>
|
|
||||||
<View className="flex flex-col gap-y-2">
|
|
||||||
<ArtistPoster item={item} />
|
|
||||||
<Text numberOfLines={2}>{item.Name}</Text>
|
|
||||||
<Text className="opacity-50 text-xs">{item.ProductionYear}</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableItemRouter>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</ParallaxScrollView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
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";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { getArtistsApi, getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { FlatList, TouchableOpacity, View } from "react-native";
|
|
||||||
|
|
||||||
export default function page() {
|
|
||||||
const searchParams = useLocalSearchParams();
|
|
||||||
const { collectionId } = searchParams as { collectionId: string };
|
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
|
||||||
const [user] = useAtom(userAtom);
|
|
||||||
|
|
||||||
const { data: collection } = useQuery({
|
|
||||||
queryKey: ["collection", collectionId],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api) return null;
|
|
||||||
const response = await getItemsApi(api).getItems({
|
|
||||||
userId: user?.Id,
|
|
||||||
ids: [collectionId],
|
|
||||||
});
|
|
||||||
const data = response.data.Items?.[0];
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
enabled: !!api && !!user?.Id && !!collectionId,
|
|
||||||
staleTime: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [startIndex, setStartIndex] = useState<number>(0);
|
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery<{
|
|
||||||
Items: BaseItemDto[];
|
|
||||||
TotalRecordCount: number;
|
|
||||||
}>({
|
|
||||||
queryKey: ["collection-items", collection?.Id, startIndex],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api || !collectionId)
|
|
||||||
return {
|
|
||||||
Items: [],
|
|
||||||
TotalRecordCount: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await getArtistsApi(api).getArtists({
|
|
||||||
sortBy: ["SortName"],
|
|
||||||
sortOrder: ["Ascending"],
|
|
||||||
fields: ["PrimaryImageAspectRatio", "SortName"],
|
|
||||||
imageTypeLimit: 1,
|
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Banner", "Thumb"],
|
|
||||||
parentId: collectionId,
|
|
||||||
userId: user?.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = response.data.Items;
|
|
||||||
|
|
||||||
return {
|
|
||||||
Items: data || [],
|
|
||||||
TotalRecordCount: response.data.TotalRecordCount || 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
enabled: !!collection?.Id && !!api && !!user?.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalItems = useMemo(() => {
|
|
||||||
return data?.TotalRecordCount;
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
contentContainerStyle={{
|
|
||||||
padding: 16,
|
|
||||||
paddingBottom: 140,
|
|
||||||
}}
|
|
||||||
ListHeaderComponent={
|
|
||||||
<View className="mb-4">
|
|
||||||
<Text className="font-bold text-3xl mb-2">Artists</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
nestedScrollEnabled
|
|
||||||
data={data.Items}
|
|
||||||
numColumns={3}
|
|
||||||
columnWrapperStyle={{
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
renderItem={({ item, index }) => (
|
|
||||||
<TouchableItemRouter
|
|
||||||
style={{
|
|
||||||
maxWidth: "30%",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
key={index}
|
|
||||||
item={item}
|
|
||||||
>
|
|
||||||
<View className="flex flex-col gap-y-2">
|
|
||||||
{collection?.CollectionType === "movies" && (
|
|
||||||
<MoviePoster item={item} />
|
|
||||||
)}
|
|
||||||
{collection?.CollectionType === "music" && (
|
|
||||||
<ArtistPoster item={item} />
|
|
||||||
)}
|
|
||||||
<Text>{item.Name}</Text>
|
|
||||||
<Text className="opacity-50 text-xs">{item.ProductionYear}</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableItemRouter>
|
|
||||||
)}
|
|
||||||
keyExtractor={(item) => item.Id || ""}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -109,7 +109,7 @@ const page: React.FC = () => {
|
|||||||
genres: selectedGenres,
|
genres: selectedGenres,
|
||||||
tags: selectedTags,
|
tags: selectedTags,
|
||||||
years: selectedYears.map((year) => parseInt(year)),
|
years: selectedYears.map((year) => parseInt(year)),
|
||||||
includeItemTypes: ["Movie", "Series", "MusicAlbum"],
|
includeItemTypes: ["Movie", "Series"],
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data || null;
|
return response.data || null;
|
||||||
|
|||||||
@@ -150,8 +150,6 @@ const Page = () => {
|
|||||||
itemType = "Series";
|
itemType = "Series";
|
||||||
} else if (library.CollectionType === "boxsets") {
|
} else if (library.CollectionType === "boxsets") {
|
||||||
itemType = "BoxSet";
|
itemType = "BoxSet";
|
||||||
} else if (library.CollectionType === "music") {
|
|
||||||
itemType = "MusicAlbum";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getItemsApi(api).getItems({
|
const response = await getItemsApi(api).getItems({
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ export default function index() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const libraries = useMemo(
|
const libraries = useMemo(
|
||||||
() => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)),
|
() =>
|
||||||
|
data
|
||||||
|
?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!))
|
||||||
|
.filter((l) => l.CollectionType !== "music")
|
||||||
|
.filter((l) => l.CollectionType !== "books") || [],
|
||||||
[data, settings?.hiddenLibraries]
|
[data, settings?.hiddenLibraries]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
|
|||||||
import { Tag } from "@/components/GenreTags";
|
import { Tag } from "@/components/GenreTags";
|
||||||
import { ItemCardText } from "@/components/ItemCardText";
|
import { ItemCardText } from "@/components/ItemCardText";
|
||||||
import { JellyserrIndexPage } from "@/components/jellyseerr/JellyseerrIndexPage";
|
import { JellyserrIndexPage } from "@/components/jellyseerr/JellyseerrIndexPage";
|
||||||
import AlbumCover from "@/components/posters/AlbumCover";
|
|
||||||
import MoviePoster from "@/components/posters/MoviePoster";
|
import MoviePoster from "@/components/posters/MoviePoster";
|
||||||
import SeriesPoster from "@/components/posters/SeriesPoster";
|
import SeriesPoster from "@/components/posters/SeriesPoster";
|
||||||
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
|
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
|
||||||
@@ -184,52 +183,19 @@ export default function search() {
|
|||||||
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: artists, isFetching: l4 } = useQuery({
|
|
||||||
queryKey: ["search", "artists", debouncedSearch],
|
|
||||||
queryFn: () =>
|
|
||||||
searchFn({
|
|
||||||
query: debouncedSearch,
|
|
||||||
types: ["MusicArtist"],
|
|
||||||
}),
|
|
||||||
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: albums, isFetching: l5 } = useQuery({
|
|
||||||
queryKey: ["search", "albums", debouncedSearch],
|
|
||||||
queryFn: () =>
|
|
||||||
searchFn({
|
|
||||||
query: debouncedSearch,
|
|
||||||
types: ["MusicAlbum"],
|
|
||||||
}),
|
|
||||||
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: songs, isFetching: l6 } = useQuery({
|
|
||||||
queryKey: ["search", "songs", debouncedSearch],
|
|
||||||
queryFn: () =>
|
|
||||||
searchFn({
|
|
||||||
query: debouncedSearch,
|
|
||||||
types: ["Audio"],
|
|
||||||
}),
|
|
||||||
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const noResults = useMemo(() => {
|
const noResults = useMemo(() => {
|
||||||
return !(
|
return !(
|
||||||
artists?.length ||
|
|
||||||
albums?.length ||
|
|
||||||
songs?.length ||
|
|
||||||
movies?.length ||
|
movies?.length ||
|
||||||
episodes?.length ||
|
episodes?.length ||
|
||||||
series?.length ||
|
series?.length ||
|
||||||
collections?.length ||
|
collections?.length ||
|
||||||
actors?.length
|
actors?.length
|
||||||
);
|
);
|
||||||
}, [artists, episodes, albums, songs, movies, series, collections, actors]);
|
}, [episodes, movies, series, collections, actors]);
|
||||||
|
|
||||||
const loading = useMemo(() => {
|
const loading = useMemo(() => {
|
||||||
return l1 || l2 || l3 || l4 || l5 || l6 || l7 || l8;
|
return l1 || l2 || l3 || l7 || l8;
|
||||||
}, [l1, l2, l3, l4, l5, l6, l7, l8]);
|
}, [l1, l2, l3, l7, l8]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -365,48 +331,6 @@ export default function search() {
|
|||||||
</TouchableItemRouter>
|
</TouchableItemRouter>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
|
||||||
ids={artists?.map((m) => m.Id!)}
|
|
||||||
header="Artists"
|
|
||||||
renderItem={(item: BaseItemDto) => (
|
|
||||||
<TouchableItemRouter
|
|
||||||
item={item}
|
|
||||||
key={item.Id}
|
|
||||||
className="flex flex-col w-28 mr-2"
|
|
||||||
>
|
|
||||||
<AlbumCover id={item.Id} />
|
|
||||||
<ItemCardText item={item} />
|
|
||||||
</TouchableItemRouter>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<SearchItemWrapper
|
|
||||||
ids={albums?.map((m) => m.Id!)}
|
|
||||||
header="Albums"
|
|
||||||
renderItem={(item: BaseItemDto) => (
|
|
||||||
<TouchableItemRouter
|
|
||||||
item={item}
|
|
||||||
key={item.Id}
|
|
||||||
className="flex flex-col w-28 mr-2"
|
|
||||||
>
|
|
||||||
<AlbumCover id={item.Id} />
|
|
||||||
<ItemCardText item={item} />
|
|
||||||
</TouchableItemRouter>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<SearchItemWrapper
|
|
||||||
ids={songs?.map((m) => m.Id!)}
|
|
||||||
header="Songs"
|
|
||||||
renderItem={(item: BaseItemDto) => (
|
|
||||||
<TouchableItemRouter
|
|
||||||
item={item}
|
|
||||||
key={item.Id}
|
|
||||||
className="flex flex-col w-28 mr-2"
|
|
||||||
>
|
|
||||||
<AlbumCover id={item.AlbumId} />
|
|
||||||
<ItemCardText item={item} />
|
|
||||||
</TouchableItemRouter>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<JellyserrIndexPage searchQuery={debouncedSearch} />
|
<JellyserrIndexPage searchQuery={debouncedSearch} />
|
||||||
|
|||||||
@@ -25,15 +25,6 @@ export default function Layout() {
|
|||||||
animation: "fade",
|
animation: "fade",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
|
||||||
name="music-player"
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
autoHideHomeIndicator: true,
|
|
||||||
title: "",
|
|
||||||
animation: "fade",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,419 +0,0 @@
|
|||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { Loader } from "@/components/Loader";
|
|
||||||
import { Controls } from "@/components/video-player/controls/Controls";
|
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
|
||||||
import { useOrientationSettings } from "@/hooks/useOrientationSettings";
|
|
||||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
|
||||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
|
||||||
import { secondsToTicks } from "@/utils/secondsToTicks";
|
|
||||||
import { Api } from "@jellyfin/sdk";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
|
||||||
import {
|
|
||||||
getPlaystateApi,
|
|
||||||
getUserLibraryApi,
|
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
import { Image } from "expo-image";
|
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
||||||
import { Pressable, useWindowDimensions, View } from "react-native";
|
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
|
||||||
import Video, { OnProgressData, VideoRef } from "react-native-video";
|
|
||||||
|
|
||||||
export default function page() {
|
|
||||||
const api = useAtomValue(apiAtom);
|
|
||||||
const user = useAtomValue(userAtom);
|
|
||||||
const [settings] = useSettings();
|
|
||||||
const videoRef = useRef<VideoRef | null>(null);
|
|
||||||
const windowDimensions = useWindowDimensions();
|
|
||||||
|
|
||||||
const firstTime = useRef(true);
|
|
||||||
|
|
||||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
|
||||||
const [showControls, setShowControls] = useState(true);
|
|
||||||
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
|
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
|
||||||
const [isBuffering, setIsBuffering] = useState(true);
|
|
||||||
|
|
||||||
const progress = useSharedValue(0);
|
|
||||||
const isSeeking = useSharedValue(false);
|
|
||||||
const cacheProgress = useSharedValue(0);
|
|
||||||
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const {
|
|
||||||
itemId,
|
|
||||||
audioIndex: audioIndexStr,
|
|
||||||
subtitleIndex: subtitleIndexStr,
|
|
||||||
mediaSourceId,
|
|
||||||
bitrateValue: bitrateValueStr,
|
|
||||||
} = useLocalSearchParams<{
|
|
||||||
itemId: string;
|
|
||||||
audioIndex: string;
|
|
||||||
subtitleIndex: string;
|
|
||||||
mediaSourceId: string;
|
|
||||||
bitrateValue: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
|
|
||||||
const subtitleIndex = subtitleIndexStr
|
|
||||||
? parseInt(subtitleIndexStr, 10)
|
|
||||||
: undefined;
|
|
||||||
const bitrateValue = bitrateValueStr
|
|
||||||
? parseInt(bitrateValueStr, 10)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: item,
|
|
||||||
isLoading: isLoadingItem,
|
|
||||||
isError: isErrorItem,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["item", itemId],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api) return;
|
|
||||||
const res = await getUserLibraryApi(api).getItem({
|
|
||||||
itemId,
|
|
||||||
userId: user?.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
enabled: !!itemId && !!api,
|
|
||||||
staleTime: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: stream,
|
|
||||||
isLoading: isLoadingStreamUrl,
|
|
||||||
isError: isErrorStreamUrl,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["stream-url"],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api) return;
|
|
||||||
const res = await getStreamUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
|
||||||
userId: user?.Id,
|
|
||||||
audioStreamIndex: audioIndex,
|
|
||||||
maxStreamingBitrate: bitrateValue,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
subtitleStreamIndex: subtitleIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res) return null;
|
|
||||||
|
|
||||||
const { mediaSource, sessionId, url } = res;
|
|
||||||
|
|
||||||
if (!sessionId || !mediaSource || !url) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
mediaSource,
|
|
||||||
sessionId,
|
|
||||||
url,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const poster = usePoster(item, api);
|
|
||||||
const videoSource = useVideoSource(item, api, poster, stream?.url);
|
|
||||||
|
|
||||||
const togglePlay = useCallback(
|
|
||||||
async (ticks: number) => {
|
|
||||||
lightHapticFeedback();
|
|
||||||
if (isPlaying) {
|
|
||||||
videoRef.current?.pause();
|
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
|
||||||
itemId: item?.Id!,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
positionTicks: Math.floor(ticks),
|
|
||||||
isPaused: true,
|
|
||||||
playMethod: stream?.url.includes("m3u8")
|
|
||||||
? "Transcode"
|
|
||||||
: "DirectStream",
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
videoRef.current?.resume();
|
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
|
||||||
itemId: item?.Id!,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
positionTicks: Math.floor(ticks),
|
|
||||||
isPaused: false,
|
|
||||||
playMethod: stream?.url.includes("m3u8")
|
|
||||||
? "Transcode"
|
|
||||||
: "DirectStream",
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
isPlaying,
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
videoRef,
|
|
||||||
settings,
|
|
||||||
audioIndex,
|
|
||||||
subtitleIndex,
|
|
||||||
mediaSourceId,
|
|
||||||
stream,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const play = useCallback(() => {
|
|
||||||
videoRef.current?.resume();
|
|
||||||
reportPlaybackStart();
|
|
||||||
}, [videoRef]);
|
|
||||||
|
|
||||||
const pause = useCallback(() => {
|
|
||||||
videoRef.current?.pause();
|
|
||||||
}, [videoRef]);
|
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
|
||||||
setIsPlaybackStopped(true);
|
|
||||||
videoRef.current?.pause();
|
|
||||||
reportPlaybackStopped();
|
|
||||||
}, [videoRef]);
|
|
||||||
|
|
||||||
const seek = useCallback(
|
|
||||||
(seconds: number) => {
|
|
||||||
videoRef.current?.seek(seconds);
|
|
||||||
},
|
|
||||||
[videoRef]
|
|
||||||
);
|
|
||||||
|
|
||||||
const reportPlaybackStopped = async () => {
|
|
||||||
if (!item?.Id) return;
|
|
||||||
await getPlaystateApi(api!).onPlaybackStopped({
|
|
||||||
itemId: item.Id,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
positionTicks: Math.floor(progress.value),
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const reportPlaybackStart = async () => {
|
|
||||||
if (!item?.Id) return;
|
|
||||||
await getPlaystateApi(api!).onPlaybackStart({
|
|
||||||
itemId: item?.Id,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onProgress = useCallback(
|
|
||||||
async (data: OnProgressData) => {
|
|
||||||
if (isSeeking.value === true) return;
|
|
||||||
if (isPlaybackStopped === true) return;
|
|
||||||
|
|
||||||
const ticks = data.currentTime * 10000000;
|
|
||||||
|
|
||||||
progress.value = secondsToTicks(data.currentTime);
|
|
||||||
cacheProgress.value = secondsToTicks(data.playableDuration);
|
|
||||||
setIsBuffering(data.playableDuration === 0);
|
|
||||||
|
|
||||||
if (!item?.Id || data.currentTime === 0) return;
|
|
||||||
|
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
|
||||||
itemId: item.Id!,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
positionTicks: Math.round(ticks),
|
|
||||||
isPaused: !isPlaying,
|
|
||||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
|
||||||
playSessionId: stream?.sessionId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[
|
|
||||||
item,
|
|
||||||
isPlaying,
|
|
||||||
api,
|
|
||||||
isPlaybackStopped,
|
|
||||||
audioIndex,
|
|
||||||
subtitleIndex,
|
|
||||||
mediaSourceId,
|
|
||||||
stream,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
play();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stop();
|
|
||||||
};
|
|
||||||
}, [play, stop])
|
|
||||||
);
|
|
||||||
|
|
||||||
useOrientation();
|
|
||||||
useOrientationSettings();
|
|
||||||
|
|
||||||
useWebSocket({
|
|
||||||
isPlaying: isPlaying,
|
|
||||||
pauseVideo: pause,
|
|
||||||
playVideo: play,
|
|
||||||
stopPlayback: stop,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoadingItem || isLoadingStreamUrl)
|
|
||||||
return (
|
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
|
||||||
<Loader />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isErrorItem || isErrorStreamUrl)
|
|
||||||
return (
|
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
|
||||||
<Text className="text-white">Error</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!item || !stream)
|
|
||||||
return (
|
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
|
||||||
<Text className="text-white">Error</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: windowDimensions.width,
|
|
||||||
height: windowDimensions.height,
|
|
||||||
position: "relative",
|
|
||||||
}}
|
|
||||||
className="flex flex-col items-center justify-center"
|
|
||||||
>
|
|
||||||
<View className="h-screen w-screen top-0 left-0 flex flex-col items-center justify-center p-4 absolute z-0">
|
|
||||||
<Image
|
|
||||||
source={poster}
|
|
||||||
style={{ width: "100%", height: "100%", resizeMode: "contain" }}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Pressable
|
|
||||||
onPress={() => {
|
|
||||||
setShowControls(!showControls);
|
|
||||||
}}
|
|
||||||
className="absolute z-0 h-full w-full opacity-0"
|
|
||||||
>
|
|
||||||
{videoSource && (
|
|
||||||
<Video
|
|
||||||
ref={videoRef}
|
|
||||||
source={videoSource}
|
|
||||||
style={{ width: "100%", height: "100%" }}
|
|
||||||
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
|
|
||||||
onProgress={onProgress}
|
|
||||||
onError={() => {}}
|
|
||||||
onLoad={() => {
|
|
||||||
if (firstTime.current === true) {
|
|
||||||
play();
|
|
||||||
firstTime.current = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
progressUpdateInterval={500}
|
|
||||||
playWhenInactive={true}
|
|
||||||
allowsExternalPlayback={true}
|
|
||||||
playInBackground={true}
|
|
||||||
pictureInPicture={true}
|
|
||||||
showNotificationControls={true}
|
|
||||||
ignoreSilentSwitch="ignore"
|
|
||||||
fullscreen={false}
|
|
||||||
onPlaybackStateChanged={(state) => {
|
|
||||||
setIsPlaying(state.isPlaying);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
<Controls
|
|
||||||
item={item}
|
|
||||||
videoRef={videoRef}
|
|
||||||
togglePlay={togglePlay}
|
|
||||||
isPlaying={isPlaying}
|
|
||||||
isSeeking={isSeeking}
|
|
||||||
progress={progress}
|
|
||||||
cacheProgress={cacheProgress}
|
|
||||||
isBuffering={isBuffering}
|
|
||||||
showControls={showControls}
|
|
||||||
setShowControls={setShowControls}
|
|
||||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
|
||||||
ignoreSafeAreas={ignoreSafeAreas}
|
|
||||||
enableTrickplay={false}
|
|
||||||
pause={pause}
|
|
||||||
play={play}
|
|
||||||
seek={seek}
|
|
||||||
isVlc={false}
|
|
||||||
stop={stop}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePoster(
|
|
||||||
item: BaseItemDto | null | undefined,
|
|
||||||
api: Api | null
|
|
||||||
): string | undefined {
|
|
||||||
const poster = useMemo(() => {
|
|
||||||
if (!item || !api) return undefined;
|
|
||||||
return item.Type === "Audio"
|
|
||||||
? `${api.basePath}/Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200`
|
|
||||||
: getBackdropUrl({
|
|
||||||
api,
|
|
||||||
item: item,
|
|
||||||
quality: 70,
|
|
||||||
width: 200,
|
|
||||||
});
|
|
||||||
}, [item, api]);
|
|
||||||
|
|
||||||
return poster ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVideoSource(
|
|
||||||
item: BaseItemDto | null | undefined,
|
|
||||||
api: Api | null,
|
|
||||||
poster: string | undefined,
|
|
||||||
url?: string | null
|
|
||||||
) {
|
|
||||||
const videoSource = useMemo(() => {
|
|
||||||
if (!item || !api || !url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startPosition = item?.UserData?.PlaybackPositionTicks
|
|
||||||
? Math.round(item.UserData.PlaybackPositionTicks / 10000)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
uri: url,
|
|
||||||
isNetwork: true,
|
|
||||||
startPosition,
|
|
||||||
headers: getAuthHeaders(api),
|
|
||||||
metadata: {
|
|
||||||
artist: item?.AlbumArtist ?? undefined,
|
|
||||||
title: item?.Name || "Unknown",
|
|
||||||
description: item?.Overview ?? undefined,
|
|
||||||
imageUri: poster,
|
|
||||||
subtitle: item?.Album ?? undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [item, api, poster]);
|
|
||||||
|
|
||||||
return videoSource;
|
|
||||||
}
|
|
||||||
@@ -414,7 +414,6 @@ const Player = () => {
|
|||||||
playWhenInactive={true}
|
playWhenInactive={true}
|
||||||
allowsExternalPlayback={true}
|
allowsExternalPlayback={true}
|
||||||
playInBackground={true}
|
playInBackground={true}
|
||||||
pictureInPicture={true}
|
|
||||||
showNotificationControls={true}
|
showNotificationControls={true}
|
||||||
ignoreSilentSwitch="ignore"
|
ignoreSilentSwitch="ignore"
|
||||||
fullscreen={false}
|
fullscreen={false}
|
||||||
@@ -532,7 +531,6 @@ export function useVideoSource(
|
|||||||
startPosition,
|
startPosition,
|
||||||
headers: getAuthHeaders(api),
|
headers: getAuthHeaders(api),
|
||||||
metadata: {
|
metadata: {
|
||||||
artist: item?.AlbumArtist ?? undefined,
|
|
||||||
title: item?.Name || "Unknown",
|
title: item?.Name || "Unknown",
|
||||||
description: item?.Overview ?? undefined,
|
description: item?.Overview ?? undefined,
|
||||||
imageUri: poster,
|
imageUri: poster,
|
||||||
|
|||||||
@@ -26,18 +26,6 @@ export const itemRouter = (
|
|||||||
return `/(auth)/(tabs)/${from}/series/${item.Id}`;
|
return `/(auth)/(tabs)/${from}/series/${item.Id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Type === "MusicAlbum") {
|
|
||||||
return `/(auth)/(tabs)/${from}/albums/${item.Id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.Type === "Audio") {
|
|
||||||
return `/(auth)/(tabs)/${from}/albums/${item.AlbumId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.Type === "MusicArtist") {
|
|
||||||
return `/(auth)/(tabs)/${from}/artists/${item.Id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.Type === "Person" || item.Type === "Actor") {
|
if (item.Type === "Person" || item.Type === "Actor") {
|
||||||
return `/(auth)/(tabs)/${from}/actors/${item.Id}`;
|
return `/(auth)/(tabs)/${from}/actors/${item.Id}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,14 +54,6 @@ export const Favorites = () => {
|
|||||||
() => fetchFavoritesByType("Playlist"),
|
() => fetchFavoritesByType("Playlist"),
|
||||||
[fetchFavoritesByType]
|
[fetchFavoritesByType]
|
||||||
);
|
);
|
||||||
const fetchFavoriteMusicAlbum = useCallback(
|
|
||||||
() => fetchFavoritesByType("MusicAlbum"),
|
|
||||||
[fetchFavoritesByType]
|
|
||||||
);
|
|
||||||
const fetchFavoriteAudio = useCallback(
|
|
||||||
() => fetchFavoritesByType("Audio"),
|
|
||||||
[fetchFavoritesByType]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex flex-co gap-y-4">
|
<View className="flex flex-co gap-y-4">
|
||||||
@@ -102,18 +94,6 @@ export const Favorites = () => {
|
|||||||
title="Playlists"
|
title="Playlists"
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
|
||||||
queryFn={fetchFavoriteMusicAlbum}
|
|
||||||
queryKey={["home", "favorites", "musicAlbums"]}
|
|
||||||
title="Music Albums"
|
|
||||||
hideIfEmpty
|
|
||||||
/>
|
|
||||||
<ScrollingCollectionList
|
|
||||||
queryFn={fetchFavoriteAudio}
|
|
||||||
queryKey={["home", "favorites", "audio"]}
|
|
||||||
title="Audio"
|
|
||||||
hideIfEmpty
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
|||||||
_itemType = "Series";
|
_itemType = "Series";
|
||||||
} else if (library.CollectionType === "boxsets") {
|
} else if (library.CollectionType === "boxsets") {
|
||||||
_itemType = "BoxSet";
|
_itemType = "BoxSet";
|
||||||
} else if (library.CollectionType === "music") {
|
|
||||||
_itemType = "MusicAlbum";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _itemType;
|
return _itemType;
|
||||||
@@ -76,8 +74,6 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
|||||||
nameStr = "series";
|
nameStr = "series";
|
||||||
} else if (library.CollectionType === "boxsets") {
|
} else if (library.CollectionType === "boxsets") {
|
||||||
nameStr = "box sets";
|
nameStr = "box sets";
|
||||||
} else if (library.CollectionType === "music") {
|
|
||||||
nameStr = "albums";
|
|
||||||
} else {
|
} else {
|
||||||
nameStr = "items";
|
nameStr = "items";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { View, ViewProps } from "react-native";
|
|
||||||
import { SongsListItem } from "./SongsListItem";
|
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
|
||||||
songs?: BaseItemDto[] | null;
|
|
||||||
collectionId: string;
|
|
||||||
artistId: string;
|
|
||||||
albumId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SongsList: React.FC<Props> = ({
|
|
||||||
collectionId,
|
|
||||||
artistId,
|
|
||||||
albumId,
|
|
||||||
songs = [],
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
return (
|
|
||||||
<View className="flex flex-col space-y-2" {...props}>
|
|
||||||
{songs?.map((item: BaseItemDto, index: number) => (
|
|
||||||
<SongsListItem
|
|
||||||
key={item.Id}
|
|
||||||
item={item}
|
|
||||||
index={index}
|
|
||||||
collectionId={collectionId}
|
|
||||||
artistId={artistId}
|
|
||||||
albumId={albumId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
|
||||||
import { runtimeTicksToSeconds } from "@/utils/time";
|
|
||||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
|
|
||||||
import CastContext, {
|
|
||||||
PlayServicesState,
|
|
||||||
useCastDevice,
|
|
||||||
useRemoteMediaClient,
|
|
||||||
} from "react-native-google-cast";
|
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
|
||||||
collectionId: string;
|
|
||||||
artistId: string;
|
|
||||||
albumId: string;
|
|
||||||
item: BaseItemDto;
|
|
||||||
index: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SongsListItem: React.FC<Props> = ({
|
|
||||||
collectionId,
|
|
||||||
artistId,
|
|
||||||
albumId,
|
|
||||||
item,
|
|
||||||
index,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const [api] = useAtom(apiAtom);
|
|
||||||
const [user] = useAtom(userAtom);
|
|
||||||
const castDevice = useCastDevice();
|
|
||||||
const router = useRouter();
|
|
||||||
const client = useRemoteMediaClient();
|
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
|
||||||
|
|
||||||
const { setPlaySettings } = usePlaySettings();
|
|
||||||
|
|
||||||
const openSelect = () => {
|
|
||||||
if (!castDevice?.deviceId) {
|
|
||||||
play("device");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = ["Chromecast", "Device", "Cancel"];
|
|
||||||
const cancelButtonIndex = 2;
|
|
||||||
|
|
||||||
showActionSheetWithOptions(
|
|
||||||
{
|
|
||||||
options,
|
|
||||||
cancelButtonIndex,
|
|
||||||
},
|
|
||||||
(selectedIndex: number | undefined) => {
|
|
||||||
switch (selectedIndex) {
|
|
||||||
case 0:
|
|
||||||
play("cast");
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
play("device");
|
|
||||||
break;
|
|
||||||
case cancelButtonIndex:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const play = useCallback(async (type: "device" | "cast") => {
|
|
||||||
if (!user?.Id || !api || !item.Id) {
|
|
||||||
console.warn("No user, api or item", user, api, item.Id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await setPlaySettings({
|
|
||||||
item,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data?.url) {
|
|
||||||
throw new Error("play-music ~ No stream url");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "cast" && client) {
|
|
||||||
await CastContext.getPlayServicesState().then((state) => {
|
|
||||||
if (state && state !== PlayServicesState.SUCCESS)
|
|
||||||
CastContext.showPlayServicesErrorDialog(state);
|
|
||||||
else {
|
|
||||||
client.loadMedia({
|
|
||||||
mediaInfo: {
|
|
||||||
contentUrl: data.url!,
|
|
||||||
contentType: "video/mp4",
|
|
||||||
metadata: {
|
|
||||||
type: item.Type === "Episode" ? "tvShow" : "movie",
|
|
||||||
title: item.Name || "",
|
|
||||||
subtitle: item.Overview || "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
startTime: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("Playing on device", data.url, item.Id);
|
|
||||||
router.push("/music-player");
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
openSelect();
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<View className="flex flex-row items-center space-x-4 bg-neutral-900 border-neutral-800 px-4 py-4 rounded-xl">
|
|
||||||
<Text className="opacity-50">{index + 1}</Text>
|
|
||||||
<View>
|
|
||||||
<Text className="mb-0.5 font-semibold">{item.Name}</Text>
|
|
||||||
<Text className="opacity-50 text-xs">
|
|
||||||
{runtimeTicksToSeconds(item.RunTimeTicks)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
|
||||||
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { Image } from "expo-image";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
type ArtistPosterProps = {
|
|
||||||
item?: BaseItemDto | null;
|
|
||||||
id?: string | null;
|
|
||||||
showProgress?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AlbumCover: React.FC<ArtistPosterProps> = ({ item, id }) => {
|
|
||||||
const [api] = useAtom(apiAtom);
|
|
||||||
|
|
||||||
const url = useMemo(() => {
|
|
||||||
const u = getPrimaryImageUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
});
|
|
||||||
return u;
|
|
||||||
}, [item]);
|
|
||||||
|
|
||||||
const url2 = useMemo(() => {
|
|
||||||
const u = getPrimaryImageUrlById({
|
|
||||||
api,
|
|
||||||
id,
|
|
||||||
quality: 85,
|
|
||||||
width: 300,
|
|
||||||
});
|
|
||||||
return u;
|
|
||||||
}, [item]);
|
|
||||||
|
|
||||||
if (!item && id)
|
|
||||||
return (
|
|
||||||
<View className="relative rounded-lg overflow-hidden border border-neutral-900">
|
|
||||||
<Image
|
|
||||||
key={id}
|
|
||||||
id={id}
|
|
||||||
source={
|
|
||||||
url2
|
|
||||||
? {
|
|
||||||
uri: url2,
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
cachePolicy={"memory-disk"}
|
|
||||||
contentFit="cover"
|
|
||||||
style={{
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (item)
|
|
||||||
return (
|
|
||||||
<View className="relative rounded-md overflow-hidden border border-neutral-900">
|
|
||||||
<Image
|
|
||||||
key={item.Id}
|
|
||||||
id={item.Id}
|
|
||||||
source={
|
|
||||||
url
|
|
||||||
? {
|
|
||||||
uri: url,
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
cachePolicy={"memory-disk"}
|
|
||||||
contentFit="cover"
|
|
||||||
style={{
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlbumCover;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { Image } from "expo-image";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
type ArtistPosterProps = {
|
|
||||||
item: BaseItemDto;
|
|
||||||
showProgress?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ArtistPoster: React.FC<ArtistPosterProps> = ({
|
|
||||||
item,
|
|
||||||
showProgress = false,
|
|
||||||
}) => {
|
|
||||||
const [api] = useAtom(apiAtom);
|
|
||||||
|
|
||||||
const url = useMemo(
|
|
||||||
() =>
|
|
||||||
getPrimaryImageUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
}),
|
|
||||||
[item]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!url)
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
className="rounded-lg overflow-hidden border border-neutral-900"
|
|
||||||
style={{
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
}}
|
|
||||||
></View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="relative rounded-md overflow-hidden border border-neutral-900">
|
|
||||||
<Image
|
|
||||||
key={item.Id}
|
|
||||||
id={item.Id}
|
|
||||||
source={{
|
|
||||||
uri: url,
|
|
||||||
}}
|
|
||||||
cachePolicy={"memory-disk"}
|
|
||||||
contentFit="cover"
|
|
||||||
style={{
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ArtistPoster;
|
|
||||||
@@ -17,14 +17,7 @@ export const commonScreenOptions: ICommonScreenOptions = {
|
|||||||
headerLeft: () => <HeaderBackButton />,
|
headerLeft: () => <HeaderBackButton />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const routes = [
|
const routes = ["actors/[actorId]", "items/page", "series/[id]"];
|
||||||
"actors/[actorId]",
|
|
||||||
"albums/[albumId]",
|
|
||||||
"artists/index",
|
|
||||||
"artists/[artistId]",
|
|
||||||
"items/page",
|
|
||||||
"series/[id]",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const nestedTabPageScreenOptions: Record<string, ICommonScreenOptions> =
|
export const nestedTabPageScreenOptions: Record<string, ICommonScreenOptions> =
|
||||||
Object.fromEntries(routes.map((route) => [route, commonScreenOptions]));
|
Object.fromEntries(routes.map((route) => [route, commonScreenOptions]));
|
||||||
|
|||||||
@@ -449,12 +449,11 @@ export const Controls: React.FC<Props> = ({
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: -57,
|
left: -62,
|
||||||
bottom: 15,
|
bottom: 0,
|
||||||
paddingTop: 30,
|
paddingTop: 30,
|
||||||
paddingBottom: 5,
|
paddingBottom: 5,
|
||||||
width: tileWidth * 1.5,
|
width: tileWidth * 1.5,
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
@@ -533,7 +532,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
pointerEvents={showControls ? "auto" : "none"}
|
pointerEvents={showControls ? "auto" : "none"}
|
||||||
className={`flex flex-row w-full p-4 `}
|
className={`flex flex-row w-full pt-2`}
|
||||||
>
|
>
|
||||||
<View className="mr-auto">
|
<View className="mr-auto">
|
||||||
<VideoProvider
|
<VideoProvider
|
||||||
@@ -557,7 +556,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
switchOnEpisodeMode();
|
switchOnEpisodeMode();
|
||||||
}}
|
}}
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||||
>
|
>
|
||||||
<Ionicons name="list" size={24} color="white" />
|
<Ionicons name="list" size={24} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -565,7 +564,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
{previousItem && !offline && (
|
{previousItem && !offline && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={goToPreviousItem}
|
onPress={goToPreviousItem}
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||||
>
|
>
|
||||||
<Ionicons name="play-skip-back" size={24} color="white" />
|
<Ionicons name="play-skip-back" size={24} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -574,7 +573,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
{nextItem && !offline && (
|
{nextItem && !offline && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={goToNextItem}
|
onPress={goToNextItem}
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||||
>
|
>
|
||||||
<Ionicons name="play-skip-forward" size={24} color="white" />
|
<Ionicons name="play-skip-forward" size={24} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -583,7 +582,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
{/* {mediaSource?.TranscodingUrl && ( */}
|
{/* {mediaSource?.TranscodingUrl && ( */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={toggleIgnoreSafeAreas}
|
onPress={toggleIgnoreSafeAreas}
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={ignoreSafeAreas ? "contract-outline" : "expand"}
|
name={ignoreSafeAreas ? "contract-outline" : "expand"}
|
||||||
@@ -600,7 +599,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
router.back();
|
router.back();
|
||||||
}}
|
}}
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||||
>
|
>
|
||||||
<Ionicons name="close" size={24} color="white" />
|
<Ionicons name="close" size={24} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -730,11 +729,11 @@ export const Controls: React.FC<Props> = ({
|
|||||||
bottom: settings?.safeAreaInControlsEnabled ? insets.bottom : 0,
|
bottom: settings?.safeAreaInControlsEnabled ? insets.bottom : 0,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
className={`flex flex-col p-4`}
|
className={`flex flex-col px-2`}
|
||||||
onTouchStart={handleControlsInteraction}
|
onTouchStart={handleControlsInteraction}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
className="shrink flex flex-col justify-center h-full mb-2"
|
className="shrink flex flex-col justify-center h-full"
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
@@ -748,10 +747,12 @@ export const Controls: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
pointerEvents={showControls ? "box-none" : "none"}
|
pointerEvents={showControls ? "box-none" : "none"}
|
||||||
>
|
>
|
||||||
<Text className="font-bold">{item?.Name}</Text>
|
|
||||||
{item?.Type === "Episode" && (
|
{item?.Type === "Episode" && (
|
||||||
<Text className="opacity-50">{item.SeriesName}</Text>
|
<Text className="opacity-50">
|
||||||
|
{`${item.SeriesName} - ${item.SeasonName} Episode ${item.IndexNumber}`}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
<Text className="font-bold text-xl">{item?.Name}</Text>
|
||||||
{item?.Type === "Movie" && (
|
{item?.Type === "Movie" && (
|
||||||
<Text className="text-xs opacity-50">
|
<Text className="text-xs opacity-50">
|
||||||
{item?.ProductionYear}
|
{item?.ProductionYear}
|
||||||
@@ -786,7 +787,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
className={`flex flex-col-reverse py-4 pb-1 px-4 rounded-lg items-center bg-neutral-800`}
|
className={`flex flex-col-reverse rounded-lg items-center my-2`}
|
||||||
style={{
|
style={{
|
||||||
opacity: showControls ? 1 : 0,
|
opacity: showControls ? 1 : 0,
|
||||||
}}
|
}}
|
||||||
@@ -802,19 +803,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
bubbleTextColor: "#666",
|
bubbleTextColor: "#666",
|
||||||
heartbeatColor: "#999",
|
heartbeatColor: "#999",
|
||||||
}}
|
}}
|
||||||
renderThumb={() => (
|
renderThumb={() => null}
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
left: -2,
|
|
||||||
borderRadius: 10,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
cache={cacheProgress}
|
cache={cacheProgress}
|
||||||
onSlidingStart={handleSliderStart}
|
onSlidingStart={handleSliderStart}
|
||||||
onSlidingComplete={handleSliderComplete}
|
onSlidingComplete={handleSliderComplete}
|
||||||
@@ -829,7 +818,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
minimumValue={min}
|
minimumValue={min}
|
||||||
maximumValue={max}
|
maximumValue={max}
|
||||||
/>
|
/>
|
||||||
<View className="flex flex-row items-center justify-between mt-0.5">
|
<View className="flex flex-row items-center justify-between mt-2">
|
||||||
<Text className="text-[12px] text-neutral-400">
|
<Text className="text-[12px] text-neutral-400">
|
||||||
{formatTimeString(currentTime, isVlc ? "ms" : "s")}
|
{formatTimeString(currentTime, isVlc ? "ms" : "s")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const VideoTouchOverlay = ({
|
|||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
opacity: showControls ? 0.5 : 0,
|
opacity: showControls ? 0.75 : 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const DropdownViewDirect: React.FC<DropdownViewDirectProps> = ({
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<TouchableOpacity className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2">
|
<TouchableOpacity className="aspect-square flex flex-col rounded-xl items-center justify-center p-2">
|
||||||
<Ionicons name="ellipsis-horizontal" size={24} color={"white"} />
|
<Ionicons name="ellipsis-horizontal" size={24} color={"white"} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const DropdownView: React.FC<DropdownViewProps> = ({ showControls }) => {
|
|||||||
<View>
|
<View>
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<TouchableOpacity className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2">
|
<TouchableOpacity className="aspect-square flex flex-col rounded-xl items-center justify-center p-2">
|
||||||
<Ionicons name="ellipsis-horizontal" size={24} color={"white"} />
|
<Ionicons name="ellipsis-horizontal" size={24} color={"white"} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
"preset": "jest-expo"
|
"preset": "jest-expo"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bottom-tabs/react-navigation": "^0.7.1",
|
"@bottom-tabs/react-navigation": "0.7.8",
|
||||||
|
"react-native-bottom-tabs": "0.7.8",
|
||||||
"@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
|
"@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
|
||||||
"@expo/react-native-action-sheet": "^4.1.0",
|
"@expo/react-native-action-sheet": "^4.1.0",
|
||||||
"@expo/vector-icons": "^14.0.4",
|
"@expo/vector-icons": "^14.0.4",
|
||||||
@@ -73,7 +74,6 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.74.5",
|
"react-native": "0.74.5",
|
||||||
"react-native-awesome-slider": "^2.5.6",
|
"react-native-awesome-slider": "^2.5.6",
|
||||||
"react-native-bottom-tabs": "0.7.8",
|
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
"react-native-compressor": "^1.9.0",
|
"react-native-compressor": "^1.9.0",
|
||||||
"react-native-country-flag": "^2.0.2",
|
"react-native-country-flag": "^2.0.2",
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ type PlaySettingsContextType = {
|
|||||||
setPlayUrl: React.Dispatch<React.SetStateAction<string | null>>;
|
setPlayUrl: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
playSessionId?: string | null;
|
playSessionId?: string | null;
|
||||||
setOfflineSettings: (data: PlaybackType) => void;
|
setOfflineSettings: (data: PlaybackType) => void;
|
||||||
setMusicPlaySettings: (item: BaseItemDto, url: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlaySettingsContext = createContext<PlaySettingsContextType | undefined>(
|
const PlaySettingsContext = createContext<PlaySettingsContextType | undefined>(
|
||||||
@@ -61,13 +60,6 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
_setPlaySettings(data);
|
_setPlaySettings(data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setMusicPlaySettings = (item: BaseItemDto, url: string) => {
|
|
||||||
setPlaySettings({
|
|
||||||
item: item,
|
|
||||||
});
|
|
||||||
setPlayUrl(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setPlaySettings = useCallback(
|
const setPlaySettings = useCallback(
|
||||||
async (
|
async (
|
||||||
dataOrUpdater:
|
dataOrUpdater:
|
||||||
@@ -147,7 +139,6 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
setPlaySettings,
|
setPlaySettings,
|
||||||
playUrl,
|
playUrl,
|
||||||
setPlayUrl,
|
setPlayUrl,
|
||||||
setMusicPlaySettings,
|
|
||||||
setOfflineSettings,
|
setOfflineSettings,
|
||||||
playSessionId,
|
playSessionId,
|
||||||
mediaSource,
|
mediaSource,
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import {
|
|||||||
* readonly Unknown: "unknown";
|
* readonly Unknown: "unknown";
|
||||||
readonly Movies: "movies";
|
readonly Movies: "movies";
|
||||||
readonly Tvshows: "tvshows";
|
readonly Tvshows: "tvshows";
|
||||||
readonly Music: "music";
|
|
||||||
readonly Musicvideos: "musicvideos";
|
|
||||||
readonly Trailers: "trailers";
|
readonly Trailers: "trailers";
|
||||||
readonly Homevideos: "homevideos";
|
readonly Homevideos: "homevideos";
|
||||||
readonly Boxsets: "boxsets";
|
readonly Boxsets: "boxsets";
|
||||||
@@ -33,8 +31,6 @@ export const colletionTypeToItemType = (
|
|||||||
return BaseItemKind.Series;
|
return BaseItemKind.Series;
|
||||||
case CollectionType.Homevideos:
|
case CollectionType.Homevideos:
|
||||||
return BaseItemKind.Video;
|
return BaseItemKind.Video;
|
||||||
case CollectionType.Musicvideos:
|
|
||||||
return BaseItemKind.MusicVideo;
|
|
||||||
case CollectionType.Books:
|
case CollectionType.Books:
|
||||||
return BaseItemKind.Book;
|
return BaseItemKind.Book;
|
||||||
case CollectionType.Playlists:
|
case CollectionType.Playlists:
|
||||||
|
|||||||
Reference in New Issue
Block a user