Compare commits

..

19 Commits

Author SHA1 Message Date
Fredrik Burmester
3e433afd4d Update issue templates 2024-08-13 10:13:41 +02:00
Fredrik Burmester
3e1fd5a0ad chore: deps & versions 2024-08-13 10:00:04 +02:00
Fredrik Burmester
0ae8a0a58c fix: change text to remove the word Jellyfin
Because apple denied my app because of it
2024-08-13 09:59:55 +02:00
Fredrik Burmester
34d9392a8b feat: enable screen rotation 2024-08-13 09:59:25 +02:00
Fredrik Burmester
1b463382c5 chore 2024-08-13 09:15:23 +02:00
Fredrik Burmester
4b94bd33ce chore: version 2024-08-13 08:54:16 +02:00
Fredrik Burmester
315d9cbc63 fix: Support for unsecure plaintext authentication (HTTP) logins 2024-08-13 08:54:12 +02:00
Fredrik Burmester
d939f7c9e3 chore 2024-08-13 08:53:59 +02:00
Fredrik Burmester
4d5e544fb0 chore 2024-08-13 08:53:55 +02:00
Fredrik Burmester
5e17f2ac88 fix: check for google play services before chromecast 2024-08-13 08:53:47 +02:00
Fredrik Burmester
74fa279f8d fix: wrong user agent
fixes #14
2024-08-12 22:52:52 +02:00
Fredrik Burmester
4382e585fe fix: typing indicator on android
fixes #15
2024-08-12 22:50:50 +02:00
Fredrik Burmester
a9486c57d2 chore: tipjar 2024-08-12 22:25:04 +02:00
Fredrik Burmester
da9ac3efde fix: download instructions 2024-08-12 20:51:53 +02:00
Fredrik Burmester
7bab4a78bc chore: version 2024-08-12 20:48:09 +02:00
Fredrik Burmester
5f323d5132 chore: version 2024-08-12 20:45:03 +02:00
Fredrik Burmester
18152b9d5b fix: casting should now work 2024-08-12 20:44:57 +02:00
Fredrik Burmester
6b69250ecb fix: splash screen background color 2024-08-12 19:26:39 +02:00
Fredrik Burmester
89a992e7c1 chore: versions 2024-08-12 19:10:08 +02:00
12 changed files with 159 additions and 37 deletions

26
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,26 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone15Pro]
- OS: [e.g. iOS18]
- Version [e.g. 0.3.1]

View File

@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

3
.gitignore vendored
View File

@@ -27,3 +27,6 @@ Streamyfin.app
pc-api-7079014811501811218-719-3b9f15aeccf8.json
credentials.json
development.apk
Streamyfin.apk
Streamyfin.ipa

View File

