mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
wip
This commit is contained in:
5
app.json
5
app.json
@@ -30,7 +30,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"jsEngine": "hermes",
|
"jsEngine": "hermes",
|
||||||
"versionCode": 17,
|
"versionCode": 18,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/images/icon.png"
|
"foregroundImage": "./assets/images/icon.png"
|
||||||
},
|
},
|
||||||
@@ -96,7 +96,8 @@
|
|||||||
{
|
{
|
||||||
"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"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { router, Tabs } from "expo-router";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import * as NavigationBar from "expo-navigation-bar";
|
|
||||||
import { TabBarIcon } from "@/components/navigation/TabBarIcon";
|
import { TabBarIcon } from "@/components/navigation/TabBarIcon";
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
import { Platform, TouchableOpacity, View } from "react-native";
|
|
||||||
import { Feather } from "@expo/vector-icons";
|
|
||||||
import { Chromecast } from "@/components/Chromecast";
|
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import { StyleSheet } from "react-native";
|
import * as NavigationBar from "expo-navigation-bar";
|
||||||
|
import { Tabs } from "expo-router";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Platform, StyleSheet } from "react-native";
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS === "android") {
|
if (Platform.OS === "android") {
|
||||||
NavigationBar.setBackgroundColorAsync("#121212");
|
NavigationBar.setBackgroundColorAsync("#121212");
|
||||||
@@ -53,7 +53,7 @@ export default function TabLayout() {
|
|||||||
name="home"
|
name="home"
|
||||||
options={{
|
options={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
title: "Home",
|
title: t("tabs.home"),
|
||||||
tabBarIcon: ({ color, focused }) => (
|
tabBarIcon: ({ color, focused }) => (
|
||||||
<TabBarIcon
|
<TabBarIcon
|
||||||
name={focused ? "home" : "home-outline"}
|
name={focused ? "home" : "home-outline"}
|
||||||
@@ -66,7 +66,7 @@ export default function TabLayout() {
|
|||||||
name="search"
|
name="search"
|
||||||
options={{
|
options={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
title: "Search",
|
title: t("tabs.search"),
|
||||||
tabBarIcon: ({ color, focused }) => (
|
tabBarIcon: ({ color, focused }) => (
|
||||||
<TabBarIcon name={focused ? "search" : "search"} color={color} />
|
<TabBarIcon name={focused ? "search" : "search"} color={color} />
|
||||||
),
|
),
|
||||||
@@ -76,7 +76,7 @@ export default function TabLayout() {
|
|||||||
name="library"
|
name="library"
|
||||||
options={{
|
options={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
title: "Library",
|
title: t("tabs.library"),
|
||||||
tabBarIcon: ({ color, focused }) => (
|
tabBarIcon: ({ color, focused }) => (
|
||||||
<TabBarIcon
|
<TabBarIcon
|
||||||
name={focused ? "apps" : "apps-outline"}
|
name={focused ? "apps" : "apps-outline"}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Chromecast } from "@/components/Chromecast";
|
import { Chromecast } from "@/components/Chromecast";
|
||||||
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 { useTranslation } from "react-i18next";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { TouchableOpacity } from "react-native";
|
import { TouchableOpacity } from "react-native";
|
||||||
|
|
||||||
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
|
||||||
@@ -13,7 +15,7 @@ export default function IndexLayout() {
|
|||||||
options={{
|
options={{
|
||||||
headerShown: true,
|
headerShown: true,
|
||||||
headerLargeTitle: true,
|
headerLargeTitle: true,
|
||||||
headerTitle: "Home",
|
headerTitle: t("home.home"),
|
||||||
headerBlurEffect: "prominent",
|
headerBlurEffect: "prominent",
|
||||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||||
headerShadowVisible: false,
|
headerShadowVisible: false,
|
||||||
|
|||||||
@@ -20,12 +20,15 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { RefreshControl, ScrollView, View } from "react-native";
|
import { RefreshControl, ScrollView, View } from "react-native";
|
||||||
|
|
||||||
export default function index() {
|
export default function index() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { i18n, t } = useTranslation();
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
|
||||||
@@ -216,9 +219,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.noInternet")}</Text>
|
||||||
<Text className="text-center opacity-70">
|
<Text className="text-center opacity-70">
|
||||||
No worries, you can still watch{"\n"}downloaded content.
|
{t("home.noInternetMessage")}
|
||||||
</Text>
|
</Text>
|
||||||
<View className="mt-4">
|
<View className="mt-4">
|
||||||
<Button
|
<Button
|
||||||
@@ -229,7 +232,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.goToDownloads")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -239,10 +242,8 @@ export default function index() {
|
|||||||
if (isError)
|
if (isError)
|
||||||
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.errorMessage")}</Text>
|
||||||
Something went wrong.{"\n"}Please log out and in again.
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -265,14 +266,14 @@ export default function index() {
|
|||||||
<LargeMovieCarousel />
|
<LargeMovieCarousel />
|
||||||
|
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
title="Continue Watching"
|
title={t("home.continueWatching")}
|
||||||
data={data}
|
data={data}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
title="Next Up"
|
title={t("home.nextUp")}
|
||||||
data={nextUpData}
|
data={nextUpData}
|
||||||
loading={isLoadingNextUp}
|
loading={isLoadingNextUp}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
@@ -283,19 +284,19 @@ export default function index() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
title="Recently Added in Movies"
|
title={t("home.recentlyAddedMovies")}
|
||||||
data={recentlyAddedInMovies}
|
data={recentlyAddedInMovies}
|
||||||
loading={isLoadingRecentlyAddedMovies}
|
loading={isLoadingRecentlyAddedMovies}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
title="Recently Added in TV-Shows"
|
title={t("home.recentlyAddedTVShows")}
|
||||||
data={recentlyAddedInTVShows}
|
data={recentlyAddedInTVShows}
|
||||||
loading={isLoadingRecentlyAddedTVShows}
|
loading={isLoadingRecentlyAddedTVShows}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScrollingCollectionList
|
<ScrollingCollectionList
|
||||||
title="Suggestions"
|
title={t("home.suggestions")}
|
||||||
data={suggestions}
|
data={suggestions}
|
||||||
loading={isLoadingSuggestions}
|
loading={isLoadingSuggestions}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import { useKeepAwake } from "expo-keep-awake";
|
|||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||||
|
import { I18nextProvider, useTranslation } from "react-i18next";
|
||||||
|
import i18n from "@/i18n";
|
||||||
|
import { getLocales } from "expo-localization";
|
||||||
|
|
||||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
@@ -42,7 +45,9 @@ export default function RootLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<JotaiProvider>
|
<JotaiProvider>
|
||||||
<Layout />
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<Layout />
|
||||||
|
</I18nextProvider>
|
||||||
</JotaiProvider>
|
</JotaiProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -52,6 +57,8 @@ function Layout() {
|
|||||||
|
|
||||||
useKeepAwake();
|
useKeepAwake();
|
||||||
|
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
const queryClientRef = useRef<QueryClient>(
|
const queryClientRef = useRef<QueryClient>(
|
||||||
new QueryClient({
|
new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -75,6 +82,12 @@ function Layout() {
|
|||||||
);
|
);
|
||||||
}, [settings]);
|
}, [settings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
i18n.changeLanguage(
|
||||||
|
settings?.preferedLanguage || getLocales()[0].languageCode || "en"
|
||||||
|
);
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<QueryClientProvider client={queryClientRef.current}>
|
<QueryClientProvider client={queryClientRef.current}>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Input } from "@/components/common/Input";
|
import { Input } from "@/components/common/Input";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
||||||
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
|
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
@@ -21,6 +23,7 @@ const CredentialsSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
const { setServer, login, removeServer } = useJellyfin();
|
const { setServer, login, removeServer } = useJellyfin();
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
|
|
||||||
@@ -72,7 +75,7 @@ const Login: React.FC = () => {
|
|||||||
<View className="mb-4">
|
<View className="mb-4">
|
||||||
<Text className="text-3xl font-bold mb-2">Streamyfin</Text>
|
<Text className="text-3xl font-bold mb-2">Streamyfin</Text>
|
||||||
<Text className="text-neutral-500 mb-2">
|
<Text className="text-neutral-500 mb-2">
|
||||||
Server: {api.basePath}
|
{t("server.server_label", { serverURL: api.basePath })}
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
color="black"
|
color="black"
|
||||||
@@ -89,17 +92,17 @@ const Login: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Change server
|
{t("server.change_server")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex flex-col space-y-2">
|
<View className="flex flex-col space-y-2">
|
||||||
<Text className="text-2xl font-bold">Log in</Text>
|
<Text className="text-2xl font-bold">{t("login.login")}</Text>
|
||||||
<Text className="text-neutral-500">
|
<Text className="text-neutral-500">
|
||||||
Log in to any user account
|
{t("login.login_subtitle")}
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder={t("login.username_placeholder")}
|
||||||
onChangeText={(text) =>
|
onChangeText={(text) =>
|
||||||
setCredentials({ ...credentials, username: text })
|
setCredentials({ ...credentials, username: text })
|
||||||
}
|
}
|
||||||
@@ -116,7 +119,7 @@ const Login: React.FC = () => {
|
|||||||
|
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
placeholder="Password"
|
placeholder={t("login.password_placeholder")}
|
||||||
onChangeText={(text) =>
|
onChangeText={(text) =>
|
||||||
setCredentials({ ...credentials, password: text })
|
setCredentials({ ...credentials, password: text })
|
||||||
}
|
}
|
||||||
@@ -139,7 +142,7 @@ const Login: React.FC = () => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
className="mt-auto mb-2"
|
className="mt-auto mb-2"
|
||||||
>
|
>
|
||||||
Log in
|
{t("login.login_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
@@ -158,10 +161,10 @@ const Login: React.FC = () => {
|
|||||||
<View className="flex flex-col gap-y-2">
|
<View className="flex flex-col gap-y-2">
|
||||||
<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">
|
||||||
Connect to your Jellyfin server
|
{t("server.connect_to_server")}
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Server URL"
|
placeholder={t("server.server_url_placeholder")}
|
||||||
onChangeText={setServerURL}
|
onChangeText={setServerURL}
|
||||||
value={serverURL}
|
value={serverURL}
|
||||||
keyboardType="url"
|
keyboardType="url"
|
||||||
@@ -170,12 +173,11 @@ const Login: React.FC = () => {
|
|||||||
textContentType="URL"
|
textContentType="URL"
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
/>
|
/>
|
||||||
<Text className="opacity-30">
|
<Text className="opacity-30">{t("server.server_url_hint")}</Text>
|
||||||
Server URL requires http or https
|
<LanguageSwitcher />
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<Button onPress={() => handleConnect(serverURL)} className="mb-2">
|
<Button onPress={() => handleConnect(serverURL)} className="mb-2">
|
||||||
Connect
|
{t("server.connect_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|||||||
36
components/LanguageSwitcher.tsx
Normal file
36
components/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { getLocales } from "expo-localization";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||||
|
|
||||||
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
|
export const LanguageSwitcher: React.FC<Props> = ({ ...props }) => {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
const lngs = ["en", "sv"];
|
||||||
|
|
||||||
|
const [settings, updateSettings] = useSettings();
|
||||||
|
return (
|
||||||
|
<View className="flex flex-row space-x-2" {...props}>
|
||||||
|
{lngs.map((l) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={l}
|
||||||
|
onPress={() => {
|
||||||
|
i18n.changeLanguage(l);
|
||||||
|
updateSettings({ preferedLanguage: l });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className={`uppercase ${
|
||||||
|
i18n.language === l ? "text-blue-500" : "text-gray-400 underline"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{l}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
22
i18n.ts
Normal file
22
i18n.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
|
import en from "./translations/en.json";
|
||||||
|
import sv from "./translations/sv.json";
|
||||||
|
import { getLocales } from "expo-localization";
|
||||||
|
|
||||||
|
i18n.use(initReactI18next).init({
|
||||||
|
compatibilityJSON: "v3",
|
||||||
|
resources: {
|
||||||
|
en: { translation: en },
|
||||||
|
sv: { translation: sv },
|
||||||
|
},
|
||||||
|
|
||||||
|
lng: getLocales()[0].languageCode || "en",
|
||||||
|
fallbackLng: "en",
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
"expo-image": "~1.12.13",
|
"expo-image": "~1.12.13",
|
||||||
"expo-keep-awake": "~13.0.2",
|
"expo-keep-awake": "~13.0.2",
|
||||||
"expo-linking": "~6.3.1",
|
"expo-linking": "~6.3.1",
|
||||||
|
"expo-localization": "~15.0.3",
|
||||||
"expo-navigation-bar": "~3.0.7",
|
"expo-navigation-bar": "~3.0.7",
|
||||||
"expo-router": "~3.5.23",
|
"expo-router": "~3.5.23",
|
||||||
"expo-screen-orientation": "~7.0.5",
|
"expo-screen-orientation": "~7.0.5",
|
||||||
@@ -51,11 +52,13 @@
|
|||||||
"expo-updates": "~0.25.22",
|
"expo-updates": "~0.25.22",
|
||||||
"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",
|
||||||
|
"i18next": "^23.13.0",
|
||||||
"jotai": "^2.9.1",
|
"jotai": "^2.9.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.0.1",
|
||||||
"react-native": "0.74.5",
|
"react-native": "0.74.5",
|
||||||
"react-native-circular-progress": "^1.4.0",
|
"react-native-circular-progress": "^1.4.0",
|
||||||
"react-native-compressor": "^1.8.25",
|
"react-native-compressor": "^1.8.25",
|
||||||
|
|||||||
38
translations/en.json
Normal file
38
translations/en.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"login": {
|
||||||
|
"username_required": "Username is required",
|
||||||
|
"error_title": "Error",
|
||||||
|
"url_error_message": "URL needs to start with http or https.",
|
||||||
|
"login": "Log in",
|
||||||
|
"login_subtitle": "Log in to any user account",
|
||||||
|
"username_placeholder": "Username",
|
||||||
|
"password_placeholder": "Password",
|
||||||
|
"login_button": "Log in"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"server_label": "Server: {{serverURL}}",
|
||||||
|
"change_server": "Change server",
|
||||||
|
"connect_to_server": "Connect to your Jellyfin server",
|
||||||
|
"server_url_placeholder": "Server URL",
|
||||||
|
"server_url_hint": "Server URL requires http or https",
|
||||||
|
"connect_button": "Connect"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"home": "Home",
|
||||||
|
"noInternet": "No Internet",
|
||||||
|
"noInternetMessage": "No worries, you can still watch\ndownloaded content.",
|
||||||
|
"goToDownloads": "Go to downloads",
|
||||||
|
"oops": "Oops!",
|
||||||
|
"errorMessage": "Something went wrong.\nPlease log out and in again.",
|
||||||
|
"continueWatching": "Continue Watching",
|
||||||
|
"nextUp": "Next Up",
|
||||||
|
"recentlyAddedMovies": "Recently Added in Movies",
|
||||||
|
"recentlyAddedTVShows": "Recently Added in TV-Shows",
|
||||||
|
"suggestions": "Suggestions"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"home": "Home",
|
||||||
|
"search": "Search",
|
||||||
|
"library": "Library"
|
||||||
|
}
|
||||||
|
}
|
||||||
38
translations/sv.json
Normal file
38
translations/sv.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"login": {
|
||||||
|
"username_required": "Användarnamn krävs",
|
||||||
|
"error_title": "Fel",
|
||||||
|
"url_error_message": "URL måste börja med http eller https.",
|
||||||
|
"login_title": "Logga in",
|
||||||
|
"login_subtitle": "Logga in på ett användarkonto",
|
||||||
|
"username_placeholder": "Användarnamn",
|
||||||
|
"password_placeholder": "Lösenord",
|
||||||
|
"login_button": "Logga in"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"server_label": "Server: {{serverURL}}",
|
||||||
|
"change_server": "Byt server",
|
||||||
|
"connect_to_server": "Anslut till din Jellyfin-server",
|
||||||
|
"server_url_placeholder": "Server URL",
|
||||||
|
"server_url_hint": "Server URL kräver http eller https",
|
||||||
|
"connect_button": "Anslut"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"home": "Hem",
|
||||||
|
"noInternet": "Ingen Internet",
|
||||||
|
"noInternetMessage": "Ingen fara, du kan fortfarande titta\npå nedladdat innehåll.",
|
||||||
|
"goToDownloads": "Gå till nedladdningar",
|
||||||
|
"oops": "Hoppsan!",
|
||||||
|
"errorMessage": "Något gick fel.\nLogga ut och in igen.",
|
||||||
|
"continueWatching": "Fortsätt titta",
|
||||||
|
"nextUp": "Nästa upp",
|
||||||
|
"recentlyAddedMovies": "Nyligen tillagt i Filmer",
|
||||||
|
"recentlyAddedTVShows": "Nyligen tillagt i TV-Serier",
|
||||||
|
"suggestions": "Förslag"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"home": "Hem",
|
||||||
|
"search": "Sök",
|
||||||
|
"library": "Bibliotek"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { getLocales } from "expo-localization";
|
||||||
|
|
||||||
type Settings = {
|
type Settings = {
|
||||||
autoRotate?: boolean;
|
autoRotate?: boolean;
|
||||||
@@ -10,6 +11,7 @@ type Settings = {
|
|||||||
deviceProfile?: "Expo" | "Native" | "Old";
|
deviceProfile?: "Expo" | "Native" | "Old";
|
||||||
forceDirectPlay?: boolean;
|
forceDirectPlay?: boolean;
|
||||||
mediaListCollectionIds?: string[];
|
mediaListCollectionIds?: string[];
|
||||||
|
preferedLanguage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,6 +35,7 @@ const loadSettings = async (): Promise<Settings> => {
|
|||||||
deviceProfile: "Expo",
|
deviceProfile: "Expo",
|
||||||
forceDirectPlay: false,
|
forceDirectPlay: false,
|
||||||
mediaListCollectionIds: [],
|
mediaListCollectionIds: [],
|
||||||
|
preferedLanguage: getLocales()[0] || "en",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user