mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-19 07:46:24 +00:00
Compare commits
2 Commits
v0.26.1
...
hotfix/fil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
017bd4d074 | ||
|
|
8b3141dfc6 |
39
.github/workflows/main.yml
vendored
39
.github/workflows/main.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
name: Handle Stale Issues
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "30 1 * * *" # Runs at 1:30 UTC every day
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v9
|
|
||||||
with:
|
|
||||||
# Issue specific settings
|
|
||||||
days-before-issue-stale: 90
|
|
||||||
days-before-issue-close: 7
|
|
||||||
stale-issue-label: "stale"
|
|
||||||
stale-issue-message: |
|
|
||||||
This issue has been automatically marked as stale because it has had no activity in the last 30 days.
|
|
||||||
|
|
||||||
If this issue is still relevant, please leave a comment to keep it open.
|
|
||||||
Otherwise, it will be closed in 7 days if no further activity occurs.
|
|
||||||
|
|
||||||
Thank you for your contributions!
|
|
||||||
close-issue-message: |
|
|
||||||
This issue has been automatically closed because it has been inactive for 7 days since being marked as stale.
|
|
||||||
|
|
||||||
If you believe this issue is still relevant, please feel free to reopen it and add a comment explaining the current status.
|
|
||||||
|
|
||||||
# Pull request settings (disabled)
|
|
||||||
days-before-pr-stale: -1
|
|
||||||
days-before-pr-close: -1
|
|
||||||
|
|
||||||
# Other settings
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
operations-per-run: 100
|
|
||||||
exempt-issue-labels: "Roadmap v1,help needed,enhancement"
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -41,5 +41,4 @@ credentials.json
|
|||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
.ruby-lsp
|
.ruby-lsp
|
||||||
modules/hls-downloader/android/build
|
|
||||||
@@ -85,9 +85,9 @@ We welcome any help to make Streamyfin better. If you'd like to contribute, plea
|
|||||||
|
|
||||||
1. Use node `>20`
|
1. Use node `>20`
|
||||||
2. Install dependencies `bun i && bun run submodule-reload`
|
2. Install dependencies `bun i && bun run submodule-reload`
|
||||||
3. Make sure you have xcode and/or android studio installed. (follow the guides for expo: https://docs.expo.dev/workflow/android-studio-emulator/)
|
3. Make sure you have xcode and/or android studio installed.
|
||||||
4. run `npm run prebuild`
|
4. run `npm run prebuild`
|
||||||
5. Create an expo dev build by running `npm run ios` or `npm run android`. This will open a simulator on your computer and run the app.
|
5. Create an expo dev build by running `npm run ios` or `nom run android`. This will open a simulator on your computer and run the app.
|
||||||
|
|
||||||
For the TV version suffix the npm commands with `:tv`.
|
For the TV version suffix the npm commands with `:tv`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,498 @@
|
|||||||
import { SettingsIndex } from "@/components/settings/SettingsIndex";
|
import { Button } from "@/components/Button";
|
||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel";
|
||||||
|
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
|
||||||
|
import { Loader } from "@/components/Loader";
|
||||||
|
import { MediaListSection } from "@/components/medialists/MediaListSection";
|
||||||
|
import { Colors } from "@/constants/Colors";
|
||||||
|
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||||
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||||
|
import { Api } from "@jellyfin/sdk";
|
||||||
|
import {
|
||||||
|
BaseItemDto,
|
||||||
|
BaseItemKind,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import {
|
||||||
|
getItemsApi,
|
||||||
|
getSuggestionsApi,
|
||||||
|
getTvShowsApi,
|
||||||
|
getUserLibraryApi,
|
||||||
|
getUserViewsApi,
|
||||||
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import NetInfo from "@react-native-community/netinfo";
|
||||||
|
import { QueryFunction, useQuery } from "@tanstack/react-query";
|
||||||
|
import { useNavigation, useRouter } from "expo-router";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
RefreshControl,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import {
|
||||||
|
useSplashScreenLoading,
|
||||||
|
useSplashScreenVisible,
|
||||||
|
} from "@/providers/SplashScreenProvider";
|
||||||
|
|
||||||
export default function page() {
|
type ScrollingCollectionListSection = {
|
||||||
return <SettingsIndex />;
|
type: "ScrollingCollectionList";
|
||||||
|
title?: string;
|
||||||
|
queryKey: (string | undefined | null)[];
|
||||||
|
queryFn: QueryFunction<BaseItemDto[]>;
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
|
};
|
||||||
|
|
||||||
|
type MediaListSection = {
|
||||||
|
type: "MediaListSection";
|
||||||
|
queryKey: (string | undefined)[];
|
||||||
|
queryFn: QueryFunction<BaseItemDto>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Section = ScrollingCollectionListSection | MediaListSection;
|
||||||
|
|
||||||
|
export default function index() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const api = useAtomValue(apiAtom);
|
||||||
|
const user = useAtomValue(userAtom);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
|
pluginSettings,
|
||||||
|
setPluginSettings,
|
||||||
|
refreshStreamyfinPluginSettings,
|
||||||
|
] = useSettings();
|
||||||
|
|
||||||
|
const [isConnected, setIsConnected] = useState<boolean | null>(null);
|
||||||
|
const [loadingRetry, setLoadingRetry] = useState(false);
|
||||||
|
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
if (!Platform.isTV) {
|
||||||
|
const { downloadedFiles, cleanCacheDirectory } = useDownload();
|
||||||
|
useEffect(() => {
|
||||||
|
const hasDownloads = downloadedFiles && downloadedFiles.length > 0;
|
||||||
|
navigation.setOptions({
|
||||||
|
headerLeft: () => (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/(auth)/downloads");
|
||||||
|
}}
|
||||||
|
className="p-2"
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name="download"
|
||||||
|
color={hasDownloads ? Colors.primary : "white"}
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}, [downloadedFiles, navigation, router]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cleanCacheDirectory().catch((e) =>
|
||||||
|
console.error("Something went wrong cleaning cache directory")
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkConnection = useCallback(async () => {
|
||||||
|
setLoadingRetry(true);
|
||||||
|
const state = await NetInfo.fetch();
|
||||||
|
setIsConnected(state.isConnected);
|
||||||
|
setLoadingRetry(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = NetInfo.addEventListener((state) => {
|
||||||
|
if (state.isConnected == false || state.isInternetReachable === false)
|
||||||
|
setIsConnected(false);
|
||||||
|
else setIsConnected(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
NetInfo.fetch().then((state) => {
|
||||||
|
setIsConnected(state.isConnected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// cleanCacheDirectory().catch((e) =>
|
||||||
|
// console.error("Something went wrong cleaning cache directory")
|
||||||
|
// );
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isError: e1,
|
||||||
|
isLoading: l1,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["home", "userViews", user?.Id],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!api || !user?.Id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getUserViewsApi(api).getUserViews({
|
||||||
|
userId: user.Id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.Items || null;
|
||||||
|
},
|
||||||
|
enabled: !!api && !!user?.Id,
|
||||||
|
staleTime: 60 * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// show splash screen until query loaded
|
||||||
|
useSplashScreenLoading(l1);
|
||||||
|
const splashScreenVisible = useSplashScreenVisible();
|
||||||
|
|
||||||
|
const userViews = useMemo(
|
||||||
|
() => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)),
|
||||||
|
[data, settings?.hiddenLibraries]
|
||||||
|
);
|
||||||
|
|
||||||
|
const collections = useMemo(() => {
|
||||||
|
const allow = ["movies", "tvshows"];
|
||||||
|
return (
|
||||||
|
userViews?.filter(
|
||||||
|
(c) => c.CollectionType && allow.includes(c.CollectionType)
|
||||||
|
) || []
|
||||||
|
);
|
||||||
|
}, [userViews]);
|
||||||
|
|
||||||
|
const invalidateCache = useInvalidatePlaybackProgressCache();
|
||||||
|
|
||||||
|
const refetch = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await refreshStreamyfinPluginSettings();
|
||||||
|
await invalidateCache();
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createCollectionConfig = useCallback(
|
||||||
|
(
|
||||||
|
title: string,
|
||||||
|
queryKey: string[],
|
||||||
|
includeItemTypes: BaseItemKind[],
|
||||||
|
parentId: string | undefined
|
||||||
|
): ScrollingCollectionListSection => ({
|
||||||
|
title,
|
||||||
|
queryKey,
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!api) return [];
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
await getUserLibraryApi(api).getLatestMedia({
|
||||||
|
userId: user?.Id,
|
||||||
|
limit: 20,
|
||||||
|
fields: ["PrimaryImageAspectRatio", "Path"],
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||||
|
includeItemTypes,
|
||||||
|
parentId,
|
||||||
|
})
|
||||||
|
).data || []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
type: "ScrollingCollectionList",
|
||||||
|
}),
|
||||||
|
[api, user?.Id]
|
||||||
|
);
|
||||||
|
|
||||||
|
let sections: Section[] = [];
|
||||||
|
if (!settings?.home || !settings?.home?.sections) {
|
||||||
|
sections = useMemo(() => {
|
||||||
|
if (!api || !user?.Id) return [];
|
||||||
|
|
||||||
|
const latestMediaViews = collections.map((c) => {
|
||||||
|
const includeItemTypes: BaseItemKind[] =
|
||||||
|
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
|
||||||
|
const title = t("home.recently_added_in", { libraryName: c.Name });
|
||||||
|
const queryKey = [
|
||||||
|
"home",
|
||||||
|
"recentlyAddedIn" + c.CollectionType,
|
||||||
|
user?.Id!,
|
||||||
|
c.Id!,
|
||||||
|
];
|
||||||
|
return createCollectionConfig(
|
||||||
|
title || "",
|
||||||
|
queryKey,
|
||||||
|
includeItemTypes,
|
||||||
|
c.Id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ss: Section[] = [
|
||||||
|
{
|
||||||
|
title: t("home.continue_watching"),
|
||||||
|
queryKey: ["home", "resumeItems"],
|
||||||
|
queryFn: async () =>
|
||||||
|
(
|
||||||
|
await getItemsApi(api).getResumeItems({
|
||||||
|
userId: user.Id,
|
||||||
|
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||||
|
includeItemTypes: ["Movie", "Series", "Episode"],
|
||||||
|
})
|
||||||
|
).data.Items || [],
|
||||||
|
type: "ScrollingCollectionList",
|
||||||
|
orientation: "horizontal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("home.next_up"),
|
||||||
|
queryKey: ["home", "nextUp-all"],
|
||||||
|
queryFn: async () =>
|
||||||
|
(
|
||||||
|
await getTvShowsApi(api).getNextUp({
|
||||||
|
userId: user?.Id,
|
||||||
|
fields: ["MediaSourceCount"],
|
||||||
|
limit: 20,
|
||||||
|
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||||
|
enableResumable: false,
|
||||||
|
})
|
||||||
|
).data.Items || [],
|
||||||
|
type: "ScrollingCollectionList",
|
||||||
|
orientation: "horizontal",
|
||||||
|
},
|
||||||
|
...latestMediaViews,
|
||||||
|
// ...(mediaListCollections?.map(
|
||||||
|
// (ml) =>
|
||||||
|
// ({
|
||||||
|
// title: ml.Name,
|
||||||
|
// queryKey: ["home", "mediaList", ml.Id!],
|
||||||
|
// queryFn: async () => ml,
|
||||||
|
// type: "MediaListSection",
|
||||||
|
// orientation: "vertical",
|
||||||
|
// } as Section)
|
||||||
|
// ) || []),
|
||||||
|
{
|
||||||
|
title: t("home.suggested_movies"),
|
||||||
|
queryKey: ["home", "suggestedMovies", user?.Id],
|
||||||
|
queryFn: async () =>
|
||||||
|
(
|
||||||
|
await getSuggestionsApi(api).getSuggestions({
|
||||||
|
userId: user?.Id,
|
||||||
|
limit: 10,
|
||||||
|
mediaType: ["Video"],
|
||||||
|
type: ["Movie"],
|
||||||
|
})
|
||||||
|
).data.Items || [],
|
||||||
|
type: "ScrollingCollectionList",
|
||||||
|
orientation: "vertical",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("home.suggested_episodes"),
|
||||||
|
queryKey: ["home", "suggestedEpisodes", user?.Id],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const suggestions = await getSuggestions(api, user.Id);
|
||||||
|
const nextUpPromises = suggestions.map((series) =>
|
||||||
|
getNextUp(api, user.Id, series.Id)
|
||||||
|
);
|
||||||
|
const nextUpResults = await Promise.all(nextUpPromises);
|
||||||
|
|
||||||
|
return nextUpResults.filter((item) => item !== null) || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: "ScrollingCollectionList",
|
||||||
|
orientation: "horizontal",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return ss;
|
||||||
|
}, [api, user?.Id, collections]);
|
||||||
|
} else {
|
||||||
|
sections = useMemo(() => {
|
||||||
|
if (!api || !user?.Id) return [];
|
||||||
|
const ss: Section[] = [];
|
||||||
|
|
||||||
|
for (const key in settings.home?.sections) {
|
||||||
|
// @ts-expect-error
|
||||||
|
const section = settings.home?.sections[key];
|
||||||
|
const id = section.title || key;
|
||||||
|
ss.push({
|
||||||
|
title: id,
|
||||||
|
queryKey: ["home", id],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (section.items) {
|
||||||
|
const response = await getItemsApi(api).getItems({
|
||||||
|
userId: user?.Id,
|
||||||
|
limit: section.items?.limit || 25,
|
||||||
|
recursive: true,
|
||||||
|
includeItemTypes: section.items?.includeItemTypes,
|
||||||
|
sortBy: section.items?.sortBy,
|
||||||
|
sortOrder: section.items?.sortOrder,
|
||||||
|
filters: section.items?.filters,
|
||||||
|
parentId: section.items?.parentId,
|
||||||
|
});
|
||||||
|
return response.data.Items || [];
|
||||||
|
} else if (section.nextUp) {
|
||||||
|
const response = await getTvShowsApi(api).getNextUp({
|
||||||
|
userId: user?.Id,
|
||||||
|
fields: ["MediaSourceCount"],
|
||||||
|
limit: section.items?.limit || 25,
|
||||||
|
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||||
|
enableResumable: section.items?.enableResumable || false,
|
||||||
|
enableRewatching: section.items?.enableRewatching || false,
|
||||||
|
});
|
||||||
|
return response.data.Items || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
type: "ScrollingCollectionList",
|
||||||
|
orientation: section?.orientation || "vertical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ss;
|
||||||
|
}, [api, user?.Id, settings.home?.sections]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConnected === false) {
|
||||||
|
return (
|
||||||
|
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
|
||||||
|
<Text className="text-3xl font-bold mb-2">{t("home.no_internet")}</Text>
|
||||||
|
<Text className="text-center opacity-70">
|
||||||
|
{t("home.no_internet_message")}
|
||||||
|
</Text>
|
||||||
|
<View className="mt-4">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onPress={() => router.push("/(auth)/downloads")}
|
||||||
|
justify="center"
|
||||||
|
iconRight={
|
||||||
|
<Ionicons name="arrow-forward" size={20} color="white" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("home.go_to_downloads")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="black"
|
||||||
|
onPress={() => {
|
||||||
|
checkConnection();
|
||||||
|
}}
|
||||||
|
justify="center"
|
||||||
|
className="mt-2"
|
||||||
|
iconRight={
|
||||||
|
loadingRetry ? null : (
|
||||||
|
<Ionicons name="refresh" size={20} color="white" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loadingRetry ? (
|
||||||
|
<ActivityIndicator size={"small"} color={"white"} />
|
||||||
|
) : (
|
||||||
|
"Retry"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e1)
|
||||||
|
return (
|
||||||
|
<View className="flex flex-col items-center justify-center h-full -mt-6">
|
||||||
|
<Text className="text-3xl font-bold mb-2">{t("home.oops")}</Text>
|
||||||
|
<Text className="text-center opacity-70">
|
||||||
|
{t("home.error_message")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// this spinner should only show up, when user navigates here
|
||||||
|
// on launch the splash screen is used for loading
|
||||||
|
if (l1 && !splashScreenVisible)
|
||||||
|
return (
|
||||||
|
<View className="justify-center items-center h-full">
|
||||||
|
<Loader />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
nestedScrollEnabled
|
||||||
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={loading} onRefresh={refetch} />
|
||||||
|
}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
paddingBottom: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="flex flex-col space-y-4">
|
||||||
|
<LargeMovieCarousel />
|
||||||
|
|
||||||
|
{sections.map((section, index) => {
|
||||||
|
if (section.type === "ScrollingCollectionList") {
|
||||||
|
return (
|
||||||
|
<ScrollingCollectionList
|
||||||
|
key={index}
|
||||||
|
title={section.title}
|
||||||
|
queryKey={section.queryKey}
|
||||||
|
queryFn={section.queryFn}
|
||||||
|
orientation={section.orientation}
|
||||||
|
hideIfEmpty
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (section.type === "MediaListSection") {
|
||||||
|
return (
|
||||||
|
<MediaListSection
|
||||||
|
key={index}
|
||||||
|
queryKey={section.queryKey}
|
||||||
|
queryFn={section.queryFn}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get suggestions
|
||||||
|
async function getSuggestions(api: Api, userId: string | undefined) {
|
||||||
|
if (!userId) return [];
|
||||||
|
const response = await getSuggestionsApi(api).getSuggestions({
|
||||||
|
userId,
|
||||||
|
limit: 10,
|
||||||
|
mediaType: ["Unknown"],
|
||||||
|
type: ["Series"],
|
||||||
|
});
|
||||||
|
return response.data.Items ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get the next up TV show for a series
|
||||||
|
async function getNextUp(
|
||||||
|
api: Api,
|
||||||
|
userId: string | undefined,
|
||||||
|
seriesId: string | undefined
|
||||||
|
) {
|
||||||
|
if (!userId || !seriesId) return null;
|
||||||
|
const response = await getTvShowsApi(api).getNextUp({
|
||||||
|
userId,
|
||||||
|
seriesId,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
return response.data.Items?.[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import { Platform } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { ListGroup } from "@/components/list/ListGroup";
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
import { ListItem } from "@/components/list/ListItem";
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector";
|
|
||||||
import { AudioToggles } from "@/components/settings/AudioToggles";
|
import { AudioToggles } from "@/components/settings/AudioToggles";
|
||||||
import DownloadSettings from "@/components/settings/DownloadSettings";
|
|
||||||
import { MediaProvider } from "@/components/settings/MediaContext";
|
import { MediaProvider } from "@/components/settings/MediaContext";
|
||||||
import { MediaToggles } from "@/components/settings/MediaToggles";
|
import { MediaToggles } from "@/components/settings/MediaToggles";
|
||||||
import { OtherSettings } from "@/components/settings/OtherSettings";
|
import { OtherSettings } from "@/components/settings/OtherSettings";
|
||||||
@@ -11,16 +10,20 @@ import { PluginSettings } from "@/components/settings/PluginSettings";
|
|||||||
import { QuickConnect } from "@/components/settings/QuickConnect";
|
import { QuickConnect } from "@/components/settings/QuickConnect";
|
||||||
import { StorageSettings } from "@/components/settings/StorageSettings";
|
import { StorageSettings } from "@/components/settings/StorageSettings";
|
||||||
import { SubtitleToggles } from "@/components/settings/SubtitleToggles";
|
import { SubtitleToggles } from "@/components/settings/SubtitleToggles";
|
||||||
|
import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector";
|
||||||
import { UserInfo } from "@/components/settings/UserInfo";
|
import { UserInfo } from "@/components/settings/UserInfo";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
import { useJellyfin } from "@/providers/JellyfinProvider";
|
import { useJellyfin } from "@/providers/JellyfinProvider";
|
||||||
import { clearLogs } from "@/utils/log";
|
import { clearLogs } from "@/utils/log";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation, useRouter } from "expo-router";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import React, { useEffect } from "react";
|
import React, { lazy, useEffect } from "react";
|
||||||
import { ScrollView, TouchableOpacity, View } from "react-native";
|
import { ScrollView, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
const DownloadSettings = lazy(
|
||||||
|
() => import("@/components/settings/DownloadSettings")
|
||||||
|
);
|
||||||
|
|
||||||
export default function settings() {
|
export default function settings() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -69,7 +72,7 @@ export default function settings() {
|
|||||||
|
|
||||||
<OtherSettings />
|
<OtherSettings />
|
||||||
|
|
||||||
<DownloadSettings />
|
{!Platform.isTV && <DownloadSettings />}
|
||||||
|
|
||||||
<PluginSettings />
|
<PluginSettings />
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function page() {
|
|||||||
const local = useLocalSearchParams();
|
const local = useLocalSearchParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { jellyseerrApi, jellyseerrUser, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr();
|
const { jellyseerrApi, jellyseerrUser } = useJellyseerr();
|
||||||
|
|
||||||
const { personId } = local as { personId: string };
|
const { personId } = local as { personId: string };
|
||||||
|
|
||||||
@@ -32,6 +32,15 @@ export default function page() {
|
|||||||
enabled: !!jellyseerrApi && !!personId,
|
enabled: !!jellyseerrApi && !!personId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const locale = useMemo(() => {
|
||||||
|
return jellyseerrUser?.settings?.locale || "en";
|
||||||
|
}, [jellyseerrUser]);
|
||||||
|
|
||||||
|
const region = useMemo(
|
||||||
|
() => jellyseerrUser?.settings?.region || "US",
|
||||||
|
[jellyseerrUser]
|
||||||
|
);
|
||||||
|
|
||||||
const castedRoles: PersonCreditCast[] = useMemo(
|
const castedRoles: PersonCreditCast[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
uniqBy(orderBy(
|
uniqBy(orderBy(
|
||||||
|
|||||||
@@ -209,12 +209,7 @@ export default function search() {
|
|||||||
paddingRight: insets.right,
|
paddingRight: insets.right,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View className="flex flex-col">
|
||||||
className="flex flex-col"
|
|
||||||
style={{
|
|
||||||
marginTop: Platform.OS === "android" ? 16 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{jellyseerrApi && (
|
{jellyseerrApi && (
|
||||||
<View className="flex flex-row flex-wrap space-x-2 px-4 mb-2">
|
<View className="flex flex-row flex-wrap space-x-2 px-4 mb-2">
|
||||||
<TouchableOpacity onPress={() => setSearchType("Library")}>
|
<TouchableOpacity onPress={() => setSearchType("Library")}>
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ export default function TabLayout() {
|
|||||||
<NativeTabs
|
<NativeTabs
|
||||||
sidebarAdaptable={false}
|
sidebarAdaptable={false}
|
||||||
ignoresTopSafeArea
|
ignoresTopSafeArea
|
||||||
tabBarStyle={{
|
barTintColor={Platform.OS === "android" ? "#121212" : undefined}
|
||||||
backgroundColor: "#121212",
|
|
||||||
}}
|
|
||||||
tabBarActiveTintColor={Colors.primary}
|
tabBarActiveTintColor={Colors.primary}
|
||||||
scrollEdgeAppearance="default"
|
scrollEdgeAppearance="default"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
|||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import native from "@/utils/profiles/native";
|
import native from "@/utils/profiles/native";
|
||||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||||
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
|
||||||
import {
|
import {
|
||||||
getPlaystateApi,
|
getPlaystateApi,
|
||||||
getUserLibraryApi,
|
getUserLibraryApi,
|
||||||
@@ -299,18 +298,16 @@ export default function page() {
|
|||||||
setIsPipStarted(pipStarted);
|
setIsPipStarted(pipStarted);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onPlaybackStateChanged = useCallback(async (e: PlaybackStatePayload) => {
|
const onPlaybackStateChanged = useCallback((e: PlaybackStatePayload) => {
|
||||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||||
|
|
||||||
if (state === "Playing") {
|
if (state === "Playing") {
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
if (!Platform.isTV) await activateKeepAwakeAsync()
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state === "Paused") {
|
if (state === "Paused") {
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
if (!Platform.isTV) await deactivateKeepAwake();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,21 +361,6 @@ export default function page() {
|
|||||||
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const externalSubtitles = allSubs
|
|
||||||
.filter((sub: any) => sub.DeliveryMethod === "External")
|
|
||||||
.map((sub: any) => ({
|
|
||||||
name: sub.DisplayTitle,
|
|
||||||
DeliveryUrl: api?.basePath + sub.DeliveryUrl,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
|
||||||
|
|
||||||
// Add useEffect to handle mounting
|
|
||||||
useEffect(() => {
|
|
||||||
setIsMounted(true);
|
|
||||||
return () => setIsMounted(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
|
||||||
@@ -401,6 +383,13 @@ export default function page() {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const externalSubtitles = allSubs
|
||||||
|
.filter((sub: any) => sub.DeliveryMethod === "External")
|
||||||
|
.map((sub: any) => ({
|
||||||
|
name: sub.DisplayTitle,
|
||||||
|
DeliveryUrl: api?.basePath + sub.DeliveryUrl,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: "black" }}>
|
<View style={{ flex: 1, backgroundColor: "black" }}>
|
||||||
<View
|
<View
|
||||||
@@ -430,6 +419,7 @@ export default function page() {
|
|||||||
progressUpdateInterval={1000}
|
progressUpdateInterval={1000}
|
||||||
onVideoStateChange={onPlaybackStateChanged}
|
onVideoStateChange={onPlaybackStateChanged}
|
||||||
onPipStarted={onPipStarted}
|
onPipStarted={onPipStarted}
|
||||||
|
onVideoLoadStart={() => {}}
|
||||||
onVideoLoadEnd={() => {
|
onVideoLoadEnd={() => {
|
||||||
setIsVideoLoaded(true);
|
setIsVideoLoaded(true);
|
||||||
}}
|
}}
|
||||||
@@ -443,7 +433,7 @@ export default function page() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{videoRef.current && !isPipStarted && isMounted === true ? (
|
{videoRef.current && !isPipStarted && (
|
||||||
<Controls
|
<Controls
|
||||||
mediaSource={stream?.mediaSource}
|
mediaSource={stream?.mediaSource}
|
||||||
item={item}
|
item={item}
|
||||||
@@ -473,7 +463,7 @@ export default function page() {
|
|||||||
stop={stop}
|
stop={stop}
|
||||||
isVlc
|
isVlc
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import "@/augmentations";
|
import "@/augmentations";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import { Text } from "@/components/common/Text";
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import { DownloadProvider } from "@/providers/DownloadProvider";
|
import { DownloadProvider } from "@/providers/DownloadProvider";
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +10,10 @@ import {
|
|||||||
} from "@/providers/JellyfinProvider";
|
} from "@/providers/JellyfinProvider";
|
||||||
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
||||||
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
||||||
|
import {
|
||||||
|
SplashScreenProvider,
|
||||||
|
useSplashScreenLoading,
|
||||||
|
} from "@/providers/SplashScreenProvider";
|
||||||
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
||||||
import { Settings, useSettings } from "@/utils/atoms/settings";
|
import { Settings, useSettings } from "@/utils/atoms/settings";
|
||||||
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
|
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
|
||||||
@@ -27,15 +32,16 @@ const BackgroundFetch = !Platform.isTV
|
|||||||
? require("expo-background-fetch")
|
? require("expo-background-fetch")
|
||||||
: null;
|
: null;
|
||||||
import * as FileSystem from "expo-file-system";
|
import * as FileSystem from "expo-file-system";
|
||||||
|
import { useFonts } from "expo-font";
|
||||||
|
import { useKeepAwake } from "expo-keep-awake";
|
||||||
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
|
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import * as SplashScreen from "expo-splash-screen";
|
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
|
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
|
||||||
import { getLocales } from "expo-localization";
|
import { getLocales } from "expo-localization";
|
||||||
import { Provider as JotaiProvider } from "jotai";
|
import { Provider as JotaiProvider } from "jotai";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider, useTranslation } from "react-i18next";
|
||||||
import { Appearance, AppState } from "react-native";
|
import { Appearance, AppState } from "react-native";
|
||||||
import { SystemBars } from "react-native-edge-to-edge";
|
import { SystemBars } from "react-native-edge-to-edge";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
@@ -52,15 +58,6 @@ if (!Platform.isTV) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the splash screen visible while we fetch resources
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
|
||||||
|
|
||||||
// Set the animation options. This is optional.
|
|
||||||
SplashScreen.setOptions({
|
|
||||||
duration: 500,
|
|
||||||
fade: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
function useNotificationObserver() {
|
function useNotificationObserver() {
|
||||||
if (Platform.isTV) return;
|
if (Platform.isTV) return;
|
||||||
|
|
||||||
@@ -227,15 +224,17 @@ export default function RootLayout() {
|
|||||||
Appearance.setColorScheme("dark");
|
Appearance.setColorScheme("dark");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<SplashScreenProvider>
|
||||||
<JotaiProvider>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<ActionSheetProvider>
|
<JotaiProvider>
|
||||||
<I18nextProvider i18n={i18n}>
|
<ActionSheetProvider>
|
||||||
<Layout />
|
<I18nextProvider i18n={i18n}>
|
||||||
</I18nextProvider>
|
<Layout />
|
||||||
</ActionSheetProvider>
|
</I18nextProvider>
|
||||||
</JotaiProvider>
|
</ActionSheetProvider>
|
||||||
</GestureHandlerRootView>
|
</JotaiProvider>
|
||||||
|
</GestureHandlerRootView>
|
||||||
|
</SplashScreenProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +261,11 @@ function Layout() {
|
|||||||
}, [settings?.preferedLanguage, i18n]);
|
}, [settings?.preferedLanguage, i18n]);
|
||||||
|
|
||||||
if (!Platform.isTV) {
|
if (!Platform.isTV) {
|
||||||
|
useKeepAwake();
|
||||||
useNotificationObserver();
|
useNotificationObserver();
|
||||||
|
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAndRequestPermissions();
|
checkAndRequestPermissions();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -301,6 +303,16 @@ function Layout() {
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [loaded] = useFonts({
|
||||||
|
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
||||||
|
});
|
||||||
|
|
||||||
|
useSplashScreenLoading(!loaded);
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<JobQueueProvider>
|
<JobQueueProvider>
|
||||||
@@ -312,7 +324,7 @@ function Layout() {
|
|||||||
<BottomSheetModalProvider>
|
<BottomSheetModalProvider>
|
||||||
<SystemBars style="light" hidden={false} />
|
<SystemBars style="light" hidden={false} />
|
||||||
<ThemeProvider value={DarkTheme}>
|
<ThemeProvider value={DarkTheme}>
|
||||||
<Stack initialRouteName="(auth)/(tabs)">
|
<Stack>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="(auth)/(tabs)"
|
name="(auth)/(tabs)"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
|||||||
import { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
|
import { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useLocalSearchParams, useNavigation } from "expo-router";
|
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
@@ -19,20 +19,17 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Keyboard } from "react-native";
|
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { t } from "i18next";
|
import { t } from 'i18next';
|
||||||
const CredentialsSchema = z.object({
|
const CredentialsSchema = z.object({
|
||||||
username: z.string().min(1, t("login.username_required")),
|
username: z.string().min(1, t("login.username_required")),});
|
||||||
});
|
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const api = useAtomValue(apiAtom);
|
|
||||||
const navigation = useNavigation();
|
|
||||||
const params = useLocalSearchParams();
|
|
||||||
const { setServer, login, removeServer, initiateQuickConnect } =
|
const { setServer, login, removeServer, initiateQuickConnect } =
|
||||||
useJellyfin();
|
useJellyfin();
|
||||||
|
const [api] = useAtom(apiAtom);
|
||||||
|
const params = useLocalSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
apiUrl: _apiUrl,
|
apiUrl: _apiUrl,
|
||||||
@@ -40,8 +37,6 @@ const Login: React.FC = () => {
|
|||||||
password: _password,
|
password: _password,
|
||||||
} = params as { apiUrl: string; username: string; password: string };
|
} = params as { apiUrl: string; username: string; password: string };
|
||||||
|
|
||||||
const [loadingServerCheck, setLoadingServerCheck] = useState<boolean>(false);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [serverURL, setServerURL] = useState<string>(_apiUrl);
|
const [serverURL, setServerURL] = useState<string>(_apiUrl);
|
||||||
const [serverName, setServerName] = useState<string>("");
|
const [serverName, setServerName] = useState<string>("");
|
||||||
const [credentials, setCredentials] = useState<{
|
const [credentials, setCredentials] = useState<{
|
||||||
@@ -52,11 +47,10 @@ const Login: React.FC = () => {
|
|||||||
password: _password,
|
password: _password,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* A way to auto login based on a link
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// we might re-use the checkUrl function here to check the url as well
|
||||||
|
// however, I don't think it should be necessary for now
|
||||||
if (_apiUrl) {
|
if (_apiUrl) {
|
||||||
setServer({
|
setServer({
|
||||||
address: _apiUrl,
|
address: _apiUrl,
|
||||||
@@ -72,6 +66,7 @@ const Login: React.FC = () => {
|
|||||||
})();
|
})();
|
||||||
}, [_apiUrl, _username, _password]);
|
}, [_apiUrl, _username, _password]);
|
||||||
|
|
||||||
|
const navigation = useNavigation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerTitle: serverName,
|
headerTitle: serverName,
|
||||||
@@ -84,17 +79,15 @@ const Login: React.FC = () => {
|
|||||||
className="flex flex-row items-center"
|
className="flex flex-row items-center"
|
||||||
>
|
>
|
||||||
<Ionicons name="chevron-back" size={18} color={Colors.primary} />
|
<Ionicons name="chevron-back" size={18} color={Colors.primary} />
|
||||||
<Text className="ml-2 text-purple-600">
|
<Text className="ml-2 text-purple-600">{t("login.change_server")}</Text>
|
||||||
{t("login.change_server")}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : null,
|
) : null,
|
||||||
});
|
});
|
||||||
}, [serverName, navigation, api?.basePath]);
|
}, [serverName, navigation, api?.basePath]);
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
Keyboard.dismiss();
|
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = CredentialsSchema.safeParse(credentials);
|
const result = CredentialsSchema.safeParse(credentials);
|
||||||
@@ -105,16 +98,15 @@ const Login: React.FC = () => {
|
|||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
Alert.alert(t("login.connection_failed"), error.message);
|
Alert.alert(t("login.connection_failed"), error.message);
|
||||||
} else {
|
} else {
|
||||||
Alert.alert(
|
Alert.alert(t("login.connection_failed"), t("login.an_unexpected_error_occured"));
|
||||||
t("login.connection_failed"),
|
|
||||||
t("login.an_unexpected_error_occured")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [loadingServerCheck, setLoadingServerCheck] = useState<boolean>(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the availability and validity of a Jellyfin server URL.
|
* Checks the availability and validity of a Jellyfin server URL.
|
||||||
*
|
*
|
||||||
@@ -188,21 +180,14 @@ const Login: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const code = await initiateQuickConnect();
|
const code = await initiateQuickConnect();
|
||||||
if (code) {
|
if (code) {
|
||||||
Alert.alert(
|
Alert.alert(t("login.quick_connect"), t("login.enter_code_to_login", {code: code}), [
|
||||||
t("login.quick_connect"),
|
{
|
||||||
t("login.enter_code_to_login", { code: code }),
|
text: t("login.got_it"),
|
||||||
[
|
},
|
||||||
{
|
]);
|
||||||
text: t("login.got_it"),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Alert.alert(
|
Alert.alert(t("login.error_title"), t("login.failed_to_initiate_quick_connect"));
|
||||||
t("login.error_title"),
|
|
||||||
t("login.failed_to_initiate_quick_connect")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,18 +201,16 @@ const Login: React.FC = () => {
|
|||||||
<View className="flex flex-col h-full relative items-center justify-center">
|
<View className="flex flex-col h-full relative items-center justify-center">
|
||||||
<View className="px-4 -mt-20 w-full">
|
<View className="px-4 -mt-20 w-full">
|
||||||
<View className="flex flex-col space-y-2">
|
<View className="flex flex-col space-y-2">
|
||||||
<Text className="text-2xl font-bold -mb-2">
|
<Text className="text-2xl font-bold -mb-2">
|
||||||
<>
|
<>
|
||||||
{serverName ? (
|
{serverName ? (
|
||||||
<>
|
<>
|
||||||
{t("login.login_to_title") + " "}
|
{t("login.login_to_title") + " "}
|
||||||
<Text className="text-purple-600">{serverName}</Text>
|
<Text className="text-purple-600">{serverName}</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : t("login.login_title")}
|
||||||
t("login.login_title")
|
</>
|
||||||
)}
|
</Text>
|
||||||
</>
|
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-neutral-400">
|
<Text className="text-xs text-neutral-400">
|
||||||
{api.basePath}
|
{api.basePath}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -237,6 +220,7 @@ const Login: React.FC = () => {
|
|||||||
setCredentials({ ...credentials, username: text })
|
setCredentials({ ...credentials, username: text })
|
||||||
}
|
}
|
||||||
value={credentials.username}
|
value={credentials.username}
|
||||||
|
autoFocus
|
||||||
secureTextEntry={false}
|
secureTextEntry={false}
|
||||||
keyboardType="default"
|
keyboardType="default"
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
@@ -316,9 +300,7 @@ const Login: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
loading={loadingServerCheck}
|
loading={loadingServerCheck}
|
||||||
disabled={loadingServerCheck}
|
disabled={loadingServerCheck}
|
||||||
onPress={async () => {
|
onPress={async () => await handleConnect(serverURL)}
|
||||||
await handleConnect(serverURL);
|
|
||||||
}}
|
|
||||||
className="w-full grow"
|
className="w-full grow"
|
||||||
>
|
>
|
||||||
{t("server.connect_button")}
|
{t("server.connect_button")}
|
||||||
|
|||||||
49
bun.lock
49
bun.lock
@@ -13,6 +13,7 @@
|
|||||||
"@gorhom/bottom-sheet": "^5.1.0",
|
"@gorhom/bottom-sheet": "^5.1.0",
|
||||||
"@jellyfin/sdk": "^0.11.0",
|
"@jellyfin/sdk": "^0.11.0",
|
||||||
"@kesha-antonov/react-native-background-downloader": "3.2.6",
|
"@kesha-antonov/react-native-background-downloader": "3.2.6",
|
||||||
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/netinfo": "11.4.1",
|
"@react-native-community/netinfo": "11.4.1",
|
||||||
"@react-native-menu/menu": "^1.2.2",
|
"@react-native-menu/menu": "^1.2.2",
|
||||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||||
@@ -20,6 +21,9 @@
|
|||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@shopify/flash-list": "1.7.3",
|
"@shopify/flash-list": "1.7.3",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
|
"@types/lodash": "^4.17.15",
|
||||||
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"expo": "^52.0.31",
|
"expo": "^52.0.31",
|
||||||
@@ -44,7 +48,7 @@
|
|||||||
"expo-router": "~4.0.17",
|
"expo-router": "~4.0.17",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
"expo-sensors": "~14.0.2",
|
"expo-sensors": "~14.0.2",
|
||||||
"expo-splash-screen": "~0.29.22",
|
"expo-splash-screen": "~0.29.21",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-system-ui": "~4.0.8",
|
"expo-system-ui": "~4.0.8",
|
||||||
"expo-task-manager": "~12.0.5",
|
"expo-task-manager": "~12.0.5",
|
||||||
@@ -60,7 +64,7 @@
|
|||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "0.8.7",
|
"react-native-bottom-tabs": "0.8.6",
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
"react-native-compressor": "^1.10.3",
|
"react-native-compressor": "^1.10.3",
|
||||||
"react-native-country-flag": "^2.0.2",
|
"react-native-country-flag": "^2.0.2",
|
||||||
@@ -101,11 +105,8 @@
|
|||||||
"@react-native-community/cli": "15.1.3",
|
"@react-native-community/cli": "15.1.3",
|
||||||
"@react-native-tvos/config-tv": "^0.1.1",
|
"@react-native-tvos/config-tv": "^0.1.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/lodash": "^4.17.15",
|
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
|
||||||
"@types/react-test-renderer": "^19.0.0",
|
"@types/react-test-renderer": "^19.0.0",
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"react-test-renderer": "19.0.0",
|
"react-test-renderer": "19.0.0",
|
||||||
@@ -574,6 +575,8 @@
|
|||||||
|
|
||||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="],
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="],
|
||||||
|
|
||||||
|
"@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@1.23.1", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.60 <1.0" } }, "sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA=="],
|
||||||
|
|
||||||
"@react-native-community/cli": ["@react-native-community/cli@15.1.3", "", { "dependencies": { "@react-native-community/cli-clean": "15.1.3", "@react-native-community/cli-config": "15.1.3", "@react-native-community/cli-debugger-ui": "15.1.3", "@react-native-community/cli-doctor": "15.1.3", "@react-native-community/cli-server-api": "15.1.3", "@react-native-community/cli-tools": "15.1.3", "@react-native-community/cli-types": "15.1.3", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-+ih/WYUkJsEV2CMAnOHvVoSIz/Ahg5UJk+sqSIOmY79mWAglQzfLP71o7b0neJCnJWLmWiO6G6/S+kmULefD5g=="],
|
"@react-native-community/cli": ["@react-native-community/cli@15.1.3", "", { "dependencies": { "@react-native-community/cli-clean": "15.1.3", "@react-native-community/cli-config": "15.1.3", "@react-native-community/cli-debugger-ui": "15.1.3", "@react-native-community/cli-doctor": "15.1.3", "@react-native-community/cli-server-api": "15.1.3", "@react-native-community/cli-tools": "15.1.3", "@react-native-community/cli-types": "15.1.3", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-+ih/WYUkJsEV2CMAnOHvVoSIz/Ahg5UJk+sqSIOmY79mWAglQzfLP71o7b0neJCnJWLmWiO6G6/S+kmULefD5g=="],
|
||||||
|
|
||||||
"@react-native-community/cli-clean": ["@react-native-community/cli-clean@15.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "15.1.3", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-3s9NGapIkONFoCUN2s77NYI987GPSCdr74rTf0TWyGIDf4vTYgKoWKKR+Ml3VTa1BCj51r4cYuHEKE1pjUSc0w=="],
|
"@react-native-community/cli-clean": ["@react-native-community/cli-clean@15.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "15.1.3", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-3s9NGapIkONFoCUN2s77NYI987GPSCdr74rTf0TWyGIDf4vTYgKoWKKR+Ml3VTa1BCj51r4cYuHEKE1pjUSc0w=="],
|
||||||
@@ -754,7 +757,7 @@
|
|||||||
|
|
||||||
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
|
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
|
||||||
|
|
||||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
"ajv": ["ajv@8.11.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg=="],
|
||||||
|
|
||||||
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||||
|
|
||||||
@@ -1056,7 +1059,7 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.100", "", {}, "sha512-u1z9VuzDXV86X2r3vAns0/5ojfXBue9o0+JDUDBKYqGLjxLkSqsSUoPU/6kW0gx76V44frHaf6Zo+QF74TQCMg=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.101", "", {}, "sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
@@ -1086,6 +1089,8 @@
|
|||||||
|
|
||||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
@@ -1204,8 +1209,6 @@
|
|||||||
|
|
||||||
"fast-loops": ["fast-loops@1.1.4", "", {}, "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg=="],
|
"fast-loops": ["fast-loops@1.1.4", "", {}, "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg=="],
|
||||||
|
|
||||||
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
|
|
||||||
|
|
||||||
"fast-xml-parser": ["fast-xml-parser@4.5.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w=="],
|
"fast-xml-parser": ["fast-xml-parser@4.5.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
|
"fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
|
||||||
@@ -1248,7 +1251,7 @@
|
|||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
|
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
|
||||||
|
|
||||||
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
|
"form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
|
||||||
|
|
||||||
"freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="],
|
"freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="],
|
||||||
|
|
||||||
@@ -1392,6 +1395,8 @@
|
|||||||
|
|
||||||
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
||||||
|
|
||||||
|
"is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="],
|
||||||
|
|
||||||
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
|
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
|
||||||
|
|
||||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||||
@@ -1546,6 +1551,8 @@
|
|||||||
|
|
||||||
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
|
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
|
||||||
|
|
||||||
|
"merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="],
|
||||||
|
|
||||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
@@ -1826,7 +1833,7 @@
|
|||||||
|
|
||||||
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
|
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
|
||||||
|
|
||||||
"react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.7", "", { "dependencies": { "react-freeze": "^1.0.0", "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-cVQYs4r8Hb9V9oOO/SqsmBaZ7IzE/3Tpvz4mmRjNXKi1cBWC+ZpKTuqRx6EPjBCYTVK+vbAfoTM6IHS+6NVg4w=="],
|
"react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.6", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-N5b3MoSfsEqlmvFyIyL0X0bd+QAtB+cXH1rl/+R2Kr0BefBTC7ZldGcPhgK3FhBbt0vJDpd3kLb/dvmqZd+Eag=="],
|
||||||
|
|
||||||
"react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="],
|
"react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="],
|
||||||
|
|
||||||
@@ -1904,7 +1911,7 @@
|
|||||||
|
|
||||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.3", "", { "dependencies": { "process": "^0.11.10", "readable-stream": "^4.7.0" } }, "sha512-In3boYjBnbGVrLuuRu/Ath/H6h1jgk30nAsk/71tCare1dTVoe1oMBGRn5LGf0n3c1BcHwwAqpraxX4AUAP5KA=="],
|
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
@@ -1970,7 +1977,7 @@
|
|||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
"send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="],
|
||||||
|
|
||||||
"serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="],
|
"serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="],
|
||||||
|
|
||||||
@@ -2286,7 +2293,7 @@
|
|||||||
|
|
||||||
"@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
"@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||||
|
|
||||||
"@expo/cli/form-data": ["form-data@3.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ=="],
|
"@expo/cli/form-data": ["form-data@3.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.35" } }, "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w=="],
|
||||||
|
|
||||||
"@expo/cli/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"@expo/cli/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
@@ -2448,8 +2455,6 @@
|
|||||||
|
|
||||||
"expo-build-properties/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
"expo-build-properties/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||||
|
|
||||||
"expo-dev-launcher/ajv": ["ajv@8.11.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg=="],
|
|
||||||
|
|
||||||
"expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
"expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||||
|
|
||||||
"expo-modules-autolinking/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
"expo-modules-autolinking/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
@@ -2600,10 +2605,10 @@
|
|||||||
|
|
||||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
|
||||||
|
|
||||||
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||||
|
|
||||||
|
"serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||||
|
|
||||||
"simple-plist/bplist-creator": ["bplist-creator@0.1.0", "", { "dependencies": { "stream-buffers": "2.2.x" } }, "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg=="],
|
"simple-plist/bplist-creator": ["bplist-creator@0.1.0", "", { "dependencies": { "stream-buffers": "2.2.x" } }, "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg=="],
|
||||||
|
|
||||||
"simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="],
|
"simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="],
|
||||||
@@ -2798,6 +2803,12 @@
|
|||||||
|
|
||||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
|
"serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
|
"serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||||
|
|
||||||
|
"serve-static/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||||
|
|
||||||
"slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
"slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||||
|
|
||||||
"tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
@@ -2876,6 +2887,8 @@
|
|||||||
|
|
||||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|
||||||
|
"serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
"slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||||
|
|
||||||
"temp/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"temp/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|||||||
@@ -115,100 +115,96 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
case 0:
|
case 0:
|
||||||
if (!Platform.isTV) {
|
if (!Platform.isTV) {
|
||||||
await CastContext.getPlayServicesState().then(async (state) => {
|
await CastContext.getPlayServicesState().then(async (state) => {
|
||||||
if (state && state !== PlayServicesState.SUCCESS) {
|
if (state && state !== PlayServicesState.SUCCESS)
|
||||||
CastContext.showPlayServicesErrorDialog(state);
|
CastContext.showPlayServicesErrorDialog(state);
|
||||||
} else {
|
else {
|
||||||
// Get a new URL with the Chromecast device profile:
|
// Get a new URL with the Chromecast device profile:
|
||||||
try {
|
const data = await getStreamUrl({
|
||||||
const data = await getStreamUrl({
|
api,
|
||||||
api,
|
item,
|
||||||
item,
|
deviceProfile: chromecastProfile,
|
||||||
deviceProfile: chromecastProfile,
|
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
||||||
startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
|
userId: user?.Id,
|
||||||
userId: user?.Id,
|
audioStreamIndex: selectedOptions.audioIndex,
|
||||||
audioStreamIndex: selectedOptions.audioIndex,
|
maxStreamingBitrate: selectedOptions.bitrate?.value,
|
||||||
maxStreamingBitrate: selectedOptions.bitrate?.value,
|
mediaSourceId: selectedOptions.mediaSource?.Id,
|
||||||
mediaSourceId: selectedOptions.mediaSource?.Id,
|
subtitleStreamIndex: selectedOptions.subtitleIndex,
|
||||||
subtitleStreamIndex: selectedOptions.subtitleIndex,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!data?.url) {
|
if (!data?.url) {
|
||||||
console.warn("No URL returned from getStreamUrl", data);
|
console.warn("No URL returned from getStreamUrl", data);
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t("player.client_error"),
|
t("player.client_error"),
|
||||||
t("player.could_not_create_stream_for_chromecast")
|
t("player.could_not_create_stream_for_chromecast")
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
client
|
|
||||||
.loadMedia({
|
|
||||||
mediaInfo: {
|
|
||||||
contentUrl: data?.url,
|
|
||||||
contentType: "video/mp4",
|
|
||||||
metadata:
|
|
||||||
item.Type === "Episode"
|
|
||||||
? {
|
|
||||||
type: "tvShow",
|
|
||||||
title: item.Name || "",
|
|
||||||
episodeNumber: item.IndexNumber || 0,
|
|
||||||
seasonNumber: item.ParentIndexNumber || 0,
|
|
||||||
seriesTitle: item.SeriesName || "",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: getParentBackdropImageUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
quality: 90,
|
|
||||||
width: 2000,
|
|
||||||
})!,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: item.Type === "Movie"
|
|
||||||
? {
|
|
||||||
type: "movie",
|
|
||||||
title: item.Name || "",
|
|
||||||
subtitle: item.Overview || "",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: getPrimaryImageUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
quality: 90,
|
|
||||||
width: 2000,
|
|
||||||
})!,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: "generic",
|
|
||||||
title: item.Name || "",
|
|
||||||
subtitle: item.Overview || "",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: getPrimaryImageUrl({
|
|
||||||
api,
|
|
||||||
item,
|
|
||||||
quality: 90,
|
|
||||||
width: 2000,
|
|
||||||
})!,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
startTime: 0,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// state is already set when reopening current media, so skip it here.
|
|
||||||
if (isOpeningCurrentlyPlayingMedia) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CastContext.showExpandedControls();
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client
|
||||||
|
.loadMedia({
|
||||||
|
mediaInfo: {
|
||||||
|
contentUrl: data?.url,
|
||||||
|
contentType: "video/mp4",
|
||||||
|
metadata:
|
||||||
|
item.Type === "Episode"
|
||||||
|
? {
|
||||||
|
type: "tvShow",
|
||||||
|
title: item.Name || "",
|
||||||
|
episodeNumber: item.IndexNumber || 0,
|
||||||
|
seasonNumber: item.ParentIndexNumber || 0,
|
||||||
|
seriesTitle: item.SeriesName || "",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: getParentBackdropImageUrl({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
quality: 90,
|
||||||
|
width: 2000,
|
||||||
|
})!,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: item.Type === "Movie"
|
||||||
|
? {
|
||||||
|
type: "movie",
|
||||||
|
title: item.Name || "",
|
||||||
|
subtitle: item.Overview || "",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: getPrimaryImageUrl({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
quality: 90,
|
||||||
|
width: 2000,
|
||||||
|
})!,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: "generic",
|
||||||
|
title: item.Name || "",
|
||||||
|
subtitle: item.Overview || "",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: getPrimaryImageUrl({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
quality: 90,
|
||||||
|
width: 2000,
|
||||||
|
})!,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: 0,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// state is already set when reopening current media, so skip it here.
|
||||||
|
if (isOpeningCurrentlyPlayingMedia) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CastContext.showExpandedControls();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface Release {
|
|||||||
type: number;
|
type: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dateOpts: Intl.DateTimeFormatOptions = {
|
const dateOpts: Intl.DateTimeFormatOptions = {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
@@ -50,9 +50,18 @@ const Fact: React.FC<{ title: string; fact?: string | null } & ViewProps> = ({
|
|||||||
const DetailFacts: React.FC<
|
const DetailFacts: React.FC<
|
||||||
{ details?: MovieDetails | TvDetails } & ViewProps
|
{ details?: MovieDetails | TvDetails } & ViewProps
|
||||||
> = ({ details, className, ...props }) => {
|
> = ({ details, className, ...props }) => {
|
||||||
const { jellyseerrUser, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr();
|
const { jellyseerrUser } = useJellyseerr();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const locale = useMemo(() => {
|
||||||
|
return jellyseerrUser?.settings?.locale || "en";
|
||||||
|
}, [jellyseerrUser]);
|
||||||
|
|
||||||
|
const region = useMemo(
|
||||||
|
() => jellyseerrUser?.settings?.region || "US",
|
||||||
|
[jellyseerrUser]
|
||||||
|
);
|
||||||
|
|
||||||
const releases = useMemo(
|
const releases = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(details as MovieDetails)?.releases?.results.find(
|
(details as MovieDetails)?.releases?.results.find(
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import { Loader } from "../Loader";
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
|
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
|
||||||
import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
||||||
import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard";
|
|
||||||
import {dateOpts} from "@/components/jellyseerr/DetailFacts";
|
|
||||||
|
|
||||||
const JellyseerrSeasonEpisodes: React.FC<{
|
const JellyseerrSeasonEpisodes: React.FC<{
|
||||||
details: TvDetails;
|
details: TvDetails;
|
||||||
@@ -54,51 +52,26 @@ const JellyseerrSeasonEpisodes: React.FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RenderItem = ({ item, index }: any) => {
|
const RenderItem = ({ item, index }: any) => {
|
||||||
const { jellyseerrApi, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr();
|
const { jellyseerrApi } = useJellyseerr();
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
const upcomingAirDate = useMemo(() => {
|
|
||||||
const airDate = item.airDate;
|
|
||||||
if (airDate) {
|
|
||||||
let airDateObj = new Date(airDate);
|
|
||||||
|
|
||||||
if (new Date() < airDateObj) {
|
|
||||||
return airDateObj.toLocaleDateString(
|
|
||||||
`${locale}-${region}`,
|
|
||||||
dateOpts
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [item]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex flex-col w-44 mt-2">
|
<View className="flex flex-col w-44 mt-2">
|
||||||
<View className="relative aspect-video rounded-lg overflow-hidden border border-neutral-800">
|
<View className="relative aspect-video rounded-lg overflow-hidden border border-neutral-800">
|
||||||
{!imageError ? (
|
{!imageError ? (
|
||||||
<>
|
<Image
|
||||||
<Image
|
key={item.id}
|
||||||
key={item.id}
|
id={item.id}
|
||||||
id={item.id}
|
source={{
|
||||||
source={{
|
uri: jellyseerrApi?.imageProxy(item.stillPath),
|
||||||
uri: jellyseerrApi?.imageProxy(item.stillPath),
|
}}
|
||||||
}}
|
cachePolicy={"memory-disk"}
|
||||||
cachePolicy={"memory-disk"}
|
contentFit="cover"
|
||||||
contentFit="cover"
|
className="w-full h-full"
|
||||||
className="w-full h-full"
|
onError={(e) => {
|
||||||
onError={(e) => {
|
setImageError(true);
|
||||||
setImageError(true);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
{upcomingAirDate && (
|
|
||||||
<View className="absolute justify-center bottom-0 right-0.5 items-center">
|
|
||||||
<View className="rounded-full bg-purple-600/30 p-1">
|
|
||||||
<Text className="text-center text-xs" style={textShadowStyle.shadow}>
|
|
||||||
{upcomingAirDate}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<View className="flex flex-col w-full h-full items-center justify-center border border-neutral-800 bg-neutral-900">
|
<View className="flex flex-col w-full h-full items-center justify-center border border-neutral-800 bg-neutral-900">
|
||||||
<Ionicons
|
<Ionicons
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function DownloadSettings({ ...props }) {
|
|||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Label>
|
<DropdownMenu.Label>
|
||||||
{t("home.settings.downloads.download_method")}
|
{t("home.settings.downloads.methods")}
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key="1"
|
key="1"
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function DownloadSettings({ ...props }) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,9 @@ export const JellyseerrSettings = () => {
|
|||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings();
|
const [settings, updateSettings, pluginSettings] = useSettings();
|
||||||
|
|
||||||
|
const [promptForJellyseerrPass, setPromptForJellyseerrPass] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
const [jellyseerrPassword, setJellyseerrPassword] = useState<
|
const [jellyseerrPassword, setJellyseerrPassword] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@@ -36,16 +39,11 @@ export const JellyseerrSettings = () => {
|
|||||||
|
|
||||||
const loginToJellyseerrMutation = useMutation({
|
const loginToJellyseerrMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (!jellyseerrServerUrl && !settings?.jellyseerrServerUrl)
|
if (!jellyseerrServerUrl || !user?.Name || !jellyseerrPassword) {
|
||||||
throw new Error("Missing server url");
|
|
||||||
if (!user?.Name)
|
|
||||||
throw new Error("Missing required information for login");
|
throw new Error("Missing required information for login");
|
||||||
const jellyseerrTempApi = new JellyseerrApi(
|
}
|
||||||
jellyseerrServerUrl || settings.jellyseerrServerUrl || ""
|
const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl);
|
||||||
);
|
return jellyseerrTempApi.login(user.Name, jellyseerrPassword);
|
||||||
const testResult = await jellyseerrTempApi.test();
|
|
||||||
if (!testResult.isValid) throw new Error("Invalid server url");
|
|
||||||
return jellyseerrTempApi.login(user.Name, jellyseerrPassword || "");
|
|
||||||
},
|
},
|
||||||
onSuccess: (user) => {
|
onSuccess: (user) => {
|
||||||
setJellyseerrUser(user);
|
setJellyseerrUser(user);
|
||||||
@@ -59,11 +57,31 @@ export const JellyseerrSettings = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const testJellyseerrServerUrlMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!jellyseerrServerUrl || jellyseerrApi) return null;
|
||||||
|
const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl);
|
||||||
|
return jellyseerrTempApi.test();
|
||||||
|
},
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result && result.isValid) {
|
||||||
|
if (result.requiresPass) {
|
||||||
|
setPromptForJellyseerrPass(true);
|
||||||
|
} else {
|
||||||
|
updateSettings({ jellyseerrServerUrl });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setPromptForJellyseerrPass(false);
|
||||||
|
setjellyseerrServerUrl(undefined);
|
||||||
|
clearAllJellyseerData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const clearData = () => {
|
const clearData = () => {
|
||||||
clearAllJellyseerData().finally(() => {
|
clearAllJellyseerData().finally(() => {
|
||||||
setJellyseerrUser(undefined);
|
|
||||||
setJellyseerrPassword(undefined);
|
|
||||||
setjellyseerrServerUrl(undefined);
|
setjellyseerrServerUrl(undefined);
|
||||||
|
setPromptForJellyseerrPass(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,46 +92,34 @@ export const JellyseerrSettings = () => {
|
|||||||
<>
|
<>
|
||||||
<ListGroup title={"Jellyseerr"}>
|
<ListGroup title={"Jellyseerr"}>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t(
|
title={t("home.settings.plugins.jellyseerr.total_media_requests")}
|
||||||
"home.settings.plugins.jellyseerr.total_media_requests"
|
|
||||||
)}
|
|
||||||
value={jellyseerrUser?.requestCount?.toString()}
|
value={jellyseerrUser?.requestCount?.toString()}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.plugins.jellyseerr.movie_quota_limit")}
|
title={t("home.settings.plugins.jellyseerr.movie_quota_limit")}
|
||||||
value={
|
value={
|
||||||
jellyseerrUser?.movieQuotaLimit?.toString() ??
|
jellyseerrUser?.movieQuotaLimit?.toString() ?? t("home.settings.plugins.jellyseerr.unlimited")
|
||||||
t("home.settings.plugins.jellyseerr.unlimited")
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.plugins.jellyseerr.movie_quota_days")}
|
title={t("home.settings.plugins.jellyseerr.movie_quota_days")}
|
||||||
value={
|
value={
|
||||||
jellyseerrUser?.movieQuotaDays?.toString() ??
|
jellyseerrUser?.movieQuotaDays?.toString() ?? t("home.settings.plugins.jellyseerr.unlimited")
|
||||||
t("home.settings.plugins.jellyseerr.unlimited")
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.plugins.jellyseerr.tv_quota_limit")}
|
title={t("home.settings.plugins.jellyseerr.tv_quota_limit")}
|
||||||
value={
|
value={jellyseerrUser?.tvQuotaLimit?.toString() ?? t("home.settings.plugins.jellyseerr.unlimited")}
|
||||||
jellyseerrUser?.tvQuotaLimit?.toString() ??
|
|
||||||
t("home.settings.plugins.jellyseerr.unlimited")
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.plugins.jellyseerr.tv_quota_days")}
|
title={t("home.settings.plugins.jellyseerr.tv_quota_days")}
|
||||||
value={
|
value={jellyseerrUser?.tvQuotaDays?.toString() ?? t("home.settings.plugins.jellyseerr.unlimited")}
|
||||||
jellyseerrUser?.tvQuotaDays?.toString() ??
|
|
||||||
t("home.settings.plugins.jellyseerr.unlimited")
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
<Button color="red" onPress={clearData}>
|
<Button color="red" onPress={clearData}>
|
||||||
{t(
|
{t("home.settings.plugins.jellyseerr.reset_jellyseerr_config_button")}
|
||||||
"home.settings.plugins.jellyseerr.reset_jellyseerr_config_button"
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
@@ -122,20 +128,15 @@ export const JellyseerrSettings = () => {
|
|||||||
<Text className="text-xs text-red-600 mb-2">
|
<Text className="text-xs text-red-600 mb-2">
|
||||||
{t("home.settings.plugins.jellyseerr.jellyseerr_warning")}
|
{t("home.settings.plugins.jellyseerr.jellyseerr_warning")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="font-bold mb-1">
|
<Text className="font-bold mb-1">{t("home.settings.plugins.jellyseerr.server_url")}</Text>
|
||||||
{t("home.settings.plugins.jellyseerr.server_url")}
|
|
||||||
</Text>
|
|
||||||
<View className="flex flex-col shrink mb-2">
|
<View className="flex flex-col shrink mb-2">
|
||||||
<Text className="text-xs text-gray-600">
|
<Text className="text-xs text-gray-600">
|
||||||
{t("home.settings.plugins.jellyseerr.server_url_hint")}
|
{t("home.settings.plugins.jellyseerr.server_url_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Input
|
<Input
|
||||||
className="border border-neutral-800 mb-2"
|
placeholder={t("home.settings.plugins.jellyseerr.server_url_placeholder")}
|
||||||
placeholder={t(
|
value={settings?.jellyseerrServerUrl ?? jellyseerrServerUrl}
|
||||||
"home.settings.plugins.jellyseerr.server_url_placeholder"
|
|
||||||
)}
|
|
||||||
value={jellyseerrServerUrl ?? settings?.jellyseerrServerUrl}
|
|
||||||
defaultValue={
|
defaultValue={
|
||||||
settings?.jellyseerrServerUrl ?? jellyseerrServerUrl
|
settings?.jellyseerrServerUrl ?? jellyseerrServerUrl
|
||||||
}
|
}
|
||||||
@@ -144,20 +145,40 @@ export const JellyseerrSettings = () => {
|
|||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
textContentType="URL"
|
textContentType="URL"
|
||||||
onChangeText={setjellyseerrServerUrl}
|
onChangeText={setjellyseerrServerUrl}
|
||||||
editable={!loginToJellyseerrMutation.isPending}
|
editable={!testJellyseerrServerUrlMutation.isPending}
|
||||||
/>
|
/>
|
||||||
<View>
|
|
||||||
<Text className="font-bold mb-2">
|
<Button
|
||||||
{t("home.settings.plugins.jellyseerr.password")}
|
loading={testJellyseerrServerUrlMutation.isPending}
|
||||||
</Text>
|
disabled={testJellyseerrServerUrlMutation.isPending}
|
||||||
|
color={promptForJellyseerrPass ? "red" : "purple"}
|
||||||
|
className="h-12 mt-2"
|
||||||
|
onPress={() => {
|
||||||
|
if (promptForJellyseerrPass) {
|
||||||
|
clearData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
testJellyseerrServerUrlMutation.mutate();
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{promptForJellyseerrPass ? t("home.settings.plugins.jellyseerr.clear_button") : t("home.settings.plugins.jellyseerr.save_button")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<View
|
||||||
|
pointerEvents={promptForJellyseerrPass ? "auto" : "none"}
|
||||||
|
style={{
|
||||||
|
opacity: promptForJellyseerrPass ? 1 : 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className="font-bold mb-2">{t("home.settings.plugins.jellyseerr.password")}</Text>
|
||||||
<Input
|
<Input
|
||||||
className="border border-neutral-800"
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
focusable={true}
|
focusable={true}
|
||||||
placeholder={t(
|
placeholder={t("home.settings.plugins.jellyseerr.password_placeholder", {username: user?.Name})}
|
||||||
"home.settings.plugins.jellyseerr.password_placeholder",
|
|
||||||
{ username: user?.Name }
|
|
||||||
)}
|
|
||||||
value={jellyseerrPassword}
|
value={jellyseerrPassword}
|
||||||
keyboardType="default"
|
keyboardType="default"
|
||||||
secureTextEntry={true}
|
secureTextEntry={true}
|
||||||
@@ -165,7 +186,10 @@ export const JellyseerrSettings = () => {
|
|||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
textContentType="password"
|
textContentType="password"
|
||||||
onChangeText={setJellyseerrPassword}
|
onChangeText={setJellyseerrPassword}
|
||||||
editable={!loginToJellyseerrMutation.isPending}
|
editable={
|
||||||
|
!loginToJellyseerrMutation.isPending &&
|
||||||
|
promptForJellyseerrPass
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
loading={loginToJellyseerrMutation.isPending}
|
loading={loginToJellyseerrMutation.isPending}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
showArrow
|
showArrow
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.default_quality")}
|
title="Default quality"
|
||||||
disabled={pluginSettings?.defaultBitrate?.locked}
|
disabled={pluginSettings?.defaultBitrate?.locked}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -186,7 +186,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
label={t("home.settings.other.default_quality")}
|
label={t("home.settings.other.quality")}
|
||||||
onSelected={(defaultBitrate) => updateSettings({ defaultBitrate })}
|
onSelected={(defaultBitrate) => updateSettings({ defaultBitrate })}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@@ -1,485 +0,0 @@
|
|||||||
import { Button } from "@/components/Button";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel";
|
|
||||||
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
|
|
||||||
import { Loader } from "@/components/Loader";
|
|
||||||
import { MediaListSection } from "@/components/medialists/MediaListSection";
|
|
||||||
import { Colors } from "@/constants/Colors";
|
|
||||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
|
||||||
import { Api } from "@jellyfin/sdk";
|
|
||||||
import {
|
|
||||||
BaseItemDto,
|
|
||||||
BaseItemKind,
|
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import {
|
|
||||||
getItemsApi,
|
|
||||||
getSuggestionsApi,
|
|
||||||
getTvShowsApi,
|
|
||||||
getUserLibraryApi,
|
|
||||||
getUserViewsApi,
|
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import NetInfo from "@react-native-community/netinfo";
|
|
||||||
import { QueryFunction, useQuery } from "@tanstack/react-query";
|
|
||||||
import { useNavigation, useRouter } from "expo-router";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
RefreshControl,
|
|
||||||
ScrollView,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
|
|
||||||
type ScrollingCollectionListSection = {
|
|
||||||
type: "ScrollingCollectionList";
|
|
||||||
title?: string;
|
|
||||||
queryKey: (string | undefined | null)[];
|
|
||||||
queryFn: QueryFunction<BaseItemDto[]>;
|
|
||||||
orientation?: "horizontal" | "vertical";
|
|
||||||
};
|
|
||||||
|
|
||||||
type MediaListSection = {
|
|
||||||
type: "MediaListSection";
|
|
||||||
queryKey: (string | undefined)[];
|
|
||||||
queryFn: QueryFunction<BaseItemDto>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Section = ScrollingCollectionListSection | MediaListSection;
|
|
||||||
|
|
||||||
export const SettingsIndex = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const api = useAtomValue(apiAtom);
|
|
||||||
const user = useAtomValue(userAtom);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [
|
|
||||||
settings,
|
|
||||||
updateSettings,
|
|
||||||
pluginSettings,
|
|
||||||
setPluginSettings,
|
|
||||||
refreshStreamyfinPluginSettings,
|
|
||||||
] = useSettings();
|
|
||||||
|
|
||||||
const [isConnected, setIsConnected] = useState<boolean | null>(null);
|
|
||||||
const [loadingRetry, setLoadingRetry] = useState(false);
|
|
||||||
|
|
||||||
const navigation = useNavigation();
|
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
|
|
||||||
const { downloadedFiles, cleanCacheDirectory } = useDownload();
|
|
||||||
useEffect(() => {
|
|
||||||
const hasDownloads = downloadedFiles && downloadedFiles.length > 0;
|
|
||||||
navigation.setOptions({
|
|
||||||
headerLeft: () => (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
router.push("/(auth)/downloads");
|
|
||||||
}}
|
|
||||||
className="p-2"
|
|
||||||
>
|
|
||||||
<Feather
|
|
||||||
name="download"
|
|
||||||
color={hasDownloads ? Colors.primary : "white"}
|
|
||||||
size={22}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}, [downloadedFiles, navigation, router]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
cleanCacheDirectory().catch((e) =>
|
|
||||||
console.error("Something went wrong cleaning cache directory")
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const checkConnection = useCallback(async () => {
|
|
||||||
setLoadingRetry(true);
|
|
||||||
const state = await NetInfo.fetch();
|
|
||||||
setIsConnected(state.isConnected);
|
|
||||||
setLoadingRetry(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = NetInfo.addEventListener((state) => {
|
|
||||||
if (state.isConnected == false || state.isInternetReachable === false)
|
|
||||||
setIsConnected(false);
|
|
||||||
else setIsConnected(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
NetInfo.fetch().then((state) => {
|
|
||||||
setIsConnected(state.isConnected);
|
|
||||||
});
|
|
||||||
|
|
||||||
// cleanCacheDirectory().catch((e) =>
|
|
||||||
// console.error("Something went wrong cleaning cache directory")
|
|
||||||
// );
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
isError: e1,
|
|
||||||
isLoading: l1,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["home", "userViews", user?.Id],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api || !user?.Id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await getUserViewsApi(api).getUserViews({
|
|
||||||
userId: user.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data.Items || null;
|
|
||||||
},
|
|
||||||
enabled: !!api && !!user?.Id,
|
|
||||||
staleTime: 60 * 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const userViews = useMemo(
|
|
||||||
() => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)),
|
|
||||||
[data, settings?.hiddenLibraries]
|
|
||||||
);
|
|
||||||
|
|
||||||
const collections = useMemo(() => {
|
|
||||||
const allow = ["movies", "tvshows"];
|
|
||||||
return (
|
|
||||||
userViews?.filter(
|
|
||||||
(c) => c.CollectionType && allow.includes(c.CollectionType)
|
|
||||||
) || []
|
|
||||||
);
|
|
||||||
}, [userViews]);
|
|
||||||
|
|
||||||
const invalidateCache = useInvalidatePlaybackProgressCache();
|
|
||||||
|
|
||||||
const refetch = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
await refreshStreamyfinPluginSettings();
|
|
||||||
await invalidateCache();
|
|
||||||
setLoading(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const createCollectionConfig = useCallback(
|
|
||||||
(
|
|
||||||
title: string,
|
|
||||||
queryKey: string[],
|
|
||||||
includeItemTypes: BaseItemKind[],
|
|
||||||
parentId: string | undefined
|
|
||||||
): ScrollingCollectionListSection => ({
|
|
||||||
title,
|
|
||||||
queryKey,
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api) return [];
|
|
||||||
return (
|
|
||||||
(
|
|
||||||
await getUserLibraryApi(api).getLatestMedia({
|
|
||||||
userId: user?.Id,
|
|
||||||
limit: 20,
|
|
||||||
fields: ["PrimaryImageAspectRatio", "Path"],
|
|
||||||
imageTypeLimit: 1,
|
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
|
||||||
includeItemTypes,
|
|
||||||
parentId,
|
|
||||||
})
|
|
||||||
).data || []
|
|
||||||
);
|
|
||||||
},
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
}),
|
|
||||||
[api, user?.Id]
|
|
||||||
);
|
|
||||||
|
|
||||||
let sections: Section[] = [];
|
|
||||||
if (!settings?.home || !settings?.home?.sections) {
|
|
||||||
sections = useMemo(() => {
|
|
||||||
if (!api || !user?.Id) return [];
|
|
||||||
|
|
||||||
const latestMediaViews = collections.map((c) => {
|
|
||||||
const includeItemTypes: BaseItemKind[] =
|
|
||||||
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
|
|
||||||
const title = t("home.recently_added_in", { libraryName: c.Name });
|
|
||||||
const queryKey = [
|
|
||||||
"home",
|
|
||||||
"recentlyAddedIn" + c.CollectionType,
|
|
||||||
user?.Id!,
|
|
||||||
c.Id!,
|
|
||||||
];
|
|
||||||
return createCollectionConfig(
|
|
||||||
title || "",
|
|
||||||
queryKey,
|
|
||||||
includeItemTypes,
|
|
||||||
c.Id
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ss: Section[] = [
|
|
||||||
{
|
|
||||||
title: t("home.continue_watching"),
|
|
||||||
queryKey: ["home", "resumeItems"],
|
|
||||||
queryFn: async () =>
|
|
||||||
(
|
|
||||||
await getItemsApi(api).getResumeItems({
|
|
||||||
userId: user.Id,
|
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
|
||||||
includeItemTypes: ["Movie", "Series", "Episode"],
|
|
||||||
})
|
|
||||||
).data.Items || [],
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: "horizontal",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("home.next_up"),
|
|
||||||
queryKey: ["home", "nextUp-all"],
|
|
||||||
queryFn: async () =>
|
|
||||||
(
|
|
||||||
await getTvShowsApi(api).getNextUp({
|
|
||||||
userId: user?.Id,
|
|
||||||
fields: ["MediaSourceCount"],
|
|
||||||
limit: 20,
|
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
|
||||||
enableResumable: false,
|
|
||||||
})
|
|
||||||
).data.Items || [],
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: "horizontal",
|
|
||||||
},
|
|
||||||
...latestMediaViews,
|
|
||||||
// ...(mediaListCollections?.map(
|
|
||||||
// (ml) =>
|
|
||||||
// ({
|
|
||||||
// title: ml.Name,
|
|
||||||
// queryKey: ["home", "mediaList", ml.Id!],
|
|
||||||
// queryFn: async () => ml,
|
|
||||||
// type: "MediaListSection",
|
|
||||||
// orientation: "vertical",
|
|
||||||
// } as Section)
|
|
||||||
// ) || []),
|
|
||||||
{
|
|
||||||
title: t("home.suggested_movies"),
|
|
||||||
queryKey: ["home", "suggestedMovies", user?.Id],
|
|
||||||
queryFn: async () =>
|
|
||||||
(
|
|
||||||
await getSuggestionsApi(api).getSuggestions({
|
|
||||||
userId: user?.Id,
|
|
||||||
limit: 10,
|
|
||||||
mediaType: ["Video"],
|
|
||||||
type: ["Movie"],
|
|
||||||
})
|
|
||||||
).data.Items || [],
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: "vertical",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("home.suggested_episodes"),
|
|
||||||
queryKey: ["home", "suggestedEpisodes", user?.Id],
|
|
||||||
queryFn: async () => {
|
|
||||||
try {
|
|
||||||
const suggestions = await getSuggestions(api, user.Id);
|
|
||||||
const nextUpPromises = suggestions.map((series) =>
|
|
||||||
getNextUp(api, user.Id, series.Id)
|
|
||||||
);
|
|
||||||
const nextUpResults = await Promise.all(nextUpPromises);
|
|
||||||
|
|
||||||
return nextUpResults.filter((item) => item !== null) || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching data:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: "horizontal",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return ss;
|
|
||||||
}, [api, user?.Id, collections]);
|
|
||||||
} else {
|
|
||||||
sections = useMemo(() => {
|
|
||||||
if (!api || !user?.Id) return [];
|
|
||||||
const ss: Section[] = [];
|
|
||||||
|
|
||||||
for (const key in settings.home?.sections) {
|
|
||||||
// @ts-expect-error
|
|
||||||
const section = settings.home?.sections[key];
|
|
||||||
const id = section.title || key;
|
|
||||||
ss.push({
|
|
||||||
title: id,
|
|
||||||
queryKey: ["home", id],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (section.items) {
|
|
||||||
const response = await getItemsApi(api).getItems({
|
|
||||||
userId: user?.Id,
|
|
||||||
limit: section.items?.limit || 25,
|
|
||||||
recursive: true,
|
|
||||||
includeItemTypes: section.items?.includeItemTypes,
|
|
||||||
sortBy: section.items?.sortBy,
|
|
||||||
sortOrder: section.items?.sortOrder,
|
|
||||||
filters: section.items?.filters,
|
|
||||||
parentId: section.items?.parentId,
|
|
||||||
});
|
|
||||||
return response.data.Items || [];
|
|
||||||
} else if (section.nextUp) {
|
|
||||||
const response = await getTvShowsApi(api).getNextUp({
|
|
||||||
userId: user?.Id,
|
|
||||||
fields: ["MediaSourceCount"],
|
|
||||||
limit: section.items?.limit || 25,
|
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
|
||||||
enableResumable: section.items?.enableResumable || false,
|
|
||||||
enableRewatching: section.items?.enableRewatching || false,
|
|
||||||
});
|
|
||||||
return response.data.Items || [];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: section?.orientation || "vertical",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return ss;
|
|
||||||
}, [api, user?.Id, settings.home?.sections]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isConnected === false) {
|
|
||||||
return (
|
|
||||||
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
|
|
||||||
<Text className="text-3xl font-bold mb-2">{t("home.no_internet")}</Text>
|
|
||||||
<Text className="text-center opacity-70">
|
|
||||||
{t("home.no_internet_message")}
|
|
||||||
</Text>
|
|
||||||
<View className="mt-4">
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
onPress={() => router.push("/(auth)/downloads")}
|
|
||||||
justify="center"
|
|
||||||
iconRight={
|
|
||||||
<Ionicons name="arrow-forward" size={20} color="white" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("home.go_to_downloads")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="black"
|
|
||||||
onPress={() => {
|
|
||||||
checkConnection();
|
|
||||||
}}
|
|
||||||
justify="center"
|
|
||||||
className="mt-2"
|
|
||||||
iconRight={
|
|
||||||
loadingRetry ? null : (
|
|
||||||
<Ionicons name="refresh" size={20} color="white" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loadingRetry ? (
|
|
||||||
<ActivityIndicator size={"small"} color={"white"} />
|
|
||||||
) : (
|
|
||||||
"Retry"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e1)
|
|
||||||
return (
|
|
||||||
<View className="flex flex-col items-center justify-center h-full -mt-6">
|
|
||||||
<Text className="text-3xl font-bold mb-2">{t("home.oops")}</Text>
|
|
||||||
<Text className="text-center opacity-70">
|
|
||||||
{t("home.error_message")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (l1)
|
|
||||||
return (
|
|
||||||
<View className="justify-center items-center h-full">
|
|
||||||
<Loader />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView
|
|
||||||
nestedScrollEnabled
|
|
||||||
contentInsetAdjustmentBehavior="automatic"
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={loading} onRefresh={refetch} />
|
|
||||||
}
|
|
||||||
contentContainerStyle={{
|
|
||||||
paddingLeft: insets.left,
|
|
||||||
paddingRight: insets.right,
|
|
||||||
paddingBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View className="flex flex-col space-y-4">
|
|
||||||
<LargeMovieCarousel />
|
|
||||||
|
|
||||||
{sections.map((section, index) => {
|
|
||||||
if (section.type === "ScrollingCollectionList") {
|
|
||||||
return (
|
|
||||||
<ScrollingCollectionList
|
|
||||||
key={index}
|
|
||||||
title={section.title}
|
|
||||||
queryKey={section.queryKey}
|
|
||||||
queryFn={section.queryFn}
|
|
||||||
orientation={section.orientation}
|
|
||||||
hideIfEmpty
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (section.type === "MediaListSection") {
|
|
||||||
return (
|
|
||||||
<MediaListSection
|
|
||||||
key={index}
|
|
||||||
queryKey={section.queryKey}
|
|
||||||
queryFn={section.queryFn}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to get suggestions
|
|
||||||
async function getSuggestions(api: Api, userId: string | undefined) {
|
|
||||||
if (!userId) return [];
|
|
||||||
const response = await getSuggestionsApi(api).getSuggestions({
|
|
||||||
userId,
|
|
||||||
limit: 10,
|
|
||||||
mediaType: ["Unknown"],
|
|
||||||
type: ["Series"],
|
|
||||||
});
|
|
||||||
return response.data.Items ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to get the next up TV show for a series
|
|
||||||
async function getNextUp(
|
|
||||||
api: Api,
|
|
||||||
userId: string | undefined,
|
|
||||||
seriesId: string | undefined
|
|
||||||
) {
|
|
||||||
if (!userId || !seriesId) return null;
|
|
||||||
const response = await getTvShowsApi(api).getNextUp({
|
|
||||||
userId,
|
|
||||||
seriesId,
|
|
||||||
limit: 1,
|
|
||||||
});
|
|
||||||
return response.data.Items?.[0] ?? null;
|
|
||||||
}
|
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
import { Button } from "@/components/Button";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel";
|
|
||||||
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
|
|
||||||
import { Loader } from "@/components/Loader";
|
|
||||||
import { MediaListSection } from "@/components/medialists/MediaListSection";
|
|
||||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import { Api } from "@jellyfin/sdk";
|
|
||||||
import {
|
|
||||||
BaseItemDto,
|
|
||||||
BaseItemKind,
|
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import {
|
|
||||||
getItemsApi,
|
|
||||||
getSuggestionsApi,
|
|
||||||
getTvShowsApi,
|
|
||||||
getUserLibraryApi,
|
|
||||||
getUserViewsApi,
|
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
|
||||||
import NetInfo from "@react-native-community/netinfo";
|
|
||||||
import { QueryFunction, useQuery } from "@tanstack/react-query";
|
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
RefreshControl,
|
|
||||||
ScrollView,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
|
|
||||||
type ScrollingCollectionListSection = {
|
|
||||||
type: "ScrollingCollectionList";
|
|
||||||
title?: string;
|
|
||||||
queryKey: (string | undefined | null)[];
|
|
||||||
queryFn: QueryFunction<BaseItemDto[]>;
|
|
||||||
orientation?: "horizontal" | "vertical";
|
|
||||||
};
|
|
||||||
|
|
||||||
type MediaListSection = {
|
|
||||||
type: "MediaListSection";
|
|
||||||
queryKey: (string | undefined)[];
|
|
||||||
queryFn: QueryFunction<BaseItemDto>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Section = ScrollingCollectionListSection | MediaListSection;
|
|
||||||
|
|
||||||
export const SettingsIndex = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const api = useAtomValue(apiAtom);
|
|
||||||
const user = useAtomValue(userAtom);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [
|
|
||||||
settings,
|
|
||||||
updateSettings,
|
|
||||||
pluginSettings,
|
|
||||||
setPluginSettings,
|
|
||||||
refreshStreamyfinPluginSettings,
|
|
||||||
] = useSettings();
|
|
||||||
|
|
||||||
const [isConnected, setIsConnected] = useState<boolean | null>(null);
|
|
||||||
const [loadingRetry, setLoadingRetry] = useState(false);
|
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
|
|
||||||
const checkConnection = useCallback(async () => {
|
|
||||||
setLoadingRetry(true);
|
|
||||||
const state = await NetInfo.fetch();
|
|
||||||
setIsConnected(state.isConnected);
|
|
||||||
setLoadingRetry(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = NetInfo.addEventListener((state) => {
|
|
||||||
if (state.isConnected == false || state.isInternetReachable === false)
|
|
||||||
setIsConnected(false);
|
|
||||||
else setIsConnected(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
NetInfo.fetch().then((state) => {
|
|
||||||
setIsConnected(state.isConnected);
|
|
||||||
});
|
|
||||||
|
|
||||||
// cleanCacheDirectory().catch((e) =>
|
|
||||||
// console.error("Something went wrong cleaning cache directory")
|
|
||||||
// );
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
isError: e1,
|
|
||||||
isLoading: l1,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["home", "userViews", user?.Id],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api || !user?.Id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await getUserViewsApi(api).getUserViews({
|
|
||||||
userId: user.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data.Items || null;
|
|
||||||
},
|
|
||||||
enabled: !!api && !!user?.Id,
|
|
||||||
staleTime: 60 * 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const userViews = useMemo(
|
|
||||||
() => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)),
|
|
||||||
[data, settings?.hiddenLibraries]
|
|
||||||
);
|
|
||||||
|
|
||||||
const collections = useMemo(() => {
|
|
||||||
const allow = ["movies", "tvshows"];
|
|
||||||
return (
|
|
||||||
userViews?.filter(
|
|
||||||
(c) => c.CollectionType && allow.includes(c.CollectionType)
|
|
||||||
) || []
|
|
||||||
);
|
|
||||||
}, [userViews]);
|
|
||||||
|
|
||||||
const invalidateCache = useInvalidatePlaybackProgressCache();
|
|
||||||
|
|
||||||
const refetch = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
await refreshStreamyfinPluginSettings();
|
|
||||||
await invalidateCache();
|
|
||||||
setLoading(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const createCollectionConfig = useCallback(
|
|
||||||
(
|
|
||||||
title: string,
|
|
||||||
queryKey: string[],
|
|
||||||
includeItemTypes: BaseItemKind[],
|
|
||||||
parentId: string | undefined
|
|
||||||
): ScrollingCollectionListSection => ({
|
|
||||||
title,
|
|
||||||
queryKey,
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!api) return [];
|
|
||||||
return (
|
|
||||||
(
|
|
||||||
await getUserLibraryApi(api).getLatestMedia({
|
|
||||||
userId: user?.Id,
|
|
||||||
limit: 20,
|
|
||||||
fields: ["PrimaryImageAspectRatio", "Path"],
|
|
||||||
imageTypeLimit: 1,
|
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
|
||||||
includeItemTypes,
|
|
||||||
parentId,
|
|
||||||
})
|
|
||||||
).data || []
|
|
||||||
);
|
|
||||||
},
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
}),
|
|
||||||
[api, user?.Id]
|
|
||||||
);
|
|
||||||
|
|
||||||
let sections: Section[] = [];
|
|
||||||
if (!settings?.home || !settings?.home?.sections) {
|
|
||||||
sections = useMemo(() => {
|
|
||||||
if (!api || !user?.Id) return [];
|
|
||||||
|
|
||||||
const latestMediaViews = collections.map((c) => {
|
|
||||||
const includeItemTypes: BaseItemKind[] =
|
|
||||||
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
|
|
||||||
const title = t("home.recently_added_in", { libraryName: c.Name });
|
|
||||||
const queryKey = [
|
|
||||||
"home",
|
|
||||||
"recentlyAddedIn" + c.CollectionType,
|
|
||||||
user?.Id!,
|
|
||||||
c.Id!,
|
|
||||||
];
|
|
||||||
return createCollectionConfig(
|
|
||||||
title || "",
|
|
||||||
queryKey,
|
|
||||||
includeItemTypes,
|
|
||||||
c.Id
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ss: Section[] = [
|
|
||||||
{
|
|
||||||
title: t("home.continue_watching"),
|
|
||||||
queryKey: ["home", "resumeItems"],
|
|
||||||
queryFn: async () =>
|
|
||||||
(
|
|
||||||
await getItemsApi(api).getResumeItems({
|
|
||||||
userId: user.Id,
|
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
|
||||||
includeItemTypes: ["Movie", "Series", "Episode"],
|
|
||||||
})
|
|
||||||
).data.Items || [],
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: "horizontal",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("home.next_up"),
|
|
||||||
queryKey: ["home", "nextUp-all"],
|
|
||||||
queryFn: async () =>
|
|
||||||
(
|
|
||||||
await getTvShowsApi(api).getNextUp({
|
|
||||||
userId: user?.Id,
|
|
||||||
fields: ["MediaSourceCount"],
|
|
||||||
limit: 20,
|
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
|
||||||
enableResumable: false,
|
|
||||||
})
|
|
||||||
).data.Items || [],
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: "horizontal",
|
|
||||||
},
|
|
||||||
...latestMediaViews,
|
|
||||||
// ...(mediaListCollections?.map(
|
|
||||||
// (ml) =>
|
|
||||||
// ({
|
|
||||||
// title: ml.Name,
|
|
||||||
// queryKey: ["home", "mediaList", ml.Id!],
|
|
||||||
// queryFn: async () => ml,
|
|
||||||
// type: "MediaListSection",
|
|
||||||
// orientation: "vertical",
|
|
||||||
// } as Section)
|
|
||||||
// ) || []),
|
|
||||||
{
|
|
||||||
title: t("home.suggested_movies"),
|
|
||||||
queryKey: ["home", "suggestedMovies", user?.Id],
|
|
||||||
queryFn: async () =>
|
|
||||||
(
|
|
||||||
await getSuggestionsApi(api).getSuggestions({
|
|
||||||
userId: user?.Id,
|
|
||||||
limit: 10,
|
|
||||||
mediaType: ["Video"],
|
|
||||||
type: ["Movie"],
|
|
||||||
})
|
|
||||||
).data.Items || [],
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: "vertical",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("home.suggested_episodes"),
|
|
||||||
queryKey: ["home", "suggestedEpisodes", user?.Id],
|
|
||||||
queryFn: async () => {
|
|
||||||
try {
|
|
||||||
const suggestions = await getSuggestions(api, user.Id);
|
|
||||||
const nextUpPromises = suggestions.map((series) =>
|
|
||||||
getNextUp(api, user.Id, series.Id)
|
|
||||||
);
|
|
||||||
const nextUpResults = await Promise.all(nextUpPromises);
|
|
||||||
|
|
||||||
return nextUpResults.filter((item) => item !== null) || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching data:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: "horizontal",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return ss;
|
|
||||||
}, [api, user?.Id, collections]);
|
|
||||||
} else {
|
|
||||||
sections = useMemo(() => {
|
|
||||||
if (!api || !user?.Id) return [];
|
|
||||||
const ss: Section[] = [];
|
|
||||||
|
|
||||||
for (const key in settings.home?.sections) {
|
|
||||||
// @ts-expect-error
|
|
||||||
const section = settings.home?.sections[key];
|
|
||||||
const id = section.title || key;
|
|
||||||
ss.push({
|
|
||||||
title: id,
|
|
||||||
queryKey: ["home", id],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (section.items) {
|
|
||||||
const response = await getItemsApi(api).getItems({
|
|
||||||
userId: user?.Id,
|
|
||||||
limit: section.items?.limit || 25,
|
|
||||||
recursive: true,
|
|
||||||
includeItemTypes: section.items?.includeItemTypes,
|
|
||||||
sortBy: section.items?.sortBy,
|
|
||||||
sortOrder: section.items?.sortOrder,
|
|
||||||
filters: section.items?.filters,
|
|
||||||
parentId: section.items?.parentId,
|
|
||||||
});
|
|
||||||
return response.data.Items || [];
|
|
||||||
} else if (section.nextUp) {
|
|
||||||
const response = await getTvShowsApi(api).getNextUp({
|
|
||||||
userId: user?.Id,
|
|
||||||
fields: ["MediaSourceCount"],
|
|
||||||
limit: section.items?.limit || 25,
|
|
||||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
|
||||||
enableResumable: section.items?.enableResumable || false,
|
|
||||||
enableRewatching: section.items?.enableRewatching || false,
|
|
||||||
});
|
|
||||||
return response.data.Items || [];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
type: "ScrollingCollectionList",
|
|
||||||
orientation: section?.orientation || "vertical",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return ss;
|
|
||||||
}, [api, user?.Id, settings.home?.sections]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isConnected === false) {
|
|
||||||
return (
|
|
||||||
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
|
|
||||||
<Text className="text-3xl font-bold mb-2">{t("home.no_internet")}</Text>
|
|
||||||
<Text className="text-center opacity-70">
|
|
||||||
{t("home.no_internet_message")}
|
|
||||||
</Text>
|
|
||||||
<View className="mt-4">
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
onPress={() => router.push("/(auth)/downloads")}
|
|
||||||
justify="center"
|
|
||||||
iconRight={
|
|
||||||
<Ionicons name="arrow-forward" size={20} color="white" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("home.go_to_downloads")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="black"
|
|
||||||
onPress={() => {
|
|
||||||
checkConnection();
|
|
||||||
}}
|
|
||||||
justify="center"
|
|
||||||
className="mt-2"
|
|
||||||
iconRight={
|
|
||||||
loadingRetry ? null : (
|
|
||||||
<Ionicons name="refresh" size={20} color="white" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loadingRetry ? (
|
|
||||||
<ActivityIndicator size={"small"} color={"white"} />
|
|
||||||
) : (
|
|
||||||
"Retry"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e1)
|
|
||||||
return (
|
|
||||||
<View className="flex flex-col items-center justify-center h-full -mt-6">
|
|
||||||
<Text className="text-3xl font-bold mb-2">{t("home.oops")}</Text>
|
|
||||||
<Text className="text-center opacity-70">
|
|
||||||
{t("home.error_message")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (l1)
|
|
||||||
return (
|
|
||||||
<View className="justify-center items-center h-full">
|
|
||||||
<Loader />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView
|
|
||||||
nestedScrollEnabled
|
|
||||||
contentInsetAdjustmentBehavior="automatic"
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl refreshing={loading} onRefresh={refetch} />
|
|
||||||
}
|
|
||||||
contentContainerStyle={{
|
|
||||||
paddingLeft: insets.left,
|
|
||||||
paddingRight: insets.right,
|
|
||||||
paddingBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View className="flex flex-col space-y-4">
|
|
||||||
<LargeMovieCarousel />
|
|
||||||
|
|
||||||
{sections.map((section, index) => {
|
|
||||||
if (section.type === "ScrollingCollectionList") {
|
|
||||||
return (
|
|
||||||
<ScrollingCollectionList
|
|
||||||
key={index}
|
|
||||||
title={section.title}
|
|
||||||
queryKey={section.queryKey}
|
|
||||||
queryFn={section.queryFn}
|
|
||||||
orientation={section.orientation}
|
|
||||||
hideIfEmpty
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (section.type === "MediaListSection") {
|
|
||||||
return (
|
|
||||||
<MediaListSection
|
|
||||||
key={index}
|
|
||||||
queryKey={section.queryKey}
|
|
||||||
queryFn={section.queryFn}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to get suggestions
|
|
||||||
async function getSuggestions(api: Api, userId: string | undefined) {
|
|
||||||
if (!userId) return [];
|
|
||||||
const response = await getSuggestionsApi(api).getSuggestions({
|
|
||||||
userId,
|
|
||||||
limit: 10,
|
|
||||||
mediaType: ["Unknown"],
|
|
||||||
type: ["Series"],
|
|
||||||
});
|
|
||||||
return response.data.Items ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to get the next up TV show for a series
|
|
||||||
async function getNextUp(
|
|
||||||
api: Api,
|
|
||||||
userId: string | undefined,
|
|
||||||
seriesId: string | undefined
|
|
||||||
) {
|
|
||||||
if (!userId || !seriesId) return null;
|
|
||||||
const response = await getTvShowsApi(api).getNextUp({
|
|
||||||
userId,
|
|
||||||
seriesId,
|
|
||||||
limit: 1,
|
|
||||||
});
|
|
||||||
return response.data.Items?.[0] ?? null;
|
|
||||||
}
|
|
||||||
@@ -54,12 +54,12 @@ import AudioSlider from "./AudioSlider";
|
|||||||
import BrightnessSlider from "./BrightnessSlider";
|
import BrightnessSlider from "./BrightnessSlider";
|
||||||
import { ControlProvider } from "./contexts/ControlContext";
|
import { ControlProvider } from "./contexts/ControlContext";
|
||||||
import { VideoProvider } from "./contexts/VideoContext";
|
import { VideoProvider } from "./contexts/VideoContext";
|
||||||
import DropdownView from "./dropdown/DropdownView";
|
|
||||||
import { EpisodeList } from "./EpisodeList";
|
import { EpisodeList } from "./EpisodeList";
|
||||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||||
import SkipButton from "./SkipButton";
|
import SkipButton from "./SkipButton";
|
||||||
import { useControlsTimeout } from "./useControlsTimeout";
|
import { useControlsTimeout } from "./useControlsTimeout";
|
||||||
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
||||||
|
import DropdownView from "./dropdown/DropdownView";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -220,8 +220,13 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
stop();
|
stop();
|
||||||
|
|
||||||
|
if (!bitrateValue) {
|
||||||
|
// @ts-expect-error
|
||||||
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
router.replace(`player/transcoding-player?${queryParams}`);
|
||||||
}, [previousItem, settings, subtitleIndex, audioIndex]);
|
}, [previousItem, settings, subtitleIndex, audioIndex]);
|
||||||
|
|
||||||
const goToNextItem = useCallback(() => {
|
const goToNextItem = useCallback(() => {
|
||||||
@@ -255,8 +260,13 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
stop();
|
stop();
|
||||||
|
|
||||||
|
if (!bitrateValue) {
|
||||||
|
// @ts-expect-error
|
||||||
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
router.replace(`player/transcoding-player?${queryParams}`);
|
||||||
}, [nextItem, settings, subtitleIndex, audioIndex]);
|
}, [nextItem, settings, subtitleIndex, audioIndex]);
|
||||||
|
|
||||||
const updateTimes = useCallback(
|
const updateTimes = useCallback(
|
||||||
@@ -415,8 +425,13 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
stop();
|
stop();
|
||||||
|
|
||||||
|
if (!bitrateValue) {
|
||||||
|
// @ts-expect-error
|
||||||
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
router.replace(`player/transcoding-player?${queryParams}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in gotoEpisode:", error);
|
console.error("Error in gotoEpisode:", error);
|
||||||
}
|
}
|
||||||
@@ -554,10 +569,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
<View className="flex flex-row items-center space-x-2 ">
|
<View className="flex flex-row items-center space-x-2 ">
|
||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity onPress={startPictureInPicture}>
|
||||||
onPress={startPictureInPicture}
|
|
||||||
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="picture-in-picture"
|
name="picture-in-picture"
|
||||||
size={24}
|
size={24}
|
||||||
|
|||||||
@@ -449,23 +449,12 @@ export const useJellyseerr = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const jellyseerrRegion = useMemo(
|
|
||||||
() => jellyseerrUser?.settings?.region || "US",
|
|
||||||
[jellyseerrUser]
|
|
||||||
);
|
|
||||||
|
|
||||||
const jellyseerrLocale = useMemo(() => {
|
|
||||||
return jellyseerrUser?.settings?.locale || "en";
|
|
||||||
}, [jellyseerrUser]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jellyseerrApi,
|
jellyseerrApi,
|
||||||
jellyseerrUser,
|
jellyseerrUser,
|
||||||
setJellyseerrUser,
|
setJellyseerrUser,
|
||||||
clearAllJellyseerData,
|
clearAllJellyseerData,
|
||||||
isJellyseerrResult,
|
isJellyseerrResult,
|
||||||
jellyseerrRegion,
|
|
||||||
jellyseerrLocale,
|
|
||||||
requestMedia,
|
requestMedia,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
9
i18n.ts
9
i18n.ts
@@ -5,10 +5,7 @@ import de from "./translations/de.json";
|
|||||||
import en from "./translations/en.json";
|
import en from "./translations/en.json";
|
||||||
import es from "./translations/es.json";
|
import es from "./translations/es.json";
|
||||||
import fr from "./translations/fr.json";
|
import fr from "./translations/fr.json";
|
||||||
import nl from "./translations/nl.json";
|
|
||||||
import sv from "./translations/sv.json";
|
import sv from "./translations/sv.json";
|
||||||
import it from "./translations/it.json";
|
|
||||||
import zhTW from './translations/zh-TW.json';
|
|
||||||
import { getLocales } from "expo-localization";
|
import { getLocales } from "expo-localization";
|
||||||
|
|
||||||
export const APP_LANGUAGES = [
|
export const APP_LANGUAGES = [
|
||||||
@@ -16,10 +13,7 @@ export const APP_LANGUAGES = [
|
|||||||
{ label: "English", value: "en" },
|
{ label: "English", value: "en" },
|
||||||
{ label: "Español", value: "es" },
|
{ label: "Español", value: "es" },
|
||||||
{ label: "Français", value: "fr" },
|
{ label: "Français", value: "fr" },
|
||||||
{ label: "Nederlands", value: "nl" },
|
|
||||||
{ label: "Svenska", value: "sv" },
|
{ label: "Svenska", value: "sv" },
|
||||||
{ label: "Italiano", value: "it" },
|
|
||||||
{ label: "繁體中文", value: "zh-TW" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
@@ -29,10 +23,7 @@ i18n.use(initReactI18next).init({
|
|||||||
en: { translation: en },
|
en: { translation: en },
|
||||||
es: { translation: es },
|
es: { translation: es },
|
||||||
fr: { translation: fr },
|
fr: { translation: fr },
|
||||||
nl: { translation: nl },
|
|
||||||
sv: { translation: sv },
|
sv: { translation: sv },
|
||||||
it: { translation: it },
|
|
||||||
"zh-TW": { translation: zhTW },
|
|
||||||
},
|
},
|
||||||
|
|
||||||
lng: getLocales()[0].languageCode || "en",
|
lng: getLocales()[0].languageCode || "en",
|
||||||
|
|||||||
@@ -452,19 +452,11 @@ extension VlcPlayerView: SimpleAppLifecycleListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidEnterForeground() {
|
func applicationDidEnterForeground() {
|
||||||
logger.debug("Entering foreground, is player visible? \(self.vlc.getPlayerView().superview != nil)")
|
logger.debug("Entering foreground")
|
||||||
if !self.vlc.getPlayerView().isDescendant(of: self) {
|
if !self.vlc.getPlayerView().isDescendant(of: self) {
|
||||||
logger.debug("Player view is missing. Adding back as subview")
|
logger.debug("Player view is missing. Adding back as subview")
|
||||||
self.addSubview(self.vlc.getPlayerView())
|
self.addSubview(self.vlc.getPlayerView())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current solution to fixing black screen when re-entering application
|
|
||||||
if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true }, !self.vlc.isMediaPlaying() {
|
|
||||||
videoTrack.isSelected = false
|
|
||||||
videoTrack.isSelectedExclusively = true
|
|
||||||
self.vlc.player.play()
|
|
||||||
self.vlc.player.pause()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -27,6 +27,7 @@
|
|||||||
"@gorhom/bottom-sheet": "^5.1.0",
|
"@gorhom/bottom-sheet": "^5.1.0",
|
||||||
"@jellyfin/sdk": "^0.11.0",
|
"@jellyfin/sdk": "^0.11.0",
|
||||||
"@kesha-antonov/react-native-background-downloader": "3.2.6",
|
"@kesha-antonov/react-native-background-downloader": "3.2.6",
|
||||||
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/netinfo": "11.4.1",
|
"@react-native-community/netinfo": "11.4.1",
|
||||||
"@react-native-menu/menu": "^1.2.2",
|
"@react-native-menu/menu": "^1.2.2",
|
||||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||||
@@ -34,6 +35,9 @@
|
|||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@shopify/flash-list": "1.7.3",
|
"@shopify/flash-list": "1.7.3",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
|
"@types/lodash": "^4.17.15",
|
||||||
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"expo": "^52.0.31",
|
"expo": "^52.0.31",
|
||||||
@@ -58,7 +62,7 @@
|
|||||||
"expo-router": "~4.0.17",
|
"expo-router": "~4.0.17",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
"expo-sensors": "~14.0.2",
|
"expo-sensors": "~14.0.2",
|
||||||
"expo-splash-screen": "~0.29.22",
|
"expo-splash-screen": "~0.29.21",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-system-ui": "~4.0.8",
|
"expo-system-ui": "~4.0.8",
|
||||||
"expo-task-manager": "~12.0.5",
|
"expo-task-manager": "~12.0.5",
|
||||||
@@ -74,7 +78,7 @@
|
|||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
"react-native": "npm:react-native-tvos@~0.77.0-0",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "0.8.7",
|
"react-native-bottom-tabs": "0.8.6",
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
"react-native-compressor": "^1.10.3",
|
"react-native-compressor": "^1.10.3",
|
||||||
"react-native-country-flag": "^2.0.2",
|
"react-native-country-flag": "^2.0.2",
|
||||||
@@ -120,10 +124,7 @@
|
|||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"react-test-renderer": "19.0.0",
|
"react-test-renderer": "19.0.0",
|
||||||
"typescript": "~5.7.3",
|
"typescript": "~5.7.3"
|
||||||
"@types/lodash": "^4.17.15",
|
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
|
||||||
"@types/uuid": "^10.0.0"
|
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"expo": {
|
"expo": {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -18,13 +18,11 @@ import {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import BackGroundDownloader from "@kesha-antonov/react-native-background-downloader";
|
|
||||||
import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
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 { FileInfo } from "expo-file-system";
|
import { FileInfo } from "expo-file-system";
|
||||||
import Notifications from "expo-notifications";
|
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
@@ -38,6 +36,11 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { AppState, AppStateStatus, Platform } from "react-native";
|
import { AppState, AppStateStatus, Platform } from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { apiAtom } from "./JellyfinProvider";
|
import { apiAtom } from "./JellyfinProvider";
|
||||||
|
const BackGroundDownloader = !Platform.isTV
|
||||||
|
? (require("@kesha-antonov/react-native-background-downloader") as typeof import("@kesha-antonov/react-native-background-downloader"))
|
||||||
|
: null;
|
||||||
|
// import * as Notifications from "expo-notifications";
|
||||||
|
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
|
||||||
|
|
||||||
export type DownloadedItem = {
|
export type DownloadedItem = {
|
||||||
item: Partial<BaseItemDto>;
|
item: Partial<BaseItemDto>;
|
||||||
@@ -55,6 +58,8 @@ const DownloadContext = createContext<ReturnType<
|
|||||||
> | null>(null);
|
> | null>(null);
|
||||||
|
|
||||||
function useDownloadProvider() {
|
function useDownloadProvider() {
|
||||||
|
if (Platform.isTV) return;
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
@@ -742,8 +747,5 @@ export function useDownload() {
|
|||||||
if (context === null) {
|
if (context === null) {
|
||||||
throw new Error("useDownload must be used within a DownloadProvider");
|
throw new Error("useDownload must be used within a DownloadProvider");
|
||||||
}
|
}
|
||||||
if (Platform.isTV) {
|
|
||||||
throw new Error("useDownload is not supported on TVOS");
|
|
||||||
}
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
import { storage } from "@/utils/mmkv";
|
|
||||||
import { JobStatus } from "@/utils/optimize-server";
|
|
||||||
import {
|
|
||||||
BaseItemDto,
|
|
||||||
MediaSourceInfo,
|
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import * as Application from "expo-application";
|
|
||||||
import * as FileSystem from "expo-file-system";
|
|
||||||
import { atom, useAtom } from "jotai";
|
|
||||||
import React, { createContext, useCallback, useContext, useMemo } from "react";
|
|
||||||
|
|
||||||
export type DownloadedItem = {
|
|
||||||
item: Partial<BaseItemDto>;
|
|
||||||
mediaSource: MediaSourceInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const processesAtom = atom<JobStatus[]>([]);
|
|
||||||
|
|
||||||
const DownloadContext = createContext<ReturnType<
|
|
||||||
typeof useDownloadProvider
|
|
||||||
> | null>(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dummy download provider for tvOS
|
|
||||||
*/
|
|
||||||
function useDownloadProvider() {
|
|
||||||
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
|
||||||
|
|
||||||
const downloadedFiles: DownloadedItem[] = [];
|
|
||||||
|
|
||||||
const removeProcess = useCallback(async (id: string) => {}, []);
|
|
||||||
|
|
||||||
const startDownload = useCallback(async (process: JobStatus) => {
|
|
||||||
return null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const startBackgroundDownload = useCallback(
|
|
||||||
async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteAllFiles = async (): Promise<void> => {};
|
|
||||||
|
|
||||||
const deleteFile = async (id: string): Promise<void> => {};
|
|
||||||
|
|
||||||
const deleteItems = async (items: BaseItemDto[]) => {};
|
|
||||||
|
|
||||||
const cleanCacheDirectory = async () => {};
|
|
||||||
|
|
||||||
const deleteFileByType = async (type: BaseItemDto["Type"]) => {};
|
|
||||||
|
|
||||||
const appSizeUsage = useMemo(async () => {
|
|
||||||
return 0;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function getDownloadedItem(itemId: string): DownloadedItem | null {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveDownloadedItemInfo(item: BaseItemDto, size: number = 0) {}
|
|
||||||
|
|
||||||
function getDownloadedItemSize(itemId: string): number {
|
|
||||||
const size = storage.getString("downloadedItemSize-" + itemId);
|
|
||||||
return size ? parseInt(size) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
processes,
|
|
||||||
startBackgroundDownload,
|
|
||||||
downloadedFiles,
|
|
||||||
deleteAllFiles,
|
|
||||||
deleteFile,
|
|
||||||
deleteItems,
|
|
||||||
saveDownloadedItemInfo,
|
|
||||||
removeProcess,
|
|
||||||
setProcesses,
|
|
||||||
startDownload,
|
|
||||||
getDownloadedItem,
|
|
||||||
deleteFileByType,
|
|
||||||
appSizeUsage,
|
|
||||||
getDownloadedItemSize,
|
|
||||||
APP_CACHE_DOWNLOAD_DIRECTORY,
|
|
||||||
cleanCacheDirectory,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DownloadProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const downloadProviderValue = useDownloadProvider();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DownloadContext.Provider value={downloadProviderValue}>
|
|
||||||
{children}
|
|
||||||
</DownloadContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDownload() {
|
|
||||||
const context = useContext(DownloadContext);
|
|
||||||
if (context === null) {
|
|
||||||
throw new Error("useDownload must be used within a DownloadProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import "@/augmentations";
|
import "@/augmentations";
|
||||||
import { useInterval } from "@/hooks/useInterval";
|
import { useInterval } from "@/hooks/useInterval";
|
||||||
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
import { Api, Jellyfin } from "@jellyfin/sdk";
|
import { Api, Jellyfin } from "@jellyfin/sdk";
|
||||||
import { UserDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { UserDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
@@ -9,7 +7,6 @@ import { getUserApi } from "@jellyfin/sdk/lib/utils/api";
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import { router, useSegments } from "expo-router";
|
import { router, useSegments } from "expo-router";
|
||||||
import * as SplashScreen from "expo-splash-screen";
|
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -20,10 +17,16 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { getDeviceName } from "react-native-device-info";
|
|
||||||
import uuid from "react-native-uuid";
|
import uuid from "react-native-uuid";
|
||||||
|
import { getDeviceName } from "react-native-device-info";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
|
import {
|
||||||
|
useSplashScreenLoading,
|
||||||
|
useSplashScreenVisible,
|
||||||
|
} from "./SplashScreenProvider";
|
||||||
|
|
||||||
interface Server {
|
interface Server {
|
||||||
address: string;
|
address: string;
|
||||||
@@ -85,6 +88,22 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
] = useSettings();
|
] = useSettings();
|
||||||
const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr();
|
const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr();
|
||||||
|
|
||||||
|
useQuery({
|
||||||
|
queryKey: ["user", api],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!api) return null;
|
||||||
|
const response = await getUserApi(api).getCurrentUser();
|
||||||
|
if (response.data) setUser(response.data);
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
enabled: !!api,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchInterval: 1000 * 60,
|
||||||
|
refetchIntervalInBackground: true,
|
||||||
|
refetchOnMount: true,
|
||||||
|
refetchOnReconnect: true,
|
||||||
|
});
|
||||||
|
|
||||||
const headers = useMemo(() => {
|
const headers = useMemo(() => {
|
||||||
if (!deviceId) return {};
|
if (!deviceId) return {};
|
||||||
return {
|
return {
|
||||||
@@ -160,13 +179,14 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
}
|
}
|
||||||
}, [api, secret, headers]);
|
}, [api, secret, headers]);
|
||||||
|
|
||||||
|
useInterval(pollQuickConnect, isPolling ? 1000 : null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
await refreshStreamyfinPluginSettings();
|
await refreshStreamyfinPluginSettings();
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useInterval(pollQuickConnect, isPolling ? 1000 : null);
|
|
||||||
useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min
|
useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min
|
||||||
|
|
||||||
const discoverServers = async (url: string): Promise<Server[]> => {
|
const discoverServers = async (url: string): Promise<Server[]> => {
|
||||||
@@ -283,7 +303,6 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
storage.delete("token");
|
storage.delete("token");
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setApi(null);
|
|
||||||
setPluginSettings(undefined);
|
setPluginSettings(undefined);
|
||||||
await clearAllJellyseerData();
|
await clearAllJellyseerData();
|
||||||
},
|
},
|
||||||
@@ -292,44 +311,33 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
const { isLoading, isFetching } = useQuery({
|
||||||
const [initialLoaded, setInitialLoaded] = useState(false);
|
queryKey: [
|
||||||
|
"initializeJellyfin",
|
||||||
useEffect(() => {
|
user?.Id,
|
||||||
if (initialLoaded) {
|
api?.basePath,
|
||||||
setLoaded(true);
|
jellyfin?.clientInfo,
|
||||||
}
|
],
|
||||||
}, [initialLoaded]);
|
queryFn: async () => {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initializeJellyfin = async () => {
|
|
||||||
if (!jellyfin) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = getTokenFromStorage();
|
const token = getTokenFromStorage();
|
||||||
const serverUrl = getServerUrlFromStorage();
|
const serverUrl = getServerUrlFromStorage();
|
||||||
const storedUser = getUserFromStorage();
|
const user = getUserFromStorage();
|
||||||
|
if (serverUrl && token && user?.Id && jellyfin) {
|
||||||
if (serverUrl && token) {
|
|
||||||
const apiInstance = jellyfin.createApi(serverUrl, token);
|
const apiInstance = jellyfin.createApi(serverUrl, token);
|
||||||
setApi(apiInstance);
|
setApi(apiInstance);
|
||||||
|
setUser(user);
|
||||||
if (storedUser?.Id) {
|
|
||||||
setUser(storedUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await getUserApi(apiInstance).getCurrentUser();
|
|
||||||
setUser(response.data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
return false;
|
||||||
setInitialLoaded(true);
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
staleTime: 0,
|
||||||
initializeJellyfin();
|
enabled: !user?.Id || !api || !jellyfin,
|
||||||
}, [jellyfin]);
|
});
|
||||||
|
|
||||||
const contextValue: JellyfinContextValue = {
|
const contextValue: JellyfinContextValue = {
|
||||||
discoverServers,
|
discoverServers,
|
||||||
@@ -341,17 +349,17 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
initiateQuickConnect,
|
initiateQuickConnect,
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
let isLoadingOrFetching = isLoading || isFetching;
|
||||||
if (loaded) {
|
useProtectedRoute(user, isLoadingOrFetching);
|
||||||
SplashScreen.hideAsync();
|
|
||||||
}
|
|
||||||
}, [loaded]);
|
|
||||||
|
|
||||||
useProtectedRoute(user, loaded);
|
// show splash screen until everything loaded
|
||||||
|
useSplashScreenLoading(isLoadingOrFetching);
|
||||||
|
const splashScreenVisible = useSplashScreenVisible();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JellyfinContext.Provider value={contextValue}>
|
<JellyfinContext.Provider value={contextValue}>
|
||||||
{children}
|
{/* don't render login page when loading and splash screen visible */}
|
||||||
|
{isLoadingOrFetching && splashScreenVisible ? undefined : children}
|
||||||
</JellyfinContext.Provider>
|
</JellyfinContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -363,24 +371,20 @@ export const useJellyfin = (): JellyfinContextValue => {
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
function useProtectedRoute(user: UserDto | null, loaded = false) {
|
function useProtectedRoute(user: UserDto | null, loading = false) {
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded === false) return;
|
if (loading) return;
|
||||||
|
|
||||||
console.log("Loaded", user);
|
|
||||||
|
|
||||||
const inAuthGroup = segments[0] === "(auth)";
|
const inAuthGroup = segments[0] === "(auth)";
|
||||||
|
|
||||||
if (!user?.Id && inAuthGroup) {
|
if (!user?.Id && inAuthGroup) {
|
||||||
console.log("Redirected to login");
|
|
||||||
router.replace("/login");
|
router.replace("/login");
|
||||||
} else if (user?.Id && !inAuthGroup) {
|
} else if (user?.Id && !inAuthGroup) {
|
||||||
console.log("Redirected to home");
|
|
||||||
router.replace("/(auth)/(tabs)/(home)/");
|
router.replace("/(auth)/(tabs)/(home)/");
|
||||||
}
|
}
|
||||||
}, [user, segments, loaded]);
|
}, [user, segments, loading]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTokenFromStorage(): string | null {
|
export function getTokenFromStorage(): string | null {
|
||||||
|
|||||||
103
providers/SplashScreenProvider.tsx
Normal file
103
providers/SplashScreenProvider.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
import * as SplashScreen from "expo-splash-screen";
|
||||||
|
|
||||||
|
type SplashScreenContextValue = {
|
||||||
|
registerLoadingComponent: () => () => void;
|
||||||
|
splashScreenVisible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SplashScreenContext = createContext<SplashScreenContextValue | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prevent splash screen from auto-hiding
|
||||||
|
void SplashScreen.preventAutoHideAsync();
|
||||||
|
|
||||||
|
export const SplashScreenProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [splashScreenVisible, setSplashScreenVisible] = useState(true);
|
||||||
|
const loadingComponentsCount = useRef(0);
|
||||||
|
const isHidingRef = useRef(false);
|
||||||
|
|
||||||
|
const hideScreenIfNoLoadingComponents = async () => {
|
||||||
|
if (loadingComponentsCount.current === 0 && !isHidingRef.current) {
|
||||||
|
try {
|
||||||
|
isHidingRef.current = true;
|
||||||
|
await SplashScreen.hideAsync();
|
||||||
|
setSplashScreenVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to hide splash screen:", error);
|
||||||
|
} finally {
|
||||||
|
isHidingRef.current = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerLoadingComponent = () => {
|
||||||
|
loadingComponentsCount.current += 1;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
loadingComponentsCount.current -= 1;
|
||||||
|
void hideScreenIfNoLoadingComponents();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue: SplashScreenContextValue = {
|
||||||
|
registerLoadingComponent,
|
||||||
|
splashScreenVisible,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SplashScreenContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</SplashScreenContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the Splash Screen until component is ready to be displayed.
|
||||||
|
*
|
||||||
|
* @param isLoading The loading state of the component
|
||||||
|
*
|
||||||
|
* ## Usage
|
||||||
|
* ```
|
||||||
|
* const isLoading = loadSomething()
|
||||||
|
* useSplashScreenLoading(isLoading) // splash screen visible until isLoading is false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useSplashScreenLoading(isLoading: boolean) {
|
||||||
|
const context = useContext(SplashScreenContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useSplashScreenLoading must be used within a SplashScreenProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
return context.registerLoadingComponent();
|
||||||
|
}
|
||||||
|
}, [isLoading]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the visibility of the Splash Screen.
|
||||||
|
* @returns the visibility of the Splash Screen
|
||||||
|
*/
|
||||||
|
export function useSplashScreenVisible() {
|
||||||
|
const context = useContext(SplashScreenContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useSplashScreenVisible must be used within a SplashScreenProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context.splashScreenVisible;
|
||||||
|
}
|
||||||
@@ -132,8 +132,7 @@
|
|||||||
"show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen",
|
"show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen",
|
||||||
"hide_libraries": "Bibliotheken ausblenden",
|
"hide_libraries": "Bibliotheken ausblenden",
|
||||||
"select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.",
|
"select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.",
|
||||||
"disable_haptic_feedback": "Haptisches Feedback deaktivieren",
|
"disable_haptic_feedback": "Haptisches Feedback deaktivieren"
|
||||||
"default_quality": "Standardqualität"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
@@ -355,7 +354,7 @@
|
|||||||
"index": "Index:"
|
"index": "Index:"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Als Nächstes",
|
"next_up": "Als nächstes",
|
||||||
"no_items_to_display": "Keine Elemente zum Anzeigen",
|
"no_items_to_display": "Keine Elemente zum Anzeigen",
|
||||||
"cast_and_crew": "Besetzung und Crew",
|
"cast_and_crew": "Besetzung und Crew",
|
||||||
"series": "Serien",
|
"series": "Serien",
|
||||||
|
|||||||
@@ -132,8 +132,7 @@
|
|||||||
"show_custom_menu_links": "Show Custom Menu Links",
|
"show_custom_menu_links": "Show Custom Menu Links",
|
||||||
"hide_libraries": "Hide Libraries",
|
"hide_libraries": "Hide Libraries",
|
||||||
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||||
"disable_haptic_feedback": "Disable Haptic Feedback",
|
"disable_haptic_feedback": "Disable Haptic Feedback"
|
||||||
"default_quality": "Default quality"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"audio_title": "Audio",
|
"audio_title": "Audio",
|
||||||
"set_audio_track": "Establecer pista del elemento anterior",
|
"set_audio_track": "Establecer pista de audio del elemento anterior",
|
||||||
"audio_language": "Idioma de audio",
|
"audio_language": "Idioma de audio",
|
||||||
"audio_hint": "Elige un idioma de audio por defecto.",
|
"audio_hint": "Elige un idioma de audio por defecto.",
|
||||||
"none": "Ninguno",
|
"none": "Ninguno",
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
"subtitle_title": "Subtítulos",
|
"subtitle_title": "Subtítulos",
|
||||||
"subtitle_language": "Idioma de subtítulos",
|
"subtitle_language": "Idioma de subtítulos",
|
||||||
"subtitle_mode": "Modo de subtítulos",
|
"subtitle_mode": "Modo de subtítulos",
|
||||||
"set_subtitle_track": "Establecer pista del elemento anterior",
|
"set_subtitle_track": "Establecer pista de subtítulos del elemento anterior",
|
||||||
"subtitle_size": "Tamaño de subtítulos",
|
"subtitle_size": "Tamaño de subtítulos",
|
||||||
"subtitle_hint": "Configurar preferencias de subtítulos.",
|
"subtitle_hint": "Configurar preferencias de subtítulos.",
|
||||||
"none": "Ninguno",
|
"none": "Ninguno",
|
||||||
@@ -132,8 +132,7 @@
|
|||||||
"show_custom_menu_links": "Mostrar enlaces de menú personalizados",
|
"show_custom_menu_links": "Mostrar enlaces de menú personalizados",
|
||||||
"hide_libraries": "Ocultar bibliotecas",
|
"hide_libraries": "Ocultar bibliotecas",
|
||||||
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
|
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
|
||||||
"disable_haptic_feedback": "Desactivar feedback háptico",
|
"disable_haptic_feedback": "Desactivar feedback háptico"
|
||||||
"default_quality": "Calidad por defecto"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Descargas",
|
"downloads_title": "Descargas",
|
||||||
|
|||||||
@@ -132,9 +132,7 @@
|
|||||||
"show_custom_menu_links": "Afficher les liens personnalisés",
|
"show_custom_menu_links": "Afficher les liens personnalisés",
|
||||||
"hide_libraries": "Cacher des bibliothèques",
|
"hide_libraries": "Cacher des bibliothèques",
|
||||||
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l’onglet Bibliothèque et les sections de la page d’accueil.",
|
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l’onglet Bibliothèque et les sections de la page d’accueil.",
|
||||||
"disable_haptic_feedback": "Désactiver le retour haptique",
|
"disable_haptic_feedback": "Désactiver le retour haptique"
|
||||||
"default_quality": "Qualité par défaut"
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Téléchargements",
|
"downloads_title": "Téléchargements",
|
||||||
|
|||||||
@@ -1,458 +0,0 @@
|
|||||||
{
|
|
||||||
"login": {
|
|
||||||
"username_required": "Nome utente è obbligatorio",
|
|
||||||
"error_title": "Errore",
|
|
||||||
"login_title": "Accesso",
|
|
||||||
"login_to_title": "Accedi a",
|
|
||||||
"username_placeholder": "Nome utente",
|
|
||||||
"password_placeholder": "Password",
|
|
||||||
"login_button": "Accedi",
|
|
||||||
"quick_connect": "Connessione Rapida",
|
|
||||||
"enter_code_to_login": "Inserire {{code}} per accedere",
|
|
||||||
"failed_to_initiate_quick_connect": "Impossibile avviare la Connessione Rapida",
|
|
||||||
"got_it": "Capito",
|
|
||||||
"connection_failed": "Connessione fallita",
|
|
||||||
"could_not_connect_to_server": "Impossibile connettersi al server. Controllare l'URL e la connessione di rete.",
|
|
||||||
"an_unexpected_error_occured": "Si è verificato un errore inaspettato",
|
|
||||||
"change_server": "Cambiare il server",
|
|
||||||
"invalid_username_or_password": "Nome utente o password non validi",
|
|
||||||
"user_does_not_have_permission_to_log_in": "L'utente non ha il permesso di accedere",
|
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "Il server sta impiegando troppo tempo per rispondere, riprovare più tardi",
|
|
||||||
"server_received_too_many_requests_try_again_later": "Il server ha ricevuto troppe richieste, riprovare più tardi.",
|
|
||||||
"there_is_a_server_error": "Si è verificato un errore del server",
|
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Si è verificato un errore imprevisto. L'URL del server è stato inserito correttamente?"
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"enter_url_to_jellyfin_server": "Inserisci l'URL del tuo server Jellyfin",
|
|
||||||
"server_url_placeholder": "http(s)://tuo-server.com",
|
|
||||||
"connect_button": "Connetti",
|
|
||||||
"previous_servers": "server precedente",
|
|
||||||
"clear_button": "Cancella",
|
|
||||||
"search_for_local_servers": "Ricerca dei server locali",
|
|
||||||
"searching": "Cercando...",
|
|
||||||
"servers": "Servers"
|
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"no_internet": "Nessun Internet",
|
|
||||||
"no_items": "Nessun oggetto",
|
|
||||||
"no_internet_message": "Non c'è da preoccuparsi, è ancora possibile guardare\n i contenuti scaricati.",
|
|
||||||
"go_to_downloads": "Vai agli elementi scaricati",
|
|
||||||
"oops": "Oops!",
|
|
||||||
"error_message": "Qualcosa è andato storto. \nEffetturare il logout e riaccedere.",
|
|
||||||
"continue_watching": "Continua a guardare",
|
|
||||||
"next_up": "Prossimo",
|
|
||||||
"recently_added_in": "Aggiunti di recente a {{libraryName}}",
|
|
||||||
"suggested_movies": "Film consigliati",
|
|
||||||
"suggested_episodes": "Episodi consigliati",
|
|
||||||
"intro": {
|
|
||||||
"welcome_to_streamyfin": "Benvenuto a Streamyfin",
|
|
||||||
"a_free_and_open_source_client_for_jellyfin": "Un client gratuito e open-source per Jellyfin.",
|
|
||||||
"features_title": "Funzioni",
|
|
||||||
"features_description": "Streamyfin dispone di numerose funzioni e si integra con un'ampia gamma di software che si possono trovare nel menu delle impostazioni:",
|
|
||||||
"jellyseerr_feature_description": "Connettetevi alla vostra istanza Jellyseerr e richiedete i film direttamente nell'app.",
|
|
||||||
"downloads_feature_title": "Scaricamento",
|
|
||||||
"downloads_feature_description": "Scaricate film e serie tv da vedere offline. Utilizzate il metodo predefinito o installate il server di ottimizzazione per scaricare i file in background.",
|
|
||||||
"chromecast_feature_description": "Trasmettete film e serie tv ai vostri dispositivi Chromecast.",
|
|
||||||
"centralised_settings_plugin_title": "Impostazioni dei Plugin Centralizzate",
|
|
||||||
"centralised_settings_plugin_description": "Configura le impostazioni da una posizione centralizzata sul server Jellyfin. Tutte le impostazioni del client per tutti gli utenti saranno sincronizzate automaticamente.",
|
|
||||||
"done_button": "Fatto",
|
|
||||||
"go_to_settings_button": "Vai alle impostazioni",
|
|
||||||
"read_more": "Leggi di più"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"settings_title": "Impostazioni",
|
|
||||||
"log_out_button": "Esci",
|
|
||||||
"user_info": {
|
|
||||||
"user_info_title": "Info utente",
|
|
||||||
"user": "Utente",
|
|
||||||
"server": "Server",
|
|
||||||
"token": "Token",
|
|
||||||
"app_version": "Versione dell'App"
|
|
||||||
},
|
|
||||||
"quick_connect": {
|
|
||||||
"quick_connect_title": "Connessione Rapida",
|
|
||||||
"authorize_button": "Autorizza Connessione Rapida",
|
|
||||||
"enter_the_quick_connect_code": "Inserisci il codice per la Connessione Rapida...",
|
|
||||||
"success": "Successo",
|
|
||||||
"quick_connect_autorized": "Connessione Rapida autorizzata",
|
|
||||||
"error": "Errore",
|
|
||||||
"invalid_code": "Codice invalido",
|
|
||||||
"authorize": "Autorizza"
|
|
||||||
},
|
|
||||||
"media_controls": {
|
|
||||||
"media_controls_title": "Controlli multimediali",
|
|
||||||
"forward_skip_length": "Lunghezza del salto in avanti",
|
|
||||||
"rewind_length": "Lunghezza del riavvolgimento",
|
|
||||||
"seconds_unit": "s"
|
|
||||||
},
|
|
||||||
"audio": {
|
|
||||||
"audio_title": "Audio",
|
|
||||||
"set_audio_track": "Imposta la traccia audio dall'elemento precedente",
|
|
||||||
"audio_language": "Lingua Audio",
|
|
||||||
"audio_hint": "Scegli la lingua audio predefinita.",
|
|
||||||
"none": "Nessuno",
|
|
||||||
"language": "Lingua"
|
|
||||||
},
|
|
||||||
"subtitles": {
|
|
||||||
"subtitle_title": "Sottotitoli",
|
|
||||||
"subtitle_language": "Lingua dei sottotitoli",
|
|
||||||
"subtitle_mode": "Modalità dei sottotitoli",
|
|
||||||
"set_subtitle_track": "Imposta la traccia dei sottotitoli dall'elemento precedente",
|
|
||||||
"subtitle_size": "Dimensione dei sottotitoli",
|
|
||||||
"subtitle_hint": "Configura la preferenza dei sottotitoli.",
|
|
||||||
"none": "Nessuno",
|
|
||||||
"language": "Lingua",
|
|
||||||
"loading": "Caricamento",
|
|
||||||
"modes": {
|
|
||||||
"Default": "Predefinito",
|
|
||||||
"Smart": "Intelligente",
|
|
||||||
"Always": "Sempre",
|
|
||||||
"None": "Nessuno",
|
|
||||||
"OnlyForced": "Solo forzati"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"other_title": "Altro",
|
|
||||||
"auto_rotate": "Rotazione automatica",
|
|
||||||
"video_orientation": "Orientamento del video",
|
|
||||||
"orientation": "Orientamento",
|
|
||||||
"orientations": {
|
|
||||||
"DEFAULT": "Predefinito",
|
|
||||||
"ALL": "Tutto",
|
|
||||||
"PORTRAIT": "Verticale",
|
|
||||||
"PORTRAIT_UP": "Verticale sopra",
|
|
||||||
"PORTRAIT_DOWN": "Verticale sotto",
|
|
||||||
"LANDSCAPE": "Orizzontale",
|
|
||||||
"LANDSCAPE_LEFT": "Orizzontale sinitra",
|
|
||||||
"LANDSCAPE_RIGHT": "Orizzontale destra",
|
|
||||||
"OTHER": "Altro",
|
|
||||||
"UNKNOWN": "Sconosciuto"
|
|
||||||
},
|
|
||||||
"safe_area_in_controls": "Area sicura per i controlli",
|
|
||||||
"show_custom_menu_links": "Mostra i link del menu personalizzato",
|
|
||||||
"hide_libraries": "Nascondi Librerie",
|
|
||||||
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
|
|
||||||
"disable_haptic_feedback": "Disabilita il feedback aptico",
|
|
||||||
"default_quality": "Qualità predefinita"
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "Scaricamento",
|
|
||||||
"download_method": "Metodo per lo scaricamento",
|
|
||||||
"remux_max_download": "Numero di Remux da scaricare al massimo",
|
|
||||||
"auto_download": "Scaricamento automatico",
|
|
||||||
"optimized_versions_server": "Versioni del server di ottimizzazione",
|
|
||||||
"save_button": "Salva",
|
|
||||||
"optimized_server": "Server di ottimizzazione",
|
|
||||||
"optimized": "Ottimizzato",
|
|
||||||
"default": "Predefinito",
|
|
||||||
"optimized_version_hint": "Inserire l'URL del server di ottimizzazione. L'URL deve includere http o https e, facoltativamente, la porta.",
|
|
||||||
"read_more_about_optimized_server": "Per saperne di più sul server di ottimizzazione.",
|
|
||||||
"url":"URL",
|
|
||||||
"server_url_placeholder": "http(s)://dominio.org:porta"
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"plugins_title": "Plugin",
|
|
||||||
"jellyseerr": {
|
|
||||||
"jellyseerr_warning": "Questa integrazione è in fase iniziale. Aspettarsi cambiamenti.",
|
|
||||||
"server_url": "URL del Server",
|
|
||||||
"server_url_hint": "Esempio: http(s)://tuo-host.url\n(aggiungere la porta se richiesto)",
|
|
||||||
"server_url_placeholder": "URL di Jellyseerr...",
|
|
||||||
"password": "Password",
|
|
||||||
"password_placeholder": "Inserire la password per l'utente {{username}} di Jellyfin",
|
|
||||||
"save_button": "Salva",
|
|
||||||
"clear_button": "Cancella",
|
|
||||||
"login_button": "Accedi",
|
|
||||||
"total_media_requests": "Totale di richieste di media",
|
|
||||||
"movie_quota_limit": "Limite di quota per i film",
|
|
||||||
"movie_quota_days": "Giorni di quota per i film",
|
|
||||||
"tv_quota_limit": "Limite di quota per le serie TV",
|
|
||||||
"tv_quota_days": "Giorni di quota per le serie TV",
|
|
||||||
"reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr",
|
|
||||||
"unlimited": "Illimitato"
|
|
||||||
},
|
|
||||||
"marlin_search": {
|
|
||||||
"enable_marlin_search": "Abilita la ricerca Marlin ",
|
|
||||||
"url": "URL",
|
|
||||||
"server_url_placeholder": "http(s)://dominio.org:porta",
|
|
||||||
"marlin_search_hint": "Inserire l'URL del server Marlin. L'URL deve includere http o https e, facoltativamente, la porta.",
|
|
||||||
"read_more_about_marlin": "Leggi di più su Marlin.",
|
|
||||||
"save_button": "Salva",
|
|
||||||
"toasts": {
|
|
||||||
"saved": "Salvato"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"storage": {
|
|
||||||
"storage_title": "Spazio",
|
|
||||||
"app_usage": "App {{usedSpace}}%",
|
|
||||||
"device_usage": "Dispositivo {{availableSpace}}%",
|
|
||||||
"size_used": "{{used}} di {{total}} usato",
|
|
||||||
"delete_all_downloaded_files": "Cancella Tutti i File Scaricati"
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"show_intro": "Mostra intro",
|
|
||||||
"reset_intro": "Ripristina intro"
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"logs_title": "Log",
|
|
||||||
"no_logs_available": "Nessun log disponibile",
|
|
||||||
"delete_all_logs": "Cancella tutti i log"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"title": "Lingue",
|
|
||||||
"app_language": "Lingua dell'App",
|
|
||||||
"app_language_description": "Selezione la lingua dell'app.",
|
|
||||||
"system": "Sistema"
|
|
||||||
},
|
|
||||||
"toasts":{
|
|
||||||
"error_deleting_files": "Errore nella cancellazione dei file",
|
|
||||||
"background_downloads_enabled": "Scaricamento in background abilitato",
|
|
||||||
"background_downloads_disabled": "Scaricamento in background disabilitato",
|
|
||||||
"connected": "Connesso",
|
|
||||||
"could_not_connect": "Non è stato possibile connettersi",
|
|
||||||
"invalid_url": "URL invalido"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "Scaricati",
|
|
||||||
"tvseries": "Serie TV",
|
|
||||||
"movies": "Film",
|
|
||||||
"queue": "Coda",
|
|
||||||
"queue_hint": "La coda e gli elementi scaricati saranno persi con il riavvio dell'app",
|
|
||||||
"no_items_in_queue": "Nessun elemento in coda",
|
|
||||||
"no_downloaded_items": "Nessun elemento scaricato",
|
|
||||||
"delete_all_movies_button": "Cancella tutti i film",
|
|
||||||
"delete_all_tvseries_button": "Cancella tutte le serie TV",
|
|
||||||
"delete_all_button": "Cancella tutti",
|
|
||||||
"active_download": "Scaricamento in corso",
|
|
||||||
"no_active_downloads": "Nessun scaricamento in corso",
|
|
||||||
"active_downloads": "Scaricamenti in corso",
|
|
||||||
"new_app_version_requires_re_download": "La nuova verione dell'app richiede di scaricare nuovamente i contenuti",
|
|
||||||
"new_app_version_requires_re_download_description": "Il nuovo aggiornamento richiede di scaricare nuovamente i contenuti. Rimuovere tutti i contenuti scaricati e riprovare.",
|
|
||||||
"back": "Indietro",
|
|
||||||
"delete": "Cancella",
|
|
||||||
"something_went_wrong": "Qualcosa è andato storto",
|
|
||||||
"could_not_get_stream_url_from_jellyfin": "Impossibile ottenere l'URL del flusso da Jellyfin",
|
|
||||||
"eta": "ETA {{eta}}",
|
|
||||||
"methods": "Metodi",
|
|
||||||
"toasts": {
|
|
||||||
"you_are_not_allowed_to_download_files": "Non è consentito scaricare file.",
|
|
||||||
"deleted_all_movies_successfully": "Cancellati tutti i film con successo!",
|
|
||||||
"failed_to_delete_all_movies": "Impossibile eliminare tutti i film",
|
|
||||||
"deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!",
|
|
||||||
"failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV",
|
|
||||||
"download_cancelled": "Scaricamento annullato",
|
|
||||||
"could_not_cancel_download": "Impossibile annullare lo scaricamento",
|
|
||||||
"download_completed": "Scaricamento completato",
|
|
||||||
"download_started_for": "Scaricamento iniziato per {{item}}",
|
|
||||||
"item_is_ready_to_be_downloaded": "{{item}} è pronto per essere scaricato",
|
|
||||||
"download_stated_for_item": "Scaricamento iniziato per {{item}}",
|
|
||||||
"download_failed_for_item": "Scaricamento fallito per {{item}} - {{error}}",
|
|
||||||
"download_completed_for_item": "Scaricamento completato per {{item}}",
|
|
||||||
"queued_item_for_optimization": "Messo in coda {{item}} per l'ottimizzazione",
|
|
||||||
"failed_to_start_download_for_item": "Failed to start downloading for {{item}}: {{message}}",
|
|
||||||
"server_responded_with_status_code": "Server responded with status {{statusCode}}",
|
|
||||||
"no_response_received_from_server": "No response received from the server",
|
|
||||||
"error_setting_up_the_request": "Error setting up the request",
|
|
||||||
"failed_to_start_download_for_item_unexpected_error": "Impossibile avviare il download per {{item}}: Errore imprevisto",
|
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "Tutti i file, le cartelle e i processi sono stati eliminati con successo.",
|
|
||||||
"an_error_occured_while_deleting_files_and_jobs": "Si è verificato un errore durante l'eliminazione di file e processi",
|
|
||||||
"go_to_downloads": "Vai agli elementi scaricati"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"search_here": "Cerca qui...",
|
|
||||||
"search": "Cerca...",
|
|
||||||
"x_items": "{{count}} elementi",
|
|
||||||
"library": "Libreria",
|
|
||||||
"discover": "Scopri",
|
|
||||||
"no_results": "Nessun risultato",
|
|
||||||
"no_results_found_for": "Nessun risultato trovato per",
|
|
||||||
"movies": "Film",
|
|
||||||
"series": "Serie",
|
|
||||||
"episodes": "Episodi",
|
|
||||||
"collections": "Collezioni",
|
|
||||||
"actors": "Attori",
|
|
||||||
"request_movies": "Film Richiesti",
|
|
||||||
"request_series": "Serie Richieste",
|
|
||||||
"recently_added": "Aggiunti di Recente",
|
|
||||||
"recent_requests": "Richiesti di Recente",
|
|
||||||
"plex_watchlist": "Plex Watchlist",
|
|
||||||
"trending": "In tendenza",
|
|
||||||
"popular_movies": "Film Popolari",
|
|
||||||
"movie_genres": "Generi Film",
|
|
||||||
"upcoming_movies": "Film in arrivo",
|
|
||||||
"studios": "Studio",
|
|
||||||
"popular_tv": "Serie Popolari",
|
|
||||||
"tv_genres": "Generi Televisivi",
|
|
||||||
"upcoming_tv": "Serie in Arrivo",
|
|
||||||
"networks": "Network",
|
|
||||||
"tmdb_movie_keyword": "TMDB Parola chiave del film",
|
|
||||||
"tmdb_movie_genre": "TMDB Genere Film",
|
|
||||||
"tmdb_tv_keyword": "TMDB Parola chiave della serie",
|
|
||||||
"tmdb_tv_genre": "TMDB Genere Televisivo",
|
|
||||||
"tmdb_search": "TMDB Cerca",
|
|
||||||
"tmdb_studio": "TMDB Studio",
|
|
||||||
"tmdb_network": "TMDB Network",
|
|
||||||
"tmdb_movie_streaming_services": "TMDB Servizi di Streaming di Film",
|
|
||||||
"tmdb_tv_streaming_services": "TMDB Servizi di Streaming di Serie"
|
|
||||||
},
|
|
||||||
"library": {
|
|
||||||
"no_items_found": "Nessun elemento trovato",
|
|
||||||
"no_results": "Nessun risultato",
|
|
||||||
"no_libraries_found": "Nessuna libreria trovata",
|
|
||||||
"item_types": {
|
|
||||||
"movies": "film",
|
|
||||||
"series": "serie TV",
|
|
||||||
"boxsets": "cofanetti",
|
|
||||||
"items": "elementi"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"display": "Display",
|
|
||||||
"row": "Fila",
|
|
||||||
"list": "Lista",
|
|
||||||
"image_style": "Stile dell'immagine",
|
|
||||||
"poster": "Poster",
|
|
||||||
"cover": "Cover",
|
|
||||||
"show_titles": "Mostra titoli",
|
|
||||||
"show_stats": "Mostra statistiche"
|
|
||||||
},
|
|
||||||
"filters": {
|
|
||||||
"genres": "Generi",
|
|
||||||
"years": "Anni",
|
|
||||||
"sort_by": "Ordina per",
|
|
||||||
"sort_order": "Criterio di ordinamento",
|
|
||||||
"tags": "Tag"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"favorites": {
|
|
||||||
"series": "Serie TV",
|
|
||||||
"movies": "Film",
|
|
||||||
"episodes": "Episodi",
|
|
||||||
"videos": "Video",
|
|
||||||
"boxsets": "Boxset",
|
|
||||||
"playlists": "Playlist"
|
|
||||||
},
|
|
||||||
"custom_links": {
|
|
||||||
"no_links": "Nessun link"
|
|
||||||
},
|
|
||||||
"player": {
|
|
||||||
"error": "Errore",
|
|
||||||
"failed_to_get_stream_url": "Impossibile ottenere l'URL dello stream",
|
|
||||||
"an_error_occured_while_playing_the_video": "Si è verificato un errore durante la riproduzione del video. Controllare i log nelle impostazioni.",
|
|
||||||
"client_error": "Errore del client",
|
|
||||||
"could_not_create_stream_for_chromecast": "Impossibile creare uno stream per Chromecast",
|
|
||||||
"message_from_server": "Messaggio dal server: {{messagge}}",
|
|
||||||
"video_has_finished_playing": "La riproduzione del video è terminata!",
|
|
||||||
"no_video_source": "Nessuna sorgente video...",
|
|
||||||
"next_episode": "Prossimo Episodio",
|
|
||||||
"refresh_tracks": "Aggiorna tracce",
|
|
||||||
"subtitle_tracks": "Tracce di sottotitoli:",
|
|
||||||
"audio_tracks": "Tracce audio:",
|
|
||||||
"playback_state": "Stato della riproduzione:",
|
|
||||||
"no_data_available": "Nessun dato disponibile",
|
|
||||||
"index": "Indice:"
|
|
||||||
},
|
|
||||||
"item_card": {
|
|
||||||
"next_up": "Il prossimo",
|
|
||||||
"no_items_to_display": "Nessun elemento da visualizzare",
|
|
||||||
"cast_and_crew": "Cast e Equipaggio",
|
|
||||||
"series": "Serie",
|
|
||||||
"seasons": "Stagioni",
|
|
||||||
"season": "Stagione",
|
|
||||||
"no_episodes_for_this_season": "Nessun episodio per questa stagione",
|
|
||||||
"overview": "Panoramica",
|
|
||||||
"more_with": "Altri con {{name}}",
|
|
||||||
"similar_items": "Elementi simili",
|
|
||||||
"no_similar_items_found": "Non sono stati trovati elementi simili",
|
|
||||||
"video": "Video",
|
|
||||||
"more_details": "Più dettagli",
|
|
||||||
"quality": "Qualità",
|
|
||||||
"audio": "Audio",
|
|
||||||
"subtitles": "Sottotitoli",
|
|
||||||
"show_more": "Mostra di più",
|
|
||||||
"show_less": "Mostra di meno",
|
|
||||||
"appeared_in": "Apparso in",
|
|
||||||
"could_not_load_item": "Impossibile caricare l'elemento",
|
|
||||||
"none": "Nessuno",
|
|
||||||
"download": {
|
|
||||||
"download_season": "Scarica Stagione",
|
|
||||||
"download_series": "Scarica Serie",
|
|
||||||
"download_episode": "Scarica Episodio",
|
|
||||||
"download_movie": "Scarica Film",
|
|
||||||
"download_x_item": "Scarica {{item_count}} elementi",
|
|
||||||
"download_button": "Scarica",
|
|
||||||
"using_optimized_server": "Utilizzando il server di ottimizzazione",
|
|
||||||
"using_default_method": "Utilizzando il metodo predefinito"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"live_tv": {
|
|
||||||
"next": "Prossimo",
|
|
||||||
"previous": "Precedente",
|
|
||||||
"live_tv": "TV in diretta",
|
|
||||||
"coming_soon": "Prossimamente",
|
|
||||||
"on_now": "In onda ora",
|
|
||||||
"shows": "Programmi",
|
|
||||||
"movies": "Film",
|
|
||||||
"sports": "Sport",
|
|
||||||
"for_kids": "Per Bambini",
|
|
||||||
"news": "Notiziari"
|
|
||||||
},
|
|
||||||
"jellyseerr":{
|
|
||||||
"confirm": "Conferma",
|
|
||||||
"cancel": "Cancella",
|
|
||||||
"yes": "Si",
|
|
||||||
"whats_wrong": "Cosa c'è che non va?",
|
|
||||||
"issue_type": "Tipo di problema",
|
|
||||||
"select_an_issue": "Seleziona un problema",
|
|
||||||
"types": "Tipi",
|
|
||||||
"describe_the_issue": "(facoltativo) Descrivere il problema...",
|
|
||||||
"submit_button": "Invia",
|
|
||||||
"report_issue_button": "Segnalare il problema",
|
|
||||||
"request_button": "Richiedi",
|
|
||||||
"are_you_sure_you_want_to_request_all_seasons": "Sei sicuro di voler richiedere tutte le stagioni?",
|
|
||||||
"failed_to_login": "Accesso non riuscito",
|
|
||||||
"cast": "Cast",
|
|
||||||
"details": "Dettagli",
|
|
||||||
"status": "Stato",
|
|
||||||
"original_title": "Titolo originale",
|
|
||||||
"series_type": "Tipo di Serie",
|
|
||||||
"release_dates": "Date di Uscita",
|
|
||||||
"first_air_date": "Prima Data di Messa in Onda",
|
|
||||||
"next_air_date": "Prossima Data di Messa in Onda",
|
|
||||||
"revenue": "Ricavi",
|
|
||||||
"budget": "Budget",
|
|
||||||
"original_language": "Lingua Originale",
|
|
||||||
"production_country": "Paese di Produzione",
|
|
||||||
"studios": "Studio",
|
|
||||||
"network": "Network",
|
|
||||||
"currently_streaming_on": "Attualmente in streaming su",
|
|
||||||
"advanced": "Avanzate",
|
|
||||||
"request_as": "Richiedi Come",
|
|
||||||
"tags": "Tag",
|
|
||||||
"quality_profile": "Profilo qualità",
|
|
||||||
"root_folder": "Cartella radice",
|
|
||||||
"season_x": "Stagione {{seasons}}",
|
|
||||||
"season_number": "Stagione {{season_number}}",
|
|
||||||
"number_episodes": "{{episode_number}} Episodio",
|
|
||||||
"born": "Nato",
|
|
||||||
"appearances": "Aspetto",
|
|
||||||
"toasts": {
|
|
||||||
"jellyseer_does_not_meet_requirements": "Il server Jellyseerr non soddisfa i requisiti minimi di versione! Aggiornare almeno alla versione 2.0.0.",
|
|
||||||
"jellyseerr_test_failed": "Il test di Jellyseerr non è riuscito. Riprovare.",
|
|
||||||
"failed_to_test_jellyseerr_server_url": "Fallito il test dell'url del server jellyseerr",
|
|
||||||
"issue_submitted": "Problema inviato!",
|
|
||||||
"requested_item": "Richiesto {{item}}!",
|
|
||||||
"you_dont_have_permission_to_request": "Non hai il permesso di richiedere!",
|
|
||||||
"something_went_wrong_requesting_media": "Qualcosa è andato storto nella richiesta dei media!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tabs": {
|
|
||||||
"home": "Home",
|
|
||||||
"search": "Cerca",
|
|
||||||
"library": "Libreria",
|
|
||||||
"custom_links": "Collegamenti personalizzati",
|
|
||||||
"favorites": "Preferiti"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,458 +0,0 @@
|
|||||||
{
|
|
||||||
"login": {
|
|
||||||
"username_required": "Gebruikersnaam is verplicht",
|
|
||||||
"error_title": "Fout",
|
|
||||||
"login_title": "Aanmelden",
|
|
||||||
"login_to_title": "Aanmelden bij",
|
|
||||||
"username_placeholder": "Gebruikersnaam",
|
|
||||||
"password_placeholder": "Wachtwoord",
|
|
||||||
"login_button": "Aanmelden",
|
|
||||||
"quick_connect": "Snel Verbinden",
|
|
||||||
"enter_code_to_login": "Vul code {{code}} in om aan te melden",
|
|
||||||
"failed_to_initiate_quick_connect": "Gefaald om Snel Verbinden op te starten",
|
|
||||||
"got_it": "Begrepen",
|
|
||||||
"connection_failed": "Verbinding gefaald",
|
|
||||||
"could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.",
|
|
||||||
"an_unexpected_error_occured": "Er is een onverwachte fout opgetreden",
|
|
||||||
"change_server": "Verander server",
|
|
||||||
"invalid_username_or_password": "Ongeldige gebruikersnaam of wachtwoord",
|
|
||||||
"user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden",
|
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw",
|
|
||||||
"server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw",
|
|
||||||
"there_is_a_server_error": "Er is een serverfout",
|
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Er is een onverwachte fout opgetreden. Heb je de server URL correct ingegeven?"
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"enter_url_to_jellyfin_server": "Geef de URL van je Jellyfin server in",
|
|
||||||
"server_url_placeholder": "http(s)://je-server.com",
|
|
||||||
"connect_button": "Verbinden",
|
|
||||||
"previous_servers": "vorige servers",
|
|
||||||
"clear_button": "Wissen",
|
|
||||||
"search_for_local_servers": "Zoek naar lokale servers",
|
|
||||||
"searching": "Zoeken...",
|
|
||||||
"servers": "Servers"
|
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"no_internet": "Geen Internet",
|
|
||||||
"no_items": "Geen items",
|
|
||||||
"no_internet_message": "Geen zorgen, je kan nog steeds\ngedownloade content bekijken",
|
|
||||||
"go_to_downloads": "Ga naar downloads",
|
|
||||||
"oops": "Oeps!",
|
|
||||||
"error_message": "Er ging iets fout\nGelieve af en aan te melden.",
|
|
||||||
"continue_watching": "Verder Kijken",
|
|
||||||
"next_up": "Volgende",
|
|
||||||
"recently_added_in": "Recent toegevoegd in {{libraryName}}",
|
|
||||||
"suggested_movies": "Voorgestelde Films",
|
|
||||||
"suggested_episodes": "Voorgestelde Afleveringen",
|
|
||||||
"intro": {
|
|
||||||
"welcome_to_streamyfin": "Welkom bij Streamyfin",
|
|
||||||
"a_free_and_open_source_client_for_jellyfin": "Een gratis en open-source client voor Jellyfin.",
|
|
||||||
"features_title": "Functies",
|
|
||||||
"features_description": "Streamyfin heeft een heleboel functies en integreert met een breed scala aan software die je kunt vinden in het instellingenmenu, onder andere:",
|
|
||||||
"jellyseerr_feature_description": "Verbind met je Jellyseerr instantie en vraag films direct in de app aan.",
|
|
||||||
"downloads_feature_title": "Downloads",
|
|
||||||
"downloads_feature_description": "Download films en series om offline te kijken. Gebruik de standaardmethode of installeer de optimalisatieserver om bestanden op de achtergrond te downloaden.",
|
|
||||||
"chromecast_feature_description": "Cast films en series naar je Chromecast toestellen.",
|
|
||||||
"centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen",
|
|
||||||
"centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.",
|
|
||||||
"done_button": "Gedaan",
|
|
||||||
"go_to_settings_button": "Go naar instellingen",
|
|
||||||
"read_more": "Lees meer"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"settings_title": "Instellingen",
|
|
||||||
"log_out_button": "Afmelden",
|
|
||||||
"user_info": {
|
|
||||||
"user_info_title": "Gebruiker Info",
|
|
||||||
"user": "Gebruiker",
|
|
||||||
"server": "Server",
|
|
||||||
"token": "Token",
|
|
||||||
"app_version": "App Versie"
|
|
||||||
},
|
|
||||||
"quick_connect": {
|
|
||||||
"quick_connect_title": "Snel Verbinden",
|
|
||||||
"authorize_button": "Snel Verbinden toestaan",
|
|
||||||
"enter_the_quick_connect_code": "Vul de Snel Verbinden code in...",
|
|
||||||
"success": "Succes",
|
|
||||||
"quick_connect_autorized": "Snel Verbinden toegestaan",
|
|
||||||
"error": "Fout",
|
|
||||||
"invalid_code": "Ongeldige code",
|
|
||||||
"authorize": "Toestaan"
|
|
||||||
},
|
|
||||||
"media_controls": {
|
|
||||||
"media_controls_title": "Media Bedieningen",
|
|
||||||
"forward_skip_length": "Duur voorwaarts overslaan",
|
|
||||||
"rewind_length": "Duur terugspeolen",
|
|
||||||
"seconds_unit": "s"
|
|
||||||
},
|
|
||||||
"audio": {
|
|
||||||
"audio_title": "Audio",
|
|
||||||
"set_audio_track": "Gebruik Audio Track Van Vorig Item",
|
|
||||||
"audio_language": "Audio taal",
|
|
||||||
"audio_hint": "Kies een standaard audio taal.",
|
|
||||||
"none": "Geen",
|
|
||||||
"language": "Taal"
|
|
||||||
},
|
|
||||||
"subtitles": {
|
|
||||||
"subtitle_title": "Ondertitels",
|
|
||||||
"subtitle_language": "Ondertitel taal",
|
|
||||||
"subtitle_mode": "Ondertitle Modus",
|
|
||||||
"set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item",
|
|
||||||
"subtitle_size": "Ondertitel Grootte",
|
|
||||||
"subtitle_hint": "Stel ondertitel voorkeuren in.",
|
|
||||||
"none": "Geen",
|
|
||||||
"language": "Taal",
|
|
||||||
"loading": "Laden",
|
|
||||||
"modes": {
|
|
||||||
"Default": "Standaard",
|
|
||||||
"Smart": "Slim",
|
|
||||||
"Always": "Altijd",
|
|
||||||
"None": "Geen",
|
|
||||||
"OnlyForced": "Alleen Geforceeerd"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"other_title": "Andere",
|
|
||||||
"auto_rotate": "Automatisch draaien",
|
|
||||||
"video_orientation": "Video oriëntatie",
|
|
||||||
"orientation": "Oriëntatie",
|
|
||||||
"orientations": {
|
|
||||||
"DEFAULT": "Standaard",
|
|
||||||
"ALL": "Alle",
|
|
||||||
"PORTRAIT": "Portret",
|
|
||||||
"PORTRAIT_UP": "Portret Omhoog",
|
|
||||||
"PORTRAIT_DOWN": "Portret Omlaag",
|
|
||||||
"LANDSCAPE": "Landschap",
|
|
||||||
"LANDSCAPE_LEFT": "Landschap Links",
|
|
||||||
"LANDSCAPE_RIGHT": "Landschap Rechts",
|
|
||||||
"OTHER": "Andere",
|
|
||||||
"UNKNOWN": "Onbekend"
|
|
||||||
},
|
|
||||||
"safe_area_in_controls": "Veilig gebied in bedieningen",
|
|
||||||
"show_custom_menu_links": "Aangepaste menulinks tonen",
|
|
||||||
"hide_libraries": "Verberg Bibliotheken",
|
|
||||||
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheek tab en hoofdpagina onderdelen.",
|
|
||||||
"disable_haptic_feedback": "Haptische feedback uitschakelen",
|
|
||||||
"default_quality": "Standaard kwaliteit"
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "Downloads",
|
|
||||||
"download_method": "Download methode",
|
|
||||||
"remux_max_download": "Remux max download",
|
|
||||||
"auto_download": "Auto download",
|
|
||||||
"optimized_versions_server": "Geoptimaliseerde server versies",
|
|
||||||
"save_button": "Opslaan",
|
|
||||||
"optimized_server": "Geoptimailseerde Server",
|
|
||||||
"optimized": "Geoptimaliseerd",
|
|
||||||
"default": "Standaard",
|
|
||||||
"optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.",
|
|
||||||
"read_more_about_optimized_server": "Lees meer over de optimalisatieserver.",
|
|
||||||
"url":"URL",
|
|
||||||
"server_url_placeholder": "http(s)://domein.org:poort"
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"plugins_title": "Plugins",
|
|
||||||
"jellyseerr": {
|
|
||||||
"jellyseerr_warning": "Deze integratie is nog in een vroeg stadium. Verwacht dat zaken nog veranderen.",
|
|
||||||
"server_url": "Server URL",
|
|
||||||
"server_url_hint": "Voorbeeld: http(s)://je-host.url\n(indien nodig: voeg de poort toe)",
|
|
||||||
"server_url_placeholder": "Jellyseerr URL...",
|
|
||||||
"password": "Wachtwoord",
|
|
||||||
"password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}",
|
|
||||||
"save_button": "Opslaan",
|
|
||||||
"clear_button": "Wissen",
|
|
||||||
"login_button": "Aannmelden",
|
|
||||||
"total_media_requests": "Totaal aantal mediaverzoeken",
|
|
||||||
"movie_quota_limit": "Limiet filmquota",
|
|
||||||
"movie_quota_days": "Filmquota dagen",
|
|
||||||
"tv_quota_limit": "Limiet serie quota",
|
|
||||||
"tv_quota_days": "Serie Quota dagen",
|
|
||||||
"reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen",
|
|
||||||
"unlimited": "Ongelimiteerd"
|
|
||||||
},
|
|
||||||
"marlin_search": {
|
|
||||||
"enable_marlin_search": "Marlin Search inschakeln ",
|
|
||||||
"url": "URL",
|
|
||||||
"server_url_placeholder": "http(s)://domein.org:poort",
|
|
||||||
"marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.",
|
|
||||||
"read_more_about_marlin": "Lees meer over Marlin.",
|
|
||||||
"save_button": "Opslaan",
|
|
||||||
"toasts": {
|
|
||||||
"saved": "Opgeslagen"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"storage": {
|
|
||||||
"storage_title": "Opslag",
|
|
||||||
"app_usage": "App {{usedSpace}}%",
|
|
||||||
"device_usage": "Toestel {{availableSpace}}%",
|
|
||||||
"size_used": "{{used}} van {{total}} gebruikt",
|
|
||||||
"delete_all_downloaded_files": "Verwijder alle gedownloade bestanden"
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"show_intro": "Toon intro",
|
|
||||||
"reset_intro": "intro opnieuw instellen"
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"logs_title": "Logs",
|
|
||||||
"no_logs_available": "Geen logs beschikbaar",
|
|
||||||
"delete_all_logs": "Verwijder alle logs"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"title": "Talen",
|
|
||||||
"app_language": "App taal",
|
|
||||||
"app_language_description": "Selecteer een taal voor de app.",
|
|
||||||
"system": "Systeem"
|
|
||||||
},
|
|
||||||
"toasts":{
|
|
||||||
"error_deleting_files": "Fout bij het verwijden van bestanden",
|
|
||||||
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
|
||||||
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld",
|
|
||||||
"connected": "Verbonden",
|
|
||||||
"could_not_connect": "Kon niet verbinden",
|
|
||||||
"invalid_url": "Ongeldige URL"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "Downloads",
|
|
||||||
"tvseries": "Series",
|
|
||||||
"movies": "Films",
|
|
||||||
"queue": "Wachtrij",
|
|
||||||
"queue_hint": "Wachtrij en downloads verdwijnen bij een herstart van de app",
|
|
||||||
"no_items_in_queue": "Geen items in wachtrij",
|
|
||||||
"no_downloaded_items": "Geen gedownloade items",
|
|
||||||
"delete_all_movies_button": "Verwijder alle films",
|
|
||||||
"delete_all_tvseries_button": "Verwijder alle Series",
|
|
||||||
"delete_all_button": "Verwijder alles",
|
|
||||||
"active_download": "Actieve download",
|
|
||||||
"no_active_downloads": "Geen actieve downloads",
|
|
||||||
"active_downloads": "Actieve downloads",
|
|
||||||
"new_app_version_requires_re_download": "Nieuwe app-versie vereist opnieuw downloaden",
|
|
||||||
"new_app_version_requires_re_download_description": "Voor de nieuwe update moet de content opnieuw worden gedownload. Verwijder alle gedownloade content en probeer het opnieuw.",
|
|
||||||
"back": "Terug",
|
|
||||||
"delete": "Verwijder",
|
|
||||||
"something_went_wrong": "Er ging iets mis",
|
|
||||||
"could_not_get_stream_url_from_jellyfin": "Kon de URL van de stream niet krijgen van Jellyfin",
|
|
||||||
"eta": "ETA {{eta}}",
|
|
||||||
"methods": "Methoden",
|
|
||||||
"toasts": {
|
|
||||||
"you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.",
|
|
||||||
"deleted_all_movies_successfully": "Alle filns succesvol verwijderd!",
|
|
||||||
"failed_to_delete_all_movies": "Alle films zijn niet verwijderd",
|
|
||||||
"deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!",
|
|
||||||
"failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd",
|
|
||||||
"download_cancelled": "Download geannuleerd",
|
|
||||||
"could_not_cancel_download": "Kon de download niet annuleren",
|
|
||||||
"download_completed": "Download afgerond",
|
|
||||||
"download_started_for": "Download gestart voor {{item}}",
|
|
||||||
"item_is_ready_to_be_downloaded": "{{item}} is klaar op te downloaden",
|
|
||||||
"download_stated_for_item": "Download gestart voor {{item}}",
|
|
||||||
"download_failed_for_item": "Download gefaald voor {{item}} - {{error}}",
|
|
||||||
"download_completed_for_item": "Download afgerond voor {{item}}",
|
|
||||||
"queued_item_for_optimization": "{{item}} in de wachtrij gezet voor optimalisatie",
|
|
||||||
"failed_to_start_download_for_item": "Kon de download voor {{item}} niet starten: {{message}}",
|
|
||||||
"server_responded_with_status_code": "Server heeft geantwoord met {{statusCode}}",
|
|
||||||
"no_response_received_from_server": "Geen antwoord gekregen van de server",
|
|
||||||
"error_setting_up_the_request": "Fout bij het opstellen van de aanvraag",
|
|
||||||
"failed_to_start_download_for_item_unexpected_error": "Kon de download voor {{item}} niet starten: Onverwachte fout",
|
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "Alle bestanden, mappen en taken succesvol verwijderd",
|
|
||||||
"an_error_occured_while_deleting_files_and_jobs": "Er is een fout opgetreden tijdens het verwijderen van bestanden en taken",
|
|
||||||
"go_to_downloads": "Ga naar downloads"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"search_here": "Zoek hier...",
|
|
||||||
"search": "Zoek...",
|
|
||||||
"x_items": "{{count}} items",
|
|
||||||
"library": "Bibliotheek",
|
|
||||||
"discover": "Ontdek",
|
|
||||||
"no_results": "Geen resultaten",
|
|
||||||
"no_results_found_for": "Geen resultaten gevonden voor",
|
|
||||||
"movies": "Films",
|
|
||||||
"series": "Series",
|
|
||||||
"episodes": "Afleveringen",
|
|
||||||
"collections": "Collecties",
|
|
||||||
"actors": "Acteurs",
|
|
||||||
"request_movies": "Vraag films aan",
|
|
||||||
"request_series": "Vraag series aan",
|
|
||||||
"recently_added": "Recent Toegevoegd",
|
|
||||||
"recent_requests": "Recent Aangevraagd",
|
|
||||||
"plex_watchlist": "Plex Kijklijst",
|
|
||||||
"trending": "Trending",
|
|
||||||
"popular_movies": "Populaire Films",
|
|
||||||
"movie_genres": "Film Genres",
|
|
||||||
"upcoming_movies": "Aankomende Movies",
|
|
||||||
"studios": "Studios",
|
|
||||||
"popular_tv": "Populaire TV",
|
|
||||||
"tv_genres": "TV Genres",
|
|
||||||
"upcoming_tv": "Opkomend TV",
|
|
||||||
"networks": "Netwerken",
|
|
||||||
"tmdb_movie_keyword": "TMDB Film Trefwoord",
|
|
||||||
"tmdb_movie_genre": "TMDB Film Genre",
|
|
||||||
"tmdb_tv_keyword": "TMDB TV Trefwoord",
|
|
||||||
"tmdb_tv_genre": "TMDB TV Genre",
|
|
||||||
"tmdb_search": "TMDB Zoeken",
|
|
||||||
"tmdb_studio": "TMDB Studio",
|
|
||||||
"tmdb_network": "TMDB Netwerk",
|
|
||||||
"tmdb_movie_streaming_services": "TMDB Film Streaming Diensten",
|
|
||||||
"tmdb_tv_streaming_services": "TMDB TV Streaming Diensten"
|
|
||||||
},
|
|
||||||
"library": {
|
|
||||||
"no_items_found": "Geen items gevonden",
|
|
||||||
"no_results": "Geen resultaten",
|
|
||||||
"no_libraries_found": "Geen bibliotheken gevonden",
|
|
||||||
"item_types": {
|
|
||||||
"movies": "films",
|
|
||||||
"series": "series",
|
|
||||||
"boxsets": "box sets",
|
|
||||||
"items": "items"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"display": "Weergave",
|
|
||||||
"row": "Rij",
|
|
||||||
"list": "Lijst",
|
|
||||||
"image_style": "Stijl van afbeelding",
|
|
||||||
"poster": "Poster",
|
|
||||||
"cover": "Cover",
|
|
||||||
"show_titles": "Toon titels",
|
|
||||||
"show_stats": "Toon statistieken"
|
|
||||||
},
|
|
||||||
"filters": {
|
|
||||||
"genres": "Genres",
|
|
||||||
"years": "Jaren",
|
|
||||||
"sort_by": "Sorteren op",
|
|
||||||
"sort_order": "Sorteer volgorde",
|
|
||||||
"tags": "Labels"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"favorites": {
|
|
||||||
"series": "Series",
|
|
||||||
"movies": "Films",
|
|
||||||
"episodes": "Afleveringen",
|
|
||||||
"videos": "Videos",
|
|
||||||
"boxsets": "Boxsets",
|
|
||||||
"playlists": "Afspeellijsten"
|
|
||||||
},
|
|
||||||
"custom_links": {
|
|
||||||
"no_links": "Geen links"
|
|
||||||
},
|
|
||||||
"player": {
|
|
||||||
"error": "Fout",
|
|
||||||
"failed_to_get_stream_url": "De stream-URL kon niet worden verkregen",
|
|
||||||
"an_error_occured_while_playing_the_video": "Er is een fout opgetreden tijdens het afspelen van de video. Controleer de logs in de instellingen.",
|
|
||||||
"client_error": "Fout van de client",
|
|
||||||
"could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast",
|
|
||||||
"message_from_server": "Bericht van de server: {{message}}",
|
|
||||||
"video_has_finished_playing": "Video is gedaan met spelen!",
|
|
||||||
"no_video_source": "Geen video bron...",
|
|
||||||
"next_episode": "Volgende Aflevering",
|
|
||||||
"refresh_tracks": "Tracks verversen",
|
|
||||||
"subtitle_tracks": "Ondertitel Tracks:",
|
|
||||||
"audio_tracks": "Audio Tracks:",
|
|
||||||
"playback_state": "Afspeelstatus:",
|
|
||||||
"no_data_available": "Geen data beschikbaar",
|
|
||||||
"index": "Index:"
|
|
||||||
},
|
|
||||||
"item_card": {
|
|
||||||
"next_up": "Volgende",
|
|
||||||
"no_items_to_display": "Geen items om te tonen",
|
|
||||||
"cast_and_crew": "Cast & Crew",
|
|
||||||
"series": "Series",
|
|
||||||
"seasons": "Seizoenen",
|
|
||||||
"season": "Seizoen",
|
|
||||||
"no_episodes_for_this_season": "Geen afleveringen voor dit seizoen",
|
|
||||||
"overview": "Overzicht",
|
|
||||||
"more_with": "Meer met {{name}}",
|
|
||||||
"similar_items": "Gelijkaardige items",
|
|
||||||
"no_similar_items_found": "Geen gelijkaardige items gevonden",
|
|
||||||
"video": "Video",
|
|
||||||
"more_details": "Meer details",
|
|
||||||
"quality": "Kwaliteit",
|
|
||||||
"audio": "Audio",
|
|
||||||
"subtitles": "Ondertitel",
|
|
||||||
"show_more": "Toon meer",
|
|
||||||
"show_less": "Toon minden",
|
|
||||||
"appeared_in": "Verschenen in",
|
|
||||||
"could_not_load_item": "Kon item niet laden",
|
|
||||||
"none": "Geen",
|
|
||||||
"download": {
|
|
||||||
"download_season": "Download Seizoen",
|
|
||||||
"download_series": "Download Serie",
|
|
||||||
"download_episode": "Download Aflevering",
|
|
||||||
"download_movie": "Download Film",
|
|
||||||
"download_x_item": "Download {{item_count}} items",
|
|
||||||
"download_button": "Download",
|
|
||||||
"using_optimized_server": "Geoptimaliseerde server gebruiken",
|
|
||||||
"using_default_method": "Standaard methode gebruiken"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"live_tv": {
|
|
||||||
"next": "Volgende ",
|
|
||||||
"previous": "Vorige",
|
|
||||||
"live_tv": "Live TV",
|
|
||||||
"coming_soon": "Binnenkort beschikbaar",
|
|
||||||
"on_now": "Nu op",
|
|
||||||
"shows": "Shows",
|
|
||||||
"movies": "Films",
|
|
||||||
"sports": "Sport",
|
|
||||||
"for_kids": "Voor kinderen",
|
|
||||||
"news": "Nieuws"
|
|
||||||
},
|
|
||||||
"jellyseerr":{
|
|
||||||
"confirm": "Bevestig",
|
|
||||||
"cancel": "Annuleer",
|
|
||||||
"yes": "Ja",
|
|
||||||
"whats_wrong": "Wat is er mis?",
|
|
||||||
"issue_type": "Type probleem",
|
|
||||||
"select_an_issue": "Selecteer een probleem",
|
|
||||||
"types": "Types",
|
|
||||||
"describe_the_issue": "(optioneel) beschrijf het probleem...",
|
|
||||||
"submit_button": "Verzenden",
|
|
||||||
"report_issue_button": "Meld een probleem",
|
|
||||||
"request_button": "Aanvragen",
|
|
||||||
"are_you_sure_you_want_to_request_all_seasons": "Ben je zeker dat je alle seizoenen wil aanvragen?",
|
|
||||||
"failed_to_login": "Kon niet aanmelden",
|
|
||||||
"cast": "Cast",
|
|
||||||
"details": "Details",
|
|
||||||
"status": "Status",
|
|
||||||
"original_title": "Originele titel",
|
|
||||||
"series_type": "Serie Type",
|
|
||||||
"release_dates": "Verschijningsdatums",
|
|
||||||
"first_air_date": "Eerste uitzenddatum",
|
|
||||||
"next_air_date": "Volgende uitzenddatum",
|
|
||||||
"revenue": "Inkomsten",
|
|
||||||
"budget": "Budget",
|
|
||||||
"original_language": "Originele taal",
|
|
||||||
"production_country": "Land van productie",
|
|
||||||
"studios": "Studio",
|
|
||||||
"network": "Netwerk",
|
|
||||||
"currently_streaming_on": "Momenteel te streamen op",
|
|
||||||
"advanced": "Geavanceerd",
|
|
||||||
"request_as": "Vraag aan als",
|
|
||||||
"tags": "Labels",
|
|
||||||
"quality_profile": "Kwaliteitsprofiel",
|
|
||||||
"root_folder": "Hoofdmap",
|
|
||||||
"season_x": "Seizoen {{seasons}}",
|
|
||||||
"season_number": "Seizoen {{season_number}}",
|
|
||||||
"number_episodes": "{{episode_number}} Afleveringen",
|
|
||||||
"born": "Geboren",
|
|
||||||
"appearances": "Verschijningen",
|
|
||||||
"toasts": {
|
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0",
|
|
||||||
"jellyseerr_test_failed": "Jellyseerr test gefaald. Probeer opnieuw.",
|
|
||||||
"failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url",
|
|
||||||
"issue_submitted": "Probleem ingediend!",
|
|
||||||
"requested_item": "{{item}} aangevraagd!",
|
|
||||||
"you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!",
|
|
||||||
"something_went_wrong_requesting_media": "Er ging iets iets mis met het aavragen van media!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tabs": {
|
|
||||||
"home": "Thuis",
|
|
||||||
"search": "Zoeken",
|
|
||||||
"library": "Bibliotheek",
|
|
||||||
"custom_links": "Aangepaste links",
|
|
||||||
"favorites": "Favorieten"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,457 +0,0 @@
|
|||||||
{
|
|
||||||
"login": {
|
|
||||||
"username_required": "需要用戶名",
|
|
||||||
"error_title": "錯誤",
|
|
||||||
"login_title": "登入",
|
|
||||||
"login_to_title": "登入至",
|
|
||||||
"username_placeholder": "用戶名",
|
|
||||||
"password_placeholder": "密碼",
|
|
||||||
"login_button": "登入",
|
|
||||||
"quick_connect": "快速連接",
|
|
||||||
"enter_code_to_login": "輸入代碼 {{code}} 以登入",
|
|
||||||
"failed_to_initiate_quick_connect": "無法啟動快速連接",
|
|
||||||
"got_it": "知道了",
|
|
||||||
"connection_failed": "連接失敗",
|
|
||||||
"could_not_connect_to_server": "無法連接到伺服器。請檢查 URL 和您的網絡連接。",
|
|
||||||
"an_unexpected_error_occured": "發生意外錯誤",
|
|
||||||
"change_server": "更改伺服器",
|
|
||||||
"invalid_username_or_password": "無效的用戶名或密碼",
|
|
||||||
"user_does_not_have_permission_to_log_in": "用戶無權登入",
|
|
||||||
"server_is_taking_too_long_to_respond_try_again_later": "伺服器響應時間過長,請稍後再試",
|
|
||||||
"server_received_too_many_requests_try_again_later": "伺服器收到太多請求,請稍後再試。",
|
|
||||||
"there_is_a_server_error": "伺服器出錯",
|
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "發生意外錯誤。您是否正確輸入了伺服器 URL?"
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"enter_url_to_jellyfin_server": "輸入您的 Jellyfin 伺服器的 URL",
|
|
||||||
"server_url_placeholder": "http(s)://your-server.com",
|
|
||||||
"connect_button": "連接",
|
|
||||||
"previous_servers": "先前的伺服器",
|
|
||||||
"clear_button": "清除",
|
|
||||||
"search_for_local_servers": "搜尋本地伺服器",
|
|
||||||
"searching": "搜尋中...",
|
|
||||||
"servers": "伺服器"
|
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"no_internet": "無網絡",
|
|
||||||
"no_items": "無項目",
|
|
||||||
"no_internet_message": "別擔心,您仍然可以觀看\n已下載的內容。",
|
|
||||||
"go_to_downloads": "前往下載",
|
|
||||||
"oops": "哎呀!",
|
|
||||||
"error_message": "出錯了。\n請重新登出並登入。",
|
|
||||||
"continue_watching": "繼續觀看",
|
|
||||||
"next_up": "下一個",
|
|
||||||
"recently_added_in": "最近添加於 {{libraryName}}",
|
|
||||||
"suggested_movies": "推薦電影",
|
|
||||||
"suggested_episodes": "推薦劇集",
|
|
||||||
"intro": {
|
|
||||||
"welcome_to_streamyfin": "歡迎來到 Streamyfin",
|
|
||||||
"a_free_and_open_source_client_for_jellyfin": "一個免費且開源的 Jellyfin 客戶端。",
|
|
||||||
"features_title": "功能",
|
|
||||||
"features_description": "Streamyfin 擁有許多功能,並與多種軟體整合,您可以在設置菜單中找到這些功能,包括:",
|
|
||||||
"jellyseerr_feature_description": "連接到您的 Jellyseerr 實例並直接在應用程序中請求電影。",
|
|
||||||
"downloads_feature_title": "下載",
|
|
||||||
"downloads_feature_description": "下載電影和電視節目以離線觀看。使用默認方法或安裝 Optimized Server 以在背景中下載文件。",
|
|
||||||
"chromecast_feature_description": "將電影和電視節目投射到您的 Chromecast 設備。",
|
|
||||||
"centralised_settings_plugin_title": "統一設置插件",
|
|
||||||
"centralised_settings_plugin_description": "從 Jellyfin 伺服器上的統一位置改變設置。所有用戶的所有客戶端設置將會自動同步。",
|
|
||||||
"done_button": "完成",
|
|
||||||
"go_to_settings_button": "前往設置",
|
|
||||||
"read_more": "閱讀更多"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"settings_title": "設置",
|
|
||||||
"log_out_button": "登出",
|
|
||||||
"user_info": {
|
|
||||||
"user_info_title": "用戶信息",
|
|
||||||
"user": "用戶",
|
|
||||||
"server": "伺服器",
|
|
||||||
"token": "令牌",
|
|
||||||
"app_version": "應用版本"
|
|
||||||
},
|
|
||||||
"quick_connect": {
|
|
||||||
"quick_connect_title": "快速連接",
|
|
||||||
"authorize_button": "授權快速連接",
|
|
||||||
"enter_the_quick_connect_code": "輸入快速連接代碼...",
|
|
||||||
"success": "成功",
|
|
||||||
"quick_connect_autorized": "快速連接已授權",
|
|
||||||
"error": "錯誤",
|
|
||||||
"invalid_code": "無效代碼",
|
|
||||||
"authorize": "授權"
|
|
||||||
},
|
|
||||||
"media_controls": {
|
|
||||||
"media_controls_title": "媒體控制",
|
|
||||||
"forward_skip_length": "前進跳過長度",
|
|
||||||
"rewind_length": "倒帶長度",
|
|
||||||
"seconds_unit": "秒"
|
|
||||||
},
|
|
||||||
"audio": {
|
|
||||||
"audio_title": "音頻",
|
|
||||||
"set_audio_track": "從上一個項目設置音軌",
|
|
||||||
"audio_language": "音頻語言",
|
|
||||||
"audio_hint": "選擇默認音頻語言。",
|
|
||||||
"none": "無",
|
|
||||||
"language": "語言"
|
|
||||||
},
|
|
||||||
"subtitles": {
|
|
||||||
"subtitle_title": "字幕",
|
|
||||||
"subtitle_language": "字幕語言",
|
|
||||||
"subtitle_mode": "字幕模式",
|
|
||||||
"set_subtitle_track": "從上一個項目設置字幕軌道",
|
|
||||||
"subtitle_size": "字幕大小",
|
|
||||||
"subtitle_hint": "配置字幕偏好。",
|
|
||||||
"none": "無",
|
|
||||||
"language": "語言",
|
|
||||||
"loading": "加載中",
|
|
||||||
"modes": {
|
|
||||||
"Default": "默認",
|
|
||||||
"Smart": "智能",
|
|
||||||
"Always": "總是",
|
|
||||||
"None": "無",
|
|
||||||
"OnlyForced": "僅強制"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"other": {
|
|
||||||
"other_title": "其他",
|
|
||||||
"auto_rotate": "自動旋轉",
|
|
||||||
"video_orientation": "影片方向",
|
|
||||||
"orientation": "方向",
|
|
||||||
"orientations": {
|
|
||||||
"DEFAULT": "默認",
|
|
||||||
"ALL": "全部",
|
|
||||||
"PORTRAIT": "縱向",
|
|
||||||
"PORTRAIT_UP": "縱向向上",
|
|
||||||
"PORTRAIT_DOWN": "縱向向下",
|
|
||||||
"LANDSCAPE": "橫向",
|
|
||||||
"LANDSCAPE_LEFT": "橫向左",
|
|
||||||
"LANDSCAPE_RIGHT": "橫向右",
|
|
||||||
"OTHER": "其他",
|
|
||||||
"UNKNOWN": "未知"
|
|
||||||
},
|
|
||||||
"safe_area_in_controls": "控制中的安全區域",
|
|
||||||
"show_custom_menu_links": "顯示自定義菜單鏈接",
|
|
||||||
"hide_libraries": "隱藏媒體庫",
|
|
||||||
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
|
|
||||||
"disable_haptic_feedback": "禁用觸覺回饋"
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "下載",
|
|
||||||
"download_method": "下載方法",
|
|
||||||
"remux_max_download": "Remux 最大下載",
|
|
||||||
"auto_download": "自動下載",
|
|
||||||
"optimized_versions_server": "Optimized Version 伺服器",
|
|
||||||
"save_button": "保存",
|
|
||||||
"optimized_server": "Optimized Server",
|
|
||||||
"optimized": "優化",
|
|
||||||
"default": "默認",
|
|
||||||
"optimized_version_hint": "輸入 Optimized Server 的 URL。URL 應包括 http(s) 和端口 (可選)。",
|
|
||||||
"read_more_about_optimized_server": "閱讀更多關於 Optimized Server 的信息。",
|
|
||||||
"url": "URL",
|
|
||||||
"server_url_placeholder": "http(s)://domain.org:port"
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"plugins_title": "插件",
|
|
||||||
"jellyseerr": {
|
|
||||||
"jellyseerr_warning": "此集成處於早期階段。功能可能會有變化。",
|
|
||||||
"server_url": "伺服器 URL",
|
|
||||||
"server_url_hint": "示例:http(s)://your-host.url\n(如果需要,添加端口)",
|
|
||||||
"server_url_placeholder": "Jellyseerr URL...",
|
|
||||||
"password": "密碼",
|
|
||||||
"password_placeholder": "輸入 Jellyfin 用戶 {{username}} 的密碼",
|
|
||||||
"save_button": "保存",
|
|
||||||
"clear_button": "清除",
|
|
||||||
"login_button": "登入",
|
|
||||||
"total_media_requests": "總媒體請求",
|
|
||||||
"movie_quota_limit": "電影配額限制",
|
|
||||||
"movie_quota_days": "電影配額天數",
|
|
||||||
"tv_quota_limit": "電視配額限制",
|
|
||||||
"tv_quota_days": "電視配額天數",
|
|
||||||
"reset_jellyseerr_config_button": "重置 Jellyseerr 配置",
|
|
||||||
"unlimited": "無限制"
|
|
||||||
},
|
|
||||||
"marlin_search": {
|
|
||||||
"enable_marlin_search": "啟用 Marlin 搜索",
|
|
||||||
"url": "URL",
|
|
||||||
"server_url_placeholder": "http(s)://domain.org:port",
|
|
||||||
"marlin_search_hint": "輸入 Marlin 伺服器的 URL。URL 應包括 http(s) 和端口 (可選)。",
|
|
||||||
"read_more_about_marlin": "閱讀更多關於 Marlin 的信息。",
|
|
||||||
"save_button": "保存",
|
|
||||||
"toasts": {
|
|
||||||
"saved": "已保存"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"storage": {
|
|
||||||
"storage_title": "存儲",
|
|
||||||
"app_usage": "應用 {{usedSpace}}%",
|
|
||||||
"device_usage": "設備 {{availableSpace}}%",
|
|
||||||
"size_used": "已使用 {{used}} / {{total}}",
|
|
||||||
"delete_all_downloaded_files": "刪除所有已下載文件"
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"show_intro": "顯示介紹",
|
|
||||||
"reset_intro": "重置介紹"
|
|
||||||
},
|
|
||||||
"logs": {
|
|
||||||
"logs_title": "日誌",
|
|
||||||
"no_logs_available": "無可用日誌",
|
|
||||||
"delete_all_logs": "刪除所有日誌"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"title": "語言",
|
|
||||||
"app_language": "應用語言",
|
|
||||||
"app_language_description": "選擇應用的語言。",
|
|
||||||
"system": "系統"
|
|
||||||
},
|
|
||||||
"toasts": {
|
|
||||||
"error_deleting_files": "刪除文件時出錯",
|
|
||||||
"background_downloads_enabled": "背景下載已啟用",
|
|
||||||
"background_downloads_disabled": "背景下載已禁用",
|
|
||||||
"connected": "已連接",
|
|
||||||
"could_not_connect": "無法連接",
|
|
||||||
"invalid_url": "無效的 URL"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"downloads": {
|
|
||||||
"downloads_title": "下載",
|
|
||||||
"tvseries": "電視劇",
|
|
||||||
"movies": "電影",
|
|
||||||
"queue": "隊列",
|
|
||||||
"queue_hint": "應用重啟後隊列和下載將會丟失",
|
|
||||||
"no_items_in_queue": "隊列中無項目",
|
|
||||||
"no_downloaded_items": "無已下載項目",
|
|
||||||
"delete_all_movies_button": "刪除所有電影",
|
|
||||||
"delete_all_tvseries_button": "刪除所有電視劇",
|
|
||||||
"delete_all_button": "刪除全部",
|
|
||||||
"active_download": "活動下載",
|
|
||||||
"no_active_downloads": "無活動下載",
|
|
||||||
"active_downloads": "活動下載",
|
|
||||||
"new_app_version_requires_re_download": "新應用版本需要重新下載",
|
|
||||||
"new_app_version_requires_re_download_description": "新更新需要重新下載內容。請刪除所有已下載內容後再重試。",
|
|
||||||
"back": "返回",
|
|
||||||
"delete": "刪除",
|
|
||||||
"something_went_wrong": "出了些問題",
|
|
||||||
"could_not_get_stream_url_from_jellyfin": "無法從 Jellyfin 獲取串流 URL",
|
|
||||||
"eta": "預計完成時間 {{eta}}",
|
|
||||||
"methods": "方法",
|
|
||||||
"toasts": {
|
|
||||||
"you_are_not_allowed_to_download_files": "您無權下載文件。",
|
|
||||||
"deleted_all_movies_successfully": "成功刪除所有電影!",
|
|
||||||
"failed_to_delete_all_movies": "刪除所有電影失敗",
|
|
||||||
"deleted_all_tvseries_successfully": "成功刪除所有電視劇!",
|
|
||||||
"failed_to_delete_all_tvseries": "刪除所有電視劇失敗",
|
|
||||||
"download_cancelled": "下載已取消",
|
|
||||||
"could_not_cancel_download": "無法取消下載",
|
|
||||||
"download_completed": "下載完成",
|
|
||||||
"download_started_for": "開始下載 {{item}}",
|
|
||||||
"item_is_ready_to_be_downloaded": "{{item}} 準備好下載",
|
|
||||||
"download_stated_for_item": "開始下載 {{item}}",
|
|
||||||
"download_failed_for_item": "下載失敗 {{item}} - {{error}}",
|
|
||||||
"download_completed_for_item": "下載完成 {{item}}",
|
|
||||||
"queued_item_for_optimization": "已將 {{item}} 排隊進行優化",
|
|
||||||
"failed_to_start_download_for_item": "無法開始下載 {{item}}: {{message}}",
|
|
||||||
"server_responded_with_status_code": "伺服器響應狀態 {{statusCode}}",
|
|
||||||
"no_response_received_from_server": "未收到伺服器的響應",
|
|
||||||
"error_setting_up_the_request": "設置請求時出錯",
|
|
||||||
"failed_to_start_download_for_item_unexpected_error": "無法開始下載 {{item}}: 發生意外錯誤",
|
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "所有文件、文件夾和任務成功刪除",
|
|
||||||
"an_error_occured_while_deleting_files_and_jobs": "刪除文件和任務時發生錯誤",
|
|
||||||
"go_to_downloads": "前往下載"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"search_here": "在這裡搜索...",
|
|
||||||
"search": "搜索...",
|
|
||||||
"x_items": "{{count}} 項目",
|
|
||||||
"library": "媒體庫",
|
|
||||||
"discover": "發現",
|
|
||||||
"no_results": "沒有結果",
|
|
||||||
"no_results_found_for": "未找到結果",
|
|
||||||
"movies": "電影",
|
|
||||||
"series": "系列",
|
|
||||||
"episodes": "劇集",
|
|
||||||
"collections": "收藏",
|
|
||||||
"actors": "演員",
|
|
||||||
"request_movies": "請求電影",
|
|
||||||
"request_series": "請求系列",
|
|
||||||
"recently_added": "最近添加",
|
|
||||||
"recent_requests": "最近請求",
|
|
||||||
"plex_watchlist": "Plex 觀影清單",
|
|
||||||
"trending": "趨勢",
|
|
||||||
"popular_movies": "熱門電影",
|
|
||||||
"movie_genres": "電影類型",
|
|
||||||
"upcoming_movies": "即將上映的電影",
|
|
||||||
"studios": "工作室",
|
|
||||||
"popular_tv": "熱門電視",
|
|
||||||
"tv_genres": "電視類型",
|
|
||||||
"upcoming_tv": "即將上映的電視",
|
|
||||||
"networks": "網絡",
|
|
||||||
"tmdb_movie_keyword": "TMDB 電影關鍵詞",
|
|
||||||
"tmdb_movie_genre": "TMDB 電影類型",
|
|
||||||
"tmdb_tv_keyword": "TMDB 電視關鍵詞",
|
|
||||||
"tmdb_tv_genre": "TMDB 電視類型",
|
|
||||||
"tmdb_search": "TMDB 搜索",
|
|
||||||
"tmdb_studio": "TMDB 工作室",
|
|
||||||
"tmdb_network": "TMDB 網絡",
|
|
||||||
"tmdb_movie_streaming_services": "TMDB 電影流媒體服務",
|
|
||||||
"tmdb_tv_streaming_services": "TMDB 電視流媒體服務"
|
|
||||||
},
|
|
||||||
"library": {
|
|
||||||
"no_items_found": "未找到項目",
|
|
||||||
"no_results": "沒有結果",
|
|
||||||
"no_libraries_found": "未找到媒體庫",
|
|
||||||
"item_types": {
|
|
||||||
"movies": "電影",
|
|
||||||
"series": "系列",
|
|
||||||
"boxsets": "套裝",
|
|
||||||
"items": "項目"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"display": "顯示",
|
|
||||||
"row": "行",
|
|
||||||
"list": "列表",
|
|
||||||
"image_style": "圖片樣式",
|
|
||||||
"poster": "海報",
|
|
||||||
"cover": "封面",
|
|
||||||
"show_titles": "顯示標題",
|
|
||||||
"show_stats": "顯示統計"
|
|
||||||
},
|
|
||||||
"filters": {
|
|
||||||
"genres": "類型",
|
|
||||||
"years": "年份",
|
|
||||||
"sort_by": "排序依據",
|
|
||||||
"sort_order": "排序順序",
|
|
||||||
"tags": "標籤"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"favorites": {
|
|
||||||
"series": "系列",
|
|
||||||
"movies": "電影",
|
|
||||||
"episodes": "劇集",
|
|
||||||
"videos": "影片",
|
|
||||||
"boxsets": "套裝",
|
|
||||||
"playlists": "播放列表"
|
|
||||||
},
|
|
||||||
"custom_links": {
|
|
||||||
"no_links": "無鏈接"
|
|
||||||
},
|
|
||||||
"player": {
|
|
||||||
"error": "錯誤",
|
|
||||||
"failed_to_get_stream_url": "無法獲取流 URL",
|
|
||||||
"an_error_occured_while_playing_the_video": "播放影片時發生錯誤。請檢查設置中的日誌。",
|
|
||||||
"client_error": "客戶端錯誤",
|
|
||||||
"could_not_create_stream_for_chromecast": "無法為 Chromecast 建立串流",
|
|
||||||
"message_from_server": "來自伺服器的消息:{{message}}",
|
|
||||||
"video_has_finished_playing": "影片播放完畢!",
|
|
||||||
"no_video_source": "無影片來源...",
|
|
||||||
"next_episode": "下一集",
|
|
||||||
"refresh_tracks": "刷新軌道",
|
|
||||||
"subtitle_tracks": "字幕軌道:",
|
|
||||||
"audio_tracks": "音頻軌道:",
|
|
||||||
"playback_state": "播放狀態:",
|
|
||||||
"no_data_available": "無可用數據",
|
|
||||||
"index": "索引:"
|
|
||||||
},
|
|
||||||
"item_card": {
|
|
||||||
"next_up": "下一個",
|
|
||||||
"no_items_to_display": "無項目顯示",
|
|
||||||
"cast_and_crew": "演員和工作人員",
|
|
||||||
"series": "系列",
|
|
||||||
"seasons": "季",
|
|
||||||
"season": "季",
|
|
||||||
"no_episodes_for_this_season": "本季無劇集",
|
|
||||||
"overview": "概覽",
|
|
||||||
"more_with": "更多 {{name}} 的作品",
|
|
||||||
"similar_items": "類似項目",
|
|
||||||
"no_similar_items_found": "未找到類似項目",
|
|
||||||
"video": "影片",
|
|
||||||
"more_details": "更多詳情",
|
|
||||||
"quality": "質量",
|
|
||||||
"audio": "音頻",
|
|
||||||
"subtitles": "字幕",
|
|
||||||
"show_more": "顯示更多",
|
|
||||||
"show_less": "顯示更少",
|
|
||||||
"appeared_in": "出現於",
|
|
||||||
"could_not_load_item": "無法加載項目",
|
|
||||||
"none": "無",
|
|
||||||
"download": {
|
|
||||||
"download_season": "下載季度",
|
|
||||||
"download_series": "下載系列",
|
|
||||||
"download_episode": "下載劇集",
|
|
||||||
"download_movie": "下載電影",
|
|
||||||
"download_x_item": "下載 {{item_count}} 項目",
|
|
||||||
"download_button": "下載",
|
|
||||||
"using_optimized_server": "使用 Optimized Server",
|
|
||||||
"using_default_method": "使用默認方法"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"live_tv": {
|
|
||||||
"next": "下一個",
|
|
||||||
"previous": "上一個",
|
|
||||||
"live_tv": "直播電視",
|
|
||||||
"coming_soon": "即將推出",
|
|
||||||
"on_now": "正在播放",
|
|
||||||
"shows": "節目",
|
|
||||||
"movies": "電影",
|
|
||||||
"sports": "體育",
|
|
||||||
"for_kids": "兒童",
|
|
||||||
"news": "新聞"
|
|
||||||
},
|
|
||||||
"jellyseerr": {
|
|
||||||
"confirm": "確認",
|
|
||||||
"cancel": "取消",
|
|
||||||
"yes": "是",
|
|
||||||
"whats_wrong": "出了什麼問題?",
|
|
||||||
"issue_type": "問題類型",
|
|
||||||
"select_an_issue": "選擇一個問題",
|
|
||||||
"types": "類型",
|
|
||||||
"describe_the_issue": "(可選)描述問題...",
|
|
||||||
"submit_button": "提交",
|
|
||||||
"report_issue_button": "報告問題",
|
|
||||||
"request_button": "請求",
|
|
||||||
"are_you_sure_you_want_to_request_all_seasons": "您確定要請求所有季度的節目嗎?",
|
|
||||||
"failed_to_login": "登入失敗",
|
|
||||||
"cast": "演員",
|
|
||||||
"details": "詳情",
|
|
||||||
"status": "狀態",
|
|
||||||
"original_title": "原標題",
|
|
||||||
"series_type": "系列類型",
|
|
||||||
"release_dates": "發行日期",
|
|
||||||
"first_air_date": "首次播出日期",
|
|
||||||
"next_air_date": "下次播出日期",
|
|
||||||
"revenue": "收入",
|
|
||||||
"budget": "預算",
|
|
||||||
"original_language": "原始語言",
|
|
||||||
"production_country": "製作國家",
|
|
||||||
"studios": "工作室",
|
|
||||||
"network": "網絡",
|
|
||||||
"currently_streaming_on": "目前在以下流媒體上播放",
|
|
||||||
"advanced": "高級",
|
|
||||||
"request_as": "請求為",
|
|
||||||
"tags": "標籤",
|
|
||||||
"quality_profile": "質量配置文件",
|
|
||||||
"root_folder": "根文件夾",
|
|
||||||
"season_x": "第 {{seasons}} 季",
|
|
||||||
"season_number": "第 {{season_number}} 季",
|
|
||||||
"number_episodes": "{{episode_number}} 集",
|
|
||||||
"born": "出生",
|
|
||||||
"appearances": "出場",
|
|
||||||
"toasts": {
|
|
||||||
"jellyseer_does_not_meet_requirements": "Jellyseerr 伺服器不符合最低版本要求!請更新至至少 2.0.0",
|
|
||||||
"jellyseerr_test_failed": "Jellyseerr 測試失敗。請再試一次。",
|
|
||||||
"failed_to_test_jellyseerr_server_url": "無法測試 Jellyseerr 伺服器 URL",
|
|
||||||
"issue_submitted": "問題已提交!",
|
|
||||||
"requested_item": "已請求 {{item}}!",
|
|
||||||
"you_dont_have_permission_to_request": "您無權請求媒體!",
|
|
||||||
"something_went_wrong_requesting_media": "請求媒體時出了些問題!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tabs": {
|
|
||||||
"home": "主頁",
|
|
||||||
"search": "搜索",
|
|
||||||
"library": "庫",
|
|
||||||
"custom_links": "自定義鏈接",
|
|
||||||
"favorites": "收藏"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,15 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".expo/types/**/*.ts",
|
||||||
|
"expo-env.d.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,8 +268,6 @@ export const useSettings = () => {
|
|||||||
const newSettings = { ..._settings, ...update };
|
const newSettings = { ..._settings, ...update };
|
||||||
|
|
||||||
setSettings(newSettings);
|
setSettings(newSettings);
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
saveSettings(newSettings);
|
saveSettings(newSettings);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export const getStreamUrl = async ({
|
|||||||
mediaSource: MediaSourceInfo | undefined;
|
mediaSource: MediaSourceInfo | undefined;
|
||||||
} | null> => {
|
} | null> => {
|
||||||
if (!api || !userId || !item?.Id) {
|
if (!api || !userId || !item?.Id) {
|
||||||
console.warn("Missing required parameters for getStreamUrl");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +120,9 @@ export const getStreamUrl = async ({
|
|||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
mediaSource,
|
mediaSource,
|
||||||
};
|
};
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (mediaSource?.SupportsDirectPlay) {
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
playSessionId: sessionData?.PlaySessionId || "",
|
playSessionId: sessionData?.PlaySessionId || "",
|
||||||
mediaSourceId: mediaSource?.Id || "",
|
mediaSourceId: mediaSource?.Id || "",
|
||||||
@@ -148,4 +149,39 @@ export const getStreamUrl = async ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.MediaType === "Audio") {
|
||||||
|
if (mediaSource?.TranscodingUrl) {
|
||||||
|
return {
|
||||||
|
url: `${api.basePath}${mediaSource.TranscodingUrl}`,
|
||||||
|
sessionId,
|
||||||
|
mediaSource,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
UserId: userId,
|
||||||
|
DeviceId: api.deviceInfo.id,
|
||||||
|
MaxStreamingBitrate: "140000000",
|
||||||
|
Container:
|
||||||
|
"opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg",
|
||||||
|
TranscodingContainer: "mp4",
|
||||||
|
TranscodingProtocol: "hls",
|
||||||
|
AudioCodec: "aac",
|
||||||
|
api_key: api.accessToken,
|
||||||
|
PlaySessionId: sessionData?.PlaySessionId || "",
|
||||||
|
StartTimeTicks: "0",
|
||||||
|
EnableRedirection: "true",
|
||||||
|
EnableRemoteMedia: "false",
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
url: `${
|
||||||
|
api.basePath
|
||||||
|
}/Audio/${itemId}/universal?${searchParams.toString()}`,
|
||||||
|
sessionId,
|
||||||
|
mediaSource,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unsupported media type");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const chromecastProfile: DeviceProfile = {
|
|||||||
Codec: "aac,mp3,flac,opus,vorbis",
|
Codec: "aac,mp3,flac,opus,vorbis",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
ContainerProfiles: [],
|
|
||||||
DirectPlayProfiles: [
|
DirectPlayProfiles: [
|
||||||
{
|
{
|
||||||
Container: "mp4",
|
Container: "mp4",
|
||||||
|
|||||||
Reference in New Issue
Block a user