@@ -21,6 +21,10 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp
Streamyfin includes some exciting experimental features like media downloading and Chromecast support. These are still in development, and we appreciate your patience and feedback as we work to improve them.
### Downloading
Downloading works by using ffmpeg to convert a HLS stream into a video file on the device. This means that you can download and view any file you can stream! The file is converted by Jellyfin on the server in real time as it is downloaded. This means a **bit longer download times** but supports any file that your server can transcode.
## 🛠️ Beta testing (iOS/Android)
## TestFlight
@@ -93,6 +97,11 @@ If you have questions or need support, feel free to reach out:
- GitHub Issues: Report bugs or request features here.
- Email: [fredrik.burmester@gmail.com](mailto:fredrik.burmester@gmail.com)
-
## Support
<a href="https://www.buymeacoffee.com/fredrikbur3" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
## 📝 Credits

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.2.0",
"version": "0.3.2",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
@@ -10,11 +10,12 @@
"splash": {
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
"backgroundColor": "#29164B"
},
"jsEngine": "hermes",
"assetBundlePatterns": ["**/*"],
"ios": {
"requireFullScreen": true,
"infoPlist": {
"NSCameraUsageDescription": "The app needs access to your camera to scan barcodes.",
"NSMicrophoneUsageDescription": "The app needs access to your microphone."
@@ -23,7 +24,9 @@
"bundleIdentifier": "com.fredrikburmester.streamyfin"
},
"android": {
"jsEngine": "jsc",
"jsEngine": "hermes",
"versionCode": 10,
"orientation": "default",
"androidNavigationBar": {
"visible": true,
"barStyle": "dark-content",
@@ -36,8 +39,7 @@
"permissions": [
"android.permission.FOREGROUND_SERVICE",
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
],
"versionCode": 6
]
},
"web": {
"bundler": "metro",
@@ -75,6 +77,7 @@
},
"android": {
"minSdkVersion": 24,
"usesCleartextTraffic": true,
"packagingOptions": {
"jniLibs": {
"useLegacyPackaging": true
@@ -82,6 +85,18 @@
}
}
}
],
[
"expo-screen-orientation",
{
"initialOrientation": "DEFAULT"
}
],
[
"expo-sensors",
{
"motionPermission": "Allow Streamyfin to access your device motion for landscape video watching."
}
]
],
"experiments": {

View File

@@ -26,7 +26,11 @@ import { PlayButton } from "@/components/PlayButton";
import { Bitrate, BitrateSelector } from "@/components/BitrateSelector";
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { useCastDevice } from "react-native-google-cast";
import CastContext, {
PlayServicesState,
useCastDevice,
useRemoteMediaClient,
} from "react-native-google-cast";
import { chromecastProfile } from "@/utils/profiles/chromecast";
import ios12 from "@/utils/profiles/ios12";
import { currentlyPlayingItemAtom } from "@/components/CurrentlyPlayingBar";
@@ -40,6 +44,8 @@ const page: React.FC = () => {
const castDevice = useCastDevice();
const chromecastReady = useMemo(() => !!castDevice?.deviceId, [castDevice]);
const [maxBitrate, setMaxBitrate] = useState<Bitrate>({
key: "Max",
value: undefined,
@@ -110,13 +116,36 @@ const page: React.FC = () => {
});
const [cp, setCp] = useAtom(currentlyPlayingItemAtom);
const client = useRemoteMediaClient();
const onPressPlay = useCallback(() => {
const onPressPlay = useCallback(async () => {
if (!playbackUrl || !item) return;
setCp({
item,
playbackUrl,
});
if (chromecastReady && client) {
await CastContext.getPlayServicesState().then((state) => {
if (state && state !== PlayServicesState.SUCCESS)
CastContext.showPlayServicesErrorDialog(state);
else {
client.loadMedia({
mediaInfo: {
contentUrl: playbackUrl,
contentType: "video/mp4",
metadata: {
type: item.Type === "Episode" ? "tvShow" : "movie",
title: item.Name || "",
subtitle: item.Overview || "",
},
},
startTime: 0,
});
}
});
} else {
setCp({
item,
playbackUrl,
});
}
}, [playbackUrl, item]);
if (l1)
@@ -222,7 +251,11 @@ const page: React.FC = () => {
onChange={(val) => setMaxBitrate(val)}
selected={maxBitrate}
/>
<PlayButton item={item} chromecastReady={false} onPress={onPressPlay} />
<PlayButton
item={item}
chromecastReady={chromecastReady}
onPress={onPressPlay}
/>
</View>
<ScrollView horizontal className="flex px-4 mb-4">
<View className="flex flex-row space-x-2 ">

View File

@@ -2,22 +2,14 @@ import { JellyfinProvider } from "@/providers/JellyfinProvider";
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useFonts } from "expo-font";
import * as NavigationBar from "expo-navigation-bar";
import { Stack, router } from "expo-router";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { Provider as JotaiProvider } from "jotai";
import { useEffect, useRef } from "react";
import { Platform, TouchableOpacity } from "react-native";
import { useEffect, useRef, useState } from "react";
import "react-native-reanimated";
import Feather from "@expo/vector-icons/Feather";
import * as ScreenOrientation from "expo-screen-orientation";
import { StatusBar } from "expo-status-bar";
import { Colors } from "@/constants/Colors";
import { View } from "react-native";
import { Text } from "@/components/common/Text";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Ionicons } from "@expo/vector-icons";
import Video from "react-native-video";
import { CurrentlyPlayingBar } from "@/components/CurrentlyPlayingBar";
// Prevent the splash screen from auto-hiding before asset loading is complete.
@@ -46,14 +38,36 @@ export default function RootLayout() {
}),
);
const insets = useSafeAreaInsets();
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
const [orientation, setOrientation] = useState(
ScreenOrientation.Orientation.PORTRAIT_UP,
);
useEffect(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT);
ScreenOrientation.getOrientationAsync().then((info) => {
setOrientation(info);
});
// subscribe to future changes
const subscription = ScreenOrientation.addOrientationChangeListener(
(evt) => {
setOrientation(evt.orientationInfo.orientation);
},
);
// return a clean up function to unsubscribe from notifications
return () => {
ScreenOrientation.removeOrientationChangeListener(subscription);
};
}, []);
if (!loaded) {
return null;
}

