diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 82018c50..a4d04018 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 diff --git a/.github/workflows/build-ios.yaml b/.github/workflows/build-ios.yaml new file mode 100644 index 00000000..d66594ea --- /dev/null +++ b/.github/workflows/build-ios.yaml @@ -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 diff --git a/README.md b/README.md index 342daa48..6b3bcf99 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app.json b/app.json index 81ba2a80..1bef8558 100644 --- a/app.json +++ b/app.json @@ -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" }, diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index d720408c..a689ccb6 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -437,7 +437,6 @@ export default function page() { position: "relative", flexDirection: "column", justifyContent: "center", - opacity: showControls ? (Platform.OS === "android" ? 0.7 : 0.5) : 1, }} > { position: "relative", flexDirection: "column", justifyContent: "center", - opacity: showControls ? 0.5 : 1, }} > {videoSource ? ( diff --git a/app/login.tsx b/app/login.tsx index 0ecd5fb5..d8e4ef33 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -188,16 +188,16 @@ const CredentialsSchema = z.object({ } }; - if (api?.basePath) { - return ( - - - - - + return ( + + + {api?.basePath ? ( + <> + + + <> {serverName ? ( @@ -208,112 +208,108 @@ const CredentialsSchema = z.object({ ) : t("login.login_title")} - {api.basePath} - - setCredentials({ ...credentials, username: text }) - } - value={credentials.username} - autoFocus - secureTextEntry={false} - keyboardType="default" - returnKeyType="done" - autoCapitalize="none" - textContentType="username" - clearButtonMode="while-editing" - maxLength={500} - /> + + {api.basePath} + + + setCredentials({ ...credentials, username: text }) + } + value={credentials.username} + autoFocus + secureTextEntry={false} + keyboardType="default" + returnKeyType="done" + autoCapitalize="none" + textContentType="username" + clearButtonMode="while-editing" + maxLength={500} + /> - - setCredentials({ ...credentials, password: text }) - } - value={credentials.password} - secureTextEntry - keyboardType="default" - returnKeyType="done" - autoCapitalize="none" - textContentType="password" - clearButtonMode="while-editing" - maxLength={500} - /> + + setCredentials({ ...credentials, password: text }) + } + value={credentials.password} + secureTextEntry + keyboardType="default" + returnKeyType="done" + autoCapitalize="none" + textContentType="password" + clearButtonMode="while-editing" + maxLength={500} + /> + + + {error} - {error} + + + + - - - - + + ) : ( + <> + + + + Streamyfin + + {t("server.enter_url_to_jellyfin_server")} + + + + {t("server.server_url_hint")} + + { + handleConnect(s.address); + }} + /> + + + + - - - - ); - } - - return ( - - - - - - Streamyfin - - {t("server.enter_url_to_jellyfin_server")} - - - - {t("server.server_url_hint")} - - { - handleConnect(s.address); - }} - /> - - - - - + + )} ); diff --git a/bun.lockb b/bun.lockb index e0dcadd5..219a11b3 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/settings/Jellyseerr.tsx b/components/settings/Jellyseerr.tsx index 5a33efd5..5c3f6afc 100644 --- a/components/settings/Jellyseerr.tsx +++ b/components/settings/Jellyseerr.tsx @@ -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 { diff --git a/components/settings/QuickConnect.tsx b/components/settings/QuickConnect.tsx index fe7c7df1..5cab8b15 100644 --- a/components/settings/QuickConnect.tsx +++ b/components/settings/QuickConnect.tsx @@ -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 }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); + const [quickConnectCode, setQuickConnectCode] = useState(); + const bottomSheetModalRef = useRef(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) => ( + + ), + [] + ); + + 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 ( bottomSheetModalRef?.current?.present()} title={t("home.settings.quick_connect.authorize_button")} textColor="blue" - > + /> + + + + + + + Quick Connect + + + + + + + + + + + ); }; diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index d7386134..2fd1cba3 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -497,33 +497,6 @@ export const Controls: React.FC = ({ /> ) : ( <> - - - {!mediaSource?.TranscodingUrl ? ( - - ) : ( - - )} - - - { toggleControls(); @@ -532,6 +505,8 @@ export const Controls: React.FC = ({ position: "absolute", width: Dimensions.get("window").width, height: Dimensions.get("window").height, + backgroundColor: "black", + opacity: showControls ? 0.5 : 0, }} > @@ -541,61 +516,82 @@ export const Controls: React.FC = ({ 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 && ( + + + {!mediaSource?.TranscodingUrl ? ( + + ) : ( + + )} + + + + + {item?.Type === "Episode" && !offline && ( + { + switchOnEpisodeMode(); + }} + className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" + > + + + )} + {previousItem && !offline && ( + + + + )} + + {nextItem && !offline && ( + + + + )} + + {mediaSource?.TranscodingUrl && ( + + + + )} { - 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" > - + - )} - {previousItem && !offline && ( - - - - )} - - {nextItem && !offline && ( - - - - )} - - {mediaSource?.TranscodingUrl && ( - - - - )} - { - 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" - > - - + = ({ showControls }) => { ); return ( - + diff --git a/eas.json b/eas.json index af2b7e82..8ce5fc71 100644 --- a/eas.json +++ b/eas.json @@ -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" diff --git a/package.json b/package.json index cef45157..2cfdbc7f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 5145c4b2..57dd94c6 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -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]);