Merge branch 'master' into feat/i18n

This commit is contained in:
Simon Caron
2025-01-04 14:57:45 -05:00
15 changed files with 336 additions and 250 deletions

View File

@@ -45,7 +45,7 @@ body:
label: Version
description: What version of Streamyfin are you running?
options:
- 0.23.0
- 0.24.0
- 0.22.0
- 0.21.0
- older

49
.github/workflows/build-ios.yaml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: release
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build:
runs-on: macos-15
name: Build IOS
steps:
- uses: actions/checkout@v2
name: Check out repository
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: |
bun i && bun run submodule-reload
npx expo prebuild
- uses: sparkfabrik/ios-build-action@v2.3.0
with:
upload-to-testflight: false
increment-build-number: false
build-pods: true
pods-path: "ios/Podfile"
configuration: Release
# Change later to app-store if wanted
#export-method: app-store
export-method: ad-hoc
workspace-path: "ios/Streamyfin.xcodeproj/project.xcworkspace/"
project-path: "ios/Streamyfin.xcodeproj"
scheme: Streamyfin
apple-key-id: ${{ secrets.APPLE_KEY_ID }}
apple-key-issuer-id: ${{ secrets.APPLE_KEY_ISSUER_ID }}
apple-key-content: ${{ secrets.APPLE_KEY_CONTENT }}
team-id: ${{ secrets.TEAM_ID }}
team-name: ${{ secrets.TEAM_NAME }}
#match-password: ${{ secrets.MATCH_PASSWORD }}
#match-git-url: ${{ secrets.MATCH_GIT_URL }}
#match-git-basic-authorization: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
#match-build-type: "appstore"
#browserstack-upload: true
#browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }}
#browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
#fastlane-env: stage
ios-app-id: com.stetsed.teststreamyfin
output-path: build-${{ github.sha }}.ipa

View File

@@ -13,7 +13,7 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp
## 🌟 Features
- 🚀 **Skp intro / credits support**
- 🚀 **Skip Intro / Credits Support**
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking.
- 🔊 **Background audio**: Stream music in the background, even when locking the phone.
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.23.0",
"version": "0.24.0",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
@@ -36,7 +36,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 49,
"versionCode": 50,
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive_icon.png"
},

View File

@@ -437,7 +437,6 @@ export default function page() {
position: "relative",
flexDirection: "column",
justifyContent: "center",
opacity: showControls ? (Platform.OS === "android" ? 0.7 : 0.5) : 1,
}}
>
<VlcPlayerView

View File