View File

@@ -71,7 +71,7 @@ const Login: React.FC = () => {
>
<View className="flex flex-col px-4 justify-center h-full gap-y-2">
<View>
<Text className="text-3xl font-bold">Jellyfin</Text>
<Text className="text-3xl font-bold">Streamyfin</Text>
<Text className="opacity-50 mb-2">Server: {api.basePath}</Text>
<Button
color="black"
@@ -137,15 +137,13 @@ const Login: React.FC = () => {
>
<View className="flex flex-col px-4 justify-center h-full">
<View className="flex flex-col gap-y-2">
<Text className="text-3xl font-bold">Jellyfin</Text>
<Text className="text-3xl font-bold">Streamyfin</Text>
<Text className="opacity-50">Enter a server adress</Text>
<Input
className="mb-2"
placeholder="http(s)://..."
onChangeText={setServerURL}
value={serverURL}
autoFocus
secureTextEntry={false}
keyboardType="url"
returnKeyType="done"
autoCapitalize="none"

BIN
bun.lockb

Binary file not shown.

View File

@@ -21,10 +21,17 @@
}
},
"production": {
"channel": "0.0.6",
"channel": "0.3.1",
"android": {
"image": "latest"
}
},
"production-apk": {
"channel": "0.3.1",
"android": {
"buildType": "apk",
"image": "latest"
}
}
},
"submit": {

View File

@@ -25,6 +25,7 @@
"@react-navigation/native": "^6.0.2",
"@tanstack/react-query": "^5.51.16",
"@types/uuid": "^10.0.0",
"axios": "^1.7.3",
"expo": "~51.0.26",
"expo-blur": "~13.0.2",
"expo-build-properties": "~0.12.5",
@@ -38,6 +39,8 @@
"expo-linking": "~6.3.1",
"expo-navigation-bar": "~3.0.7",
"expo-router": "~3.5.21",
"expo-screen-orientation": "~7.0.5",
"expo-sensors": "~13.0.9",
"expo-splash-screen": "~0.27.5",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7",

View File

@@ -12,6 +12,7 @@ import React, {
useEffect,
useState,
} from "react";
import { Platform } from "react-native";
import uuid from "react-native-uuid";
interface Server {
@@ -30,7 +31,7 @@ interface JellyfinContextValue {
}
const JellyfinContext = createContext<JellyfinContextValue | undefined>(
undefined
undefined,
);
const getOrSetDeviceId = async () => {
@@ -56,8 +57,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
() =>
new Jellyfin({
clientInfo: { name: "Streamyfin", version: "1.0.0" },
deviceInfo: { name: "iOS", id },
})
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
}),
);
})();
}, []);
@@ -66,9 +67,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const [user, setUser] = useAtom(userAtom);
const discoverServers = async (url: string): Promise<Server[]> => {
const servers = await jellyfin?.discovery.getRecommendedServerCandidates(
url
);
const servers =
await jellyfin?.discovery.getRecommendedServerCandidates(url);
return servers?.map((server) => ({ address: server.address })) || [];
};
@@ -144,7 +144,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const token = await AsyncStorage.getItem("token");
const serverUrl = await AsyncStorage.getItem("serverUrl");
const user = JSON.parse(
(await AsyncStorage.getItem("user")) as string
(await AsyncStorage.getItem("user")) as string,
) as UserDto;
if (serverUrl && token && user.Id && jellyfin) {