mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
Merge pull request #340 from simoncaron/feat/i18n
Implement translation with i18next
This commit is contained in:
1
app.json
1
app.json
@@ -105,6 +105,7 @@
|
|||||||
"motionPermission": "Allow Streamyfin to access your device motion for landscape video watching."
|
"motionPermission": "Allow Streamyfin to access your device motion for landscape video watching."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"expo-localization",
|
||||||
"expo-asset",
|
"expo-asset",
|
||||||
[
|
[
|
||||||
"react-native-edge-to-edge",
|
"react-native-edge-to-edge",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import {Stack} from "expo-router";
|
import {Stack} from "expo-router";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function CustomMenuLayout() {
|
export default function CustomMenuLayout() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -9,7 +11,7 @@ export default function CustomMenuLayout() {
|
|||||||
options={{
|
options={{
|
||||||
headerShown: true,
|
headerShown: true,
|
||||||
headerLargeTitle: true,
|
headerLargeTitle: true,
|
||||||
headerTitle: "Custom Links",
|
headerTitle: t("tabs.custom_links"),
|
||||||
headerBlurEffect: "prominent",
|
headerBlurEffect: "prominent",
|
||||||
headerTransparent: Platform.OS === "ios",
|
headerTransparent: Platform.OS === "ios",
|
||||||
headerShadowVisible: false,
|
headerShadowVisible: false,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ListItem } from "@/components/list/ListItem";
|
|||||||
import * as WebBrowser from "expo-web-browser";
|
import * as WebBrowser from "expo-web-browser";
|
||||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface MenuLink {
|
export interface MenuLink {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -18,6 +19,7 @@ export default function menuLinks() {
|
|||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [menuLinks, setMenuLinks] = useState<MenuLink[]>([]);
|
const [menuLinks, setMenuLinks] = useState<MenuLink[]>([]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const getMenuLinks = useCallback(async () => {
|
const getMenuLinks = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -67,7 +69,7 @@ export default function menuLinks() {
|
|||||||
)}
|
)}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View className="flex flex-col items-center justify-center h-full">
|
<View className="flex flex-col items-center justify-center h-full">
|
||||||
<Text className="font-bold text-xl text-neutral-500">No links</Text>
|
<Text className="font-bold text-xl text-neutral-500">{t("custom_links.no_links")}</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function SearchLayout() {
|
export default function SearchLayout() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -10,7 +12,7 @@ export default function SearchLayout() {
|
|||||||
options={{
|
options={{
|
||||||
headerShown: true,
|
headerShown: true,
|
||||||
headerLargeTitle: true,
|
headerLargeTitle: true,
|
||||||
headerTitle: "Favorites",
|
headerTitle: t("tabs.favorites"),
|
||||||
headerLargeStyle: {
|
headerLargeStyle: {
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageSta
|
|||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import { Stack, useRouter } from "expo-router";
|
import { Stack, useRouter } from "expo-router";
|
||||||
import { Platform, TouchableOpacity, View } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function IndexLayout() {
|
export default function IndexLayout() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -14,7 +16,7 @@ export default function IndexLayout() {
|
|||||||
options={{
|
options={{
|
||||||
headerShown: true,
|
headerShown: true,
|
||||||
headerLargeTitle: true,
|
headerLargeTitle: true,
|
||||||
headerTitle: "Home",
|
headerTitle: t("tabs.home"),
|
||||||
headerBlurEffect: "prominent",
|
headerBlurEffect: "prominent",
|
||||||
headerLargeStyle: {
|
headerLargeStyle: {
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
@@ -38,19 +40,19 @@ export default function IndexLayout() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="downloads/index"
|
name="downloads/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Downloads",
|
title: t("home.downloads.downloads_title"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="downloads/[seriesId]"
|
name="downloads/[seriesId]"
|
||||||
options={{
|
options={{
|
||||||
title: "TV-Series",
|
title: t("home.downloads.tvseries"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="settings"
|
name="settings"
|
||||||
options={{
|
options={{
|
||||||
title: "Settings",
|
title: t("home.settings.settings_title"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import React, { useEffect, useMemo, useRef } from "react";
|
|||||||
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
|
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { t } from 'i18next';
|
||||||
import { DownloadSize } from "@/components/downloads/DownloadSize";
|
import { DownloadSize } from "@/components/downloads/DownloadSize";
|
||||||
import {
|
import {
|
||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
@@ -24,6 +26,7 @@ import { writeToLog } from "@/utils/log";
|
|||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [queue, setQueue] = useAtom(queueAtom);
|
const [queue, setQueue] = useAtom(queueAtom);
|
||||||
const { removeProcess, downloadedFiles, deleteFileByType } = useDownload();
|
const { removeProcess, downloadedFiles, deleteFileByType } = useDownload();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -70,17 +73,17 @@ export default function page() {
|
|||||||
|
|
||||||
const deleteMovies = () =>
|
const deleteMovies = () =>
|
||||||
deleteFileByType("Movie")
|
deleteFileByType("Movie")
|
||||||
.then(() => toast.success("Deleted all movies successfully!"))
|
.then(() => toast.success(t("home.downloads.toasts.deleted_all_movies_successfully")))
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
writeToLog("ERROR", reason);
|
writeToLog("ERROR", reason);
|
||||||
toast.error("Failed to delete all movies");
|
toast.error(t("home.downloads.toasts.failed_to_delete_all_movies"));
|
||||||
});
|
});
|
||||||
const deleteShows = () =>
|
const deleteShows = () =>
|
||||||
deleteFileByType("Episode")
|
deleteFileByType("Episode")
|
||||||
.then(() => toast.success("Deleted all TV-Series successfully!"))
|
.then(() => toast.success(t("home.downloads.toasts.deleted_all_tvseries_successfully")))
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
writeToLog("ERROR", reason);
|
writeToLog("ERROR", reason);
|
||||||
toast.error("Failed to delete all TV-Series");
|
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
|
||||||
});
|
});
|
||||||
const deleteAllMedia = async () =>
|
const deleteAllMedia = async () =>
|
||||||
await Promise.all([deleteMovies(), deleteShows()]);
|
await Promise.all([deleteMovies(), deleteShows()]);
|
||||||
@@ -98,9 +101,9 @@ export default function page() {
|
|||||||
<View className="mb-4 flex flex-col space-y-4 px-4">
|
<View className="mb-4 flex flex-col space-y-4 px-4">
|
||||||
{settings?.downloadMethod === DownloadMethod.Remux && (
|
{settings?.downloadMethod === DownloadMethod.Remux && (
|
||||||
<View className="bg-neutral-900 p-4 rounded-2xl">
|
<View className="bg-neutral-900 p-4 rounded-2xl">
|
||||||
<Text className="text-lg font-bold">Queue</Text>
|
<Text className="text-lg font-bold">{t("home.downloads.queue")}</Text>
|
||||||
<Text className="text-xs opacity-70 text-red-600">
|
<Text className="text-xs opacity-70 text-red-600">
|
||||||
Queue and active downloads will be lost on app restart
|
{t("home.downloads.queue_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
<View className="flex flex-col space-y-2 mt-2">
|
<View className="flex flex-col space-y-2 mt-2">
|
||||||
{queue.map((q, index) => (
|
{queue.map((q, index) => (
|
||||||
@@ -133,7 +136,7 @@ export default function page() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{queue.length === 0 && (
|
{queue.length === 0 && (
|
||||||
<Text className="opacity-50">No items in queue</Text>
|
<Text className="opacity-50">{t("home.downloads.no_items_in_queue")}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -144,7 +147,7 @@ export default function page() {
|
|||||||
{movies.length > 0 && (
|
{movies.length > 0 && (
|
||||||
<View className="mb-4">
|
<View className="mb-4">
|
||||||
<View className="flex flex-row items-center justify-between mb-2 px-4">
|
<View className="flex flex-row items-center justify-between mb-2 px-4">
|
||||||
<Text className="text-lg font-bold">Movies</Text>
|
<Text className="text-lg font-bold">{t("home.downloads.movies")}</Text>
|
||||||
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
|
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
|
||||||
<Text className="text-xs font-bold">{movies?.length}</Text>
|
<Text className="text-xs font-bold">{movies?.length}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -163,7 +166,7 @@ export default function page() {
|
|||||||
{groupedBySeries.length > 0 && (
|
{groupedBySeries.length > 0 && (
|
||||||
<View className="mb-4">
|
<View className="mb-4">
|
||||||
<View className="flex flex-row items-center justify-between mb-2 px-4">
|
<View className="flex flex-row items-center justify-between mb-2 px-4">
|
||||||
<Text className="text-lg font-bold">TV-Series</Text>
|
<Text className="text-lg font-bold">{t("home.downloads.tvseries")}</Text>
|
||||||
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
|
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
|
||||||
<Text className="text-xs font-bold">
|
<Text className="text-xs font-bold">
|
||||||
{groupedBySeries?.length}
|
{groupedBySeries?.length}
|
||||||
@@ -189,7 +192,7 @@ export default function page() {
|
|||||||
)}
|
)}
|
||||||
{downloadedFiles?.length === 0 && (
|
{downloadedFiles?.length === 0 && (
|
||||||
<View className="flex px-4">
|
<View className="flex px-4">
|
||||||
<Text className="opacity-50">No downloaded items</Text>
|
<Text className="opacity-50">{t("home.downloads.no_downloaded_items")}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -214,13 +217,13 @@ export default function page() {
|
|||||||
<BottomSheetView>
|
<BottomSheetView>
|
||||||
<View className="p-4 space-y-4 mb-4">
|
<View className="p-4 space-y-4 mb-4">
|
||||||
<Button color="purple" onPress={deleteMovies}>
|
<Button color="purple" onPress={deleteMovies}>
|
||||||
Delete all Movies
|
{t("home.downloads.delete_all_movies_button")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="purple" onPress={deleteShows}>
|
<Button color="purple" onPress={deleteShows}>
|
||||||
Delete all TV-Series
|
{t("home.downloads.delete_all_tvseries_button")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="red" onPress={deleteAllMedia}>
|
<Button color="red" onPress={deleteAllMedia}>
|
||||||
Delete all
|
{t("home.downloads.delete_all_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</BottomSheetView>
|
</BottomSheetView>
|
||||||
@@ -233,15 +236,15 @@ function migration_20241124() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { deleteAllFiles } = useDownload();
|
const { deleteAllFiles } = useDownload();
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"New app version requires re-download",
|
t("home.downloads.new_app_version_requires_re_download"),
|
||||||
"The new update reqires content to be downloaded again. Please remove all downloaded content and try again.",
|
t("home.downloads.new_app_version_requires_re_download_description"),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: "Back",
|
text: t("home.downloads.back"),
|
||||||
onPress: () => router.back(),
|
onPress: () => router.back(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Delete",
|
text: t("home.downloads.delete"),
|
||||||
style: "destructive",
|
style: "destructive",
|
||||||
onPress: async () => await deleteAllFiles(),
|
onPress: async () => await deleteAllFiles(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { QueryFunction, useQuery } from "@tanstack/react-query";
|
|||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation, useRouter } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
@@ -55,6 +56,8 @@ type Section = ScrollingCollectionListSection | MediaListSection;
|
|||||||
export default function index() {
|
export default function index() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
|
|
||||||
@@ -204,7 +207,7 @@ export default function index() {
|
|||||||
const latestMediaViews = collections.map((c) => {
|
const latestMediaViews = collections.map((c) => {
|
||||||
const includeItemTypes: BaseItemKind[] =
|
const includeItemTypes: BaseItemKind[] =
|
||||||
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
|
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
|
||||||
const title = "Recently Added in " + c.Name;
|
const title = t("home.recently_added_in", {libraryName: c.Name});
|
||||||
const queryKey = [
|
const queryKey = [
|
||||||
"home",
|
"home",
|
||||||
"recentlyAddedIn" + c.CollectionType,
|
"recentlyAddedIn" + c.CollectionType,
|
||||||
@@ -221,7 +224,7 @@ export default function index() {
|
|||||||
|
|
||||||
const ss: Section[] = [
|
const ss: Section[] = [
|
||||||
{
|
{
|
||||||
title: "Continue Watching",
|
title: t("home.continue_watching"),
|
||||||
queryKey: ["home", "resumeItems"],
|
queryKey: ["home", "resumeItems"],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
(
|
(
|
||||||
@@ -235,7 +238,7 @@ export default function index() {
|
|||||||
orientation: "horizontal",
|
orientation: "horizontal",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Next Up",
|
title: t("home.next_up"),
|
||||||
queryKey: ["home", "nextUp-all"],
|
queryKey: ["home", "nextUp-all"],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
(
|
(
|
||||||
@@ -262,7 +265,7 @@ export default function index() {
|
|||||||
// } as Section)
|
// } as Section)
|
||||||
// ) || []),
|
// ) || []),
|
||||||
{
|
{
|
||||||
title: "Suggested Movies",
|
title: t("home.suggested_movies"),
|
||||||
queryKey: ["home", "suggestedMovies", user?.Id],
|
queryKey: ["home", "suggestedMovies", user?.Id],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
(
|
(
|
||||||
@@ -277,7 +280,7 @@ export default function index() {
|
|||||||
orientation: "vertical",
|
orientation: "vertical",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Suggested Episodes",
|
title: t("home.suggested_episodes"),
|
||||||
queryKey: ["home", "suggestedEpisodes", user?.Id],
|
queryKey: ["home", "suggestedEpisodes", user?.Id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
@@ -347,9 +350,9 @@ export default function index() {
|
|||||||
if (isConnected === false) {
|
if (isConnected === false) {
|
||||||
return (
|
return (
|
||||||
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
|
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
|
||||||
<Text className="text-3xl font-bold mb-2">No Internet</Text>
|
<Text className="text-3xl font-bold mb-2">{t("home.no_internet")}</Text>
|
||||||
<Text className="text-center opacity-70">
|
<Text className="text-center opacity-70">
|
||||||
No worries, you can still watch{"\n"}downloaded content.
|
{t("home.no_internet_message")}
|
||||||
</Text>
|
</Text>
|
||||||
<View className="mt-4">
|
<View className="mt-4">
|
||||||
<Button
|
<Button
|
||||||
@@ -360,7 +363,7 @@ export default function index() {
|
|||||||
<Ionicons name="arrow-forward" size={20} color="white" />
|
<Ionicons name="arrow-forward" size={20} color="white" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Go to downloads
|
{t("home.go_to_downloads")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="black"
|
color="black"
|
||||||
@@ -389,10 +392,8 @@ export default function index() {
|
|||||||
if (e1)
|
if (e1)
|
||||||
return (
|
return (
|
||||||
<View className="flex flex-col items-center justify-center h-full -mt-6">
|
<View className="flex flex-col items-center justify-center h-full -mt-6">
|
||||||
<Text className="text-3xl font-bold mb-2">Oops!</Text>
|
<Text className="text-3xl font-bold mb-2">{t("home.oops")}</Text>
|
||||||
<Text className="text-center opacity-70">
|
<Text className="text-center opacity-70">{t("home.error_message")}</Text>
|
||||||
Something went wrong.{"\n"}Please log out and in again.
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { Feather, Ionicons } from "@expo/vector-icons";
|
|||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useFocusEffect, useRouter } from "expo-router";
|
import { useFocusEffect, useRouter } from "expo-router";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import {useTranslation } from "react-i18next";
|
||||||
import { Linking, TouchableOpacity, View } from "react-native";
|
import { Linking, TouchableOpacity, View } from "react-native";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -20,18 +22,17 @@ export default function page() {
|
|||||||
<View className="bg-neutral-900 h-full py-16 px-4 space-y-8">
|
<View className="bg-neutral-900 h-full py-16 px-4 space-y-8">
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-3xl font-bold text-center mb-2">
|
<Text className="text-3xl font-bold text-center mb-2">
|
||||||
Welcome to Streamyfin
|
{t("home.intro.welcome_to_streamyfin")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-center">
|
<Text className="text-center">
|
||||||
A free and open source client for Jellyfin.
|
{t("home.intro.a_free_and_open_source_client_for_jellyfin")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-lg font-bold">Features</Text>
|
<Text className="text-lg font-bold">{t("home.intro.features_title")}</Text>
|
||||||
<Text className="text-xs">
|
<Text className="text-xs">
|
||||||
Streamyfin has a bunch of features and integrates with a wide array of
|
{t("home.intro.features_description")}
|
||||||
software which you can find in the settings menu, these include:
|
|
||||||
</Text>
|
</Text>
|
||||||
<View className="flex flex-row items-center mt-4">
|
<View className="flex flex-row items-center mt-4">
|
||||||
<Image
|
<Image
|
||||||
@@ -44,8 +45,7 @@ export default function page() {
|
|||||||
<View className="shrink ml-2">
|
<View className="shrink ml-2">
|
||||||
<Text className="font-bold mb-1">Jellyseerr</Text>
|
<Text className="font-bold mb-1">Jellyseerr</Text>
|
||||||
<Text className="shrink text-xs">
|
<Text className="shrink text-xs">
|
||||||
Connect to your Jellyseerr instance and request movies directly in
|
{t("home.intro.jellyseerr_feature_description")}
|
||||||
the app.
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -60,11 +60,9 @@ export default function page() {
|
|||||||
<Ionicons name="cloud-download-outline" size={32} color="white" />
|
<Ionicons name="cloud-download-outline" size={32} color="white" />
|
||||||
</View>
|
</View>
|
||||||
<View className="shrink ml-2">
|
<View className="shrink ml-2">
|
||||||
<Text className="font-bold mb-1">Downloads</Text>
|
<Text className="font-bold mb-1">{t("home.intro.downloads_feature_title")}</Text>
|
||||||
<Text className="shrink text-xs">
|
<Text className="shrink text-xs">
|
||||||
Download movies and tv-shows to view offline. Use either the
|
{t("home.intro.downloads_feature_description")}
|
||||||
default method or install the optimize server to download files in
|
|
||||||
the background.
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -81,7 +79,7 @@ export default function page() {
|
|||||||
<View className="shrink ml-2">
|
<View className="shrink ml-2">
|
||||||
<Text className="font-bold mb-1">Chromecast</Text>
|
<Text className="font-bold mb-1">Chromecast</Text>
|
||||||
<Text className="shrink text-xs">
|
<Text className="shrink text-xs">
|
||||||
Cast movies and tv-shows to your Chromecast devices.
|
{t("home.intro.chromecast_feature_description")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -96,11 +94,9 @@ export default function page() {
|
|||||||
<Feather name="settings" size={28} color={"white"} />
|
<Feather name="settings" size={28} color={"white"} />
|
||||||
</View>
|
</View>
|
||||||
<View className="shrink ml-2">
|
<View className="shrink ml-2">
|
||||||
<Text className="font-bold mb-1">Centralised Settings Plugin</Text>
|
<Text className="font-bold mb-1">{t("home.intro.centralised_settings_plugin_title")}</Text>
|
||||||
<Text className="shrink text-xs">
|
<Text className="shrink text-xs">
|
||||||
Configure settings from a centralised location on your Jellyfin
|
{t("home.intro.centralised_settings_plugin_description")}{" "}
|
||||||
server. All client settings for all users will be synced
|
|
||||||
automatically.{" "}
|
|
||||||
<Text
|
<Text
|
||||||
className="text-purple-600"
|
className="text-purple-600"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -109,7 +105,7 @@ export default function page() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Read more
|
{t("home.intro.read_more")}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -122,7 +118,7 @@ export default function page() {
|
|||||||
}}
|
}}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
>
|
>
|
||||||
Done
|
{t("home.intro.done_button")}
|
||||||
</Button>
|
</Button>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -131,7 +127,7 @@ export default function page() {
|
|||||||
}}
|
}}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
>
|
>
|
||||||
<Text className="text-purple-600 text-center">Go to settings</Text>
|
<Text className="text-purple-600 text-center">{t("home.intro.go_to_settings_button")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ 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 { useJellyfin } from "@/providers/JellyfinProvider";
|
import { useJellyfin } from "@/providers/JellyfinProvider";
|
||||||
import { clearLogs } from "@/utils/log";
|
import { clearLogs } from "@/utils/log";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation, useRouter } from "expo-router";
|
||||||
|
import { t } from "i18next";
|
||||||
import React, { useEffect } from "react";
|
import React, { 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";
|
||||||
@@ -40,7 +42,7 @@ export default function settings() {
|
|||||||
logout();
|
logout();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text className="text-red-600">Log out</Text>
|
<Text className="text-red-600">{t("home.settings.log_out_button")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -68,33 +70,35 @@ export default function settings() {
|
|||||||
|
|
||||||
<PluginSettings />
|
<PluginSettings />
|
||||||
|
|
||||||
|
<AppLanguageSelector/>
|
||||||
|
|
||||||
<ListGroup title={"Intro"}>
|
<ListGroup title={"Intro"}>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.push("/intro/page");
|
router.push("/intro/page");
|
||||||
}}
|
}}
|
||||||
title={"Show intro"}
|
title={t("home.settings.intro.show_intro")}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
textColor="red"
|
textColor="red"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
storage.set("hasShownIntro", false);
|
storage.set("hasShownIntro", false);
|
||||||
}}
|
}}
|
||||||
title={"Reset intro"}
|
title={t("home.settings.intro.reset_intro")}
|
||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
<View className="mb-4">
|
<View className="mb-4">
|
||||||
<ListGroup title={"Logs"}>
|
<ListGroup title={t("home.settings.logs.logs_title")}>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => router.push("/settings/logs/page")}
|
onPress={() => router.push("/settings/logs/page")}
|
||||||
showArrow
|
showArrow
|
||||||
title={"Logs"}
|
title={t("home.settings.logs.logs_title")}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
textColor="red"
|
textColor="red"
|
||||||
onPress={onClearLogsClicked}
|
onPress={onClearLogsClicked}
|
||||||
title={"Delete All Logs"}
|
title={t("home.settings.logs.delete_all_logs")}
|
||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { Switch, View } from "react-native";
|
import { Switch, View } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
@@ -15,6 +16,8 @@ export default function page() {
|
|||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, isLoading: isLoading } = useQuery({
|
const { data, isLoading: isLoading } = useQuery({
|
||||||
queryKey: ["user-views", user?.Id],
|
queryKey: ["user-views", user?.Id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -57,8 +60,7 @@ export default function page() {
|
|||||||
))}
|
))}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
||||||
Select the libraries you want to hide from the Library tab and home page
|
{t("home.settings.other.select_liraries_you_want_to_hide")}
|
||||||
sections.
|
|
||||||
</Text>
|
</Text>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { useLog } from "@/utils/log";
|
import { useLog } from "@/utils/log";
|
||||||
import { ScrollView, View } from "react-native";
|
import { ScrollView, View } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const { logs } = useLog();
|
const { logs } = useLog();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView className="p-4">
|
<ScrollView className="p-4">
|
||||||
@@ -25,7 +27,7 @@ export default function page() {
|
|||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
{logs?.length === 0 && (
|
{logs?.length === 0 && (
|
||||||
<Text className="opacity-50">No logs available</Text>
|
<Text className="opacity-50">{t("home.settings.logs.no_logs_available")}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { ListItem } from "@/components/list/ListItem";
|
|||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useNavigation } from "expo-router";
|
import { useNavigation } from "expo-router";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import React, {useEffect, useMemo, useState} from "react";
|
import React, {useEffect, useMemo, useState} from "react";
|
||||||
import {
|
import {
|
||||||
Linking,
|
Linking,
|
||||||
@@ -18,6 +20,8 @@ import DisabledSetting from "@/components/settings/DisabledSetting";
|
|||||||
export default function page() {
|
export default function page() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings();
|
const [settings, updateSettings, pluginSettings] = useSettings();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -27,7 +31,7 @@ export default function page() {
|
|||||||
updateSettings({
|
updateSettings({
|
||||||
marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1),
|
marlinServerUrl: !val.endsWith("/") ? val : val.slice(0, -1),
|
||||||
});
|
});
|
||||||
toast.success("Saved");
|
toast.success(t("home.settings.plugins.marlin_search.toasts.saved"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenLink = () => {
|
const handleOpenLink = () => {
|
||||||
@@ -43,7 +47,7 @@ export default function page() {
|
|||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<TouchableOpacity onPress={() => onSave(value)}>
|
<TouchableOpacity onPress={() => onSave(value)}>
|
||||||
<Text className="text-blue-500">Save</Text>
|
<Text className="text-blue-500">{t("home.settings.plugins.marlin_search.save_button")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -63,7 +67,7 @@ export default function page() {
|
|||||||
showText={!pluginSettings?.marlinServerUrl?.locked}
|
showText={!pluginSettings?.marlinServerUrl?.locked}
|
||||||
>
|
>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={"Enable Marlin Search"}
|
title={t("home.settings.plugins.marlin_search.enable_marlin_search")}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
updateSettings({ searchEngine: "Jellyfin" });
|
updateSettings({ searchEngine: "Jellyfin" });
|
||||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||||
@@ -88,11 +92,11 @@ export default function page() {
|
|||||||
<View
|
<View
|
||||||
className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}
|
className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}
|
||||||
>
|
>
|
||||||
<Text className="mr-4">URL</Text>
|
<Text className="mr-4">{t("home.settings.plugins.marlin_search.url")}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
editable={settings.searchEngine === "Marlin"}
|
editable={settings.searchEngine === "Marlin"}
|
||||||
className="text-white"
|
className="text-white"
|
||||||
placeholder="http(s)://domain.org:port"
|
placeholder={t("home.settings.plugins.marlin_search.server_url_placeholder")}
|
||||||
value={value}
|
value={value}
|
||||||
keyboardType="url"
|
keyboardType="url"
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
@@ -103,10 +107,9 @@ export default function page() {
|
|||||||
</View>
|
</View>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
||||||
Enter the URL for the Marlin server. The URL should include http or
|
{t("home.settings.plugins.marlin_search.marlin_search_hint")}{" "}
|
||||||
https and optionally the port.{" "}
|
|
||||||
<Text className="text-blue-500" onPress={handleOpenLink}>
|
<Text className="text-blue-500" onPress={handleOpenLink}>
|
||||||
Read more about Marlin.
|
{t("home.settings.plugins.marlin_search.read_more_about_marlin")}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ import { useAtom } from "jotai";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings();
|
const [settings, updateSettings, pluginSettings] = useSettings();
|
||||||
|
|
||||||
@@ -24,7 +27,7 @@ export default function page() {
|
|||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: async (newVal: string) => {
|
mutationFn: async (newVal: string) => {
|
||||||
if (newVal.length === 0 || !newVal.startsWith("http")) {
|
if (newVal.length === 0 || !newVal.startsWith("http")) {
|
||||||
toast.error("Invalid URL");
|
toast.error(t("home.settings.toasts.invalid_url"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,13 +45,13 @@ export default function page() {
|
|||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
toast.success("Connected");
|
toast.success(t("home.settings.toasts.connected"));
|
||||||
} else {
|
} else {
|
||||||
toast.error("Could not connect");
|
toast.error(t("home.settings.toasts.could_not_connect"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error("Could not connect");
|
toast.error(t("home.settings.toasts.could_not_connect"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,13 +62,13 @@ export default function page() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pluginSettings?.optimizedVersionsServerUrl?.locked) {
|
if (!pluginSettings?.optimizedVersionsServerUrl?.locked) {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
title: "Optimized Server",
|
title: t("home.settings.downloads.optimized_server"),
|
||||||
headerRight: () =>
|
headerRight: () =>
|
||||||
saveMutation.isPending ? (
|
saveMutation.isPending ? (
|
||||||
<ActivityIndicator size={"small"} color={"white"} />
|
<ActivityIndicator size={"small"} color={"white"} />
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity onPress={() => onSave(optimizedVersionsServerUrl)}>
|
<TouchableOpacity onPress={() => onSave(optimizedVersionsServerUrl)}>
|
||||||
<Text className="text-blue-500">Save</Text>
|
<Text className="text-blue-500">{t("home.settings.downloads.save_button")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ import { useLocalSearchParams } from "expo-router";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const page: React.FC = () => {
|
const page: React.FC = () => {
|
||||||
const local = useLocalSearchParams();
|
const local = useLocalSearchParams();
|
||||||
const { actorId } = local as { actorId: string };
|
const { actorId } = local as { actorId: string };
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
@@ -110,7 +112,7 @@ const page: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text className="px-4 text-2xl font-bold mb-2 text-neutral-100">
|
<Text className="px-4 text-2xl font-bold mb-2 text-neutral-100">
|
||||||
Appeared In
|
{t("item_card.appeared_in")}
|
||||||
</Text>
|
</Text>
|
||||||
<InfiniteHorizontalScroll
|
<InfiniteHorizontalScroll
|
||||||
height={247}
|
height={247}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import * as ScreenOrientation from "expo-screen-orientation";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { FlatList, View } from "react-native";
|
import { FlatList, View } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const page: React.FC = () => {
|
const page: React.FC = () => {
|
||||||
const searchParams = useLocalSearchParams();
|
const searchParams = useLocalSearchParams();
|
||||||
@@ -45,6 +46,8 @@ const page: React.FC = () => {
|
|||||||
ScreenOrientation.Orientation.PORTRAIT_UP
|
ScreenOrientation.Orientation.PORTRAIT_UP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [selectedGenres, setSelectedGenres] = useAtom(genreFilterAtom);
|
const [selectedGenres, setSelectedGenres] = useAtom(genreFilterAtom);
|
||||||
const [selectedYears, setSelectedYears] = useAtom(yearFilterAtom);
|
const [selectedYears, setSelectedYears] = useAtom(yearFilterAtom);
|
||||||
const [selectedTags, setSelectedTags] = useAtom(tagsFilterAtom);
|
const [selectedTags, setSelectedTags] = useAtom(tagsFilterAtom);
|
||||||
@@ -244,7 +247,7 @@ const page: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
set={setSelectedGenres}
|
set={setSelectedGenres}
|
||||||
values={selectedGenres}
|
values={selectedGenres}
|
||||||
title="Genres"
|
title={t("library.filters.genres")}
|
||||||
renderItemLabel={(item) => item.toString()}
|
renderItemLabel={(item) => item.toString()}
|
||||||
searchFilter={(item, search) =>
|
searchFilter={(item, search) =>
|
||||||
item.toLowerCase().includes(search.toLowerCase())
|
item.toLowerCase().includes(search.toLowerCase())
|
||||||
@@ -271,7 +274,7 @@ const page: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
set={setSelectedYears}
|
set={setSelectedYears}
|
||||||
values={selectedYears}
|
values={selectedYears}
|
||||||
title="Years"
|
title={t("library.filters.years")}
|
||||||
renderItemLabel={(item) => item.toString()}
|
renderItemLabel={(item) => item.toString()}
|
||||||
searchFilter={(item, search) => item.includes(search)}
|
searchFilter={(item, search) => item.includes(search)}
|
||||||
/>
|
/>
|
||||||
@@ -296,7 +299,7 @@ const page: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
set={setSelectedTags}
|
set={setSelectedTags}
|
||||||
values={selectedTags}
|
values={selectedTags}
|
||||||
title="Tags"
|
title={t("library.filters.tags")}
|
||||||
renderItemLabel={(item) => item.toString()}
|
renderItemLabel={(item) => item.toString()}
|
||||||
searchFilter={(item, search) =>
|
searchFilter={(item, search) =>
|
||||||
item.toLowerCase().includes(search.toLowerCase())
|
item.toLowerCase().includes(search.toLowerCase())
|
||||||
@@ -314,7 +317,7 @@ const page: React.FC = () => {
|
|||||||
queryFn={async () => sortOptions.map((s) => s.key)}
|
queryFn={async () => sortOptions.map((s) => s.key)}
|
||||||
set={setSortBy}
|
set={setSortBy}
|
||||||
values={sortBy}
|
values={sortBy}
|
||||||
title="Sort By"
|
title={t("library.filters.sort_by")}
|
||||||
renderItemLabel={(item) =>
|
renderItemLabel={(item) =>
|
||||||
sortOptions.find((i) => i.key === item)?.value || ""
|
sortOptions.find((i) => i.key === item)?.value || ""
|
||||||
}
|
}
|
||||||
@@ -334,7 +337,7 @@ const page: React.FC = () => {
|
|||||||
queryFn={async () => sortOrderOptions.map((s) => s.key)}
|
queryFn={async () => sortOrderOptions.map((s) => s.key)}
|
||||||
set={setSortOrder}
|
set={setSortOrder}
|
||||||
values={sortOrder}
|
values={sortOrder}
|
||||||
title="Sort Order"
|
title={t("library.filters.sort_order")}
|
||||||
renderItemLabel={(item) =>
|
renderItemLabel={(item) =>
|
||||||
sortOrderOptions.find((i) => i.key === item)?.value || ""
|
sortOrderOptions.find((i) => i.key === item)?.value || ""
|
||||||
}
|
}
|
||||||
@@ -374,7 +377,7 @@ const page: React.FC = () => {
|
|||||||
<FlashList
|
<FlashList
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View className="flex flex-col items-center justify-center h-full">
|
<View className="flex flex-col items-center justify-center h-full">
|
||||||
<Text className="font-bold text-xl text-neutral-500">No results</Text>
|
<Text className="font-bold text-xl text-neutral-500">{t("search.no_results")}</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
extraData={[
|
extraData={[
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import Animated, {
|
|||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
withTiming,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const { id } = useLocalSearchParams() as { id: string };
|
const { id } = useLocalSearchParams() as { id: string };
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data: item, isError } = useQuery({
|
const { data: item, isError } = useQuery({
|
||||||
queryKey: ["item", id],
|
queryKey: ["item", id],
|
||||||
@@ -74,7 +76,7 @@ const Page: React.FC = () => {
|
|||||||
if (isError)
|
if (isError)
|
||||||
return (
|
return (
|
||||||
<View className="flex flex-col items-center justify-center h-screen w-screen">
|
<View className="flex flex-col items-center justify-center h-screen w-screen">
|
||||||
<Text>Could not load item</Text>
|
<Text>{t("item_card.could_not_load_item")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||||
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||||
import { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
|
import { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import {
|
import {
|
||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
@@ -39,6 +40,8 @@ import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/request
|
|||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { mediaTitle, releaseYear, posterSrc, ...result } =
|
const { mediaTitle, releaseYear, posterSrc, ...result } =
|
||||||
params as unknown as {
|
params as unknown as {
|
||||||
mediaTitle: string;
|
mediaTitle: string;
|
||||||
@@ -214,7 +217,7 @@ const Page: React.FC = () => {
|
|||||||
<Button loading={true} disabled={true} color="purple"></Button>
|
<Button loading={true} disabled={true} color="purple"></Button>
|
||||||
) : canRequest ? (
|
) : canRequest ? (
|
||||||
<Button color="purple" onPress={request}>
|
<Button color="purple" onPress={request}>
|
||||||
Request
|
{t("jellyseerr.request_button")}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -229,7 +232,7 @@ const Page: React.FC = () => {
|
|||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Report issue
|
{t("jellyseerr.report_issue_button")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<OverviewText text={result.overview} className="mt-4" />
|
<OverviewText text={result.overview} className="mt-4" />
|
||||||
@@ -281,7 +284,7 @@ const Page: React.FC = () => {
|
|||||||
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
||||||
<View>
|
<View>
|
||||||
<Text className="font-bold text-2xl text-neutral-100">
|
<Text className="font-bold text-2xl text-neutral-100">
|
||||||
Whats wrong?
|
{t("jellyseerr.whats_wrong")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex flex-col space-y-2 items-start">
|
<View className="flex flex-col space-y-2 items-start">
|
||||||
@@ -290,13 +293,13 @@ const Page: React.FC = () => {
|
|||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<View className="flex flex-col">
|
<View className="flex flex-col">
|
||||||
<Text className="opacity-50 mb-1 text-xs">
|
<Text className="opacity-50 mb-1 text-xs">
|
||||||
Issue Type
|
{t("jellyseerr.issue_type")}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
<Text style={{}} className="" numberOfLines={1}>
|
<Text style={{}} className="" numberOfLines={1}>
|
||||||
{issueType
|
{issueType
|
||||||
? IssueTypeName[issueType]
|
? IssueTypeName[issueType]
|
||||||
: "Select an issue"}
|
: t("jellyseerr.select_an_issue")}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -310,7 +313,7 @@ const Page: React.FC = () => {
|
|||||||
collisionPadding={0}
|
collisionPadding={0}
|
||||||
sideOffset={0}
|
sideOffset={0}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Label>Types</DropdownMenu.Label>
|
<DropdownMenu.Label>{t("jellyseerr.types")}</DropdownMenu.Label>
|
||||||
{Object.entries(IssueTypeName)
|
{Object.entries(IssueTypeName)
|
||||||
.reverse()
|
.reverse()
|
||||||
.map(([key, value], idx) => (
|
.map(([key, value], idx) => (
|
||||||
@@ -335,7 +338,7 @@ const Page: React.FC = () => {
|
|||||||
maxLength={254}
|
maxLength={254}
|
||||||
style={{ color: "white" }}
|
style={{ color: "white" }}
|
||||||
clearButtonMode="always"
|
clearButtonMode="always"
|
||||||
placeholder="(optional) Describe the issue..."
|
placeholder={t("jellyseerr.describe_the_issue")}
|
||||||
placeholderTextColor="#9CA3AF"
|
placeholderTextColor="#9CA3AF"
|
||||||
// Issue with multiline + Textinput inside a portal
|
// Issue with multiline + Textinput inside a portal
|
||||||
// https://github.com/callstack/react-native-paper/issues/1668
|
// https://github.com/callstack/react-native-paper/issues/1668
|
||||||
@@ -345,7 +348,7 @@ const Page: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Button className="mt-auto" onPress={submitIssue} color="purple">
|
<Button className="mt-auto" onPress={submitIssue} color="purple">
|
||||||
Submit
|
{t("jellyseerr.submit_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</BottomSheetView>
|
</BottomSheetView>
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
|||||||
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
|
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
|
||||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||||
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const local = useLocalSearchParams();
|
const local = useLocalSearchParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { jellyseerrApi, jellyseerrUser } = useJellyseerr();
|
const { jellyseerrApi, jellyseerrUser } = useJellyseerr();
|
||||||
|
|
||||||
const { personId } = local as { personId: string };
|
const { personId } = local as { personId: string };
|
||||||
@@ -58,7 +61,7 @@ export default function page() {
|
|||||||
<ParallaxSlideShow
|
<ParallaxSlideShow
|
||||||
data={castedRoles}
|
data={castedRoles}
|
||||||
images={backdrops}
|
images={backdrops}
|
||||||
listHeader="Appearances"
|
listHeader={t("jellyseerr.appearances")}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
logo={
|
logo={
|
||||||
<Image
|
<Image
|
||||||
@@ -85,7 +88,7 @@ export default function page() {
|
|||||||
{data?.details?.name}
|
{data?.details?.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="opacity-50">
|
<Text className="opacity-50">
|
||||||
Born{" "}
|
{t("jellyseerr.born")}{" "}
|
||||||
{new Date(data?.details?.birthday!!).toLocaleDateString(
|
{new Date(data?.details?.birthday!!).toLocaleDateString(
|
||||||
`${locale}-${region}`,
|
`${locale}-${region}`,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const HOUR_HEIGHT = 30;
|
const HOUR_HEIGHT = 30;
|
||||||
const ITEMS_PER_PAGE = 20;
|
const ITEMS_PER_PAGE = 20;
|
||||||
@@ -177,6 +178,7 @@ const PageButtons: React.FC<PageButtonsProps> = ({
|
|||||||
onNextPage,
|
onNextPage,
|
||||||
isNextDisabled,
|
isNextDisabled,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<View className="flex flex-row justify-between items-center bg-neutral-800 w-full px-4 py-2">
|
<View className="flex flex-row justify-between items-center bg-neutral-800 w-full px-4 py-2">
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -194,7 +196,7 @@ const PageButtons: React.FC<PageButtonsProps> = ({
|
|||||||
currentPage === 1 ? "text-gray-500" : "text-white"
|
currentPage === 1 ? "text-gray-500" : "text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Previous
|
{t("live_tv.previous")}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text className="text-white">Page {currentPage}</Text>
|
<Text className="text-white">Page {currentPage}</Text>
|
||||||
@@ -206,7 +208,7 @@ const PageButtons: React.FC<PageButtonsProps> = ({
|
|||||||
<Text
|
<Text
|
||||||
className={`mr-1 ${isNextDisabled ? "text-gray-500" : "text-white"}`}
|
className={`mr-1 ${isNextDisabled ? "text-gray-500" : "text-white"}`}
|
||||||
>
|
>
|
||||||
Next
|
{t("live_tv.next")}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-forward"
|
name="chevron-forward"
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import { useAtom } from "jotai";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ScrollView, View } from "react-native";
|
import { ScrollView, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
nestedScrollEnabled
|
nestedScrollEnabled
|
||||||
@@ -28,7 +31,7 @@ export default function page() {
|
|||||||
<View className="flex flex-col space-y-2">
|
<View className="flex flex-col space-y-2">
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryKey={["livetv", "recommended"]}
|
queryKey={["livetv", "recommended"]}
|
||||||
title={"On now"}
|
title={t("live_tv.on_now")}
|
||||||
queryFn={async () => {
|
queryFn={async () => {
|
||||||
if (!api) return [] as BaseItemDto[];
|
if (!api) return [] as BaseItemDto[];
|
||||||
const res = await getLiveTvApi(api).getRecommendedPrograms({
|
const res = await getLiveTvApi(api).getRecommendedPrograms({
|
||||||
@@ -46,7 +49,7 @@ export default function page() {
|
|||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryKey={["livetv", "shows"]}
|
queryKey={["livetv", "shows"]}
|
||||||
title={"Shows"}
|
title={t("live_tv.shows")}
|
||||||
queryFn={async () => {
|
queryFn={async () => {
|
||||||
if (!api) return [] as BaseItemDto[];
|
if (!api) return [] as BaseItemDto[];
|
||||||
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
||||||
@@ -68,7 +71,7 @@ export default function page() {
|
|||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryKey={["livetv", "movies"]}
|
queryKey={["livetv", "movies"]}
|
||||||
title={"Movies"}
|
title={t("live_tv.movies")}
|
||||||
queryFn={async () => {
|
queryFn={async () => {
|
||||||
if (!api) return [] as BaseItemDto[];
|
if (!api) return [] as BaseItemDto[];
|
||||||
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
||||||
@@ -86,7 +89,7 @@ export default function page() {
|
|||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryKey={["livetv", "sports"]}
|
queryKey={["livetv", "sports"]}
|
||||||
title={"Sports"}
|
title={t("live_tv.sports")}
|
||||||
queryFn={async () => {
|
queryFn={async () => {
|
||||||
if (!api) return [] as BaseItemDto[];
|
if (!api) return [] as BaseItemDto[];
|
||||||
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
||||||
@@ -104,7 +107,7 @@ export default function page() {
|
|||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryKey={["livetv", "kids"]}
|
queryKey={["livetv", "kids"]}
|
||||||
title={"For Kids"}
|
title={t("live_tv.for_kids")}
|
||||||
queryFn={async () => {
|
queryFn={async () => {
|
||||||
if (!api) return [] as BaseItemDto[];
|
if (!api) return [] as BaseItemDto[];
|
||||||
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
||||||
@@ -122,7 +125,7 @@ export default function page() {
|
|||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryKey={["livetv", "news"]}
|
queryKey={["livetv", "news"]}
|
||||||
title={"News"}
|
title={t("live_tv.news")}
|
||||||
queryFn={async () => {
|
queryFn={async () => {
|
||||||
if (!api) return [] as BaseItemDto[];
|
if (!api) return [] as BaseItemDto[];
|
||||||
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
const res = await getLiveTvApi(api).getLiveTvPrograms({
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<View className="flex items-center justify-center h-full -mt-12">
|
<View className="flex items-center justify-center h-full -mt-12">
|
||||||
<Text>Coming soon</Text>
|
<Text>{t("live_tv.coming_soon")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import { useLocalSearchParams, useNavigation } from "expo-router";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useEffect, useMemo } from "react";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const page: React.FC = () => {
|
const page: React.FC = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const { t } = useTranslation();
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
const { id: seriesId, seasonIndex } = params as {
|
const { id: seriesId, seasonIndex } = params as {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -85,7 +87,7 @@ const page: React.FC = () => {
|
|||||||
<AddToFavorites item={item} type="series" />
|
<AddToFavorites item={item} type="series" />
|
||||||
<DownloadItems
|
<DownloadItems
|
||||||
size="large"
|
size="large"
|
||||||
title="Download Series"
|
title={t("item_card.download.download_series")}
|
||||||
items={allEpisodes || []}
|
items={allEpisodes || []}
|
||||||
MissingDownloadIconComponent={() => (
|
MissingDownloadIconComponent={() => (
|
||||||
<Ionicons name="download" size={22} color="white" />
|
<Ionicons name="download" size={22} color="white" />
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const searchParams = useLocalSearchParams();
|
const searchParams = useLocalSearchParams();
|
||||||
@@ -62,6 +63,8 @@ const Page = () => {
|
|||||||
|
|
||||||
const { orientation } = useOrientation();
|
const { orientation } = useOrientation();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sop = getSortOrderPreference(libraryId, sortOrderPreference);
|
const sop = getSortOrderPreference(libraryId, sortOrderPreference);
|
||||||
if (sop) {
|
if (sop) {
|
||||||
@@ -298,7 +301,7 @@ const Page = () => {
|
|||||||
}}
|
}}
|
||||||
set={setSelectedGenres}
|
set={setSelectedGenres}
|
||||||
values={selectedGenres}
|
values={selectedGenres}
|
||||||
title="Genres"
|
title={t("library.filters.genres")}
|
||||||
renderItemLabel={(item) => item.toString()}
|
renderItemLabel={(item) => item.toString()}
|
||||||
searchFilter={(item, search) =>
|
searchFilter={(item, search) =>
|
||||||
item.toLowerCase().includes(search.toLowerCase())
|
item.toLowerCase().includes(search.toLowerCase())
|
||||||
@@ -325,7 +328,7 @@ const Page = () => {
|
|||||||
}}
|
}}
|
||||||
set={setSelectedYears}
|
set={setSelectedYears}
|
||||||
values={selectedYears}
|
values={selectedYears}
|
||||||
title="Years"
|
title={t("library.filters.years")}
|
||||||
renderItemLabel={(item) => item.toString()}
|
renderItemLabel={(item) => item.toString()}
|
||||||
searchFilter={(item, search) => item.includes(search)}
|
searchFilter={(item, search) => item.includes(search)}
|
||||||
/>
|
/>
|
||||||
@@ -350,7 +353,7 @@ const Page = () => {
|
|||||||
}}
|
}}
|
||||||
set={setSelectedTags}
|
set={setSelectedTags}
|
||||||
values={selectedTags}
|
values={selectedTags}
|
||||||
title="Tags"
|
title={t("library.filters.tags")}
|
||||||
renderItemLabel={(item) => item.toString()}
|
renderItemLabel={(item) => item.toString()}
|
||||||
searchFilter={(item, search) =>
|
searchFilter={(item, search) =>
|
||||||
item.toLowerCase().includes(search.toLowerCase())
|
item.toLowerCase().includes(search.toLowerCase())
|
||||||
@@ -368,7 +371,7 @@ const Page = () => {
|
|||||||
queryFn={async () => sortOptions.map((s) => s.key)}
|
queryFn={async () => sortOptions.map((s) => s.key)}
|
||||||
set={setSortBy}
|
set={setSortBy}
|
||||||
values={sortBy}
|
values={sortBy}
|
||||||
title="Sort By"
|
title={t("library.filters.sort_by")}
|
||||||
renderItemLabel={(item) =>
|
renderItemLabel={(item) =>
|
||||||
sortOptions.find((i) => i.key === item)?.value || ""
|
sortOptions.find((i) => i.key === item)?.value || ""
|
||||||
}
|
}
|
||||||
@@ -388,7 +391,7 @@ const Page = () => {
|
|||||||
queryFn={async () => sortOrderOptions.map((s) => s.key)}
|
queryFn={async () => sortOrderOptions.map((s) => s.key)}
|
||||||
set={setSortOrder}
|
set={setSortOrder}
|
||||||
values={sortOrder}
|
values={sortOrder}
|
||||||
title="Sort Order"
|
title={t("library.filters.sort_order")}
|
||||||
renderItemLabel={(item) =>
|
renderItemLabel={(item) =>
|
||||||
sortOrderOptions.find((i) => i.key === item)?.value || ""
|
sortOrderOptions.find((i) => i.key === item)?.value || ""
|
||||||
}
|
}
|
||||||
@@ -434,7 +437,7 @@ const Page = () => {
|
|||||||
if (flatData.length === 0)
|
if (flatData.length === 0)
|
||||||
return (
|
return (
|
||||||
<View className="h-full w-full flex justify-center items-center">
|
<View className="h-full w-full flex justify-center items-center">
|
||||||
<Text className="text-lg text-neutral-500">No items found</Text>
|
<Text className="text-lg text-neutral-500">{t("library.no_items_found")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -443,7 +446,7 @@ const Page = () => {
|
|||||||
key={orientation}
|
key={orientation}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View className="flex flex-col items-center justify-center h-full">
|
<View className="flex flex-col items-center justify-center h-full">
|
||||||
<Text className="font-bold text-xl text-neutral-500">No results</Text>
|
<Text className="font-bold text-xl text-neutral-500">{t("library.no_results")}</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
contentInsetAdjustmentBehavior="automatic"
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function IndexLayout() {
|
export default function IndexLayout() {
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings();
|
const [settings, updateSettings, pluginSettings] = useSettings();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!settings?.libraryOptions) return null;
|
if (!settings?.libraryOptions) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -17,7 +20,7 @@ export default function IndexLayout() {
|
|||||||
options={{
|
options={{
|
||||||
headerShown: true,
|
headerShown: true,
|
||||||
headerLargeTitle: true,
|
headerLargeTitle: true,
|
||||||
headerTitle: "Library",
|
headerTitle: t("tabs.library"),
|
||||||
headerBlurEffect: "prominent",
|
headerBlurEffect: "prominent",
|
||||||
headerLargeStyle: {
|
headerLargeStyle: {
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
@@ -43,11 +46,11 @@ export default function IndexLayout() {
|
|||||||
side={"bottom"}
|
side={"bottom"}
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Label>Display</DropdownMenu.Label>
|
<DropdownMenu.Label>{t("library.options.display")}</DropdownMenu.Label>
|
||||||
<DropdownMenu.Group key="display-group">
|
<DropdownMenu.Group key="display-group">
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger key="image-style-trigger">
|
<DropdownMenu.SubTrigger key="image-style-trigger">
|
||||||
Display
|
{t("library.options.display")}
|
||||||
</DropdownMenu.SubTrigger>
|
</DropdownMenu.SubTrigger>
|
||||||
<DropdownMenu.SubContent
|
<DropdownMenu.SubContent
|
||||||
alignOffset={-10}
|
alignOffset={-10}
|
||||||
@@ -70,7 +73,7 @@ export default function IndexLayout() {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.ItemIndicator />
|
<DropdownMenu.ItemIndicator />
|
||||||
<DropdownMenu.ItemTitle key="display-title-1">
|
<DropdownMenu.ItemTitle key="display-title-1">
|
||||||
Row
|
{t("library.options.row")}
|
||||||
</DropdownMenu.ItemTitle>
|
</DropdownMenu.ItemTitle>
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
@@ -87,14 +90,14 @@ export default function IndexLayout() {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.ItemIndicator />
|
<DropdownMenu.ItemIndicator />
|
||||||
<DropdownMenu.ItemTitle key="display-title-2">
|
<DropdownMenu.ItemTitle key="display-title-2">
|
||||||
List
|
{t("library.options.list")}
|
||||||
</DropdownMenu.ItemTitle>
|
</DropdownMenu.ItemTitle>
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
</DropdownMenu.Sub>
|
</DropdownMenu.Sub>
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger key="image-style-trigger">
|
<DropdownMenu.SubTrigger key="image-style-trigger">
|
||||||
Image style
|
{t("library.options.image_style")}
|
||||||
</DropdownMenu.SubTrigger>
|
</DropdownMenu.SubTrigger>
|
||||||
<DropdownMenu.SubContent
|
<DropdownMenu.SubContent
|
||||||
alignOffset={-10}
|
alignOffset={-10}
|
||||||
@@ -117,7 +120,7 @@ export default function IndexLayout() {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.ItemIndicator />
|
<DropdownMenu.ItemIndicator />
|
||||||
<DropdownMenu.ItemTitle key="poster-title">
|
<DropdownMenu.ItemTitle key="poster-title">
|
||||||
Poster
|
{t("library.options.poster")}
|
||||||
</DropdownMenu.ItemTitle>
|
</DropdownMenu.ItemTitle>
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
@@ -134,7 +137,7 @@ export default function IndexLayout() {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.ItemIndicator />
|
<DropdownMenu.ItemIndicator />
|
||||||
<DropdownMenu.ItemTitle key="cover-title">
|
<DropdownMenu.ItemTitle key="cover-title">
|
||||||
Cover
|
{t("library.options.cover")}
|
||||||
</DropdownMenu.ItemTitle>
|
</DropdownMenu.ItemTitle>
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
@@ -158,7 +161,7 @@ export default function IndexLayout() {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.ItemIndicator />
|
<DropdownMenu.ItemIndicator />
|
||||||
<DropdownMenu.ItemTitle key="show-titles-title">
|
<DropdownMenu.ItemTitle key="show-titles-title">
|
||||||
Show titles
|
{t("library.options.show_titles")}
|
||||||
</DropdownMenu.ItemTitle>
|
</DropdownMenu.ItemTitle>
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
@@ -175,7 +178,7 @@ export default function IndexLayout() {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.ItemIndicator />
|
<DropdownMenu.ItemIndicator />
|
||||||
<DropdownMenu.ItemTitle key="show-stats-title">
|
<DropdownMenu.ItemTitle key="show-stats-title">
|
||||||
Show stats
|
{t("library.options.show_stats")}
|
||||||
</DropdownMenu.ItemTitle>
|
</DropdownMenu.ItemTitle>
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useAtom } from "jotai";
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { StyleSheet, View } from "react-native";
|
import { StyleSheet, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function index() {
|
export default function index() {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
@@ -20,6 +21,8 @@ export default function index() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, isLoading: isLoading } = useQuery({
|
const { data, isLoading: isLoading } = useQuery({
|
||||||
queryKey: ["user-views", user?.Id],
|
queryKey: ["user-views", user?.Id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -70,7 +73,7 @@ export default function index() {
|
|||||||
if (!libraries)
|
if (!libraries)
|
||||||
return (
|
return (
|
||||||
<View className="h-full w-full flex justify-center items-center">
|
<View className="h-full w-full flex justify-center items-center">
|
||||||
<Text className="text-lg text-neutral-500">No libraries found</Text>
|
<Text className="text-lg text-neutral-500">{t("library.no_libraries_found")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import {
|
|||||||
} from "@/components/stacks/NestedTabPageStack";
|
} from "@/components/stacks/NestedTabPageStack";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function SearchLayout() {
|
export default function SearchLayout() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -13,7 +15,7 @@ export default function SearchLayout() {
|
|||||||
options={{
|
options={{
|
||||||
headerShown: true,
|
headerShown: true,
|
||||||
headerLargeTitle: true,
|
headerLargeTitle: true,
|
||||||
headerTitle: "Search",
|
headerTitle: t("tabs.search"),
|
||||||
headerLargeStyle: {
|
headerLargeStyle: {
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import React, {
|
|||||||
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
|
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type SearchType = "Library" | "Discover";
|
type SearchType = "Library" | "Discover";
|
||||||
|
|
||||||
@@ -47,6 +48,8 @@ export default function search() {
|
|||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { q, prev } = params as { q: string; prev: Href<string> };
|
const { q, prev } = params as { q: string; prev: Href<string> };
|
||||||
|
|
||||||
const [searchType, setSearchType] = useState<SearchType>("Library");
|
const [searchType, setSearchType] = useState<SearchType>("Library");
|
||||||
@@ -122,7 +125,7 @@ export default function search() {
|
|||||||
if (Platform.OS === "ios")
|
if (Platform.OS === "ios")
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerSearchBarOptions: {
|
headerSearchBarOptions: {
|
||||||
placeholder: "Search...",
|
placeholder: t("search.search"),
|
||||||
onChangeText: (e: any) => {
|
onChangeText: (e: any) => {
|
||||||
router.setParams({ q: "" });
|
router.setParams({ q: "" });
|
||||||
setSearch(e.nativeEvent.text);
|
setSearch(e.nativeEvent.text);
|
||||||
@@ -214,7 +217,7 @@ export default function search() {
|
|||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
keyboardType="web-search"
|
keyboardType="web-search"
|
||||||
placeholder="Search here..."
|
placeholder={t("search.search_here")}
|
||||||
value={search}
|
value={search}
|
||||||
onChangeText={(text) => setSearch(text)}
|
onChangeText={(text) => setSearch(text)}
|
||||||
/>
|
/>
|
||||||
@@ -224,7 +227,7 @@ export default function search() {
|
|||||||
<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")}>
|
||||||
<Tag
|
<Tag
|
||||||
text="Library"
|
text={t("search.library")}
|
||||||
textClass="p-1"
|
textClass="p-1"
|
||||||
className={
|
className={
|
||||||
searchType === "Library" ? "bg-purple-600" : undefined
|
searchType === "Library" ? "bg-purple-600" : undefined
|
||||||
@@ -233,7 +236,7 @@ export default function search() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => setSearchType("Discover")}>
|
<TouchableOpacity onPress={() => setSearchType("Discover")}>
|
||||||
<Tag
|
<Tag
|
||||||
text="Discover"
|
text={t("search.discover")}
|
||||||
textClass="p-1"
|
textClass="p-1"
|
||||||
className={
|
className={
|
||||||
searchType === "Discover" ? "bg-purple-600" : undefined
|
searchType === "Discover" ? "bg-purple-600" : undefined
|
||||||
@@ -250,7 +253,7 @@ export default function search() {
|
|||||||
{searchType === "Library" ? (
|
{searchType === "Library" ? (
|
||||||
<View className={l1 || l2 ? "opacity-0" : "opacity-100"}>
|
<View className={l1 || l2 ? "opacity-0" : "opacity-100"}>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
header="Movies"
|
header={t("search.movies")}
|
||||||
ids={movies?.map((m) => m.Id!)}
|
ids={movies?.map((m) => m.Id!)}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
@@ -270,7 +273,7 @@ export default function search() {
|
|||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
ids={series?.map((m) => m.Id!)}
|
ids={series?.map((m) => m.Id!)}
|
||||||
header="Series"
|
header={t("search.series")}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
key={item.Id}
|
key={item.Id}
|
||||||
@@ -289,7 +292,7 @@ export default function search() {
|
|||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
ids={episodes?.map((m) => m.Id!)}
|
ids={episodes?.map((m) => m.Id!)}
|
||||||
header="Episodes"
|
header={t("search.episodes")}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
item={item}
|
item={item}
|
||||||
@@ -303,7 +306,7 @@ export default function search() {
|
|||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
ids={collections?.map((m) => m.Id!)}
|
ids={collections?.map((m) => m.Id!)}
|
||||||
header="Collections"
|
header={t("search.collections")}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
key={item.Id}
|
key={item.Id}
|
||||||
@@ -319,7 +322,7 @@ export default function search() {
|
|||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
ids={actors?.map((m) => m.Id!)}
|
ids={actors?.map((m) => m.Id!)}
|
||||||
header="Actors"
|
header={t("search.actors")}
|
||||||
renderItem={(item: BaseItemDto) => (
|
renderItem={(item: BaseItemDto) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
item={item}
|
item={item}
|
||||||
@@ -341,7 +344,7 @@ export default function search() {
|
|||||||
{!loading && noResults && debouncedSearch.length > 0 ? (
|
{!loading && noResults && debouncedSearch.length > 0 ? (
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-center text-lg font-bold mt-4">
|
<Text className="text-center text-lg font-bold mt-4">
|
||||||
No results found for
|
{t("search.no_results_found_for")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs text-purple-600 text-center">
|
<Text className="text-xs text-purple-600 text-center">
|
||||||
"{debouncedSearch}"
|
"{debouncedSearch}"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useRef } from "react";
|
import React, { useCallback, useRef } from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useFocusEffect, useRouter, withLayoutContext } from "expo-router";
|
import { useFocusEffect, useRouter, withLayoutContext } from "expo-router";
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export const NativeTabs = withLayoutContext<
|
|||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
@@ -61,7 +63,7 @@ export default function TabLayout() {
|
|||||||
<NativeTabs.Screen
|
<NativeTabs.Screen
|
||||||
name="(home)"
|
name="(home)"
|
||||||
options={{
|
options={{
|
||||||
title: "Home",
|
title: t("tabs.home"),
|
||||||
tabBarIcon:
|
tabBarIcon:
|
||||||
Platform.OS == "android"
|
Platform.OS == "android"
|
||||||
? ({ color, focused, size }) =>
|
? ({ color, focused, size }) =>
|
||||||
@@ -75,7 +77,7 @@ export default function TabLayout() {
|
|||||||
<NativeTabs.Screen
|
<NativeTabs.Screen
|
||||||
name="(search)"
|
name="(search)"
|
||||||
options={{
|
options={{
|
||||||
title: "Search",
|
title: t("tabs.search"),
|
||||||
tabBarIcon:
|
tabBarIcon:
|
||||||
Platform.OS == "android"
|
Platform.OS == "android"
|
||||||
? ({ color, focused, size }) =>
|
? ({ color, focused, size }) =>
|
||||||
@@ -89,7 +91,7 @@ export default function TabLayout() {
|
|||||||
<NativeTabs.Screen
|
<NativeTabs.Screen
|
||||||
name="(favorites)"
|
name="(favorites)"
|
||||||
options={{
|
options={{
|
||||||
title: "Favorites",
|
title: t("tabs.favorites"),
|
||||||
tabBarIcon:
|
tabBarIcon:
|
||||||
Platform.OS == "android"
|
Platform.OS == "android"
|
||||||
? ({ color, focused, size }) =>
|
? ({ color, focused, size }) =>
|
||||||
@@ -105,7 +107,7 @@ export default function TabLayout() {
|
|||||||
<NativeTabs.Screen
|
<NativeTabs.Screen
|
||||||
name="(libraries)"
|
name="(libraries)"
|
||||||
options={{
|
options={{
|
||||||
title: "Library",
|
title: t("tabs.library"),
|
||||||
tabBarIcon:
|
tabBarIcon:
|
||||||
Platform.OS == "android"
|
Platform.OS == "android"
|
||||||
? ({ color, focused, size }) =>
|
? ({ color, focused, size }) =>
|
||||||
@@ -119,7 +121,7 @@ export default function TabLayout() {
|
|||||||
<NativeTabs.Screen
|
<NativeTabs.Screen
|
||||||
name="(custom-links)"
|
name="(custom-links)"
|
||||||
options={{
|
options={{
|
||||||
title: "Custom Links",
|
title: t("tabs.custom_links"),
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
tabBarItemHidden: settings?.showCustomMenuLinks ? false : true,
|
tabBarItemHidden: settings?.showCustomMenuLinks ? false : true,
|
||||||
tabBarIcon:
|
tabBarIcon:
|
||||||
|
|||||||
@@ -48,12 +48,14 @@ import {
|
|||||||
import { useSharedValue } from "react-native-reanimated";
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
import settings from "../(tabs)/(home)/settings";
|
import settings from "../(tabs)/(home)/settings";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
||||||
const [showControls, _setShowControls] = useState(true);
|
const [showControls, _setShowControls] = useState(true);
|
||||||
@@ -161,7 +163,7 @@ export default function page() {
|
|||||||
const { mediaSource, sessionId, url } = res;
|
const { mediaSource, sessionId, url } = res;
|
||||||
|
|
||||||
if (!sessionId || !mediaSource || !url) {
|
if (!sessionId || !mediaSource || !url) {
|
||||||
Alert.alert("Error", "Failed to get stream url");
|
Alert.alert(t("player.error"), t("player.failed_to_get_stream_url"));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +428,7 @@ export default function page() {
|
|||||||
if (isErrorItem || isErrorStreamUrl)
|
if (isErrorItem || isErrorStreamUrl)
|
||||||
return (
|
return (
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||||
<Text className="text-white">Error</Text>
|
<Text className="text-white">{t("player.error")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -465,8 +467,8 @@ export default function page() {
|
|||||||
onVideoError={(e) => {
|
onVideoError={(e) => {
|
||||||
console.error("Video Error:", e.nativeEvent);
|
console.error("Video Error:", e.nativeEvent);
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Error",
|
t("player.error"),
|
||||||
"An error occurred while playing the video. Check logs in settings."
|
t("player.an_error_occured_while_playing_the_video")
|
||||||
);
|
);
|
||||||
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ import Video, {
|
|||||||
VideoRef,
|
VideoRef,
|
||||||
} from "react-native-video";
|
} from "react-native-video";
|
||||||
import { SubtitleHelper } from "@/utils/SubtitleHelper";
|
import { SubtitleHelper } from "@/utils/SubtitleHelper";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const Player = () => {
|
const Player = () => {
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const videoRef = useRef<VideoRef | null>(null);
|
const videoRef = useRef<VideoRef | null>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const firstTime = useRef(true);
|
const firstTime = useRef(true);
|
||||||
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
||||||
@@ -374,7 +376,7 @@ const Player = () => {
|
|||||||
if (isErrorItem || isErrorStreamUrl)
|
if (isErrorItem || isErrorStreamUrl)
|
||||||
return (
|
return (
|
||||||
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
|
||||||
<Text className="text-white">Error</Text>
|
<Text className="text-white">{t("player.error")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -440,7 +442,7 @@ const Player = () => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Text>No video source...</Text>
|
<Text>{t("player.no_video_source")}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ import { useEffect, useRef } from "react";
|
|||||||
import { Appearance, AppState, TouchableOpacity } from "react-native";
|
import { Appearance, AppState, TouchableOpacity } 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";
|
||||||
|
import { I18nextProvider, useTranslation } from "react-i18next";
|
||||||
|
import i18n from "@/i18n";
|
||||||
|
import { getLocales } from "expo-localization";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
import { Toaster } from "sonner-native";
|
import { Toaster } from "sonner-native";
|
||||||
|
|
||||||
@@ -228,7 +231,9 @@ export default function RootLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<JotaiProvider>
|
<JotaiProvider>
|
||||||
<Layout />
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<Layout />
|
||||||
|
</I18nextProvider>
|
||||||
</JotaiProvider>
|
</JotaiProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -252,6 +257,8 @@ function Layout() {
|
|||||||
useKeepAwake();
|
useKeepAwake();
|
||||||
useNotificationObserver();
|
useNotificationObserver();
|
||||||
|
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAndRequestPermissions();
|
checkAndRequestPermissions();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -265,6 +272,12 @@ function Layout() {
|
|||||||
);
|
);
|
||||||
}, [settings]);
|
}, [settings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
i18n.changeLanguage(
|
||||||
|
settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en"
|
||||||
|
);
|
||||||
|
}, [settings?.preferedLanguage, i18n]);
|
||||||
|
|
||||||
const appState = useRef(AppState.currentState);
|
const appState = useRef(AppState.currentState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -21,12 +21,11 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { t } from 'i18next';
|
||||||
const CredentialsSchema = z.object({
|
const CredentialsSchema = z.object({
|
||||||
username: z.string().min(1, "Username is required"),
|
username: z.string().min(1, t("login.username_required")),});
|
||||||
});
|
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const { setServer, login, removeServer, initiateQuickConnect } =
|
const { setServer, login, removeServer, initiateQuickConnect } =
|
||||||
useJellyfin();
|
useJellyfin();
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
@@ -80,7 +79,7 @@ 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">Change server</Text>
|
<Text className="ml-2 text-purple-600">{t("login.change_server")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : null,
|
) : null,
|
||||||
});
|
});
|
||||||
@@ -97,9 +96,9 @@ const Login: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
Alert.alert("Connection failed", error.message);
|
Alert.alert(t("login.connection_failed"), error.message);
|
||||||
} else {
|
} else {
|
||||||
Alert.alert("Connection failed", "An unexpected error occurred");
|
Alert.alert(t("login.connection_failed"), t("login.an_unexpected_error_occured"));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -168,8 +167,8 @@ const Login: React.FC = () => {
|
|||||||
|
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Connection failed",
|
t("login.connection_failed"),
|
||||||
"Could not connect to the server. Please check the URL and your network connection."
|
t("login.could_not_connect_to_server")
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -181,14 +180,14 @@ const Login: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const code = await initiateQuickConnect();
|
const code = await initiateQuickConnect();
|
||||||
if (code) {
|
if (code) {
|
||||||
Alert.alert("Quick Connect", `Enter code ${code} to login`, [
|
Alert.alert(t("login.quick_connect"), t("login.enter_code_to_login", {code: code}), [
|
||||||
{
|
{
|
||||||
text: "Got It",
|
text: t("login.got_it"),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Alert.alert("Error", "Failed to initiate Quick Connect");
|
Alert.alert(t("login.error_title"), t("login.failed_to_initiate_quick_connect"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,22 +201,21 @@ 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">
|
||||||
Log in
|
<>
|
||||||
<>
|
{serverName ? (
|
||||||
{serverName ? (
|
<>
|
||||||
<>
|
{t("login.login_to_title") + " "}
|
||||||
{" to "}
|
<Text className="text-purple-600">{serverName}</Text>
|
||||||
<Text className="text-purple-600">{serverName}</Text>
|
</>
|
||||||
</>
|
) : t("login.login_title")}
|
||||||
) : null}
|
</>
|
||||||
</>
|
</Text>
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-neutral-400">
|
<Text className="text-xs text-neutral-400">
|
||||||
{api.basePath}
|
{api.basePath}
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder={t("login.username_placeholder")}
|
||||||
onChangeText={(text) =>
|
onChangeText={(text) =>
|
||||||
setCredentials({ ...credentials, username: text })
|
setCredentials({ ...credentials, username: text })
|
||||||
}
|
}
|
||||||
@@ -233,7 +231,7 @@ const Login: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Password"
|
placeholder={t("login.password_placeholder")}
|
||||||
onChangeText={(text) =>
|
onChangeText={(text) =>
|
||||||
setCredentials({ ...credentials, password: text })
|
setCredentials({ ...credentials, password: text })
|
||||||
}
|
}
|
||||||
@@ -252,7 +250,7 @@ const Login: React.FC = () => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
className="flex-1 mr-2"
|
className="flex-1 mr-2"
|
||||||
>
|
>
|
||||||
Log in
|
{t("login.login_button")}
|
||||||
</Button>
|
</Button>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleQuickConnect}
|
onPress={handleQuickConnect}
|
||||||
@@ -286,11 +284,11 @@ const Login: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<Text className="text-3xl font-bold">Streamyfin</Text>
|
<Text className="text-3xl font-bold">Streamyfin</Text>
|
||||||
<Text className="text-neutral-500">
|
<Text className="text-neutral-500">
|
||||||
Enter the URL to your Jellyfin server
|
{t("server.enter_url_to_jellyfin_server")}
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
aria-label="Server URL"
|
aria-label="Server URL"
|
||||||
placeholder="http(s)://your-server.com"
|
placeholder={t("server.server_url_placeholder")}
|
||||||
onChangeText={setServerURL}
|
onChangeText={setServerURL}
|
||||||
value={serverURL}
|
value={serverURL}
|
||||||
keyboardType="url"
|
keyboardType="url"
|
||||||
@@ -299,14 +297,13 @@ const Login: React.FC = () => {
|
|||||||
textContentType="URL"
|
textContentType="URL"
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
loading={loadingServerCheck}
|
loading={loadingServerCheck}
|
||||||
disabled={loadingServerCheck}
|
disabled={loadingServerCheck}
|
||||||
onPress={async () => await handleConnect(serverURL)}
|
onPress={async () => await handleConnect(serverURL)}
|
||||||
className="w-full grow"
|
className="w-full grow"
|
||||||
>
|
>
|
||||||
Connect
|
{t("server.connect_button")}
|
||||||
</Button>
|
</Button>
|
||||||
<JellyfinServerDiscovery
|
<JellyfinServerDiscovery
|
||||||
onServerSelect={(server) => {
|
onServerSelect={(server) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useMemo } from "react";
|
|||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof View> {
|
interface Props extends React.ComponentProps<typeof View> {
|
||||||
source?: MediaSourceInfo;
|
source?: MediaSourceInfo;
|
||||||
@@ -26,6 +27,8 @@ export const AudioTrackSelector: React.FC<Props> = ({
|
|||||||
[audioStreams, selected]
|
[audioStreams, selected]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className="flex shrink"
|
className="flex shrink"
|
||||||
@@ -36,7 +39,7 @@ export const AudioTrackSelector: React.FC<Props> = ({
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<View className="flex flex-col" {...props}>
|
<View className="flex flex-col" {...props}>
|
||||||
<Text className="opacity-50 mb-1 text-xs">Audio</Text>
|
<Text className="opacity-50 mb-1 text-xs">{t("item_card.audio")}</Text>
|
||||||
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
<Text className="" numberOfLines={1}>
|
<Text className="" numberOfLines={1}>
|
||||||
{selectedAudioSteam?.DisplayTitle}
|
{selectedAudioSteam?.DisplayTitle}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { TouchableOpacity, View } from "react-native";
|
|||||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export type Bitrate = {
|
export type Bitrate = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -63,6 +64,8 @@ export const BitrateSelector: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className="flex shrink"
|
className="flex shrink"
|
||||||
@@ -74,7 +77,7 @@ export const BitrateSelector: React.FC<Props> = ({
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<View className="flex flex-col" {...props}>
|
<View className="flex flex-col" {...props}>
|
||||||
<Text className="opacity-50 mb-1 text-xs">Quality</Text>
|
<Text className="opacity-50 mb-1 text-xs">{t("item_card.quality")}</Text>
|
||||||
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
<Text style={{}} className="" numberOfLines={1}>
|
<Text style={{}} className="" numberOfLines={1}>
|
||||||
{BITRATES.find((b) => b.value === selected?.value)?.key}
|
{BITRATES.find((b) => b.value === selected?.value)?.key}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { MediaSourceSelector } from "./MediaSourceSelector";
|
|||||||
import ProgressCircle from "./ProgressCircle";
|
import ProgressCircle from "./ProgressCircle";
|
||||||
import { RoundButton } from "./RoundButton";
|
import { RoundButton } from "./RoundButton";
|
||||||
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
|
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
interface DownloadProps extends ViewProps {
|
interface DownloadProps extends ViewProps {
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[];
|
||||||
@@ -55,6 +56,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [queue, setQueue] = useAtom(queueAtom);
|
const [queue, setQueue] = useAtom(queueAtom);
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
|
||||||
const { processes, startBackgroundDownload, downloadedFiles } = useDownload();
|
const { processes, startBackgroundDownload, downloadedFiles } = useDownload();
|
||||||
const { startRemuxing } = useRemuxHlsToMp4();
|
const { startRemuxing } = useRemuxHlsToMp4();
|
||||||
|
|
||||||
@@ -160,7 +162,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error("You are not allowed to download files.");
|
toast.error(t("home.downloads.toasts.you_are_not_allowed_to_download_files"));
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
queue,
|
queue,
|
||||||
@@ -212,8 +214,8 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Something went wrong",
|
t("home.downloads.something_went_wrong"),
|
||||||
"Could not get stream url from Jellyfin"
|
t("home.downloads.could_not_get_stream_url_from_jellyfin")
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -330,7 +332,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-neutral-300">
|
<Text className="text-neutral-300">
|
||||||
{subtitle || `Download ${itemsNotDownloaded.length} items`}
|
{subtitle || t("item_card.download.download_x_item", {item_count: itemsNotDownloaded.length})}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex flex-col space-y-2 w-full items-start">
|
<View className="flex flex-col space-y-2 w-full items-start">
|
||||||
@@ -368,13 +370,13 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
onPress={acceptDownloadOptions}
|
onPress={acceptDownloadOptions}
|
||||||
color="purple"
|
color="purple"
|
||||||
>
|
>
|
||||||
Download
|
{t("item_card.download.download_button")}
|
||||||
</Button>
|
</Button>
|
||||||
<View className="opacity-70 text-center w-full flex items-center">
|
<View className="opacity-70 text-center w-full flex items-center">
|
||||||
<Text className="text-xs">
|
<Text className="text-xs">
|
||||||
{usingOptimizedServer
|
{usingOptimizedServer
|
||||||
? "Using optimized server"
|
? t("item_card.download.using_optimized_server")
|
||||||
: "Using default method"}
|
: t("item_card.download.using_default_method")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -391,7 +393,9 @@ export const DownloadSingleItem: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<DownloadItems
|
<DownloadItems
|
||||||
size={size}
|
size={size}
|
||||||
title="Download Episode"
|
title={item.Type == "Episode"
|
||||||
|
? t("item_card.download.download_episode")
|
||||||
|
: t("item_card.download.download_movie")}
|
||||||
subtitle={item.Name!}
|
subtitle={item.Name!}
|
||||||
items={[item]}
|
items={[item]}
|
||||||
MissingDownloadIconComponent={() => (
|
MissingDownloadIconComponent={() => (
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
source?: MediaSourceInfo;
|
source?: MediaSourceInfo;
|
||||||
@@ -22,15 +23,16 @@ interface Props {
|
|||||||
|
|
||||||
export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
|
export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
|
||||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="px-4 mt-2 mb-4">
|
<View className="px-4 mt-2 mb-4">
|
||||||
<Text className="text-lg font-bold mb-4">Video</Text>
|
<Text className="text-lg font-bold mb-4">{t("item_card.video")}</Text>
|
||||||
<TouchableOpacity onPress={() => bottomSheetModalRef.current?.present()}>
|
<TouchableOpacity onPress={() => bottomSheetModalRef.current?.present()}>
|
||||||
<View className="flex flex-row space-x-2">
|
<View className="flex flex-row space-x-2">
|
||||||
<VideoStreamInfo source={source} />
|
<VideoStreamInfo source={source} />
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-purple-600">More details</Text>
|
<Text className="text-purple-600">{t("item_card.more_details")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<BottomSheetModal
|
<BottomSheetModal
|
||||||
ref={bottomSheetModalRef}
|
ref={bottomSheetModalRef}
|
||||||
@@ -52,14 +54,14 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
|
|||||||
<BottomSheetScrollView>
|
<BottomSheetScrollView>
|
||||||
<View className="flex flex-col space-y-2 p-4 mb-4">
|
<View className="flex flex-col space-y-2 p-4 mb-4">
|
||||||
<View className="">
|
<View className="">
|
||||||
<Text className="text-lg font-bold mb-4">Video</Text>
|
<Text className="text-lg font-bold mb-4">{t("item_card.video")}</Text>
|
||||||
<View className="flex flex-row space-x-2">
|
<View className="flex flex-row space-x-2">
|
||||||
<VideoStreamInfo source={source} />
|
<VideoStreamInfo source={source} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="">
|
<View className="">
|
||||||
<Text className="text-lg font-bold mb-2">Audio</Text>
|
<Text className="text-lg font-bold mb-2">{t("item_card.audio")}</Text>
|
||||||
<AudioStreamInfo
|
<AudioStreamInfo
|
||||||
audioStreams={
|
audioStreams={
|
||||||
source?.MediaStreams?.filter(
|
source?.MediaStreams?.filter(
|
||||||
@@ -70,7 +72,7 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="">
|
<View className="">
|
||||||
<Text className="text-lg font-bold mb-2">Subtitles</Text>
|
<Text className="text-lg font-bold mb-2">{t("item_card.subtitles")}</Text>
|
||||||
<SubtitleStreamInfo
|
<SubtitleStreamInfo
|
||||||
subtitleStreams={
|
subtitleStreams={
|
||||||
source?.MediaStreams?.filter(
|
source?.MediaStreams?.filter(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useJellyfinDiscovery } from "@/hooks/useJellyfinDiscovery";
|
|||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { ListGroup } from "./list/ListGroup";
|
import { ListGroup } from "./list/ListGroup";
|
||||||
import { ListItem } from "./list/ListItem";
|
import { ListItem } from "./list/ListItem";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onServerSelect?: (server: { address: string; serverName?: string }) => void;
|
onServerSelect?: (server: { address: string; serverName?: string }) => void;
|
||||||
@@ -11,17 +12,18 @@ interface Props {
|
|||||||
|
|
||||||
const JellyfinServerDiscovery: React.FC<Props> = ({ onServerSelect }) => {
|
const JellyfinServerDiscovery: React.FC<Props> = ({ onServerSelect }) => {
|
||||||
const { servers, isSearching, startDiscovery } = useJellyfinDiscovery();
|
const { servers, isSearching, startDiscovery } = useJellyfinDiscovery();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="mt-2">
|
<View className="mt-2">
|
||||||
<Button onPress={startDiscovery} color="black">
|
<Button onPress={startDiscovery} color="black">
|
||||||
<Text className="text-white text-center">
|
<Text className="text-white text-center">
|
||||||
{isSearching ? "Searching..." : "Search for local servers"}
|
{isSearching ? t("server.searching") : t("server.search_for_local_servers")}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{servers.length ? (
|
{servers.length ? (
|
||||||
<ListGroup title="Servers" className="mt-4">
|
<ListGroup title={t("server.servers")} className="mt-4">
|
||||||
{servers.map((server) => (
|
{servers.map((server) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={server.address}
|
key={server.address}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useMemo } from "react";
|
|||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof View> {
|
interface Props extends React.ComponentProps<typeof View> {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -27,6 +28,8 @@ export const MediaSourceSelector: React.FC<Props> = ({
|
|||||||
[item, selected]
|
[item, selected]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const commonPrefix = useMemo(() => {
|
const commonPrefix = useMemo(() => {
|
||||||
const mediaSources = item.MediaSources || [];
|
const mediaSources = item.MediaSources || [];
|
||||||
if (!mediaSources.length) return "";
|
if (!mediaSources.length) return "";
|
||||||
@@ -58,7 +61,7 @@ export const MediaSourceSelector: React.FC<Props> = ({
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<View className="flex flex-col" {...props}>
|
<View className="flex flex-col" {...props}>
|
||||||
<Text className="opacity-50 mb-1 text-xs">Video</Text>
|
<Text className="opacity-50 mb-1 text-xs">{t("item_card.video")}</Text>
|
||||||
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center">
|
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center">
|
||||||
<Text numberOfLines={1}>{selectedName}</Text>
|
<Text numberOfLines={1}>{selectedName}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
|||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
actorId: string;
|
actorId: string;
|
||||||
@@ -24,6 +25,7 @@ export const MoreMoviesWithActor: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data: actor } = useQuery({
|
const { data: actor } = useQuery({
|
||||||
queryKey: ["actor", actorId],
|
queryKey: ["actor", actorId],
|
||||||
@@ -76,7 +78,7 @@ export const MoreMoviesWithActor: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<Text className="text-lg font-bold mb-2 px-4">
|
<Text className="text-lg font-bold mb-2 px-4">
|
||||||
More with {actor?.Name}
|
{t("item_card.more_with", {name: actor?.Name})}
|
||||||
</Text>
|
</Text>
|
||||||
<HorizontalScroll
|
<HorizontalScroll
|
||||||
data={items}
|
data={items}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { TouchableOpacity, View, ViewProps } from "react-native";
|
|||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { tc } from "@/utils/textTools";
|
import { tc } from "@/utils/textTools";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
@@ -14,12 +15,13 @@ export const OverviewText: React.FC<Props> = ({
|
|||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [limit, setLimit] = useState(characterLimit);
|
const [limit, setLimit] = useState(characterLimit);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex flex-col" {...props}>
|
<View className="flex flex-col" {...props}>
|
||||||
<Text className="text-lg font-bold mb-2">Overview</Text>
|
<Text className="text-lg font-bold mb-2">{t("item_card.overview")}</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
setLimit((prev) =>
|
setLimit((prev) =>
|
||||||
@@ -31,7 +33,7 @@ export const OverviewText: React.FC<Props> = ({
|
|||||||
<Text>{tc(text, limit)}</Text>
|
<Text>{tc(text, limit)}</Text>
|
||||||
{text.length > characterLimit && (
|
{text.length > characterLimit && (
|
||||||
<Text className="text-purple-600 mt-1">
|
<Text className="text-purple-600 mt-1">
|
||||||
{limit === characterLimit ? "Show more" : "Show less"}
|
{limit === characterLimit ? t("item_card.show_more") : t("item_card.show_less")}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import Animated, {
|
|||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { SelectedOptions } from "./ItemContent";
|
import { SelectedOptions } from "./ItemContent";
|
||||||
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof Button> {
|
interface Props extends React.ComponentProps<typeof Button> {
|
||||||
@@ -50,6 +51,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const client = useRemoteMediaClient();
|
const client = useRemoteMediaClient();
|
||||||
const mediaStatus = useMediaStatus();
|
const mediaStatus = useMediaStatus();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [colorAtom] = useAtom(itemThemeColorAtom);
|
const [colorAtom] = useAtom(itemThemeColorAtom);
|
||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
@@ -132,8 +134,8 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
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(
|
||||||
"Client error",
|
t("player.client_error"),
|
||||||
"Could not create stream for Chromecast"
|
t("player.could_not_create_stream_for_chromecast")
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { View } from "react-native";
|
|||||||
import { useMMKVString } from "react-native-mmkv";
|
import { useMMKVString } from "react-native-mmkv";
|
||||||
import { ListGroup } from "./list/ListGroup";
|
import { ListGroup } from "./list/ListGroup";
|
||||||
import { ListItem } from "./list/ListItem";
|
import { ListItem } from "./list/ListItem";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Server {
|
interface Server {
|
||||||
address: string;
|
address: string;
|
||||||
@@ -22,11 +23,13 @@ export const PreviousServersList: React.FC<PreviousServersListProps> = ({
|
|||||||
return JSON.parse(_previousServers || "[]") as Server[];
|
return JSON.parse(_previousServers || "[]") as Server[];
|
||||||
}, [_previousServers]);
|
}, [_previousServers]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!previousServers.length) return null;
|
if (!previousServers.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ListGroup title="previous servers" className="mt-4">
|
<ListGroup title={t("server.previous_servers")} className="mt-4">
|
||||||
{previousServers.map((s) => (
|
{previousServers.map((s) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={s.address}
|
key={s.address}
|
||||||
@@ -39,7 +42,7 @@ export const PreviousServersList: React.FC<PreviousServersListProps> = ({
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
setPreviousServers("[]");
|
setPreviousServers("[]");
|
||||||
}}
|
}}
|
||||||
title={"Clear"}
|
title={t("server.clear_button")}
|
||||||
textColor="red"
|
textColor="red"
|
||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ItemCardText } from "./ItemCardText";
|
|||||||
import { Loader } from "./Loader";
|
import { Loader } from "./Loader";
|
||||||
import { HorizontalScroll } from "./common/HorrizontalScroll";
|
import { HorizontalScroll } from "./common/HorrizontalScroll";
|
||||||
import { TouchableItemRouter } from "./common/TouchableItemRouter";
|
import { TouchableItemRouter } from "./common/TouchableItemRouter";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface SimilarItemsProps extends ViewProps {
|
interface SimilarItemsProps extends ViewProps {
|
||||||
itemId?: string | null;
|
itemId?: string | null;
|
||||||
@@ -23,6 +24,7 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data: similarItems, isLoading } = useQuery<BaseItemDto[]>({
|
const { data: similarItems, isLoading } = useQuery<BaseItemDto[]>({
|
||||||
queryKey: ["similarItems", itemId],
|
queryKey: ["similarItems", itemId],
|
||||||
@@ -47,12 +49,12 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<Text className="px-4 text-lg font-bold mb-2">Similar items</Text>
|
<Text className="px-4 text-lg font-bold mb-2">{t("item_card.similar_items")}</Text>
|
||||||
<HorizontalScroll
|
<HorizontalScroll
|
||||||
data={movies}
|
data={movies}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
height={247}
|
height={247}
|
||||||
noItemsText="No similar items found"
|
noItemsText={t("item_card.no_similar_items_found")}
|
||||||
renderItem={(item: BaseItemDto, idx: number) => (
|
renderItem={(item: BaseItemDto, idx: number) => (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
key={idx}
|
key={idx}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Platform, TouchableOpacity, View } from "react-native";
|
|||||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
import { SubtitleHelper } from "@/utils/SubtitleHelper";
|
import { SubtitleHelper } from "@/utils/SubtitleHelper";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof View> {
|
interface Props extends React.ComponentProps<typeof View> {
|
||||||
source?: MediaSourceInfo;
|
source?: MediaSourceInfo;
|
||||||
@@ -37,6 +38,8 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
|
|||||||
|
|
||||||
if (subtitleStreams.length === 0) return null;
|
if (subtitleStreams.length === 0) return null;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className="flex col shrink justify-start place-self-start items-start"
|
className="flex col shrink justify-start place-self-start items-start"
|
||||||
@@ -48,12 +51,12 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<View className="flex flex-col " {...props}>
|
<View className="flex flex-col " {...props}>
|
||||||
<Text className="opacity-50 mb-1 text-xs">Subtitle</Text>
|
<Text className="opacity-50 mb-1 text-xs">{t("item_card.subtitles")}</Text>
|
||||||
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
<Text className=" ">
|
<Text className=" ">
|
||||||
{selectedSubtitleSteam
|
{selectedSubtitleSteam
|
||||||
? tc(selectedSubtitleSteam?.DisplayTitle, 7)
|
? tc(selectedSubtitleSteam?.DisplayTitle, 7)
|
||||||
: "None"}
|
: t("item_card.none")}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import Animated, {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { Loader } from "../Loader";
|
import { Loader } from "../Loader";
|
||||||
import { Text } from "./Text";
|
import { Text } from "./Text";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
interface HorizontalScrollProps
|
interface HorizontalScrollProps
|
||||||
extends Omit<FlashListProps<BaseItemDto>, "renderItem" | "data" | "style"> {
|
extends Omit<FlashListProps<BaseItemDto>, "renderItem" | "data" | "style"> {
|
||||||
@@ -136,7 +137,7 @@ export function InfiniteHorizontalScroll({
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View className="flex-1 justify-center items-center">
|
<View className="flex-1 justify-center items-center">
|
||||||
<Text className="text-center text-gray-500">No data available</Text>
|
<Text className="text-center text-gray-500">{t("item_card.no_data_available")}</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { Button } from "../Button";
|
|||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -28,14 +29,14 @@ export const ActiveDownloads: React.FC<Props> = ({ ...props }) => {
|
|||||||
if (processes?.length === 0)
|
if (processes?.length === 0)
|
||||||
return (
|
return (
|
||||||
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
||||||
<Text className="text-lg font-bold">Active download</Text>
|
<Text className="text-lg font-bold">{t("home.downloads.active_download")}</Text>
|
||||||
<Text className="opacity-50">No active downloads</Text>
|
<Text className="opacity-50">{t("home.downloads.no_active_downloads")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
|
||||||
<Text className="text-lg font-bold mb-2">Active downloads</Text>
|
<Text className="text-lg font-bold mb-2">{t("home.downloads.active_downloads")}</Text>
|
||||||
<View className="space-y-2">
|
<View className="space-y-2">
|
||||||
{processes?.map((p) => (
|
{processes?.map((p) => (
|
||||||
<DownloadCard key={p.item.Id} process={p} />
|
<DownloadCard key={p.item.Id} process={p} />
|
||||||
@@ -80,11 +81,11 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Download canceled");
|
toast.success(t("home.downloads.toasts.download_cancelled"));
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error("Could not cancel download");
|
toast.error(t("home.downloads.toasts.could_not_cancel_download"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,7 +152,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
|||||||
<Text className="text-xs">{process.speed?.toFixed(2)}x</Text>
|
<Text className="text-xs">{process.speed?.toFixed(2)}x</Text>
|
||||||
)}
|
)}
|
||||||
{eta(process) && (
|
{eta(process) && (
|
||||||
<Text className="text-xs">ETA {eta(process)}</Text>
|
<Text className="text-xs">{t("home.downloads.eta", {eta: eta(process)})}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { StyleSheet, TouchableOpacity, View, ViewProps } from "react-native";
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import { Input } from "../common/Input";
|
import { Input } from "../common/Input";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props<T> extends ViewProps {
|
interface Props<T> extends ViewProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -76,6 +77,7 @@ export const FilterSheet = <T,>({
|
|||||||
}: Props<T>) => {
|
}: Props<T>) => {
|
||||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||||
const snapPoints = useMemo(() => ["80%"], []);
|
const snapPoints = useMemo(() => ["80%"], []);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [data, setData] = useState<T[]>([]);
|
const [data, setData] = useState<T[]>([]);
|
||||||
const [offset, setOffset] = useState<number>(0);
|
const [offset, setOffset] = useState<number>(0);
|
||||||
@@ -153,10 +155,10 @@ export const FilterSheet = <T,>({
|
|||||||
>
|
>
|
||||||
<View className="px-4 mt-2 mb-8">
|
<View className="px-4 mt-2 mb-8">
|
||||||
<Text className="font-bold text-2xl">{title}</Text>
|
<Text className="font-bold text-2xl">{title}</Text>
|
||||||
<Text className="mb-2 text-neutral-500">{_data?.length} items</Text>
|
<Text className="mb-2 text-neutral-500">{t("search.items", {count: _data?.length})}</Text>
|
||||||
{showSearch && (
|
{showSearch && (
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search..."
|
placeholder={t("search.search")}
|
||||||
className="my-2"
|
className="my-2"
|
||||||
value={search}
|
value={search}
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { View } from "react-native";
|
|||||||
import { ScrollingCollectionList } from "./ScrollingCollectionList";
|
import { ScrollingCollectionList } from "./ScrollingCollectionList";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client";
|
import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
export const Favorites = () => {
|
export const Favorites = () => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
@@ -60,38 +61,38 @@ export const Favorites = () => {
|
|||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryFn={fetchFavoriteSeries}
|
queryFn={fetchFavoriteSeries}
|
||||||
queryKey={["home", "favorites", "series"]}
|
queryKey={["home", "favorites", "series"]}
|
||||||
title="Series"
|
title={t("favorites.series")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryFn={fetchFavoriteMovies}
|
queryFn={fetchFavoriteMovies}
|
||||||
queryKey={["home", "favorites", "movies"]}
|
queryKey={["home", "favorites", "movies"]}
|
||||||
title="Movies"
|
title={t("favorites.movies")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryFn={fetchFavoriteEpisodes}
|
queryFn={fetchFavoriteEpisodes}
|
||||||
queryKey={["home", "favorites", "episodes"]}
|
queryKey={["home", "favorites", "episodes"]}
|
||||||
title="Episodes"
|
title={t("favorites.episodes")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryFn={fetchFavoriteVideos}
|
queryFn={fetchFavoriteVideos}
|
||||||
queryKey={["home", "favorites", "videos"]}
|
queryKey={["home", "favorites", "videos"]}
|
||||||
title="Videos"
|
title={t("favorites.videos")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryFn={fetchFavoriteBoxsets}
|
queryFn={fetchFavoriteBoxsets}
|
||||||
queryKey={["home", "favorites", "boxsets"]}
|
queryKey={["home", "favorites", "boxsets"]}
|
||||||
title="Boxsets"
|
title={t("favorites.boxsets")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
/>
|
/>
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
queryFn={fetchFavoritePlaylists}
|
queryFn={fetchFavoritePlaylists}
|
||||||
queryKey={["home", "favorites", "playlists"]}
|
queryKey={["home", "favorites", "playlists"]}
|
||||||
title="Playlists"
|
title={t("favorites.playlists")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
|||||||
import { ItemCardText } from "../ItemCardText";
|
import { ItemCardText } from "../ItemCardText";
|
||||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||||
import SeriesPoster from "../posters/SeriesPoster";
|
import SeriesPoster from "../posters/SeriesPoster";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
@@ -43,6 +44,8 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
|
|
||||||
if (hideIfEmpty === true && data?.length === 0) return null;
|
if (hideIfEmpty === true && data?.length === 0) return null;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<Text className="px-4 text-lg font-bold mb-2 text-neutral-100">
|
<Text className="px-4 text-lg font-bold mb-2 text-neutral-100">
|
||||||
@@ -50,7 +53,7 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
{isLoading === false && data?.length === 0 && (
|
{isLoading === false && data?.length === 0 && (
|
||||||
<View className="px-4">
|
<View className="px-4">
|
||||||
<Text className="text-neutral-500">No items</Text>
|
<Text className="text-neutral-500">{t("home.no_items")}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import React from "react";
|
|||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import PersonPoster from "@/components/jellyseerr/PersonPoster";
|
import PersonPoster from "@/components/jellyseerr/PersonPoster";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const CastSlide: React.FC<
|
const CastSlide: React.FC<
|
||||||
{ details?: MovieDetails | TvDetails } & ViewProps
|
{ details?: MovieDetails | TvDetails } & ViewProps
|
||||||
> = ({ details, ...props }) => {
|
> = ({ details, ...props }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
details?.credits?.cast &&
|
details?.credits?.cast &&
|
||||||
details?.credits?.cast?.length > 0 && (
|
details?.credits?.cast?.length > 0 && (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<Text className="text-lg font-bold mb-2 px-4">Cast</Text>
|
<Text className="text-lg font-bold mb-2 px-4">{t("jellyseerr.cast")}</Text>
|
||||||
<FlashList
|
<FlashList
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces
|
|||||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
import CountryFlag from "react-native-country-flag";
|
import CountryFlag from "react-native-country-flag";
|
||||||
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
|
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Release {
|
interface Release {
|
||||||
certification: string;
|
certification: string;
|
||||||
@@ -50,6 +51,7 @@ const DetailFacts: React.FC<
|
|||||||
{ details?: MovieDetails | TvDetails } & ViewProps
|
{ details?: MovieDetails | TvDetails } & ViewProps
|
||||||
> = ({ details, className, ...props }) => {
|
> = ({ details, className, ...props }) => {
|
||||||
const { jellyseerrUser } = useJellyseerr();
|
const { jellyseerrUser } = useJellyseerr();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const locale = useMemo(() => {
|
const locale = useMemo(() => {
|
||||||
return jellyseerrUser?.settings?.locale || "en";
|
return jellyseerrUser?.settings?.locale || "en";
|
||||||
@@ -144,21 +146,21 @@ const DetailFacts: React.FC<
|
|||||||
return (
|
return (
|
||||||
details && (
|
details && (
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
<Text className="text-lg font-bold">Details</Text>
|
<Text className="text-lg font-bold">{t("jellyseerr.details")}</Text>
|
||||||
<View
|
<View
|
||||||
className={`${className} flex flex-col justify-center divide-y-2 divide-neutral-800`}
|
className={`${className} flex flex-col justify-center divide-y-2 divide-neutral-800`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Fact title="Status" fact={details?.status} />
|
<Fact title={t("jellyseerr.status")} fact={details?.status} />
|
||||||
<Fact
|
<Fact
|
||||||
title="Original Title"
|
title={t("jellyseerr.original_title")}
|
||||||
fact={(details as TvDetails)?.originalName}
|
fact={(details as TvDetails)?.originalName}
|
||||||
/>
|
/>
|
||||||
{details.keywords.some(
|
{details.keywords.some(
|
||||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||||
) && <Fact title="Series Type" fact="Anime" />}
|
) && <Fact title={t("jellyseerr.series_type")} fact="Anime" />}
|
||||||
<Facts
|
<Facts
|
||||||
title="Release Dates"
|
title={t("jellyseerr.release_dates")}
|
||||||
facts={filteredReleases?.map?.((r: Release, idx) => (
|
facts={filteredReleases?.map?.((r: Release, idx) => (
|
||||||
<View key={idx} className="flex flex-row space-x-2 items-center">
|
<View key={idx} className="flex flex-row space-x-2 items-center">
|
||||||
{r.type === 3 ? (
|
{r.type === 3 ? (
|
||||||
@@ -184,13 +186,13 @@ const DetailFacts: React.FC<
|
|||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
<Fact title="First Air Date" fact={firstAirDate} />
|
<Fact title={t("jellyseerr.first_air_date")} fact={firstAirDate} />
|
||||||
<Fact title="Next Air Date" fact={nextAirDate} />
|
<Fact title={t("jellyseerr.next_air_date")} fact={nextAirDate} />
|
||||||
<Fact title="Revenue" fact={revenue} />
|
<Fact title={t("jellyseerr.revenue")} fact={revenue} />
|
||||||
<Fact title="Budget" fact={budget} />
|
<Fact title={t("jellyseerr.budget")} fact={budget} />
|
||||||
<Fact title="Original Language" fact={spokenLanguage} />
|
<Fact title={t("jellyseerr.original_language")} fact={spokenLanguage} />
|
||||||
<Facts
|
<Facts
|
||||||
title="Production Country"
|
title={t("jellyseerr.production_country")}
|
||||||
facts={details?.productionCountries?.map((n, idx) => (
|
facts={details?.productionCountries?.map((n, idx) => (
|
||||||
<View key={idx} className="flex flex-row items-center space-x-2">
|
<View key={idx} className="flex flex-row items-center space-x-2">
|
||||||
<CountryFlag isoCode={n.iso_3166_1} size={10} />
|
<CountryFlag isoCode={n.iso_3166_1} size={10} />
|
||||||
@@ -199,14 +201,14 @@ const DetailFacts: React.FC<
|
|||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
<Facts
|
<Facts
|
||||||
title="Studios"
|
title={t("jellyseerr.studios")}
|
||||||
facts={uniqBy(details?.productionCompanies, "name")?.map(
|
facts={uniqBy(details?.productionCompanies, "name")?.map(
|
||||||
(n) => n.name
|
(n) => n.name
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Facts title="Network" facts={networks?.map((n) => n.name)} />
|
<Facts title={t("jellyseerr.network")}facts={networks?.map((n) => n.name)} />
|
||||||
<Facts
|
<Facts
|
||||||
title="Currently Streaming on"
|
title={t("jellyseerr.currently_streaming_on")}
|
||||||
facts={streamingProviders?.map((s) => s.name)}
|
facts={streamingProviders?.map((s) => s.name)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import JellyseerrPoster from "../posters/JellyseerrPoster";
|
|||||||
import { LoadingSkeleton } from "../search/LoadingSkeleton";
|
import { LoadingSkeleton } from "../search/LoadingSkeleton";
|
||||||
import { SearchItemWrapper } from "../search/SearchItemWrapper";
|
import { SearchItemWrapper } from "../search/SearchItemWrapper";
|
||||||
import PersonPoster from "./PersonPoster";
|
import PersonPoster from "./PersonPoster";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@@ -28,6 +29,7 @@ interface Props extends ViewProps {
|
|||||||
export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi } = useJellyseerr();
|
||||||
const opacity = useSharedValue(1);
|
const opacity = useSharedValue(1);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: jellyseerrDiscoverSettings,
|
data: jellyseerrDiscoverSettings,
|
||||||
@@ -117,7 +119,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
|||||||
!l2 && (
|
!l2 && (
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-center text-lg font-bold mt-4">
|
<Text className="text-center text-lg font-bold mt-4">
|
||||||
No results found for
|
{t("search.no_results_found_for")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs text-purple-600 text-center">
|
<Text className="text-xs text-purple-600 text-center">
|
||||||
"{searchQuery}"
|
"{searchQuery}"
|
||||||
@@ -127,21 +129,21 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
|||||||
|
|
||||||
<View className={f1 || f2 || l1 || l2 ? "opacity-0" : "opacity-100"}>
|
<View className={f1 || f2 || l1 || l2 ? "opacity-0" : "opacity-100"}>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
header="Request Movies"
|
header={t("search.request_movies")}
|
||||||
items={jellyseerrMovieResults}
|
items={jellyseerrMovieResults}
|
||||||
renderItem={(item: MovieResult) => (
|
renderItem={(item: MovieResult) => (
|
||||||
<JellyseerrPoster item={item} key={item.id} />
|
<JellyseerrPoster item={item} key={item.id} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
header="Request Series"
|
header={t("search.request_series")}
|
||||||
items={jellyseerrTvResults}
|
items={jellyseerrTvResults}
|
||||||
renderItem={(item: TvResult) => (
|
renderItem={(item: TvResult) => (
|
||||||
<JellyseerrPoster item={item} key={item.id} />
|
<JellyseerrPoster item={item} key={item.id} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SearchItemWrapper
|
<SearchItemWrapper
|
||||||
header="Actors"
|
header={t("search.actors")}
|
||||||
items={jellyseerrPersonResults}
|
items={jellyseerrPersonResults}
|
||||||
renderItem={(item: PersonResult) => (
|
renderItem={(item: PersonResult) => (
|
||||||
<PersonPoster
|
<PersonPoster
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/request
|
|||||||
import {BottomSheetModalMethods} from "@gorhom/bottom-sheet/lib/typescript/types";
|
import {BottomSheetModalMethods} from "@gorhom/bottom-sheet/lib/typescript/types";
|
||||||
import {Button} from "@/components/Button";
|
import {Button} from "@/components/Button";
|
||||||
import {Text} from "@/components/common/Text";
|
import {Text} from "@/components/common/Text";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -36,6 +37,8 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
userId: jellyseerrUser?.id
|
userId: jellyseerrUser?.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [modalRequestProps, setModalRequestProps] = useState<MediaRequestBody>();
|
const [modalRequestProps, setModalRequestProps] = useState<MediaRequestBody>();
|
||||||
|
|
||||||
const {data: serviceSettings} = useQuery({
|
const {data: serviceSettings} = useQuery({
|
||||||
@@ -103,7 +106,7 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
);
|
);
|
||||||
|
|
||||||
const seasonTitle = useMemo(
|
const seasonTitle = useMemo(
|
||||||
() => modalRequestProps?.seasons?.length ? `Season (${modalRequestProps?.seasons})` : undefined,
|
() => modalRequestProps?.seasons?.length ? t("jellyseerr.season_x", {seasons: modalRequestProps?.seasons}) : undefined,
|
||||||
[modalRequestProps?.seasons]
|
[modalRequestProps?.seasons]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -148,7 +151,7 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
return <BottomSheetView>
|
return <BottomSheetView>
|
||||||
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
||||||
<View>
|
<View>
|
||||||
<Text className="font-bold text-2xl text-neutral-100">Advanced</Text>
|
<Text className="font-bold text-2xl text-neutral-100">{t("jellyseerr.advanced")}</Text>
|
||||||
{seasonTitle &&
|
{seasonTitle &&
|
||||||
<Text className="text-neutral-300">{seasonTitle}</Text>
|
<Text className="text-neutral-300">{seasonTitle}</Text>
|
||||||
}
|
}
|
||||||
@@ -161,27 +164,27 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
titleExtractor={(item) => item.name}
|
titleExtractor={(item) => item.name}
|
||||||
placeholderText={defaultProfile.name}
|
placeholderText={defaultProfile.name}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
label={"Quality Profile"}
|
label={t("jellyseerr.quality_profile")}
|
||||||
onSelected={(item) =>
|
onSelected={(item) =>
|
||||||
item && setRequestOverrides((prev) => ({
|
item && setRequestOverrides((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
profileId: item?.id
|
profileId: item?.id
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
title={"Quality Profile"}
|
title={t("jellyseerr.quality_profile")}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
data={defaultServiceDetails.rootFolders}
|
data={defaultServiceDetails.rootFolders}
|
||||||
titleExtractor={pathTitleExtractor}
|
titleExtractor={pathTitleExtractor}
|
||||||
placeholderText={defaultFolder ? pathTitleExtractor(defaultFolder) : ""}
|
placeholderText={defaultFolder ? pathTitleExtractor(defaultFolder) : ""}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
label={"Root Folder"}
|
label={t("jellyseerr.root_folder")}
|
||||||
onSelected={(item) =>
|
onSelected={(item) =>
|
||||||
item && setRequestOverrides((prev) => ({
|
item && setRequestOverrides((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
rootFolder: item.path
|
rootFolder: item.path
|
||||||
}))}
|
}))}
|
||||||
title={"Root Folder"}
|
title={t("jellyseerr.root_folder")}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
multi={true}
|
multi={true}
|
||||||
@@ -189,28 +192,28 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
titleExtractor={(item) => item.label}
|
titleExtractor={(item) => item.label}
|
||||||
placeholderText={defaultTags.map(t => t.label).join(",")}
|
placeholderText={defaultTags.map(t => t.label).join(",")}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
label={"Tags"}
|
label={t("jellyseerr.tags")}
|
||||||
onSelected={(...item) =>
|
onSelected={(...item) =>
|
||||||
item && setRequestOverrides((prev) => ({
|
item && setRequestOverrides((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
tags: item.map(i => i.id)
|
tags: item.map(i => i.id)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
title={"Tags"}
|
title={t("jellyseerr.tags")}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
data={users}
|
data={users}
|
||||||
titleExtractor={(item) => item.displayName}
|
titleExtractor={(item) => item.displayName}
|
||||||
placeholderText={jellyseerrUser!!.displayName}
|
placeholderText={jellyseerrUser!!.displayName}
|
||||||
keyExtractor={(item) => item.id.toString() || ""}
|
keyExtractor={(item) => item.id.toString() || ""}
|
||||||
label={"Request As"}
|
label={t("jellyseerr.request_as")}
|
||||||
onSelected={(item) =>
|
onSelected={(item) =>
|
||||||
item && setRequestOverrides((prev) => ({
|
item && setRequestOverrides((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
userId: item?.id
|
userId: item?.id
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
title={"Request As"}
|
title={t("jellyseerr.request_as")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -221,7 +224,7 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
|
|||||||
onPress={request}
|
onPress={request}
|
||||||
color="purple"
|
color="purple"
|
||||||
>
|
>
|
||||||
Request
|
{t("jellyseerr.request_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</BottomSheetView>
|
</BottomSheetView>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover
|
|||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
import {View, ViewProps} from "react-native";
|
import {View, ViewProps} from "react-native";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
export interface SlideProps {
|
export interface SlideProps {
|
||||||
slide: DiscoverSlider;
|
slide: DiscoverSlider;
|
||||||
@@ -32,7 +33,7 @@ const Slide = <T extends unknown>({
|
|||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<Text className="font-bold text-lg mb-2 px-4">
|
<Text className="font-bold text-lg mb-2 px-4">
|
||||||
{DiscoverSliderType[slide.type].toString().toTitle()}
|
{t("search." + DiscoverSliderType[slide.type].toString().toLowerCase())}
|
||||||
</Text>
|
</Text>
|
||||||
<FlashList
|
<FlashList
|
||||||
horizontal
|
horizontal
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useAtom } from "jotai";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { TouchableOpacityProps, View } from "react-native";
|
import { TouchableOpacityProps, View } from "react-native";
|
||||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
library: BaseItemDto;
|
library: BaseItemDto;
|
||||||
@@ -42,6 +43,8 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
|||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const url = useMemo(
|
const url = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getPrimaryImageUrl({
|
getPrimaryImageUrl({
|
||||||
@@ -69,13 +72,13 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
|||||||
let nameStr: string;
|
let nameStr: string;
|
||||||
|
|
||||||
if (library.CollectionType === "movies") {
|
if (library.CollectionType === "movies") {
|
||||||
nameStr = "movies";
|
nameStr = t("library.item_types.movies");
|
||||||
} else if (library.CollectionType === "tvshows") {
|
} else if (library.CollectionType === "tvshows") {
|
||||||
nameStr = "series";
|
nameStr = t("library.item_types.series");
|
||||||
} else if (library.CollectionType === "boxsets") {
|
} else if (library.CollectionType === "boxsets") {
|
||||||
nameStr = "box sets";
|
nameStr = t("library.item_types.boxsets");
|
||||||
} else {
|
} else {
|
||||||
nameStr = "items";
|
nameStr = t("library.item_types.items");
|
||||||
}
|
}
|
||||||
|
|
||||||
return nameStr;
|
return nameStr;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { HorizontalScroll } from "../common/HorrizontalScroll";
|
|||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import Poster from "../posters/Poster";
|
import Poster from "../posters/Poster";
|
||||||
import { itemRouter } from "../common/TouchableItemRouter";
|
import { itemRouter } from "../common/TouchableItemRouter";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
item?: BaseItemDto | null;
|
item?: BaseItemDto | null;
|
||||||
@@ -21,6 +22,7 @@ interface Props extends ViewProps {
|
|||||||
export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
|
export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
|
const { t } = useTranslation();
|
||||||
const from = segments[2];
|
const from = segments[2];
|
||||||
|
|
||||||
const destinctPeople = useMemo(() => {
|
const destinctPeople = useMemo(() => {
|
||||||
@@ -40,7 +42,7 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props} className="flex flex-col">
|
<View {...props} className="flex flex-col">
|
||||||
<Text className="text-lg font-bold mb-2 px-4">Cast & Crew</Text>
|
<Text className="text-lg font-bold mb-2 px-4">{t("item_card.cast_and_crew")}</Text>
|
||||||
<HorizontalScroll
|
<HorizontalScroll
|
||||||
loading={loading}
|
loading={loading}
|
||||||
keyExtractor={(i, idx) => i.Id.toString()}
|
keyExtractor={(i, idx) => i.Id.toString()}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Poster from "../posters/Poster";
|
|||||||
import { HorizontalScroll } from "../common/HorrizontalScroll";
|
import { HorizontalScroll } from "../common/HorrizontalScroll";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
item?: BaseItemDto | null;
|
item?: BaseItemDto | null;
|
||||||
@@ -15,10 +16,11 @@ interface Props extends ViewProps {
|
|||||||
|
|
||||||
export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
|
export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<Text className="text-lg font-bold mb-2 px-4">Series</Text>
|
<Text className="text-lg font-bold mb-2 px-4">{t("item_card.series")}</Text>
|
||||||
<HorizontalScroll
|
<HorizontalScroll
|
||||||
data={[item]}
|
data={[item]}
|
||||||
height={247}
|
height={247}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
|
|||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
||||||
import { Loader } from "../Loader";
|
import { Loader } from "../Loader";
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -173,13 +174,13 @@ const JellyseerrSeasons: React.FC<{
|
|||||||
|
|
||||||
const promptRequestAll = useCallback(
|
const promptRequestAll = useCallback(
|
||||||
() =>
|
() =>
|
||||||
Alert.alert("Confirm", "Are you sure you want to request all seasons?", [
|
Alert.alert(t("jellyseerr.confirm"), t("jellyseerr.are_you_sure_you_want_to_request_all_seasons"), [
|
||||||
{
|
{
|
||||||
text: "Cancel",
|
text: t("jellyseerr.cancel"),
|
||||||
style: "cancel",
|
style: "cancel",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Yes",
|
text: t("jellyseerr.yes"),
|
||||||
onPress: requestAll,
|
onPress: requestAll,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -207,7 +208,7 @@ const JellyseerrSeasons: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View className="flex flex-row justify-between items-end px-4">
|
<View className="flex flex-row justify-between items-end px-4">
|
||||||
<Text className="text-lg font-bold mb-2">Seasons</Text>
|
<Text className="text-lg font-bold mb-2">{t("item_card.seasons")}</Text>
|
||||||
{!allSeasonsAvailable && (
|
{!allSeasonsAvailable && (
|
||||||
<RoundButton className="mb-2 pa-2" onPress={promptRequestAll}>
|
<RoundButton className="mb-2 pa-2" onPress={promptRequestAll}>
|
||||||
<Ionicons name="bag-add" color="white" size={26} />
|
<Ionicons name="bag-add" color="white" size={26} />
|
||||||
@@ -227,7 +228,7 @@ const JellyseerrSeasons: React.FC<{
|
|||||||
)}
|
)}
|
||||||
ListHeaderComponent={() => (
|
ListHeaderComponent={() => (
|
||||||
<View className="flex flex-row justify-between items-end px-4">
|
<View className="flex flex-row justify-between items-end px-4">
|
||||||
<Text className="text-lg font-bold mb-2">Seasons</Text>
|
<Text className="text-lg font-bold mb-2">{t("item_card.seasons")}</Text>
|
||||||
{!allSeasonsAvailable && (
|
{!allSeasonsAvailable && (
|
||||||
<RoundButton className="mb-2 pa-2" onPress={promptRequestAll}>
|
<RoundButton className="mb-2 pa-2" onPress={promptRequestAll}>
|
||||||
<Ionicons name="bag-add" color="white" size={26} />
|
<Ionicons name="bag-add" color="white" size={26} />
|
||||||
@@ -255,8 +256,8 @@ const JellyseerrSeasons: React.FC<{
|
|||||||
<Tags
|
<Tags
|
||||||
textClass=""
|
textClass=""
|
||||||
tags={[
|
tags={[
|
||||||
`Season ${season.seasonNumber}`,
|
t("jellyseerr.season_number", {season_number: season.seasonNumber}),
|
||||||
`${season.episodeCount} Episodes`,
|
t("jellyseerr.number_episodes", {episode_number: season.episodeCount}),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{[0].map(() => {
|
{[0].map(() => {
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
|||||||
import { ItemCardText } from "../ItemCardText";
|
import { ItemCardText } from "../ItemCardText";
|
||||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
|
export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data: items } = useQuery({
|
const { data: items } = useQuery({
|
||||||
queryKey: ["nextUp", seriesId],
|
queryKey: ["nextUp", seriesId],
|
||||||
@@ -37,14 +39,14 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
|
|||||||
if (!items?.length)
|
if (!items?.length)
|
||||||
return (
|
return (
|
||||||
<View className="px-4">
|
<View className="px-4">
|
||||||
<Text className="text-lg font-bold mb-2">Next up</Text>
|
<Text className="text-lg font-bold mb-2">{t("item_card.next_up")}</Text>
|
||||||
<Text className="opacity-50">No items to display</Text>
|
<Text className="opacity-50">{t("item_card.no_items_to_display")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-lg font-bold px-4 mb-2">Next up</Text>
|
<Text className="text-lg font-bold px-4 mb-2">{t("item_card.next_up")}</Text>
|
||||||
<FlashList
|
<FlashList
|
||||||
contentContainerStyle={{ paddingLeft: 16 }}
|
contentContainerStyle={{ paddingLeft: 16 }}
|
||||||
horizontal
|
horizontal
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useEffect, useMemo } from "react";
|
|||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -91,7 +92,7 @@ export const SeasonDropdown: React.FC<Props> = ({
|
|||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<View className="flex flex-row">
|
<View className="flex flex-row">
|
||||||
<TouchableOpacity className="bg-neutral-900 rounded-2xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
<TouchableOpacity className="bg-neutral-900 rounded-2xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
<Text>Season {seasonIndex}</Text>
|
<Text>{t("item_card.season")} {seasonIndex}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
@@ -104,7 +105,7 @@ export const SeasonDropdown: React.FC<Props> = ({
|
|||||||
collisionPadding={8}
|
collisionPadding={8}
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Label>Seasons</DropdownMenu.Label>
|
<DropdownMenu.Label>{t("item_card.seasons")}</DropdownMenu.Label>
|
||||||
{seasons?.sort(sortByIndex).map((season: any) => (
|
{seasons?.sort(sortByIndex).map((season: any) => (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key={season[keys.title]}
|
key={season[keys.title]}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
SeasonIndexState,
|
SeasonIndexState,
|
||||||
} from "@/components/series/SeasonDropdown";
|
} from "@/components/series/SeasonDropdown";
|
||||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
type Props = {
|
type Props = {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
initialSeasonIndex?: number;
|
initialSeasonIndex?: number;
|
||||||
@@ -29,6 +29,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
|
|||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom);
|
const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const seasonIndex = useMemo(
|
const seasonIndex = useMemo(
|
||||||
() => seasonIndexState[item.Id ?? ""],
|
() => seasonIndexState[item.Id ?? ""],
|
||||||
@@ -145,7 +146,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
|
|||||||
/>
|
/>
|
||||||
{episodes?.length || 0 > 0 ? (
|
{episodes?.length || 0 > 0 ? (
|
||||||
<DownloadItems
|
<DownloadItems
|
||||||
title="Download Season"
|
title={t("item_card.download.download_season")}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
items={episodes || []}
|
items={episodes || []}
|
||||||
MissingDownloadIconComponent={() => (
|
MissingDownloadIconComponent={() => (
|
||||||
@@ -210,7 +211,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
|
|||||||
{(episodes?.length || 0) === 0 ? (
|
{(episodes?.length || 0) === 0 ? (
|
||||||
<View className="flex flex-col">
|
<View className="flex flex-col">
|
||||||
<Text className="text-neutral-500">
|
<Text className="text-neutral-500">
|
||||||
No episodes for this season
|
{t("item_card.no_episodes_for_this_season")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
76
components/settings/AppLanguageSelector.tsx
Normal file
76
components/settings/AppLanguageSelector.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||||
|
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { APP_LANGUAGES } from "@/i18n";
|
||||||
|
|
||||||
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
|
export const AppLanguageSelector: React.FC<Props> = ({ ...props }) => {
|
||||||
|
const [settings, updateSettings] = useSettings();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!settings) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ListGroup
|
||||||
|
title={t("home.settings.languages.title")}
|
||||||
|
>
|
||||||
|
<ListItem title={t("home.settings.languages.app_language")}>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
|
<Text>
|
||||||
|
{APP_LANGUAGES.find(
|
||||||
|
(l) => l.value === settings?.preferedLanguage
|
||||||
|
)?.label || t("home.settings.languages.system")}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
loop={true}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
alignOffset={0}
|
||||||
|
avoidCollisions={true}
|
||||||
|
collisionPadding={8}
|
||||||
|
sideOffset={8}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Label>
|
||||||
|
{t("home.settings.languages.title")}
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={"unknown"}
|
||||||
|
onSelect={() => {
|
||||||
|
updateSettings({
|
||||||
|
preferedLanguage: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>
|
||||||
|
{t("home.settings.languages.system")}
|
||||||
|
</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{APP_LANGUAGES?.map((l) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={l?.value ?? "unknown"}
|
||||||
|
onSelect={() => {
|
||||||
|
updateSettings({
|
||||||
|
preferedLanguage: l.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>{l.label}</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</ListItem>
|
||||||
|
</ListGroup>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import * as DropdownMenu from "zeego/dropdown-menu";
|
|||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { useMedia } from "./MediaContext";
|
import { useMedia } from "./MediaContext";
|
||||||
import { Switch } from "react-native-gesture-handler";
|
import { Switch } from "react-native-gesture-handler";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
@@ -15,21 +16,22 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
const [_, __, pluginSettings] = useSettings();
|
const [_, __, pluginSettings] = useSettings();
|
||||||
const { settings, updateSettings } = media;
|
const { settings, updateSettings } = media;
|
||||||
const cultures = media.cultures;
|
const cultures = media.cultures;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup
|
<ListGroup
|
||||||
title={"Audio"}
|
title={t("home.settings.audio.audio_title")}
|
||||||
description={
|
description={
|
||||||
<Text className="text-[#8E8D91] text-xs">
|
<Text className="text-[#8E8D91] text-xs">
|
||||||
Choose a default audio language.
|
{t("home.settings.audio.audio_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={"Set Audio Track From Previous Item"}
|
title={t("home.settings.audio.set_audio_track")}
|
||||||
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
disabled={pluginSettings?.rememberAudioSelections?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -40,12 +42,12 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem title="Audio language">
|
<ListItem title={t("home.settings.audio.audio_language")}>
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3 ">
|
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3 ">
|
||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
{settings?.defaultAudioLanguage?.DisplayName || "None"}
|
{settings?.defaultAudioLanguage?.DisplayName || t("home.settings.audio.none")}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-expand-sharp"
|
name="chevron-expand-sharp"
|
||||||
@@ -63,7 +65,7 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
collisionPadding={8}
|
collisionPadding={8}
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Label>Languages</DropdownMenu.Label>
|
<DropdownMenu.Label>{t("home.settings.audio.language")}</DropdownMenu.Label>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key={"none-audio"}
|
key={"none-audio"}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
@@ -72,7 +74,7 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle>None</DropdownMenu.ItemTitle>
|
<DropdownMenu.ItemTitle>{t("home.settings.audio.none")}</DropdownMenu.ItemTitle>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{cultures?.map((l) => (
|
{cultures?.map((l) => (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as DropdownMenu from "zeego/dropdown-menu";
|
|||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
|
||||||
export const DownloadSettings: React.FC = ({ ...props }) => {
|
export const DownloadSettings: React.FC = ({ ...props }) => {
|
||||||
@@ -17,6 +18,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
|
|||||||
const { setProcesses } = useDownload();
|
const { setProcesses } = useDownload();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const allDisabled = useMemo(
|
const allDisabled = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -30,9 +32,9 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DisabledSetting disabled={allDisabled} {...props} className="mb-4">
|
<DisabledSetting disabled={allDisabled} {...props} className="mb-4">
|
||||||
<ListGroup title="Downloads">
|
<ListGroup title={t("home.settings.downloads.downloads_title")}>
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Download method"
|
title={t("home.settings.downloads.download_method")}
|
||||||
disabled={pluginSettings?.downloadMethod?.locked}
|
disabled={pluginSettings?.downloadMethod?.locked}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
@@ -40,8 +42,8 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
|
|||||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
{settings.downloadMethod === DownloadMethod.Remux
|
{settings.downloadMethod === DownloadMethod.Remux
|
||||||
? "Default"
|
? t("home.settings.downloads.default")
|
||||||
: "Optimized"}
|
: t("home.settings.downloads.optimized")}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-expand-sharp"
|
name="chevron-expand-sharp"
|
||||||
@@ -59,7 +61,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
|
|||||||
collisionPadding={8}
|
collisionPadding={8}
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Label>Methods</DropdownMenu.Label>
|
<DropdownMenu.Label>{t("home.settings.downloads.methods")}</DropdownMenu.Label>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key="1"
|
key="1"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
@@ -67,7 +69,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
|
|||||||
setProcesses([]);
|
setProcesses([]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle>Default</DropdownMenu.ItemTitle>
|
<DropdownMenu.ItemTitle>{t("home.settings.downloads.default")}</DropdownMenu.ItemTitle>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key="2"
|
key="2"
|
||||||
@@ -77,14 +79,14 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["search"] });
|
queryClient.invalidateQueries({ queryKey: ["search"] });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemTitle>Optimized</DropdownMenu.ItemTitle>
|
<DropdownMenu.ItemTitle>{t("home.settings.downloads.optimized")}</DropdownMenu.ItemTitle>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Remux max download"
|
title={t("home.settings.downloads.remux_max_download")}
|
||||||
disabled={
|
disabled={
|
||||||
pluginSettings?.remuxConcurrentLimit?.locked ||
|
pluginSettings?.remuxConcurrentLimit?.locked ||
|
||||||
settings.downloadMethod !== DownloadMethod.Remux
|
settings.downloadMethod !== DownloadMethod.Remux
|
||||||
@@ -104,7 +106,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Auto download"
|
title={t("home.settings.downloads.auto_download")}
|
||||||
disabled={
|
disabled={
|
||||||
pluginSettings?.autoDownload?.locked ||
|
pluginSettings?.autoDownload?.locked ||
|
||||||
settings.downloadMethod !== DownloadMethod.Optimized
|
settings.downloadMethod !== DownloadMethod.Optimized
|
||||||
@@ -127,7 +129,7 @@ export const DownloadSettings: React.FC = ({ ...props }) => {
|
|||||||
}
|
}
|
||||||
onPress={() => router.push("/settings/optimized-server/page")}
|
onPress={() => router.push("/settings/optimized-server/page")}
|
||||||
showArrow
|
showArrow
|
||||||
title="Optimized Versions Server"
|
title={t("home.settings.downloads.optimized_versions_server")}
|
||||||
></ListItem>
|
></ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
|||||||
import { userAtom } from "@/providers/JellyfinProvider";
|
import { userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
@@ -20,6 +21,8 @@ export const JellyseerrSettings = () => {
|
|||||||
clearAllJellyseerData,
|
clearAllJellyseerData,
|
||||||
} = useJellyseerr();
|
} = useJellyseerr();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings();
|
const [settings, updateSettings, pluginSettings] = useSettings();
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ export const JellyseerrSettings = () => {
|
|||||||
updateSettings({ jellyseerrServerUrl });
|
updateSettings({ jellyseerrServerUrl });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error("Failed to login");
|
toast.error(t("jellyseerr.failed_to_login"));
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
setJellyseerrPassword(undefined);
|
setJellyseerrPassword(undefined);
|
||||||
@@ -89,53 +92,50 @@ export const JellyseerrSettings = () => {
|
|||||||
<>
|
<>
|
||||||
<ListGroup title={"Jellyseerr"}>
|
<ListGroup title={"Jellyseerr"}>
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Total media requests"
|
title={t("home.settings.plugins.jellyseerr.total_media_requests")}
|
||||||
value={jellyseerrUser?.requestCount?.toString()}
|
value={jellyseerrUser?.requestCount?.toString()}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Movie quota limit"
|
title={t("home.settings.plugins.jellyseerr.movie_quota_limit")}
|
||||||
value={
|
value={
|
||||||
jellyseerrUser?.movieQuotaLimit?.toString() ?? "Unlimited"
|
jellyseerrUser?.movieQuotaLimit?.toString() ?? t("home.settings.plugins.jellyseerr.unlimited")
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Movie quota days"
|
title={t("home.settings.plugins.jellyseerr.movie_quota_days")}
|
||||||
value={
|
value={
|
||||||
jellyseerrUser?.movieQuotaDays?.toString() ?? "Unlimited"
|
jellyseerrUser?.movieQuotaDays?.toString() ?? t("home.settings.plugins.jellyseerr.unlimited")
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title="TV quota limit"
|
title={t("home.settings.plugins.jellyseerr.tv_quota_limit")}
|
||||||
value={jellyseerrUser?.tvQuotaLimit?.toString() ?? "Unlimited"}
|
value={jellyseerrUser?.tvQuotaLimit?.toString() ?? t("home.settings.plugins.jellyseerr.unlimited")}
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title="TV quota days"
|
title={t("home.settings.plugins.jellyseerr.tv_quota_days")}
|
||||||
value={jellyseerrUser?.tvQuotaDays?.toString() ?? "Unlimited"}
|
value={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}>
|
||||||
Reset Jellyseerr config
|
{t("home.settings.plugins.jellyseerr.reset_jellyseerr_config_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<View className="flex flex-col rounded-xl overflow-hidden p-4 bg-neutral-900">
|
<View className="flex flex-col rounded-xl overflow-hidden p-4 bg-neutral-900">
|
||||||
<Text className="text-xs text-red-600 mb-2">
|
<Text className="text-xs text-red-600 mb-2">
|
||||||
This integration is in its early stages. Expect things to change.
|
{t("home.settings.plugins.jellyseerr.jellyseerr_warning")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="font-bold mb-1">Server URL</Text>
|
<Text className="font-bold mb-1">{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">
|
||||||
Example: http(s)://your-host.url
|
{t("home.settings.plugins.jellyseerr.server_url_hint")}
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-gray-600">
|
|
||||||
(add port if required)
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Jellyseerr URL..."
|
placeholder={t("home.settings.plugins.jellyseerr.server_url_placeholder")}
|
||||||
value={settings?.jellyseerrServerUrl ?? jellyseerrServerUrl}
|
value={settings?.jellyseerrServerUrl ?? jellyseerrServerUrl}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
settings?.jellyseerrServerUrl ?? jellyseerrServerUrl
|
settings?.jellyseerrServerUrl ?? jellyseerrServerUrl
|
||||||
@@ -165,7 +165,7 @@ export const JellyseerrSettings = () => {
|
|||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{promptForJellyseerrPass ? "Clear" : "Save"}
|
{promptForJellyseerrPass ? t("home.settings.plugins.jellyseerr.clear_button") : t("home.settings.plugins.jellyseerr.save_button")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -174,11 +174,11 @@ export const JellyseerrSettings = () => {
|
|||||||
opacity: promptForJellyseerrPass ? 1 : 0.5,
|
opacity: promptForJellyseerrPass ? 1 : 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text className="font-bold mb-2">Password</Text>
|
<Text className="font-bold mb-2">{t("home.settings.plugins.jellyseerr.password")}</Text>
|
||||||
<Input
|
<Input
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
focusable={true}
|
focusable={true}
|
||||||
placeholder={`Enter password for Jellyfin user ${user?.Name}`}
|
placeholder={t("home.settings.plugins.jellyseerr.password_placeholder", {username: user?.Name})}
|
||||||
value={jellyseerrPassword}
|
value={jellyseerrPassword}
|
||||||
keyboardType="default"
|
keyboardType="default"
|
||||||
secureTextEntry={true}
|
secureTextEntry={true}
|
||||||
@@ -198,7 +198,7 @@ export const JellyseerrSettings = () => {
|
|||||||
className="h-12 mt-2"
|
className="h-12 mt-2"
|
||||||
onPress={() => loginToJellyseerrMutation.mutate()}
|
onPress={() => loginToJellyseerrMutation.mutate()}
|
||||||
>
|
>
|
||||||
Login
|
{t("home.settings.plugins.jellyseerr.login_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { ViewProps } from "react-native";
|
|||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import {Stepper} from "@/components/inputs/Stepper";
|
import {Stepper} from "@/components/inputs/Stepper";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
export const MediaToggles: React.FC<Props> = ({ ...props }) => {
|
export const MediaToggles: React.FC<Props> = ({ ...props }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings();
|
const [settings, updateSettings, pluginSettings] = useSettings();
|
||||||
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
@@ -25,16 +28,16 @@ export const MediaToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ListGroup title="Media Controls">
|
<ListGroup title={t("home.settings.media_controls.media_controls_title")}>
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Forward Skip Length"
|
title={t("home.settings.media_controls.forward_skip_length")}
|
||||||
disabled={pluginSettings?.forwardSkipTime?.locked}
|
disabled={pluginSettings?.forwardSkipTime?.locked}
|
||||||
>
|
>
|
||||||
<Stepper
|
<Stepper
|
||||||
value={settings.forwardSkipTime}
|
value={settings.forwardSkipTime}
|
||||||
disabled={pluginSettings?.forwardSkipTime?.locked}
|
disabled={pluginSettings?.forwardSkipTime?.locked}
|
||||||
step={5}
|
step={5}
|
||||||
appendValue="s"
|
appendValue={t("home.settings.media_controls.seconds_unit")}
|
||||||
min={0}
|
min={0}
|
||||||
max={60}
|
max={60}
|
||||||
onUpdate={(forwardSkipTime) => updateSettings({forwardSkipTime})}
|
onUpdate={(forwardSkipTime) => updateSettings({forwardSkipTime})}
|
||||||
@@ -42,14 +45,14 @@ export const MediaToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Rewind Length"
|
title={t("home.settings.media_controls.rewind_length")}
|
||||||
disabled={pluginSettings?.rewindSkipTime?.locked}
|
disabled={pluginSettings?.rewindSkipTime?.locked}
|
||||||
>
|
>
|
||||||
<Stepper
|
<Stepper
|
||||||
value={settings.rewindSkipTime}
|
value={settings.rewindSkipTime}
|
||||||
disabled={pluginSettings?.rewindSkipTime?.locked}
|
disabled={pluginSettings?.rewindSkipTime?.locked}
|
||||||
step={5}
|
step={5}
|
||||||
appendValue="s"
|
appendValue={t("home.settings.media_controls.seconds_unit")}
|
||||||
min={0}
|
min={0}
|
||||||
max={60}
|
max={60}
|
||||||
onUpdate={(rewindSkipTime) => updateSettings({rewindSkipTime})}
|
onUpdate={(rewindSkipTime) => updateSettings({rewindSkipTime})}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TextInput, View, Linking } from "react-native";
|
import { TextInput, View, Linking } from "react-native";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -14,14 +15,16 @@ export const OptimizedServerForm: React.FC<Props> = ({
|
|||||||
Linking.openURL("https://github.com/streamyfin/optimized-versions-server");
|
Linking.openURL("https://github.com/streamyfin/optimized-versions-server");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View className="flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4">
|
<View className="flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4">
|
||||||
<View className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}>
|
<View className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}>
|
||||||
<Text className="mr-4">URL</Text>
|
<Text className="mr-4">{t("home.settings.downloads.url")}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
className="text-white"
|
className="text-white"
|
||||||
placeholder="http(s)://domain.org:port"
|
placeholder={t("home.settings.downloads.server_url_placeholder")}
|
||||||
value={value}
|
value={value}
|
||||||
keyboardType="url"
|
keyboardType="url"
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
@@ -32,10 +35,9 @@ export const OptimizedServerForm: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
<Text className="px-4 text-xs text-neutral-500 mt-1">
|
||||||
Enter the URL for the optimize server. The URL should include http or
|
{t("home.settings.downloads.optimized_version_hint")}{" "}
|
||||||
https and optionally the port.{" "}
|
|
||||||
<Text className="text-blue-500" onPress={handleOpenLink}>
|
<Text className="text-blue-500" onPress={handleOpenLink}>
|
||||||
Read more about the optimize server.
|
{t("home.settings.downloads.read_more_about_optimized_server")}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { toast } from "sonner-native";
|
|||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import Dropdown from "@/components/common/Dropdown";
|
import Dropdown from "@/components/common/Dropdown";
|
||||||
|
|
||||||
@@ -22,6 +23,8 @@ export const OtherSettings: React.FC = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [settings, updateSettings, pluginSettings] = useSettings();
|
const [settings, updateSettings, pluginSettings] = useSettings();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
/********************
|
/********************
|
||||||
* Background task
|
* Background task
|
||||||
*******************/
|
*******************/
|
||||||
@@ -74,9 +77,9 @@ export const OtherSettings: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DisabledSetting disabled={disabled}>
|
<DisabledSetting disabled={disabled}>
|
||||||
<ListGroup title="Other" className="">
|
<ListGroup title={t("home.settings.other.other_title")} className="">
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Auto rotate"
|
title={t("home.settings.other.auto_rotate")}
|
||||||
disabled={pluginSettings?.autoRotate?.locked}
|
disabled={pluginSettings?.autoRotate?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -87,7 +90,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Video orientation"
|
title={t("home.settings.other.video_orientation")}
|
||||||
disabled={
|
disabled={
|
||||||
pluginSettings?.defaultVideoOrientation?.locked ||
|
pluginSettings?.defaultVideoOrientation?.locked ||
|
||||||
settings.autoRotate
|
settings.autoRotate
|
||||||
@@ -104,7 +107,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
title={
|
title={
|
||||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
{ScreenOrientationEnum[settings.defaultVideoOrientation]}
|
{t(ScreenOrientationEnum[settings.defaultVideoOrientation])}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-expand-sharp"
|
name="chevron-expand-sharp"
|
||||||
@@ -113,7 +116,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
label="Orientation"
|
label={t("home.settings.other.orientation")}
|
||||||
onSelected={(defaultVideoOrientation) =>
|
onSelected={(defaultVideoOrientation) =>
|
||||||
updateSettings({ defaultVideoOrientation })
|
updateSettings({ defaultVideoOrientation })
|
||||||
}
|
}
|
||||||
@@ -121,7 +124,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Safe area in controls"
|
title={t("home.settings.other.safe_area_in_controls")}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -134,7 +137,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Show Custom Menu Links"
|
title={t("home.settings.other.show_custom_menu_links")}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
Linking.openURL(
|
Linking.openURL(
|
||||||
@@ -152,11 +155,11 @@ export const OtherSettings: React.FC = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => router.push("/settings/hide-libraries/page")}
|
onPress={() => router.push("/settings/hide-libraries/page")}
|
||||||
title="Hide Libraries"
|
title={t("home.settings.other.hide_libraries")}
|
||||||
showArrow
|
showArrow
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Disable Haptic Feedback"
|
title={t("home.settings.other.disable_haptic_feedback")}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import React from "react";
|
|||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const PluginSettings = () => {
|
export const PluginSettings = () => {
|
||||||
const [settings, updateSettings] = useSettings();
|
const [settings, updateSettings] = useSettings();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ListGroup title="Plugins">
|
<ListGroup title={t("home.settings.plugins.plugins_title")} className="mb-4">
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => router.push("/settings/jellyseerr/page")}
|
onPress={() => router.push("/settings/jellyseerr/page")}
|
||||||
title={"Jellyseerr"}
|
title={"Jellyseerr"}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
BottomSheetView,
|
BottomSheetView,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
@@ -26,6 +27,8 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
|||||||
const successHapticFeedback = useHaptic("success");
|
const successHapticFeedback = useHaptic("success");
|
||||||
const errorHapticFeedback = useHaptic("error");
|
const errorHapticFeedback = useHaptic("error");
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const renderBackdrop = useCallback(
|
const renderBackdrop = useCallback(
|
||||||
(props: BottomSheetBackdropProps) => (
|
(props: BottomSheetBackdropProps) => (
|
||||||
<BottomSheetBackdrop
|
<BottomSheetBackdrop
|
||||||
@@ -46,26 +49,26 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
|||||||
});
|
});
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
successHapticFeedback();
|
successHapticFeedback();
|
||||||
Alert.alert("Success", "Quick connect authorized");
|
Alert.alert(t("home.settings.quick_connect.success"), t("home.settings.quick_connect.quick_connect_autorized"));
|
||||||
setQuickConnectCode(undefined);
|
setQuickConnectCode(undefined);
|
||||||
bottomSheetModalRef?.current?.close();
|
bottomSheetModalRef?.current?.close();
|
||||||
} else {
|
} else {
|
||||||
errorHapticFeedback();
|
errorHapticFeedback();
|
||||||
Alert.alert("Error", "Invalid code");
|
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorHapticFeedback();
|
errorHapticFeedback();
|
||||||
Alert.alert("Error", "Invalid code");
|
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [api, user, quickConnectCode]);
|
}, [api, user, quickConnectCode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup title={"Quick Connect"}>
|
<ListGroup title={t("home.settings.quick_connect.quick_connect_title")}>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => bottomSheetModalRef?.current?.present()}
|
onPress={() => bottomSheetModalRef?.current?.present()}
|
||||||
title="Authorize Quick Connect"
|
title={t("home.settings.quick_connect.authorize_button")}
|
||||||
textColor="blue"
|
textColor="blue"
|
||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
@@ -85,7 +88,7 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
|||||||
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
||||||
<View>
|
<View>
|
||||||
<Text className="font-bold text-2xl text-neutral-100">
|
<Text className="font-bold text-2xl text-neutral-100">
|
||||||
Quick Connect
|
{t("home.settings.quick_connect.quick_connect_title")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex flex-col space-y-2">
|
<View className="flex flex-col space-y-2">
|
||||||
@@ -93,7 +96,7 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
|||||||
<BottomSheetTextInput
|
<BottomSheetTextInput
|
||||||
style={{ color: "white" }}
|
style={{ color: "white" }}
|
||||||
clearButtonMode="always"
|
clearButtonMode="always"
|
||||||
placeholder="Enter the quick connect code..."
|
placeholder={t("home.settings.quick_connect.enter_the_quick_connect_code")}
|
||||||
placeholderTextColor="#9CA3AF"
|
placeholderTextColor="#9CA3AF"
|
||||||
value={quickConnectCode}
|
value={quickConnectCode}
|
||||||
onChangeText={setQuickConnectCode}
|
onChangeText={setQuickConnectCode}
|
||||||
@@ -105,7 +108,7 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
|||||||
onPress={authorizeQuickConnect}
|
onPress={authorizeQuickConnect}
|
||||||
color="purple"
|
color="purple"
|
||||||
>
|
>
|
||||||
Authorize
|
{t("home.settings.quick_connect.authorize")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</BottomSheetView>
|
</BottomSheetView>
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import { View } from "react-native";
|
|||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const StorageSettings = () => {
|
export const StorageSettings = () => {
|
||||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||||
|
const { t } = useTranslation();
|
||||||
const successHapticFeedback = useHaptic("success");
|
const successHapticFeedback = useHaptic("success");
|
||||||
const errorHapticFeedback = useHaptic("error");
|
const errorHapticFeedback = useHaptic("error");
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ export const StorageSettings = () => {
|
|||||||
successHapticFeedback();
|
successHapticFeedback();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorHapticFeedback();
|
errorHapticFeedback();
|
||||||
toast.error("Error deleting files");
|
toast.error(t("home.settings.toasts.error_deleting_files"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,11 +45,10 @@ export const StorageSettings = () => {
|
|||||||
<View>
|
<View>
|
||||||
<View className="flex flex-col gap-y-1">
|
<View className="flex flex-col gap-y-1">
|
||||||
<View className="flex flex-row items-center justify-between">
|
<View className="flex flex-row items-center justify-between">
|
||||||
<Text className="">Storage</Text>
|
<Text className="">{t("home.settings.storage.storage_title")}</Text>
|
||||||
{size && (
|
{size && (
|
||||||
<Text className="text-neutral-500">
|
<Text className="text-neutral-500">
|
||||||
{Number(size.total - size.remaining).bytesToReadable()} of{" "}
|
{t("home.settings.storage.size_used", {used: Number(size.total - size.remaining).bytesToReadable(), total: size.total?.bytesToReadable()})}
|
||||||
{size.total?.bytesToReadable()} used
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -78,18 +79,13 @@ export const StorageSettings = () => {
|
|||||||
<View className="flex flex-row items-center">
|
<View className="flex flex-row items-center">
|
||||||
<View className="w-3 h-3 rounded-full bg-purple-600 mr-1"></View>
|
<View className="w-3 h-3 rounded-full bg-purple-600 mr-1"></View>
|
||||||
<Text className="text-white text-xs">
|
<Text className="text-white text-xs">
|
||||||
App {calculatePercentage(size.app, size.total)}%
|
{t("home.settings.storage.app_usage", {usedSpace: calculatePercentage(size.app, size.total)})}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex flex-row items-center">
|
<View className="flex flex-row items-center">
|
||||||
<View className="w-3 h-3 rounded-full bg-purple-400 mr-1"></View>
|
<View className="w-3 h-3 rounded-full bg-purple-400 mr-1"></View>
|
||||||
<Text className="text-white text-xs">
|
<Text className="text-white text-xs">
|
||||||
Phone{" "}
|
{t("home.settings.storage.phone_usage", {availableSpace: calculatePercentage(size.total - size.remaining - size.app, size.total)})}
|
||||||
{calculatePercentage(
|
|
||||||
size.total - size.remaining - size.app,
|
|
||||||
size.total
|
|
||||||
)}
|
|
||||||
%
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
@@ -100,7 +96,7 @@ export const StorageSettings = () => {
|
|||||||
<ListItem
|
<ListItem
|
||||||
textColor="red"
|
textColor="red"
|
||||||
onPress={onDeleteClicked}
|
onPress={onDeleteClicked}
|
||||||
title="Delete All Downloaded Files"
|
title={t("home.settings.storage.delete_all_downloaded_files")}
|
||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ListGroup } from "../list/ListGroup";
|
|||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client";
|
import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {useSettings} from "@/utils/atoms/settings";
|
import {useSettings} from "@/utils/atoms/settings";
|
||||||
import {Stepper} from "@/components/inputs/Stepper";
|
import {Stepper} from "@/components/inputs/Stepper";
|
||||||
import Dropdown from "@/components/common/Dropdown";
|
import Dropdown from "@/components/common/Dropdown";
|
||||||
@@ -18,6 +19,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
const [_, __, pluginSettings] = useSettings();
|
const [_, __, pluginSettings] = useSettings();
|
||||||
const { settings, updateSettings } = media;
|
const { settings, updateSettings } = media;
|
||||||
const cultures = media.cultures;
|
const cultures = media.cultures;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|
||||||
@@ -29,25 +31,33 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
SubtitlePlaybackMode.None,
|
SubtitlePlaybackMode.None,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const subtitleModeKeys = {
|
||||||
|
[SubtitlePlaybackMode.Default]: "home.settings.subtitles.modes.Default",
|
||||||
|
[SubtitlePlaybackMode.Smart]: "home.settings.subtitles.modes.Smart",
|
||||||
|
[SubtitlePlaybackMode.OnlyForced]: "home.settings.subtitles.modes.OnlyForced",
|
||||||
|
[SubtitlePlaybackMode.Always]: "home.settings.subtitles.modes.Always",
|
||||||
|
[SubtitlePlaybackMode.None]: "home.settings.subtitles.modes.None",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup
|
<ListGroup
|
||||||
title={"Subtitles"}
|
title={t("home.settings.subtitles.subtitle_title")}
|
||||||
description={
|
description={
|
||||||
<Text className="text-[#8E8D91] text-xs">
|
<Text className="text-[#8E8D91] text-xs">
|
||||||
Configure subtitle preferences.
|
{t("home.settings.subtitles.subtitle_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem title="Subtitle language">
|
<ListItem title={t("home.settings.subtitles.subtitle_language")}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
data={[{DisplayName: "None", ThreeLetterISOLanguageName: "none-subs" },...(cultures ?? [])]}
|
data={[{DisplayName: t("home.settings.subtitles.none"), ThreeLetterISOLanguageName: "none-subs" },...(cultures ?? [])]}
|
||||||
keyExtractor={(item) => item?.ThreeLetterISOLanguageName ?? "unknown"}
|
keyExtractor={(item) => item?.ThreeLetterISOLanguageName ?? "unknown"}
|
||||||
titleExtractor={(item) => item?.DisplayName}
|
titleExtractor={(item) => item?.DisplayName}
|
||||||
title={
|
title={
|
||||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
{settings?.defaultSubtitleLanguage?.DisplayName || "None"}
|
{settings?.defaultSubtitleLanguage?.DisplayName || t("home.settings.subtitles.none")}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-expand-sharp"
|
name="chevron-expand-sharp"
|
||||||
@@ -56,10 +66,10 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
label="Languages"
|
label={t("home.settings.subtitles.language")}
|
||||||
onSelected={(defaultSubtitleLanguage) =>
|
onSelected={(defaultSubtitleLanguage) =>
|
||||||
updateSettings({
|
updateSettings({
|
||||||
defaultSubtitleLanguage: defaultSubtitleLanguage.DisplayName === "None"
|
defaultSubtitleLanguage: defaultSubtitleLanguage.DisplayName === t("home.settings.subtitles.none")
|
||||||
? null
|
? null
|
||||||
: defaultSubtitleLanguage
|
: defaultSubtitleLanguage
|
||||||
})
|
})
|
||||||
@@ -68,18 +78,18 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Subtitle Mode"
|
title={t("home.settings.subtitles.subtitle_mode")}
|
||||||
disabled={pluginSettings?.subtitleMode?.locked}
|
disabled={pluginSettings?.subtitleMode?.locked}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
data={subtitleModes}
|
data={subtitleModes}
|
||||||
disabled={pluginSettings?.subtitleMode?.locked}
|
disabled={pluginSettings?.subtitleMode?.locked}
|
||||||
keyExtractor={String}
|
keyExtractor={String}
|
||||||
titleExtractor={String}
|
titleExtractor={(item) => t(subtitleModeKeys[item]) || String(item)}
|
||||||
title={
|
title={
|
||||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
{settings?.subtitleMode || "Loading"}
|
{t(subtitleModeKeys[settings?.subtitleMode]) || t("home.settings.subtitles.loading")}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-expand-sharp"
|
name="chevron-expand-sharp"
|
||||||
@@ -88,7 +98,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
label="Subtitle Mode"
|
label={t("home.settings.subtitles.subtitle_mode")}
|
||||||
onSelected={(subtitleMode) =>
|
onSelected={(subtitleMode) =>
|
||||||
updateSettings({subtitleMode})
|
updateSettings({subtitleMode})
|
||||||
}
|
}
|
||||||
@@ -96,7 +106,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Set Subtitle Track From Previous Item"
|
title={t("home.settings.subtitles.set_subtitle_track")}
|
||||||
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
disabled={pluginSettings?.rememberSubtitleSelections?.locked}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -109,7 +119,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Subtitle Size"
|
title={t("home.settings.subtitles.subtitle_size")}
|
||||||
disabled={pluginSettings?.subtitleSize?.locked}
|
disabled={pluginSettings?.subtitleSize?.locked}
|
||||||
>
|
>
|
||||||
<Stepper
|
<Stepper
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import { useAtom } from "jotai";
|
|||||||
import Constants from "expo-constants";
|
import Constants from "expo-constants";
|
||||||
import Application from "expo-application";
|
import Application from "expo-application";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
export const UserInfo: React.FC<Props> = ({ ...props }) => {
|
export const UserInfo: React.FC<Props> = ({ ...props }) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const version =
|
const version =
|
||||||
Application?.nativeApplicationVersion ||
|
Application?.nativeApplicationVersion ||
|
||||||
@@ -21,11 +23,11 @@ export const UserInfo: React.FC<Props> = ({ ...props }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup title={"User Info"}>
|
<ListGroup title={t("home.settings.user_info.user_info_title")}>
|
||||||
<ListItem title="User" value={user?.Name} />
|
<ListItem title={t("home.settings.user_info.user")} value={user?.Name} />
|
||||||
<ListItem title="Server" value={api?.basePath} />
|
<ListItem title={t("home.settings.user_info.server")} value={api?.basePath} />
|
||||||
<ListItem title="Token" value={api?.accessToken} />
|
<ListItem title={t("home.settings.user_info.token")} value={api?.accessToken} />
|
||||||
<ListItem title="App version" value={version} />
|
<ListItem title={t("home.settings.user_info.app_version")} value={version} />
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Animated, {
|
|||||||
runOnJS,
|
runOnJS,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface NextEpisodeCountDownButtonProps extends TouchableOpacityProps {
|
interface NextEpisodeCountDownButtonProps extends TouchableOpacityProps {
|
||||||
onFinish?: () => void;
|
onFinish?: () => void;
|
||||||
@@ -63,6 +64,8 @@ const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
className="w-32 overflow-hidden rounded-md bg-black/60 border border-neutral-900"
|
className="w-32 overflow-hidden rounded-md bg-black/60 border border-neutral-900"
|
||||||
@@ -71,7 +74,7 @@ const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
|
|||||||
>
|
>
|
||||||
<Animated.View style={animatedStyle} />
|
<Animated.View style={animatedStyle} />
|
||||||
<View className="px-3 py-3">
|
<View className="px-3 py-3">
|
||||||
<Text className="text-center font-bold">Next Episode</Text>
|
<Text className="text-center font-bold">{t("player.next_episode")}</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
playerRef: React.RefObject<VlcPlayerViewRef>;
|
playerRef: React.RefObject<VlcPlayerViewRef>;
|
||||||
@@ -32,6 +33,8 @@ export const VideoDebugInfo: React.FC<Props> = ({ playerRef, ...props }) => {
|
|||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -42,19 +45,19 @@ export const VideoDebugInfo: React.FC<Props> = ({ playerRef, ...props }) => {
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Text className="font-bold">Playback State:</Text>
|
<Text className="font-bold">{t("player.playback_state")}</Text>
|
||||||
<Text className="font-bold mt-2.5">Audio Tracks:</Text>
|
<Text className="font-bold mt-2.5">{t("player.audio_tracks")}</Text>
|
||||||
{audioTracks &&
|
{audioTracks &&
|
||||||
audioTracks.map((track, index) => (
|
audioTracks.map((track, index) => (
|
||||||
<Text key={index}>
|
<Text key={index}>
|
||||||
{track.name} (Index: {track.index})
|
{track.name} ({t("player.index")} {track.index})
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
<Text className="font-bold mt-2.5">Subtitle Tracks:</Text>
|
<Text className="font-bold mt-2.5">{t("player.subtitles_tracks")}</Text>
|
||||||
{subtitleTracks &&
|
{subtitleTracks &&
|
||||||
subtitleTracks.map((track, index) => (
|
subtitleTracks.map((track, index) => (
|
||||||
<Text key={index}>
|
<Text key={index}>
|
||||||
{track.name} (Index: {track.index})
|
{track.name} ({t("player.index")} {track.index})
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -66,7 +69,7 @@ export const VideoDebugInfo: React.FC<Props> = ({ playerRef, ...props }) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text className="text-white text-center">Refresh Tracks</Text>
|
<Text className="text-white text-center">{t("player.refresh_tracks")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import Issue from "@/utils/jellyseerr/server/entity/Issue";
|
|||||||
import { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
|
import { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
|
||||||
import { writeErrorLog } from "@/utils/log";
|
import { writeErrorLog } from "@/utils/log";
|
||||||
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
||||||
|
import { t } from "i18next";
|
||||||
import {
|
import {
|
||||||
CombinedCredit,
|
CombinedCredit,
|
||||||
PersonDetails,
|
PersonDetails,
|
||||||
@@ -134,7 +135,7 @@ export class JellyseerrApi {
|
|||||||
if (inRange(status, 200, 299)) {
|
if (inRange(status, 200, 299)) {
|
||||||
if (data.version < "2.0.0") {
|
if (data.version < "2.0.0") {
|
||||||
const error =
|
const error =
|
||||||
"Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0";
|
t("jellyseerr.toasts.jellyseer_does_not_meet_requirements");
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
throw Error(error);
|
throw Error(error);
|
||||||
}
|
}
|
||||||
@@ -148,7 +149,7 @@ export class JellyseerrApi {
|
|||||||
requiresPass: true,
|
requiresPass: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
toast.error(`Jellyseerr test failed. Please try again.`);
|
toast.error(t("jellyseerr.toasts.jellyseerr_test_failed"));
|
||||||
writeErrorLog(
|
writeErrorLog(
|
||||||
`Jellyseerr returned a ${status} for url:\n` +
|
`Jellyseerr returned a ${status} for url:\n` +
|
||||||
response.config.url +
|
response.config.url +
|
||||||
@@ -161,7 +162,7 @@ export class JellyseerrApi {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
const msg = "Failed to test jellyseerr server url";
|
const msg = t("jellyseerr.toasts.failed_to_test_jellyseerr_server_url");
|
||||||
toast.error(msg);
|
toast.error(msg);
|
||||||
console.error(msg, e);
|
console.error(msg, e);
|
||||||
return {
|
return {
|
||||||
@@ -322,7 +323,7 @@ export class JellyseerrApi {
|
|||||||
const issue = response.data;
|
const issue = response.data;
|
||||||
|
|
||||||
if (issue.status === IssueStatus.OPEN) {
|
if (issue.status === IssueStatus.OPEN) {
|
||||||
toast.success("Issue submitted!");
|
toast.success(t("jellyseerr.toasts.issue_submitted"));
|
||||||
}
|
}
|
||||||
return issue;
|
return issue;
|
||||||
});
|
});
|
||||||
@@ -422,14 +423,14 @@ export const useJellyseerr = () => {
|
|||||||
switch (mediaRequest.status) {
|
switch (mediaRequest.status) {
|
||||||
case MediaRequestStatus.PENDING:
|
case MediaRequestStatus.PENDING:
|
||||||
case MediaRequestStatus.APPROVED:
|
case MediaRequestStatus.APPROVED:
|
||||||
toast.success(`Requested ${title}!`);
|
toast.success(t("jellyseerr.toasts.requested_item", {item: title}));
|
||||||
onSuccess?.();
|
onSuccess?.()
|
||||||
break;
|
break;
|
||||||
case MediaRequestStatus.DECLINED:
|
case MediaRequestStatus.DECLINED:
|
||||||
toast.error(`You don't have permission to request!`);
|
toast.error(t("jellyseerr.toasts.you_dont_have_permission_to_request"));
|
||||||
break;
|
break;
|
||||||
case MediaRequestStatus.FAILED:
|
case MediaRequestStatus.FAILED:
|
||||||
toast.error(`Something went wrong requesting media!`);
|
toast.error(t("jellyseerr.toasts.something_went_wrong_requesting_media"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import useDownloadHelper from "@/utils/download";
|
|||||||
import { Api } from "@jellyfin/sdk";
|
import { Api } from "@jellyfin/sdk";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { JobStatus } from "@/utils/optimize-server";
|
import { JobStatus } from "@/utils/optimize-server";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const createFFmpegCommand = (url: string, output: string) => [
|
const createFFmpegCommand = (url: string, output: string) => [
|
||||||
"-y", // overwrite output files without asking
|
"-y", // overwrite output files without asking
|
||||||
@@ -49,6 +50,7 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const { saveImage } = useImageStorage();
|
const { saveImage } = useImageStorage();
|
||||||
@@ -84,7 +86,7 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
queryKey: ["downloadedItems"],
|
queryKey: ["downloadedItems"],
|
||||||
});
|
});
|
||||||
saveDownloadedItemInfo(item, stat.getSize());
|
saveDownloadedItemInfo(item, stat.getSize());
|
||||||
toast.success("Download completed");
|
toast.success(t("home.downloads.toasts.download_completed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
setProcesses((prev) => {
|
setProcesses((prev) => {
|
||||||
@@ -144,7 +146,7 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
// First lets save any important assets we want to present to the user offline
|
// First lets save any important assets we want to present to the user offline
|
||||||
await onSaveAssets(api, item);
|
await onSaveAssets(api, item);
|
||||||
|
|
||||||
toast.success(`Download started for ${item.Name}`, {
|
toast.success(t("home.downloads.toasts.download_started_for", {item: item.Name}), {
|
||||||
action: {
|
action: {
|
||||||
label: "Go to download",
|
label: "Go to download",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
|||||||
import { Alert } from "react-native";
|
import { Alert } from "react-native";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface UseWebSocketProps {
|
interface UseWebSocketProps {
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
@@ -18,6 +19,7 @@ export const useWebSocket = ({
|
|||||||
}: UseWebSocketProps) => {
|
}: UseWebSocketProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { ws } = useWebSocketContext();
|
const { ws } = useWebSocketContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ws) return;
|
if (!ws) return;
|
||||||
@@ -40,7 +42,7 @@ export const useWebSocket = ({
|
|||||||
console.log("Command ~ DisplayMessage");
|
console.log("Command ~ DisplayMessage");
|
||||||
const title = json?.Data?.Arguments?.Header;
|
const title = json?.Data?.Arguments?.Header;
|
||||||
const body = json?.Data?.Arguments?.Text;
|
const body = json?.Data?.Arguments?.Text;
|
||||||
Alert.alert("Message from server: " + title, body);
|
Alert.alert(t("player.message_from_server", {message: title}), body);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
30
i18n.ts
Normal file
30
i18n.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
|
import en from "./translations/en.json";
|
||||||
|
import fr from "./translations/fr.json";
|
||||||
|
import sv from "./translations/sv.json";
|
||||||
|
import { getLocales } from "expo-localization";
|
||||||
|
|
||||||
|
export const APP_LANGUAGES = [
|
||||||
|
{ label: "English", value: "en" },
|
||||||
|
{ label: "Français", value: "fr" },
|
||||||
|
{ label: "Svenska", value: "sv" },
|
||||||
|
];
|
||||||
|
|
||||||
|
i18n.use(initReactI18next).init({
|
||||||
|
compatibilityJSON: "v4",
|
||||||
|
resources: {
|
||||||
|
en: { translation: en },
|
||||||
|
fr: { translation: fr },
|
||||||
|
sv: { translation: sv },
|
||||||
|
},
|
||||||
|
|
||||||
|
lng: getLocales()[0].languageCode || "en",
|
||||||
|
fallbackLng: "en",
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
"expo-keep-awake": "~13.0.2",
|
"expo-keep-awake": "~13.0.2",
|
||||||
"expo-linear-gradient": "~13.0.2",
|
"expo-linear-gradient": "~13.0.2",
|
||||||
"expo-linking": "~6.3.1",
|
"expo-linking": "~6.3.1",
|
||||||
|
"expo-localization": "~16.0.0",
|
||||||
"expo-network": "~6.0.1",
|
"expo-network": "~6.0.1",
|
||||||
"expo-notifications": "~0.28.19",
|
"expo-notifications": "~0.28.19",
|
||||||
"expo-router": "~3.5.24",
|
"expo-router": "~3.5.24",
|
||||||
@@ -67,11 +68,13 @@
|
|||||||
"expo-web-browser": "~13.0.3",
|
"expo-web-browser": "~13.0.3",
|
||||||
"ffmpeg-kit-react-native": "^6.0.2",
|
"ffmpeg-kit-react-native": "^6.0.2",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
|
"i18next": "^24.2.0",
|
||||||
"jotai": "^2.10.1",
|
"jotai": "^2.10.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nativewind": "^2.0.11",
|
"nativewind": "^2.0.11",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-i18next": "^15.4.0",
|
||||||
"react-native": "0.74.5",
|
"react-native": "0.74.5",
|
||||||
"react-native-awesome-slider": "^2.5.6",
|
"react-native-awesome-slider": "^2.5.6",
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import useDownloadHelper from "@/utils/download";
|
|||||||
import { FileInfo } from "expo-file-system";
|
import { FileInfo } from "expo-file-system";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import * as Application from "expo-application";
|
import * as Application from "expo-application";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export type DownloadedItem = {
|
export type DownloadedItem = {
|
||||||
item: Partial<BaseItemDto>;
|
item: Partial<BaseItemDto>;
|
||||||
@@ -68,6 +69,7 @@ const DownloadContext = createContext<ReturnType<
|
|||||||
|
|
||||||
function useDownloadProvider() {
|
function useDownloadProvider() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
@@ -139,9 +141,9 @@ function useDownloadProvider() {
|
|||||||
if (settings.autoDownload) {
|
if (settings.autoDownload) {
|
||||||
startDownload(job);
|
startDownload(job);
|
||||||
} else {
|
} else {
|
||||||
toast.info(`${job.item.Name} is ready to be downloaded`, {
|
toast.info(t("home.downloads.toasts.item_is_ready_to_be_downloaded",{item: job.item.Name}), {
|
||||||
action: {
|
action: {
|
||||||
label: "Go to downloads",
|
label: t("home.downloads.toasts.go_to_downloads"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push("/downloads");
|
router.push("/downloads");
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
@@ -224,9 +226,9 @@ function useDownloadProvider() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.info(`Download started for ${process.item.Name}`, {
|
toast.info(t("home.downloads.toasts.download_stated_for_item", {item: process.item.Name}), {
|
||||||
action: {
|
action: {
|
||||||
label: "Go to downloads",
|
label: t("home.downloads.toasts.go_to_downloads"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push("/downloads");
|
router.push("/downloads");
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
@@ -275,10 +277,10 @@ function useDownloadProvider() {
|
|||||||
process.item,
|
process.item,
|
||||||
doneHandler.bytesDownloaded
|
doneHandler.bytesDownloaded
|
||||||
);
|
);
|
||||||
toast.success(`Download completed for ${process.item.Name}`, {
|
toast.success(t("home.downloads.toasts.download_completed_for_item", {item: process.item.Name}), {
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
action: {
|
action: {
|
||||||
label: "Go to downloads",
|
label: t("home.downloads.toasts.go_to_downloads"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push("/downloads");
|
router.push("/downloads");
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
@@ -300,7 +302,7 @@ function useDownloadProvider() {
|
|||||||
if (error.errorCode === 404) {
|
if (error.errorCode === 404) {
|
||||||
errorMsg = "File not found on server";
|
errorMsg = "File not found on server";
|
||||||
}
|
}
|
||||||
toast.error(`Download failed for ${process.item.Name} - ${errorMsg}`);
|
toast.error(t("home.downloads.toasts.download_failed_for_item", {item: process.item.Name, error: errorMsg}));
|
||||||
writeToLog("ERROR", `Download failed for ${process.item.Name}`, {
|
writeToLog("ERROR", `Download failed for ${process.item.Name}`, {
|
||||||
error,
|
error,
|
||||||
processDetails: {
|
processDetails: {
|
||||||
@@ -357,9 +359,9 @@ function useDownloadProvider() {
|
|||||||
throw new Error("Failed to start optimization job");
|
throw new Error("Failed to start optimization job");
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(`Queued ${item.Name} for optimization`, {
|
toast.success(t("home.downloads.toasts.queued_item_for_optimization", {item: item.Name}), {
|
||||||
action: {
|
action: {
|
||||||
label: "Go to download",
|
label: t("home.downloads.toasts.go_to_downloads"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push("/downloads");
|
router.push("/downloads");
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
@@ -377,21 +379,21 @@ function useDownloadProvider() {
|
|||||||
headers: error.response?.headers,
|
headers: error.response?.headers,
|
||||||
});
|
});
|
||||||
toast.error(
|
toast.error(
|
||||||
`Failed to start download for ${item.Name}: ${error.message}`
|
t("home.downloads.toasts.failed_to_start_download_for_item", {item: item.Name, message: error.message})
|
||||||
);
|
);
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Server responded with status ${error.response.status}`
|
t("home.downloads.toasts.server_responded_with_status", {statusCode: error.response.status})
|
||||||
);
|
);
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
toast.error("No response received from server");
|
t("home.downloads.toasts.no_response_received_from_server");
|
||||||
} else {
|
} else {
|
||||||
toast.error("Error setting up the request");
|
toast.error("Error setting up the request");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Non-Axios error:", error);
|
console.error("Non-Axios error:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
`Failed to start download for ${item.Name}: Unexpected error`
|
t("home.downloads.toasts.failed_to_start_download_for_item_unexpected_error", {item: item.Name})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,11 +409,11 @@ function useDownloadProvider() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] }),
|
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] }),
|
||||||
])
|
])
|
||||||
.then(() =>
|
.then(() =>
|
||||||
toast.success("All files, folders, and jobs deleted successfully")
|
toast.success(t("home.downloads.toasts.all_files_folders_and_jobs_deleted_successfully"))
|
||||||
)
|
)
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
console.error("Failed to delete all files, folders, and jobs:", reason);
|
console.error("Failed to delete all files, folders, and jobs:", reason);
|
||||||
toast.error("An error occurred while deleting files and jobs");
|
toast.error(t("home.downloads.toasts.an_error_occured_while_deleting_files_and_jobs"));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import React, {
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import uuid from "react-native-uuid";
|
import uuid from "react-native-uuid";
|
||||||
import { getDeviceName } from "react-native-device-info";
|
import { getDeviceName } from "react-native-device-info";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
|
|
||||||
@@ -50,6 +51,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
const [jellyfin, setJellyfin] = useState<Jellyfin | undefined>(undefined);
|
const [jellyfin, setJellyfin] = useState<Jellyfin | undefined>(undefined);
|
||||||
const [deviceId, setDeviceId] = useState<string | undefined>(undefined);
|
const [deviceId, setDeviceId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const id = getOrSetDeviceId();
|
const id = getOrSetDeviceId();
|
||||||
@@ -261,22 +264,22 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
switch (error.response?.status) {
|
switch (error.response?.status) {
|
||||||
case 401:
|
case 401:
|
||||||
throw new Error("Invalid username or password");
|
throw new Error(t("login.invalid_username_or_password"));
|
||||||
case 403:
|
case 403:
|
||||||
throw new Error("User does not have permission to log in");
|
throw new Error(t("login.user_does_not_have_permission_to_log_in"));
|
||||||
case 408:
|
case 408:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Server is taking too long to respond, try again later"
|
t("login.server_is_taking_too_long_to_respond_try_again_later")
|
||||||
);
|
);
|
||||||
case 429:
|
case 429:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Server received too many requests, try again later"
|
t("login.server_received_too_many_requests_try_again_later")
|
||||||
);
|
);
|
||||||
case 500:
|
case 500:
|
||||||
throw new Error("There is a server error");
|
throw new Error(t("login.there_is_a_server_error"));
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"An unexpected error occurred. Did you enter the server URL correctly?"
|
t("login.an_unexpected_error_occured_did_you_enter_the_correct_url")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
457
translations/en.json
Normal file
457
translations/en.json
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
{
|
||||||
|
"login": {
|
||||||
|
"username_required": "Username is required",
|
||||||
|
"error_title": "Error",
|
||||||
|
"login_title": "Log in",
|
||||||
|
"login_to_title": "Log in to",
|
||||||
|
"username_placeholder": "Username",
|
||||||
|
"password_placeholder": "Password",
|
||||||
|
"login_button": "Log in",
|
||||||
|
"quick_connect": "Quick Connect",
|
||||||
|
"enter_code_to_login": "Enter code {{code}} to login",
|
||||||
|
"failed_to_initiate_quick_connect": "Failed to initiate Quick Connect",
|
||||||
|
"got_it": "Got it",
|
||||||
|
"connection_failed": "Connection failed",
|
||||||
|
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
|
||||||
|
"an_unexpected_error_occured": "An unexpected error occurred",
|
||||||
|
"change_server": "Change server",
|
||||||
|
"invalid_username_or_password": "Invalid username or password",
|
||||||
|
"user_does_not_have_permission_to_log_in": "User does not have permission to log in",
|
||||||
|
"server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
|
||||||
|
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
|
||||||
|
"there_is_a_server_error": "There is a server error",
|
||||||
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
|
||||||
|
"server_url_placeholder": "http(s)://your-server.com",
|
||||||
|
"connect_button": "Connect",
|
||||||
|
"previous_servers": "previous servers",
|
||||||
|
"clear_button": "Clear",
|
||||||
|
"search_for_local_servers": "Search for local servers",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"servers": "Servers"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"no_internet": "No Internet",
|
||||||
|
"no_items": "No items",
|
||||||
|
"no_internet_message": "No worries, you can still watch\ndownloaded content.",
|
||||||
|
"go_to_downloads": "Go to downloads",
|
||||||
|
"oops": "Oops!",
|
||||||
|
"error_message": "Something went wrong.\nPlease log out and in again.",
|
||||||
|
"continue_watching": "Continue Watching",
|
||||||
|
"next_up": "Next Up",
|
||||||
|
"recently_added_in": "Recently Added in {{libraryName}}",
|
||||||
|
"suggested_movies": "Suggested Movies",
|
||||||
|
"suggested_episodes": "Suggested Episodes",
|
||||||
|
"intro": {
|
||||||
|
"welcome_to_streamyfin": "Welcome to Streamyfin",
|
||||||
|
"a_free_and_open_source_client_for_jellyfin": "A free and open-source client for Jellyfin.",
|
||||||
|
"features_title": "Features",
|
||||||
|
"features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu, these include:",
|
||||||
|
"jellyseerr_feature_description": "Connect to your Jellyseerr instance and request movies directly in the app.",
|
||||||
|
"downloads_feature_title": "Downloads",
|
||||||
|
"downloads_feature_description": "Download movies and tv-shows to view offline. Use either the default method or install the optimize server to download files in the background.",
|
||||||
|
"chromecast_feature_description": "Cast movies and tv-shows to your Chromecast devices.",
|
||||||
|
"centralised_settings_plugin_title": "Centralised Settings Plugin",
|
||||||
|
"centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.",
|
||||||
|
"done_button": "Done",
|
||||||
|
"go_to_settings_button": "Go to settings",
|
||||||
|
"read_more": "Read more"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"settings_title": "Settings",
|
||||||
|
"log_out_button": "Log out",
|
||||||
|
"user_info": {
|
||||||
|
"user_info_title": "User Info",
|
||||||
|
"user": "User",
|
||||||
|
"server": "Server",
|
||||||
|
"token": "Token",
|
||||||
|
"app_version": "App Version"
|
||||||
|
},
|
||||||
|
"quick_connect": {
|
||||||
|
"quick_connect_title": "Quick Connect",
|
||||||
|
"authorize_button": "Authorize Quick Connect",
|
||||||
|
"enter_the_quick_connect_code": "Enter the quick connect code...",
|
||||||
|
"success": "Success",
|
||||||
|
"quick_connect_autorized": "Quick Connect authorized",
|
||||||
|
"error": "Error",
|
||||||
|
"invalid_code": "Invalid code",
|
||||||
|
"authorize": "Authorize"
|
||||||
|
},
|
||||||
|
"media_controls": {
|
||||||
|
"media_controls_title": "Media Controls",
|
||||||
|
"forward_skip_length": "Forward skip length",
|
||||||
|
"rewind_length": "Rewind length",
|
||||||
|
"seconds_unit": "s"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"audio_title": "Audio",
|
||||||
|
"set_audio_track": "Set Audio Track From Previous Item",
|
||||||
|
"audio_language": "Audio language",
|
||||||
|
"audio_hint": "Choose a default audio language.",
|
||||||
|
"none": "None",
|
||||||
|
"language": "Language"
|
||||||
|
},
|
||||||
|
"subtitles": {
|
||||||
|
"subtitle_title": "Subtitles",
|
||||||
|
"subtitle_language": "Subtitle language",
|
||||||
|
"subtitle_mode": "Subtitle Mode",
|
||||||
|
"set_subtitle_track": "Set Subtitle Track From Previous Item",
|
||||||
|
"subtitle_size": "Subtitle Size",
|
||||||
|
"subtitle_hint": "Configure subtitle preference.",
|
||||||
|
"none": "None",
|
||||||
|
"language": "Language",
|
||||||
|
"loading": "Loading",
|
||||||
|
"modes": {
|
||||||
|
"Default": "Default",
|
||||||
|
"Smart": "Smart",
|
||||||
|
"Always": "Always",
|
||||||
|
"None": "None",
|
||||||
|
"OnlyForced": "OnlyForced"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"other_title": "Other",
|
||||||
|
"auto_rotate": "Auto rotate",
|
||||||
|
"video_orientation": "Video orientation",
|
||||||
|
"orientation": "Orientation",
|
||||||
|
"orientations": {
|
||||||
|
"DEFAULT": "Default",
|
||||||
|
"ALL": "All",
|
||||||
|
"PORTRAIT": "Portrait",
|
||||||
|
"PORTRAIT_UP": "Portrait Up",
|
||||||
|
"PORTRAIT_DOWN": "Portrait Down",
|
||||||
|
"LANDSCAPE": "Landscape",
|
||||||
|
"LANDSCAPE_LEFT": "Landscape Left",
|
||||||
|
"LANDSCAPE_RIGHT": "Landscape Right",
|
||||||
|
"OTHER": "Other",
|
||||||
|
"UNKNOWN": "Unknown"
|
||||||
|
},
|
||||||
|
"safe_area_in_controls": "Safe area in controls",
|
||||||
|
"show_custom_menu_links": "Show Custom Menu Links",
|
||||||
|
"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.",
|
||||||
|
"disable_haptic_feedback": "Disable Haptic Feedback"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"downloads_title": "Downloads",
|
||||||
|
"download_method": "Download method",
|
||||||
|
"remux_max_download": "Remux max download",
|
||||||
|
"auto_download": "Auto download",
|
||||||
|
"optimized_versions_server": "Optimized versions server",
|
||||||
|
"save_button": "Save",
|
||||||
|
"optimized_server": "Optimized Server",
|
||||||
|
"optimized": "Optimized",
|
||||||
|
"default": "Default",
|
||||||
|
"optimized_version_hint": "Enter the URL for the optimize server. The URL should include http or https and optionally the port.",
|
||||||
|
"read_more_about_optimized_server": "Read more about the optimize server.",
|
||||||
|
"url":"URL",
|
||||||
|
"server_url_placeholder": "http(s)://domain.org:port"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"plugins_title": "Plugins",
|
||||||
|
"jellyseerr": {
|
||||||
|
"jellyseerr_warning": "This integration is in its early stages. Expect things to change.",
|
||||||
|
"server_url": "Server URL",
|
||||||
|
"server_url_hint": "Example: http(s)://your-host.url\n(add port if required)",
|
||||||
|
"server_url_placeholder": "Jellyseerr URL...",
|
||||||
|
"password": "Password",
|
||||||
|
"password_placeholder": "Enter password for Jellyfin user {{username}}",
|
||||||
|
"save_button": "Save",
|
||||||
|
"clear_button": "Clear",
|
||||||
|
"login_button": "Login",
|
||||||
|
"total_media_requests": "Total media requests",
|
||||||
|
"movie_quota_limit": "Movie quota limit",
|
||||||
|
"movie_quota_days": "Movie quota days",
|
||||||
|
"tv_quota_limit": "TV quota limit",
|
||||||
|
"tv_quota_days": "TV quota days",
|
||||||
|
"reset_jellyseerr_config_button": "Reset Jellyseerr config",
|
||||||
|
"unlimited": "Unlimited"
|
||||||
|
},
|
||||||
|
"marlin_search": {
|
||||||
|
"enable_marlin_search": "Enable Marlin Search ",
|
||||||
|
"url": "URL",
|
||||||
|
"server_url_placeholder": "http(s)://domain.org:port",
|
||||||
|
"marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.",
|
||||||
|
"read_more_about_marlin": "Read more about Marlin.",
|
||||||
|
"save_button": "Save",
|
||||||
|
"toasts": {
|
||||||
|
"saved": "Saved"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"storage_title": "Storage",
|
||||||
|
"app_usage": "App {{usedSpace}}%",
|
||||||
|
"phone_usage": "Phone {{availableSpace}}%",
|
||||||
|
"size_used": "{{used}} of {{total}} used",
|
||||||
|
"delete_all_downloaded_files": "Delete All Downloaded Files"
|
||||||
|
},
|
||||||
|
"intro": {
|
||||||
|
"show_intro": "Show intro",
|
||||||
|
"reset_intro": "Reset intro"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"logs_title": "Logs",
|
||||||
|
"no_logs_available": "No logs available",
|
||||||
|
"delete_all_logs": "Delete all logs"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"title": "Languages",
|
||||||
|
"app_language": "App language",
|
||||||
|
"app_language_description": "Select the language for the app.",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
|
"toasts":{
|
||||||
|
"error_deleting_files": "Error deleting files",
|
||||||
|
"background_downloads_enabled": "Background downloads enabled",
|
||||||
|
"background_downloads_disabled": "Background downloads disabled",
|
||||||
|
"connected": "Connected",
|
||||||
|
"could_not_connect": "Could not connect",
|
||||||
|
"invalid_url": "Invalid URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"downloads_title": "Downloads",
|
||||||
|
"tvseries": "TV-Series",
|
||||||
|
"movies": "Movies",
|
||||||
|
"queue": "Queue",
|
||||||
|
"queue_hint": "Queue and downloads will be lost on app restart",
|
||||||
|
"no_items_in_queue": "No items in queue",
|
||||||
|
"no_downloaded_items": "No downloaded items",
|
||||||
|
"delete_all_movies_button": "Delete all Movies",
|
||||||
|
"delete_all_tvseries_button": "Delete all TV-Series",
|
||||||
|
"delete_all_button": "Delete all",
|
||||||
|
"active_download": "Active download",
|
||||||
|
"no_active_downloads": "No active downloads",
|
||||||
|
"active_downloads": "Active downloads",
|
||||||
|
"new_app_version_requires_re_download": "New app version requires re-download",
|
||||||
|
"new_app_version_requires_re_download_description": "The new update requires content to be downloaded again. Please remove all downloaded content and try again.",
|
||||||
|
"back": "Back",
|
||||||
|
"delete": "Delete",
|
||||||
|
"something_went_wrong": "Something went wrong",
|
||||||
|
"could_not_get_stream_url_from_jellyfin": "Could not get the stream URL from Jellyfin",
|
||||||
|
"eta": "ETA {{eta}}",
|
||||||
|
"methods": "Methods",
|
||||||
|
"toasts": {
|
||||||
|
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
||||||
|
"deleted_all_movies_successfully": "Deleted all movies successfully!",
|
||||||
|
"failed_to_delete_all_movies": "Failed to delete all movies",
|
||||||
|
"deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!",
|
||||||
|
"failed_to_delete_all_tvseries": "Failed to delete all TV-Series",
|
||||||
|
"download_cancelled": "Download cancelled",
|
||||||
|
"could_not_cancel_download": "Could not cancel download",
|
||||||
|
"download_completed": "Download completed",
|
||||||
|
"download_started_for": "Download started for {{item}}",
|
||||||
|
"item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded",
|
||||||
|
"download_stated_for_item": "Download started for {{item}}",
|
||||||
|
"download_failed_for_item": "Download failed for {{item}} - {{error}}",
|
||||||
|
"download_completed_for_item": "Download completed for {{item}}",
|
||||||
|
"queued_item_for_optimization": "Queued {{item}} for optimization",
|
||||||
|
"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": "Failed to start downloading for {{item}}: Unexpected error",
|
||||||
|
"all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully",
|
||||||
|
"an_error_occured_while_deleting_files_and_jobs": "An error occurred while deleting files and jobs",
|
||||||
|
"go_to_downloads": "Go to downloads"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"search_here": "Search here...",
|
||||||
|
"search": "Search...",
|
||||||
|
"x_items": "{{count}} items",
|
||||||
|
"library": "Library",
|
||||||
|
"discover": "Discover",
|
||||||
|
"no_results": "No results",
|
||||||
|
"no_results_found_for": "No results found for",
|
||||||
|
"movies": "Movies",
|
||||||
|
"series": "Series",
|
||||||
|
"episodes": "Episodes",
|
||||||
|
"collections": "Collections",
|
||||||
|
"actors": "Actors",
|
||||||
|
"request_movies": "Request Movies",
|
||||||
|
"request_series": "Request Series",
|
||||||
|
"recently_added": "Recently Added",
|
||||||
|
"recent_requests": "Recent Requests",
|
||||||
|
"plex_watchlist": "Plex Watchlist",
|
||||||
|
"trending": "Trending",
|
||||||
|
"popular_movies": "Popular Movies",
|
||||||
|
"movie_genres": "Movie Genres",
|
||||||
|
"upcoming_movies": "Upcoming Movies",
|
||||||
|
"studios": "Studios",
|
||||||
|
"popular_tv": "Popular TV",
|
||||||
|
"tv_genres": "TV Genres",
|
||||||
|
"upcoming_tv": "Upcoming TV",
|
||||||
|
"networks": "Networks",
|
||||||
|
"tmdb_movie_keyword": "TMDB Movie Keyword",
|
||||||
|
"tmdb_movie_genre": "TMDB Movie Genre",
|
||||||
|
"tmdb_tv_keyword": "TMDB TV Keyword",
|
||||||
|
"tmdb_tv_genre": "TMDB TV Genre",
|
||||||
|
"tmdb_search": "TMDB Search",
|
||||||
|
"tmdb_studio": "TMDB Studio",
|
||||||
|
"tmdb_network": "TMDB Network",
|
||||||
|
"tmdb_movie_streaming_services": "TMDB Movie Streaming Services",
|
||||||
|
"tmdb_tv_streaming_services": "TMDB TV Streaming Services"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"no_items_found": "No items found",
|
||||||
|
"no_results": "No results",
|
||||||
|
"no_libraries_found": "No libraries found",
|
||||||
|
"item_types": {
|
||||||
|
"movies": "movies",
|
||||||
|
"series": "series",
|
||||||
|
"boxsets": "box sets",
|
||||||
|
"items": "items"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"display": "Display",
|
||||||
|
"row": "Row",
|
||||||
|
"list": "List",
|
||||||
|
"image_style": "Image style",
|
||||||
|
"poster": "Poster",
|
||||||
|
"cover": "Cover",
|
||||||
|
"show_titles": "Show titles",
|
||||||
|
"show_stats": "Show stats"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"genres": "Genres",
|
||||||
|
"years": "Years",
|
||||||
|
"sort_by": "Sort By",
|
||||||
|
"sort_order": "Sort Order",
|
||||||
|
"tags": "Tags"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"series": "Series",
|
||||||
|
"movies": "Movies",
|
||||||
|
"episodes": "Episodes",
|
||||||
|
"videos": "Videos",
|
||||||
|
"boxsets": "Boxsets",
|
||||||
|
"playlists": "Playlists"
|
||||||
|
},
|
||||||
|
"custom_links": {
|
||||||
|
"no_links": "No links"
|
||||||
|
},
|
||||||
|
"player": {
|
||||||
|
"error": "Error",
|
||||||
|
"failed_to_get_stream_url": "Failed to get the stream URL",
|
||||||
|
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
||||||
|
"client_error": "Client error",
|
||||||
|
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
|
||||||
|
"message_from_server": "Message from server: {{message}}",
|
||||||
|
"video_has_finished_playing": "Video has finished playing!",
|
||||||
|
"no_video_source": "No video source...",
|
||||||
|
"next_episode": "Next Episode",
|
||||||
|
"refresh_tracks": "Refresh Tracks",
|
||||||
|
"subtitle_tracks": "Subtitle Tracks:",
|
||||||
|
"audio_tracks": "Audio Tracks:",
|
||||||
|
"playback_state": "Playback State:",
|
||||||
|
"no_data_available": "No data available",
|
||||||
|
"index": "Index:"
|
||||||
|
},
|
||||||
|
"item_card": {
|
||||||
|
"next_up": "Next up",
|
||||||
|
"no_items_to_display": "No items to display",
|
||||||
|
"cast_and_crew": "Cast & Crew",
|
||||||
|
"series": "Series",
|
||||||
|
"seasons": "Seasons",
|
||||||
|
"season": "Season",
|
||||||
|
"no_episodes_for_this_season": "No episodes for this season",
|
||||||
|
"overview": "Overview",
|
||||||
|
"more_with": "More with {{name}}",
|
||||||
|
"similar_items": "Similar items",
|
||||||
|
"no_similar_items_found": "No similar items found",
|
||||||
|
"video": "Video",
|
||||||
|
"more_details": "More details",
|
||||||
|
"quality": "Quality",
|
||||||
|
"audio": "Audio",
|
||||||
|
"subtitles": "Subtitle",
|
||||||
|
"show_more": "Show more",
|
||||||
|
"show_less": "Show less",
|
||||||
|
"appeared_in": "Appeared in",
|
||||||
|
"could_not_load_item": "Could not load item",
|
||||||
|
"none": "None",
|
||||||
|
"download": {
|
||||||
|
"download_season": "Download Season",
|
||||||
|
"download_series": "Download Series",
|
||||||
|
"download_episode": "Download Episode",
|
||||||
|
"download_movie": "Download Movie",
|
||||||
|
"download_x_item": "Download {{item_count}} items",
|
||||||
|
"download_button": "Download",
|
||||||
|
"using_optimized_server": "Using optimized server",
|
||||||
|
"using_default_method": "Using default method"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"live_tv": {
|
||||||
|
"next": "Next",
|
||||||
|
"previous": "Previous",
|
||||||
|
"live_tv": "Live TV",
|
||||||
|
"coming_soon": "Coming soon",
|
||||||
|
"on_now": "On now",
|
||||||
|
"shows": "Shows",
|
||||||
|
"movies": "Movies",
|
||||||
|
"sports": "Sports",
|
||||||
|
"for_kids": "For Kids",
|
||||||
|
"news": "News"
|
||||||
|
},
|
||||||
|
"jellyseerr":{
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"yes": "Yes",
|
||||||
|
"whats_wrong": "What's wrong?",
|
||||||
|
"issue_type": "Issue type",
|
||||||
|
"select_an_issue": "Select an issue",
|
||||||
|
"types": "Types",
|
||||||
|
"describe_the_issue": "(optional) Describe the issue...",
|
||||||
|
"submit_button": "Submit",
|
||||||
|
"report_issue_button": "Report issue",
|
||||||
|
"request_button": "Request",
|
||||||
|
"are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?",
|
||||||
|
"failed_to_login": "Failed to login",
|
||||||
|
"cast": "Cast",
|
||||||
|
"details": "Details",
|
||||||
|
"status": "Status",
|
||||||
|
"original_title": "Original Title",
|
||||||
|
"series_type": "Series Type",
|
||||||
|
"release_dates": "Release Dates",
|
||||||
|
"first_air_date": "First Air Date",
|
||||||
|
"next_air_date": "Next Air Date",
|
||||||
|
"revenue": "Revenue",
|
||||||
|
"budget": "Budget",
|
||||||
|
"original_language": "Original Language",
|
||||||
|
"production_country": "Production Country",
|
||||||
|
"studios": "Studios",
|
||||||
|
"network": "Network",
|
||||||
|
"currently_streaming_on": "Currently Streaming on",
|
||||||
|
"advanced": "Advanced",
|
||||||
|
"request_as": "Request As",
|
||||||
|
"tags": "Tags",
|
||||||
|
"quality_profile": "Quality Profile",
|
||||||
|
"root_folder": "Root Folder",
|
||||||
|
"season_x": "Season {{seasons}}",
|
||||||
|
"season_number": "Season {{season_number}}",
|
||||||
|
"number_episodes": "{{episode_number}} Episodes",
|
||||||
|
"born": "Born",
|
||||||
|
"appearances": "Appearances",
|
||||||
|
"toasts": {
|
||||||
|
"jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0",
|
||||||
|
"jellyseerr_test_failed": "Jellyseerr test failed. Please try again.",
|
||||||
|
"failed_to_test_jellyseerr_server_url": "Failed to test jellyseerr server url",
|
||||||
|
"issue_submitted": "Issue submitted!",
|
||||||
|
"requested_item": "Requested {{item}}!",
|
||||||
|
"you_dont_have_permission_to_request": "You don't have permission to request!",
|
||||||
|
"something_went_wrong_requesting_media": "Something went wrong requesting media!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"home": "Home",
|
||||||
|
"search": "Search",
|
||||||
|
"library": "Library",
|
||||||
|
"custom_links": "Custom Links",
|
||||||
|
"favorites": "Favorites"
|
||||||
|
}
|
||||||
|
}
|
||||||
457
translations/fr.json
Normal file
457
translations/fr.json
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
{
|
||||||
|
"login": {
|
||||||
|
"username_required": "Nom d'utilisateur requis",
|
||||||
|
"error_title": "Erreur",
|
||||||
|
"login_title": "Se connecter",
|
||||||
|
"login_to_title": "Se connecter à",
|
||||||
|
"username_placeholder": "Nom d'utilisateur",
|
||||||
|
"password_placeholder": "Mot de passe",
|
||||||
|
"login_button": "Se connecter",
|
||||||
|
"quick_connect": "Connexion Rapide",
|
||||||
|
"enter_code_to_login": "Entrez le code {{code}} pour vous connecter",
|
||||||
|
"failed_to_initiate_quick_connect": "Échec de l'initialisation de Connexion Rapide",
|
||||||
|
"got_it": "D'accord",
|
||||||
|
"connection_failed": "La connection a échouée",
|
||||||
|
"could_not_connect_to_server": "Impossible de se connecter au serveur. Veuillez vérifier l'URL et votre connection réseau.",
|
||||||
|
"an_unexpected_error_occured": "Une erreur inattendue s'est produite",
|
||||||
|
"change_server": "Changer de serveur",
|
||||||
|
"invalid_username_or_password": "Nom d'utilisateur ou mot de passe invalide",
|
||||||
|
"user_does_not_have_permission_to_log_in": "L'utilisateur n'a pas la permission de se connecter",
|
||||||
|
"server_is_taking_too_long_to_respond_try_again_later": "Le serveur met trop de temps à répondre, réessayez plus tard",
|
||||||
|
"server_received_too_many_requests_try_again_later": "Le serveur a reçu trop de demandes, réessayez plus tard",
|
||||||
|
"there_is_a_server_error": "Il y a une erreur de serveur",
|
||||||
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Une erreur inattendue s'est produite. Avez-vous entré la bonne URL?"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"enter_url_to_jellyfin_server": "Entrez l'URL de votre serveur Jellyfin",
|
||||||
|
"server_url_placeholder": "http(s)://votre-serveur.com",
|
||||||
|
"connect_button": "Connexion",
|
||||||
|
"previous_servers": "Serveurs précédents",
|
||||||
|
"clear_button": "Effacer",
|
||||||
|
"search_for_local_servers": "Rechercher des serveurs locaux",
|
||||||
|
"searching": "Recherche...",
|
||||||
|
"servers": "Serveurs"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"no_internet": "Pas d'Internet",
|
||||||
|
"no_items": "Aucun item",
|
||||||
|
"no_internet_message": "Aucun problème, vous pouvez toujours regarder\nle contenu téléchargé.",
|
||||||
|
"go_to_downloads": "Aller aux téléchargements",
|
||||||
|
"oops": "Oups!",
|
||||||
|
"error_message": "Quelque chose s'est mal passé.\nVeuillez vous reconnecter à nouveau.",
|
||||||
|
"continue_watching": "Continuer à regarder",
|
||||||
|
"next_up": "À suivre",
|
||||||
|
"recently_added_in": "Ajoutés récemment dans {{libraryName}}",
|
||||||
|
"suggested_movies": "Films suggérés",
|
||||||
|
"suggested_episodes": "Épisodes suggérés",
|
||||||
|
"intro": {
|
||||||
|
"welcome_to_streamyfin": "Bienvenue sur Streamyfin",
|
||||||
|
"a_free_and_open_source_client_for_jellyfin": "Un client gratuit et open source pour Jellyfin",
|
||||||
|
"features_title": "Fonctionnalités",
|
||||||
|
"features_description": "Streamyfin possède de nombreuses fonctionnalités et s'intègre à un large éventail de logiciels que vous pouvez trouver dans le menu des paramètres, notamment:",
|
||||||
|
"jellyseerr_feature_description": "Connectez-vous à votre instance Jellyseerr et demandez des films directement dans l'application.",
|
||||||
|
"downloads_feature_title": "Téléchargements",
|
||||||
|
"downloads_feature_description": "Téléchargez des films et des émissions de télévision pour les regarder hors ligne. Utilisez la méthode par défaut ou installez le serveur d'optimisation pour télécharger les fichiers en arrière-plan.",
|
||||||
|
"chromecast_feature_description": "Diffusez des films et des émissions de télévision sur vos appareils Chromecast.",
|
||||||
|
"centralised_settings_plugin_title": "Plugin de paramètres centralisés",
|
||||||
|
"centralised_settings_plugin_description": "Configuration des paramètres d'un emplacement centralisé sur votre serveur Jellyfin. Tous les paramètres clients pour tous les utilisateurs seront synchronisés automatiquement.",
|
||||||
|
"done_button": "Fait",
|
||||||
|
"go_to_settings_button": "Allez dans les paramètres",
|
||||||
|
"read_more": "Lisez-en plus"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"settings_title": "Paramètres",
|
||||||
|
"log_out_button": "Déconnexion",
|
||||||
|
"user_info": {
|
||||||
|
"user_info_title": "Informations utilisateur",
|
||||||
|
"user": "Utilisateur",
|
||||||
|
"server": "Serveur",
|
||||||
|
"token": "Jeton",
|
||||||
|
"app_version": "Version de l'application"
|
||||||
|
},
|
||||||
|
"quick_connect": {
|
||||||
|
"quick_connect_title": "Connexion Rapide",
|
||||||
|
"authorize_button": "Autoriser Connexion Rapide",
|
||||||
|
"enter_the_quick_connect_code": "Entrez le code Connexion Rapide...",
|
||||||
|
"success": "Succès",
|
||||||
|
"quick_connect_autorized": "Connexion Rapide autorisé",
|
||||||
|
"error": "Erreur",
|
||||||
|
"invalid_code": "Code invalide",
|
||||||
|
"authorize": "Autoriser"
|
||||||
|
},
|
||||||
|
"media_controls": {
|
||||||
|
"media_controls_title": "Contrôles Média",
|
||||||
|
"forward_skip_length": "Durée de saut en avant",
|
||||||
|
"rewind_length": "Durée de retour arrière",
|
||||||
|
"seconds_unit": "s"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"audio_title": "Audio",
|
||||||
|
"set_audio_track": "Piste audio de l'élément précédent",
|
||||||
|
"audio_language": "Langue audio",
|
||||||
|
"audio_hint": "Choisissez une langue audio par défaut.",
|
||||||
|
"none": "Aucune",
|
||||||
|
"language": "Langage"
|
||||||
|
},
|
||||||
|
"subtitles": {
|
||||||
|
"subtitle_title": "Sous-titres",
|
||||||
|
"subtitle_language": "Langue des sous-titres",
|
||||||
|
"subtitle_mode": "Mode des sous-titres",
|
||||||
|
"set_subtitle_track": "Piste de sous-titres de l'élément précédent",
|
||||||
|
"subtitle_size": "Taille des sous-titres",
|
||||||
|
"subtitle_hint": "Configurez les préférences des sous-titres.",
|
||||||
|
"none": "Aucune",
|
||||||
|
"language": "Langage",
|
||||||
|
"loading": "Chargement",
|
||||||
|
"modes": {
|
||||||
|
"Default": "Par défaut",
|
||||||
|
"Smart": "Intelligent",
|
||||||
|
"Always": "Toujours",
|
||||||
|
"None": "Aucun",
|
||||||
|
"OnlyForced": "Forcés seulement"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"other_title": "Autres",
|
||||||
|
"auto_rotate": "Rotation automatique",
|
||||||
|
"video_orientation": "Orientation vidéo",
|
||||||
|
"orientation": "Orientation",
|
||||||
|
"orientations": {
|
||||||
|
"DEFAULT": "Par défaut",
|
||||||
|
"ALL": "Toutes",
|
||||||
|
"PORTRAIT": "Portrait",
|
||||||
|
"PORTRAIT_UP": "Portrait Haut",
|
||||||
|
"PORTRAIT_DOWN": "Portrait Bas",
|
||||||
|
"LANDSCAPE": "Paysage",
|
||||||
|
"LANDSCAPE_LEFT": "Paysage Gauche",
|
||||||
|
"LANDSCAPE_RIGHT": "Paysage Droite",
|
||||||
|
"OTHER": "Autre",
|
||||||
|
"UNKNOWN": "Inconnu"
|
||||||
|
},
|
||||||
|
"safe_area_in_controls": "Zone de sécurité dans les contrôles",
|
||||||
|
"show_custom_menu_links": "Afficher les liens personnalisés",
|
||||||
|
"hide_libraries": "Cacher des bibliothèques",
|
||||||
|
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez obtenir de la table de bibliothèque et de la page d'accueil des sections.",
|
||||||
|
"disable_haptic_feedback": "Désactiver le retour haptique"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"downloads_title": "Téléchargements",
|
||||||
|
"download_method": "Méthode de téléchargement",
|
||||||
|
"remux_max_download": "Téléchargement max remux",
|
||||||
|
"auto_download": "Téléchargement automatique",
|
||||||
|
"optimized_versions_server": "Serveur de versions optimisées",
|
||||||
|
"save_button": "Enregistrer",
|
||||||
|
"optimized_server": "Serveur optimisé",
|
||||||
|
"optimized": "Optimisé",
|
||||||
|
"default": "Par défaut",
|
||||||
|
"optimized_version_hint": "Entrez l'URL du serveur de versions optimisées. L'URL devrait inclure http ou https et optionnellement le port.",
|
||||||
|
"read_more_about_optimized_server": "Lisez-en plus sur le serveur de versions optimisées.",
|
||||||
|
"url": "URL",
|
||||||
|
"server_url_placeholder": "http(s)://domaine.org:port"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"plugins_title": "Plugiciels",
|
||||||
|
"jellyseerr": {
|
||||||
|
"jellyseerr_warning": "Cette intégration est dans ses débuts. Attendez-vous à ce que des choses changent.",
|
||||||
|
"server_url": "URL du serveur",
|
||||||
|
"server_url_hint": "Exemple: http(s)://votre-domaine.url\n(ajouter le port si nécessaire)",
|
||||||
|
"server_url_placeholder": "URL de Jellyseerr...",
|
||||||
|
"password": "Mot de passe",
|
||||||
|
"password_placeholder": "Entrez le mot de passe pour l'utilisateur Jellyfin {{username}}",
|
||||||
|
"save_button": "Enregistrer",
|
||||||
|
"clear_button": "Effacer",
|
||||||
|
"login_button": "Connexion",
|
||||||
|
"total_media_requests": "Total de demandes de médias",
|
||||||
|
"movie_quota_limit": "Limite de quota de film",
|
||||||
|
"movie_quota_days": "Jours de quota de film",
|
||||||
|
"tv_quota_limit": "Limite de quota TV",
|
||||||
|
"tv_quota_days": "Jours de quota TV",
|
||||||
|
"reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr",
|
||||||
|
"unlimited": "Illimité"
|
||||||
|
},
|
||||||
|
"marlin_search": {
|
||||||
|
"enable_marlin_search": "Activer Marlin Search ",
|
||||||
|
"url": "URL",
|
||||||
|
"server_url_placeholder": "http(s)://domaine.org:port",
|
||||||
|
"marlin_search_hint": "Entrez l'URL du serveur Marlin. L'URL devrait inclure http ou https et optionnellement le port.",
|
||||||
|
"read_more_about_marlin": "Lisez-en plus sur Marlin.",
|
||||||
|
"save_button": "Enregistrer",
|
||||||
|
"toasts": {
|
||||||
|
"saved": "Enregistré"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"storage_title": "Stockage",
|
||||||
|
"app_usage": "App {{usedSpace}}%",
|
||||||
|
"phone_usage": "Téléphone {{availableSpace}}%",
|
||||||
|
"size_used": "{{used}} de {{total}} utilisés",
|
||||||
|
"delete_all_downloaded_files": "Supprimer tous les fichiers téléchargés"
|
||||||
|
},
|
||||||
|
"intro": {
|
||||||
|
"show_intro": "Afficher l'intro",
|
||||||
|
"reset_intro": "Réinitialiser l'intro"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"logs_title": "Journaux",
|
||||||
|
"no_logs_available": "Aucun journal disponible",
|
||||||
|
"delete_all_logs": "Supprimer tous les journaux"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"title": "Langues",
|
||||||
|
"app_language": "Langue de l'application",
|
||||||
|
"app_language_description": "Sélectionnez la langue de l'application",
|
||||||
|
"system": "Système"
|
||||||
|
},
|
||||||
|
"toasts":{
|
||||||
|
"error_deleting_files": "Erreur lors de la suppression des fichiers",
|
||||||
|
"background_downloads_enabled": "Téléchargements en arrière-plan activés",
|
||||||
|
"background_downloads_disabled": "Téléchargements en arrière-plan désactivés",
|
||||||
|
"connected": "Connecté",
|
||||||
|
"could_not_connect": "Impossible de se connecter",
|
||||||
|
"invalid_url": "URL invalide"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"downloads_title": "Téléchargements",
|
||||||
|
"tvseries": "Séries TV",
|
||||||
|
"movies": "Films",
|
||||||
|
"queue": "File d'attente",
|
||||||
|
"queue_hint": "La file d'attente et les téléchargements seront perdus au redémarrage de l'application",
|
||||||
|
"no_items_in_queue": "Aucun item dans la file d'attente",
|
||||||
|
"no_downloaded_items": "Aucun item téléchargé",
|
||||||
|
"delete_all_movies_button": "Supprimer tous les films",
|
||||||
|
"delete_all_tvseries_button": "Supprimer toutes les séries",
|
||||||
|
"delete_all_button": "Supprimer tout",
|
||||||
|
"active_download": "Téléchargement actif",
|
||||||
|
"no_active_downloads": "Aucun téléchargements actifs",
|
||||||
|
"active_downloads": "Téléchargements actifs",
|
||||||
|
"new_app_version_requires_re_download": "La nouvelle version de l'application nécessite un nouveau téléchargement",
|
||||||
|
"new_app_version_requires_re_download_description": "Une nouvelle version de l'application est disponible. Veuillez supprimer tous les téléchargements et redémarrer l'application pour télécharger à nouveau",
|
||||||
|
"back": "Retour",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"something_went_wrong": "Quelque chose s'est mal passé",
|
||||||
|
"could_not_get_stream_url_from_jellyfin": "Impossible d'obtenir l'URL du flux depuis Jellyfin",
|
||||||
|
"eta": "ETA {{eta}}",
|
||||||
|
"methods": "Méthodes",
|
||||||
|
"toasts": {
|
||||||
|
"you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers",
|
||||||
|
"deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès!",
|
||||||
|
"failed_to_delete_all_movies": "Échec de la suppression de tous les films",
|
||||||
|
"deleted_all_tvseries_successfully": "Toutes les séries ont été supprimées avec succès!",
|
||||||
|
"failed_to_delete_all_tvseries": "Échec de la suppression de toutes les séries",
|
||||||
|
"download_cancelled": "Téléchargement annulé",
|
||||||
|
"could_not_cancel_download": "Impossible d'annuler le téléchargement",
|
||||||
|
"download_completed": "Téléchargement terminé",
|
||||||
|
"download_started_for": "Téléchargement démarré pour {{item}}",
|
||||||
|
"item_is_ready_to_be_downloaded": "{{item}} est prêt à être téléchargé",
|
||||||
|
"download_stated_for_item": "Téléchargement démarré pour {{item}}",
|
||||||
|
"download_failed_for_item": "Échec du téléchargement pour {{item}} - {{error}}",
|
||||||
|
"download_completed_for_item": "Téléchargement terminé pour {{item}}",
|
||||||
|
"queued_item_for_optimization": "{{item}} mis en file d'attente pour l'optimisation",
|
||||||
|
"failed_to_start_download_for_item": "Échec du démarrage du téléchargement pour {{item}}: {{message}}",
|
||||||
|
"server_responded_with_status_code": "Le serveur a répondu avec le code de statut {{statusCode}}",
|
||||||
|
"no_response_received_from_server": "Aucune réponse reçue du serveur",
|
||||||
|
"error_setting_up_the_request": "Erreur lors de la configuration de la demande",
|
||||||
|
"failed_to_start_download_for_item_unexpected_error": "Échec du démarrage du téléchargement pour {{item}}: Erreur inattendue",
|
||||||
|
"all_files_folders_and_jobs_deleted_successfully": "Tous les fichiers, dossiers et travaux ont été supprimés avec succès",
|
||||||
|
"an_error_occured_while_deleting_files_and_jobs": "Une erreur s'est produite lors de la suppression des fichiers et des travaux",
|
||||||
|
"go_to_downloads": "Aller aux téléchargements"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"search_here": "Rechercher ici...",
|
||||||
|
"search": "Rechercher...",
|
||||||
|
"x_items": "{{count}} items",
|
||||||
|
"library": "Bibliothèque",
|
||||||
|
"discover": "Découvrir",
|
||||||
|
"no_results": "Aucun résultat",
|
||||||
|
"no_results_found_for": "Aucun résultat trouvé pour",
|
||||||
|
"movies": "Films",
|
||||||
|
"series": "Séries",
|
||||||
|
"episodes": "Épisodes",
|
||||||
|
"collections": "Collections",
|
||||||
|
"actors": "Acteurs",
|
||||||
|
"request_movies": "Demander un film",
|
||||||
|
"request_series": "Demander une série",
|
||||||
|
"recently_added": "Ajoutés récemment",
|
||||||
|
"recent_requests": "Demandes récentes",
|
||||||
|
"plex_watchlist": "Liste de lecture Plex",
|
||||||
|
"trending": "Tendance",
|
||||||
|
"popular_movies": "Films populaires",
|
||||||
|
"movie_genres": "Genres de films",
|
||||||
|
"upcoming_movies": "Films à venir",
|
||||||
|
"studios": "Studios",
|
||||||
|
"popular_tv": "TV populaire",
|
||||||
|
"tv_genres": "Genres TV",
|
||||||
|
"upcoming_tv": "TV à venir",
|
||||||
|
"networks": "Réseaux",
|
||||||
|
"tmdb_movie_keyword": "Mot-clé Films TMDB",
|
||||||
|
"tmdb_movie_genre": "Genre de film TMDB",
|
||||||
|
"tmdb_tv_keyword": "Mot-clé TV TMDB",
|
||||||
|
"tmdb_tv_genre": "Genre TV TMDB",
|
||||||
|
"tmdb_search": "Recherche TMDB",
|
||||||
|
"tmdb_studio": "Studio TMDB",
|
||||||
|
"tmdb_network": "Réseau TMDB",
|
||||||
|
"tmdb_movie_streaming_services": "Services de streaming de films TMDB",
|
||||||
|
"tmdb_tv_streaming_services": "Services de streaming TV TMDB"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"no_items_found": "Aucun item trouvé",
|
||||||
|
"no_results": "Aucun résultat",
|
||||||
|
"no_libraries_found": "Aucune bibliothèque trouvée",
|
||||||
|
"item_types": {
|
||||||
|
"movies": "films",
|
||||||
|
"series": "séries",
|
||||||
|
"boxsets": "coffrets",
|
||||||
|
"items": "items"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"display": "Affichage",
|
||||||
|
"row": "Rangée",
|
||||||
|
"list": "Liste",
|
||||||
|
"image_style": "Style d'image",
|
||||||
|
"poster": "Affiche",
|
||||||
|
"cover": "Couverture",
|
||||||
|
"show_titles": "Afficher les titres",
|
||||||
|
"show_stats": "Afficher les statistiques"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"genres": "Genres",
|
||||||
|
"years": "Années",
|
||||||
|
"sort_by": "Trier par",
|
||||||
|
"sort_order": "Ordre de tri",
|
||||||
|
"tags": "Tags"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"series": "Séries",
|
||||||
|
"movies": "Films",
|
||||||
|
"episodes": "Épisodes",
|
||||||
|
"videos": "Vidéos",
|
||||||
|
"boxsets": "Coffrets",
|
||||||
|
"playlists": "Listes de lecture"
|
||||||
|
},
|
||||||
|
"custom_links": {
|
||||||
|
"no_links": "Aucun lien"
|
||||||
|
},
|
||||||
|
"player": {
|
||||||
|
"error": "Erreur",
|
||||||
|
"failed_to_get_stream_url": "Échec de l'obtention de l'URL du flux",
|
||||||
|
"an_error_occured_while_playing_the_video": "Une erreur s'est produite lors de la lecture de la vidéo",
|
||||||
|
"client_error": "Erreur client",
|
||||||
|
"could_not_create_stream_for_chromecast": "Impossible de créer un flux pour Chromecast",
|
||||||
|
"message_from_server": "Message du serveur: {{message}}",
|
||||||
|
"video_has_finished_playing": "La vidéo a fini de jouer!",
|
||||||
|
"no_video_source": "Aucune source vidéo...",
|
||||||
|
"next_episode": "Épisode suivant",
|
||||||
|
"refresh_tracks": "Rafraîchir les pistes",
|
||||||
|
"subtitle_tracks": "Pistes de sous-titres:",
|
||||||
|
"audio_tracks": "Pistes audio:",
|
||||||
|
"playback_state": "État de lecture:",
|
||||||
|
"no_data_available": "Aucune donnée disponible",
|
||||||
|
"index": "Index:"
|
||||||
|
},
|
||||||
|
"item_card": {
|
||||||
|
"next_up": "À suivre",
|
||||||
|
"no_items_to_display": "Aucun item à afficher",
|
||||||
|
"cast_and_crew": "Distribution et équipe",
|
||||||
|
"series": "Séries",
|
||||||
|
"seasons": "Saisons",
|
||||||
|
"season": "Saison",
|
||||||
|
"no_episodes_for_this_season": "Aucun épisode pour cette saison",
|
||||||
|
"overview": "Aperçu",
|
||||||
|
"more_with": "Plus avec {{name}}",
|
||||||
|
"similar_items": "Items similaires",
|
||||||
|
"no_similar_items_found": "Aucun item similaire trouvé",
|
||||||
|
"video": "Vidéo",
|
||||||
|
"more_details": "Plus de détails",
|
||||||
|
"quality": "Qualité",
|
||||||
|
"audio": "Audio",
|
||||||
|
"subtitles": "Sous-titres",
|
||||||
|
"show_more": "Afficher plus",
|
||||||
|
"show_less": "Afficher moins",
|
||||||
|
"appeared_in": "Apparu dans",
|
||||||
|
"could_not_load_item": "Impossible de charger l'item",
|
||||||
|
"none": "Aucun",
|
||||||
|
"download": {
|
||||||
|
"download_season": "Télécharger la saison",
|
||||||
|
"download_series": "Télécharger la série",
|
||||||
|
"download_episode": "Télécharger l'épisode",
|
||||||
|
"download_movie": "Télécharger le film",
|
||||||
|
"download_x_item": "Télécharger {{item_count}} items",
|
||||||
|
"download_button": "Télécharger",
|
||||||
|
"using_optimized_server": "Avec le serveur de versions optimisées",
|
||||||
|
"using_default_method": "Avec la méthode par défaut"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"live_tv": {
|
||||||
|
"next": "Suivant",
|
||||||
|
"previous": "Précédent",
|
||||||
|
"live_tv": "TV en direct",
|
||||||
|
"coming_soon": "Bientôt",
|
||||||
|
"on_now": "En ce moment",
|
||||||
|
"shows": "Émissions",
|
||||||
|
"movies": "Films",
|
||||||
|
"sports": "Sports",
|
||||||
|
"for_kids": "Pour enfants",
|
||||||
|
"news": "Actualités"
|
||||||
|
},
|
||||||
|
"jellyseerr":{
|
||||||
|
"confirm": "Confirmer",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"yes": "Oui",
|
||||||
|
"whats_wrong": "Qu'est-ce qui ne va pas?",
|
||||||
|
"issue_type": "Type de problème",
|
||||||
|
"select_an_issue": "Sélectionnez un problème",
|
||||||
|
"types": "Types",
|
||||||
|
"describe_the_issue": "(optionnel) Décrivez le problème...",
|
||||||
|
"submit_button": "Soumettre",
|
||||||
|
"report_issue_button": "Signaler un problème",
|
||||||
|
"request_button": "Demander",
|
||||||
|
"are_you_sure_you_want_to_request_all_seasons": "Êtes-vous sûr de vouloir demander toutes les saisons?",
|
||||||
|
"failed_to_login": "Échec de la connexion",
|
||||||
|
"cast": "Distribution",
|
||||||
|
"details": "Détails",
|
||||||
|
"status": "Statut",
|
||||||
|
"original_title": "Titre original",
|
||||||
|
"series_type": "Type de série",
|
||||||
|
"release_dates": "Dates de sortie",
|
||||||
|
"first_air_date": "Date de première diffusion",
|
||||||
|
"next_air_date": "Date de prochaine diffusion",
|
||||||
|
"revenue": "Revenu",
|
||||||
|
"budget": "Budget",
|
||||||
|
"original_language": "Langue originale",
|
||||||
|
"production_country": "Pays de production",
|
||||||
|
"studios": "Studios",
|
||||||
|
"network": "Réseaux",
|
||||||
|
"currently_streaming_on": "En diffusion continue sur",
|
||||||
|
"advanced": "Avancé",
|
||||||
|
"request_as": "Demander en tant que",
|
||||||
|
"tags": "Tags",
|
||||||
|
"quality_profile": "Profil de qualité",
|
||||||
|
"root_folder": "Dossier racine",
|
||||||
|
"season_x": "Saison {{seasons}}",
|
||||||
|
"season_number": "Saison {{season_number}}",
|
||||||
|
"number_episodes": "{{episode_number}} épisodes",
|
||||||
|
"born": "Né(e) le",
|
||||||
|
"appearances": "Apparitions",
|
||||||
|
"toasts": {
|
||||||
|
"jellyseer_does_not_meet_requirements": "Jellyseer ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.",
|
||||||
|
"jellyseerr_test_failed": "Échec du test de Jellyseerr",
|
||||||
|
"failed_to_test_jellyseerr_server_url": "Échec du test de l'URL du serveur Jellyseerr",
|
||||||
|
"issue_submitted": "Problème soumis!",
|
||||||
|
"requested_item": "{{item}}} demandé!",
|
||||||
|
"you_dont_have_permission_to_request": "Vous n'avez pas la permission de demander {{item}}",
|
||||||
|
"something_went_wrong_requesting_media": "Quelque chose s'est mal passé en demandant le média!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"home": "Accueil",
|
||||||
|
"search": "Recherche",
|
||||||
|
"library": "Bibliothèque",
|
||||||
|
"custom_links": "Liens personnalisés",
|
||||||
|
"favorites": "Favoris"
|
||||||
|
}
|
||||||
|
}
|
||||||
30
translations/sv.json
Normal file
30
translations/sv.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"login": {
|
||||||
|
"username_required": "Användarnamn krävs",
|
||||||
|
"error_title": "Fel",
|
||||||
|
"login_title": "Logga in",
|
||||||
|
"username_placeholder": "Användarnamn",
|
||||||
|
"password_placeholder": "Lösenord",
|
||||||
|
"login_button": "Logga in"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"server_url_placeholder": "Server URL",
|
||||||
|
"connect_button": "Anslut"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"home": "Hem",
|
||||||
|
"no_internet": "Ingen Internet",
|
||||||
|
"no_internet_message": "Ingen fara, du kan fortfarande titta\npå nedladdat innehåll.",
|
||||||
|
"go_to_downloads": "Gå till nedladdningar",
|
||||||
|
"oops": "Hoppsan!",
|
||||||
|
"error_message": "Något gick fel.\nLogga ut och in igen.",
|
||||||
|
"continue_watching": "Fortsätt titta",
|
||||||
|
"next_up": "Nästa upp",
|
||||||
|
"recently_added_in": "Nyligen tillagt i {{libraryName}}"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"home": "Hem",
|
||||||
|
"search": "Sök",
|
||||||
|
"library": "Bibliotek"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,16 +28,16 @@ export const ScreenOrientationEnum: Record<
|
|||||||
ScreenOrientation.OrientationLock,
|
ScreenOrientation.OrientationLock,
|
||||||
string
|
string
|
||||||
> = {
|
> = {
|
||||||
[ScreenOrientation.OrientationLock.DEFAULT]: "Default",
|
[ScreenOrientation.OrientationLock.DEFAULT]: "home.settings.other.orientations.DEFAULT",
|
||||||
[ScreenOrientation.OrientationLock.ALL]: "All",
|
[ScreenOrientation.OrientationLock.ALL]: "home.settings.other.orientations.ALL",
|
||||||
[ScreenOrientation.OrientationLock.PORTRAIT]: "Portrait",
|
[ScreenOrientation.OrientationLock.PORTRAIT]: "home.settings.other.orientations.PORTRAIT",
|
||||||
[ScreenOrientation.OrientationLock.PORTRAIT_UP]: "Portrait Up",
|
[ScreenOrientation.OrientationLock.PORTRAIT_UP]: "home.settings.other.orientations.PORTRAIT_UP",
|
||||||
[ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: "Portrait Down",
|
[ScreenOrientation.OrientationLock.PORTRAIT_DOWN]: "home.settings.other.orientations.PORTRAIT_DOWN",
|
||||||
[ScreenOrientation.OrientationLock.LANDSCAPE]: "Landscape",
|
[ScreenOrientation.OrientationLock.LANDSCAPE]: "home.settings.other.orientations.LANDSCAPE",
|
||||||
[ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: "Landscape Left",
|
[ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]: "home.settings.other.orientations.LANDSCAPE_LEFT",
|
||||||
[ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: "Landscape Right",
|
[ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]: "home.settings.other.orientations.LANDSCAPE_RIGHT",
|
||||||
[ScreenOrientation.OrientationLock.OTHER]: "Other",
|
[ScreenOrientation.OrientationLock.OTHER]: "home.settings.other.orientations.OTHER",
|
||||||
[ScreenOrientation.OrientationLock.UNKNOWN]: "Unknown",
|
[ScreenOrientation.OrientationLock.UNKNOWN]: "home.settings.other.orientations.UNKNOWN",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DownloadOptions: DownloadOption[] = [
|
export const DownloadOptions: DownloadOption[] = [
|
||||||
@@ -107,6 +107,7 @@ export type Settings = {
|
|||||||
forceLandscapeInVideoPlayer?: boolean;
|
forceLandscapeInVideoPlayer?: boolean;
|
||||||
deviceProfile?: "Expo" | "Native" | "Old";
|
deviceProfile?: "Expo" | "Native" | "Old";
|
||||||
mediaListCollectionIds?: string[];
|
mediaListCollectionIds?: string[];
|
||||||
|
preferedLanguage?: string;
|
||||||
searchEngine: "Marlin" | "Jellyfin";
|
searchEngine: "Marlin" | "Jellyfin";
|
||||||
marlinServerUrl?: string;
|
marlinServerUrl?: string;
|
||||||
openInVLC?: boolean;
|
openInVLC?: boolean;
|
||||||
@@ -153,6 +154,7 @@ const loadSettings = (): Settings => {
|
|||||||
forceLandscapeInVideoPlayer: false,
|
forceLandscapeInVideoPlayer: false,
|
||||||
deviceProfile: "Expo",
|
deviceProfile: "Expo",
|
||||||
mediaListCollectionIds: [],
|
mediaListCollectionIds: [],
|
||||||
|
preferedLanguage: undefined,
|
||||||
searchEngine: "Jellyfin",
|
searchEngine: "Jellyfin",
|
||||||
marlinServerUrl: "",
|
marlinServerUrl: "",
|
||||||
openInVLC: false,
|
openInVLC: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user