This commit is contained in:
Fredrik Burmester
2024-08-18 17:10:31 +02:00
parent 21c1221138
commit 752cb1cdc6
13 changed files with 198 additions and 39 deletions

View File

@@ -30,7 +30,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 17,
"versionCode": 18,
"adaptiveIcon": {
"foregroundImage": "./assets/images/icon.png"
},
@@ -96,7 +96,8 @@
{
"motionPermission": "Allow Streamyfin to access your device motion for landscape video watching."
}
]
],
"expo-localization"
],
"experiments": {
"typedRoutes": true

View File

@@ -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 { 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 { 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() {
const { t } = useTranslation();
useEffect(() => {
if (Platform.OS === "android") {
NavigationBar.setBackgroundColorAsync("#121212");
@@ -53,7 +53,7 @@ export default function TabLayout() {
name="home"
options={{
headerShown: false,
title: "Home",
title: t("tabs.home"),
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
name={focused ? "home" : "home-outline"}
@@ -66,7 +66,7 @@ export default function TabLayout() {
name="search"
options={{
headerShown: false,
title: "Search",
title: t("tabs.search"),
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name={focused ? "search" : "search"} color={color} />
),
@@ -76,7 +76,7 @@ export default function TabLayout() {
name="library"
options={{
headerShown: false,
title: "Library",
title: t("tabs.library"),
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
name={focused ? "apps" : "apps-outline"}

View File

@@ -1,11 +1,13 @@
import { Chromecast } from "@/components/Chromecast";
import { Feather } from "@expo/vector-icons";
import { Stack, useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native";
import { TouchableOpacity } from "react-native";
export default function IndexLayout() {
const router = useRouter();
const { t } = useTranslation();
return (
<Stack>
<Stack.Screen
@@ -13,7 +15,7 @@ export default function IndexLayout() {
options={{
headerShown: true,
headerLargeTitle: true,
headerTitle: "Home",
headerTitle: t("home.home"),
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios" ? true : false,
headerShadowVisible: false,

View File

@@ -20,12 +20,15 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { RefreshControl, ScrollView, View } from "react-native";
export default function index() {
const router = useRouter();
const queryClient = useQueryClient();
const { i18n, t } = useTranslation();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -216,9 +219,9 @@ export default function index() {
if (isConnected === false) {
return (
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
<Text className="text-3xl font-bold mb-2">No Internet</Text>
<Text className="text-3xl font-bold mb-2">{t("home.noInternet")}</Text>
<Text className="text-center opacity-70">
No worries, you can still watch{"\n"}downloaded content.
{t("home.noInternetMessage")}
</Text>
<View className="mt-4">
<Button
@@ -229,7 +232,7 @@ export default function index() {
<Ionicons name="arrow-forward" size={20} color="white" />
}
>
Go to downloads
{t("home.goToDownloads")}
</Button>
</View>
</View>
@@ -239,10 +242,8 @@ export default function index() {
if (isError)
return (
<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-center opacity-70">
Something went wrong.{"\n"}Please log out and in again.
</Text>
<Text className="text-3xl font-bold mb-2">{t("home.oops")}</Text>
<Text className="text-center opacity-70">{t("home.errorMessage")}</Text>
</View>
);
@@ -265,14 +266,14 @@ export default function index() {
<LargeMovieCarousel />
<ScrollingCollectionList
title="Continue Watching"
title={t("home.continueWatching")}
data={data}
loading={isLoading}
orientation="horizontal"
/>
<ScrollingCollectionList
title="Next Up"
title={t("home.nextUp")}
data={nextUpData}
loading={isLoadingNextUp}
orientation="horizontal"
@@ -283,19 +284,19 @@ export default function index() {
))}
<ScrollingCollectionList
title="Recently Added in Movies"
title={t("home.recentlyAddedMovies")}
data={recentlyAddedInMovies}
loading={isLoadingRecentlyAddedMovies}
/>
<ScrollingCollectionList
title="Recently Added in TV-Shows"
title={t("home.recentlyAddedTVShows")}
data={recentlyAddedInTVShows}
loading={isLoadingRecentlyAddedTVShows}
/>
<ScrollingCollectionList
title="Suggestions"
title={t("home.suggestions")}
data={suggestions}
loading={isLoadingSuggestions}
orientation="horizontal"

View File

@@ -17,6 +17,9 @@ import { useKeepAwake } from "expo-keep-awake";
import { useSettings } from "@/utils/atoms/settings";
import { GestureHandlerRootView } from "react-native-gesture-handler";
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.
SplashScreen.preventAutoHideAsync();
@@ -42,7 +45,9 @@ export default function RootLayout() {
return (
<JotaiProvider>
<Layout />
<I18nextProvider i18n={i18n}>
<Layout />
</I18nextProvider>
</JotaiProvider>
);
}
@@ -52,6 +57,8 @@ function Layout() {
useKeepAwake();
const { i18n } = useTranslation();
const queryClientRef = useRef<QueryClient>(
new QueryClient({
defaultOptions: {
@@ -75,6 +82,12 @@ function Layout() {
);
}, [settings]);
useEffect(() => {
i18n.changeLanguage(
settings?.preferedLanguage || getLocales()[0].languageCode || "en"
);
}, [settings]);
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<QueryClientProvider client={queryClientRef.current}>

View File

@@ -1,11 +1,13 @@
import { Button } from "@/components/Button";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
import { Ionicons } from "@expo/vector-icons";
import { AxiosError } from "axios";
import { useAtom } from "jotai";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Alert,
KeyboardAvoidingView,
@@ -21,6 +23,7 @@ const CredentialsSchema = z.object({
});
const Login: React.FC = () => {
const { t, i18n } = useTranslation();
const { setServer, login, removeServer } = useJellyfin();
const [api] = useAtom(apiAtom);
@@ -72,7 +75,7 @@ const Login: React.FC = () => {
<View className="mb-4">
<Text className="text-3xl font-bold mb-2">Streamyfin</Text>
<Text className="text-neutral-500 mb-2">
Server: {api.basePath}
{t("server.server_label", { serverURL: api.basePath })}
</Text>
<Button
color="black"
@@ -89,17 +92,17 @@ const Login: React.FC = () => {
/>
}
>
Change server
{t("server.change_server")}
</Button>
</View>
<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">
Log in to any user account
{t("login.login_subtitle")}
</Text>
<Input
placeholder="Username"
placeholder={t("login.username_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, username: text })
}
@@ -116,7 +119,7 @@ const Login: React.FC = () => {
<Input
className="mb-2"
placeholder="Password"
placeholder={t("login.password_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, password: text })
}
@@ -139,7 +142,7 @@ const Login: React.FC = () => {
loading={loading}
className="mt-auto mb-2"
>
Log in
{t("login.login_button")}
</Button>
</View>
</KeyboardAvoidingView>
@@ -158,10 +161,10 @@ const Login: React.FC = () => {
<View className="flex flex-col gap-y-2">
<Text className="text-3xl font-bold">Streamyfin</Text>
<Text className="text-neutral-500">
Connect to your Jellyfin server
{t("server.connect_to_server")}
</Text>
<Input
placeholder="Server URL"
placeholder={t("server.server_url_placeholder")}
onChangeText={setServerURL}
value={serverURL}
keyboardType="url"
@@ -170,12 +173,11 @@ const Login: React.FC = () => {
textContentType="URL"
maxLength={500}
/>
<Text className="opacity-30">
Server URL requires http or https
</Text>
<Text className="opacity-30">{t("server.server_url_hint")}</Text>
<LanguageSwitcher />
</View>
<Button onPress={() => handleConnect(serverURL)} className="mb-2">
Connect
{t("server.connect_button")}
</Button>
</View>
</KeyboardAvoidingView>

BIN
bun.lockb

Binary file not shown.

View 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
View 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;

View File

@@ -41,6 +41,7 @@
"expo-image": "~1.12.13",
"expo-keep-awake": "~13.0.2",
"expo-linking": "~6.3.1",
"expo-localization": "~15.0.3",
"expo-navigation-bar": "~3.0.7",
"expo-router": "~3.5.23",
"expo-screen-orientation": "~7.0.5",
@@ -51,11 +52,13 @@
"expo-updates": "~0.25.22",
"expo-web-browser": "~13.0.3",
"ffmpeg-kit-react-native": "^6.0.2",
"i18next": "^23.13.0",
"jotai": "^2.9.1",
"lodash": "^4.17.21",
"nativewind": "^2.0.11",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "^15.0.1",
"react-native": "0.74.5",
"react-native-circular-progress": "^1.4.0",
"react-native-compressor": "^1.8.25",

38
translations/en.json Normal file
View 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
View 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"
}
}

View File

@@ -1,6 +1,7 @@
import { atom, useAtom } from "jotai";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useEffect } from "react";
import { getLocales } from "expo-localization";
type Settings = {
autoRotate?: boolean;
@@ -10,6 +11,7 @@ type Settings = {
deviceProfile?: "Expo" | "Native" | "Old";
forceDirectPlay?: boolean;
mediaListCollectionIds?: string[];
preferedLanguage?: string;
};
/**
@@ -33,6 +35,7 @@ const loadSettings = async (): Promise<Settings> => {
deviceProfile: "Expo",
forceDirectPlay: false,
mediaListCollectionIds: [],
preferedLanguage: getLocales()[0] || "en",
};
};