@@ -387,7 +387,6 @@ const Player = () => {
position: "relative",
flexDirection: "column",
justifyContent: "center",
opacity: showControls ? 0.5 : 1,
}}
>
{videoSource ? (

View File

@@ -188,16 +188,16 @@ const CredentialsSchema = z.object({
}
};
if (api?.basePath) {
return (
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1, height: "100%" }}
>
<View className="flex flex-col h-full relative items-center justify-center">
<View className="px-4 -mt-20 w-full">
<View className="flex flex-col space-y-2">
return (
<SafeAreaView style={{ flex: 1, paddingBottom: 16 }}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
{api?.basePath ? (
<>
<View className="flex flex-col h-full relative items-center justify-center">
<View className="px-4 -mt-20 w-full">
<View className="flex flex-col space-y-2">
<Text className="text-2xl font-bold -mb-2">
<>
{serverName ? (
@@ -208,112 +208,108 @@ const CredentialsSchema = z.object({
) : t("login.login_title")}
</>
</Text>
<Text className="text-xs text-neutral-400">{api.basePath}</Text>
<Input
placeholder={t("login.username_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, username: text })
}
value={credentials.username}
autoFocus
secureTextEntry={false}
keyboardType="default"
returnKeyType="done"
autoCapitalize="none"
textContentType="username"
clearButtonMode="while-editing"
maxLength={500}
/>
<Text className="text-xs text-neutral-400">
{api.basePath}
</Text>
<Input
placeholder={t("login.username_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, username: text })
}
value={credentials.username}
autoFocus
secureTextEntry={false}
keyboardType="default"
returnKeyType="done"
autoCapitalize="none"
textContentType="username"
clearButtonMode="while-editing"
maxLength={500}
/>
<Input
className="mb-2"
placeholder={t("login.password_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, password: text })
}
value={credentials.password}
secureTextEntry
keyboardType="default"
returnKeyType="done"
autoCapitalize="none"
textContentType="password"
clearButtonMode="while-editing"
maxLength={500}
/>
<Input
className="mb-2"
placeholder={t("login.password_placeholder")}
onChangeText={(text) =>
setCredentials({ ...credentials, password: text })
}
value={credentials.password}
secureTextEntry
keyboardType="default"
returnKeyType="done"
autoCapitalize="none"
textContentType="password"
clearButtonMode="while-editing"
maxLength={500}
/>
</View>
<Text className="text-red-600 mb-2">{error}</Text>
</View>
<Text className="text-red-600 mb-2">{error}</Text>
<View className="absolute bottom-0 left-0 w-full px-4 mb-2">
<Button
color="black"
onPress={handleQuickConnect}
className="w-full mb-2"
>
{t("login.use_quick_connect")}
</Button>
<Button onPress={handleLogin} loading={loading}>
{t("login.login_button")}
</Button>
</View>
</View>
<View className="absolute bottom-0 left-0 w-full px-4 mb-2">
<Button
color="black"
onPress={handleQuickConnect}
className="w-full mb-2"
>
{t("login.use_quick_connect")}
</Button>
<Button onPress={handleLogin} loading={loading}>
{t("login.login_button")}
</Button>
</>
) : (
<>
<View className="flex flex-col h-full relative items-center justify-center w-full">
<View className="flex flex-col gap-y-2 px-4 w-full -mt-36">
<Image
style={{
width: 100,
height: 100,
marginLeft: -23,
marginBottom: -20,
}}
source={require("@/assets/images/StreamyFinFinal.png")}
/>
<Text className="text-3xl font-bold">Streamyfin</Text>
<Text className="text-neutral-500">
{t("server.enter_url_to_jellyfin_server")}
</Text>
<Input
placeholder={t("server.server_url_placeholder")}
onChangeText={setServerURL}
value={serverURL}
keyboardType="url"
returnKeyType="done"
autoCapitalize="none"
textContentType="URL"
maxLength={500}
/>
<Text className="text-xs text-neutral-500 ml-4">
{t("server.server_url_hint")}
</Text>
<PreviousServersList
onServerSelect={(s) => {
handleConnect(s.address);
}}
/>
</View>
<View className="mb-2 absolute bottom-0 left-0 w-full px-4">
<Button
loading={loadingServerCheck}
disabled={loadingServerCheck}
onPress={async () => await handleConnect(serverURL)}
className="w-full grow"
>
{t("server.connect_button")}
</Button>
</View>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1, height: "100%" }}
>
<View className="flex flex-col h-full relative items-center justify-center w-full">
<View className="flex flex-col gap-y-2 px-4 w-full -mt-36">
<Image
style={{
width: 100,
height: 100,
marginLeft: -23,
marginBottom: -20,
}}
source={require("@/assets/images/StreamyFinFinal.png")}
/>
<Text className="text-3xl font-bold">Streamyfin</Text>
<Text className="text-neutral-500">
{t("server.enter_url_to_jellyfin_server")}
</Text>
<Input
placeholder={t("server.server_url_placeholder")}
onChangeText={setServerURL}
value={serverURL}
keyboardType="url"
returnKeyType="done"
autoCapitalize="none"
textContentType="URL"
maxLength={500}
/>
<Text className="text-xs text-neutral-500 ml-4">
{t("server.server_url_hint")}
</Text>
<PreviousServersList
onServerSelect={(s) => {
handleConnect(s.address);
}}
/>
</View>
<View className="mb-2 absolute bottom-0 left-0 w-full px-4">
<Button
loading={loadingServerCheck}
disabled={loadingServerCheck}
onPress={async () => await handleConnect(serverURL)}
className="w-full grow"
>
{t("server.connect_button")}
</Button>
</View>
</View>
</>
)}
</KeyboardAvoidingView>
</SafeAreaView>
);

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,18 +1,17 @@
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
import { View } from "react-native";
import { Text } from "../common/Text";
import { useCallback, useRef, useState } from "react";
import { Input } from "../common/Input";
import { ListItem } from "../list/ListItem";
import { Loader } from "../Loader";
import { userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { Button } from "../Button";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useAtom } from "jotai";
import { toast } from "sonner-native";
import { useMutation } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useAtom } from "jotai";
import { useState } from "react";
import { View } from "react-native";
import { toast } from "sonner-native";
import { Button } from "../Button";
import { Input } from "../common/Input";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
export const JellyseerrSettings = () => {
const {

View File

@@ -1,61 +1,116 @@
import { Alert, View, ViewProps } from "react-native";
import { Text } from "../common/Text";
import { ListItem } from "../list/ListItem";
import { Button } from "../Button";
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import { useAtom } from "jotai";
import Constants from "expo-constants";
import Application from "expo-application";
import { ListGroup } from "../list/ListGroup";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
BottomSheetBackdrop,
BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetTextInput,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
import * as Haptics from "expo-haptics";
import { useTranslation } from "react-i18next";
import { useAtom } from "jotai";
import React, { useCallback, useRef, useState } from "react";
import { Alert, View, ViewProps } from "react-native";
import { Button } from "../Button";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
interface Props extends ViewProps {}
export const QuickConnect: React.FC<Props> = ({ ...props }) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const [quickConnectCode, setQuickConnectCode] = useState<string>();
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { t } = useTranslation();
const openQuickConnectAuthCodeInput = () => {
Alert.prompt(
t("home.settings.quick_connect.quick_connect_title"),
t("home.settings.quick_connect.enter_the_quick_connect_code"),
async (text) => {
if (text) {
try {
const res = await getQuickConnectApi(api!).authorizeQuickConnect({
code: text,
userId: user?.Id,
});
if (res.status === 200) {
Haptics.notificationAsync(
Haptics.NotificationFeedbackType.Success
);
Alert.alert(t("home.settings.quick_connect.success"), t("home.settings.quick_connect.quick_connect_autorized"));
} else {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
}
} catch (e) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
}
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[]
);
const authorizeQuickConnect = useCallback(async () => {
if (quickConnectCode) {
try {
const res = await getQuickConnectApi(api!).authorizeQuickConnect({
code: quickConnectCode,
userId: user?.Id,
});
if (res.status === 200) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Alert.alert(t("home.settings.quick_connect.success"), t("home.settings.quick_connect.quick_connect_autorized"));
setQuickConnectCode(undefined);
bottomSheetModalRef?.current?.close();
} else {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
}
} catch (e) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
}
);
};
}
}, [api, user, quickConnectCode]);
return (
<View {...props}>
<ListGroup title={"Quick Connect"}>
<ListItem
onPress={openQuickConnectAuthCodeInput}
onPress={() => bottomSheetModalRef?.current?.present()}
title={t("home.settings.quick_connect.authorize_button")}
textColor="blue"
></ListItem>
/>
</ListGroup>
<BottomSheetModal
ref={bottomSheetModalRef}
enableDynamicSizing
handleIndicatorStyle={{
backgroundColor: "white",
}}
backgroundStyle={{
backgroundColor: "#171717",
}}
backdropComponent={renderBackdrop}
>
<BottomSheetView>
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
<View>
<Text className="font-bold text-2xl text-neutral-100">
Quick Connect
</Text>
</View>
<View className="flex flex-col space-y-2">
<View className="p-4 border border-neutral-800 rounded-xl bg-neutral-900 w-full">
<BottomSheetTextInput
style={{ color: "white" }}
clearButtonMode="always"
placeholder="Enter the quick connect code..."
placeholderTextColor="#9CA3AF"
value={quickConnectCode}
onChangeText={setQuickConnectCode}
/>
</View>
</View>
<Button
className="mt-auto"
onPress={authorizeQuickConnect}
color="purple"
>
Authorize
</Button>
</View>
</BottomSheetView>
</BottomSheetModal>
</View>
);
};

View File

@@ -497,33 +497,6 @@ export const Controls: React.FC<Props> = ({
/>
) : (
<>
<VideoProvider
getAudioTracks={getAudioTracks}
getSubtitleTracks={getSubtitleTracks}
setAudioTrack={setAudioTrack}
setSubtitleTrack={setSubtitleTrack}
setSubtitleURL={setSubtitleURL}
>
<View
style={[
{
position: "absolute",
top: settings?.safeAreaInControlsEnabled ? insets.top : 0,
left: settings?.safeAreaInControlsEnabled ? insets.left : 0,
opacity: showControls ? 1 : 0,
zIndex: 1000,
},
]}
className={`flex flex-row items-center space-x-2 z-10 p-4 `}
>
{!mediaSource?.TranscodingUrl ? (
<DropdownViewDirect showControls={showControls} />
) : (
<DropdownViewTranscoding showControls={showControls} />
)}
</View>
</VideoProvider>
<Pressable
onPressIn={() => {
toggleControls();
@@ -532,6 +505,8 @@ export const Controls: React.FC<Props> = ({
position: "absolute",
width: Dimensions.get("window").width,
height: Dimensions.get("window").height,
backgroundColor: "black",
opacity: showControls ? 0.5 : 0,
}}
></Pressable>
@@ -541,61 +516,82 @@ export const Controls: React.FC<Props> = ({
position: "absolute",
top: settings?.safeAreaInControlsEnabled ? insets.top : 0,
right: settings?.safeAreaInControlsEnabled ? insets.right : 0,
width: settings?.safeAreaInControlsEnabled
? Dimensions.get("window").width - insets.left - insets.right
: Dimensions.get("window").width,
opacity: showControls ? 1 : 0,
},
]}
pointerEvents={showControls ? "auto" : "none"}
className={`flex flex-row items-center space-x-2 z-10 p-4 `}
className={`flex flex-row w-full p-4 `}
>
{item?.Type === "Episode" && !offline && (
<View className="mr-auto">
<VideoProvider
getAudioTracks={getAudioTracks}
getSubtitleTracks={getSubtitleTracks}
setAudioTrack={setAudioTrack}
setSubtitleTrack={setSubtitleTrack}
setSubtitleURL={setSubtitleURL}
>
{!mediaSource?.TranscodingUrl ? (
<DropdownViewDirect showControls={showControls} />
) : (
<DropdownViewTranscoding showControls={showControls} />
)}
</VideoProvider>
</View>
<View className="flex flex-row items-center space-x-2 ">
{item?.Type === "Episode" && !offline && (
<TouchableOpacity
onPress={() => {
switchOnEpisodeMode();
}}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons name="list" size={24} color="white" />
</TouchableOpacity>
)}
{previousItem && !offline && (
<TouchableOpacity
onPress={goToPreviousItem}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons name="play-skip-back" size={24} color="white" />
</TouchableOpacity>
)}
{nextItem && !offline && (
<TouchableOpacity
onPress={goToNextItem}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons name="play-skip-forward" size={24} color="white" />
</TouchableOpacity>
)}
{mediaSource?.TranscodingUrl && (
<TouchableOpacity
onPress={toggleIgnoreSafeAreas}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons
name={ignoreSafeAreas ? "contract-outline" : "expand"}
size={24}
color="white"
/>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={() => {
switchOnEpisodeMode();
onPress={async () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.back();
}}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons name="list" size={24} color="white" />
<Ionicons name="close" size={24} color="white" />
</TouchableOpacity>
)}
{previousItem && !offline && (
<TouchableOpacity
onPress={goToPreviousItem}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons name="play-skip-back" size={24} color="white" />
</TouchableOpacity>
)}
{nextItem && !offline && (
<TouchableOpacity
onPress={goToNextItem}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons name="play-skip-forward" size={24} color="white" />
</TouchableOpacity>
)}
{mediaSource?.TranscodingUrl && (
<TouchableOpacity
onPress={toggleIgnoreSafeAreas}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons
name={ignoreSafeAreas ? "contract-outline" : "expand"}
size={24}
color="white"
/>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={async () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.back();
}}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons name="close" size={24} color="white" />
</TouchableOpacity>
</View>
</View>
<View

View File

@@ -118,14 +118,7 @@ const DropdownView: React.FC<DropdownViewProps> = ({ showControls }) => {
);
return (
<View
style={{
position: "absolute",
zIndex: 1000,
opacity: showControls ? 1 : 0,
}}
className="p-4"
>
<View>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<TouchableOpacity className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2">

View File

@@ -22,13 +22,13 @@
}
},
"production": {
"channel": "0.23.0",
"channel": "0.24.0",
"android": {
"image": "latest"
}
},
"production-apk": {
"channel": "0.23.0",
"channel": "0.24.0",
"android": {
"buildType": "apk",
"image": "latest"

View File

@@ -24,7 +24,7 @@
"@futurejj/react-native-visibility-sensor": "^1.3.5",
"@gorhom/bottom-sheet": "^4.6.4",
"@jellyfin/sdk": "^0.11.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.1",
"@kesha-antonov/react-native-background-downloader": "3.1.2",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/netinfo": "11.3.1",
"@react-native-menu/menu": "^1.1.6",
@@ -79,7 +79,7 @@
"react-native-circular-progress": "^1.4.1",
"react-native-compressor": "^1.9.0",
"react-native-device-info": "^14.0.1",
"react-native-edge-to-edge": "^1.1.1",
"react-native-edge-to-edge": "^1.1.3",
"react-native-gesture-handler": "~2.16.1",
"react-native-get-random-values": "^1.11.0",
"react-native-google-cast": "^4.8.3",

View File

@@ -55,7 +55,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setJellyfin(
() =>
new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.23.0" },
clientInfo: { name: "Streamyfin", version: "0.24.0" },
deviceInfo: {
name: deviceName,
id,
@@ -92,7 +92,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
return {
authorization: `MediaBrowser Client="Streamyfin", Device=${
Platform.OS === "android" ? "Android" : "iOS"
}, DeviceId="${deviceId}", Version="0.23.0"`,
}, DeviceId="${deviceId}", Version="0.24.0"`,
};
}, [deviceId]);