mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
Merge branch 'develop' into view-password
This commit is contained in:
4
app.json
4
app.json
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Streamyfin",
|
"name": "Streamyfin",
|
||||||
"slug": "streamyfin",
|
"slug": "streamyfin",
|
||||||
"version": "0.35.0",
|
"version": "0.35.1",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "streamyfin",
|
"scheme": "streamyfin",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"jsEngine": "hermes",
|
"jsEngine": "hermes",
|
||||||
"versionCode": 66,
|
"versionCode": 67,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/images/icon-android-plain.png",
|
"foregroundImage": "./assets/images/icon-android-plain.png",
|
||||||
"monochromeImage": "./assets/images/icon-android-themed.png",
|
"monochromeImage": "./assets/images/icon-android-themed.png",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings(null);
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
|
|||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const [_settings, _updateSettings, pluginSettings] = useSettings(null);
|
const { pluginSettings } = useSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisabledSetting
|
<DisabledSetting
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function page() {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings(null);
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");
|
const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Dis
|
|||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const local = useLocalSearchParams();
|
const local = useLocalSearchParams();
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
|
||||||
|
|
||||||
const { companyId, image, type } = local as unknown as {
|
const { companyId, image, type } = local as unknown as {
|
||||||
companyId: string;
|
companyId: string;
|
||||||
@@ -53,7 +53,10 @@ export default function page() {
|
|||||||
uniqBy(
|
uniqBy(
|
||||||
data?.pages
|
data?.pages
|
||||||
?.filter((p) => p?.results.length)
|
?.filter((p) => p?.results.length)
|
||||||
.flatMap((p) => p?.results ?? []),
|
.flatMap(
|
||||||
|
(p) =>
|
||||||
|
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [],
|
||||||
|
),
|
||||||
"id",
|
"id",
|
||||||
) ?? [],
|
) ?? [],
|
||||||
[data],
|
[data],
|
||||||
@@ -98,9 +101,7 @@ export default function page() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
renderItem={(item, _index) => (
|
renderItem={(item, _index) => <JellyseerrPoster item={item} />}
|
||||||
<JellyseerrPoster item={item as MovieResult | TvResult} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,10 @@ import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
|
|||||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||||
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
|
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
||||||
import {
|
|
||||||
type MovieResult,
|
|
||||||
type TvResult,
|
|
||||||
} from "@/utils/jellyseerr/server/models/Search";
|
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const local = useLocalSearchParams();
|
const local = useLocalSearchParams();
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
|
||||||
|
|
||||||
const { genreId, name, type } = local as unknown as {
|
const { genreId, name, type } = local as unknown as {
|
||||||
genreId: string;
|
genreId: string;
|
||||||
@@ -51,7 +47,10 @@ export default function page() {
|
|||||||
uniqBy(
|
uniqBy(
|
||||||
data?.pages
|
data?.pages
|
||||||
?.filter((p) => p?.results.length)
|
?.filter((p) => p?.results.length)
|
||||||
.flatMap((p) => p?.results ?? []),
|
.flatMap(
|
||||||
|
(p) =>
|
||||||
|
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [],
|
||||||
|
),
|
||||||
"id",
|
"id",
|
||||||
) ?? [],
|
) ?? [],
|
||||||
[data],
|
[data],
|
||||||
@@ -62,7 +61,7 @@ export default function page() {
|
|||||||
jellyseerrApi
|
jellyseerrApi
|
||||||
? flatData.map((r) =>
|
? flatData.map((r) =>
|
||||||
jellyseerrApi.imageProxy(
|
jellyseerrApi.imageProxy(
|
||||||
(r as TvResult | MovieResult).backdropPath,
|
r.backdropPath,
|
||||||
"w1920_and_h800_multi_faces",
|
"w1920_and_h800_multi_faces",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -92,9 +91,7 @@ export default function page() {
|
|||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
renderItem={(item, _index) => (
|
renderItem={(item, _index) => <JellyseerrPoster item={item} />}
|
||||||
<JellyseerrPoster item={item as MovieResult | TvResult} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import { OverviewText } from "@/components/OverviewText";
|
|||||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
||||||
import type {
|
|
||||||
MovieResult,
|
|
||||||
TvResult,
|
|
||||||
} from "@/utils/jellyseerr/server/models/Search";
|
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const local = useLocalSearchParams();
|
const local = useLocalSearchParams();
|
||||||
@@ -106,9 +102,7 @@ export default function page() {
|
|||||||
MainContent={() => (
|
MainContent={() => (
|
||||||
<OverviewText text={data?.details?.biography} className='mt-4' />
|
<OverviewText text={data?.details?.biography} className='mt-4' />
|
||||||
)}
|
)}
|
||||||
renderItem={(item, _index) => (
|
renderItem={(item, _index) => <JellyseerrPoster item={item} />}
|
||||||
<JellyseerrPoster item={item as MovieResult | TvResult} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useAtom } from "jotai";
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorrizontalScroll";
|
import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorizontalScroll";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||||
import { ItemCardText } from "@/components/ItemCardText";
|
import { ItemCardText } from "@/components/ItemCardText";
|
||||||
@@ -22,21 +22,21 @@ import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
|||||||
|
|
||||||
const page: React.FC = () => {
|
const page: React.FC = () => {
|
||||||
const local = useLocalSearchParams();
|
const local = useLocalSearchParams();
|
||||||
const { actorId } = local as { actorId: string };
|
const { personId } = local as { personId: string };
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
|
||||||
const { data: item, isLoading: l1 } = useQuery({
|
const { data: item, isLoading: l1 } = useQuery({
|
||||||
queryKey: ["item", actorId],
|
queryKey: ["item", personId],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
await getUserItemData({
|
await getUserItemData({
|
||||||
api,
|
api,
|
||||||
userId: user?.Id,
|
userId: user?.Id,
|
||||||
itemId: actorId,
|
itemId: personId,
|
||||||
}),
|
}),
|
||||||
enabled: !!actorId && !!api,
|
enabled: !!personId && !!api,
|
||||||
staleTime: 60,
|
staleTime: 60,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ const page: React.FC = () => {
|
|||||||
|
|
||||||
const response = await getItemsApi(api).getItems({
|
const response = await getItemsApi(api).getItems({
|
||||||
userId: user.Id,
|
userId: user.Id,
|
||||||
personIds: [actorId],
|
personIds: [personId],
|
||||||
startIndex: pageParam,
|
startIndex: pageParam,
|
||||||
limit: 16,
|
limit: 16,
|
||||||
sortOrder: ["Descending", "Descending", "Ascending"],
|
sortOrder: ["Descending", "Descending", "Ascending"],
|
||||||
@@ -68,7 +68,7 @@ const page: React.FC = () => {
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
[api, user?.Id, actorId],
|
[api, user?.Id, personId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const backdropUrl = useMemo(
|
const backdropUrl = useMemo(
|
||||||
@@ -131,7 +131,7 @@ const page: React.FC = () => {
|
|||||||
</TouchableItemRouter>
|
</TouchableItemRouter>
|
||||||
)}
|
)}
|
||||||
queryFn={fetchItems}
|
queryFn={fetchItems}
|
||||||
queryKey={["actor", "movies", actorId]}
|
queryKey={["actor", "movies", personId]}
|
||||||
/>
|
/>
|
||||||
<View className='h-12' />
|
<View className='h-12' />
|
||||||
</View>
|
</View>
|
||||||
@@ -9,7 +9,7 @@ const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function IndexLayout() {
|
export default function IndexLayout() {
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings(null);
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function index() {
|
|||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function search() {
|
|||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
|
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi } = useJellyseerr();
|
||||||
const [jellyseerrOrderBy, setJellyseerrOrderBy] =
|
const [jellyseerrOrderBy, setJellyseerrOrderBy] =
|
||||||
useState<JellyseerrSearchSort>(
|
useState<JellyseerrSearchSort>(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const NativeTabs = withLayoutContext<
|
|||||||
>(Navigator);
|
>(Navigator);
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default function page() {
|
|||||||
/** Playback position in ticks. */
|
/** Playback position in ticks. */
|
||||||
playbackPosition?: string;
|
playbackPosition?: string;
|
||||||
}>();
|
}>();
|
||||||
const [_settings] = useSettings(null);
|
useSettings();
|
||||||
|
|
||||||
const offline = offlineStr === "true";
|
const offline = offlineStr === "true";
|
||||||
const playbackManager = usePlaybackManager();
|
const playbackManager = usePlaybackManager();
|
||||||
@@ -748,7 +748,6 @@ export default function page() {
|
|||||||
setAspectRatio={setAspectRatio}
|
setAspectRatio={setAspectRatio}
|
||||||
setScaleFactor={setScaleFactor}
|
setScaleFactor={setScaleFactor}
|
||||||
isVlc
|
isVlc
|
||||||
api={api}
|
|
||||||
downloadedFiles={downloadedFiles}
|
downloadedFiles={downloadedFiles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ const queryClient = new QueryClient({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Layout() {
|
function Layout() {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const appState = useRef(AppState.currentState);
|
const appState = useRef(AppState.currentState);
|
||||||
@@ -269,6 +269,15 @@ function Layout() {
|
|||||||
await Notifications?.setNotificationChannelAsync("default", {
|
await Notifications?.setNotificationChannelAsync("default", {
|
||||||
name: "default",
|
name: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create dedicated channel for download notifications
|
||||||
|
console.log("Setting android notification channel 'downloads'");
|
||||||
|
await Notifications?.setNotificationChannelAsync("downloads", {
|
||||||
|
name: "Downloads",
|
||||||
|
importance: Notifications.AndroidImportance.DEFAULT,
|
||||||
|
vibrationPattern: [0, 250, 250, 250],
|
||||||
|
lightColor: "#FF231F7C",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const granted = await checkAndRequestPermissions();
|
const granted = await checkAndRequestPermissions();
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [queue, _setQueue] = useAtom(queueAtom);
|
const [queue, _setQueue] = useAtom(queueAtom);
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false);
|
const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false);
|
||||||
|
|
||||||
const { processes, startBackgroundDownload, getDownloadedItems } =
|
const { processes, startBackgroundDownload, getDownloadedItems } =
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ interface ItemContentProps {
|
|||||||
export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||||
({ item, isOffline }) => {
|
({ item, isOffline }) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const { orientation } = useOrientation();
|
const { orientation } = useOrientation();
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useAtom } from "jotai";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View, type ViewProps } from "react-native";
|
import { View, type ViewProps } from "react-native";
|
||||||
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
|
import { HorizontalScroll } from "@/components/common/HorizontalScroll";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||||
import { ItemCardText } from "@/components/ItemCardText";
|
import { ItemCardText } from "@/components/ItemCardText";
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const startColor = useSharedValue(colorAtom);
|
const startColor = useSharedValue(colorAtom);
|
||||||
const widthProgress = useSharedValue(0);
|
const widthProgress = useSharedValue(0);
|
||||||
const colorChangeProgress = useSharedValue(0);
|
const colorChangeProgress = useSharedValue(0);
|
||||||
const [settings, updateSettings] = useSettings(null);
|
const { settings, updateSettings } = useSettings();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const startColor = useSharedValue(colorAtom);
|
const startColor = useSharedValue(colorAtom);
|
||||||
const widthProgress = useSharedValue(0);
|
const widthProgress = useSharedValue(0);
|
||||||
const colorChangeProgress = useSharedValue(0);
|
const colorChangeProgress = useSharedValue(0);
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { View, type ViewProps } from "react-native";
|
import { View, type ViewProps } from "react-native";
|
||||||
import MoviePoster from "@/components/posters/MoviePoster";
|
import MoviePoster from "@/components/posters/MoviePoster";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { HorizontalScroll } from "./common/HorrizontalScroll";
|
import { HorizontalScroll } from "./common/HorizontalScroll";
|
||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
import { TouchableItemRouter } from "./common/TouchableItemRouter";
|
import { TouchableItemRouter } from "./common/TouchableItemRouter";
|
||||||
import { ItemCardText } from "./ItemCardText";
|
import { ItemCardText } from "./ItemCardText";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Permission,
|
Permission,
|
||||||
} from "@/utils/jellyseerr/server/lib/permissions";
|
} from "@/utils/jellyseerr/server/lib/permissions";
|
||||||
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
||||||
|
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
||||||
import type {
|
import type {
|
||||||
MovieResult,
|
MovieResult,
|
||||||
TvResult,
|
TvResult,
|
||||||
@@ -17,7 +18,7 @@ import type {
|
|||||||
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
|
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
result?: MovieResult | TvResult | MovieDetails | TvDetails;
|
result?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast;
|
||||||
mediaTitle: string;
|
mediaTitle: string;
|
||||||
releaseYear: number;
|
releaseYear: number;
|
||||||
canRequest: boolean;
|
canRequest: boolean;
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||||
import type {
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
BaseItemDto,
|
|
||||||
BaseItemPerson,
|
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { useRouter, useSegments } from "expo-router";
|
import { useRouter, useSegments } from "expo-router";
|
||||||
import { type PropsWithChildren, useCallback } from "react";
|
import { type PropsWithChildren, useCallback } from "react";
|
||||||
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
||||||
@@ -14,10 +11,7 @@ interface Props extends TouchableOpacityProps {
|
|||||||
isOffline?: boolean;
|
isOffline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const itemRouter = (
|
export const itemRouter = (item: BaseItemDto, from: string) => {
|
||||||
item: BaseItemDto | BaseItemPerson,
|
|
||||||
from: string,
|
|
||||||
) => {
|
|
||||||
if ("CollectionType" in item && item.CollectionType === "livetv") {
|
if ("CollectionType" in item && item.CollectionType === "livetv") {
|
||||||
return `/(auth)/(tabs)/${from}/livetv`;
|
return `/(auth)/(tabs)/${from}/livetv`;
|
||||||
}
|
}
|
||||||
@@ -26,8 +20,8 @@ export const itemRouter = (
|
|||||||
return `/(auth)/(tabs)/${from}/series/${item.Id}`;
|
return `/(auth)/(tabs)/${from}/series/${item.Id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Type === "Person" || item.Type === "Actor") {
|
if (item.Type === "Person") {
|
||||||
return `/(auth)/(tabs)/${from}/actors/${item.Id}`;
|
return `/(auth)/(tabs)/${from}/persons/${item.Id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Type === "BoxSet") {
|
if (item.Type === "BoxSet") {
|
||||||
|
|||||||
@@ -1,27 +1,9 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { Image } from "expo-image";
|
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useMemo } from "react";
|
import { View, type ViewProps } from "react-native";
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
TouchableOpacity,
|
|
||||||
type TouchableOpacityProps,
|
|
||||||
View,
|
|
||||||
type ViewProps,
|
|
||||||
} from "react-native";
|
|
||||||
import { toast } from "sonner-native";
|
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { JobStatus } from "@/providers/Downloads/types";
|
import { JobStatus } from "@/providers/Downloads/types";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { DownloadCard } from "./DownloadCard";
|
||||||
import { formatTimeString } from "@/utils/time";
|
|
||||||
import { Button } from "../Button";
|
|
||||||
|
|
||||||
const bytesToMB = (bytes: number) => {
|
|
||||||
return bytes / 1024 / 1024;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ActiveDownloadsProps extends ViewProps {}
|
interface ActiveDownloadsProps extends ViewProps {}
|
||||||
|
|
||||||
@@ -52,163 +34,3 @@ export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DownloadCardProps extends TouchableOpacityProps {
|
|
||||||
process: JobStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
|
||||||
const { startDownload, pauseDownload, resumeDownload, removeProcess } =
|
|
||||||
useDownload();
|
|
||||||
const router = useRouter();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const handlePause = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await pauseDownload(id);
|
|
||||||
toast.success(t("home.downloads.toasts.download_paused"));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error pausing download:", error);
|
|
||||||
toast.error(t("home.downloads.toasts.could_not_pause_download"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResume = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await resumeDownload(id);
|
|
||||||
toast.success(t("home.downloads.toasts.download_resumed"));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error resuming download:", error);
|
|
||||||
toast.error(t("home.downloads.toasts.could_not_resume_download"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await removeProcess(id);
|
|
||||||
toast.success(t("home.downloads.toasts.download_deleted"));
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["downloads"] });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting download:", error);
|
|
||||||
toast.error(t("home.downloads.toasts.could_not_delete_download"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const eta = (p: JobStatus) => {
|
|
||||||
if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null;
|
|
||||||
|
|
||||||
const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0);
|
|
||||||
if (bytesRemaining <= 0) return null;
|
|
||||||
|
|
||||||
const secondsRemaining = bytesRemaining / p.speed;
|
|
||||||
|
|
||||||
return formatTimeString(secondsRemaining, "s");
|
|
||||||
};
|
|
||||||
|
|
||||||
const base64Image = useMemo(() => {
|
|
||||||
return storage.getString(process.item.Id!);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => router.push(`/(auth)/items/page?id=${process.item.Id}`)}
|
|
||||||
className='relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden'
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{process.status === "downloading" && (
|
|
||||||
<View
|
|
||||||
className={`
|
|
||||||
bg-purple-600 h-1 absolute bottom-0 left-0
|
|
||||||
`}
|
|
||||||
style={{
|
|
||||||
width: process.progress
|
|
||||||
? `${Math.max(5, process.progress)}%`
|
|
||||||
: "5%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<View className='px-3 py-1.5 flex flex-col w-full'>
|
|
||||||
<View className='flex flex-row items-center w-full'>
|
|
||||||
{base64Image && (
|
|
||||||
<View className='w-14 aspect-[10/15] rounded-lg overflow-hidden mr-4'>
|
|
||||||
<Image
|
|
||||||
source={{
|
|
||||||
uri: `data:image/jpeg;base64,${base64Image}`,
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
contentFit='cover'
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View className='shrink mb-1'>
|
|
||||||
<Text className='text-xs opacity-50'>{process.item.Type}</Text>
|
|
||||||
<Text className='font-semibold shrink'>{process.item.Name}</Text>
|
|
||||||
<Text className='text-xs opacity-50'>
|
|
||||||
{process.item.ProductionYear}
|
|
||||||
</Text>
|
|
||||||
<View className='flex flex-row items-center space-x-2 mt-1 text-purple-600'>
|
|
||||||
{process.progress === 0 ? (
|
|
||||||
<ActivityIndicator size={"small"} color={"white"} />
|
|
||||||
) : (
|
|
||||||
<Text className='text-xs'>{process.progress.toFixed(0)}%</Text>
|
|
||||||
)}
|
|
||||||
{process.speed && process.speed > 0 && (
|
|
||||||
<Text className='text-xs'>
|
|
||||||
{bytesToMB(process.speed).toFixed(2)} MB/s
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{eta(process) && (
|
|
||||||
<Text className='text-xs'>
|
|
||||||
{t("home.downloads.eta", { eta: eta(process) })}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className='flex flex-row items-center space-x-2 mt-1 text-purple-600'>
|
|
||||||
<Text className='text-xs capitalize'>{process.status}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View className='ml-auto flex flex-row items-center space-x-2'>
|
|
||||||
{process.status === "downloading" && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => handlePause(process.id)}
|
|
||||||
className='p-2 rounded-full bg-yellow-600'
|
|
||||||
>
|
|
||||||
<Ionicons name='pause' size={20} color='white' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{process.status === "paused" && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => handleResume(process.id)}
|
|
||||||
className='p-2 rounded-full bg-green-600'
|
|
||||||
>
|
|
||||||
<Ionicons name='play' size={20} color='white' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => handleDelete(process.id)}
|
|
||||||
className='p-2 rounded-full bg-red-600'
|
|
||||||
>
|
|
||||||
<Ionicons name='close' size={20} color='white' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{process.status === "completed" && (
|
|
||||||
<View className='flex flex-row mt-4 space-x-4'>
|
|
||||||
<Button
|
|
||||||
onPress={() => {
|
|
||||||
startDownload(process);
|
|
||||||
}}
|
|
||||||
className='w-full'
|
|
||||||
>
|
|
||||||
Download now
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
198
components/downloads/DownloadCard.tsx
Normal file
198
components/downloads/DownloadCard.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Image } from "expo-image";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
TouchableOpacity,
|
||||||
|
type TouchableOpacityProps,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
|
import { JobStatus } from "@/providers/Downloads/types";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
import { formatTimeString } from "@/utils/time";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
const bytesToMB = (bytes: number) => {
|
||||||
|
return bytes / 1024 / 1024;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DownloadCardProps extends TouchableOpacityProps {
|
||||||
|
process: JobStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||||
|
const { startDownload, pauseDownload, resumeDownload, removeProcess } =
|
||||||
|
useDownload();
|
||||||
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const handlePause = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await pauseDownload(id);
|
||||||
|
toast.success(t("home.downloads.toasts.download_paused"));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error pausing download:", error);
|
||||||
|
toast.error(t("home.downloads.toasts.could_not_pause_download"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResume = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await resumeDownload(id);
|
||||||
|
toast.success(t("home.downloads.toasts.download_resumed"));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error resuming download:", error);
|
||||||
|
toast.error(t("home.downloads.toasts.could_not_resume_download"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await removeProcess(id);
|
||||||
|
toast.success(t("home.downloads.toasts.download_deleted"));
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["downloads"] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting download:", error);
|
||||||
|
toast.error(t("home.downloads.toasts.could_not_delete_download"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const eta = (p: JobStatus) => {
|
||||||
|
if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null;
|
||||||
|
|
||||||
|
const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0);
|
||||||
|
if (bytesRemaining <= 0) return null;
|
||||||
|
|
||||||
|
const secondsRemaining = bytesRemaining / p.speed;
|
||||||
|
|
||||||
|
return formatTimeString(secondsRemaining, "s");
|
||||||
|
};
|
||||||
|
|
||||||
|
const base64Image = useMemo(() => {
|
||||||
|
return storage.getString(process.item.Id!);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Sanitize progress to ensure it's within valid bounds
|
||||||
|
const sanitizedProgress = useMemo(() => {
|
||||||
|
if (
|
||||||
|
typeof process.progress !== "number" ||
|
||||||
|
Number.isNaN(process.progress)
|
||||||
|
) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.min(100, process.progress));
|
||||||
|
}, [process.progress]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.push(`/(auth)/items/page?id=${process.item.Id}`)}
|
||||||
|
className='relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{process.status === "downloading" && (
|
||||||
|
<View
|
||||||
|
className={`
|
||||||
|
bg-purple-600 h-1 absolute bottom-0 left-0
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
width:
|
||||||
|
sanitizedProgress > 0
|
||||||
|
? `${Math.max(5, sanitizedProgress)}%`
|
||||||
|
: "5%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action buttons in top right corner */}
|
||||||
|
<View className='absolute top-2 right-2 flex flex-row items-center space-x-2 z-10'>
|
||||||
|
{process.status === "downloading" && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handlePause(process.id)}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Ionicons name='pause' size={20} color='white' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
{process.status === "paused" && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleResume(process.id)}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Ionicons name='play' size={20} color='white' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleDelete(process.id)}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Ionicons name='close' size={20} color='red' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='px-3 py-1.5 flex flex-col w-full'>
|
||||||
|
<View className='flex flex-row items-center w-full'>
|
||||||
|
{base64Image && (
|
||||||
|
<View className='w-14 aspect-[10/15] rounded-lg overflow-hidden mr-4'>
|
||||||
|
<Image
|
||||||
|
source={{
|
||||||
|
uri: `data:image/jpeg;base64,${base64Image}`,
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
contentFit='cover'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View className='shrink mb-1 flex-1'>
|
||||||
|
<Text className='text-xs opacity-50'>{process.item.Type}</Text>
|
||||||
|
<Text className='font-semibold shrink'>{process.item.Name}</Text>
|
||||||
|
<Text className='text-xs opacity-50'>
|
||||||
|
{process.item.ProductionYear}
|
||||||
|
</Text>
|
||||||
|
<View className='flex flex-row items-center space-x-2 mt-1 text-purple-600'>
|
||||||
|
{sanitizedProgress === 0 ? (
|
||||||
|
<ActivityIndicator size={"small"} color={"white"} />
|
||||||
|
) : (
|
||||||
|
<Text className='text-xs'>{sanitizedProgress.toFixed(0)}%</Text>
|
||||||
|
)}
|
||||||
|
{process.speed && process.speed > 0 && (
|
||||||
|
<Text className='text-xs'>
|
||||||
|
{bytesToMB(process.speed).toFixed(2)} MB/s
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{eta(process) && (
|
||||||
|
<Text className='text-xs'>
|
||||||
|
{t("home.downloads.eta", { eta: eta(process) })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='flex flex-row items-center space-x-2 mt-1 text-purple-600'>
|
||||||
|
<Text className='text-xs capitalize'>{process.status}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{process.status === "completed" && (
|
||||||
|
<View className='flex flex-row mt-4 space-x-4'>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
startDownload(process);
|
||||||
|
}}
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
|
Download now
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -26,7 +26,7 @@ import { itemRouter } from "../common/TouchableItemRouter";
|
|||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => {
|
export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
|
|
||||||
const ref = React.useRef<ICarouselInstance>(null);
|
const ref = React.useRef<ICarouselInstance>(null);
|
||||||
const progress = useSharedValue<number>(0);
|
const progress = useSharedValue<number>(0);
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ import {
|
|||||||
useJellyseerr,
|
useJellyseerr,
|
||||||
} from "@/hooks/useJellyseerr";
|
} from "@/hooks/useJellyseerr";
|
||||||
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
||||||
import type {
|
|
||||||
MovieResult,
|
|
||||||
TvResult,
|
|
||||||
} from "@/utils/jellyseerr/server/models/Search";
|
|
||||||
|
|
||||||
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
|
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
|
||||||
slide,
|
slide,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
|
||||||
|
|
||||||
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
||||||
queryKey: ["jellyseerr", "discover", slide.id],
|
queryKey: ["jellyseerr", "discover", slide.id],
|
||||||
@@ -69,7 +65,9 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
|
|||||||
uniqBy(
|
uniqBy(
|
||||||
data?.pages
|
data?.pages
|
||||||
?.filter((p) => p?.results.length)
|
?.filter((p) => p?.results.length)
|
||||||
.flatMap((p) => p?.results),
|
.flatMap((p) =>
|
||||||
|
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)),
|
||||||
|
),
|
||||||
"id",
|
"id",
|
||||||
),
|
),
|
||||||
[data],
|
[data],
|
||||||
@@ -86,12 +84,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
|
|||||||
onEndReached={() => {
|
onEndReached={() => {
|
||||||
if (hasNextPage) fetchNextPage();
|
if (hasNextPage) fetchNextPage();
|
||||||
}}
|
}}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => <JellyseerrPoster item={item} key={item?.id} />}
|
||||||
<JellyseerrPoster
|
|
||||||
item={item as MovieResult | TvResult}
|
|
||||||
key={item?.id}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const icons: Record<CollectionType, IconName> = {
|
|||||||
export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useAtom } from "jotai";
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { View, type ViewProps } from "react-native";
|
import { View, type ViewProps } from "react-native";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { InfiniteHorizontalScroll } from "../common/InfiniteHorrizontalScroll";
|
import { InfiniteHorizontalScroll } from "../common/InfiniteHorizontalScroll";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||||
import { ItemCardText } from "../ItemCardText";
|
import { ItemCardText } from "../ItemCardText";
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
|
|||||||
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
||||||
import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker";
|
import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker";
|
||||||
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
||||||
|
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
||||||
import type {
|
import type {
|
||||||
MovieResult,
|
MovieResult,
|
||||||
TvResult,
|
TvResult,
|
||||||
@@ -27,7 +28,7 @@ import type {
|
|||||||
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
|
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
item?: MovieResult | TvResult | MovieDetails | TvDetails;
|
item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast;
|
||||||
horizontal?: boolean;
|
horizontal?: boolean;
|
||||||
showDownloadInfo?: boolean;
|
showDownloadInfo?: boolean;
|
||||||
mediaRequest?: MediaRequest;
|
mediaRequest?: MediaRequest;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
import { HorizontalScroll } from "../common/HorrizontalScroll";
|
import { HorizontalScroll } from "../common/HorizontalScroll";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { itemRouter } from "../common/TouchableItemRouter";
|
import { itemRouter } from "../common/TouchableItemRouter";
|
||||||
import Poster from "../posters/Poster";
|
import Poster from "../posters/Poster";
|
||||||
@@ -27,16 +27,18 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
|
|||||||
const from = segments[2];
|
const from = segments[2];
|
||||||
|
|
||||||
const destinctPeople = useMemo(() => {
|
const destinctPeople = useMemo(() => {
|
||||||
const people: BaseItemPerson[] = [];
|
const people: Record<string, BaseItemPerson> = {};
|
||||||
item?.People?.forEach((person) => {
|
item?.People?.forEach((person) => {
|
||||||
const existingPerson = people.find((p) => p.Id === person.Id);
|
if (!person.Id) return;
|
||||||
|
|
||||||
|
const existingPerson = people[person.Id];
|
||||||
if (existingPerson) {
|
if (existingPerson) {
|
||||||
existingPerson.Role = `${existingPerson.Role}, ${person.Role}`;
|
existingPerson.Role = `${existingPerson.Role}, ${person.Role}`;
|
||||||
} else {
|
} else {
|
||||||
people.push(person);
|
people[person.Id] = person;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return people;
|
return Object.values(people);
|
||||||
}, [item?.People]);
|
}, [item?.People]);
|
||||||
|
|
||||||
if (!from) return null;
|
if (!from) return null;
|
||||||
@@ -54,7 +56,13 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
|
|||||||
renderItem={(i) => (
|
renderItem={(i) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const url = itemRouter(i, from);
|
const url = itemRouter(
|
||||||
|
{
|
||||||
|
Id: i.Id,
|
||||||
|
Type: "Person",
|
||||||
|
},
|
||||||
|
from,
|
||||||
|
);
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.push(url);
|
router.push(url);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
||||||
import { HorizontalScroll } from "../common/HorrizontalScroll";
|
import { HorizontalScroll } from "../common/HorizontalScroll";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import Poster from "../posters/Poster";
|
import Poster from "../posters/Poster";
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { orderBy } from "lodash";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Alert, TouchableOpacity, View } from "react-native";
|
import { Alert, TouchableOpacity, View } from "react-native";
|
||||||
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
|
import { HorizontalScroll } from "@/components/common/HorizontalScroll";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { Tags } from "@/components/GenreTags";
|
import { Tags } from "@/components/GenreTags";
|
||||||
import { dateOpts } from "@/components/jellyseerr/DetailFacts";
|
import { dateOpts } from "@/components/jellyseerr/DetailFacts";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
|||||||
import {
|
import {
|
||||||
HorizontalScroll,
|
HorizontalScroll,
|
||||||
type HorizontalScrollRef,
|
type HorizontalScrollRef,
|
||||||
} from "../common/HorrizontalScroll";
|
} from "../common/HorizontalScroll";
|
||||||
import { ItemCardText } from "../ItemCardText";
|
import { ItemCardText } from "../ItemCardText";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
@@ -42,11 +42,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
return item?.SeasonId;
|
return item?.SeasonId;
|
||||||
}, [item]);
|
}, [item]);
|
||||||
|
|
||||||
const {
|
const { data: episodes, isPending } = useQuery({
|
||||||
data: episodes,
|
|
||||||
isLoading,
|
|
||||||
isFetching,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["episodes", seasonId, isOffline],
|
queryKey: ["episodes", seasonId, isOffline],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (isOffline) {
|
if (isOffline) {
|
||||||
@@ -132,7 +128,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
data={episodes}
|
data={episodes}
|
||||||
extraData={item}
|
extraData={item}
|
||||||
loading={loading || isLoading || isFetching}
|
loading={loading || isPending}
|
||||||
renderItem={(_item, _idx) => (
|
renderItem={(_item, _idx) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={_item.Id}
|
key={_item.Id}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
return season.Id!;
|
return season.Id!;
|
||||||
}, [seasons, seasonIndex]);
|
}, [seasons, seasonIndex]);
|
||||||
|
|
||||||
const { data: episodes, isFetching } = useQuery({
|
const { data: episodes, isPending } = useQuery({
|
||||||
queryKey: ["episodes", item.Id, selectedSeasonId],
|
queryKey: ["episodes", item.Id, selectedSeasonId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!api || !user?.Id || !item.Id || !selectedSeasonId) {
|
if (!api || !user?.Id || !item.Id || !selectedSeasonId) {
|
||||||
@@ -165,7 +165,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<View className='px-4 flex flex-col mt-4'>
|
<View className='px-4 flex flex-col mt-4'>
|
||||||
{isFetching ? (
|
{isPending ? (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
minHeight: 144 * nrOfEpisodes,
|
minHeight: 144 * nrOfEpisodes,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface Props extends ViewProps {}
|
|||||||
|
|
||||||
export const AppLanguageSelector: React.FC<Props> = () => {
|
export const AppLanguageSelector: React.FC<Props> = () => {
|
||||||
const isTv = Platform.isTV;
|
const isTv = Platform.isTV;
|
||||||
const [settings, updateSettings] = useSettings(null);
|
const { settings, updateSettings } = useSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (isTv) return null;
|
if (isTv) return null;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
const isTv = Platform.isTV;
|
const isTv = Platform.isTV;
|
||||||
|
|
||||||
const media = useMedia();
|
const media = useMedia();
|
||||||
const [_, __, pluginSettings] = useSettings(null);
|
const { pluginSettings } = useSettings();
|
||||||
const { settings, updateSettings } = media;
|
const { settings, updateSettings } = media;
|
||||||
const cultures = media.cultures;
|
const cultures = media.cultures;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ListGroup } from "../list/ListGroup";
|
|||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
export const ChromecastSettings: React.FC = ({ ...props }) => {
|
export const ChromecastSettings: React.FC = ({ ...props }) => {
|
||||||
const [settings, updateSettings] = useSettings(null);
|
const { settings, updateSettings } = useSettings();
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup title={"Chromecast"}>
|
<ListGroup title={"Chromecast"}>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ListGroup } from "../list/ListGroup";
|
|||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
export const Dashboard = () => {
|
export const Dashboard = () => {
|
||||||
const [settings, _updateSettings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const { sessions = [] } = useSessions({} as useSessionsProps);
|
const { sessions = [] } = useSessions({} as useSessionsProps);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ListGroup } from "../list/ListGroup";
|
|||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
export default function DownloadSettings({ ...props }) {
|
export default function DownloadSettings({ ...props }) {
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings(null);
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const allDisabled = useMemo(
|
const allDisabled = useMemo(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface Props extends ViewProps {}
|
|||||||
export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
export const GestureControls: React.FC<Props> = ({ ...props }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings(null);
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
|
|
||||||
const disabled = useMemo(
|
const disabled = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -64,13 +64,7 @@ export const HomeIndex = () => {
|
|||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [
|
const { settings, refreshStreamyfinPluginSettings } = useSettings();
|
||||||
settings,
|
|
||||||
_updateSettings,
|
|
||||||
_pluginSettings,
|
|
||||||
_setPluginSettings,
|
|
||||||
refreshStreamyfinPluginSettings,
|
|
||||||
] = useSettings(null);
|
|
||||||
|
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const JellyseerrSettings = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [settings, updateSettings, _pluginSettings] = useSettings(null);
|
const { settings, updateSettings } = useSettings();
|
||||||
|
|
||||||
const [jellyseerrPassword, setJellyseerrPassword] = useState<
|
const [jellyseerrPassword, setJellyseerrPassword] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const useMedia = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MediaProvider = ({ children }: { children: ReactNode }) => {
|
export const MediaProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [settings, updateSettings] = useSettings(null);
|
const { settings, updateSettings } = useSettings();
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface Props extends ViewProps {}
|
|||||||
export const MediaToggles: React.FC<Props> = ({ ...props }) => {
|
export const MediaToggles: React.FC<Props> = ({ ...props }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings(null);
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
|
|
||||||
const disabled = useMemo(
|
const disabled = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { ListItem } from "../list/ListItem";
|
|||||||
|
|
||||||
export const OtherSettings: React.FC = () => {
|
export const OtherSettings: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings(null);
|
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ListGroup } from "../list/ListGroup";
|
|||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
export const PluginSettings = () => {
|
export const PluginSettings = () => {
|
||||||
const [settings, _updateSettings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
const isTv = Platform.isTV;
|
const isTv = Platform.isTV;
|
||||||
|
|
||||||
const media = useMedia();
|
const media = useMedia();
|
||||||
const [_, __, pluginSettings] = useSettings(null);
|
const { pluginSettings } = useSettings();
|
||||||
const { settings, updateSettings } = media;
|
const { settings, updateSettings } = media;
|
||||||
const cultures = media.cultures;
|
const cultures = media.cultures;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const commonScreenOptions: ICommonScreenOptions = {
|
|||||||
headerLeft: () => <HeaderBackButton />,
|
headerLeft: () => <HeaderBackButton />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const routes = ["actors/[actorId]", "items/page", "series/[id]"];
|
const routes = ["persons/[personId]", "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]));
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
|||||||
trickplayInfo,
|
trickplayInfo,
|
||||||
time,
|
time,
|
||||||
}) => {
|
}) => {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const CenterControls: FC<CenterControlsProps> = ({
|
|||||||
handleSkipBackward,
|
handleSkipBackward,
|
||||||
handleSkipForward,
|
handleSkipForward,
|
||||||
}) => {
|
}) => {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface ContinueWatchingOverlayProps {
|
|||||||
const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
|
const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
|
||||||
goToNextItem,
|
goToNextItem,
|
||||||
}) => {
|
}) => {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return settings.autoPlayEpisodeCount >=
|
return settings.autoPlayEpisodeCount >=
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export const Controls: FC<Props> = ({
|
|||||||
api = null,
|
api = null,
|
||||||
downloadedFiles = undefined,
|
downloadedFiles = undefined,
|
||||||
}) => {
|
}) => {
|
||||||
const [settings, updateSettings] = useSettings(api);
|
const { settings, updateSettings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
|
|||||||
import {
|
import {
|
||||||
HorizontalScroll,
|
HorizontalScroll,
|
||||||
type HorizontalScrollRef,
|
type HorizontalScrollRef,
|
||||||
} from "@/components/common/HorrizontalScroll";
|
} from "@/components/common/HorizontalScroll";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const GestureOverlay = ({
|
|||||||
onSkipForward,
|
onSkipForward,
|
||||||
onSkipBackward,
|
onSkipBackward,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const lightHaptic = useHaptic("light");
|
const lightHaptic = useHaptic("light");
|
||||||
|
|
||||||
const [feedback, setFeedback] = useState<FeedbackState>({
|
const [feedback, setFeedback] = useState<FeedbackState>({
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
|||||||
setVideoAspectRatio,
|
setVideoAspectRatio,
|
||||||
setVideoScaleFactor,
|
setVideoScaleFactor,
|
||||||
}) => {
|
}) => {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { width: screenWidth } = useWindowDimensions();
|
const { width: screenWidth } = useWindowDimensions();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function useVideoNavigation({
|
|||||||
seek,
|
seek,
|
||||||
play,
|
play,
|
||||||
}: UseVideoNavigationProps) {
|
}: UseVideoNavigationProps) {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
const wasPlayingRef = useRef(false);
|
const wasPlayingRef = useRef(false);
|
||||||
|
|
||||||
|
|||||||
19
eas.json
19
eas.json
@@ -26,13 +26,6 @@
|
|||||||
"EXPO_PUBLIC_WRITE_DEBUG": "1"
|
"EXPO_PUBLIC_WRITE_DEBUG": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preview": {
|
|
||||||
"environment": "development",
|
|
||||||
"distribution": "internal",
|
|
||||||
"env": {
|
|
||||||
"EXPO_PUBLIC_WRITE_DEBUG": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"development-simulator": {
|
"development-simulator": {
|
||||||
"environment": "development",
|
"environment": "development",
|
||||||
"developmentClient": true,
|
"developmentClient": true,
|
||||||
@@ -44,16 +37,22 @@
|
|||||||
"EXPO_PUBLIC_WRITE_DEBUG": "1"
|
"EXPO_PUBLIC_WRITE_DEBUG": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"preview": {
|
||||||
|
"distribution": "internal",
|
||||||
|
"env": {
|
||||||
|
"EXPO_PUBLIC_WRITE_DEBUG": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"environment": "production",
|
"environment": "production",
|
||||||
"channel": "0.35.0",
|
"channel": "0.35.1",
|
||||||
"android": {
|
"android": {
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production-apk": {
|
"production-apk": {
|
||||||
"environment": "production",
|
"environment": "production",
|
||||||
"channel": "0.35.0",
|
"channel": "0.35.1",
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
@@ -61,7 +60,7 @@
|
|||||||
},
|
},
|
||||||
"production-apk-tv": {
|
"production-apk-tv": {
|
||||||
"environment": "production",
|
"environment": "production",
|
||||||
"channel": "0.35.0",
|
"channel": "0.35.1",
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type HapticFeedbackType =
|
|||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
|
export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const isTv = Platform.isTV;
|
const isTv = Platform.isTV;
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
isTv ||
|
isTv ||
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { defaultValues, Settings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
|
import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
|
||||||
import {
|
import {
|
||||||
IssueStatus,
|
IssueStatus,
|
||||||
@@ -40,6 +40,7 @@ import type { UserResultsResponse } from "@/utils/jellyseerr/server/interfaces/a
|
|||||||
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
||||||
import type {
|
import type {
|
||||||
CombinedCredit,
|
CombinedCredit,
|
||||||
|
PersonCreditCast,
|
||||||
PersonDetails,
|
PersonDetails,
|
||||||
} from "@/utils/jellyseerr/server/models/Person";
|
} from "@/utils/jellyseerr/server/models/Person";
|
||||||
import type {
|
import type {
|
||||||
@@ -416,10 +417,8 @@ export class JellyseerrApi {
|
|||||||
|
|
||||||
const jellyseerrUserAtom = atom(storage.get<JellyseerrUser>(JELLYSEERR_USER));
|
const jellyseerrUserAtom = atom(storage.get<JellyseerrUser>(JELLYSEERR_USER));
|
||||||
|
|
||||||
export const useJellyseerr = (
|
export const useJellyseerr = () => {
|
||||||
settings: Settings = defaultValues,
|
const { settings, updateSettings } = useSettings();
|
||||||
updateSettings: (update: Partial<Settings>) => void = () => {},
|
|
||||||
) => {
|
|
||||||
const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom);
|
const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -468,49 +467,47 @@ export const useJellyseerr = (
|
|||||||
[jellyseerrApi],
|
[jellyseerrApi],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isJellyseerrResult = (
|
const isJellyseerrMovieOrTvResult = (
|
||||||
items: any | null | undefined,
|
items: any | null | undefined,
|
||||||
): items is Results => {
|
): items is MovieResult | TvResult => {
|
||||||
return (
|
return (
|
||||||
items &&
|
items &&
|
||||||
Object.hasOwn(items, "mediaType") &&
|
Object.hasOwn(items, "mediaType") &&
|
||||||
Object.values(MediaType).includes(items.mediaType as MediaType)
|
(items.mediaType === MediaType.MOVIE || items.mediaType === MediaType.TV)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTitle = (
|
const getTitle = (
|
||||||
item?: TvResult | TvDetails | MovieResult | MovieDetails,
|
item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
|
||||||
) => {
|
) => {
|
||||||
return isJellyseerrResult(item)
|
return isJellyseerrMovieOrTvResult(item)
|
||||||
? item.mediaType === MediaType.MOVIE
|
? item.mediaType === MediaType.MOVIE
|
||||||
? item?.title
|
? item?.title
|
||||||
: item?.name
|
: item?.name
|
||||||
: item?.mediaInfo.mediaType === MediaType.MOVIE
|
: item?.mediaInfo?.mediaType === MediaType.MOVIE
|
||||||
? (item as MovieDetails)?.title
|
? (item as MovieDetails)?.title
|
||||||
: (item as TvDetails)?.name;
|
: (item as TvDetails)?.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getYear = (
|
const getYear = (
|
||||||
item?: TvResult | TvDetails | MovieResult | MovieDetails,
|
item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
|
||||||
) => {
|
) => {
|
||||||
return new Date(
|
return new Date(
|
||||||
(isJellyseerrResult(item)
|
(isJellyseerrMovieOrTvResult(item)
|
||||||
? item.mediaType === MediaType.MOVIE
|
? item.mediaType === MediaType.MOVIE
|
||||||
? item?.releaseDate
|
? item?.releaseDate
|
||||||
: item?.firstAirDate
|
: item?.firstAirDate
|
||||||
: item?.mediaInfo.mediaType === MediaType.MOVIE
|
: item?.mediaInfo?.mediaType === MediaType.MOVIE
|
||||||
? (item as MovieDetails)?.releaseDate
|
? (item as MovieDetails)?.releaseDate
|
||||||
: (item as TvDetails)?.firstAirDate) || "",
|
: (item as TvDetails)?.firstAirDate) || "",
|
||||||
)?.getFullYear?.();
|
)?.getFullYear?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMediaType = (
|
const getMediaType = (
|
||||||
item?: TvResult | TvDetails | MovieResult | MovieDetails,
|
item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
|
||||||
): MediaType => {
|
): MediaType => {
|
||||||
return isJellyseerrResult(item)
|
return isJellyseerrMovieOrTvResult(item)
|
||||||
? item.mediaType === "movie"
|
? (item.mediaType as MediaType)
|
||||||
? MediaType.MOVIE
|
|
||||||
: MediaType.TV
|
|
||||||
: item?.mediaInfo?.mediaType;
|
: item?.mediaInfo?.mediaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -528,7 +525,7 @@ export const useJellyseerr = (
|
|||||||
jellyseerrUser,
|
jellyseerrUser,
|
||||||
setJellyseerrUser,
|
setJellyseerrUser,
|
||||||
clearAllJellyseerData,
|
clearAllJellyseerData,
|
||||||
isJellyseerrResult,
|
isJellyseerrMovieOrTvResult,
|
||||||
getTitle,
|
getTitle,
|
||||||
getYear,
|
getYear,
|
||||||
getMediaType,
|
getMediaType,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import * as Application from "expo-application";
|
import * as Application from "expo-application";
|
||||||
import * as FileSystem from "expo-file-system";
|
import * as FileSystem from "expo-file-system";
|
||||||
|
import * as Notifications from "expo-notifications";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
@@ -87,9 +88,72 @@ function useDownloadProvider() {
|
|||||||
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
||||||
const { saveImage } = useImageStorage();
|
const { saveImage } = useImageStorage();
|
||||||
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
const successHapticFeedback = useHaptic("success");
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
|
||||||
|
// Generate notification content based on item type
|
||||||
|
const getNotificationContent = useCallback(
|
||||||
|
(item: BaseItemDto, isSuccess: boolean) => {
|
||||||
|
if (item.Type === "Episode") {
|
||||||
|
const season = item.ParentIndexNumber
|
||||||
|
? String(item.ParentIndexNumber).padStart(2, "0")
|
||||||
|
: "??";
|
||||||
|
const episode = item.IndexNumber
|
||||||
|
? String(item.IndexNumber).padStart(2, "0")
|
||||||
|
: "??";
|
||||||
|
const subtitle = `${item.Name} - [S${season}E${episode}] (${item.SeriesName})`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: isSuccess ? "Download complete" : "Download failed",
|
||||||
|
body: subtitle,
|
||||||
|
};
|
||||||
|
} else if (item.Type === "Movie") {
|
||||||
|
const year = item.ProductionYear ? ` (${item.ProductionYear})` : "";
|
||||||
|
const subtitle = `${item.Name}${year}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: isSuccess ? "Download complete" : "Download failed",
|
||||||
|
body: subtitle,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Fallback for other types
|
||||||
|
return {
|
||||||
|
title: isSuccess
|
||||||
|
? t("home.downloads.toasts.download_completed_for_item", {
|
||||||
|
item: item.Name,
|
||||||
|
})
|
||||||
|
: t("home.downloads.toasts.download_failed_for_item", {
|
||||||
|
item: item.Name,
|
||||||
|
}),
|
||||||
|
body: item.Name || "Unknown item",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send local notification for download events
|
||||||
|
const sendDownloadNotification = useCallback(
|
||||||
|
async (title: string, body: string, data?: Record<string, any>) => {
|
||||||
|
if (Platform.isTV) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data,
|
||||||
|
...(Platform.OS === "android" && { channelId: "downloads" }),
|
||||||
|
},
|
||||||
|
trigger: null, // Show immediately
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send notification:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
/// Cant use the background downloader callback. As its not triggered if size is unknown.
|
/// Cant use the background downloader callback. As its not triggered if size is unknown.
|
||||||
const updateProgress = async () => {
|
const updateProgress = async () => {
|
||||||
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
||||||
@@ -418,6 +482,21 @@ function useDownloadProvider() {
|
|||||||
}
|
}
|
||||||
await saveDownloadsDatabase(db);
|
await saveDownloadsDatabase(db);
|
||||||
|
|
||||||
|
// Send native notification for successful download
|
||||||
|
const successNotification = getNotificationContent(
|
||||||
|
process.item,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await sendDownloadNotification(
|
||||||
|
successNotification.title,
|
||||||
|
successNotification.body,
|
||||||
|
{
|
||||||
|
itemId: process.item.Id,
|
||||||
|
itemName: process.item.Name,
|
||||||
|
type: "download_completed",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
t("home.downloads.toasts.download_completed_for_item", {
|
t("home.downloads.toasts.download_completed_for_item", {
|
||||||
item: process.item.Name,
|
item: process.item.Name,
|
||||||
@@ -425,8 +504,25 @@ function useDownloadProvider() {
|
|||||||
);
|
);
|
||||||
removeProcess(process.id);
|
removeProcess(process.id);
|
||||||
})
|
})
|
||||||
.error((error: any) => {
|
.error(async (error: any) => {
|
||||||
console.error("Download error:", error);
|
console.error("Download error:", error);
|
||||||
|
|
||||||
|
// Send native notification for failed download
|
||||||
|
const failureNotification = getNotificationContent(
|
||||||
|
process.item,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
await sendDownloadNotification(
|
||||||
|
failureNotification.title,
|
||||||
|
failureNotification.body,
|
||||||
|
{
|
||||||
|
itemId: process.item.Id,
|
||||||
|
itemName: process.item.Name,
|
||||||
|
type: "download_failed",
|
||||||
|
error: error?.message || "Unknown error",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
toast.error(
|
toast.error(
|
||||||
t("home.downloads.toasts.download_failed_for_item", {
|
t("home.downloads.toasts.download_failed_for_item", {
|
||||||
item: process.item.Name,
|
item: process.item.Name,
|
||||||
@@ -435,7 +531,7 @@ function useDownloadProvider() {
|
|||||||
removeProcess(process.id);
|
removeProcess(process.id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[authHeader],
|
[authHeader, sendDownloadNotification, getNotificationContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const manageDownloadQueue = useCallback(() => {
|
const manageDownloadQueue = useCallback(() => {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
setJellyfin(
|
setJellyfin(
|
||||||
() =>
|
() =>
|
||||||
new Jellyfin({
|
new Jellyfin({
|
||||||
clientInfo: { name: "Streamyfin", version: "0.35.0" },
|
clientInfo: { name: "Streamyfin", version: "0.35.1" },
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
name: deviceName,
|
name: deviceName,
|
||||||
id,
|
id,
|
||||||
@@ -79,24 +79,15 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
const [user, setUser] = useAtom(userAtom);
|
const [user, setUser] = useAtom(userAtom);
|
||||||
const [isPolling, setIsPolling] = useState<boolean>(false);
|
const [isPolling, setIsPolling] = useState<boolean>(false);
|
||||||
const [secret, setSecret] = useState<string | null>(null);
|
const [secret, setSecret] = useState<string | null>(null);
|
||||||
const [
|
const { setPluginSettings, refreshStreamyfinPluginSettings } = useSettings();
|
||||||
_settings,
|
const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr();
|
||||||
_updateSettings,
|
|
||||||
_pluginSettings,
|
|
||||||
setPluginSettings,
|
|
||||||
refreshStreamyfinPluginSettings,
|
|
||||||
] = useSettings(api);
|
|
||||||
const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr(
|
|
||||||
_settings || {},
|
|
||||||
_updateSettings,
|
|
||||||
);
|
|
||||||
|
|
||||||
const headers = useMemo(() => {
|
const headers = useMemo(() => {
|
||||||
if (!deviceId) return {};
|
if (!deviceId) return {};
|
||||||
return {
|
return {
|
||||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||||
Platform.OS === "android" ? "Android" : "iOS"
|
Platform.OS === "android" ? "Android" : "iOS"
|
||||||
}, DeviceId="${deviceId}", Version="0.35.0"`,
|
}, DeviceId="${deviceId}", Version="0.35.1"`,
|
||||||
};
|
};
|
||||||
}, [deviceId]);
|
}, [deviceId]);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
Permission,
|
Permission,
|
||||||
} from "@/utils/jellyseerr/server/lib/permissions";
|
} from "@/utils/jellyseerr/server/lib/permissions";
|
||||||
|
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
||||||
import type {
|
import type {
|
||||||
MovieResult,
|
MovieResult,
|
||||||
TvResult,
|
TvResult,
|
||||||
@@ -18,7 +19,7 @@ import type { MovieDetails } from "../jellyseerr/server/models/Movie";
|
|||||||
import type { TvDetails } from "../jellyseerr/server/models/Tv";
|
import type { TvDetails } from "../jellyseerr/server/models/Tv";
|
||||||
|
|
||||||
export const useJellyseerrCanRequest = (
|
export const useJellyseerrCanRequest = (
|
||||||
item?: MovieResult | TvResult | MovieDetails | TvDetails,
|
item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast,
|
||||||
) => {
|
) => {
|
||||||
const { jellyseerrUser } = useJellyseerr();
|
const { jellyseerrUser } = useJellyseerr();
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const useJobProcessor = () => {
|
|||||||
const [queue, setQueue] = useAtom(queueAtom);
|
const [queue, setQueue] = useAtom(queueAtom);
|
||||||
const [running, setRunning] = useAtom(runningAtom);
|
const [running, setRunning] = useAtom(runningAtom);
|
||||||
const [processes] = useAtom<JobStatus[]>(processesAtom);
|
const [processes] = useAtom<JobStatus[]>(processesAtom);
|
||||||
const [settings] = useSettings(null);
|
const { settings } = useSettings();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { Api } from "@jellyfin/sdk";
|
|
||||||
import {
|
import {
|
||||||
type BaseItemKind,
|
type BaseItemKind,
|
||||||
type CultureDto,
|
type CultureDto,
|
||||||
@@ -7,11 +6,12 @@ import {
|
|||||||
type SortOrder,
|
type SortOrder,
|
||||||
SubtitlePlaybackMode,
|
SubtitlePlaybackMode,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom, useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
|
import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { writeInfoLog } from "@/utils/log";
|
import { writeInfoLog } from "@/utils/log";
|
||||||
import { storage } from "../mmkv";
|
import { storage } from "../mmkv";
|
||||||
|
|
||||||
@@ -278,7 +278,8 @@ export const pluginSettingsAtom = atom<PluginLockableSettings | undefined>(
|
|||||||
loadPluginSettings(),
|
loadPluginSettings(),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useSettings = (api: Api | null) => {
|
export const useSettings = () => {
|
||||||
|
const api = useAtomValue(apiAtom);
|
||||||
const [_settings, setSettings] = useAtom(settingsAtom);
|
const [_settings, setSettings] = useAtom(settingsAtom);
|
||||||
const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom);
|
const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom);
|
||||||
|
|
||||||
@@ -302,11 +303,11 @@ export const useSettings = (api: Api | null) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const settings = await api.getStreamyfinPluginConfig().then(
|
const settings = await api.getStreamyfinPluginConfig().then(
|
||||||
({ data }: any) => {
|
({ data }) => {
|
||||||
writeInfoLog("Got plugin settings", data?.settings);
|
writeInfoLog("Got plugin settings", data?.settings);
|
||||||
return data?.settings;
|
return data?.settings;
|
||||||
},
|
},
|
||||||
(_err: any) => undefined,
|
(_err) => undefined,
|
||||||
);
|
);
|
||||||
setPluginSettings(settings);
|
setPluginSettings(settings);
|
||||||
return settings;
|
return settings;
|
||||||
@@ -367,11 +368,11 @@ export const useSettings = (api: Api | null) => {
|
|||||||
};
|
};
|
||||||
}, [_settings, pluginSettings]);
|
}, [_settings, pluginSettings]);
|
||||||
|
|
||||||
return [
|
return {
|
||||||
settings,
|
settings,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
pluginSettings,
|
pluginSettings,
|
||||||
setPluginSettings,
|
setPluginSettings,
|
||||||
refreshStreamyfinPluginSettings,
|
refreshStreamyfinPluginSettings,
|
||||||
] as const;
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user